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