Implement database migration

This commit is contained in:
2025-09-26 15:39:53 +03:00
Unverified
parent c89f0a5530
commit 8829a01e23
7 changed files with 177 additions and 4 deletions
+92
View File
@@ -0,0 +1,92 @@
from __future__ import annotations
import os
from pathlib import Path
from typing import Optional
from traceback import format_exc
import hashlib
from sqlalchemy.engine import Engine
from alembic import command
from alembic.config import Config
from models import Base
from constants import DATABASE_URL
MIGRATIONS_DIR = Path(__file__).resolve().parent / "migrations"
LOCK_FILE = MIGRATIONS_DIR / ".autogen.lock"
SCHEMA_HASH_FILE = MIGRATIONS_DIR / ".schema.hash"
def _ensure_alembic_layout() -> None:
"""Create a minimal Alembic environment if missing."""
versions = MIGRATIONS_DIR / "versions"
versions.mkdir(parents=True, exist_ok=True)
def _alembic_config() -> Config:
cfg = Config()
cfg.set_main_option("script_location", str(MIGRATIONS_DIR))
cfg.set_main_option("sqlalchemy.url", DATABASE_URL)
# Provide a minimal ini section so env.py can read config_ini_section
cfg.config_file_name = "alembic.ini"
cfg.set_section_option("alembic", "sqlalchemy.url", DATABASE_URL)
return cfg
def _model_schema_fingerprint() -> str:
"""Compute a deterministic fingerprint of the current SQLAlchemy model schema."""
parts: list[str] = []
md = Base.metadata
for table in sorted(md.tables.values(), key=lambda t: t.name):
parts.append(f"T:{table.name}")
for col in sorted(table.columns, key=lambda c: c.name):
col_type = str(col.type)
parts.append(f"C:{col.name}:{col_type}:N{int(bool(col.nullable))}")
digest = hashlib.sha256("|".join(parts).encode("utf-8")).hexdigest()
return digest
def run_auto_migration(engine: Engine) -> None:
"""Use Alembic to autogenerate and apply migrations automatically on startup."""
# Ensure env present
_ensure_alembic_layout()
cfg = _alembic_config()
try:
# Upgrade existing migrations (if any) first
command.upgrade(cfg, "head")
except Exception:
print("[alembic] upgrade to head failed:\n" + format_exc())
# Always attempt autogenerate only when model schema fingerprint changed
try:
# Avoid concurrent autogenerate on dev server reloads
try:
LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
fd = os.open(str(LOCK_FILE), os.O_CREAT | os.O_EXCL | os.O_RDWR)
os.close(fd)
have_lock = True
except FileExistsError:
have_lock = False
if have_lock:
try:
new_hash = _model_schema_fingerprint()
old_hash = SCHEMA_HASH_FILE.read_text(encoding="utf-8").strip() if SCHEMA_HASH_FILE.exists() else ""
if new_hash != old_hash:
command.revision(cfg, message="auto", autogenerate=True)
command.upgrade(cfg, "head")
# Update stored fingerprint
SCHEMA_HASH_FILE.write_text(new_hash, encoding="utf-8")
finally:
try:
LOCK_FILE.unlink(missing_ok=True)
except Exception:
pass
except Exception:
print("[alembic] autogenerate failed:\n" + format_exc())