gendesign/data/sql/80_complexes_backfill_nulls.sql
lekss361 cbe1253461 feat(db): backfill complexes nulls + add job_settings (migrations 80, 81)
80_complexes_backfill_nulls.sql:
- cad_quarter: +722 rows via ST_Contains spatial join on cad_quarters_geom
- developer_id: +247 rows via fuzzy name match on domrf_developers
- obj_class: +2 rows via objective_lots.class mode
- cad_building_num TEXT dropped (100% NULL; relation lives in
  cad_buildings.complex_id)
- v_complex_full rebuilt: cad_building_nums TEXT[] replaces dead column
- v_complex_buildings created: complex_id → cad_building_nums[], buildings_count

81_job_settings.sql:
- New table job_settings (job_type PK, enabled, queue_name, cron_schedule,
  rate_ms, max_retries, max_concurrency, extra_config JSONB)
- Seeded 4 rows: scrape_kn (queue=scrape_kn, '15 4 * * mon'), nspd_geo
  (queue=geo, rate_ms=600), objective_etl, objective_sync (cron from prod
  objective_sync_config)
- objective_sync_config marked DEPRECATED via COMMENT — kept for backward
  compat until backend refactor

Both already applied on prod via postgres MCP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:30:44 +03:00

223 lines
9.7 KiB
PL/PgSQL
Raw Permalink 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.

-- 80_complexes_backfill_nulls.sql
-- Quick-win backfill для public.complexes через spatial joins + lookups.
-- Закрывает NULL'ы без новых scrapers.
--
-- Что делает:
-- A. cad_quarter — spatial join ST_Contains(cad_quarters_geom.geom, complexes.geom)
-- B. cad_building_num — колонка 100% NULL, никто не пишет в неё.
-- Выбрана Опция A: колонка дропается, relation живёт через
-- cad_buildings.complex_id (FK 1:N, уже существует).
-- Создаётся VIEW v_complex_buildings.
-- v_complex_full пересоздаётся без cad_building_num + с
-- cad_building_nums TEXT[] из агрегации по cad_buildings.
-- C. district_name — spatial join ST_Contains(ekb_districts.geom, complexes.geom)
-- (только EKB-покрытие, ~9 районов)
-- D. developer_id — fuzzy match lower(trim(developer_name)) с domrf_developers.
-- Ambiguous (>1 match по имени) — пропускаем, берём только
-- уникальные совпадения.
-- E. obj_class — источник данных domrf_kn_objects.obj_class = NULL для
-- всех 4548 строк; objective_lots.class даёт только 2 complexes.
-- Шаг выполняется (закрывает эти 2), но большого эффекта нет.
-- F. wall_type — domrf_kn_objects.wall_type = NULL для всех строк;
-- objective_lots не содержит wall_type.
-- Источника данных нет — шаг пропускается.
--
-- Dependencies:
-- - public.complexes (таблица)
-- - public.cad_quarters_geom (GIST индекс cad_quarters_geom_gist)
-- - public.ekb_districts (GIST индекс ekb_districts_geom_idx)
-- - public.domrf_developers (btree на developer_id PK)
-- - public.objective_lots (FK complex_id, поле class)
-- - public.cad_buildings (FK complex_id)
-- - public.v_complex_full (пересоздаётся — DROP CASCADE + CREATE)
--
-- Apply order: после 78_drop_use_rosreestr2coord.sql
--
-- Идемпотентность:
-- - UPDATE ... WHERE column IS NULL — повторный запуск не перезапишет NOT NULL.
-- - DROP TABLE IF EXISTS / CREATE OR REPLACE VIEW — безопасны.
-- - DROP COLUMN IF EXISTS — безопасен.
BEGIN;
-- ============================================================
-- A. cad_quarter — spatial join
-- ============================================================
-- Spatial indexes используются (EXPLAIN: Index Scan on cad_quarters_geom).
-- LIMIT 1 защищает от boundary случаев когда точка лежит на двух кварталах.
UPDATE complexes c
SET cad_quarter = (
SELECT q.cad_number
FROM cad_quarters_geom q
WHERE ST_Contains(q.geom, c.geom)
LIMIT 1
)
WHERE c.cad_quarter IS NULL AND c.geom IS NOT NULL;
-- ============================================================
-- B. cad_building_num — Опция A: дропаем колонку, relation через cad_buildings.complex_id
-- ============================================================
-- Шаг B.1: пересоздать v_complex_full без cad_building_num.
-- Вместо неё добавляем cad_building_nums TEXT[] (агрегация из cad_buildings).
-- DROP CASCADE нужен потому что PostgreSQL не позволяет CREATE OR REPLACE VIEW
-- если изменяется список/порядок колонок (cad_building_num убираем, cad_building_nums добавляем).
DROP VIEW IF EXISTS v_complex_full CASCADE;
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,
-- cad_building_nums заменяет cad_building_num: агрегат из cad_buildings.cad_num
(
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;
-- Шаг B.2: дропаем колонку cad_building_num (100% NULL, индекс дропается автоматически)
ALTER TABLE complexes DROP COLUMN IF EXISTS cad_building_num;
-- Шаг B.3: VIEW v_complex_buildings — удобный агрегат 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;
-- ============================================================
-- C. district_name — spatial join (EKB coverage only)
-- ============================================================
-- EXPLAIN: Index Scan on ekb_districts_geom_idx — GIST index используется.
-- Закрывает только complexes внутри 9 районов EKB (geom IS NOT NULL).
-- Complexes без geom или за пределами EKB остаются NULL.
UPDATE complexes c
SET district_name = (
SELECT d.district_name
FROM ekb_districts d
WHERE d.geom IS NOT NULL
AND ST_Contains(d.geom, c.geom)
LIMIT 1
)
WHERE c.district_name IS NULL AND c.geom IS NOT NULL;
-- ============================================================
-- D. developer_id — fuzzy name match с domrf_developers
-- ============================================================
-- Берём только случаи с ровно одним совпадением по нормализованному имени
-- (lower + trim). Ambiguous (29 complexes с ≥2 candidates) — пропускаем.
UPDATE complexes c
SET developer_id = (
SELECT d.developer_id
FROM domrf_developers d
WHERE lower(trim(d.developer_name)) = lower(trim(c.developer_name))
LIMIT 1
)
WHERE c.developer_id IS NULL
AND c.developer_name IS NOT NULL
AND (
-- только если ровно одна запись в domrf_developers с таким именем
SELECT COUNT(DISTINCT d2.developer_id)
FROM domrf_developers d2
WHERE lower(trim(d2.developer_name)) = lower(trim(c.developer_name))
) = 1;
-- ============================================================
-- E. obj_class — via objective_lots.class (2 complexes покрывается)
-- ============================================================
-- domrf_kn_objects.obj_class = NULL для всех строк (источник не заполнен).
-- objective_lots.class — единственный источник; покрывает ~2 complexes с NULL obj_class.
-- Берём mode (наиболее частый класс) по complex_id.
UPDATE complexes c
SET obj_class = (
SELECT ol.class
FROM objective_lots ol
WHERE ol.complex_id = c.id
AND ol.class IS NOT NULL
GROUP BY ol.class
ORDER BY COUNT(*) DESC
LIMIT 1
)
WHERE c.obj_class IS NULL
AND EXISTS (
SELECT 1 FROM objective_lots ol2
WHERE ol2.complex_id = c.id AND ol2.class IS NOT NULL
);
-- ============================================================
-- F. wall_type — источника данных нет
-- ============================================================
-- domrf_kn_objects.wall_type = NULL для всех 4548 строк.
-- objective_lots не содержит wall_type.
-- Шаг пропускается — нет источника для backfill.
COMMIT;