from __future__ import annotations import sqlite3 import sys from pathlib import Path from alembic import command from alembic.config import Config PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from app.config import get_settings APP_BASELINE_REVISION = "20260420_03_app_auth_baseline" class AppDatabaseAdoptionError(RuntimeError): """Raised when the app database is missing or not managed as expected.""" def _database_path_from_url(database_url: str) -> Path: prefix = "sqlite:///" if not database_url.startswith(prefix): raise AppDatabaseAdoptionError( f"Only sqlite URLs are supported for app DB initialization, got: {database_url}" ) return Path(database_url[len(prefix) :]) def _make_alembic_config(database_url: str) -> Config: config = Config("alembic_app.ini") config.set_main_option("sqlalchemy.url", database_url) return config def _alembic_version_table_exists(database_path: Path) -> bool: conn = sqlite3.connect(database_path) try: row = conn.execute( "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'alembic_version'" ).fetchone() return row is not None finally: conn.close() def _fetch_alembic_revision(database_path: Path) -> str: conn = sqlite3.connect(database_path) try: row = conn.execute("SELECT version_num FROM alembic_version").fetchone() if row is None: raise AppDatabaseAdoptionError("Alembic version table exists but contains no revision") return row[0] finally: conn.close() def _list_user_tables(database_path: Path) -> list[str]: conn = sqlite3.connect(database_path) try: rows = conn.execute( """ SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' """ ).fetchall() return sorted(row[0] for row in rows) finally: conn.close() def validate_app_runtime_db(database_url: str) -> None: database_path = _database_path_from_url(database_url) if not database_path.exists(): raise AppDatabaseAdoptionError( "App DB file was not found. Run 'python scripts/app_db_adopt.py' first to " "initialize the app DB before starting the app." ) if not _alembic_version_table_exists(database_path): raise AppDatabaseAdoptionError( "App DB exists but is not yet Alembic-managed. Run " "'python scripts/app_db_adopt.py' first before starting the app." ) current_revision = _fetch_alembic_revision(database_path) if current_revision != APP_BASELINE_REVISION: raise AppDatabaseAdoptionError( "App DB revision mismatch. Refusing to start the app: " f"expected {APP_BASELINE_REVISION}, got {current_revision}" ) def adopt_or_initialize_app_db(database_url: str) -> str: database_path = _database_path_from_url(database_url) alembic_config = _make_alembic_config(database_url) if database_path.exists(): if _alembic_version_table_exists(database_path): current_revision = _fetch_alembic_revision(database_path) if current_revision != APP_BASELINE_REVISION: raise AppDatabaseAdoptionError( "App DB is already Alembic-managed but revision does not match " f"the expected baseline: expected {APP_BASELINE_REVISION}, " f"got {current_revision}" ) return "already_managed" existing_tables = _list_user_tables(database_path) if existing_tables: raise AppDatabaseAdoptionError( "App DB exists with unmanaged tables. Refusing to continue because there is " "no legacy app DB adoption path in this revision." ) database_path.parent.mkdir(parents=True, exist_ok=True) command.upgrade(alembic_config, "head") return "initialized" def main() -> None: settings = get_settings() result = adopt_or_initialize_app_db(settings.app_database_url) if result == "initialized": print("Initialized a new app DB via Alembic upgrade head.") else: print("App DB is already Alembic-managed at the expected baseline revision.") if __name__ == "__main__": main()