Andrey Epifancev 3b110b6417 feat: alembic env.py — CREATE SCHEMA dkb до таблицы версий, URL из -x dburl, compare_server_default
Схема dkb создаётся и фиксируется до context.configure (онлайн) и печатается первой в офлайн-SQL.
URL: -x dburl > sqlalchemy.url, заданный вызывающим кодом (не заглушка driver://) > настройки.
Async-движок из URL напрямую (без configparser-интерполяции %).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 20:27:15 +04:00

104 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Окружение Alembic: схема `dkb`, async-движок, URL БД из настроек или из вызывающего кода.
URL БД (по убыванию приоритета):
1. `alembic -x dburl=<url> ...` — для тестов и разовых запусков против другой БД (временной, 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 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
SCHEMA = "dkb"
#: Значение `sqlalchemy.url` в шаблонном `alembic.ini` — признак «URL не задан вызывающим кодом».
INI_PLACEHOLDER_PREFIX = "driver://"
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 видит только схему `dkb` (схема по умолчанию `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(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:
"""Онлайн-режим: 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())