| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463 |
- from __future__ import annotations
- import os
- import subprocess
- import uuid
- from pathlib import Path
- import psycopg2
- import pytest
- from sqlalchemy import create_engine, inspect
- from sqlalchemy.engine import make_url
- ROOT = Path(__file__).resolve().parents[1]
- EXPECTED_BASELINE_TABLES = {
- "data_orders",
- "data_products",
- "metadata_review_records",
- "metadata_version_history",
- "task_list",
- "users",
- }
- EXPECTED_UPGRADED_TABLES = {
- "datasource_credentials",
- "datasource_credential_audit_events",
- "workflow_schedules",
- "workflow_runs",
- "workflow_task_runs",
- "workflow_engine_bindings",
- "workflow_plan_audits",
- "runner_task_executions",
- "workflow_canary_evidence",
- "workflow_gateway_operations",
- "workflow_migration_states",
- "workflow_dual_runs",
- "workflow_reconciliation_reports",
- "workflow_cutover_operations",
- "rule_sql_staging_receipts",
- "ingestion_sources",
- "source_artifacts",
- "ingestion_jobs",
- "evidence_fragments",
- "extraction_candidates",
- "data_elements",
- "data_element_versions",
- "candidate_decisions",
- "ontologies",
- "ontology_versions",
- "ontology_domain_links",
- "ontology_change_sets",
- "ontology_publish_runs",
- "governance_responsibility_scopes",
- "governance_responsibility_assignments",
- "governance_responsibility_audit_events",
- "catalog_snapshots",
- "device_assets",
- "device_asset_source_mappings",
- "device_asset_versions",
- "device_semantic_codes",
- "device_semantic_code_versions",
- "device_semantic_code_reviews",
- }
- def test_data_research_ingestion_migration_is_additive_and_constrained():
- migration = (
- ROOT
- / "migrations"
- / "versions"
- / "20260722_100_data_research_ingestion.py"
- ).read_text(encoding="utf-8")
- assert 'revision = "20260722_100"' in migration
- assert 'down_revision = "20260720_100"' in migration
- for table in (
- "ingestion_sources",
- "source_artifacts",
- "ingestion_jobs",
- "evidence_fragments",
- "extraction_candidates",
- ):
- assert f"CREATE TABLE IF NOT EXISTS public.{table}" in migration
- assert "idempotency_key" in migration
- assert "parser_version" in migration
- assert "locator JSONB" in migration
- assert "DROP TABLE" not in migration.upper()
- def test_catalog_execution_migration_adds_attempts_and_snapshots():
- migration = (
- ROOT
- / "migrations"
- / "versions"
- / "20260729_280_catalog_ingestion_execution.py"
- ).read_text(encoding="utf-8")
- assert 'revision = "20260729_280"' in migration
- assert 'down_revision = "20260729_270"' in migration
- assert "ADD COLUMN attempt_count" in migration
- assert "ADD COLUMN failure_stage" in migration
- assert "CREATE TABLE public.catalog_snapshots" in migration
- assert "UNIQUE (job_uid, attempt)" in migration
- assert "DROP TABLE" not in migration.upper()
- def test_device_asset_catalog_migration_is_versioned_and_traceable():
- migration = (
- ROOT
- / "migrations"
- / "versions"
- / "20260729_290_device_asset_catalog.py"
- ).read_text(encoding="utf-8")
- assert 'revision = "20260729_290"' in migration
- assert 'down_revision = "20260729_280"' in migration
- for table in (
- "device_assets",
- "device_asset_source_mappings",
- "device_asset_versions",
- ):
- assert f"CREATE TABLE public.{table}" in migration
- assert (
- "UNIQUE (source_uid, source_entity, asset_type, source_code)"
- in migration
- )
- assert "UNIQUE (asset_uid, version)" in migration
- assert "source_updated_at" in migration
- assert "snapshot JSONB NOT NULL" in migration
- assert "DROP TABLE" not in migration.upper()
- def test_device_semantics_migration_is_versioned_reviewed_and_traceable():
- migration = (
- ROOT
- / "migrations"
- / "versions"
- / "20260729_300_device_semantics.py"
- ).read_text(encoding="utf-8")
- assert 'revision = "20260729_300"' in migration
- assert 'down_revision = "20260729_290"' in migration
- for table in (
- "device_semantic_codes",
- "device_semantic_code_versions",
- "device_semantic_code_reviews",
- ):
- assert f"CREATE TABLE public.{table}" in migration
- assert "UNIQUE (ontology_uid, code_type, canonical_code)" in migration
- assert "UNIQUE (code_uid, version)" in migration
- assert "source_mappings JSONB NOT NULL" in migration
- assert "evidence_uids JSONB NOT NULL" in migration
- assert "decision IN ('approve','reject')" in migration
- assert "DROP TABLE" not in migration.upper()
- def test_device_entity_resolution_migration_is_non_destructive_and_reversible():
- migration = (
- ROOT
- / "migrations"
- / "versions"
- / "20260729_310_device_entity_resolution.py"
- ).read_text(encoding="utf-8")
- assert 'revision = "20260729_310"' in migration
- assert 'down_revision = "20260729_300"' in migration
- for table in (
- "device_entity_match_candidates",
- "device_entity_match_reviews",
- "device_entity_merge_events",
- "device_entity_merge_rollbacks",
- ):
- assert f"CREATE TABLE public.{table}" in migration
- assert "explanation JSONB NOT NULL" in migration
- assert "evidence_uids JSONB NOT NULL" in migration
- assert "decision IN ('approve','reject','auto_approve')" in migration
- assert "canonical_asset_uid" in migration
- assert "member_asset_uid" in migration
- assert "snapshot JSONB NOT NULL" in migration
- assert "DROP TABLE" not in migration.upper()
- assert "DELETE FROM public.device_assets" not in migration
- assert "UPDATE public.device_asset_source_mappings" not in migration
- def test_data_element_migration_adds_versioned_governance_tables():
- migration = (
- ROOT
- / "migrations"
- / "versions"
- / "20260722_105_data_research_elements.py"
- ).read_text(encoding="utf-8")
- assert 'revision = "20260722_105"' in migration
- assert 'down_revision = "20260722_100"' in migration
- for table in ("data_elements", "data_element_versions", "candidate_decisions"):
- assert f"CREATE TABLE IF NOT EXISTS public.{table}" in migration
- assert "UNIQUE (data_element_uid, version)" in migration
- assert "DROP TABLE" not in migration.upper()
- def test_ontology_migration_adds_versioned_control_plane_tables():
- migration = (
- ROOT / "migrations" / "versions" / "20260722_110_data_research_ontology.py"
- ).read_text(encoding="utf-8")
- assert 'revision = "20260722_110"' in migration
- assert 'down_revision = "20260722_105"' in migration
- for table in (
- "ontologies",
- "ontology_versions",
- "ontology_domain_links",
- "ontology_change_sets",
- "ontology_publish_runs",
- ):
- assert f"CREATE TABLE IF NOT EXISTS public.{table}" in migration
- assert "UNIQUE (ontology_uid, version)" in migration
- assert "DROP TABLE" not in migration.upper()
- def test_alembic_configuration_is_environment_only():
- ini = (ROOT / "alembic.ini").read_text(encoding="utf-8")
- env = (ROOT / "migrations" / "env.py").read_text(encoding="utf-8")
- assert "sqlalchemy.url" not in ini
- assert "SQLALCHEMY_DATABASE_URI" in env
- assert "DATABASE_URL" in env
- assert "password" not in env.lower()
- def test_baseline_migration_is_non_destructive():
- baseline = (
- ROOT / "migrations" / "versions" / "20260716_01_baseline.py"
- ).read_text(encoding="utf-8")
- assert "def upgrade" in baseline
- assert "def downgrade" in baseline
- assert "drop_table" not in baseline
- for table in EXPECTED_BASELINE_TABLES:
- assert table in baseline
- @pytest.mark.integration
- def test_alembic_upgrade_is_repeatable_and_downgrade_preserves_tables():
- admin_url = os.environ.get("TEST_POSTGRES_ADMIN_URL")
- if not admin_url:
- pytest.skip("TEST_POSTGRES_ADMIN_URL is not configured")
- parsed = make_url(admin_url)
- database_name = f"dataops_migration_{uuid.uuid4().hex[:12]}"
- target_url = parsed.set(database=database_name).render_as_string(
- hide_password=False
- )
- connection = psycopg2.connect(admin_url)
- connection.autocommit = True
- try:
- with connection.cursor() as cursor:
- cursor.execute(f'CREATE DATABASE "{database_name}"')
- env = os.environ.copy()
- env["SQLALCHEMY_DATABASE_URI"] = target_url
- command = [str(ROOT / ".venv" / "bin" / "alembic"), "-c", "alembic.ini"]
- subprocess.run(command + ["upgrade", "head"], cwd=ROOT, env=env, check=True)
- subprocess.run(command + ["upgrade", "head"], cwd=ROOT, env=env, check=True)
- engine = create_engine(target_url)
- try:
- tables = set(inspect(engine).get_table_names(schema="public"))
- assert EXPECTED_BASELINE_TABLES | {"alembic_version"} <= tables
- assert tables >= EXPECTED_UPGRADED_TABLES
- finally:
- engine.dispose()
- subprocess.run(command + ["downgrade", "-1"], cwd=ROOT, env=env, check=True)
- engine = create_engine(target_url)
- try:
- tables = set(inspect(engine).get_table_names(schema="public"))
- assert tables >= EXPECTED_BASELINE_TABLES
- finally:
- engine.dispose()
- finally:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT pg_terminate_backend(pid) FROM pg_stat_activity "
- "WHERE datname = %s AND pid <> pg_backend_pid()",
- (database_name,),
- )
- cursor.execute(f'DROP DATABASE IF EXISTS "{database_name}"')
- connection.close()
- @pytest.mark.integration
- @pytest.mark.parametrize(
- ("legacy_rows", "diagnostic"),
- (
- ((1, 1), "duplicate rule_run_id"),
- ((101,), "rows above 100"),
- ),
- )
- def test_upgrade_from_150_rejects_malformed_legacy_samples(
- legacy_rows,
- diagnostic,
- ):
- admin_url = os.environ.get("TEST_POSTGRES_ADMIN_URL")
- if not admin_url:
- pytest.skip("TEST_POSTGRES_ADMIN_URL is not configured")
- parsed = make_url(admin_url)
- database_name = f"dataops_evidence_{uuid.uuid4().hex[:12]}"
- target_url = parsed.set(database=database_name).render_as_string(
- hide_password=False
- )
- admin = psycopg2.connect(admin_url)
- admin.autocommit = True
- try:
- with admin.cursor() as cursor:
- cursor.execute(f'CREATE DATABASE "{database_name}"')
- env = os.environ.copy()
- env["SQLALCHEMY_DATABASE_URI"] = target_url
- command = [
- str(ROOT / ".venv" / "bin" / "alembic"),
- "-c",
- "alembic.ini",
- ]
- subprocess.run(
- command + ["upgrade", "20260723_150"],
- cwd=ROOT,
- env=env,
- check=True,
- )
- database = psycopg2.connect(target_url)
- try:
- database.autocommit = True
- with database.cursor() as cursor:
- cursor.execute("SET session_replication_role = replica")
- run_id = str(uuid.uuid4())
- cursor.execute(
- """
- INSERT INTO public.rule_runs (
- id, deployment_id, component_binding_id,
- rule_version_id, plan_hash, status, correlation_id
- ) VALUES (%s, %s, %s, %s, %s, 'failed', %s)
- """,
- (
- run_id,
- str(uuid.uuid4()),
- str(uuid.uuid4()),
- str(uuid.uuid4()),
- "a" * 64,
- str(uuid.uuid4()),
- ),
- )
- for count in legacy_rows:
- cursor.execute(
- """
- INSERT INTO public.rule_violation_samples (
- id, rule_run_id, artifact_ref, sample_count,
- redaction_policy, expires_at
- ) VALUES (%s, %s, %s, %s, 'legacy-v1', NOW())
- """,
- (
- str(uuid.uuid4()),
- run_id,
- f"minio://legacy/{uuid.uuid4()}",
- count,
- ),
- )
- cursor.execute("SET session_replication_role = origin")
- finally:
- database.close()
- failed = subprocess.run(
- command + ["upgrade", "head"],
- cwd=ROOT,
- env=env,
- text=True,
- capture_output=True,
- )
- assert failed.returncode != 0
- assert diagnostic in (failed.stdout + failed.stderr)
- database = psycopg2.connect(target_url)
- try:
- with database.cursor() as cursor:
- cursor.execute(
- "SELECT version_num FROM public.alembic_version"
- )
- assert cursor.fetchone()[0] == "20260723_150"
- finally:
- database.close()
- finally:
- with admin.cursor() as cursor:
- cursor.execute(
- "SELECT pg_terminate_backend(pid) FROM pg_stat_activity "
- "WHERE datname = %s AND pid <> pg_backend_pid()",
- (database_name,),
- )
- cursor.execute(f'DROP DATABASE IF EXISTS "{database_name}"')
- admin.close()
- @pytest.mark.integration
- def test_concurrent_alembic_upgrades_are_serialized():
- admin_url = os.environ.get("TEST_POSTGRES_ADMIN_URL")
- if not admin_url:
- pytest.skip("TEST_POSTGRES_ADMIN_URL is not configured")
- parsed = make_url(admin_url)
- database_name = f"dataops_concurrent_{uuid.uuid4().hex[:12]}"
- target_url = parsed.set(database=database_name).render_as_string(
- hide_password=False
- )
- connection = psycopg2.connect(admin_url)
- connection.autocommit = True
- try:
- with connection.cursor() as cursor:
- cursor.execute(f'CREATE DATABASE "{database_name}"')
- env = os.environ.copy()
- env["SQLALCHEMY_DATABASE_URI"] = target_url
- command = [
- str(ROOT / ".venv" / "bin" / "alembic"),
- "-c",
- "alembic.ini",
- "upgrade",
- "head",
- ]
- processes = [
- subprocess.Popen(
- command,
- cwd=ROOT,
- env=env,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- text=True,
- )
- for _ in range(2)
- ]
- results = [process.communicate(timeout=120) for process in processes]
- failures = [
- output
- for process, (output, _) in zip(processes, results)
- if process.returncode != 0
- ]
- assert failures == []
- engine = create_engine(target_url)
- try:
- assert set(inspect(engine).get_table_names(schema="public")) >= (
- EXPECTED_UPGRADED_TABLES | {"alembic_version"}
- )
- finally:
- engine.dispose()
- finally:
- with connection.cursor() as cursor:
- cursor.execute(
- "SELECT pg_terminate_backend(pid) FROM pg_stat_activity "
- "WHERE datname = %s AND pid <> pg_backend_pid()",
- (database_name,),
- )
- cursor.execute(f'DROP DATABASE IF EXISTS "{database_name}"')
- connection.close()
|