Files
geointel/scripts/verify_phase2_postgres_guards.py
T
Jens faeb58ef6d
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
Initial public release
2026-08-31 21:56:53 +02:00

739 lines
28 KiB
Python

#!/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())