diff --git a/apps/api/alembic/env.py b/apps/api/alembic/env.py index cf5ad7d..6b3be69 100644 --- a/apps/api/alembic/env.py +++ b/apps/api/alembic/env.py @@ -2,7 +2,8 @@ import os from logging.config import fileConfig from urllib.parse import unquote -from sqlalchemy import engine_from_config, pool +from sqlalchemy import create_engine, pool +from sqlalchemy.engine.url import make_url from alembic import context from src.models import Base @@ -13,10 +14,9 @@ config = context.config # Override sqlalchemy.url from environment if set database_url = os.environ.get("DATABASE_URL") if database_url: - # Alembic needs the synchronous driver + # Alembic needs the synchronous driver; asyncpg -> psycopg2 database_url = database_url.replace("postgresql+asyncpg://", "postgresql://") - # Decode URL-encoded characters (e.g. %40 -> @) so ConfigParser - # interpolation doesn't choke on them + # Decode URL-encoded characters (e.g. %40 -> @) database_url = unquote(database_url) config.set_main_option("sqlalchemy.url", database_url) @@ -42,12 +42,16 @@ def run_migrations_offline() -> None: def run_migrations_online() -> None: - """Run migrations in 'online' mode.""" - connectable = engine_from_config( - config.get_section(config.config_ini_section, {}), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) + """Run migrations in 'online' mode. + + Use create_engine directly to bypass ConfigParser URL parsing + which mishandles special characters in passwords. + """ + url = config.get_main_option("sqlalchemy.url") + # Ensure dialect is postgresql (not postgres) + if url.startswith("postgres://"): + url = "postgresql://" + url[len("postgres://"):] + connectable = create_engine(url, poolclass=pool.NullPool) with connectable.connect() as connection: context.configure(