Andrey Epifancev 9e53fc64be Схема БД конфигурируемая: настройка DB_SCHEMA вместо зашитой dkb (#38)
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
2026-10-07 19:26:37 +04:00

113 lines
5.2 KiB
Python
Raw Permalink 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: схема `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())