diff --git a/data/sql/92_cad_bulk_layers.sql b/data/sql/92_cad_bulk_layers.sql new file mode 100644 index 00000000..cb5a8b83 --- /dev/null +++ b/data/sql/92_cad_bulk_layers.sql @@ -0,0 +1,790 @@ +-- ============================================================================= +-- 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; + -- Rename indexes to drop the _new suffix + 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;