From c2bc3b4863f4b94f7eb8a6a4d559066613c16849 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 7 Jun 2026 20:44:08 +0300 Subject: [PATCH] fix(sf): ST_MakeValid genplan geom + backfill 881 invalid polygons (#1156) --- backend/app/workers/tasks/genplan_zones_sync.py | 9 ++++++--- backend/tests/workers/test_genplan_zones_sync.py | 12 ++++++++++++ data/sql/143_genplan_makevalid_backfill.sql | 11 +++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 data/sql/143_genplan_makevalid_backfill.sql diff --git a/backend/app/workers/tasks/genplan_zones_sync.py b/backend/app/workers/tasks/genplan_zones_sync.py index 0d31b487..1a6d7c59 100644 --- a/backend/app/workers/tasks/genplan_zones_sync.py +++ b/backend/app/workers/tasks/genplan_zones_sync.py @@ -7,7 +7,8 @@ • SessionLocal() + try/finally close; logger (не print). • SAVEPOINT per-row (``with db.begin_nested():``) — одна битая фича не валит батч. • CAST(:x AS type) — НИКОГДА :x::type (psycopg v3). - • Геометрия: ST_SetSRID(ST_GeomFromGeoJSON, 4326) — без transform (уже 4326). + • Геометрия: ST_MakeValid(ST_SetSRID(ST_GeomFromGeoJSON, 4326)) — без transform (уже 4326); + ST_MakeValid исправляет self-intersecting/ring-order дефекты геопортала (#1156). • ON CONFLICT (globalid) DO UPDATE: обновляет все поля кроме id/fetched_at (latest wins). • commit один раз в конце. @@ -31,7 +32,9 @@ from app.workers.celery_app import celery_app logger = logging.getLogger(__name__) -# UPSERT одной фичи. geom = ST_SetSRID(ST_GeomFromGeoJSON, 4326) — данные уже в 4326. +# UPSERT одной фичи. geom = ST_MakeValid(ST_SetSRID(ST_GeomFromGeoJSON, 4326)) — +# ST_MakeValid оборачивает результат чтобы исправить self-intersecting/ring-order +# дефекты из источника геопортала (881/6436 полигонов имели ST_IsValid=false, #1156). # ON CONFLICT обновляет все аналитические поля; fetched_at = excluded (latest wins). # CAST(:x AS type) — psycopg v3 (НИКОГДА :x::type). _UPSERT_SQL = text( @@ -46,7 +49,7 @@ _UPSERT_SQL = text( CAST(:status AS text), CAST(:area_ha AS numeric), CAST(:doc_ref AS text), - ST_SetSRID(ST_GeomFromGeoJSON(CAST(:geojson AS text)), 4326), + ST_MakeValid(ST_SetSRID(ST_GeomFromGeoJSON(CAST(:geojson AS text)), 4326)), CAST(:raw_props AS jsonb), CAST(:fetched_at AS timestamptz) ) diff --git a/backend/tests/workers/test_genplan_zones_sync.py b/backend/tests/workers/test_genplan_zones_sync.py index 78f23244..d50f2587 100644 --- a/backend/tests/workers/test_genplan_zones_sync.py +++ b/backend/tests/workers/test_genplan_zones_sync.py @@ -199,3 +199,15 @@ def test_sync_empty_wfs_response(monkeypatch: Any) -> None: assert result == {"zones": 0} assert db.committed is True assert db.closed is True + + +def test_upsert_sql_wraps_geom_with_st_makevalid() -> None: + """_UPSERT_SQL содержит ST_MakeValid вокруг ST_SetSRID (#1156). + + Лёгкая проверка без PostGIS: строковое представление compiled SQL + должно включать ST_MakeValid — гарантирует что backfill-логика + присутствует в тексте запроса. + """ + sql_text = str(genplan_zones_sync._UPSERT_SQL) + assert "ST_MakeValid" in sql_text + assert "ST_SetSRID" in sql_text diff --git a/data/sql/143_genplan_makevalid_backfill.sql b/data/sql/143_genplan_makevalid_backfill.sql new file mode 100644 index 00000000..4ce472e1 --- /dev/null +++ b/data/sql/143_genplan_makevalid_backfill.sql @@ -0,0 +1,11 @@ +-- 143_genplan_makevalid_backfill.sql +-- Одноразовый backfill: исправить невалидные геометрии генплан-зон (#1156). +-- 881/6436 полигонов имели ST_IsValid=false (self-intersecting rings из геопортала). +-- Идемпотентно: повторный прогон no-op (WHERE NOT ST_IsValid). +BEGIN; + +UPDATE ekb_genplan_functional_zone + SET geom = ST_MakeValid(geom) + WHERE NOT ST_IsValid(geom); + +COMMIT;