"""Окружение Alembic: схема `settings.DB_SCHEMA` (по умолчанию `dkb`), async-движок, URL БД из настроек или вызывающего. URL БД (по убыванию приоритета): 1. `alembic -x dburl= ...` — для тестов и разовых запусков против другой БД (временной, scratch); 2. `sqlalchemy.url` в Config, если вызывающий код задал его сам (`Config.set_main_option`), — всё, кроме шаблонной заглушки `driver://...` из `alembic.ini`; 3. `settings.OPENBAO.DATABASE.DATABASE_URL` (`.env` / `ENV_FILE`) — обычный `alembic upgrade head` / `make migrate`. Схема создаётся до таблицы версий `<схема>.alembic_version` и до миграций — только если её нет: PostgreSQL проверяет право CREATE на базу раньше, чем `IF NOT EXISTS`, а платформа может выдать роли миграций готовую схему без этого права. """ import asyncio from logging.config import fileConfig from sqlalchemy import Connection, pool, text from sqlalchemy.ext.asyncio import create_async_engine from alembic import context from src.core.config import settings from src.models import Base # модели регистрируются в src/models/__init__.py from src.models.base import SCHEMA #: Значение `sqlalchemy.url` в шаблонном `alembic.ini` — признак «URL не задан вызывающим кодом». INI_PLACEHOLDER_PREFIX = "driver://" #: Офлайн-вариант условного создания схемы: условие проверяется при применении SQL-скрипта, а не при его генерации. CREATE_SCHEMA_IF_MISSING_SQL = ( f"DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = '{SCHEMA}') " f"THEN CREATE SCHEMA {SCHEMA}; END IF; END $$" ) config = context.config if config.config_file_name is not None: fileConfig(config.config_file_name) target_metadata = Base.metadata def database_url() -> str: """URL БД: `-x dburl=...` > `sqlalchemy.url`, заданный вызывающим кодом > настройки приложения.""" x_url = context.get_x_argument(as_dictionary=True).get("dburl") if x_url: return x_url config_url = config.get_main_option("sqlalchemy.url") if config_url and not config_url.startswith(INI_PLACEHOLDER_PREFIX): return config_url return settings.OPENBAO.DATABASE.DATABASE_URL def include_name(name: str | None, type_: str, parent_names: object) -> bool: """Autogenerate видит только схему модели (`SCHEMA`) (схема по умолчанию `public` и прочие — не наши).""" if type_ == "schema": return name == SCHEMA return True #: Опции, общие для онлайн- и офлайн-режима. `compare_server_default` — чтобы autogenerate ловил расхождение #: server_default между ORM и БД (иначе изменение default'а проходит незамеченным). CONFIGURE_OPTS = { "target_metadata": target_metadata, "compare_type": True, "compare_server_default": True, "version_table_schema": SCHEMA, "include_schemas": True, "include_name": include_name, } def run_migrations_offline() -> None: """Офлайн-режим (`--sql`): DDL печатается в вывод; условный `CREATE SCHEMA` идёт первым, до таблицы версий.""" context.configure( url=database_url(), literal_binds=True, dialect_opts={"paramstyle": "named"}, **CONFIGURE_OPTS, ) with context.begin_transaction(): context.execute(CREATE_SCHEMA_IF_MISSING_SQL) context.run_migrations() def do_run_migrations(connection: Connection) -> None: # Схема нужна до того, как alembic создаст `<схема>.alembic_version`. CREATE — только если схемы нет (см. docstring # модуля). Транзакцию закрываем сразу: иначе autobegin оставил бы соединение во «внешней» транзакции и alembic # не стал бы коммитить миграции. exists = connection.execute(text("SELECT 1 FROM pg_namespace WHERE nspname = :name"), {"name": SCHEMA}).scalar() if exists is None: connection.execute(text(f"CREATE SCHEMA {SCHEMA}")) connection.commit() context.configure(connection=connection, **CONFIGURE_OPTS) with context.begin_transaction(): context.run_migrations() async def run_migrations_online() -> None: """Онлайн-режим: async-движок (asyncpg), миграции в синхронном адаптере соединения.""" connectable = create_async_engine(database_url(), poolclass=pool.NullPool) 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())