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
739 lines
28 KiB
Python
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())
|