fix(cadastre): filter Point/LineString geometry in upsert_parcel/zouit (worker crash root cause) #184

Merged
lekss361 merged 1 commit from fix/cadastre-parcel-geometry-type-filter into main 2026-05-15 15:37:29 +00:00
2 changed files with 108 additions and 2 deletions

View file

@ -406,7 +406,21 @@ def upsert_parcel(db: Session, feature: NSPDBulkFeature, source: str = "search")
return
raw_opts = feature.properties.get("options") or {}
geom_json = json.dumps(feature.geometry) if feature.geometry else None
# cad_parcels.geom — geometry(Polygon, 4326). NSPD изредка возвращает Point
# для участков без detailed boundary — INSERT fail'нит на schema check.
# Сохраняем raw_props (есть координаты), geom=NULL. Не блокирует Phase 1.
geom = feature.geometry or {}
geom_type = geom.get("type") if isinstance(geom, dict) else None
if geom_type in ("Polygon", "MultiPolygon"):
geom_json: str | None = json.dumps(feature.geometry)
else:
if geom_type:
logger.info(
"upsert_parcel: cad_num=%s имеет geometry type=%s (не Polygon) — geom=NULL",
cad_num,
geom_type,
)
geom_json = None
quarter_cad = opts.quarter_cad_number or _derive_quarter_cad(cad_num)
db.execute(
@ -880,7 +894,20 @@ def upsert_zouit(db: Session, feature: NSPDBulkFeature, source: str = "search")
)
return
geom_json = json.dumps(feature.geometry) if feature.geometry else None
# cad_zouit.geom — geometry(MultiPolygon, 4326). Polygon допустим (ST_Multi
# обернёт в SQL), но Point/LineString → INSERT fail. Фильтруем как в upsert_parcel.
geom = feature.geometry or {}
geom_type = geom.get("type") if isinstance(geom, dict) else None
if geom_type in ("Polygon", "MultiPolygon"):
geom_json: str | None = json.dumps(feature.geometry)
else:
if geom_type:
logger.info(
"upsert_zouit: reg=%s имеет geometry type=%s (не Polygon) — geom=NULL",
reg_numb_border,
geom_type,
)
geom_json = None
db.execute(
text("""

View file

@ -923,6 +923,85 @@ def test_upsert_quarter_geom_sql_uses_cast_not_double_colon() -> None:
assert "CAST(:geom AS text)" in sql
def test_upsert_parcel_skips_geom_for_point_geometry() -> None:
"""NSPD изредка возвращает Point для ЗУ — cad_parcels.geom strict Polygon,
INSERT fail'нул бы. Фильтр на Python level → geom=NULL, raw_props сохранён.
"""
from app.services.cadastre.bulk_harvest import upsert_parcel
point_parcel = NSPDBulkFeature.model_validate(
{
"id": 99,
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [6753892.91, 7747664.31]},
"properties": {
"category": 36368,
"options": {
"cad_num": "66:41:0104002:18",
"quarter_cad_number": "66:41:0104002",
"declared_area": 200.0,
"readable_address": "г Екатеринбург, ул Тестовая",
},
},
}
)
db = MagicMock()
upsert_parcel(db, point_parcel, source="search")
# db.execute должен быть вызван (parcel НЕ skipped, только geom=NULL)
db.execute.assert_called_once()
call_params = db.execute.call_args[0][1]
assert call_params["cad_num"] == "66:41:0104002:18"
# geom параметр должен быть None
assert call_params["geom"] is None
def test_upsert_parcel_keeps_polygon_geometry() -> None:
"""Polygon geometry для ЗУ — обычный happy path."""
from app.services.cadastre.bulk_harvest import upsert_parcel
parcel = _make_parcel_feature() # Polygon
db = MagicMock()
upsert_parcel(db, parcel, source="search")
db.execute.assert_called_once()
call_params = db.execute.call_args[0][1]
assert call_params["geom"] is not None
assert '"type": "Polygon"' in call_params["geom"]
def test_upsert_zouit_skips_geom_for_linestring() -> None:
"""ЗОУИТ schema MultiPolygon — LineString не валиден, geom=NULL."""
from app.services.cadastre.bulk_harvest import upsert_zouit
line_zouit = NSPDBulkFeature.model_validate(
{
"id": 1,
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[6090000, 7590000], [6090100, 7590100]],
},
"properties": {
"category": 36940,
"options": {
"reg_numb_border": "66-0.1-2.1-99",
"type_zone": "Linear",
"registration_date": "2020-01-01",
},
},
}
)
db = MagicMock()
upsert_zouit(db, line_zouit, source="search")
db.execute.assert_called_once()
call_params = db.execute.call_args[0][1]
assert call_params["geom"] is None
def test_upsert_quarter_geom_custom_width_bounds() -> None:
"""min_width_m и max_width_m пробрасываются как параметры SQL (не hardcoded)."""
db = _mock_db_execute_scalar(None)