gendesign/data/sql/99_nspd_entities_denorm.sql
lekss361 f704785428 fix(sql): escape apostrophe in migration 99 COMMENT (deploy hotfix)
PR #222 deploy failed на 99_nspd_entities_denorm.sql:
  ERROR: syntax error at or near "а"
  LINE 2: '3-сегментный квартал последнего harvest'а. Используется...

Unescaped apostrophe в COMMENT string closes literal early.
Fix: harvest'а → harvest''а (SQL escape).

Других unescaped апострофов в SQL литералах нет (другие 3 — в SQL комментариях
'--' , безопасно).
2026-05-16 21:02:00 +03:00

171 lines
8.9 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 99_nspd_entities_denorm.sql
-- Context : Denormalized entity tables для NSPD parcel и building features.
-- nspd_quarter_dumps.features_json хранит все feature'ы в JSONB; эти
-- таблицы дают быстрый lookup по cad_num без json-unpacking на query time.
-- Источник: layer="parcels" (NSPD layer 36048) и layer="buildings" (36049)
-- из features_json. Геометрия трансформируется 3857→4326 при UPSERT.
--
-- Dependencies: 88_nspd_quarter_dumps.sql (logical; таблица должна существовать до backfill)
-- Deploy order: после 98_nspd_quarter_dumps_opportunity_flag.sql
-- Idempotent: yes — IF NOT EXISTS на всех объектах
-- Related:
-- - backend/app/services/scrapers/nspd_denorm.py :: denorm_parcel_feature / denorm_building_feature
-- - backend/app/workers/tasks/nspd_denorm_backfill.py :: backfill_all_dumps
-- - backend/app/workers/tasks/nspd_sync.py :: harvest_quarter (inline denorm)
-- Issue: #94 PR3
BEGIN;
-- ── nspd_parcels ──────────────────────────────────────────────────────────────
-- Denormalized parcel features из NSPD quarter dumps (layer="parcels", id 36048).
-- Каждая строка = один земельный участок из любого квартала.
-- PK cad_num: один участок может пересекать несколько кварталов, берём
-- последний harvest (ON CONFLICT DO UPDATE).
CREATE TABLE IF NOT EXISTS nspd_parcels (
-- Кадастровый номер участка (4-сегментный, e.g. '66:41:0204016:10').
-- Первичный ключ — natural, стабильный идентификатор в ЕГРН.
cad_num TEXT PRIMARY KEY,
-- Квартал из которого была взята последняя версия этого участка.
quarter_cad TEXT NOT NULL,
-- ВРИ (вид разрешённого использования) — properties.permitted_use из NSPD.
-- Nullable: НСПД не всегда возвращает ВРИ для старых записей.
permitted_use TEXT,
-- Категория земель (properties.land_category).
-- Nullable: может отсутствовать в ответе.
land_category TEXT,
-- Кадастровая стоимость ЗУ (properties.cost_value), руб.
cost_value NUMERIC,
-- Кадастровая стоимость за м² = cost_value / area_sqm.
-- Вычисляется при UPSERT; NULL если area_sqm = 0 или cost_value = NULL.
cost_per_m2 NUMERIC,
-- Площадь участка (properties.area), м².
area_sqm NUMERIC,
-- Адрес участка (properties.address).
address TEXT,
-- Геометрия участка в WGS84 (трансформируется из EPSG:3857 при UPSERT).
-- Geometry не Polygon т.к. НСПД иногда возвращает MultiPolygon или Point.
geom GEOMETRY(Geometry, 4326),
-- Дата snapshot'а (день harvest'а).
snapshot_date DATE NOT NULL DEFAULT CURRENT_DATE,
-- Timestamp последнего UPSERT.
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE nspd_parcels IS
'Denormalized parcel features из NSPD quarter dumps (layer=parcels, id 36048). '
'PK = cad_num (последний harvest wins). Заполняется Celery task backfill_all_dumps '
'или inline в harvest_quarter. Источник: features_json JSONB колонка nspd_quarter_dumps. '
'Геометрия трансформирована EPSG:3857→4326. (#94 PR3)';
COMMENT ON COLUMN nspd_parcels.cad_num IS
'Кадастровый номер участка (4-сегментный). Natural PK из НСПД.';
COMMENT ON COLUMN nspd_parcels.quarter_cad IS
'3-сегментный квартал последнего harvest''а. Используется для debug/audit.';
COMMENT ON COLUMN nspd_parcels.cost_per_m2 IS
'Кадастровая стоимость за м² (cost_value / area_sqm). '
'NULL если area_sqm = 0 или cost_value отсутствует. Вычисляется в Python при UPSERT.';
-- Индекс для lookup по кварталу (join с nspd_quarter_dumps).
CREATE INDEX IF NOT EXISTS idx_nspd_parcels_quarter_cad
ON nspd_parcels (quarter_cad);
-- GIST индекс на геометрию для spatial queries (соседние участки, ST_DWithin).
CREATE INDEX IF NOT EXISTS idx_nspd_parcels_geom_gist
ON nspd_parcels USING GIST (geom);
-- Partial B-tree на cost_per_m2 для ценовых аналитических запросов.
CREATE INDEX IF NOT EXISTS idx_nspd_parcels_cost_per_m2
ON nspd_parcels (cost_per_m2)
WHERE cost_per_m2 IS NOT NULL;
-- ── nspd_buildings ────────────────────────────────────────────────────────────
-- Denormalized building features из NSPD quarter dumps (layer="buildings", id 36049).
-- Каждая строка = одно здание/сооружение ЕГРН.
CREATE TABLE IF NOT EXISTS nspd_buildings (
-- Кадастровый номер здания (5-сегментный, e.g. '66:41:0204016:10:1').
cad_num TEXT PRIMARY KEY,
-- Квартал последнего harvest'а.
quarter_cad TEXT NOT NULL,
-- Назначение здания (properties.purpose).
-- Ключевые значения: 'Многоквартирный дом', 'Нежилое здание', etc.
purpose TEXT,
-- Количество надземных этажей (properties.floors_above_ground).
floors INTEGER,
-- Количество подземных этажей (properties.floors_underground).
floors_underground INTEGER,
-- Год постройки (properties.year_built).
year_built INTEGER,
-- Кадастровая стоимость здания (properties.cost_value), руб.
cost_value NUMERIC,
-- Площадь по данным ГКН (properties.build_record_area), м².
build_record_area NUMERIC,
-- Адрес здания (properties.address).
address TEXT,
-- Геометрия здания в WGS84 (трансформируется из EPSG:3857 при UPSERT).
geom GEOMETRY(Geometry, 4326),
-- Дата snapshot'а.
snapshot_date DATE NOT NULL DEFAULT CURRENT_DATE,
-- Timestamp последнего UPSERT.
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE nspd_buildings IS
'Denormalized building features из NSPD quarter dumps (layer=buildings, id 36049). '
'PK = cad_num (последний harvest wins). Заполняется Celery task backfill_all_dumps '
'или inline в harvest_quarter. Ключевой use-case: МКД-соседи для P2-scoring. '
'Геометрия трансформирована EPSG:3857→4326. (#94 PR3)';
COMMENT ON COLUMN nspd_buildings.purpose IS
'Назначение здания (properties.purpose). Для МКД = "Многоквартирный дом". '
'Используется в partial index idx_nspd_buildings_mkd для быстрой фильтрации.';
COMMENT ON COLUMN nspd_buildings.floors IS
'Количество надземных этажей (properties.floors_above_ground из НСПД). '
'Nullable: старые ОКС в ЕГРН не всегда имеют этажность.';
-- Индекс для lookup по кварталу.
CREATE INDEX IF NOT EXISTS idx_nspd_buildings_quarter_cad
ON nspd_buildings (quarter_cad);
-- GIST индекс на геометрию (МКД-соседи через ST_DWithin).
CREATE INDEX IF NOT EXISTS idx_nspd_buildings_geom_gist
ON nspd_buildings USING GIST (geom);
-- Partial index для МКД (основной use-case P2-scoring).
-- ILIKE не работает в partial index WHERE clause — используем точный строковый prefixmatch.
-- Здания с purpose IS NULL или другим purpose — в idx_nspd_buildings_geom_gist.
CREATE INDEX IF NOT EXISTS idx_nspd_buildings_mkd
ON nspd_buildings (quarter_cad)
WHERE purpose = 'Многоквартирный дом';
COMMENT ON INDEX idx_nspd_buildings_mkd IS
'Partial index для быстрого lookup МКД по кварталу (P2-scoring neighbors). '
'WHERE purpose = exact строка из НСПД. (#94 PR3)';
COMMIT;