From d5d09d0b5cc239b12dee3fd28b7378e32e7e245d Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 10 Oct 2026 01:03:26 +0400 Subject: [PATCH] =?UTF-8?q?model:=20DBML=20v5.18=20=E2=80=94=20stored=5Ffi?= =?UTF-8?q?le.consolidation=5Fid,=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=200006=20=D1=81=20backfill=20(#96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 2 +- Makefile | 2 +- README.md | 8 +- api/alembic/versions/0006_dbml_v5_18.py | 52 ++++++++ api/scripts/check_dbml_vs_orm.py | 2 +- api/scripts/dbml_parity.py | 2 +- api/src/models/__init__.py | 4 +- api/src/models/enums.py | 2 +- api/src/models/files.py | 5 + api/tests/test_check_dbml_script.py | 14 +-- api/tests/test_enums.py | 4 +- api/tests/test_migrations.py | 161 +++++++++++++++++++++++- 12 files changed, 233 insertions(+), 25 deletions(-) create mode 100644 api/alembic/versions/0006_dbml_v5_18.py diff --git a/CLAUDE.md b/CLAUDE.md index 5fe14f9..7ae139e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -159,7 +159,7 @@ - NATS subjects и схема ключей S3 — `../docs/context/03-architecture.md`. ## Правила репозитория -- ORM 1:1 с `../docs/model/data-model-v5.17.dbml` (текущая версия; миграции `0001_dbml_v5_13` + `0002_dbml_v5_14` + `0003_dbml_v5_15` + `0004_dbml_v5_16` + `0005_dbml_v5_17`); `api/scripts/check_dbml_vs_orm.py` (`make dbml-check`, входит в `check-full`) — обязательная проверка. +- ORM 1:1 с `../docs/model/data-model-v5.18.dbml` (текущая версия; миграции `0001_dbml_v5_13` + `0002_dbml_v5_14` + `0003_dbml_v5_15` + `0004_dbml_v5_16` + `0005_dbml_v5_17` + `0006_dbml_v5_18`); `api/scripts/check_dbml_vs_orm.py` (`make dbml-check`, входит в `check-full`) — обязательная проверка. - Алгоритмы — только из `../docs/context/05-domain-rules.md`; номер § — в docstring. - Вычисляемые объекты консолидации — `api/src/domain/`, без ORM-таблиц. - Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR. diff --git a/Makefile b/Makefile index be5b416..65ebfcc 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ VENV := api/.venv BIN := $(CURDIR)/$(VENV)/bin COMPOSE_FILE ?= ../docs/dev/docker-compose.yml # DBML относительно api/: и в основном workspace, и в worktree .wt/<срез>/ это соседний docs/ -DBML ?= ../../docs/model/data-model-v5.17.dbml +DBML ?= ../../docs/model/data-model-v5.18.dbml .PHONY: venv lint test test-integration e2e dbml-check migration-cycle check check-infra docker-build web-check check-full up migrate seed seed-dev diff --git a/README.md b/README.md index 1d6be1e..7c9a9bb 100644 --- a/README.md +++ b/README.md @@ -952,7 +952,7 @@ email без учёта регистра проверяет только сер | `make test` | юнит-тесты: `pytest -m "not integration"`, Docker не нужен | | `make test-integration` | интеграционные тесты: `pytest -m "integration and not e2e"`, нужны PostgreSQL (и NATS) | | `make e2e` | сквозной прогон загрузки ОСВ: `pytest -m e2e`, нужны compose и tabular-parser из профиля `services` (без него — skip) | -| `make dbml-check` | сверка ORM с `../docs/model/data-model-v5.17.dbml` | +| `make dbml-check` | сверка ORM с `../docs/model/data-model-v5.18.dbml` | | `make migration-cycle` | миграции на временной БД: `upgrade head` → `downgrade base` → `upgrade head`, пустой autogenerate | | `make check` | быстрая проверка без Docker: `lint` + `test` | | `make docker-build` | сборка образа: `docker build -t arm-core .` | @@ -1004,7 +1004,7 @@ queue group `arm-core`, подписчики группы получают со ### Проверка ORM и DBML -Модель данных — `../docs/model/data-model-v5.17.dbml`. ORM обязана совпадать с ней один в один: +Модель данных — `../docs/model/data-model-v5.18.dbml`. ORM обязана совпадать с ней один в один: ```bash make dbml-check # пустой вывод и код 0 — совпадение; расхождения печатаются построчно @@ -1027,7 +1027,7 @@ make migration-cycle # проверить цикл upgr ``` api/ run_server.py точка входа (uvicorn; хост и порт — из настроек) - alembic/ миграции (0001_dbml_v5_13.py — модель v5.13, 0002_dbml_v5_14.py — v5.14, 0003_dbml_v5_15.py — v5.15, 0004_dbml_v5_16.py — v5.16, 0005_dbml_v5_17.py — v5.17) + alembic/ миграции (0001_dbml_v5_13.py — модель v5.13, 0002_dbml_v5_14.py — v5.14, 0003_dbml_v5_15.py — v5.15, 0004_dbml_v5_16.py — v5.16, 0005_dbml_v5_17.py — v5.17, 0006_dbml_v5_18.py — v5.18) rules/ statement_checks.yaml — словарь проверок §7 формы ББ/ОФР (копия словаря tabular-parser; меняется в обоих репозиториях) seed/core/ справочники продукта (роли, права, филиалы, план счетов, правила элиминации, чек-лист) seed/dev/ dev-данные: филиалы и пользователи по ролям @@ -1111,7 +1111,7 @@ Dockerfile только для локального compose - [`../docs/dev/README.md`](../docs/dev/README.md) — окружение разработки (compose), порты, типичные проблемы; - [`../docs/context/03-architecture.md`](../docs/context/03-architecture.md) — архитектура, соглашения шаблона, NATS и S3; - [`../docs/context/04-data-model-guide.md`](../docs/context/04-data-model-guide.md) и - [`../docs/model/data-model-v5.17.dbml`](../docs/model/data-model-v5.17.dbml) — модель данных; + [`../docs/model/data-model-v5.18.dbml`](../docs/model/data-model-v5.18.dbml) — модель данных; - [`../docs/context/05-domain-rules.md`](../docs/context/05-domain-rules.md) — алгоритмы предметной области; - [`../docs/context/07-open-questions.md`](../docs/context/07-open-questions.md) — открытые вопросы; - [`../docs/context/09-delivery-plan.md`](../docs/context/09-delivery-plan.md) — план срезов; diff --git a/api/alembic/versions/0006_dbml_v5_18.py b/api/alembic/versions/0006_dbml_v5_18.py new file mode 100644 index 0000000..e930aa3 --- /dev/null +++ b/api/alembic/versions/0006_dbml_v5_18.py @@ -0,0 +1,52 @@ +"""DBML v5.18: stored_file.consolidation_id — консолидация (период ГК), в контексте которой загружен файл +(RSHB/arm-ka-docs#90, RSHB/arm-core#96; design D-1, D-2). + +Колонка nullable, FK → consolidation, последняя в таблице; индекс (legal_entity_id, consolidation_id) — рабочий стол +консоли для ЮЛ. Backfill: файлу с документом, привязанным к версии (consolidation_document), — консолидация этой +версии; при привязках к версиям разных консолидаций — самый ранний период (год, затем квартал; enum period_quarter +упорядочен Q1 < Q2 < Q3 < Q4 по объявлению); файлы без привязок остаются NULL. + +Revision ID: 0006_dbml_v5_18 +Revises: 0005_dbml_v5_17 +Create Date: 2026-10-09 +""" + +from collections.abc import Sequence + +from alembic import op +from src.models.base import SCHEMA + +revision: str = "0006_dbml_v5_18" +down_revision: str | Sequence[str] | None = "0005_dbml_v5_17" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + +INDEX = "ix_stored_file_legal_entity_id_consolidation_id" +FK = "fk_stored_file_consolidation_id_consolidation" + + +def upgrade() -> None: + op.execute( + f"ALTER TABLE {SCHEMA}.stored_file ADD COLUMN consolidation_id uuid " + f"CONSTRAINT {FK} REFERENCES {SCHEMA}.consolidation (id)" + ) + op.execute(f"CREATE INDEX {INDEX} ON {SCHEMA}.stored_file (legal_entity_id, consolidation_id)") + op.execute( + f""" + UPDATE {SCHEMA}.stored_file AS sf SET consolidation_id = pick.consolidation_id + FROM ( + SELECT DISTINCT ON (d.stored_file_id) d.stored_file_id, c.id AS consolidation_id + FROM {SCHEMA}.document d + JOIN {SCHEMA}.consolidation_document cd ON cd.document_id = d.id + JOIN {SCHEMA}.consolidation_version v ON v.id = cd.consolidation_version_id + JOIN {SCHEMA}.consolidation c ON c.id = v.consolidation_id + ORDER BY d.stored_file_id, c.year, c.quarter, c.id + ) AS pick + WHERE sf.id = pick.stored_file_id + """ + ) + + +def downgrade() -> None: + op.execute(f"DROP INDEX {SCHEMA}.{INDEX}") + op.execute(f"ALTER TABLE {SCHEMA}.stored_file DROP COLUMN consolidation_id") diff --git a/api/scripts/check_dbml_vs_orm.py b/api/scripts/check_dbml_vs_orm.py index 28ba2c5..24a88b8 100644 --- a/api/scripts/check_dbml_vs_orm.py +++ b/api/scripts/check_dbml_vs_orm.py @@ -2,7 +2,7 @@ Запуск из `api/`: - python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.17.dbml + python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.18.dbml Без аргумента DBML ищется в `$ARM_KA_DOCS/model/` или в соседнем `docs/` рядом с `arm-core`. diff --git a/api/scripts/dbml_parity.py b/api/scripts/dbml_parity.py index c2dc8fb..4c8dac5 100644 --- a/api/scripts/dbml_parity.py +++ b/api/scripts/dbml_parity.py @@ -57,7 +57,7 @@ from sqlalchemy.sql.functions import FunctionElement #: Эталонная схема модели. Имя схемы в БД конфигурируемо (`DB_SCHEMA`), CLI сверяет ORM, загруженную с этим значением. SCHEMA = "dkb" -DBML_FILE_NAME = "data-model-v5.17.dbml" +DBML_FILE_NAME = "data-model-v5.18.dbml" ORM = "ORM" DBML = "DBML" ABSENT = "нет" diff --git a/api/src/models/__init__.py b/api/src/models/__init__.py index 75a9c84..534245d 100644 --- a/api/src/models/__init__.py +++ b/api/src/models/__init__.py @@ -1,4 +1,4 @@ -"""ORM-модели. Источник истины — docs/model/data-model-v5.17.dbml; заполняется в Срезе 0. +"""ORM-модели. Источник истины — docs/model/data-model-v5.18.dbml; заполняется в Срезе 0. Импорт всех модулей моделей обязателен: он регистрирует таблицы в `Base.metadata` (create_all, alembic). """ @@ -7,7 +7,7 @@ from . import access, accounting, audit_ex, consolidation, directories, files, k from .base import Base, UUIDPrimaryMixin #: Версия DBML, которой соответствуют модели (сверяется с шапкой DBML в check_dbml_vs_orm). -DBML_VERSION = "5.17" +DBML_VERSION = "5.18" __all__ = [ "DBML_VERSION", diff --git a/api/src/models/enums.py b/api/src/models/enums.py index 0c48ee0..c533d8f 100644 --- a/api/src/models/enums.py +++ b/api/src/models/enums.py @@ -1,6 +1,6 @@ """Enum'ы модели данных DBML v5.17 (28 штук) и фабрика `pg_enum` для PostgreSQL-колонок. -Источник истины — docs/model/data-model-v5.17.dbml (блоки `Enum {}`). Значения — строки DBML буквально, в порядке +Источник истины — docs/model/data-model-v5.18.dbml (блоки `Enum {}`). Значения — строки DBML буквально, в порядке DBML (включая кириллические `"ЮЛ"`/`"ИП"` и значения в разном регистре); `notes` DBML значениями не являются. Имя члена совпадает со значением (все значения DBML — валидные python-идентификаторы, кириллица допустима). Классы — `enum.StrEnum` (подкласс `str`) — удобно для pydantic/JSON; `values_callable` в `pg_enum` в БД пишет `.value`. diff --git a/api/src/models/files.py b/api/src/models/files.py index 00362ef..13cd93e 100644 --- a/api/src/models/files.py +++ b/api/src/models/files.py @@ -55,6 +55,8 @@ class StoredFile(UUIDPrimaryMixin, Base): Index("ix_stored_file_legal_entity_id_status", "legal_entity_id", "status"), # v5.15 п.4 (OQ-117): проверка FK при удалении recognition_job / файла Index("ix_stored_file_current_recognition_job_id", "current_recognition_job_id"), + # v5.18: рабочий стол консолидации для ЮЛ (spec documents-api «Рабочий стол консолидации для ЮЛ») + Index("ix_stored_file_legal_entity_id_consolidation_id", "legal_entity_id", "consolidation_id"), ) legal_entity_id: Mapped[uuid.UUID] = mapped_column( @@ -79,6 +81,9 @@ class StoredFile(UUIDPrimaryMixin, Base): uploaded_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id")) uploaded_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True)) updated_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True)) + # v5.18: консолидация (период ГК), в контексте которой файл загружен; NULL — без контекста (§9.5 п. 8). Последняя + # колонка: миграция 0006 добавляет её ALTER TABLE, а check_dbml сверяет порядок + consolidation_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("consolidation.id")) class Document(UUIDPrimaryMixin, Base): diff --git a/api/tests/test_check_dbml_script.py b/api/tests/test_check_dbml_script.py index 1d249c7..502ae5a 100644 --- a/api/tests/test_check_dbml_script.py +++ b/api/tests/test_check_dbml_script.py @@ -140,18 +140,18 @@ def test_enum_values_and_sets_are_compared(dbml_file: Path) -> None: def test_newer_dbml_version_fails_before_comparing_tables(tmp_path: Path, dbml_file: Path) -> None: lines = dbml_file.read_text(encoding="utf-8").splitlines() - newer = tmp_path / "data-model-v5.18.dbml" - # шапка 5.17 + заведомо сломанное тело: если бы таблицы сравнивались, были бы другие строки/ошибка разбора - newer.write_text(lines[0].replace("v5.17", "v5.18") + "\nTable broken {{{\n", encoding="utf-8") - assert run_check(newer) == (1, ["версия DBML 5.18 не совпадает с версией ORM 5.17: обновите модели"]) + newer = tmp_path / "data-model-v5.19.dbml" + # шапка 5.18 + заведомо сломанное тело: если бы таблицы сравнивались, были бы другие строки/ошибка разбора + newer.write_text(lines[0].replace("v5.18", "v5.19") + "\nTable broken {{{\n", encoding="utf-8") + assert run_check(newer) == (1, ["версия DBML 5.19 не совпадает с версией ORM 5.18: обновите модели"]) def test_newer_dbml_version_via_cli(tmp_path: Path, dbml_file: Path) -> None: - newer = tmp_path / "data-model-v5.18.dbml" - newer.write_text(dbml_file.read_text(encoding="utf-8").replace("v5.17", "v5.18", 1), encoding="utf-8") + newer = tmp_path / "data-model-v5.19.dbml" + newer.write_text(dbml_file.read_text(encoding="utf-8").replace("v5.18", "v5.19", 1), encoding="utf-8") result = _run_cli(str(newer)) assert result.returncode == 1 - assert result.stdout == "версия DBML 5.18 не совпадает с версией ORM 5.17: обновите модели\n" + assert result.stdout == "версия DBML 5.19 не совпадает с версией ORM 5.18: обновите модели\n" def test_header_without_version_is_usage_error(tmp_path: Path) -> None: diff --git a/api/tests/test_enums.py b/api/tests/test_enums.py index b3271c1..6b13db3 100644 --- a/api/tests/test_enums.py +++ b/api/tests/test_enums.py @@ -1,4 +1,4 @@ -"""Enum'ы ORM против блоков `Enum {}` DBML v5.17: имена, значения и их порядок (без БД).""" +"""Enum'ы ORM против блоков `Enum {}` DBML v5.18: имена, значения и их порядок (без БД).""" import enum import re @@ -15,7 +15,7 @@ EXPECTED_ENUM_COUNT = 28 def test_dbml_version_format() -> None: assert re.fullmatch(r"\d+\.\d+", DBML_VERSION) - assert DBML_VERSION == "5.17" + assert DBML_VERSION == "5.18" assert f"v{DBML_VERSION}" in dbml_path().name diff --git a/api/tests/test_migrations.py b/api/tests/test_migrations.py index 695a9eb..882f681 100644 --- a/api/tests/test_migrations.py +++ b/api/tests/test_migrations.py @@ -1,5 +1,5 @@ -"""Миграции 0001 (DBML v5.13), 0002 (v5.14), 0003 (v5.15), 0004 (v5.16), 0005 (v5.17) на PostgreSQL 16: полный цикл, -откат на шаг, пустой autogenerate, схема = схеме ORM. +"""Миграции 0001 (DBML v5.13), 0002 (v5.14), 0003 (v5.15), 0004 (v5.16), 0005 (v5.17), 0006 (v5.18) на PostgreSQL 16: +полный цикл, откат на шаг, пустой autogenerate, схема = схеме ORM. Spec data-model, требование «Миграции применяются с нуля и откатываются» (сценарии «Полный цикл» и «Автогенерация ничего не находит»), design D-2. Alembic запускается отдельным процессом (`python -m alembic -x dburl=...`), как из @@ -36,7 +36,8 @@ from tests.test_models_part_07_09 import ( _check_active_lock_is_unique, ) -HEAD = "0005_dbml_v5_17" +HEAD = "0006_dbml_v5_18" +V5_17 = "0005_dbml_v5_17" V5_16 = "0004_dbml_v5_16" V5_15 = "0003_dbml_v5_15" V5_14 = "0002_dbml_v5_14" @@ -451,6 +452,156 @@ async def _kpp_columns(conn: AsyncConnection) -> list[Any]: ) +async def _consolidation_id_column(conn: AsyncConnection) -> list[Any]: + return ( + await conn.execute( + text( + "SELECT is_nullable, data_type FROM information_schema.columns " + "WHERE table_schema = 'dkb' AND table_name = 'stored_file' AND column_name = 'consolidation_id'" + ) + ) + ).all() + + +@pytest.mark.integration +async def test_upgrade_0006_backfills_consolidation_id(scratch_db_url: str) -> None: + """0006: `stored_file.consolidation_id uuid` nullable, последняя колонка, индекс + `(legal_entity_id, consolidation_id)`; backfill по `consolidation_document`: файл A привязан к версиям 2025 Q1 и + 2024 Q4 → 2024 Q4 (самый ранний период, год важнее квартала); файл B без привязок → NULL; `downgrade 0005` + убирает индекс и колонку, повторный upgrade возвращает.""" + engine = create_async_engine(scratch_db_url) + now = datetime.datetime.now(datetime.UTC) + ids = { + name: uuid.uuid4() + for name in ( + "office", + "group", + "entity", + "user", + "s3a", + "s3b", + "file_a", + "file_b", + "doc_a", + "c_2025q1", + "c_2024q4", + "v_2025q1", + "v_2024q4", + ) + } + try: + await alembic(scratch_db_url, "upgrade", V5_17) + async with engine.begin() as conn: + assert await _consolidation_id_column(conn) == [] + await conn.execute(text("INSERT INTO dkb.regional_office (id, name) VALUES (:office, 'ГО')"), ids) + await conn.execute( + text( + "INSERT INTO dkb.company_group (id, code, name, base_regional_office_id, valid_from) " + "VALUES (:group, 'G-1', 'ГК', :office, :now)" + ), + ids | {"now": now}, + ) + await conn.execute( + text( + "INSERT INTO dkb.legal_entity (id, inn, name, company_group_id, valid_from) " + "VALUES (:entity, '7701000001', 'ООО', :group, :now)" + ), + ids | {"now": now}, + ) + await conn.execute( + text( + "INSERT INTO dkb.app_user (id, email, full_name, regional_office_id) " + "VALUES (:user, 'm@x.ru', 'Тест', :office)" + ), + ids, + ) + for s3, file in (("s3a", "file_a"), ("s3b", "file_b")): + await conn.execute( + text( + "INSERT INTO dkb.s3_object (id, bucket_role, bucket, key, created_at) " + f"VALUES (:{s3}, 'uploads', 'b', 'k-{s3}', :now)" + ), + ids | {"now": now}, + ) + await conn.execute( + text( + "INSERT INTO dkb.stored_file (id, legal_entity_id, file_type, s3_object_id, status, " + f"version_no) VALUES (:{file}, :entity, 'XLS', :{s3}, 'PROCESSED', 1)" + ), + ids, + ) + await conn.execute( + text( + "INSERT INTO dkb.document (id, stored_file_id, legal_entity_id, document_type, osv_account_code, " + "period_year, period_quarter, status) " + "VALUES (:doc_a, :file_a, :entity, 'OSV', '60', 2024, 'Q4', 'CONFIRMED')" + ), + ids, + ) + for c, v, year, quarter, rd in ( + ("c_2025q1", "v_2025q1", 2025, "Q1", "2025-04-01"), + ("c_2024q4", "v_2024q4", 2024, "Q4", "2025-01-01"), + ): + await conn.execute( + text( + "INSERT INTO dkb.consolidation (id, company_group_id, year, quarter, reporting_date) " + f"VALUES (:{c}, :group, {year}, '{quarter}', '{rd}')" + ), + ids, + ) + await conn.execute( + text( + "INSERT INTO dkb.consolidation_version (id, consolidation_id, version_no, status, scenario, " + "approach, goodwill_enabled, initiator_id, initiator_office_id, is_in_work) " + f"VALUES (:{v}, :{c}, 1, 'INITIATED', 'CREATE', 'BASIC', false, :user, :office, true)" + ), + ids, + ) + await conn.execute( + text( + "INSERT INTO dkb.consolidation_document (id, consolidation_version_id, document_id) " + f"VALUES (gen_random_uuid(), :{v}, :doc_a)" + ), + ids, + ) + + await alembic(scratch_db_url, "upgrade", "head") + async with engine.begin() as conn: + assert await _consolidation_id_column(conn) == [("YES", "uuid")] + last = await _scalars( + conn, + "SELECT column_name FROM information_schema.columns WHERE table_schema = 'dkb' " + "AND table_name = 'stored_file' ORDER BY ordinal_position DESC LIMIT 1", + ) + assert last == ["consolidation_id"] + rows = (await conn.execute(text("SELECT id, consolidation_id FROM dkb.stored_file ORDER BY id"))).all() + assert dict(rows) == {ids["file_a"]: ids["c_2024q4"], ids["file_b"]: None} + assert await _scalars( + conn, + "SELECT indexname FROM pg_indexes WHERE schemaname = 'dkb' " + "AND indexname = 'ix_stored_file_legal_entity_id_consolidation_id'", + ) == ["ix_stored_file_legal_entity_id_consolidation_id"] + await engine.dispose() + + await alembic(scratch_db_url, "downgrade", V5_17) + async with engine.connect() as conn: + assert await _consolidation_id_column(conn) == [] + assert ( + await _scalars( + conn, + "SELECT indexname FROM pg_indexes WHERE schemaname = 'dkb' " + "AND indexname = 'ix_stored_file_legal_entity_id_consolidation_id'", + ) + == [] + ) + await engine.dispose() + await alembic(scratch_db_url, "upgrade", "head") + async with engine.connect() as conn: + assert await _consolidation_id_column(conn) == [("YES", "uuid")] + finally: + await engine.dispose() + + @pytest.mark.integration async def test_upgrade_0005_adds_nullable_kpp_columns(scratch_db_url: str) -> None: """0005: `kpp varchar` nullable у `legal_entity` и `legal_entity_history`, существующие ЮЛ — с `kpp IS NULL`; @@ -714,8 +865,8 @@ def test_offline_sql_creates_schema_before_version_table() -> None: assert schema_at < output.index("CREATE TABLE dkb.alembic_version") assert schema_at < output.index("CREATE TYPE dkb.entity_type AS ENUM ('ЮЛ', 'ИП')") assert f"INSERT INTO dkb.alembic_version (version_num) VALUES ('{PREVIOUS}')" in output - # версия head пишется UPDATE'ами по цепочке 0001 → 0002 → 0003 → 0004 → 0005 — офлайн-SQL доходит до head - for new_version, old_version in ((V5_14, PREVIOUS), (V5_15, V5_14), (V5_16, V5_15), (HEAD, V5_16)): + # версия head пишется UPDATE'ами по цепочке 0001 → 0002 → 0003 → 0004 → 0005 → 0006 — офлайн-SQL доходит до head + for new_version, old_version in ((V5_14, PREVIOUS), (V5_15, V5_14), (V5_16, V5_15), (V5_17, V5_16), (HEAD, V5_17)): assert ( f"UPDATE dkb.alembic_version SET version_num='{new_version}' " f"WHERE dkb.alembic_version.version_num = '{old_version}'" in output