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
97 lines
4.1 KiB
Python
97 lines
4.1 KiB
Python
"""Схема БД конфигурируема (`DB_SCHEMA`, issue #38): миграции и приложение работают в схеме с нестандартным именем.
|
||
|
||
`SCHEMA` читается при импорте моделей, поэтому и Alembic, и «приложение» запускаются отдельными процессами с
|
||
`DB_SCHEMA=dkb_alt` в окружении (обычная переменная приоритетнее `.env.test`).
|
||
"""
|
||
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
from typing import Any
|
||
|
||
import pytest
|
||
from sqlalchemy import text
|
||
from sqlalchemy.ext.asyncio import AsyncConnection, create_async_engine
|
||
|
||
from tests.pg_helpers import API_DIR, alembic_async
|
||
from tests.test_models_part_07_09 import ALL_TABLES, EXPECTED_ENUM_COUNT
|
||
|
||
ALT = "dkb_alt"
|
||
ALT_ENV = {"DB_SCHEMA": ALT}
|
||
|
||
#: «Приложение» в схеме ALT: движок и сессия из `src.db.db` (search_path из настроек), запрос ORM без указания схемы.
|
||
APP_PROBE = """
|
||
import asyncio
|
||
from sqlalchemy import func, select, text
|
||
from src.db.db import session_transaction
|
||
from src.models import Base
|
||
from src.models.access import AppUser
|
||
|
||
async def main() -> None:
|
||
async with session_transaction() as session:
|
||
assert (await session.execute(text("SHOW search_path"))).scalar() == "dkb_alt"
|
||
assert (await session.execute(select(func.count()).select_from(AppUser))).scalar() == 0
|
||
assert Base.metadata.schema == "dkb_alt"
|
||
print("PROBE_OK")
|
||
|
||
asyncio.run(main())
|
||
"""
|
||
|
||
|
||
async def _scalar(conn: AsyncConnection, sql: str, **params: str) -> Any:
|
||
return (await conn.execute(text(sql), params)).scalar()
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_migrations_and_app_work_in_custom_schema(scratch_db_url: str) -> None:
|
||
"""upgrade head -> приложение читает таблицу через сессию -> downgrade base; схема `dkb` не создаётся."""
|
||
engine = create_async_engine(scratch_db_url)
|
||
try:
|
||
await alembic_async(scratch_db_url, "upgrade", "head", env=ALT_ENV)
|
||
async with engine.connect() as conn:
|
||
tables = set(
|
||
(
|
||
await conn.execute(
|
||
text("SELECT table_name FROM information_schema.tables WHERE table_schema = :s"), {"s": ALT}
|
||
)
|
||
).scalars()
|
||
)
|
||
assert tables == ALL_TABLES | {"alembic_version"}
|
||
namespaces = set((await conn.execute(text("SELECT nspname FROM pg_namespace"))).scalars())
|
||
assert "dkb" not in namespaces
|
||
enums = await _scalar(
|
||
conn,
|
||
"SELECT count(*) FROM pg_type WHERE typnamespace = CAST(:s AS regnamespace) AND typtype = 'e'",
|
||
s=ALT,
|
||
)
|
||
assert enums == EXPECTED_ENUM_COUNT
|
||
# все внешние ключи ведут в таблицы схемы ALT (строковые FK миграций тоже берут схему из настроек)
|
||
foreign_elsewhere = await _scalar(
|
||
conn,
|
||
"SELECT count(*) FROM pg_constraint WHERE contype = 'f' AND confrelid::regclass::text NOT LIKE :p",
|
||
p=f"{ALT}.%",
|
||
)
|
||
assert foreign_elsewhere == 0
|
||
|
||
probe = subprocess.run(
|
||
[sys.executable, "-c", APP_PROBE],
|
||
cwd=API_DIR,
|
||
capture_output=True,
|
||
text=True,
|
||
check=False,
|
||
env={**os.environ, **ALT_ENV, "OPENBAO__DATABASE__DATABASE_URL": scratch_db_url},
|
||
)
|
||
assert probe.returncode == 0 and "PROBE_OK" in probe.stdout, probe.stdout + probe.stderr
|
||
|
||
await alembic_async(scratch_db_url, "downgrade", "base", env=ALT_ENV)
|
||
async with engine.connect() as conn:
|
||
left = await _scalar(
|
||
conn,
|
||
"SELECT count(*) FROM pg_class WHERE relnamespace = CAST(:s AS regnamespace) "
|
||
"AND relname NOT IN ('alembic_version', 'alembic_version_pkc')",
|
||
s=ALT,
|
||
)
|
||
assert left == 0
|
||
finally:
|
||
await engine.dispose()
|