-- NSPD scrape industrialization: schema baseline. -- -- 1. cad_buildings — формальная DDL для таблицы, которая создавалась -- напрямую в проде через 61_import_nspd_batch.py (10590 зданий ЕКБ -- на момент 2026-04-30). IF NOT EXISTS — миграция идемпотентна. -- 2. nspd_scrape_runs — журнал автоматизированных Celery-запусков -- (по образцу kn_scrape_runs из 50_schema_kn_extensions.sql). -- 3. nspd_scrape_log — построчный лог стадий (по образцу kn_scrape_log). CREATE EXTENSION IF NOT EXISTS postgis; -- ── 1. cad_buildings ───────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS cad_buildings ( cad_num TEXT PRIMARY KEY, quarter_cad_num TEXT REFERENCES cad_quarters_geom(cad_number) ON DELETE SET NULL, geom geometry(Geometry, 4326), purpose TEXT, building_name TEXT, readable_address TEXT, area NUMERIC, -- NSPD отдаёт floors как строку — встречаются диапазоны '1-2', '2-3'. -- Не приводим к INT, чтобы не терять данные. Запросы используют regex-CAST. floors TEXT, year_built INT, year_commisioning INT, cost_value NUMERIC, registration_date DATE, status TEXT, ownership_type TEXT, cultural_heritage TEXT, underground_floors TEXT, build_record_area NUMERIC, build_record_type TEXT, common_data_status TEXT, obj_type TEXT, raw_props jsonb, fetched_at timestamptz NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS cad_buildings_geom_gist ON cad_buildings USING GIST (geom); CREATE INDEX IF NOT EXISTS cad_buildings_quarter_idx ON cad_buildings(quarter_cad_num); CREATE INDEX IF NOT EXISTS cad_buildings_purpose_idx ON cad_buildings(purpose); COMMENT ON TABLE cad_buildings IS 'Здания (НСПД thematicSearchId=1) внутри cad_quarters_geom. ' 'Источник cost_value для cross-check с sale_graph.'; -- ── 2. nspd_scrape_runs ────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS nspd_scrape_runs ( run_id BIGSERIAL PRIMARY KEY, started_at timestamptz NOT NULL DEFAULT NOW(), finished_at timestamptz, heartbeat_at timestamptz, region_code INT NOT NULL, pending_count INT, quarters_ok INT NOT NULL DEFAULT 0, quarters_failed INT NOT NULL DEFAULT 0, buildings_ok INT NOT NULL DEFAULT 0, requests_count INT NOT NULL DEFAULT 0, waf_429_count INT NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'running', -- running | done | failed | zombie | skipped error TEXT, triggered_by TEXT NOT NULL DEFAULT 'beat' -- beat | manual | resume ); CREATE INDEX IF NOT EXISTS nspd_runs_status_idx ON nspd_scrape_runs(status, started_at DESC); CREATE INDEX IF NOT EXISTS nspd_runs_region_idx ON nspd_scrape_runs(region_code, started_at DESC); COMMENT ON TABLE nspd_scrape_runs IS 'Журнал NSPD-скрейпов кадастровых кварталов и зданий. Один прогон на регион. ' 'Beat-расписание: 20-е число февраля/мая/августа/ноября (после кв. публикаций rosreestr).'; -- ── 3. nspd_scrape_log ─────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS nspd_scrape_log ( log_id BIGSERIAL PRIMARY KEY, run_id BIGINT REFERENCES nspd_scrape_runs(run_id) ON DELETE CASCADE, ts timestamptz NOT NULL DEFAULT NOW(), level TEXT NOT NULL DEFAULT 'info', -- debug | info | warn | error stage TEXT, -- task_received | discover | quarter_fetch | buildings | commit | done | error cad_number TEXT, message TEXT ); CREATE INDEX IF NOT EXISTS nspd_log_run_idx ON nspd_scrape_log(run_id, ts DESC); CREATE INDEX IF NOT EXISTS nspd_log_level_idx ON nspd_scrape_log(level, ts DESC) WHERE level IN ('warn', 'error'); COMMENT ON TABLE nspd_scrape_log IS 'Построчный лог NSPD-скрейпа: какие cad-кварталы успешны/упали, ' 'WAF-403 ретраи, фазы commit. Используется UI /scrape/nspd для мониторинга.';