"""Alembic environment. Deliberately independent of velodrome.config.Settings: migrations run as a privileged/owner connection (VELODROME_DATABASE_URL_MIGRATE — a superuser or schema-owning role, which trivially satisfies "BYPASSRLS" since superusers always bypass RLS), never as either of the two runtime roles the app itself uses. Reading env vars directly here, rather than importing the app's settings, keeps a migration-only CI job from needing every runtime env var the app requires. """ import asyncio import os import sqlalchemy as sa from sqlalchemy.engine import Connection from sqlalchemy.ext.asyncio import async_engine_from_config from alembic import context from velodrome.models import Base config = context.config target_metadata = Base.metadata # postgis/postgis ships an entire pre-installed schema of its own (PostGIS core tables plus the # TIGER geocoder's tiger/topology schemas — dozens of tables) that Alembic never created and # doesn't manage. Without this filter, `alembic check`/autogenerate sees every single one as # "should be dropped" simply because it's not in our SQLAlchemy metadata — which would make CI's # `alembic check` step permanently useless (always red, for reasons that have nothing to do with # an actual drift). # # A denylist keyed on schema name is NOT reliable here: reflected foreign tables can come back # with `schema=None` on their Table object regardless of which schema they actually live in on # the server (confirmed against a real postgis/postgis:16-3.4 container — tables that `\dt` # clearly shows under the `tiger` schema still reflect with schema=None). An allowlist is the # robust version of the same idea: only ever compare tables OUR metadata declares, so a future # PostGIS/TIGER version adding more foreign tables can never cause a false positive here. def include_object( object: sa.schema.SchemaItem, name: str | None, type_: str, reflected: bool, compare_to: object ) -> bool: if type_ == "table": return name in target_metadata.tables return True def _migrate_url() -> str: url = os.environ.get("VELODROME_DATABASE_URL_MIGRATE") if not url: # Local-dev convenience only — every real environment (CI, deploy/) sets this explicitly. url = "postgresql+asyncpg://postgres:postgres@localhost:5432/velodrome" return url def run_migrations_offline() -> None: context.configure( url=_migrate_url(), target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, include_object=include_object, ) with context.begin_transaction(): context.run_migrations() def _do_run_migrations(connection: Connection) -> None: context.configure( connection=connection, target_metadata=target_metadata, include_object=include_object, ) with context.begin_transaction(): context.run_migrations() async def run_migrations_online() -> None: configuration = config.get_section(config.config_ini_section) or {} configuration["sqlalchemy.url"] = _migrate_url() connectable = async_engine_from_config(configuration, prefix="sqlalchemy.") 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())