fix(cadastre): cad_parcels.geom Polygon → MultiPolygon (migration 93) #185

Merged
lekss361 merged 1 commit from fix/cadastre-parcels-geom-multipolygon into main 2026-05-15 15:52:38 +00:00
3 changed files with 86 additions and 3 deletions

View file

@ -445,10 +445,15 @@ def upsert_parcel(db: Session, feature: NSPDBulkFeature, source: str = "search")
:readable_address, :status, :previously_posted,
CAST(:registration_date AS date),
:subcategory, :cadastral_districts_code,
-- ST_Multi: cad_parcels.geom = MULTIPOLYGON (migration 93).
-- NSPD возвращает Polygon ИЛИ MultiPolygon (Многоконтурные участки)
-- оба нужно сохранить, поэтому ST_Multi() coerce'ит Polygon → MultiPolygon.
CASE WHEN CAST(:geom AS text) IS NOT NULL THEN
ST_Transform(
ST_SetSRID(ST_GeomFromGeoJSON(CAST(:geom AS text)), 3857),
4326
ST_Multi(
ST_Transform(
ST_SetSRID(ST_GeomFromGeoJSON(CAST(:geom AS text)), 3857),
4326
)
)
ELSE NULL END,
CAST(:raw_props AS jsonb), :source, NOW(), NOW()

View file

@ -345,6 +345,23 @@ def test_upsert_parcel_uses_cast_not_double_colon() -> None:
assert "CAST(:raw_props AS jsonb)" in sql
def test_upsert_parcel_sql_uses_st_multi_for_multipolygon_schema() -> None:
"""cad_parcels.geom = MULTIPOLYGON (migration 93) — SQL должен ST_Multi() оборачивать
transform, чтобы Polygon coerce'ился в MultiPolygon (NSPD возвращает оба варианта).
"""
from app.services.cadastre.bulk_harvest import upsert_parcel
db = MagicMock()
executed_sqls: list[str] = []
db.execute = lambda stmt, params=None: (executed_sqls.append(str(stmt)) or MagicMock())
upsert_parcel(db, _make_parcel_feature(), source="search")
assert len(executed_sqls) == 1
sql = executed_sqls[0]
assert "ST_Multi(" in sql, "SQL должен содержать ST_Multi для MultiPolygon schema"
# ── harvest_quarter integration (async, mocked) ───────────────────────────────

View file

@ -0,0 +1,61 @@
-- 93_cad_parcels_geom_multipolygon.sql
-- ---------------------------------------------------------------------------
-- Контекст:
-- NSPD возвращает MultiPolygon для "Многоконтурный участок" (например
-- 66:41:0105017:4 — гаражный массив с несколькими parts). cad_parcels.geom
-- = geometry(Polygon, 4326) strict → INSERT падал с psycopg.DataError.
--
-- Pilot v8/v10 умирали на MultiPolygon parcels (worker crash → autoretry
-- exhausted → job hang).
--
-- Действия:
-- 1. DROP back-compat VIEW cad_parcels_geom (зависит от geom column)
-- 2. ALTER COLUMN cad_parcels.geom TYPE MultiPolygon USING ST_Multi(geom)
-- — лосслесная конверсия 1199 existing Polygon rows → MultiPolygon
-- 3. RECREATE GIST index (ALTER COLUMN не сохраняет тип-специфичный индекс)
-- 4. RECREATE VIEW cad_parcels_geom с тем же SELECT
--
-- Idempotent: использует ST_GeometryType check + IF NOT EXISTS на индексах.
-- Apply order: после 92_cad_bulk_layers.sql, до bulk ingest jobs.
-- ---------------------------------------------------------------------------
BEGIN;
-- 1. DROP back-compat VIEW (зависит от geom column type)
DROP VIEW IF EXISTS cad_parcels_geom;
-- 2. ALTER column type Polygon → MultiPolygon (idempotent: skip if already MultiPolygon)
DO $$
DECLARE
current_type TEXT;
BEGIN
SELECT type INTO current_type
FROM geometry_columns
WHERE f_table_name = 'cad_parcels' AND f_geometry_column = 'geom';
IF current_type = 'POLYGON' THEN
ALTER TABLE cad_parcels
ALTER COLUMN geom TYPE geometry(MultiPolygon, 4326)
USING ST_Multi(geom);
RAISE NOTICE 'cad_parcels.geom converted Polygon -> MultiPolygon (rows: %)',
(SELECT COUNT(*) FROM cad_parcels WHERE geom IS NOT NULL);
ELSE
RAISE NOTICE 'cad_parcels.geom already type=% — skip ALTER', current_type;
END IF;
END$$;
-- 3. GIST индекс — DROP + CREATE для надёжности (тип column изменился)
DROP INDEX IF EXISTS cad_parcels_geom_gist;
CREATE INDEX IF NOT EXISTS cad_parcels_geom_gist
ON cad_parcels USING GIST (geom);
-- 4. RECREATE back-compat VIEW (идентичный SELECT, но новый geom type)
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 VIEW over cad_parcels (geom column now MultiPolygon). '
'Legacy callers expecting cad_parcels_geom table continue работать.';
COMMIT;