diff --git a/backend/alembic/versions/202607160001_vector_feature_municipality_index.py b/backend/alembic/versions/202607160001_vector_feature_municipality_index.py new file mode 100644 index 00000000..8fda27f1 --- /dev/null +++ b/backend/alembic/versions/202607160001_vector_feature_municipality_index.py @@ -0,0 +1,22 @@ +"""Index partitioned vector features by dataset and municipality.""" + +from alembic import op +import sqlalchemy as sa + + +revision = "202607160001" +down_revision = "202607150001" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_index( + "ix_vector_features_dataset_municipality", + "vector_features", + ["dataset_id", sa.text("(properties_json ->> 'municipality')")], + ) + + +def downgrade() -> None: + op.drop_index("ix_vector_features_dataset_municipality", table_name="vector_features") diff --git a/backend/app/models/entities.py b/backend/app/models/entities.py index 72395040..9ec05743 100644 --- a/backend/app/models/entities.py +++ b/backend/app/models/entities.py @@ -4,7 +4,7 @@ import uuid from datetime import datetime from geoalchemy2 import Geometry -from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Float, Index, JSON, String, Text, UniqueConstraint, func +from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Float, Index, JSON, String, Text, UniqueConstraint, func, text from sqlalchemy.sql.sqltypes import Integer from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -140,6 +140,11 @@ class VectorFeature(Base): Index("ix_vector_features_dataset_id", "dataset_id"), Index("ix_vector_features_geometry", "geometry", postgresql_using="gist"), Index("ix_vector_features_dataset_source_feature", "dataset_id", "source_feature_id"), + Index( + "ix_vector_features_dataset_municipality", + "dataset_id", + text("(properties_json ->> 'municipality')"), + ), ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) diff --git a/backend/tests/test_sprint210_regional_bwk_natura2000.py b/backend/tests/test_sprint210_regional_bwk_natura2000.py index 8d88172f..8fc50e5c 100644 --- a/backend/tests/test_sprint210_regional_bwk_natura2000.py +++ b/backend/tests/test_sprint210_regional_bwk_natura2000.py @@ -237,12 +237,19 @@ def test_regional_operator_is_packaged_release_checked_and_exact_area_is_preferr service = (ROOT / "backend/app/services/vector_feature_service.py").read_text(encoding="utf-8") workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8") catalog = (ROOT / "frontend/src/components/datasets/SourceCatalogPanel.tsx").read_text(encoding="utf-8") + model = (ROOT / "backend/app/models/entities.py").read_text(encoding="utf-8") + migration = ( + ROOT / "backend/alembic/versions/202607160001_vector_feature_municipality_index.py" + ).read_text(encoding="utf-8") assert "COPY scripts/provision_regional_bwk_natura2000.py" in dockerfile assert "py_compile scripts/provision_regional_bwk_natura2000.py" in readiness assert '"provision_regional_bwk_natura2000.py"' in service assert "dataset.area_id === selectedAreaId ? 10_000_000" in workspace assert "largestBwkSnapshot" in catalog + assert "ix_vector_features_dataset_municipality" in model + assert "ix_vector_features_dataset_municipality" in migration + assert 'down_revision = "202607150001"' in migration def test_regional_bwk_uses_only_canonical_preclipped_municipality_partitions() -> None: diff --git a/docs/DATABASE_IMPLEMENTATION_PLAN.md b/docs/DATABASE_IMPLEMENTATION_PLAN.md index 2d585a5a..e5da1089 100644 --- a/docs/DATABASE_IMPLEMENTATION_PLAN.md +++ b/docs/DATABASE_IMPLEMENTATION_PLAN.md @@ -306,3 +306,12 @@ Their `provenance_metadata` records 28-partition identity, combined output checksum and retained raw-response status. Municipality-clipped source ids are partition-suffixed for uniqueness, while `identity_stable=false` prohibits object-lineage interpretation. No partition or temporal shadow table is added. +### Partitioned vector feature lookup index + +Regional operator Datasets retain their municipality partition identity in +`vector_features.properties_json.municipality`. Migration `202607160001` adds +the composite expression index +`ix_vector_features_dataset_municipality(dataset_id, properties_json->>'municipality')`. +It supports exact preclipped municipality selection without changing the +canonical `vector_features` schema or introducing operator-specific tables. +Free rectangle queries continue to use the geometry GiST index.