#!/usr/bin/env python3 """Exercise Phase-2 PostgreSQL migration guards on a disposable database. This is intentionally *not* a general migration runner. It refuses every database whose name does not start with ``geointel_phase2_`` so it cannot be pointed accidentally at a developer, staging or production database. The script upgrades the complete Alembic chain, proves the new trigger guards with real DML, and optionally downgrades again. """ from __future__ import annotations import argparse from contextlib import contextmanager from datetime import UTC, datetime import json import os from pathlib import Path import subprocess import sys from typing import Any, Iterator from uuid import UUID, uuid4 from sqlalchemy import create_engine, text from sqlalchemy.engine import Connection, Engine, make_url from sqlalchemy.exc import IntegrityError ROOT = Path(__file__).resolve().parents[1] BACKEND = ROOT / "backend" SAFE_DATABASE_PREFIX = "geointel_phase2_" def _json_dump(path: Path, value: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text( json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8" ) def _require_disposable_database(database_url: str) -> None: parsed = make_url(database_url) database_name = str(parsed.database or "") if not database_name.startswith(SAFE_DATABASE_PREFIX): raise SystemExit( "Refusing migration guard test: database name must start with " f"{SAFE_DATABASE_PREFIX!r}, received {database_name!r}." ) def _run_alembic(database_url: str, *arguments: str) -> str: environment = dict(os.environ) environment["DATABASE_URL"] = database_url completed = subprocess.run( [sys.executable, "-m", "alembic", *arguments], cwd=BACKEND, env=environment, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, ) if completed.returncode != 0: raise RuntimeError( f"Alembic {' '.join(arguments)} failed with exit code {completed.returncode}:\n{completed.stdout}" ) return completed.stdout @contextmanager def _transaction(engine: Engine) -> Iterator[Connection]: with engine.begin() as connection: yield connection def _insert_project(connection: Connection) -> UUID: project_id = uuid4() connection.execute( text("INSERT INTO projects (id, name) VALUES (:id, :name)"), {"id": project_id, "name": "Phase 2 migration guard fixture"}, ) return project_id def _source_id(connection: Connection, source_key: str) -> UUID: value = connection.execute( text("SELECT id FROM source_registry WHERE source_key = :source_key"), {"source_key": source_key}, ).scalar_one() return UUID(str(value)) def _insert_snapshot( connection: Connection, source_registry_id: UUID, *, key: str, checksum: str ) -> UUID: snapshot_id = uuid4() connection.execute( text( """ INSERT INTO source_snapshots ( id, source_registry_id, snapshot_key, checksum_sha256, crs, units, freshness_status, ingest_status ) VALUES ( :id, :source_registry_id, :snapshot_key, :checksum_sha256, 'EPSG:4326', 'metres', 'current', 'ingested' ) """ ), { "id": snapshot_id, "source_registry_id": source_registry_id, "snapshot_key": key, "checksum_sha256": checksum, }, ) return snapshot_id def _accepted_report( *, contract_key: str = "geointel.vector.geojson", contract_version: str = "1.0.0" ) -> str: """Produce a structurally complete persisted validation report fixture.""" return json.dumps( { "asset_id": "phase2-guard-fixture", "data_contract_key": contract_key, "data_contract_version": contract_version, "contract_fingerprint_sha256": "d" * 64, "validation_status": "passed", "provenance_status": "complete", "lineage_status": "complete", "quarantine_status": "not_quarantined", "validation_scope": ["contract"], "report_sha256": "e" * 64, } ) def _insert_dataset( connection: Connection, *, project_id: UUID, source_registry_id: UUID, source_snapshot_id: UUID, suffix: str, checksum_sha256: str, ) -> UUID: dataset_id = uuid4() connection.execute( text( """ INSERT INTO datasets ( id, project_id, name, dataset_type, source, source_name, status, source_registry_id, source_snapshot_id, data_contract_key, data_contract_version, validation_report_json, validation_status, provenance_status, lineage_status, quarantine_status, checksum_sha256 ) VALUES ( :id, :project_id, :name, 'vector', 'governed', 'grb', 'ready', :source_registry_id, :source_snapshot_id, 'geointel.vector.geojson', '1.0.0', CAST(:validation_report_json AS json), 'passed', 'complete', 'complete', 'not_quarantined', :checksum_sha256 ) """ ), { "id": dataset_id, "project_id": project_id, "name": f"phase2-{suffix}.geojson", "source_registry_id": source_registry_id, "source_snapshot_id": source_snapshot_id, "validation_report_json": _accepted_report(), "checksum_sha256": checksum_sha256, }, ) return dataset_id def _assert_constraint_rejection( engine: Engine, statement: str, parameters: dict[str, Any], *, label: str ) -> None: try: with _transaction(engine) as connection: connection.execute(text(statement), parameters) except IntegrityError as exc: sqlstate = getattr(getattr(exc, "orig", None), "sqlstate", None) if sqlstate == "23514": return raise AssertionError( f"{label} failed with unexpected SQLSTATE {sqlstate!r}" ) from exc raise AssertionError(f"{label} was accepted unexpectedly") def verify(database_url: str, *, verify_downgrade: bool) -> dict[str, Any]: _require_disposable_database(database_url) started_at = datetime.now(UTC).isoformat() upgrade_output = _run_alembic(database_url, "upgrade", "head") engine = create_engine(database_url) try: edge_a = uuid4() edge_b = uuid4() with _transaction(engine) as connection: project_id = _insert_project(connection) grb_id = _source_id(connection, "grb") osm_id = _source_id(connection, "osm") grb_snapshot_id = _insert_snapshot( connection, grb_id, key="phase2-grb", checksum="a" * 64 ) osm_snapshot_id = _insert_snapshot( connection, osm_id, key="phase2-osm", checksum="b" * 64 ) derived_b_snapshot_id = _insert_snapshot( connection, grb_id, key="phase2-derived-b", checksum="c" * 64 ) derived_c_snapshot_id = _insert_snapshot( connection, grb_id, key="phase2-derived-c", checksum="d" * 64 ) mutation_snapshot_id = _insert_snapshot( connection, grb_id, key="phase2-mutation", checksum="e" * 64 ) dataset_a = _insert_dataset( connection, project_id=project_id, source_registry_id=grb_id, source_snapshot_id=grb_snapshot_id, suffix="a", checksum_sha256="a" * 64, ) dataset_b = _insert_dataset( connection, project_id=project_id, source_registry_id=grb_id, source_snapshot_id=derived_b_snapshot_id, suffix="b", checksum_sha256="c" * 64, ) dataset_c = _insert_dataset( connection, project_id=project_id, source_registry_id=grb_id, source_snapshot_id=derived_c_snapshot_id, suffix="c", checksum_sha256="d" * 64, ) shared_dataset = _insert_dataset( connection, project_id=project_id, source_registry_id=grb_id, source_snapshot_id=grb_snapshot_id, suffix="shared", checksum_sha256="a" * 64, ) mutation_dataset = _insert_dataset( connection, project_id=project_id, source_registry_id=grb_id, source_snapshot_id=mutation_snapshot_id, suffix="mutation", checksum_sha256="e" * 64, ) version_id = uuid4() connection.execute( text( """ INSERT INTO dataset_versions ( id, dataset_id, version, source_registry_id, source_snapshot_id, data_contract_key, data_contract_version, validation_report_json, validation_status, provenance_status, lineage_status, checksum_sha256 ) VALUES ( :id, :dataset_id, 1, :source_registry_id, :source_snapshot_id, 'geointel.vector.geojson', '1.0.0', CAST(:validation_report_json AS json), 'passed', 'complete', 'complete', :checksum_sha256 ) """ ), { "id": version_id, "dataset_id": dataset_a, "source_registry_id": grb_id, "source_snapshot_id": grb_snapshot_id, "validation_report_json": _accepted_report(), "checksum_sha256": "a" * 64, }, ) connection.execute( text( """ INSERT INTO dataset_lineage_edges ( id, parent_dataset_id, child_dataset_id, relation_type, transformation_name ) VALUES (:edge_a, :dataset_a, :dataset_b, 'derived_from', 'clip'), (:edge_b, :dataset_b, :dataset_c, 'derived_from', 'buffer') """ ), { "edge_a": edge_a, "edge_b": edge_b, "dataset_a": dataset_a, "dataset_b": dataset_b, "dataset_c": dataset_c, }, ) _assert_constraint_rejection( engine, """ INSERT INTO datasets ( id, project_id, name, dataset_type, source, source_name, status, source_registry_id, source_snapshot_id, validation_status, data_contract_key, data_contract_version, validation_report_json, provenance_status, lineage_status, quarantine_status, checksum_sha256 ) VALUES ( :id, :project_id, 'mismatched.geojson', 'vector', 'governed', 'grb', 'ready', :source_registry_id, :source_snapshot_id, 'passed', 'geointel.vector.geojson', '1.0.0', CAST(:validation_report_json AS json), 'complete', 'complete', 'not_quarantined', :checksum_sha256 ) """, { "id": uuid4(), "project_id": project_id, "source_registry_id": grb_id, "source_snapshot_id": osm_snapshot_id, "validation_report_json": _accepted_report(), "checksum_sha256": "b" * 64, }, label="snapshot-registry mismatch guard", ) _assert_constraint_rejection( engine, """ INSERT INTO datasets ( id, project_id, name, dataset_type, source, source_name, status, source_registry_id, source_snapshot_id, validation_status, provenance_status, lineage_status, quarantine_status, checksum_sha256 ) VALUES ( :id, :project_id, 'unreported.geojson', 'vector', 'governed', 'grb', 'ready', :source_registry_id, :source_snapshot_id, 'passed', 'complete', 'complete', 'not_quarantined', :checksum_sha256 ) """, { "id": uuid4(), "project_id": project_id, "source_registry_id": grb_id, "source_snapshot_id": grb_snapshot_id, "checksum_sha256": "a" * 64, }, label="passed contract report guard", ) _assert_constraint_rejection( engine, """ INSERT INTO datasets ( id, project_id, name, dataset_type, source, source_name, status, source_registry_id, source_snapshot_id, validation_status, data_contract_key, data_contract_version, validation_report_json, provenance_status, lineage_status, quarantine_status ) VALUES ( :id, :project_id, 'missing-checksum.geojson', 'vector', 'governed', 'grb', 'ready', :source_registry_id, :source_snapshot_id, 'passed', 'geointel.vector.geojson', '1.0.0', CAST(:validation_report_json AS json), 'complete', 'complete', 'not_quarantined' ) """, { "id": uuid4(), "project_id": project_id, "source_registry_id": grb_id, "source_snapshot_id": grb_snapshot_id, "validation_report_json": _accepted_report(), }, label="passed dataset checksum required guard", ) _assert_constraint_rejection( engine, """ INSERT INTO datasets ( id, project_id, name, dataset_type, source, source_name, status, source_registry_id, source_snapshot_id, validation_status, data_contract_key, data_contract_version, validation_report_json, provenance_status, lineage_status, quarantine_status, checksum_sha256 ) VALUES ( :id, :project_id, 'checksum-mismatch.geojson', 'vector', 'governed', 'grb', 'ready', :source_registry_id, :source_snapshot_id, 'passed', 'geointel.vector.geojson', '1.0.0', CAST(:validation_report_json AS json), 'complete', 'complete', 'not_quarantined', :checksum_sha256 ) """, { "id": uuid4(), "project_id": project_id, "source_registry_id": grb_id, "source_snapshot_id": grb_snapshot_id, "validation_report_json": _accepted_report(), "checksum_sha256": "f" * 64, }, label="passed dataset snapshot checksum binding guard", ) _assert_constraint_rejection( engine, """ INSERT INTO dataset_versions ( id, dataset_id, version, source_registry_id, source_snapshot_id, data_contract_key, data_contract_version, validation_report_json, validation_status, provenance_status, lineage_status ) VALUES ( :id, :dataset_id, 2, :source_registry_id, :source_snapshot_id, 'geointel.vector.geojson', '1.0.0', CAST(:validation_report_json AS json), 'passed', 'complete', 'complete' ) """, { "id": uuid4(), "dataset_id": dataset_a, "source_registry_id": grb_id, "source_snapshot_id": grb_snapshot_id, "validation_report_json": _accepted_report(), }, label="passed dataset version checksum required guard", ) _assert_constraint_rejection( engine, """ INSERT INTO dataset_versions ( id, dataset_id, version, source_registry_id, source_snapshot_id, data_contract_key, data_contract_version, validation_report_json, validation_status, provenance_status, lineage_status, checksum_sha256 ) VALUES ( :id, :dataset_id, 2, :source_registry_id, :source_snapshot_id, 'geointel.vector.geojson', '1.0.0', CAST(:validation_report_json AS json), 'passed', 'complete', 'complete', :checksum_sha256 ) """, { "id": uuid4(), "dataset_id": dataset_a, "source_registry_id": grb_id, "source_snapshot_id": grb_snapshot_id, "validation_report_json": _accepted_report(), "checksum_sha256": "f" * 64, }, label="passed dataset version snapshot checksum binding guard", ) _assert_constraint_rejection( engine, "UPDATE datasets SET data_contract_version = '9.9.9' WHERE id = :dataset_id", {"dataset_id": dataset_b}, label="accepted contract evidence immutability guard", ) _assert_constraint_rejection( engine, "UPDATE dataset_versions SET data_contract_version = '9.9.9' WHERE id = :dataset_version_id", {"dataset_version_id": version_id}, label="accepted dataset-version contract evidence immutability guard", ) _assert_constraint_rejection( engine, "UPDATE datasets SET storage_path = '/tampered/asset.geojson' WHERE id = :dataset_id", {"dataset_id": mutation_dataset}, label="accepted dataset artifact immutability guard", ) _assert_constraint_rejection( engine, "UPDATE datasets SET observed_at = CURRENT_TIMESTAMP WHERE id = :dataset_id", {"dataset_id": mutation_dataset}, label="accepted dataset temporal evidence immutability guard", ) _assert_constraint_rejection( engine, "UPDATE dataset_versions SET storage_path = '/tampered/version.geojson' WHERE id = :dataset_version_id", {"dataset_version_id": version_id}, label="accepted dataset-version artifact immutability guard", ) _assert_constraint_rejection( engine, """ UPDATE datasets SET validation_status = 'failed', checksum_sha256 = 'f' || repeat('0', 63) WHERE id = :dataset_id """, {"dataset_id": mutation_dataset}, label="accepted dataset requires invalidation before artifact replacement", ) with _transaction(engine) as connection: connection.execute( text( "UPDATE datasets SET validation_status = 'failed' WHERE id = :dataset_id" ), {"dataset_id": mutation_dataset}, ) connection.execute( text( "UPDATE datasets SET storage_path = '/replacement/asset.geojson' WHERE id = :dataset_id" ), {"dataset_id": mutation_dataset}, ) _assert_constraint_rejection( engine, "UPDATE source_snapshots SET source_registry_id = :registry_id WHERE id = :snapshot_id", {"registry_id": osm_id, "snapshot_id": grb_snapshot_id}, label="immutable snapshot registry guard", ) _assert_constraint_rejection( engine, "UPDATE source_registry SET display_name = 'tampered' WHERE id = :registry_id", {"registry_id": grb_id}, label="server-owned source registry guard", ) _assert_constraint_rejection( engine, "UPDATE source_snapshots SET checksum_sha256 = :checksum_sha256 WHERE id = :snapshot_id", {"checksum_sha256": "c" * 64, "snapshot_id": grb_snapshot_id}, label="immutable snapshot evidence guard", ) _assert_constraint_rejection( engine, """ INSERT INTO dataset_lineage_edges ( id, parent_dataset_id, child_dataset_id, relation_type, transformation_name ) VALUES (:id, :parent_dataset_id, :child_dataset_id, 'derived_from', 'cycle') """, { "id": uuid4(), "parent_dataset_id": dataset_c, "child_dataset_id": dataset_a, }, label="lineage cycle guard", ) _assert_constraint_rejection( engine, "UPDATE dataset_lineage_edges SET transformation_name = 'tampered' WHERE id = :id", {"id": edge_a}, label="lineage edge immutability update guard", ) _assert_constraint_rejection( engine, "DELETE FROM dataset_lineage_edges WHERE id = :id", {"id": edge_b}, label="lineage edge immutability delete guard", ) with _transaction(engine) as connection: connection.execute( text( """ INSERT INTO dataset_quarantines ( id, dataset_version_id, stage, reason_code, status ) VALUES (:id, :dataset_version_id, 'integration_test', 'CHECKSUM_MISMATCH', 'quarantined') """ ), {"id": uuid4(), "dataset_version_id": version_id}, ) dataset_state = ( connection.execute( text( """ SELECT status, quarantine_status, validation_status, provenance_status, lineage_status FROM datasets WHERE id = :dataset_id """ ), {"dataset_id": dataset_a}, ) .mappings() .one() ) version_state = ( connection.execute( text( """ SELECT validation_status, provenance_status, lineage_status FROM dataset_versions WHERE id = :dataset_version_id """ ), {"dataset_version_id": version_id}, ) .mappings() .one() ) snapshot_state = connection.execute( text( "SELECT ingest_status FROM source_snapshots WHERE id = :snapshot_id" ), {"snapshot_id": grb_snapshot_id}, ).scalar_one() child_dataset_state = ( connection.execute( text( """ SELECT status, quarantine_status, validation_status, provenance_status, lineage_status FROM datasets WHERE id = :dataset_id """ ), {"dataset_id": dataset_b}, ) .mappings() .one() ) grandchild_dataset_state = ( connection.execute( text( """ SELECT status, quarantine_status, validation_status, provenance_status, lineage_status FROM datasets WHERE id = :dataset_id """ ), {"dataset_id": dataset_c}, ) .mappings() .one() ) shared_dataset_state = ( connection.execute( text( """ SELECT status, quarantine_status, validation_status, provenance_status, lineage_status FROM datasets WHERE id = :dataset_id """ ), {"dataset_id": shared_dataset}, ) .mappings() .one() ) expected_dataset_state = { "status": "quarantined", "quarantine_status": "quarantined", "validation_status": "failed", "provenance_status": "incomplete", "lineage_status": "incomplete", } if dict(dataset_state) != expected_dataset_state: raise AssertionError( f"quarantine parent propagation mismatch: {dict(dataset_state)}" ) expected_version_state = { "validation_status": "failed", "provenance_status": "incomplete", "lineage_status": "incomplete", } if dict(version_state) != expected_version_state: raise AssertionError( f"quarantine version propagation mismatch: {dict(version_state)}" ) if snapshot_state != "quarantined": raise AssertionError( f"quarantine snapshot propagation mismatch: {snapshot_state}" ) if dict(child_dataset_state) != expected_dataset_state: raise AssertionError( "quarantine transitive-child propagation mismatch: " f"{dict(child_dataset_state)}" ) if dict(grandchild_dataset_state) != expected_dataset_state: raise AssertionError( "quarantine transitive-grandchild propagation mismatch: " f"{dict(grandchild_dataset_state)}" ) if dict(shared_dataset_state) != expected_dataset_state: raise AssertionError( "quarantine shared-snapshot propagation mismatch: " f"{dict(shared_dataset_state)}" ) finally: engine.dispose() downgrade_output = ( _run_alembic(database_url, "downgrade", "base") if verify_downgrade else "not requested" ) return { "schema_version": 1, "phase": "P2", "started_at": started_at, "completed_at": datetime.now(UTC).isoformat(), "migration_revision": "202608010001", "database_name": str(make_url(database_url).database), "result": "passed", "guards": { "snapshot_registry_pairing": "passed", "snapshot_registry_immutable": "passed", "source_registry_immutable": "passed", "snapshot_evidence_immutable": "passed", "lineage_cycle": "passed", "lineage_edge_immutable": "passed", "version_quarantine_propagation": "passed", "snapshot_quarantine_fanout": "passed", "transitive_lineage_quarantine": "passed", "passed_contract_report_required": "passed", "passed_dataset_checksum_required_and_snapshot_bound": "passed", "passed_dataset_version_checksum_required_and_snapshot_bound": "passed", "accepted_contract_evidence_immutable": "passed", "accepted_dataset_version_contract_evidence_immutable": "passed", "accepted_artifact_and_temporal_evidence_immutable": "passed", }, "upgrade_output_tail": upgrade_output.splitlines()[-8:], "downgrade_output_tail": downgrade_output.splitlines()[-8:], } def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--database-url", required=True) parser.add_argument( "--output", type=Path, default=ROOT / "artifacts" / "evidence" / "accuracy" / "P2" / "postgres-migration-guards.json", ) parser.add_argument("--skip-downgrade", action="store_true") args = parser.parse_args() try: result = verify(args.database_url, verify_downgrade=not args.skip_downgrade) except Exception as exc: result = { "schema_version": 1, "phase": "P2", "completed_at": datetime.now(UTC).isoformat(), "migration_revision": "202608010001", "result": "failed", "error_type": type(exc).__name__, "error": str(exc), } _json_dump(args.output, result) print(json.dumps(result, indent=2)) return 1 _json_dump(args.output, result) print(json.dumps(result, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())