"""Окружение Alembic (асинхронное, на том же asyncpg, что и приложение).""" from __future__ import annotations import asyncio from logging.config import fileConfig from alembic import context from sqlalchemy import pool from sqlalchemy.engine import Connection from sqlalchemy.ext.asyncio import create_async_engine from app.core.config import settings # Импорт реестра моделей обязателен: без него target_metadata пуста # и автогенерация «увидит» удаление всех таблиц. from app.db.models import Base # noqa: F401 config = context.config # DSN идёт напрямую в create_async_engine(), а не через # config.set_main_option()/alembic.ini: Alembic хранит sqlalchemy.url в # ConfigParser, для которого "%" — служебный символ интерполяции. Пароль # от Postgres после percent-encoding как раз может содержать "%XX" и ломает # ConfigParser.set() ещё до запуска миграций. if config.config_file_name is not None: fileConfig(config.config_file_name) target_metadata = Base.metadata def _configure(connection: Connection) -> None: context.configure( connection=connection, target_metadata=target_metadata, compare_type=True, # ловить смену типа колонки compare_server_default=True, render_as_batch=False, ) def run_migrations_offline() -> None: """Генерация SQL без подключения к БД (`alembic upgrade head --sql`).""" context.configure( url=settings.database_url, target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, ) with context.begin_transaction(): context.run_migrations() def _run_sync(connection: Connection) -> None: _configure(connection) with context.begin_transaction(): context.run_migrations() async def run_migrations_online() -> None: connectable = create_async_engine(settings.database_url, poolclass=pool.NullPool) async with connectable.connect() as connection: await connection.run_sync(_run_sync) await connectable.dispose() if context.is_offline_mode(): run_migrations_offline() else: asyncio.run(run_migrations_online())