PR #173 fixed cad_buildings_pkey but missed 5 other indexes. Same root cause: PG does NOT auto-rename indexes on table rename → all 6 backing indexes remain on cad_buildings_old_apr26 with original names. Add 5 more `ALTER INDEX IF EXISTS ... RENAME TO ..._old_apr26_*` statements for: geom_gist, quarter_idx, objdoc_idx, complex_idx, purpose_idx. Migration 92 still NOT in _schema_migrations (4× rollback now). Next deploy will re-apply cleanly.
801 lines
32 KiB
PL/PgSQL
801 lines
32 KiB
PL/PgSQL
-- =============================================================================
|
||
-- 92_cad_bulk_layers.sql
|
||
-- Bulk Cadastre Ingest — schema migration for GH issue #168
|
||
-- =============================================================================
|
||
--
|
||
-- Context:
|
||
-- Creates 7 new cadastre layer tables + 1 saga-state jobs table for
|
||
-- bulk NSPD ingest of all 2408 EKB (66:41:*) quarters.
|
||
-- See: research/Bulk_Cadastre_Ingest_Plan_May15.md in Obsidian vault.
|
||
--
|
||
-- Dependencies:
|
||
-- - public.cad_quarters_geom (FK target for cad_quarter_stats)
|
||
-- - public.complexes (FK kept in new cad_buildings.complex_id)
|
||
-- - v_complex_full, v_complex_buildings (views that JOIN cad_buildings — must survive)
|
||
-- - PostGIS extension (geometry types)
|
||
--
|
||
-- Deploy order:
|
||
-- 1. This file (auto-applied by deploy.yml after merge PR #168 part 1)
|
||
-- 2. Backend PR #169 (NSPDClient + bulk_harvest_quarter task)
|
||
-- 3. Frontend PR #170 (admin scrape/cadastre UI)
|
||
--
|
||
-- Migration steps performed here:
|
||
-- A. CREATE TABLE cad_parcels (new, replaces cad_parcels_geom)
|
||
-- B. CREATE TABLE cad_buildings_new (interim name — wider schema)
|
||
-- C. CREATE TABLE cad_constructions
|
||
-- D. CREATE TABLE cad_oncs
|
||
-- E. CREATE TABLE cad_enk
|
||
-- F. CREATE TABLE cad_zouit
|
||
-- G. CREATE TABLE cad_quarter_stats
|
||
-- H. CREATE TABLE cadastre_jobs (saga state, mirrors nspd_geo_jobs pattern)
|
||
-- I. All indexes (GIST + BTREE)
|
||
-- J. Migrate data: cad_parcels_geom -> cad_parcels
|
||
-- K. Backup + replace: cad_buildings -> cad_buildings_old_apr26 -> DROP + rename
|
||
-- L. Restore FK + views
|
||
-- M. Replace cad_parcels_geom TABLE with back-compat VIEW over cad_parcels
|
||
-- N. COMMENT ON TABLE for all 8 tables
|
||
-- O. set_updated_at triggers for all tables with updated_at column
|
||
--
|
||
-- Idempotency: safe to re-run — all DDL uses IF NOT EXISTS / IF EXISTS guards.
|
||
-- Data migrations use INSERT ... ON CONFLICT DO NOTHING.
|
||
-- If cad_buildings_old_apr26 already exists, backup step is skipped.
|
||
--
|
||
-- Rollback: restore from cad_buildings_old_apr26 manually if needed.
|
||
-- =============================================================================
|
||
|
||
BEGIN;
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- A. cad_parcels (replaces cad_parcels_geom — adds full NSPD props)
|
||
-- ---------------------------------------------------------------------------
|
||
CREATE TABLE IF NOT EXISTS cad_parcels (
|
||
cad_num TEXT NOT NULL,
|
||
quarter_cad_number TEXT NOT NULL,
|
||
category_id INT NOT NULL DEFAULT 36368,
|
||
land_record_area NUMERIC,
|
||
specified_area NUMERIC,
|
||
declared_area NUMERIC,
|
||
land_record_category_type TEXT,
|
||
land_record_subtype TEXT,
|
||
land_record_type TEXT,
|
||
permitted_use_established_by_document TEXT,
|
||
cost_value NUMERIC,
|
||
cost_index NUMERIC,
|
||
cost_application_date DATE,
|
||
cost_registration_date DATE,
|
||
cost_determination_date DATE,
|
||
determination_couse TEXT,
|
||
ownership_type TEXT,
|
||
right_type TEXT,
|
||
readable_address TEXT,
|
||
status TEXT,
|
||
previously_posted TEXT,
|
||
registration_date DATE,
|
||
cadastral_districts_code INT,
|
||
subcategory INT,
|
||
geom GEOMETRY(Polygon, 4326),
|
||
raw_props JSONB,
|
||
source TEXT NOT NULL,
|
||
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
CONSTRAINT cad_parcels_pkey PRIMARY KEY (cad_num)
|
||
);
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- B. cad_buildings (wider schema — replaces existing table after data backup)
|
||
-- We create under a temporary name first, then swap after backup.
|
||
-- ---------------------------------------------------------------------------
|
||
CREATE TABLE IF NOT EXISTS cad_buildings_new (
|
||
cad_num TEXT NOT NULL,
|
||
quarter_cad_number TEXT NOT NULL,
|
||
objdoc_id BIGINT,
|
||
registers_id INT DEFAULT 36441,
|
||
purpose TEXT,
|
||
building_name TEXT,
|
||
floors INT,
|
||
underground_floors INT,
|
||
area NUMERIC,
|
||
build_record_area NUMERIC,
|
||
cost_value NUMERIC,
|
||
cost_index NUMERIC,
|
||
year_built INT,
|
||
year_commisioning INT,
|
||
build_record_registration_date DATE,
|
||
registration_date DATE,
|
||
ownership_type TEXT,
|
||
cultural_heritage_object BOOLEAN,
|
||
cultural_heritage_val TEXT,
|
||
readable_address TEXT,
|
||
status TEXT,
|
||
common_data_status TEXT,
|
||
type TEXT,
|
||
build_record_type_value TEXT,
|
||
complex_id BIGINT,
|
||
geom GEOMETRY(Geometry, 4326),
|
||
raw_props JSONB,
|
||
source TEXT NOT NULL,
|
||
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
CONSTRAINT cad_buildings_new_pkey PRIMARY KEY (cad_num)
|
||
);
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- C. cad_constructions (36383 — Сооружения + Сооружения ЕГРН)
|
||
-- ---------------------------------------------------------------------------
|
||
CREATE TABLE IF NOT EXISTS cad_constructions (
|
||
cad_num TEXT NOT NULL,
|
||
quarter_cad_number TEXT NOT NULL,
|
||
category_name TEXT NOT NULL,
|
||
is_egrn BOOLEAN NOT NULL DEFAULT FALSE,
|
||
params_purpose TEXT,
|
||
params_name TEXT,
|
||
params_area NUMERIC,
|
||
params_built_up_area NUMERIC,
|
||
params_year_built INT,
|
||
params_year_commisioning INT,
|
||
params_height NUMERIC,
|
||
params_volume NUMERIC,
|
||
params_extension NUMERIC,
|
||
params_depth NUMERIC,
|
||
params_occurence_depth NUMERIC,
|
||
params_floors INT,
|
||
params_underground_floors INT,
|
||
object_type_value TEXT,
|
||
facility_cad_number TEXT,
|
||
cost_value NUMERIC,
|
||
cost_index NUMERIC,
|
||
ownership_type TEXT,
|
||
right_type TEXT,
|
||
readable_address TEXT,
|
||
address_readable_address TEXT,
|
||
status TEXT,
|
||
common_data_status TEXT,
|
||
registration_date DATE,
|
||
registers_id INT,
|
||
united_cad_number TEXT,
|
||
geom GEOMETRY(Geometry, 4326),
|
||
raw_props JSONB,
|
||
source TEXT NOT NULL,
|
||
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
CONSTRAINT cad_constructions_pkey PRIMARY KEY (cad_num)
|
||
);
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- D. cad_oncs (36384 — Объекты незавершённого строительства)
|
||
-- ---------------------------------------------------------------------------
|
||
CREATE TABLE IF NOT EXISTS cad_oncs (
|
||
cad_num TEXT NOT NULL,
|
||
quarter_cad_number TEXT NOT NULL,
|
||
purpose TEXT,
|
||
area NUMERIC,
|
||
build_record_area NUMERIC,
|
||
cost_value NUMERIC,
|
||
status TEXT,
|
||
readable_address TEXT,
|
||
geom GEOMETRY(Geometry, 4326),
|
||
raw_props JSONB,
|
||
source TEXT NOT NULL,
|
||
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
CONSTRAINT cad_oncs_pkey PRIMARY KEY (cad_num)
|
||
);
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- E. cad_enk (39663 — Единые недвижимые комплексы)
|
||
-- ---------------------------------------------------------------------------
|
||
CREATE TABLE IF NOT EXISTS cad_enk (
|
||
cad_num TEXT NOT NULL,
|
||
quarter_cad_number TEXT NOT NULL,
|
||
name TEXT,
|
||
purpose TEXT,
|
||
type TEXT,
|
||
cost_value NUMERIC,
|
||
facility_cad_number TEXT,
|
||
readable_address TEXT,
|
||
registration_date DATE,
|
||
registers_id INT,
|
||
geom GEOMETRY(Geometry, 4326),
|
||
raw_props JSONB,
|
||
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
CONSTRAINT cad_enk_pkey PRIMARY KEY (cad_num)
|
||
);
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- F. cad_zouit (4 categoryIds: 36940 / 469039 / 469040 / 469042)
|
||
-- Natural key: (reg_numb_border, category_id) — one border can appear in
|
||
-- multiple category subtypes.
|
||
-- ---------------------------------------------------------------------------
|
||
CREATE TABLE IF NOT EXISTS cad_zouit (
|
||
id BIGSERIAL,
|
||
reg_numb_border TEXT NOT NULL,
|
||
category_id INT NOT NULL,
|
||
category_name TEXT,
|
||
type_zone TEXT NOT NULL,
|
||
type_boundary_value TEXT,
|
||
subcategory INT,
|
||
cadastral_district TEXT,
|
||
content_restrict_encumbrances TEXT,
|
||
name_by_doc TEXT,
|
||
legal_act_name TEXT,
|
||
legal_act_date DATE,
|
||
legal_act_number TEXT,
|
||
legal_act_issuer TEXT,
|
||
registration_date DATE,
|
||
old_account_number TEXT,
|
||
geom GEOMETRY(MultiPolygon, 4326),
|
||
raw_props JSONB,
|
||
source TEXT NOT NULL,
|
||
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
CONSTRAINT cad_zouit_pkey PRIMARY KEY (id),
|
||
CONSTRAINT cad_zouit_reg_category_uniq UNIQUE (reg_numb_border, category_id)
|
||
);
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- G. cad_quarter_stats (enrichment 1-to-1 with cad_quarters_geom)
|
||
-- ---------------------------------------------------------------------------
|
||
CREATE TABLE IF NOT EXISTS cad_quarter_stats (
|
||
cad_number TEXT NOT NULL,
|
||
cnt_land INT,
|
||
cnt_oks INT,
|
||
cnt_oks_area INT,
|
||
cnt_oks_geom INT,
|
||
cnt_oks_geom_area INT,
|
||
cnt_oks_geom_linear INT,
|
||
cnt_oks_linear INT,
|
||
cnt_oks_not_geom INT,
|
||
cnt_land_geom INT,
|
||
cnt_land_not_geom INT,
|
||
cnt_land_eou INT,
|
||
cnt_land_geom_eou INT,
|
||
cnt_enk INT,
|
||
cnt_pik INT,
|
||
cost_value_total NUMERIC,
|
||
cost_value_total_eou NUMERIC,
|
||
cost_value_total_geom NUMERIC,
|
||
sum_land_area NUMERIC,
|
||
sum_land_area_eou NUMERIC,
|
||
sum_land_geom_area NUMERIC,
|
||
sum_land_geom_area_eou NUMERIC,
|
||
date_cr DATE,
|
||
real_srid INT,
|
||
raw_props JSONB,
|
||
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
CONSTRAINT cad_quarter_stats_pkey PRIMARY KEY (cad_number),
|
||
CONSTRAINT cad_quarter_stats_quarter_fkey
|
||
FOREIGN KEY (cad_number)
|
||
REFERENCES cad_quarters_geom(cad_number)
|
||
ON DELETE CASCADE
|
||
);
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- H. cadastre_jobs (saga state — mirrors nspd_geo_jobs pattern)
|
||
-- ---------------------------------------------------------------------------
|
||
CREATE TABLE IF NOT EXISTS cadastre_jobs (
|
||
job_id BIGSERIAL NOT NULL,
|
||
name TEXT,
|
||
job_kind TEXT NOT NULL,
|
||
scope TEXT NOT NULL,
|
||
status TEXT NOT NULL DEFAULT 'queued',
|
||
triggered_by TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
started_at TIMESTAMPTZ,
|
||
finished_at TIMESTAMPTZ,
|
||
heartbeat_at TIMESTAMPTZ,
|
||
targets_total INT NOT NULL DEFAULT 0,
|
||
targets_done INT NOT NULL DEFAULT 0,
|
||
targets_failed INT NOT NULL DEFAULT 0,
|
||
targets_skipped INT NOT NULL DEFAULT 0,
|
||
requests_count INT NOT NULL DEFAULT 0,
|
||
waf_blocked_count INT NOT NULL DEFAULT 0,
|
||
rate_ms INT NOT NULL DEFAULT 333,
|
||
error TEXT,
|
||
phase_state JSONB,
|
||
raw_targets JSONB,
|
||
CONSTRAINT cadastre_jobs_pkey PRIMARY KEY (job_id),
|
||
CONSTRAINT cadastre_jobs_status_chk
|
||
CHECK (status IN (
|
||
'queued', 'running', 'done', 'failed', 'zombie', 'cancelled', 'paused'
|
||
))
|
||
);
|
||
|
||
-- ===========================================================================
|
||
-- I. INDEXES
|
||
-- ===========================================================================
|
||
|
||
-- cad_parcels
|
||
CREATE INDEX IF NOT EXISTS cad_parcels_geom_gist
|
||
ON cad_parcels USING GIST (geom);
|
||
CREATE INDEX IF NOT EXISTS cad_parcels_quarter_idx
|
||
ON cad_parcels USING BTREE (quarter_cad_number);
|
||
|
||
-- cad_buildings_new
|
||
CREATE INDEX IF NOT EXISTS cad_buildings_new_geom_gist
|
||
ON cad_buildings_new USING GIST (geom);
|
||
CREATE INDEX IF NOT EXISTS cad_buildings_new_quarter_idx
|
||
ON cad_buildings_new USING BTREE (quarter_cad_number);
|
||
CREATE INDEX IF NOT EXISTS cad_buildings_new_objdoc_idx
|
||
ON cad_buildings_new USING BTREE (objdoc_id)
|
||
WHERE objdoc_id IS NOT NULL;
|
||
CREATE INDEX IF NOT EXISTS cad_buildings_new_complex_idx
|
||
ON cad_buildings_new USING BTREE (complex_id)
|
||
WHERE complex_id IS NOT NULL;
|
||
CREATE INDEX IF NOT EXISTS cad_buildings_new_purpose_idx
|
||
ON cad_buildings_new USING BTREE (purpose);
|
||
|
||
-- cad_constructions
|
||
CREATE INDEX IF NOT EXISTS cad_constructions_geom_gist
|
||
ON cad_constructions USING GIST (geom);
|
||
CREATE INDEX IF NOT EXISTS cad_constructions_quarter_idx
|
||
ON cad_constructions USING BTREE (quarter_cad_number);
|
||
CREATE INDEX IF NOT EXISTS cad_constructions_purpose_idx
|
||
ON cad_constructions USING BTREE (params_purpose)
|
||
WHERE params_purpose IS NOT NULL;
|
||
|
||
-- cad_oncs
|
||
CREATE INDEX IF NOT EXISTS cad_oncs_geom_gist
|
||
ON cad_oncs USING GIST (geom);
|
||
CREATE INDEX IF NOT EXISTS cad_oncs_quarter_idx
|
||
ON cad_oncs USING BTREE (quarter_cad_number);
|
||
|
||
-- cad_enk
|
||
CREATE INDEX IF NOT EXISTS cad_enk_geom_gist
|
||
ON cad_enk USING GIST (geom);
|
||
CREATE INDEX IF NOT EXISTS cad_enk_quarter_idx
|
||
ON cad_enk USING BTREE (quarter_cad_number);
|
||
|
||
-- cad_zouit
|
||
CREATE INDEX IF NOT EXISTS cad_zouit_geom_gist
|
||
ON cad_zouit USING GIST (geom);
|
||
CREATE INDEX IF NOT EXISTS cad_zouit_reg_numb_idx
|
||
ON cad_zouit USING BTREE (reg_numb_border);
|
||
CREATE INDEX IF NOT EXISTS cad_zouit_category_idx
|
||
ON cad_zouit USING BTREE (category_id);
|
||
CREATE INDEX IF NOT EXISTS cad_zouit_subcategory_idx
|
||
ON cad_zouit USING BTREE (subcategory)
|
||
WHERE subcategory IS NOT NULL;
|
||
|
||
-- cadastre_jobs
|
||
CREATE INDEX IF NOT EXISTS cadastre_jobs_status_idx
|
||
ON cadastre_jobs USING BTREE (status, created_at DESC);
|
||
CREATE INDEX IF NOT EXISTS cadastre_jobs_running_idx
|
||
ON cadastre_jobs USING BTREE (heartbeat_at DESC)
|
||
WHERE status = 'running';
|
||
|
||
-- ===========================================================================
|
||
-- J. Migrate existing rows: cad_parcels_geom -> cad_parcels
|
||
-- Source has: cad_num, geom, raw_props, fetched_at
|
||
-- We set quarter_cad_number from cad_num (first 3 parts: XX:YY:ZZZZZZ)
|
||
-- source = 'nspd' (original scraper)
|
||
-- ===========================================================================
|
||
INSERT INTO cad_parcels (
|
||
cad_num,
|
||
quarter_cad_number,
|
||
geom,
|
||
raw_props,
|
||
source,
|
||
fetched_at,
|
||
updated_at
|
||
)
|
||
SELECT
|
||
p.cad_num,
|
||
-- Derive quarter from cad_num "66:41:0101001:123" -> "66:41:0101001"
|
||
regexp_replace(p.cad_num, '^(\d+:\d+:\d+):.*$', '\1'),
|
||
p.geom,
|
||
p.raw_props,
|
||
'nspd',
|
||
p.fetched_at,
|
||
p.fetched_at
|
||
FROM cad_parcels_geom p
|
||
ON CONFLICT (cad_num) DO NOTHING;
|
||
|
||
-- ===========================================================================
|
||
-- K. Backup existing cad_buildings + migrate data into cad_buildings_new
|
||
-- then drop old and rename new.
|
||
--
|
||
-- Old schema columns we map to new:
|
||
-- quarter_cad_num -> quarter_cad_number
|
||
-- floors TEXT -> floors INT (NULLIF on non-numeric)
|
||
-- underground_floors TEXT -> underground_floors INT
|
||
-- cultural_heritage TEXT -> cultural_heritage_object BOOLEAN (NOT NULL = FALSE → NULL),
|
||
-- cultural_heritage_val TEXT
|
||
-- build_record_type -> build_record_type_value
|
||
-- obj_type -> type
|
||
-- (no source in old table, default 'nspd')
|
||
-- ===========================================================================
|
||
|
||
-- K.1 Backup: rename old table (only if backup doesn't already exist)
|
||
DO $$
|
||
BEGIN
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM information_schema.tables
|
||
WHERE table_schema = 'public'
|
||
AND table_name = 'cad_buildings_old_apr26'
|
||
) THEN
|
||
ALTER TABLE cad_buildings RENAME TO cad_buildings_old_apr26;
|
||
END IF;
|
||
END $$;
|
||
|
||
-- K.2 Migrate data into cad_buildings_new
|
||
-- Old cad_buildings (now _old_apr26) -> new wider schema.
|
||
-- complex_id FK preserved as-is.
|
||
INSERT INTO cad_buildings_new (
|
||
cad_num,
|
||
quarter_cad_number,
|
||
purpose,
|
||
building_name,
|
||
floors,
|
||
underground_floors,
|
||
area,
|
||
build_record_area,
|
||
cost_value,
|
||
year_built,
|
||
year_commisioning,
|
||
registration_date,
|
||
ownership_type,
|
||
cultural_heritage_object,
|
||
cultural_heritage_val,
|
||
readable_address,
|
||
status,
|
||
common_data_status,
|
||
type,
|
||
build_record_type_value,
|
||
complex_id,
|
||
geom,
|
||
raw_props,
|
||
source,
|
||
fetched_at,
|
||
updated_at
|
||
)
|
||
SELECT
|
||
o.cad_num,
|
||
COALESCE(o.quarter_cad_num,
|
||
regexp_replace(o.cad_num, '^(\d+:\d+:\d+):.*$', '\1')),
|
||
o.purpose,
|
||
o.building_name,
|
||
-- floors was TEXT in old schema; cast safely
|
||
CASE
|
||
WHEN o.floors ~ '^\d+$' THEN o.floors::INT
|
||
ELSE NULL
|
||
END,
|
||
-- underground_floors was TEXT
|
||
CASE
|
||
WHEN o.underground_floors ~ '^\d+$' THEN o.underground_floors::INT
|
||
ELSE NULL
|
||
END,
|
||
o.area,
|
||
o.build_record_area,
|
||
o.cost_value,
|
||
o.year_built,
|
||
o.year_commisioning,
|
||
o.registration_date,
|
||
o.ownership_type,
|
||
-- cultural_heritage TEXT: treat non-NULL / non-empty string as TRUE
|
||
CASE
|
||
WHEN o.cultural_heritage IS NOT NULL
|
||
AND o.cultural_heritage <> '' THEN TRUE
|
||
ELSE NULL
|
||
END,
|
||
o.cultural_heritage,
|
||
o.readable_address,
|
||
o.status,
|
||
o.common_data_status,
|
||
o.obj_type,
|
||
o.build_record_type,
|
||
o.complex_id,
|
||
o.geom,
|
||
o.raw_props,
|
||
'nspd',
|
||
o.fetched_at,
|
||
o.fetched_at
|
||
FROM cad_buildings_old_apr26 o
|
||
ON CONFLICT (cad_num) DO NOTHING;
|
||
|
||
-- K.3 Rename cad_buildings_new -> cad_buildings
|
||
-- (only if the rename hasn't happened already)
|
||
DO $$
|
||
BEGIN
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM information_schema.tables
|
||
WHERE table_schema = 'public' AND table_name = 'cad_buildings'
|
||
) THEN
|
||
ALTER TABLE cad_buildings_new RENAME TO cad_buildings;
|
||
-- First, retire ALL legacy index names that are still attached to
|
||
-- cad_buildings_old_apr26 — PG does NOT auto-rename indexes on
|
||
-- table rename, so pre-K.1 backup left all 6 names occupied.
|
||
-- Without these renames, the _new → unsuffixed renames below collide
|
||
-- → 'relation already exists' → tx rollback.
|
||
ALTER INDEX IF EXISTS cad_buildings_pkey RENAME TO cad_buildings_old_apr26_pkey;
|
||
ALTER INDEX IF EXISTS cad_buildings_geom_gist RENAME TO cad_buildings_old_apr26_geom_gist;
|
||
ALTER INDEX IF EXISTS cad_buildings_quarter_idx RENAME TO cad_buildings_old_apr26_quarter_idx;
|
||
ALTER INDEX IF EXISTS cad_buildings_objdoc_idx RENAME TO cad_buildings_old_apr26_objdoc_idx;
|
||
ALTER INDEX IF EXISTS cad_buildings_complex_idx RENAME TO cad_buildings_old_apr26_complex_idx;
|
||
ALTER INDEX IF EXISTS cad_buildings_purpose_idx RENAME TO cad_buildings_old_apr26_purpose_idx;
|
||
-- Now rename _new → unsuffixed names safely
|
||
ALTER INDEX IF EXISTS cad_buildings_new_pkey RENAME TO cad_buildings_pkey;
|
||
-- Note: ALTER INDEX RENAME on a PK index also renames the constraint
|
||
-- automatically in PG 16. No separate RENAME CONSTRAINT needed.
|
||
ALTER INDEX IF EXISTS cad_buildings_new_geom_gist RENAME TO cad_buildings_geom_gist;
|
||
ALTER INDEX IF EXISTS cad_buildings_new_quarter_idx RENAME TO cad_buildings_quarter_idx;
|
||
ALTER INDEX IF EXISTS cad_buildings_new_objdoc_idx RENAME TO cad_buildings_objdoc_idx;
|
||
ALTER INDEX IF EXISTS cad_buildings_new_complex_idx RENAME TO cad_buildings_complex_idx;
|
||
ALTER INDEX IF EXISTS cad_buildings_new_purpose_idx RENAME TO cad_buildings_purpose_idx;
|
||
END IF;
|
||
END $$;
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- L.1 Re-add FK: cad_buildings.complex_id -> complexes.id
|
||
-- (was dropped implicitly when old table was renamed)
|
||
-- Guard: only add if FK doesn't already exist.
|
||
-- ---------------------------------------------------------------------------
|
||
DO $$
|
||
BEGIN
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM information_schema.table_constraints
|
||
WHERE table_schema = 'public'
|
||
AND table_name = 'cad_buildings'
|
||
AND constraint_name = 'cad_buildings_complex_id_fkey'
|
||
AND constraint_type = 'FOREIGN KEY'
|
||
) THEN
|
||
ALTER TABLE cad_buildings
|
||
ADD CONSTRAINT cad_buildings_complex_id_fkey
|
||
FOREIGN KEY (complex_id)
|
||
REFERENCES complexes(id)
|
||
ON DELETE SET NULL;
|
||
END IF;
|
||
END $$;
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- L.2 Restore views that JOIN cad_buildings.
|
||
-- Both views only reference: cad_num, complex_id, canonical_name.
|
||
-- These columns exist in new cad_buildings — views will still be valid
|
||
-- after the rename. However, we drop + recreate explicitly to ensure
|
||
-- pg_depend is attached to the renamed table (not the old OID).
|
||
-- ---------------------------------------------------------------------------
|
||
DROP VIEW IF EXISTS v_complex_buildings;
|
||
CREATE OR REPLACE VIEW v_complex_buildings AS
|
||
SELECT
|
||
c.id AS complex_id,
|
||
c.canonical_name,
|
||
array_agg(DISTINCT b.cad_num ORDER BY b.cad_num) AS cad_building_nums,
|
||
COUNT(b.cad_num) AS buildings_count
|
||
FROM complexes c
|
||
LEFT JOIN cad_buildings b ON b.complex_id = c.id
|
||
GROUP BY c.id, c.canonical_name;
|
||
|
||
DROP VIEW IF EXISTS v_complex_full;
|
||
CREATE OR REPLACE VIEW v_complex_full AS
|
||
SELECT
|
||
c.id AS complex_id,
|
||
c.canonical_name,
|
||
c.developer_name,
|
||
c.region_id,
|
||
c.district_name,
|
||
c.obj_class,
|
||
c.address,
|
||
c.latitude,
|
||
c.longitude,
|
||
(c.geom IS NOT NULL) AS has_geom,
|
||
c.flat_count,
|
||
c.square_living,
|
||
c.site_status,
|
||
c.cad_quarter,
|
||
(
|
||
SELECT array_agg(b.cad_num ORDER BY b.cad_num)
|
||
FROM cad_buildings b
|
||
WHERE b.complex_id = c.id
|
||
) AS cad_building_nums,
|
||
(
|
||
SELECT COUNT(*)
|
||
FROM cad_buildings b
|
||
WHERE b.complex_id = c.id
|
||
) AS cad_buildings_n,
|
||
(
|
||
SELECT array_agg(DISTINCT cs.source ORDER BY cs.source)
|
||
FROM complex_sources cs
|
||
WHERE cs.complex_id = c.id
|
||
) AS sources,
|
||
(
|
||
SELECT COUNT(*)
|
||
FROM complex_sources cs
|
||
WHERE cs.complex_id = c.id
|
||
) AS source_links_n,
|
||
(
|
||
SELECT COUNT(DISTINCT cs.source)
|
||
FROM complex_sources cs
|
||
WHERE cs.complex_id = c.id
|
||
) AS source_kinds_n,
|
||
(
|
||
SELECT array_agg(cs.source_external_id ORDER BY cs.source_external_id)
|
||
FROM complex_sources cs
|
||
WHERE cs.complex_id = c.id
|
||
AND cs.source = 'domrf_kn'
|
||
) AS domrf_obj_ids,
|
||
(
|
||
SELECT cs.source_id
|
||
FROM complex_sources cs
|
||
WHERE cs.complex_id = c.id
|
||
AND cs.source = 'objective'
|
||
LIMIT 1
|
||
) AS objective_project_name,
|
||
(
|
||
SELECT COUNT(*)
|
||
FROM domrf_kn_flats f
|
||
WHERE f.obj_id IN (
|
||
SELECT cs.source_external_id
|
||
FROM complex_sources cs
|
||
WHERE cs.complex_id = c.id
|
||
AND cs.source = 'domrf_kn'
|
||
AND cs.source_external_id IS NOT NULL
|
||
)
|
||
) AS kn_flats_n,
|
||
(
|
||
SELECT COUNT(*)
|
||
FROM objective_lots ol
|
||
WHERE ol.complex_id = c.id
|
||
) AS objective_lots_n,
|
||
c.primary_source,
|
||
c.created_at,
|
||
c.updated_at
|
||
FROM complexes c;
|
||
|
||
-- ===========================================================================
|
||
-- M. Replace cad_parcels_geom TABLE with a back-compat VIEW over cad_parcels.
|
||
-- Original table had exactly: cad_num, geom, raw_props, fetched_at.
|
||
-- The view preserves the same 4-column signature so that any legacy callers
|
||
-- (e.g. site-finder analyze fallback) continue to work without code changes.
|
||
-- Bulk PR2/5 will switch callers to cad_parcels directly, after which this
|
||
-- view can be dropped.
|
||
-- ===========================================================================
|
||
DO $$
|
||
BEGIN
|
||
-- Drop the old table only if cad_parcels (migration target) already exists.
|
||
-- Guard prevents accidental drop if J INSERT was skipped on re-run.
|
||
IF EXISTS (
|
||
SELECT 1 FROM information_schema.tables
|
||
WHERE table_schema = 'public' AND table_name = 'cad_parcels'
|
||
) AND EXISTS (
|
||
SELECT 1 FROM information_schema.tables
|
||
WHERE table_schema = 'public'
|
||
AND table_name = 'cad_parcels_geom'
|
||
AND table_type = 'BASE TABLE'
|
||
) THEN
|
||
DROP TABLE cad_parcels_geom;
|
||
END IF;
|
||
END $$;
|
||
|
||
-- Create back-compat VIEW (idempotent via CREATE OR REPLACE).
|
||
-- Matches original column order and types exactly.
|
||
CREATE OR REPLACE VIEW cad_parcels_geom AS
|
||
SELECT
|
||
cad_num,
|
||
geom,
|
||
raw_props,
|
||
fetched_at
|
||
FROM cad_parcels;
|
||
|
||
COMMENT ON VIEW cad_parcels_geom IS
|
||
'Back-compat shim: preserves original 4-column signature (cad_num, geom, '
|
||
'raw_props, fetched_at) for legacy site-finder analyze fallback. '
|
||
'Bulk PR2/5 will migrate callers to cad_parcels directly; drop this view then.';
|
||
|
||
-- ===========================================================================
|
||
-- N. COMMENT ON TABLE for all 8 tables
|
||
-- ===========================================================================
|
||
COMMENT ON TABLE cad_parcels IS
|
||
'Земельные участки ЕГРН из NSPD (categoryId 36368, EKB кадастровый округ 66:41:*). '
|
||
'Широкая схема с полными атрибутами NSPD + геометрия. Заменяет cad_parcels_geom.';
|
||
|
||
COMMENT ON TABLE cad_buildings IS
|
||
'Здания ЕГРН из NSPD (categoryId 36369). Широкая схема: objdoc_id для '
|
||
'tab-group-data lookup, complex_id FK на complexes, cultural_heritage_object BOOLEAN.';
|
||
|
||
COMMENT ON TABLE cad_constructions IS
|
||
'Сооружения из NSPD (categoryId 36383). Объединяет Сооружения и Сооружения ЕГРН '
|
||
'(is_egrn=TRUE). Полный набор технических параметров (высота, объём, протяжённость).';
|
||
|
||
COMMENT ON TABLE cad_oncs IS
|
||
'Объекты незавершённого строительства из NSPD (categoryId 36384, EKB 66:41:*).';
|
||
|
||
COMMENT ON TABLE cad_enk IS
|
||
'Единые недвижимые комплексы из NSPD (categoryId 39663, EKB 66:41:*).';
|
||
|
||
COMMENT ON TABLE cad_zouit IS
|
||
'Зоны с особыми условиями использования территории из NSPD '
|
||
'(categoryIds 36940 / 469039 / 469040 / 469042, все в одной таблице). '
|
||
'Натуральный ключ: (reg_numb_border, category_id) — один border может входить '
|
||
'в несколько категорий.';
|
||
|
||
COMMENT ON TABLE cad_quarter_stats IS
|
||
'Агрегированная статистика кадастрового квартала (1-to-1 с cad_quarters_geom). '
|
||
'Заполняется bulk-harvest worker''ом: счётчики объектов, суммы площадей, кадастровая '
|
||
'стоимость, real_srid ответа NSPD.';
|
||
|
||
COMMENT ON TABLE cadastre_jobs IS
|
||
'Resumable saga state для bulk harvest runs (один job = один кадастровый квартал '
|
||
'или batch). Зеркалит паттерн nspd_geo_jobs. heartbeat_at обновляется worker''ом '
|
||
'каждые N секунд; zombie-детектор переводит зависшие jobs в статус zombie.';
|
||
|
||
-- ===========================================================================
|
||
-- O. set_updated_at triggers for all tables with updated_at column.
|
||
-- set_updated_at() function already exists in this DB (verified).
|
||
-- CREATE OR REPLACE TRIGGER is not available in PG 16 — use DO guard.
|
||
-- ===========================================================================
|
||
|
||
DO $$ BEGIN
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM pg_trigger WHERE tgname = 'cad_parcels_set_updated_at'
|
||
) THEN
|
||
CREATE TRIGGER cad_parcels_set_updated_at
|
||
BEFORE UPDATE ON cad_parcels
|
||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||
END IF;
|
||
END $$;
|
||
|
||
DO $$ BEGIN
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM pg_trigger WHERE tgname = 'cad_buildings_set_updated_at'
|
||
) THEN
|
||
CREATE TRIGGER cad_buildings_set_updated_at
|
||
BEFORE UPDATE ON cad_buildings
|
||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||
END IF;
|
||
END $$;
|
||
|
||
DO $$ BEGIN
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM pg_trigger WHERE tgname = 'cad_constructions_set_updated_at'
|
||
) THEN
|
||
CREATE TRIGGER cad_constructions_set_updated_at
|
||
BEFORE UPDATE ON cad_constructions
|
||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||
END IF;
|
||
END $$;
|
||
|
||
DO $$ BEGIN
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM pg_trigger WHERE tgname = 'cad_oncs_set_updated_at'
|
||
) THEN
|
||
CREATE TRIGGER cad_oncs_set_updated_at
|
||
BEFORE UPDATE ON cad_oncs
|
||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||
END IF;
|
||
END $$;
|
||
|
||
DO $$ BEGIN
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM pg_trigger WHERE tgname = 'cad_enk_set_updated_at'
|
||
) THEN
|
||
CREATE TRIGGER cad_enk_set_updated_at
|
||
BEFORE UPDATE ON cad_enk
|
||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||
END IF;
|
||
END $$;
|
||
|
||
DO $$ BEGIN
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM pg_trigger WHERE tgname = 'cad_zouit_set_updated_at'
|
||
) THEN
|
||
CREATE TRIGGER cad_zouit_set_updated_at
|
||
BEFORE UPDATE ON cad_zouit
|
||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||
END IF;
|
||
END $$;
|
||
|
||
DO $$ BEGIN
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM pg_trigger WHERE tgname = 'cad_quarter_stats_set_updated_at'
|
||
) THEN
|
||
CREATE TRIGGER cad_quarter_stats_set_updated_at
|
||
BEFORE UPDATE ON cad_quarter_stats
|
||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||
END IF;
|
||
END $$;
|
||
|
||
-- cadastre_jobs intentionally excluded — heartbeat_at is the liveness mechanism,
|
||
-- not updated_at (which the table does not have).
|
||
|
||
COMMIT;
|