arm-core/api/tests/test_db_schema.py
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

97 lines
4.1 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.

"""Схема БД конфигурируема (`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()