diff --git a/backend/app/services/cadastre/bulk_harvest.py b/backend/app/services/cadastre/bulk_harvest.py index 5e4aad8c..905d15de 100644 --- a/backend/app/services/cadastre/bulk_harvest.py +++ b/backend/app/services/cadastre/bulk_harvest.py @@ -555,9 +555,7 @@ async def backfill_parcel_geom( # Один сбойный квартал не валит весь backfill — лог + продолжаем. # (WAF 403 пробросится из client и прервёт прогон — это ожидаемо, # caller-task ловит и не ретраит, как в bulk_harvest.) - logger.warning( - "backfill_parcel_geom: grid-walk failed quarter=%s: %s", quarter, e - ) + logger.warning("backfill_parcel_geom: grid-walk failed quarter=%s: %s", quarter, e) db.rollback() continue @@ -1477,6 +1475,21 @@ def _save_territorial_zones(db: Session, quarter_cad: str, features: list[dict]) zone_name = props.get("zone_name") or props.get("zone_type_name") or props.get("type_zone") permitted_use = props.get("permitted_use") or props.get("vri") + # cad_territorial_zones.geom — geography(MultiPolygon, 4326). + # Polygon допустим (ST_Multi обернёт в SQL), но Point/LineString → + # geography-INSERT fail → SAVEPOINT откат → строка дропается молча. + # Зеркалит фильтр upsert_zouit (~1196). + geom_type = geom.get("type") if isinstance(geom, dict) else None + if geom_type not in ("Polygon", "MultiPolygon"): + if geom_type: + logger.info( + "_save_territorial_zones: zone_id=%s geom type=%s (не Polygon/MultiPolygon)" + " — geom=NULL", + zone_id, + geom_type, + ) + geom_geojson = None + try: # begin_nested() требует активной outer-транзакции для SAVEPOINT. # SQLAlchemy Session (autobegin=True) автоматически начинает tx при первом @@ -1495,9 +1508,11 @@ def _save_territorial_zones(db: Session, quarter_cad: str, features: list[dict]) CAST(:permitted_use AS text), CAST(:raw_props AS jsonb), CASE WHEN CAST(:geom AS text) IS NOT NULL - THEN ST_Transform( - ST_SetSRID(ST_GeomFromGeoJSON(CAST(:geom AS text)), 3857), - 4326 + THEN ST_Multi( + ST_Transform( + ST_SetSRID(ST_GeomFromGeoJSON(CAST(:geom AS text)), 3857), + 4326 + ) )::geography ELSE NULL END diff --git a/data/sql/159_alter_cad_territorial_zones_multipolygon.sql b/data/sql/159_alter_cad_territorial_zones_multipolygon.sql new file mode 100644 index 00000000..5731421f --- /dev/null +++ b/data/sql/159_alter_cad_territorial_zones_multipolygon.sql @@ -0,0 +1,55 @@ +-- 159_alter_cad_territorial_zones_multipolygon.sql +-- --------------------------------------------------------------------------- +-- Контекст: +-- NSPD ПЗЗ территориальные зоны (layer 875838) зачастую возвращают +-- MultiPolygon (у зоны несколько геометрических контуров). Колонка +-- cad_territorial_zones.geom = GEOGRAPHY(POLYGON, 4326) strict → INSERT +-- падал через SAVEPOINT в per-row begin_nested() и строки молча дропались. +-- +-- Поведение зеркалит cad_zouit (фикс в 93_cad_parcels_geom_multipolygon.sql +-- как прецедент); патч _save_territorial_zones аналогичен upsert_zouit. +-- +-- Действия: +-- 1. ALTER COLUMN geom TYPE geography(MultiPolygon, 4326) +-- USING ST_Multi(geom::geometry)::geography +-- — lossless conversion существующих Polygon-строк в MultiPolygon. +-- 2. DROP + CREATE GIST-индекса (ALTER TYPE требует пересоздания индекса +-- для geography-колонки). +-- +-- Idempotent: DO $$...END$$ проверяет текущий type по geometry_columns +-- (geography тоже попадает через postgis_topology_views). Если уже +-- MultiPolygon — пропускает ALTER; GIST пересоздаётся DROP IF EXISTS + IF NOT EXISTS. +-- +-- Зависимые VIEW: нет (проверено grep data/sql/ и backend/). +-- Apply after: 158_pzz_zones_ekb_nulls_not_distinct.sql +-- --------------------------------------------------------------------------- + +BEGIN; + +-- 1. ALTER COLUMN: POLYGON → MULTIPOLYGON (idempotent через geometry_columns) +DO $$ +DECLARE + current_type TEXT; +BEGIN + SELECT upper(type) INTO current_type + FROM geometry_columns + WHERE f_table_name = 'cad_territorial_zones' + AND f_geometry_column = 'geom'; + + IF current_type = 'POLYGON' THEN + ALTER TABLE cad_territorial_zones + ALTER COLUMN geom TYPE geography(MultiPolygon, 4326) + USING ST_Multi(geom::geometry)::geography; + RAISE NOTICE 'cad_territorial_zones.geom converted POLYGON -> MULTIPOLYGON (rows: %)', + (SELECT COUNT(*) FROM cad_territorial_zones WHERE geom IS NOT NULL); + ELSE + RAISE NOTICE 'cad_territorial_zones.geom type=% — skip ALTER', current_type; + END IF; +END$$; + +-- 2. GIST индекс — DROP + CREATE (тип column изменился, индекс пересоздаём) +DROP INDEX IF EXISTS cad_territorial_zones_geom_gist; +CREATE INDEX IF NOT EXISTS cad_territorial_zones_geom_gist + ON cad_territorial_zones USING GIST (geom); + +COMMIT;