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