fix: make vector ingestion canonical and atomic
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user