arm-core/api/tests/test_db_schema.py
Andrey Epifancev 251c30d5ca feat(#190): DBML v5.25 — ORM и миграция фактов качества первички
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-05 15:49:15 +04:00

119 lines
5.6 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; `QUALITY_METRICS_SCHEMA`, v5.25): миграции и приложение работают
в схемах с нестандартными именами.
`SCHEMA`/`METRICS_SCHEMA` читаются при импорте моделей, поэтому и Alembic, и «приложение» запускаются отдельными
процессами с `DB_SCHEMA=dkb_alt`/`QUALITY_METRICS_SCHEMA=dkb_metrics_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, METRICS_TABLES
ALT = "dkb_alt"
METRICS_ALT = "dkb_metrics_alt"
ALT_ENV = {"DB_SCHEMA": ALT, "QUALITY_METRICS_SCHEMA": METRICS_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
from src.models.base import METRICS_SCHEMA
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"
assert METRICS_SCHEMA == "dkb_metrics_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`/`dkb_metrics` не создаются
(обе настройки — DB_SCHEMA и QUALITY_METRICS_SCHEMA, v5.25 — переопределены именами ALT/METRICS_ALT)."""
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 - METRICS_TABLES) | {"alembic_version"}
metrics_tables = set(
(
await conn.execute(
text("SELECT table_name FROM information_schema.tables WHERE table_schema = :s"),
{"s": METRICS_ALT},
)
).scalars()
)
assert metrics_tables == METRICS_TABLES
namespaces = set((await conn.execute(text("SELECT nspname FROM pg_namespace"))).scalars())
assert "dkb" not in namespaces
assert "dkb_metrics" not in namespaces # имя по умолчанию не прошито — переопределено METRICS_ALT
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 # все enum'ы (вкл. v5.25) — в SCHEMA/ALT, не в METRICS_SCHEMA
# все внешние ключи ведут в таблицы схемы ALT (строковые FK миграций тоже берут схему из настроек);
# у таблиц METRICS_ALT FK нет вовсе (design §4), поэтому это не сужает проверку
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
# METRICS_ALT остаётся (пустой) после отката — симметрично тому, что ALT сам не удаляется (0012/0001)
metrics_left = await _scalar(
conn, "SELECT count(*) FROM pg_class WHERE relnamespace = CAST(:s AS regnamespace)", s=METRICS_ALT
)
assert metrics_left == 0
finally:
await engine.dispose()