fix: make vector ingestion canonical and atomic
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-14 15:28:14 +02:00
parent 7fa4f1fac9
commit 8c694fa9ce
8 changed files with 145 additions and 25 deletions
@@ -5,6 +5,7 @@ from pathlib import Path
from uuid import uuid4
import pytest
from geoalchemy2.shape import to_shape
from app.api.routes.qa import compare_candidate_with_reference
from app.models import Dataset, Metric, Project, QualityCheck, VectorFeature
@@ -22,6 +23,7 @@ class FakeSession:
self.commits = 0
self.refreshes = []
self.flushes = 0
self.rollbacks = 0
def get(self, model, item_id):
return self.objects.get((model, item_id))
@@ -38,6 +40,9 @@ class FakeSession:
def refresh(self, item) -> None:
self.refreshes.append(item)
def rollback(self) -> None:
self.rollbacks += 1
def test_vector_feature_service_persists_geojson_features_with_properties() -> None:
db = FakeSession()
@@ -84,6 +89,34 @@ def test_vector_feature_service_persists_geojson_features_with_properties() -> N
assert db.refreshes == []
def test_vector_feature_service_normalizes_source_z_coordinates_to_canonical_2d() -> None:
db = FakeSession()
persisted = VectorFeatureService.persist_geojson_features(
db=db,
dataset_id=uuid4(),
payload={
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "sector-3d",
"properties": {"population_total": 100},
"geometry": {
"type": "Polygon",
"coordinates": [
[[5.0, 51.0, 0.0], [5.1, 51.0, 0.0], [5.1, 51.1, 0.0], [5.0, 51.0, 0.0]]
],
},
}
],
},
feature_class="population",
)
assert len(persisted) == 1
assert to_shape(persisted[0].geometry).has_z is False
def test_dataset_upload_persists_vector_features(monkeypatch, tmp_path) -> None:
project_id = uuid4()
db = FakeSession(objects={(Project, project_id): Project(id=project_id, name="Geel")})
@@ -141,6 +174,53 @@ def test_dataset_upload_persists_vector_features(monkeypatch, tmp_path) -> None:
assert result.source_name == "manual"
assert len(persisted_features) == 1
assert persisted_features[0].dataset_id == result.id
assert db.commits == 1
def test_dataset_upload_rolls_back_dataset_and_file_when_vector_indexing_fails(monkeypatch, tmp_path) -> None:
project_id = uuid4()
db = FakeSession(objects={(Project, project_id): Project(id=project_id, name="Mol")})
storage_path = tmp_path / "invalid.geojson"
storage_path.write_text("{}", encoding="utf-8")
class Upload:
filename = "invalid.geojson"
content_type = "application/geo+json"
async def read(self) -> bytes:
return b'{"type":"FeatureCollection","features":[]}'
monkeypatch.setattr(
"app.services.dataset_service.StorageService.persist_dataset_file",
lambda **_kwargs: {
"storage_path": str(storage_path),
"original_filename": "invalid.geojson",
"stored_filename": "invalid.geojson",
"content_type": "application/geo+json",
"size_bytes": 2,
"checksum_sha256": "0" * 64,
},
)
monkeypatch.setattr(
VectorFeatureService,
"persist_geojson_features",
lambda **_kwargs: (_ for _ in ()).throw(RuntimeError("PostGIS indexing failed")),
)
with pytest.raises(RuntimeError, match="PostGIS indexing failed"):
asyncio.run(
DatasetService.upload_dataset(
db=db,
project_id=project_id,
file=Upload(),
dataset_type="vector",
source="user_upload",
)
)
assert db.commits == 0
assert db.rollbacks == 1
assert storage_path.exists() is False
def test_quality_service_persists_quality_check_and_metrics() -> None: