From 3b110b6417c3715c536f8a8a7b4f19faef1f6cc8 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 20:27:15 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20alembic=20env.py=20=E2=80=94=20CREATE?= =?UTF-8?q?=20SCHEMA=20dkb=20=D0=B4=D0=BE=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D1=86=D1=8B=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B9,=20URL=20?= =?UTF-8?q?=D0=B8=D0=B7=20-x=20dburl,=20compare=5Fserver=5Fdefault?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Схема dkb создаётся и фиксируется до context.configure (онлайн) и печатается первой в офлайн-SQL. URL: -x dburl > sqlalchemy.url, заданный вызывающим кодом (не заглушка driver://) > настройки. Async-движок из URL напрямую (без configparser-интерполяции %). Co-Authored-By: Claude Fable 5.1 --- api/alembic/env.py | 117 +++++++++++++++++----------------- api/alembic/versions/.gitkeep | 0 2 files changed, 59 insertions(+), 58 deletions(-) delete mode 100644 api/alembic/versions/.gitkeep diff --git a/api/alembic/env.py b/api/alembic/env.py index 2a949b4..f2baf16 100644 --- a/api/alembic/env.py +++ b/api/alembic/env.py @@ -1,94 +1,95 @@ +"""Окружение Alembic: схема `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`. + +Схема `dkb` создаётся (`CREATE SCHEMA IF NOT EXISTS`) до таблицы версий `dkb.alembic_version` и до миграций. +""" + import asyncio from logging.config import fileConfig -from sqlalchemy import engine_from_config, pool -from sqlalchemy.ext.asyncio import AsyncEngine +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 # noqa: F401 — модели регистрируются в src/models/__init__.py +from src.models import Base # модели регистрируются в src/models/__init__.py + +SCHEMA = "dkb" +#: Значение `sqlalchemy.url` в шаблонном `alembic.ini` — признак «URL не задан вызывающим кодом». +INI_PLACEHOLDER_PREFIX = "driver://" -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. config = context.config -# Interpret the config file for Python logging. -# This line sets up loggers basically. if config.config_file_name is not None: fileConfig(config.config_file_name) -config.set_main_option("sqlalchemy.url", settings.OPENBAO.DATABASE.DATABASE_URL) -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata target_metadata = Base.metadata -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. + +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, type_, parent_names): +def include_name(name: str | None, type_: str, parent_names: object) -> bool: + """Autogenerate видит только схему `dkb` (схема по умолчанию `public` и прочие — не наши).""" if type_ == "schema": - return name == "dkb" + return name == SCHEMA return True -def do_run_migrations(connection) -> None: - context.configure( - connection=connection, - target_metadata=target_metadata, - compare_type=True, - version_table_schema="dkb", - include_schemas=True, - include_name=include_name, - ) - - with context.begin_transaction(): - context.run_migrations() +#: Опции, общие для онлайн- и офлайн-режима. `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: - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") + """Офлайн-режим (`--sql`): DDL печатается в вывод; `CREATE SCHEMA` идёт первым, до таблицы версий.""" context.configure( - url=url, - target_metadata=target_metadata, + url=database_url(), literal_binds=True, dialect_opts={"paramstyle": "named"}, - version_table_schema="dkb", - include_schemas=True, - include_name=include_name, + **CONFIGURE_OPTS, ) + with context.begin_transaction(): + context.execute(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}") + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + # Схема нужна до того, как alembic создаст `dkb.alembic_version`; фиксируем её отдельной транзакцией — + # иначе autobegin оставил бы соединение во «внешней» транзакции и alembic не стал бы коммитить миграции. + connection.execute(text(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")) + connection.commit() + + context.configure(connection=connection, **CONFIGURE_OPTS) + with context.begin_transaction(): context.run_migrations() async def run_migrations_online() -> None: - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connectable = AsyncEngine( - engine_from_config( - config.get_section(config.config_ini_section), prefix="sqlalchemy.", poolclass=pool.NullPool, future=True - ) - ) + """Онлайн-режим: async-движок (asyncpg), миграции в синхронном адаптере соединения.""" + connectable = create_async_engine(database_url(), poolclass=pool.NullPool) async with connectable.connect() as connection: await connection.run_sync(do_run_migrations) diff --git a/api/alembic/versions/.gitkeep b/api/alembic/versions/.gitkeep deleted file mode 100644 index e69de29..0000000