"""Alembic environment — runs raw SQL migrations against SQLite or Postgres. No SQLAlchemy ORM is used. Migrations are plain SQL scripts so the migration layer stays independent of any Python model objects. """ from __future__ import annotations import asyncio import logging from logging.config import fileConfig from typing import Any from alembic import context from migrations.url_resolution import coerce_sqlite_async, resolve_database_url # Load the alembic.ini logging config without disabling existing loggers — # when running inside the FastAPI app the app's own loggers must not be reset. if context.config.config_file_name is not None: fileConfig(context.config.config_file_name, disable_existing_loggers=False) logger = logging.getLogger("alembic.env") def _resolve_url() -> str: """Resolve the database URL for this migration run. This is the SINGLE place both the offline and online paths get their URL from — `alembic.ini`'s `sqlalchemy.url` is deliberately never read directly anywhere else, so Alembic can never silently target a different database than the one the running app actually uses. The actual resolution order/logic lives in `migrations.url_resolution` (kept separate so it is unit-testable without an active Alembic context); this just wires Alembic's `-x` args and ini value into it. """ x_args = context.get_x_argument(as_dictionary=True) ini_url = context.config.get_main_option("sqlalchemy.url") url = resolve_database_url(x_args, ini_url) if not url: raise RuntimeError( "Could not resolve a database URL: no -x sqlalchemy.url, no " "APP_SQLITE_PATH-derived setting, and no alembic.ini sqlalchemy.url." ) return url def run_migrations_offline() -> None: """Emit SQL to stdout (used by `alembic upgrade --sql`).""" url = _resolve_url() logger.info("Resolved database URL (offline mode): %s", url) context.configure( url=url, target_metadata=None, literal_binds=True, dialect_opts={"paramstyle": "named"}, ) with context.begin_transaction(): context.run_migrations() def do_run_migrations(connection: Any) -> None: context.configure(connection=connection, target_metadata=None) with context.begin_transaction(): context.run_migrations() async def run_migrations_online() -> None: """Run migrations against a live DB connection.""" from sqlalchemy.ext.asyncio import create_async_engine url = coerce_sqlite_async(_resolve_url()) logger.info("Resolved database URL (online mode): %s", url) connectable = create_async_engine(url) async with connectable.connect() as connection: await connection.run_sync(do_run_migrations) await connectable.dispose() if context.is_offline_mode(): run_migrations_offline() else: asyncio.run(run_migrations_online())