from pathlib import Path import json import os import sys ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from sqlalchemy import text from sqlalchemy.exc import SQLAlchemyError from src.db.engine import engine MIGRATIONS_DIR = ROOT / "migrations" def iter_migration_files() -> list[Path]: return sorted(path for path in MIGRATIONS_DIR.glob("*.sql") if path.is_file()) def split_sql_statements(sql: str) -> list[str]: statements = [] current = [] in_single_quote = False in_double_quote = False index = 0 while index < len(sql): char = sql[index] next_char = sql[index + 1] if index + 1 < len(sql) else "" if not in_single_quote and not in_double_quote and char == "-" and next_char == "-": while index < len(sql) and sql[index] != "\n": index += 1 continue if char == "'" and not in_double_quote: in_single_quote = not in_single_quote elif char == '"' and not in_single_quote: in_double_quote = not in_double_quote if char == ";" and not in_single_quote and not in_double_quote: statement = "".join(current).strip() if statement: statements.append(statement) current = [] index += 1 continue current.append(char) index += 1 statement = "".join(current).strip() if statement: statements.append(statement) return statements def apply_migration_file(connection, path: Path) -> int: sql = path.read_text(encoding="utf-8").strip() statements = split_sql_statements(sql) for statement in statements: connection.execute(text(statement)) return len(statements) def run_migrations(connection, files: list[Path]) -> list[dict]: results = [] for path in files: statement_count = apply_migration_file(connection, path) results.append({"file": path.name, "statements": statement_count}) return results def main(env=None) -> int: env = os.environ if env is None else env json_output = _get_bool(env, "MIGRATIONS_OUTPUT_JSON", False) files = iter_migration_files() if not files: summary = {"passed": True, "migration_count": 0, "results": []} _print_summary(summary, json_output=json_output) return 0 try: with engine.begin() as connection: results = run_migrations(connection=connection, files=files) except SQLAlchemyError as exc: summary = { "passed": False, "migration_count": len(files), "results": [], "error_type": exc.__class__.__name__, "error": str(exc), } _print_summary(summary, json_output=json_output) return 2 summary = {"passed": True, "migration_count": len(files), "results": results} _print_summary(summary, json_output=json_output) return 0 def _print_summary(summary: dict, json_output: bool) -> None: if json_output: print(json.dumps(summary, ensure_ascii=False, sort_keys=True)) return if not summary["results"] and summary["passed"]: print("no migration files found") for item in summary["results"]: if item["statements"]: print(f"applied {item['file']}; statements: {item['statements']}") if not summary["passed"]: print(f"Database unavailable for migrations: {summary.get('error_type')}") def _get_bool(env, key: str, default: bool) -> bool: value = env.get(key) if value is None or value == "": return default return str(value).strip().lower() in {"1", "true", "yes", "on"} if __name__ == "__main__": raise SystemExit(main())