test: чётность частей 07–09 с DBML, полнота 43 таблиц и 27 enum, partial unique consolidation_lock на PG16

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-03 19:32:48 +04:00
parent 48525731ff
commit ef55485dfc
2 changed files with 248 additions and 7 deletions

View File

@ -3,6 +3,7 @@
import datetime
import decimal
import uuid
import warnings
from dataclasses import dataclass
import pytest
@ -40,7 +41,6 @@ PART_04_06_TABLES = {
"financial_statement_line",
"financial_statement_value",
}
EXPECTED_TABLE_COUNT = 25 # на этом этапе: 16 (части 01–03) + 9 (части 04–06); задача 8 добавит остальные
def orm_tables() -> dict[str, Table]:
@ -48,8 +48,7 @@ def orm_tables() -> dict[str, Table]:
def test_part_04_06_tables_registered_in_metadata() -> None:
assert set(orm_tables()) == PART_01_03_TABLES | PART_04_06_TABLES
assert len(Base.metadata.sorted_tables) == EXPECTED_TABLE_COUNT
assert PART_01_03_TABLES | PART_04_06_TABLES <= set(orm_tables()) # полный состав и счёт — в тесте части 07–09
assert all(t.schema == "dkb" for t in Base.metadata.sorted_tables)
@ -78,7 +77,9 @@ def test_fk_cycle_is_broken_by_use_alter_without_warning() -> None:
job_fk = next(f for f in orm_tables()["recognition_job"].foreign_keys if f.parent.name == "file_id")
assert job_fk.constraint is not None
assert job_fk.constraint.use_alter is False
names = [t.name for t in Base.metadata.sorted_tables] # pytest -W error: SAWarning о цикле стал бы ошибкой
with warnings.catch_warnings():
warnings.simplefilter("error") # SAWarning о неразрешённом цикле зависимостей стал бы ошибкой
names = [t.name for t in Base.metadata.sorted_tables]
assert names.index("stored_file") < names.index("recognition_job")
@ -90,7 +91,7 @@ def test_document_unique_index_is_nulls_not_distinct() -> None:
@pytest.mark.integration
async def test_create_all_and_behavior_on_scratch_db(scratch_db_url: str) -> None:
"""`create_all` частей 01–06 (25 таблиц); NULLS NOT DISTINCT и Decimal работают на реальной PostgreSQL 16."""
"""`create_all`; таблицы частей 01–06 созданы; NULLS NOT DISTINCT и Decimal работают на реальной PostgreSQL 16."""
engine = create_async_engine(scratch_db_url)
try:
async with engine.begin() as conn:
@ -99,8 +100,7 @@ async def test_create_all_and_behavior_on_scratch_db(scratch_db_url: str) -> Non
async with engine.connect() as conn:
tables = await conn.run_sync(lambda c: set(inspect(c).get_table_names(schema="dkb")))
assert tables == PART_01_03_TABLES | PART_04_06_TABLES
assert len(tables) == EXPECTED_TABLE_COUNT
assert PART_01_03_TABLES | PART_04_06_TABLES <= tables
fk_names = {
row[0]
for row in await conn.execute(

View File

@ -0,0 +1,241 @@
"""ORM частей 07–09 DBML v5.13 (консолидация, КФО, аудит/ЕХ/прочее) и ИТОГОВАЯ полнота всех 43 таблиц."""
import datetime
import pytest
from sqlalchemy import inspect, text
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from src.models import Base
from src.models.access import AppUser
from src.models.consolidation import Consolidation, ConsolidationLock, ConsolidationVersion
from src.models.directories import CompanyGroup, RegionalOffice
from src.models.enums import (
ENUMS,
ConsolidationApproach,
ConsolidationScenario,
ConsolidationStatus,
PeriodQuarter,
)
from tests.dbml_helpers import load_dbml
from tests.dbml_parity import find_problems
from tests.test_models_part_01_03 import PART_01_03_TABLES, PG_IDENTIFIER_LIMIT
from tests.test_models_part_04_06 import PART_04_06_TABLES
PART_07_09_TABLES = {
# consolidation
"consolidation",
"consolidation_version",
"consolidation_status_history",
"consolidation_lock",
"consolidation_member",
"consolidation_document",
# kfo
"correction",
"goodwill",
"kfo_result",
"kfo_result_line",
"kfo_result_line_company",
"client_kfo_line",
# audit_ex (+ required_document_rule по рулингу R-4)
"comment",
"audit_event",
"ex_snapshot",
"ex_snapshot_member",
"ex_snapshot_item",
"required_document_rule",
}
ALL_TABLES = PART_01_03_TABLES | PART_04_06_TABLES | PART_07_09_TABLES
EXPECTED_TABLE_COUNT = 43
EXPECTED_ENUM_COUNT = 27
def test_part_07_09_tables_are_new() -> None:
assert PART_07_09_TABLES.isdisjoint(PART_01_03_TABLES | PART_04_06_TABLES)
def test_orm_matches_dbml_part_07_09() -> None:
"""Колонки (имя, порядок, тип, nullable, PK, unique, default), FK и индексы (вкл. partial WHERE) — как в DBML."""
assert find_problems(Base.metadata, PART_07_09_TABLES) == []
def test_all_constraint_names_fit_postgres_limit() -> None:
"""Имена всех ограничений и индексов всех таблиц metadata заданы и не длиннее 63 символов."""
for table in Base.metadata.sorted_tables:
for constraint in [*table.constraints, *table.indexes]:
assert constraint.name, f"{table.name}: безымянное ограничение"
assert len(str(constraint.name)) <= PG_IDENTIFIER_LIMIT, f"{table.name}: {constraint.name}"
def test_consolidation_lock_index_is_partial_unique_on_version() -> None:
table = Base.metadata.tables["dkb.consolidation_lock"]
index = next(i for i in table.indexes if i.name == "uq_consolidation_lock_version_active")
assert index.unique is True
assert [c.name for c in index.columns] == ["consolidation_version_id"]
assert str(index.dialect_options["postgresql"]["where"]) == "released_at IS NULL"
def test_composite_primary_keys() -> None:
tables = Base.metadata.tables
assert [c.name for c in tables["dkb.kfo_result_line_company"].primary_key] == [
"kfo_result_line_id",
"legal_entity_id",
]
assert [c.name for c in tables["dkb.ex_snapshot_member"].primary_key] == ["ex_snapshot_id", "legal_entity_id"]
def test_ex_snapshot_has_no_fk_to_consolidation() -> None:
"""v5.12: срез ЕХ самодостаточен — ни одна таблица ex_snapshot* не ссылается на консолидацию/КФО."""
consolidation_side = {
"consolidation",
"consolidation_version",
"consolidation_status_history",
"consolidation_lock",
"consolidation_member",
"consolidation_document",
"correction",
"goodwill",
"kfo_result",
"kfo_result_line",
"kfo_result_line_company",
"client_kfo_line",
}
for name in ("ex_snapshot", "ex_snapshot_member", "ex_snapshot_item"):
for fk in Base.metadata.tables[f"dkb.{name}"].foreign_keys:
assert fk.column.table.name not in consolidation_side, f"{name}.{fk.parent.name}"
assert not Base.metadata.tables["dkb.ex_snapshot"].c.source_version_id.foreign_keys
# ---------------------------------------------------------------- итоговая полнота моделей (43 таблицы)
def test_all_43_tables_registered_and_match_dbml_names() -> None:
"""Состав metadata == таблицы DBML v5.13 (имена буквально), всего 43, все в схеме dkb."""
tables = list(Base.metadata.sorted_tables)
assert len(tables) == EXPECTED_TABLE_COUNT
assert all(t.schema == "dkb" for t in tables)
dbml_names = {t.name for t in load_dbml().tables}
assert len(dbml_names) == EXPECTED_TABLE_COUNT
assert {t.name for t in tables} == dbml_names
assert ALL_TABLES == dbml_names # разбиение по частям покрывает модель без пропусков и дублей
def test_all_27_enums_defined() -> None:
assert len(ENUMS) == EXPECTED_ENUM_COUNT
assert set(ENUMS) == {e.name for e in load_dbml().enums}
def test_orm_matches_dbml_for_all_43_tables() -> None:
"""Сквозная сверка всех 43 таблиц одним вызовом компаратора."""
assert find_problems(Base.metadata, ALL_TABLES) == []
# ---------------------------------------------------------------- интеграция: PostgreSQL 16
@pytest.mark.integration
async def test_create_all_43_tables_on_scratch_db(scratch_db_url: str) -> None:
"""`create_all` всех 43 таблиц; 27 enum-типов в dkb; partial unique `consolidation_lock` работает поведением."""
engine = create_async_engine(scratch_db_url)
try:
async with engine.begin() as conn:
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS dkb"))
await conn.run_sync(Base.metadata.create_all)
async with engine.connect() as conn:
tables = await conn.run_sync(lambda c: set(inspect(c).get_table_names(schema="dkb")))
assert len(tables) == EXPECTED_TABLE_COUNT
assert tables == ALL_TABLES
enums = {
row[0]
for row in await conn.execute(
text(
"SELECT t.typname FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace "
"WHERE n.nspname = 'dkb' AND t.typtype = 'e'"
)
)
}
assert len(enums) == EXPECTED_ENUM_COUNT
assert enums == set(ENUMS)
indexdef = (
await conn.execute(
text(
"SELECT indexdef FROM pg_indexes "
"WHERE schemaname = 'dkb' AND indexname = 'uq_consolidation_lock_version_active'"
)
)
).scalar_one()
assert "UNIQUE" in indexdef
assert "WHERE (released_at IS NULL)" in indexdef
# у ex_snapshot.source_version_id нет FK; составные PK созданы
fk_columns = {
row[0]
for row in await conn.execute(
text(
"SELECT a.attname FROM pg_constraint c "
"JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY (c.conkey) "
"WHERE c.contype = 'f' AND c.conrelid = 'dkb.ex_snapshot'::regclass"
)
)
}
assert "source_version_id" not in fk_columns
pk = await conn.run_sync(lambda c: inspect(c).get_pk_constraint("ex_snapshot_member", schema="dkb"))
assert pk["constrained_columns"] == ["ex_snapshot_id", "legal_entity_id"]
async with AsyncSession(engine, expire_on_commit=False) as session:
await _check_active_lock_is_unique(session)
finally:
await engine.dispose()
async def _check_active_lock_is_unique(session: AsyncSession) -> None:
"""Два активных lock на одну версию -> IntegrityError; после `released_at` новый lock допустим."""
now = datetime.datetime.now(datetime.UTC)
office = RegionalOffice(name="ГО")
session.add(office)
await session.flush()
user = AppUser(full_name="Аналитик", regional_office_id=office.id)
group = CompanyGroup(code="G1", name="ГК", valid_from=now)
session.add_all([user, group])
await session.flush()
consolidation = Consolidation(
company_group_id=group.id, year=2025, quarter=PeriodQuarter.Q1, reporting_date=datetime.date(2025, 4, 1)
)
session.add(consolidation)
await session.flush()
version = ConsolidationVersion(
consolidation_id=consolidation.id,
version_no=1,
status=ConsolidationStatus.INITIATED,
scenario=ConsolidationScenario.CREATE,
initiator_id=user.id,
initiator_office_id=office.id,
)
session.add(version)
await session.flush()
await session.refresh(version)
assert version.approach == ConsolidationApproach.BASIC # server_default 'BASIC'
assert version.goodwill_enabled is False
assert version.is_in_work is True
version_id, user_id = version.id, user.id
await session.commit()
def lock() -> ConsolidationLock:
return ConsolidationLock(consolidation_version_id=version_id, locked_by=user_id, locked_at=now)
first = lock()
session.add(first)
await session.flush()
first_id = first.id
session.add(lock())
with pytest.raises(IntegrityError):
await session.flush() # второй активный lock на ту же версию
await session.rollback()
await session.execute(
text("UPDATE dkb.consolidation_lock SET released_at = :now WHERE id = :id"), {"now": now, "id": first_id}
)
session.add(lock()) # после снятия первой блокировки — можно
await session.flush()
await session.rollback()