fix(#265 review): CRS 3857→4326 + stable zone_id hash + rename 102 + tx comment
- #1 (BLOCK): ST_GeogFromGeoJSON → ST_Transform(ST_SetSRID(..., 3857), 4326)::geography so EPSG:3857 features from get_territorial_zones_in_bbox are correctly stored in GEOGRAPHY(POLYGON, 4326) column (matches cad_parcels/cad_buildings pattern) - #2: zone_id fallback changed from counter-based f"{quarter_cad}_{inserted}" to md5(json(props))[:12] — stable between runs, prevents wrong-row ON CONFLICT - #3: rename 101_cad_nspd_overlay_tables.sql → 102_* (101_user_custom_pois.sql exists) - #4: add comment clarifying SQLAlchemy autobegin guarantees outer tx before begin_nested() - Tests: updated test_feature_without_zone_id_uses_fallback to assert md5 format + idempotency
This commit is contained in:
parent
f08db43145
commit
8e377059a4
3 changed files with 31 additions and 8 deletions
|
|
@ -19,6 +19,7 @@ Resumable: phase_state в cadastre_jobs показывает прогресс.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
@ -1210,10 +1211,16 @@ def _save_territorial_zones(db: Session, quarter_cad: str, features: list[dict])
|
||||||
geom = f.get("geometry")
|
geom = f.get("geometry")
|
||||||
geom_geojson: str | None = json.dumps(geom) if geom else None
|
geom_geojson: str | None = json.dumps(geom) if geom else None
|
||||||
|
|
||||||
# zone_id — NSPD feature id или синтетический fallback
|
# zone_id — NSPD feature id или стабильный fallback на основе md5 от properties.
|
||||||
zone_id = str(
|
# md5 гарантирует идемпотентность между runs (счётчик inserted сбрасывается).
|
||||||
props.get("id") or props.get("zone_id") or f.get("id") or f"{quarter_cad}_{inserted}"
|
_raw_id = props.get("id") or props.get("zone_id") or f.get("id")
|
||||||
)
|
if _raw_id:
|
||||||
|
zone_id = str(_raw_id)
|
||||||
|
else:
|
||||||
|
_props_hash = hashlib.md5(
|
||||||
|
json.dumps(props, sort_keys=True).encode("utf-8")
|
||||||
|
).hexdigest()[:12]
|
||||||
|
zone_id = f"{quarter_cad}_{_props_hash}"
|
||||||
zone_code = (
|
zone_code = (
|
||||||
props.get("zone_code") or props.get("zone_index") or props.get("reg_numb_border")
|
props.get("zone_code") or props.get("zone_index") or props.get("reg_numb_border")
|
||||||
)
|
)
|
||||||
|
|
@ -1221,6 +1228,9 @@ def _save_territorial_zones(db: Session, quarter_cad: str, features: list[dict])
|
||||||
permitted_use = props.get("permitted_use") or props.get("vri")
|
permitted_use = props.get("permitted_use") or props.get("vri")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# begin_nested() требует активной outer-транзакции для SAVEPOINT.
|
||||||
|
# SQLAlchemy Session (autobegin=True) автоматически начинает tx при первом
|
||||||
|
# db.execute() в этом loop — outer tx гарантирована.
|
||||||
with db.begin_nested():
|
with db.begin_nested():
|
||||||
db.execute(
|
db.execute(
|
||||||
text("""
|
text("""
|
||||||
|
|
@ -1235,7 +1245,10 @@ def _save_territorial_zones(db: Session, quarter_cad: str, features: list[dict])
|
||||||
CAST(:permitted_use AS text),
|
CAST(:permitted_use AS text),
|
||||||
CAST(:raw_props AS jsonb),
|
CAST(:raw_props AS jsonb),
|
||||||
CASE WHEN CAST(:geom AS text) IS NOT NULL
|
CASE WHEN CAST(:geom AS text) IS NOT NULL
|
||||||
THEN ST_GeogFromGeoJSON(CAST(:geom AS text))
|
THEN ST_Transform(
|
||||||
|
ST_SetSRID(ST_GeomFromGeoJSON(CAST(:geom AS text)), 3857),
|
||||||
|
4326
|
||||||
|
)::geography
|
||||||
ELSE NULL
|
ELSE NULL
|
||||||
END
|
END
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ class TestSaveTerritorialZones:
|
||||||
assert call_kwargs["geom"] is None
|
assert call_kwargs["geom"] is None
|
||||||
|
|
||||||
def test_feature_without_zone_id_uses_fallback(self) -> None:
|
def test_feature_without_zone_id_uses_fallback(self) -> None:
|
||||||
"""Feature без id → синтетический fallback zone_id не вызывает краш."""
|
"""Feature без id → md5-based fallback zone_id (stable между runs)."""
|
||||||
db = _make_db_mock()
|
db = _make_db_mock()
|
||||||
features = [
|
features = [
|
||||||
{
|
{
|
||||||
|
|
@ -116,8 +116,18 @@ class TestSaveTerritorialZones:
|
||||||
|
|
||||||
assert result == 1
|
assert result == 1
|
||||||
call_kwargs = db.execute.call_args[0][1]
|
call_kwargs = db.execute.call_args[0][1]
|
||||||
# fallback zone_id содержит quarter_cad
|
zone_id: str = call_kwargs["zone_id"]
|
||||||
assert "66:41:0204016" in call_kwargs["zone_id"]
|
# fallback zone_id содержит quarter_cad и стабильный hash (12 hex chars)
|
||||||
|
assert zone_id.startswith("66:41:0204016_")
|
||||||
|
suffix = zone_id.split("_", 3)[-1]
|
||||||
|
assert len(suffix) == 12
|
||||||
|
assert all(c in "0123456789abcdef" for c in suffix)
|
||||||
|
|
||||||
|
# Второй вызов с теми же данными → тот же zone_id (идемпотентность)
|
||||||
|
db2 = _make_db_mock()
|
||||||
|
_save_territorial_zones(db2, "66:41:0204016", features)
|
||||||
|
call_kwargs2 = db2.execute.call_args[0][1]
|
||||||
|
assert call_kwargs2["zone_id"] == zone_id
|
||||||
|
|
||||||
def test_zone_id_from_props_id(self) -> None:
|
def test_zone_id_from_props_id(self) -> None:
|
||||||
"""Если feature.id=None, но props['id'] есть — используется props['id']."""
|
"""Если feature.id=None, но props['id'] есть — используется props['id']."""
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue