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