"""Guards for the migration chain being runnable from an empty database. 0001 used to build the schema from the *current* ORM models, so every later migration collided with it on a fresh database. These checks run offline (no PostgreSQL) and inspect code, not comments, so they fail if that pattern comes back. """ import ast import re from pathlib import Path from alembic.config import Config from alembic.script import ScriptDirectory ROOT = Path(__file__).parents[2] VERSIONS = ROOT / "central" / "alembic" / "versions" def _migrations(): return {path.name: ast.parse(path.read_text(encoding="utf-8")) for path in sorted(VERSIONS.glob("*.py"))} def _script_directory(): config = Config(str(ROOT / "central" / "alembic.ini")) config.set_main_option("script_location", str(ROOT / "central" / "alembic")) return ScriptDirectory.from_config(config) def test_no_migration_builds_schema_from_current_models(): offenders = [] for name, tree in _migrations().items(): for node in ast.walk(tree): if isinstance(node, ast.Attribute) and node.attr in ("create_all", "drop_all"): offenders.append((name, node.attr)) if isinstance(node, ast.ImportFrom) and (node.module or "").startswith("central.app"): offenders.append((name, node.module)) assert offenders == [] def test_chain_is_linear_with_a_single_head(): script = _script_directory() assert len(script.get_heads()) == 1 revisions = list(script.walk_revisions()) assert revisions[-1].revision == "0001" assert revisions[-1].down_revision is None def test_timestamp_defaults_are_sql_functions_not_string_literals(): # A plain string such as "now()" is rendered as a quoted literal that # PostgreSQL freezes at column creation time. 0006 did that and 0019 # repairs it; no other migration may repeat it. offenders = [] for name, tree in _migrations().items(): if name.startswith("0006_"): continue for node in ast.walk(tree): if ( isinstance(node, ast.keyword) and node.arg == "server_default" and isinstance(node.value, ast.Constant) and re.fullmatch(r"\s*(now\s*\(\s*\)|now|current_timestamp|localtimestamp)\s*", str(node.value.value), re.IGNORECASE) ): offenders.append(name) assert offenders == []