alembic fix
This commit is contained in:
114
backend/src/scripts/check_migration_chain.py
Normal file
114
backend/src/scripts/check_migration_chain.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# #region check_migration_chain [C:3] [TYPE Module] [SEMANTICS alembic,migration,validation,ci]
|
||||
# @BRIEF Validates Alembic migration chain integrity — checks for multiple heads,
|
||||
# broken chains, and missing down_revision references.
|
||||
# @RATIONALE Merge migrations (e.g., f0e9d8c7b6a5) are fragile — a new migration
|
||||
# with wrong down_revision can create multiple heads and break `alembic upgrade head`.
|
||||
# This script catches the issue before deployment.
|
||||
# @REJECTED Relying on `alembic upgrade head` to fail at deploy time was rejected —
|
||||
# it's better to catch migration chain issues in CI or pre-commit.
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure backend/src is importable
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
|
||||
# #region check_migration_heads [C:2] [TYPE Function]
|
||||
# @BRIEF Checks that there is exactly one migration head.
|
||||
# @POST Prints error and exits 1 if multiple heads found.
|
||||
def check_migration_heads() -> None:
|
||||
from alembic.config import Config as AlembicConfig
|
||||
from alembic.script import ScriptDirectory
|
||||
|
||||
alembic_cfg = AlembicConfig("alembic.ini")
|
||||
alembic_cfg.set_main_option("script_location", "alembic")
|
||||
|
||||
script_dir = ScriptDirectory.from_config(alembic_cfg)
|
||||
heads = list(script_dir.get_heads())
|
||||
|
||||
if len(heads) == 0:
|
||||
print("[migration-check] ERROR: No migration heads found — chain is empty")
|
||||
sys.exit(1)
|
||||
|
||||
if len(heads) > 1:
|
||||
print(f"[migration-check] ERROR: Multiple migration heads detected ({len(heads)}):")
|
||||
for h in heads:
|
||||
rev = script_dir.get_revision(h)
|
||||
print(f" - {h}: {rev.doc}")
|
||||
print("[migration-check] Fix: ensure new migrations reference the correct parent")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[migration-check] OK: Single migration head — {heads[0]}")
|
||||
|
||||
|
||||
# #endregion check_migration_heads
|
||||
|
||||
# #region check_migration_chain_integrity [C:2] [TYPE Function]
|
||||
# @BRIEF Walks the migration chain from head to root, checking all down_revision links exist.
|
||||
# @POST Prints error and exits 1 if a broken link is found.
|
||||
def check_migration_chain_integrity() -> None:
|
||||
from alembic.config import Config as AlembicConfig
|
||||
from alembic.script import ScriptDirectory
|
||||
|
||||
alembic_cfg = AlembicConfig("alembic.ini")
|
||||
alembic_cfg.set_main_option("script_location", "alembic")
|
||||
|
||||
script_dir = ScriptDirectory.from_config(alembic_cfg)
|
||||
heads = list(script_dir.get_heads())
|
||||
|
||||
if not heads:
|
||||
return # already checked above
|
||||
|
||||
visited = set()
|
||||
current_revs = heads
|
||||
|
||||
while current_revs:
|
||||
next_revs = []
|
||||
for rev_id in current_revs:
|
||||
if rev_id in visited:
|
||||
continue
|
||||
visited.add(rev_id)
|
||||
rev = script_dir.get_revision(rev_id)
|
||||
|
||||
down = rev.down_revision
|
||||
if down is None:
|
||||
continue # root migration
|
||||
|
||||
if isinstance(down, str):
|
||||
try:
|
||||
script_dir.get_revision(down)
|
||||
except Exception:
|
||||
print(f"[migration-check] ERROR: Broken link — {rev_id} -> {down} (not found)")
|
||||
sys.exit(1)
|
||||
next_revs.append(down)
|
||||
elif isinstance(down, tuple):
|
||||
# Merge migration — check all parents exist
|
||||
for parent in down:
|
||||
try:
|
||||
script_dir.get_revision(parent)
|
||||
except Exception:
|
||||
print(f"[migration-check] ERROR: Broken merge link — {rev_id} -> {parent} (not found)")
|
||||
sys.exit(1)
|
||||
next_revs.append(parent)
|
||||
|
||||
current_revs = next_revs
|
||||
|
||||
print(f"[migration-check] OK: Chain integrity verified — {len(visited)} migrations traversed")
|
||||
|
||||
|
||||
# #endregion check_migration_chain_integrity
|
||||
|
||||
# #region check_migration_main [C:2] [TYPE Function]
|
||||
# @BRIEF Entry point — runs all migration chain checks.
|
||||
def main() -> None:
|
||||
print("[migration-check] === Alembic Migration Chain Validation ===")
|
||||
check_migration_heads()
|
||||
check_migration_chain_integrity()
|
||||
print("[migration-check] === All checks passed ===")
|
||||
|
||||
|
||||
# #endregion check_migration_main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user