Initial import with GitLab CI/CD and registry deploy flow
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Mapping
|
||||
|
||||
from sqlalchemy import inspect, text
|
||||
|
||||
from services.shared.db import engine
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MIGRATIONS_DIR = ROOT / "migrations" / "sql"
|
||||
_MODE_ENV = "SCHEMA_MANAGEMENT_MODE"
|
||||
_VALID_MODES = {"legacy", "migrations"}
|
||||
|
||||
|
||||
def database_backend_name(db_engine=engine) -> str:
|
||||
return db_engine.url.get_backend_name()
|
||||
|
||||
|
||||
def migration_suffix(db_engine=engine) -> str:
|
||||
return "postgres" if database_backend_name(db_engine) == "postgresql" else "sqlite"
|
||||
|
||||
|
||||
def list_migration_files(*, db_engine=engine, migrations_dir: Path = MIGRATIONS_DIR) -> list[Path]:
|
||||
return sorted(migrations_dir.glob(f"*_{migration_suffix(db_engine)}.sql"))
|
||||
|
||||
|
||||
def resolve_schema_management_mode(
|
||||
raw_mode: str | None,
|
||||
*,
|
||||
backend_name: str,
|
||||
) -> str:
|
||||
normalized = str(raw_mode or "").strip().lower()
|
||||
if not normalized:
|
||||
normalized = "legacy" if backend_name == "sqlite" else "migrations"
|
||||
if normalized not in _VALID_MODES:
|
||||
allowed = ", ".join(sorted(_VALID_MODES))
|
||||
raise RuntimeError(f"{_MODE_ENV} must be one of: {allowed}")
|
||||
if backend_name != "sqlite" and normalized != "migrations":
|
||||
raise RuntimeError(
|
||||
f"{_MODE_ENV}=legacy is supported only for SQLite. "
|
||||
"Run `python scripts/migrate_core_db.py` and use SCHEMA_MANAGEMENT_MODE=migrations."
|
||||
)
|
||||
return normalized
|
||||
|
||||
|
||||
def schema_management_mode(
|
||||
*,
|
||||
db_engine=engine,
|
||||
environ: Mapping[str, str] | None = None,
|
||||
) -> str:
|
||||
source = environ or os.environ
|
||||
return resolve_schema_management_mode(
|
||||
source.get(_MODE_ENV),
|
||||
backend_name=database_backend_name(db_engine),
|
||||
)
|
||||
|
||||
|
||||
def ensure_schema_migrations_table(*, db_engine=engine) -> None:
|
||||
with db_engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version TEXT PRIMARY KEY,
|
||||
applied_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def has_schema_migrations_table(*, db_engine=engine) -> bool:
|
||||
return inspect(db_engine).has_table("schema_migrations")
|
||||
|
||||
|
||||
def applied_migration_versions(*, db_engine=engine) -> set[str]:
|
||||
if not has_schema_migrations_table(db_engine=db_engine):
|
||||
return set()
|
||||
with db_engine.begin() as conn:
|
||||
rows = conn.execute(text("SELECT version FROM schema_migrations")).fetchall()
|
||||
return {str(row[0]) for row in rows}
|
||||
|
||||
|
||||
def missing_migration_versions(*, db_engine=engine, migrations_dir: Path = MIGRATIONS_DIR) -> list[str]:
|
||||
expected = [path.name for path in list_migration_files(db_engine=db_engine, migrations_dir=migrations_dir)]
|
||||
applied = applied_migration_versions(db_engine=db_engine)
|
||||
return [name for name in expected if name not in applied]
|
||||
|
||||
|
||||
def validate_schema_migrations_applied(*, db_engine=engine, migrations_dir: Path = MIGRATIONS_DIR) -> None:
|
||||
expected = list_migration_files(db_engine=db_engine, migrations_dir=migrations_dir)
|
||||
if not expected:
|
||||
raise RuntimeError(f"No migration files found for backend {database_backend_name(db_engine)} in {migrations_dir}")
|
||||
|
||||
if not has_schema_migrations_table(db_engine=db_engine):
|
||||
raise RuntimeError(
|
||||
"Database schema is not initialized via migrations. "
|
||||
"Run `python scripts/migrate_core_db.py` before starting services."
|
||||
)
|
||||
|
||||
missing = missing_migration_versions(db_engine=db_engine, migrations_dir=migrations_dir)
|
||||
if not missing:
|
||||
return
|
||||
|
||||
preview = ", ".join(missing[:5])
|
||||
remainder = "" if len(missing) <= 5 else f" ... (+{len(missing) - 5} more)"
|
||||
raise RuntimeError(
|
||||
"Database schema is behind the checked-in migrations. "
|
||||
f"Missing: {preview}{remainder}. "
|
||||
"Run `python scripts/migrate_core_db.py` before starting services."
|
||||
)
|
||||
Reference in New Issue
Block a user