Files
GTI_GERENCIADOR_FISCAL/central/tests/test_migration_baseline.py
T
gtiandClaude Opus 5.5 a11209d123 fix: make the migration chain runnable from an empty database
0001 called Base.metadata.create_all(), building the schema from the
current models, so on a fresh PostgreSQL 0002 collided on
datasets.raw_sha256 and `alembic upgrade head` could never run from zero.

0001 is now an explicit, frozen baseline: the seven base tables exactly as
the real history created them, before 0002/0006 extended them. Databases
already past 0001 never re-run it. Proof: a fresh `upgrade head` produces a
schema identical (pg_dump --schema-only) to the long-lived dev database.

0019 repairs a latent bug found by that comparison: 0006 used
server_default="now()" as a string, which PostgreSQL froze at the instant
the column was created, so every Agent and Job got the same created_at.
It also adds three indexes the models declare but no migration created.

central/tests/test_migration_baseline.py guards (via AST) against
migrations importing the models or using string timestamp defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 00:30:51 -03:00

65 lines
2.4 KiB
Python

"""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 == []