fix(#244): harvest_quarter — CAST CASE WHEN params (psycopg3 AmbiguousParameter) #246
2 changed files with 81 additions and 4 deletions
|
|
@ -125,11 +125,17 @@ _UPSERT_SQL = text(
|
|||
harvest_error, region_code
|
||||
) VALUES (
|
||||
:quarter_cad,
|
||||
CASE WHEN :geom_json IS NULL THEN NULL
|
||||
ELSE ST_Multi(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:geom_json), 3857), 4326))
|
||||
CASE WHEN CAST(:geom_json AS text) IS NULL THEN NULL
|
||||
ELSE ST_Multi(ST_Transform(
|
||||
ST_SetSRID(ST_GeomFromGeoJSON(CAST(:geom_json AS text)), 3857), 4326))
|
||||
END,
|
||||
CASE WHEN :bbox_xmin IS NULL THEN NULL
|
||||
ELSE ST_MakeEnvelope(:bbox_xmin, :bbox_ymin, :bbox_xmax, :bbox_ymax, 3857)
|
||||
CASE WHEN CAST(:bbox_xmin AS double precision) IS NULL THEN NULL
|
||||
ELSE ST_MakeEnvelope(
|
||||
CAST(:bbox_xmin AS double precision),
|
||||
CAST(:bbox_ymin AS double precision),
|
||||
CAST(:bbox_xmax AS double precision),
|
||||
CAST(:bbox_ymax AS double precision),
|
||||
3857)
|
||||
END,
|
||||
:parcels_count, :buildings_count, :territorial_zones_count,
|
||||
:red_lines_count, :engineering_count, :zouit_count, :risks_count, :total_features,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from app.workers.tasks.nspd_sync import (
|
|||
_build_features_json,
|
||||
_build_risks_count,
|
||||
_build_zouit_count,
|
||||
_upsert_dump,
|
||||
harvest_quarter,
|
||||
harvest_stale_quarters,
|
||||
)
|
||||
|
|
@ -71,6 +72,7 @@ def _make_dump(
|
|||
engineering_structures=[_make_feat("e1")],
|
||||
zouit=zouit,
|
||||
risks=risks,
|
||||
opportunity={},
|
||||
layers_fetched=(
|
||||
"search",
|
||||
"parcels",
|
||||
|
|
@ -204,6 +206,7 @@ def test_harvest_quarter_empty_quarter(
|
|||
engineering_structures=[],
|
||||
zouit={"okn": [], "engineering": [], "natural": [], "protected": [], "other": []},
|
||||
risks={},
|
||||
opportunity={},
|
||||
layers_fetched=("search",),
|
||||
bbox_3857=None,
|
||||
fetched_at_utc="2026-05-12T03:00:00+00:00",
|
||||
|
|
@ -365,6 +368,7 @@ def test_features_json_geometry_preserved() -> None:
|
|||
engineering_structures=[],
|
||||
zouit={},
|
||||
risks={},
|
||||
opportunity={},
|
||||
layers_fetched=("search", "parcels"),
|
||||
bbox_3857=(100.0, 200.0, 300.0, 400.0),
|
||||
fetched_at_utc="2026-05-12T00:00:00+00:00",
|
||||
|
|
@ -375,3 +379,70 @@ def test_features_json_geometry_preserved() -> None:
|
|||
assert result[0]["layer"] == "parcels"
|
||||
assert result[0]["feature_id"] == "p1"
|
||||
assert result[0]["properties"] == {"k": "v"}
|
||||
|
||||
|
||||
# ── _upsert_dump CAST regression tests (#244) ────────────────────────────────
|
||||
|
||||
|
||||
@patch("app.workers.tasks.nspd_sync.SessionLocal")
|
||||
def test_upsert_dump_null_geom_executes_without_error(mock_session_cls: MagicMock) -> None:
|
||||
"""Regression #244: _upsert_dump с geom_json=None не падает на AmbiguousParameter.
|
||||
|
||||
psycopg3 не может вывести тип голого параметра $N внутри CASE WHEN ... IS NULL.
|
||||
Fix: CAST(:geom_json AS text) IS NULL даёт явный тип — нет AmbiguousParameter.
|
||||
"""
|
||||
mock_db = MagicMock()
|
||||
mock_session_cls.return_value = mock_db
|
||||
|
||||
# dump=None → error-only path: все geo-параметры None, должен execute без ошибок
|
||||
_upsert_dump(
|
||||
quarter_cad="66:41:0204016",
|
||||
region_code=66,
|
||||
dump=None,
|
||||
features_json=None,
|
||||
duration_ms=100,
|
||||
harvest_error="TestError: simulated",
|
||||
)
|
||||
|
||||
mock_db.execute.assert_called_once()
|
||||
mock_db.commit.assert_called_once()
|
||||
|
||||
|
||||
@patch("app.workers.tasks.nspd_sync.SessionLocal")
|
||||
def test_upsert_dump_with_geom_executes_without_error(mock_session_cls: MagicMock) -> None:
|
||||
"""Regression #244: _upsert_dump с реальным geom_json и bbox не падает.
|
||||
|
||||
CAST(:geom_json AS text) передаёт строку, CAST(:bbox_xmin AS double precision) —
|
||||
число. Оба branch CASE WHEN типизированы корректно.
|
||||
"""
|
||||
mock_db = MagicMock()
|
||||
mock_session_cls.return_value = mock_db
|
||||
|
||||
geom = {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[[100.0, 200.0], [300.0, 200.0], [300.0, 400.0], [100.0, 400.0], [100.0, 200.0]]
|
||||
],
|
||||
}
|
||||
dump = _make_dump(
|
||||
quarter_cad="66:41:0204016",
|
||||
quarter_feat=_make_feat_with_geom("q1", geom),
|
||||
bbox=(100.0, 200.0, 300.0, 400.0),
|
||||
)
|
||||
|
||||
_upsert_dump(
|
||||
quarter_cad="66:41:0204016",
|
||||
region_code=66,
|
||||
dump=dump,
|
||||
features_json=_build_features_json(dump),
|
||||
duration_ms=500,
|
||||
harvest_error=None,
|
||||
)
|
||||
|
||||
mock_db.execute.assert_called_once()
|
||||
# Verify params contain properly-typed values (not None for geo fields)
|
||||
call_params = mock_db.execute.call_args[0][1]
|
||||
assert call_params["geom_json"] is not None # JSON string
|
||||
assert call_params["bbox_xmin"] == 100.0
|
||||
assert call_params["bbox_ymax"] == 400.0
|
||||
mock_db.commit.assert_called_once()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue