DB_SCHEMA (по умолчанию dkb, валидация идентификатора) читают models/base.py (SCHEMA), db/db.py (search_path),
alembic/env.py и миграции 0001-0005 (SCHEMA и строковые FK f"{SCHEMA}.x.id" вместо литералов). В контуре -
OPENBAO__APP__DB_SCHEMA. Офлайн-SQL `alembic upgrade head --sql` при значении по умолчанию совпадает побайтно.
Тесты: валидация настройки, миграции и сессия приложения в схеме dkb_alt; .env.example/.env.test/README/CLAUDE.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
113 lines
5.2 KiB
Python
113 lines
5.2 KiB
Python
"""Окружение Alembic: схема `settings.DB_SCHEMA` (по умолчанию `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`.
|
||
|
||
Схема создаётся до таблицы версий `<схема>.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())
|