fix(cadastre): cad_parcels.geom Polygon -> MultiPolygon (migration 93)
NSPD returns MultiPolygon for Многоконтурный участок (e.g. 66:41:0105017:4) which crashed upsert_parcel — schema was strict POLYGON. Migration 93: ALTER COLUMN geom Polygon -> MultiPolygon USING ST_Multi(geom), DROP+RECREATE GIST index + back-compat VIEW cad_parcels_geom. upsert_parcel SQL wraps ST_Transform in ST_Multi() — coerces Polygon to MultiPolygon. NSPD returns both types (simple + multi-contour parcels).
This commit is contained in:
parent
e3359c4f03
commit
c76b794841
3 changed files with 86 additions and 3 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) ───────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
61
data/sql/93_cad_parcels_geom_multipolygon.sql
Normal file
61
data/sql/93_cad_parcels_geom_multipolygon.sql
Normal 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;
|
||||
Loading…
Add table
Reference in a new issue