fix(cadastre): filter Point/LineString geometry in upsert_parcel/zouit
cad_parcels.geom is geometry(Polygon, 4326) — strict schema. NSPD occasionally returns Point geometry for parcels without detailed boundary, causing INSERT failure: psycopg.errors.InvalidParameterValue: Geometry type (Point) does not match column type (Polygon) This killed pilot v8 at quarter 66:41:0104002 — worker autoretry exhausted, job hung at 25/50 (heartbeat stale). Fix: filter geometry.type at Python level in upsert_parcel + upsert_zouit (same pattern as upsert_quarter_geom_from_feature). Non-Polygon geometry → geom=NULL, raw_props preserved. Tables with permissive GEOMETRY schema (cad_buildings, cad_constructions, cad_enk, cad_oncs) unaffected — they accept any geometry type. Tests: 3 new (Point parcel → geom=NULL, Polygon happy path, LineString zouit).
This commit is contained in:
parent
e695bca1e4
commit
adc1eb1366
2 changed files with 108 additions and 2 deletions
|
|
@ -406,7 +406,21 @@ def upsert_parcel(db: Session, feature: NSPDBulkFeature, source: str = "search")
|
||||||
return
|
return
|
||||||
|
|
||||||
raw_opts = feature.properties.get("options") or {}
|
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)
|
quarter_cad = opts.quarter_cad_number or _derive_quarter_cad(cad_num)
|
||||||
|
|
||||||
db.execute(
|
db.execute(
|
||||||
|
|
@ -880,7 +894,20 @@ def upsert_zouit(db: Session, feature: NSPDBulkFeature, source: str = "search")
|
||||||
)
|
)
|
||||||
return
|
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(
|
db.execute(
|
||||||
text("""
|
text("""
|
||||||
|
|
|
||||||
|
|
@ -923,6 +923,85 @@ def test_upsert_quarter_geom_sql_uses_cast_not_double_colon() -> None:
|
||||||
assert "CAST(:geom AS text)" in sql
|
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:
|
def test_upsert_quarter_geom_custom_width_bounds() -> None:
|
||||||
"""min_width_m и max_width_m пробрасываются как параметры SQL (не hardcoded)."""
|
"""min_width_m и max_width_m пробрасываются как параметры SQL (не hardcoded)."""
|
||||||
db = _mock_db_execute_scalar(None)
|
db = _mock_db_execute_scalar(None)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue