feat(model): DBML v5.14 — related_group_member.kind, audit_action +2, миграция 0002, последовательности кодов
- RelatedGroupMember.kind (related_group_kind, NOT NULL) между legal_entity_id и valid_from, как в DBML; частичный уникальный индекс uq_related_group_member_entity_kind_current (legal_entity_id, kind) WHERE is_current — ЮЛ одновременно в одной ГСК и одной ГСЗ. - AuditAction: LEGAL_ENTITY_CHANGE (после GROUP_CHANGE), USER_CHANGE (после USER_ROLE_CHANGE). - Последовательности company_group_code_seq / related_group_code_seq в metadata (design D-5). - Миграция 0002_dbml_v5_14: related_group_member пересоздаётся с переносом строк (kind из группы), чтобы порядок колонок совпал с DBML и create_all; ALTER TYPE ADD VALUE ×2; downgrade пересоздаёт audit_action v5.13. - DBML_VERSION = 5.14, пути к data-model-v5.14.dbml (Makefile, dbml_parity), тесты: откат на шаг, перенос строк, уникальность gsk+gsz, офлайн-SQL до head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
13d11c074f
commit
458392e94d
2
Makefile
2
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.13.dbml
|
||||
DBML ?= ../../docs/model/data-model-v5.14.dbml
|
||||
|
||||
.PHONY: venv lint test test-integration dbml-check migration-cycle check check-infra docker-build web-check check-full up migrate seed seed-dev
|
||||
|
||||
|
||||
124
api/alembic/versions/0002_dbml_v5_14.py
Normal file
124
api/alembic/versions/0002_dbml_v5_14.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""DBML v5.14: related_group_member.kind + partial unique (legal_entity_id, kind), audit_action += LEGAL_ENTITY_CHANGE,
|
||||
USER_CHANGE, последовательности кодов ГК/ГСК (design D-1, D-5).
|
||||
|
||||
`related_group_member` пересоздаётся (копия данных → DROP → CREATE → INSERT), а не `ADD COLUMN`: DBML ставит `kind`
|
||||
сразу после `legal_entity_id`, PostgreSQL добавляет колонку только в конец, а каталог после миграции обязан совпадать
|
||||
с `Base.metadata.create_all` (порядок колонок — тоже). На таблицу никто не ссылается по FK; `kind` берётся из группы.
|
||||
|
||||
`ALTER TYPE … ADD VALUE` необратим — downgrade пересоздаёт тип audit_action со значениями v5.13 (строк с новыми
|
||||
значениями в audit_event быть не должно; иначе downgrade падает на USING — это осознанно).
|
||||
|
||||
Revision ID: 0002_dbml_v5_14
|
||||
Revises: 0001_dbml_v5_13
|
||||
Create Date: 2026-10-04
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.schema import CreateSequence, DropSequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0002_dbml_v5_14"
|
||||
down_revision: str | Sequence[str] | None = "0001_dbml_v5_13"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
SCHEMA = "dkb"
|
||||
TABLE = "related_group_member"
|
||||
OLD_INDEX = "uq_related_group_member_entity_current"
|
||||
NEW_INDEX = "uq_related_group_member_entity_kind_current"
|
||||
GROUP_ENTITY_INDEX = "uq_related_group_member_group_entity_current"
|
||||
#: Временная копия строк на время пересоздания таблицы (в схеме dkb: прав на TEMP у роли миграций может не быть).
|
||||
COPY_TABLE = "related_group_member_v5_13"
|
||||
SEQUENCES = ("company_group_code_seq", "related_group_code_seq")
|
||||
AUDIT_ACTION_V5_13 = (
|
||||
"LOGIN", "LOGIN_FAILED", "LOGOUT", "CONSOLIDATION_CREATE", "CONSOLIDATION_STATUS_CHANGE", "CONSOLIDATION_APPROVE",
|
||||
"CONSOLIDATION_VERIFY", "GROUP_CHANGE", "MEMBERSHIP_CHANGE", "ASSIGNMENT_CHANGE", "FILE_UPLOAD", "FILE_DELETE",
|
||||
"FILE_REPLACE", "DOCUMENT_CONFIRM", "CONSOLIDATION_LOCK", "CONSOLIDATION_LOCK_TAKEOVER", "DISCLOSURE_EDIT",
|
||||
"RECOGNIZED_DATA_EDIT", "OSV_COUNTERPARTY_MAP", "OSV_TYPE_OVERRIDE", "CORRECTION_CREATE", "CORRECTION_EDIT",
|
||||
"CORRECTION_REVOKE", "GOODWILL_EDIT", "EXPORT", "DIRECTORY_IMPORT", "USER_ROLE_CHANGE", "ROLE_PERMISSION_CHANGE",
|
||||
) # fmt: skip
|
||||
|
||||
|
||||
def _create_member_table() -> None:
|
||||
"""`related_group_member` v5.14: колонки в порядке DBML, имена ограничений и индексов — как в ORM."""
|
||||
op.create_table(
|
||||
TABLE,
|
||||
sa.Column("id", sa.UUID(), nullable=False),
|
||||
sa.Column("related_group_id", sa.UUID(), nullable=False),
|
||||
sa.Column("legal_entity_id", sa.UUID(), nullable=False),
|
||||
sa.Column("kind", postgresql.ENUM(name="related_group_kind", schema=SCHEMA, create_type=False), nullable=False),
|
||||
sa.Column("valid_from", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("valid_to", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("is_current", sa.Boolean(), nullable=False),
|
||||
sa.Column("created_by", sa.UUID(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["created_by"], ["dkb.app_user.id"], name=op.f("fk_related_group_member_created_by_app_user")
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["legal_entity_id"],
|
||||
["dkb.legal_entity.id"],
|
||||
name=op.f("fk_related_group_member_legal_entity_id_legal_entity"),
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["related_group_id"],
|
||||
["dkb.related_group.id"],
|
||||
name=op.f("fk_related_group_member_related_group_id_related_group"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_related_group_member")),
|
||||
schema=SCHEMA,
|
||||
)
|
||||
op.create_index(
|
||||
NEW_INDEX,
|
||||
TABLE,
|
||||
["legal_entity_id", "kind"],
|
||||
unique=True,
|
||||
schema=SCHEMA,
|
||||
postgresql_where=sa.text("is_current"),
|
||||
)
|
||||
op.create_index(
|
||||
GROUP_ENTITY_INDEX,
|
||||
TABLE,
|
||||
["related_group_id", "legal_entity_id"],
|
||||
unique=True,
|
||||
schema=SCHEMA,
|
||||
postgresql_where=sa.text("is_current"),
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# копия текущих строк с kind группы (FK related_group_id NOT NULL — JOIN не теряет строк)
|
||||
op.execute(
|
||||
f"CREATE TABLE {SCHEMA}.{COPY_TABLE} AS SELECT m.id, m.related_group_id, m.legal_entity_id, g.kind, "
|
||||
f"m.valid_from, m.valid_to, m.is_current, m.created_by FROM {SCHEMA}.{TABLE} m "
|
||||
f"JOIN {SCHEMA}.related_group g ON g.id = m.related_group_id"
|
||||
)
|
||||
op.drop_table(TABLE, schema=SCHEMA) # вместе с индексами v5.13
|
||||
_create_member_table()
|
||||
op.execute(f"INSERT INTO {SCHEMA}.{TABLE} SELECT * FROM {SCHEMA}.{COPY_TABLE}")
|
||||
op.drop_table(COPY_TABLE, schema=SCHEMA)
|
||||
# порядок значений — как в DBML v5.14 (сверка enum'ов сравнивает упорядоченные списки)
|
||||
op.execute(f"ALTER TYPE {SCHEMA}.audit_action ADD VALUE IF NOT EXISTS 'LEGAL_ENTITY_CHANGE' AFTER 'GROUP_CHANGE'")
|
||||
op.execute(f"ALTER TYPE {SCHEMA}.audit_action ADD VALUE IF NOT EXISTS 'USER_CHANGE' AFTER 'USER_ROLE_CHANGE'")
|
||||
for name in SEQUENCES:
|
||||
op.execute(CreateSequence(sa.Sequence(name, schema=SCHEMA)))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
for name in reversed(SEQUENCES):
|
||||
op.execute(DropSequence(sa.Sequence(name, schema=SCHEMA)))
|
||||
op.drop_index(NEW_INDEX, table_name=TABLE, schema=SCHEMA)
|
||||
op.create_index(
|
||||
OLD_INDEX, TABLE, ["legal_entity_id"], unique=True, schema=SCHEMA, postgresql_where=sa.text("is_current")
|
||||
)
|
||||
op.drop_column(TABLE, "kind", schema=SCHEMA)
|
||||
op.execute(f"ALTER TYPE {SCHEMA}.audit_action RENAME TO audit_action_old")
|
||||
postgresql.ENUM(*AUDIT_ACTION_V5_13, name="audit_action", schema=SCHEMA).create(op.get_bind(), checkfirst=False)
|
||||
op.execute(
|
||||
f"ALTER TABLE {SCHEMA}.audit_event ALTER COLUMN action TYPE {SCHEMA}.audit_action "
|
||||
f"USING action::text::{SCHEMA}.audit_action"
|
||||
)
|
||||
op.execute(f"DROP TYPE {SCHEMA}.audit_action_old")
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
Запуск из `api/`:
|
||||
|
||||
python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.13.dbml
|
||||
python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.14.dbml
|
||||
|
||||
Без аргумента DBML ищется в `$ARM_KA_DOCS/model/` или в соседнем `docs/` рядом с `arm-core`.
|
||||
|
||||
|
||||
@ -56,7 +56,7 @@ from sqlalchemy.sql.elements import TextClause
|
||||
from sqlalchemy.sql.functions import FunctionElement
|
||||
|
||||
SCHEMA = "dkb"
|
||||
DBML_FILE_NAME = "data-model-v5.13.dbml"
|
||||
DBML_FILE_NAME = "data-model-v5.14.dbml"
|
||||
ORM = "ORM"
|
||||
DBML = "DBML"
|
||||
ABSENT = "нет"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""ORM-модели. Источник истины — docs/model/data-model-v5.13.dbml; заполняется в Срезе 0.
|
||||
"""ORM-модели. Источник истины — docs/model/data-model-v5.14.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.13"
|
||||
DBML_VERSION = "5.14"
|
||||
|
||||
__all__ = [
|
||||
"DBML_VERSION",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Часть 02 DBML v5.13 «Пользователи и доступ»: пользователи, роли, права как данные (`effective_permissions`)."""
|
||||
"""Часть 02 DBML v5.14 «Пользователи и доступ»: пользователи, роли, права как данные (`effective_permissions`)."""
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Часть 03 DBML v5.13 (учёт): план счетов и правила элиминации ВГО (Прил. 3/4 ТЗ как данные)."""
|
||||
"""Часть 03 DBML v5.14 (учёт): план счетов и правила элиминации ВГО (Прил. 3/4 ТЗ как данные)."""
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Часть 09 DBML v5.13 «Комментарии, аудит, ЕХФД, прочее»: комментарии, журнал аудита, срезы ЕХ, чек-лист документов.
|
||||
"""Часть 09 DBML v5.14 «Комментарии, аудит, ЕХФД, прочее»: комментарии, журнал аудита, срезы ЕХ, чек-лист документов.
|
||||
|
||||
`audit_event` — append-only, высокообъёмная (before/after — jsonb). Срезы ЕХ самодостаточны (v5.12): FK на
|
||||
консолидацию и её результаты отсутствуют (их удаляют по сроку, срез хранится «вечно»): `ex_snapshot.source_version_id`
|
||||
|
||||
@ -6,7 +6,7 @@ from sqlalchemy import MetaData
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, declarative_mixin, mapped_column
|
||||
|
||||
#: Схема PostgreSQL, в которой живёт вся модель данных (DBML v5.13).
|
||||
#: Схема PostgreSQL, в которой живёт вся модель данных (DBML v5.14).
|
||||
SCHEMA = "dkb"
|
||||
|
||||
#: Стандартная схема имён SQLAlchemy. Нужна, чтобы ORM, миграция 0001 и `alembic autogenerate` давали одни и те же
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Часть 07 DBML v5.13 «Консолидация»: консолидация ГК за период, версии, маршрут, блокировка, периметр, документы.
|
||||
"""Часть 07 DBML v5.14 «Консолидация»: консолидация ГК за период, версии, маршрут, блокировка, периметр, документы.
|
||||
|
||||
Производные консолидации (сводная ОСВ, пары ВГО, шахматка) НЕ хранятся (DECISION-5): считаются запросом из
|
||||
`osv_entry` × `consolidation_member` × `elimination_rule`. Версия после VERIFIED — read-only (ТЗ 6.10).
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
"""Часть 01 DBML v5.13 «Справочники»: филиалы, ГК и ЮЛ (SCD4), алиасы, ГСК/ГСЗ (SCD2)."""
|
||||
"""Часть 01 DBML v5.14 «Справочники»: филиалы, ГК и ЮЛ (SCD4), алиасы, ГСК/ГСЗ (SCD2)."""
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Index, String, text
|
||||
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Index, Sequence, String, text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .base import Base, UUIDPrimaryMixin
|
||||
from .base import SCHEMA, Base, UUIDPrimaryMixin
|
||||
from .enums import EntityType, LeHistoryEvent, RelatedGroupKind, pg_enum
|
||||
|
||||
#: Последовательности кодов ГК и ГСК/ГСЗ (design D-5): `GK-000001`, `GSK-000001`/`GSZ-000001`. В DBML не описываются
|
||||
#: (не таблицы); создаются миграцией 0002 и `Base.metadata.create_all` (привязаны к metadata).
|
||||
COMPANY_GROUP_CODE_SEQ = Sequence("company_group_code_seq", schema=SCHEMA, metadata=Base.metadata)
|
||||
RELATED_GROUP_CODE_SEQ = Sequence("related_group_code_seq", schema=SCHEMA, metadata=Base.metadata)
|
||||
|
||||
|
||||
class RegionalOffice(UUIDPrimaryMixin, Base):
|
||||
"""Филиал: ГО и региональные филиалы (DECISION-3: ГО — обычная запись с `is_head_office`)."""
|
||||
@ -148,10 +153,11 @@ class RelatedGroupMember(UUIDPrimaryMixin, Base):
|
||||
unique=True,
|
||||
postgresql_where=text("is_current"),
|
||||
),
|
||||
# legal_entity_id: partial UNIQUE WHERE is_current — ЮЛ только в одной ГСК или ГСЗ (ТЗ 6.1, ANL-030)
|
||||
# (legal_entity_id, kind): partial UNIQUE WHERE is_current — v5.14: ЮЛ в одной ГСК И одной ГСЗ одновременно
|
||||
Index(
|
||||
"uq_related_group_member_entity_current",
|
||||
"uq_related_group_member_entity_kind_current",
|
||||
"legal_entity_id",
|
||||
"kind",
|
||||
unique=True,
|
||||
postgresql_where=text("is_current"),
|
||||
),
|
||||
@ -163,6 +169,7 @@ class RelatedGroupMember(UUIDPrimaryMixin, Base):
|
||||
legal_entity_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("legal_entity.id"), nullable=False
|
||||
)
|
||||
kind: Mapped[RelatedGroupKind] = mapped_column(pg_enum("related_group_kind"), nullable=False)
|
||||
valid_from: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
valid_to: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
is_current: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""Enum'ы модели данных DBML v5.13 (27 штук) и фабрика `pg_enum` для PostgreSQL-колонок.
|
||||
"""Enum'ы модели данных DBML v5.14 (27 штук) и фабрика `pg_enum` для PostgreSQL-колонок.
|
||||
|
||||
Источник истины — docs/model/data-model-v5.13.dbml (блоки `Enum {}`). Значения — строки DBML буквально, в порядке
|
||||
Источник истины — docs/model/data-model-v5.14.dbml (блоки `Enum {}`). Значения — строки DBML буквально, в порядке
|
||||
DBML (включая кириллические `"ЮЛ"`/`"ИП"` и значения в разном регистре); `notes` DBML значениями не являются.
|
||||
Имя члена совпадает со значением (все значения DBML — валидные python-идентификаторы, кириллица допустима).
|
||||
Классы — `enum.StrEnum` (подкласс `str`) — удобно для pydantic/JSON; `values_callable` в `pg_enum` в БД пишет `.value`.
|
||||
@ -269,6 +269,7 @@ class AuditAction(enum.StrEnum):
|
||||
CONSOLIDATION_APPROVE = "CONSOLIDATION_APPROVE"
|
||||
CONSOLIDATION_VERIFY = "CONSOLIDATION_VERIFY"
|
||||
GROUP_CHANGE = "GROUP_CHANGE"
|
||||
LEGAL_ENTITY_CHANGE = "LEGAL_ENTITY_CHANGE"
|
||||
MEMBERSHIP_CHANGE = "MEMBERSHIP_CHANGE"
|
||||
ASSIGNMENT_CHANGE = "ASSIGNMENT_CHANGE"
|
||||
FILE_UPLOAD = "FILE_UPLOAD"
|
||||
@ -288,6 +289,7 @@ class AuditAction(enum.StrEnum):
|
||||
EXPORT = "EXPORT"
|
||||
DIRECTORY_IMPORT = "DIRECTORY_IMPORT"
|
||||
USER_ROLE_CHANGE = "USER_ROLE_CHANGE"
|
||||
USER_CHANGE = "USER_CHANGE"
|
||||
ROLE_PERMISSION_CHANGE = "ROLE_PERMISSION_CHANGE"
|
||||
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Часть 04 DBML v5.13 «Файлы / распознавание»: реестр S3, физические файлы, документы, задания распознавания.
|
||||
"""Часть 04 DBML v5.14 «Файлы / распознавание»: реестр S3, физические файлы, документы, задания распознавания.
|
||||
|
||||
Цикл FK `stored_file.current_recognition_job_id` -> `recognition_job` и `recognition_job.file_id` -> `stored_file`
|
||||
разорван через `use_alter=True` на nullable-стороне (`stored_file.current_recognition_job_id`): `create_all` создаёт
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Часть 08 DBML v5.13 «КФО»: корректировки, гудвилл, результат КФО (шапка, строки, разрез по компаниям), КФО клиента.
|
||||
"""Часть 08 DBML v5.14 «КФО»: корректировки, гудвилл, результат КФО (шапка, строки, разрез по компаниям), КФО клиента.
|
||||
|
||||
Корректировки и гудвилл — ручные решения аналитика, хранятся поверх первички (overlay). Результат КФО
|
||||
замораживается при VERIFIED. Деньги — `numeric(18,2)`, проценты — `numeric(7,4)` / `numeric(9,4)`.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Часть 05 DBML v5.13 «ОСВ»: строка оборотно-сальдовой ведомости (ТЗ 6.2, 6.5).
|
||||
"""Часть 05 DBML v5.14 «ОСВ»: строка оборотно-сальдовой ведомости (ТЗ 6.2, 6.5).
|
||||
|
||||
Первичка не мутируется: сырые значения хранятся как пришли, правка аналитика — рядом
|
||||
(`is_modified`, `original_values`, `modified_by/at` + audit_event(RECOGNIZED_DATA_EDIT)).
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Часть 06 DBML v5.13 «Бухгалтерская отчётность»: шапка, колонки-периоды, строки и значения форм ББ/ОФР (157н).
|
||||
"""Часть 06 DBML v5.14 «Бухгалтерская отчётность»: шапка, колонки-периоды, строки и значения форм ББ/ОФР (157н).
|
||||
|
||||
Первичка не мутируется: `*_original_value` / `original_amount` / `is_modified` — рядом с распознанным значением.
|
||||
Логические проверки (ТЗ 6.3) и «требует проверки» не хранятся — считаются на лету.
|
||||
|
||||
@ -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.14.dbml"
|
||||
# шапка 5.14 + заведомо сломанное тело: если бы таблицы сравнивались, были бы другие строки/ошибка разбора
|
||||
newer.write_text(lines[0].replace("v5.13", "v5.14") + "\nTable broken {{{\n", encoding="utf-8")
|
||||
assert run_check(newer) == (1, ["версия DBML 5.14 не совпадает с версией ORM 5.13: обновите модели"])
|
||||
newer = tmp_path / "data-model-v5.15.dbml"
|
||||
# шапка 5.15 + заведомо сломанное тело: если бы таблицы сравнивались, были бы другие строки/ошибка разбора
|
||||
newer.write_text(lines[0].replace("v5.14", "v5.15") + "\nTable broken {{{\n", encoding="utf-8")
|
||||
assert run_check(newer) == (1, ["версия DBML 5.15 не совпадает с версией ORM 5.14: обновите модели"])
|
||||
|
||||
|
||||
def test_newer_dbml_version_via_cli(tmp_path: Path, dbml_file: Path) -> None:
|
||||
newer = tmp_path / "data-model-v5.14.dbml"
|
||||
newer.write_text(dbml_file.read_text(encoding="utf-8").replace("v5.13", "v5.14", 1), encoding="utf-8")
|
||||
newer = tmp_path / "data-model-v5.15.dbml"
|
||||
newer.write_text(dbml_file.read_text(encoding="utf-8").replace("v5.14", "v5.15", 1), encoding="utf-8")
|
||||
result = _run_cli(str(newer))
|
||||
assert result.returncode == 1
|
||||
assert result.stdout == "версия DBML 5.14 не совпадает с версией ORM 5.13: обновите модели\n"
|
||||
assert result.stdout == "версия DBML 5.15 не совпадает с версией ORM 5.14: обновите модели\n"
|
||||
|
||||
|
||||
def test_header_without_version_is_usage_error(tmp_path: Path) -> None:
|
||||
|
||||
@ -167,9 +167,9 @@ def test_nullable_pk_unique_and_order_mismatches_are_detected() -> None:
|
||||
def test_missing_extra_and_wrong_where_indexes_are_detected() -> None:
|
||||
metadata = clone_metadata()
|
||||
member = metadata.tables["dkb.related_group_member"]
|
||||
index = next(i for i in member.indexes if i.name == "uq_related_group_member_entity_current")
|
||||
index = next(i for i in member.indexes if i.name == "uq_related_group_member_entity_kind_current")
|
||||
index.dialect_options["postgresql"]["where"] = text("is_current AND valid_to IS NULL")
|
||||
label = "related_group_member: индекс (legal_entity_id)"
|
||||
label = "related_group_member: индекс (legal_entity_id, kind)"
|
||||
assert find_problems(metadata, {"related_group_member"}) == [
|
||||
f"{label}: ожидалось unique WHERE is_current / фактически unique WHERE is_current and valid_to is null"
|
||||
]
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Enum'ы ORM против блоков `Enum {}` DBML v5.13: имена, значения и их порядок (без БД)."""
|
||||
"""Enum'ы ORM против блоков `Enum {}` DBML v5.14: имена, значения и их порядок (без БД)."""
|
||||
|
||||
import enum
|
||||
import re
|
||||
@ -15,7 +15,7 @@ EXPECTED_ENUM_COUNT = 27
|
||||
|
||||
def test_dbml_version_format() -> None:
|
||||
assert re.fullmatch(r"\d+\.\d+", DBML_VERSION)
|
||||
assert DBML_VERSION == "5.13"
|
||||
assert DBML_VERSION == "5.14"
|
||||
assert f"v{DBML_VERSION}" in dbml_path().name
|
||||
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
"""Миграция 0001 (DBML v5.13) на PostgreSQL 16: полный цикл, пустой autogenerate, схема = схеме ORM.
|
||||
"""Миграции 0001 (DBML v5.13) и 0002 (v5.14) на PostgreSQL 16: полный цикл, откат на шаг, пустой autogenerate,
|
||||
схема = схеме ORM.
|
||||
|
||||
Spec data-model, требование «Миграции применяются с нуля и откатываются» (сценарии «Полный цикл» и «Автогенерация
|
||||
ничего не находит»), design D-2. Alembic запускается отдельным процессом (`python -m alembic -x dburl=...`), как из
|
||||
@ -6,6 +7,7 @@ CLI: `env.py` вызывает `asyncio.run(...)`, а внутри event loop py
|
||||
перенастраивает логирование тестов через `fileConfig`. Каждая проверка идёт на своей временной БД (`scratch_db_url`).
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
@ -28,7 +30,8 @@ from tests.test_models_part_07_09 import (
|
||||
_check_active_lock_is_unique,
|
||||
)
|
||||
|
||||
HEAD = "0001_dbml_v5_13"
|
||||
HEAD = "0002_dbml_v5_14"
|
||||
PREVIOUS = "0001_dbml_v5_13"
|
||||
VERSION_TABLE = "alembic_version"
|
||||
FK_CYCLE = "fk_stored_file_current_recognition_job_id_recognition_job"
|
||||
|
||||
@ -150,6 +153,109 @@ async def test_full_cycle_upgrade_downgrade_upgrade(scratch_db_url: str) -> None
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_downgrade_one_step(scratch_db_url: str) -> None:
|
||||
"""Сценарий «Откат на один шаг»: head → 0001 возвращает схему v5.13 (без kind, старый индекс и audit_action)."""
|
||||
engine = create_async_engine(scratch_db_url)
|
||||
try:
|
||||
await alembic(scratch_db_url, "upgrade", "head")
|
||||
await alembic(scratch_db_url, "downgrade", PREVIOUS)
|
||||
async with engine.connect() as conn:
|
||||
assert await _scalars(conn, "SELECT version_num FROM dkb.alembic_version") == [PREVIOUS]
|
||||
kind_columns = await _scalars(
|
||||
conn,
|
||||
"SELECT column_name FROM information_schema.columns WHERE table_schema = 'dkb' "
|
||||
"AND table_name = 'related_group_member' AND column_name = 'kind'",
|
||||
)
|
||||
assert kind_columns == []
|
||||
indexes = set(
|
||||
await _scalars(
|
||||
conn,
|
||||
"SELECT indexname FROM pg_indexes WHERE schemaname = 'dkb' AND tablename = 'related_group_member'",
|
||||
)
|
||||
)
|
||||
assert "uq_related_group_member_entity_current" in indexes
|
||||
assert "uq_related_group_member_entity_kind_current" not in indexes
|
||||
[labels] = await _scalars(conn, "SELECT enum_range(NULL::dkb.audit_action)::text[]")
|
||||
assert "LEGAL_ENTITY_CHANGE" not in labels
|
||||
assert "USER_CHANGE" not in labels
|
||||
sequences = await _scalars(
|
||||
conn, "SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'dkb'"
|
||||
)
|
||||
assert sequences == []
|
||||
|
||||
await alembic(scratch_db_url, "upgrade", "head")
|
||||
async with engine.connect() as conn:
|
||||
await _assert_full_model(conn)
|
||||
[labels] = await _scalars(conn, "SELECT enum_range(NULL::dkb.audit_action)::text[]")
|
||||
assert labels == [m.value for m in ENUMS["audit_action"]]
|
||||
sequences = set(
|
||||
await _scalars(
|
||||
conn, "SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'dkb'"
|
||||
)
|
||||
)
|
||||
assert sequences == {"company_group_code_seq", "related_group_code_seq"}
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_upgrade_0002_keeps_membership_and_fills_kind(scratch_db_url: str) -> None:
|
||||
"""0002 пересоздаёт `related_group_member`: строки v5.13 сохраняются, `kind` = `related_group.kind` группы."""
|
||||
engine = create_async_engine(scratch_db_url)
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
ids = {k: uuid.uuid4() for k in ("ro", "cg", "le", "gsk", "gsz", "m1", "m2")}
|
||||
try:
|
||||
await alembic(scratch_db_url, "upgrade", PREVIOUS)
|
||||
async with engine.begin() as conn:
|
||||
await conn.execute(text("INSERT INTO dkb.regional_office (id, name) VALUES (:ro, 'ГО')"), ids)
|
||||
await conn.execute(
|
||||
text("INSERT INTO dkb.company_group (id, code, name, valid_from) VALUES (:cg, 'G1', 'ГК', :now)"),
|
||||
{**ids, "now": now},
|
||||
)
|
||||
await conn.execute(
|
||||
text("INSERT INTO dkb.legal_entity (id, inn, name, valid_from) VALUES (:le, '1', 'ЮЛ', :now)"),
|
||||
{**ids, "now": now},
|
||||
)
|
||||
for kind in ("gsk", "gsz"):
|
||||
await conn.execute(
|
||||
text(
|
||||
"INSERT INTO dkb.related_group (id, code, company_group_id, kind, name) "
|
||||
"VALUES (:g, :code, :cg, :kind, 'Группа')"
|
||||
),
|
||||
{"g": ids[kind], "code": kind, "cg": ids["cg"], "kind": kind},
|
||||
)
|
||||
# v5.13: одно текущее членство (в ГСЗ) и закрытое (в ГСК)
|
||||
member = text(
|
||||
"INSERT INTO dkb.related_group_member (id, related_group_id, legal_entity_id, valid_from, valid_to, "
|
||||
"is_current) VALUES (:id, :g, :le, :now, :to, :cur)"
|
||||
)
|
||||
await conn.execute(
|
||||
member, {"id": ids["m1"], "g": ids["gsz"], "le": ids["le"], "now": now, "to": None, "cur": True}
|
||||
)
|
||||
await conn.execute(
|
||||
member, {"id": ids["m2"], "g": ids["gsk"], "le": ids["le"], "now": now, "to": now, "cur": False}
|
||||
)
|
||||
|
||||
await alembic(scratch_db_url, "upgrade", "head")
|
||||
async with engine.connect() as conn:
|
||||
rows = (
|
||||
await conn.execute(
|
||||
text("SELECT id, related_group_id, kind::text, valid_to, is_current FROM dkb.related_group_member")
|
||||
)
|
||||
).all()
|
||||
assert {tuple(r) for r in rows} == {
|
||||
(ids["m1"], ids["gsz"], "gsz", None, True),
|
||||
(ids["m2"], ids["gsk"], "gsk", now, False),
|
||||
}
|
||||
leftovers = await _scalars(
|
||||
conn, "SELECT relname FROM pg_class WHERE relnamespace = 'dkb'::regnamespace AND relname LIKE '%v5_13'"
|
||||
)
|
||||
assert leftovers == [] # временная копия удалена
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_autogenerate_finds_nothing_after_upgrade(scratch_db_url: str) -> None:
|
||||
"""Сценарий «Автогенерация ничего не находит»: compare_metadata пуст, `alembic check` не видит операций."""
|
||||
@ -228,13 +334,13 @@ async def test_migrated_schema_behaviour(scratch_db_url: str) -> None:
|
||||
text(
|
||||
"SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = 'dkb' AND indexname IN "
|
||||
"('uq_document_file_type_account', 'uq_consolidation_lock_version_active', "
|
||||
"'uq_related_group_member_entity_current', 'uq_related_group_member_group_entity_current')"
|
||||
"'uq_related_group_member_entity_kind_current', 'uq_related_group_member_group_entity_current')"
|
||||
)
|
||||
)
|
||||
indexdefs = {row[0]: row[1] for row in rows}
|
||||
assert "NULLS NOT DISTINCT" in indexdefs["uq_document_file_type_account"]
|
||||
assert "WHERE (released_at IS NULL)" in indexdefs["uq_consolidation_lock_version_active"]
|
||||
for name in ("uq_related_group_member_entity_current", "uq_related_group_member_group_entity_current"):
|
||||
for name in ("uq_related_group_member_entity_kind_current", "uq_related_group_member_group_entity_current"):
|
||||
assert indexdefs[name].startswith("CREATE UNIQUE INDEX")
|
||||
assert "WHERE is_current" in indexdefs[name]
|
||||
|
||||
@ -307,7 +413,12 @@ def test_offline_sql_creates_schema_before_version_table() -> None:
|
||||
assert "CREATE SCHEMA IF NOT EXISTS" not in output # требует права CREATE на базу даже при существующей схеме
|
||||
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 ('{HEAD}')" in output
|
||||
assert f"INSERT INTO dkb.alembic_version (version_num) VALUES ('{PREVIOUS}')" in output
|
||||
# с двумя ревизиями версия head пишется UPDATE'ом после 0001 — офлайн-SQL доходит до head
|
||||
assert (
|
||||
f"UPDATE dkb.alembic_version SET version_num='{HEAD}' WHERE dkb.alembic_version.version_num = '{PREVIOUS}'"
|
||||
in output
|
||||
)
|
||||
|
||||
|
||||
def test_cluster_commands_use_service_database() -> None:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""ORM частей 01–03 DBML v5.13 (справочники, доступ, учёт): чётность с DBML без БД и create_all на PostgreSQL."""
|
||||
"""ORM частей 01–03 DBML v5.14 (справочники, доступ, учёт): чётность с DBML без БД и create_all на PostgreSQL."""
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
@ -109,7 +109,7 @@ async def test_create_all_on_scratch_db(scratch_db_url: str) -> None:
|
||||
)
|
||||
)
|
||||
}
|
||||
for name in ("uq_related_group_member_group_entity_current", "uq_related_group_member_entity_current"):
|
||||
for name in ("uq_related_group_member_group_entity_current", "uq_related_group_member_entity_kind_current"):
|
||||
assert "UNIQUE" in indexdefs[name]
|
||||
assert "WHERE is_current" in indexdefs[name]
|
||||
pk = await conn.run_sync(lambda c: inspect(c).get_pk_constraint("role_permission", schema="dkb"))
|
||||
@ -122,9 +122,9 @@ async def test_create_all_on_scratch_db(scratch_db_url: str) -> None:
|
||||
|
||||
|
||||
async def _check_related_group_member_uniqueness(engine: AsyncEngine) -> None:
|
||||
"""ЮЛ — только в одной ГСК/ГСЗ среди текущих; закрытая версия не мешает (ТЗ 6.1, ANL-030)."""
|
||||
"""ЮЛ — в одной ГСК и одной ГСЗ одновременно (v5.14, решение владельца 04.10.2026); закрытая версия не мешает."""
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
ids = {k: uuid.uuid4() for k in ("ro", "cg", "le", "g1", "g2", "m1", "m2", "m3")}
|
||||
ids = {k: uuid.uuid4() for k in ("ro", "cg", "le", "g1", "g2", "g3", "m1", "m2", "m3", "m4")}
|
||||
async with engine.begin() as conn:
|
||||
await conn.execute(text("INSERT INTO dkb.regional_office (id, name) VALUES (:ro, 'ГО')"), ids)
|
||||
await conn.execute(
|
||||
@ -135,22 +135,28 @@ async def _check_related_group_member_uniqueness(engine: AsyncEngine) -> None:
|
||||
text("INSERT INTO dkb.legal_entity (id, inn, name, valid_from) VALUES (:le, '1', 'ЮЛ', :now)"),
|
||||
{**ids, "now": now},
|
||||
)
|
||||
for g, code in (("g1", "GSK-1"), ("g2", "GSK-2")):
|
||||
for g, code, kind in (("g1", "GSK-1", "gsk"), ("g2", "GSK-2", "gsk"), ("g3", "GSZ-1", "gsz")):
|
||||
await conn.execute(
|
||||
text(
|
||||
"INSERT INTO dkb.related_group (id, code, company_group_id, kind, name) "
|
||||
"VALUES (:g, :code, :cg, 'gsk', 'ГСК')"
|
||||
"VALUES (:g, :code, :cg, :kind, 'ГСК/ГСЗ')"
|
||||
),
|
||||
{"g": ids[g], "code": code, "cg": ids["cg"]},
|
||||
{"g": ids[g], "code": code, "cg": ids["cg"], "kind": kind},
|
||||
)
|
||||
insert = text(
|
||||
"INSERT INTO dkb.related_group_member (id, related_group_id, legal_entity_id, valid_from, is_current) "
|
||||
"VALUES (:id, :g, :le, :now, :cur)"
|
||||
"INSERT INTO dkb.related_group_member (id, related_group_id, legal_entity_id, kind, valid_from, is_current) "
|
||||
"VALUES (:id, :g, :le, :kind, :now, :cur)"
|
||||
)
|
||||
le, gsk, gsz = ids["le"], "gsk", "gsz"
|
||||
async with engine.begin() as conn:
|
||||
await conn.execute(insert, {"id": ids["m1"], "g": ids["g1"], "le": ids["le"], "now": now, "cur": True})
|
||||
# закрытая версия в другой группе — допустима
|
||||
await conn.execute(insert, {"id": ids["m2"], "g": ids["g2"], "le": ids["le"], "now": now, "cur": False})
|
||||
# текущее членство в ГСК g1
|
||||
await conn.execute(insert, {"id": ids["m1"], "g": ids["g1"], "le": le, "kind": gsk, "now": now, "cur": True})
|
||||
# текущее членство того же ЮЛ в ГСЗ g3 — допустимо с v5.14
|
||||
await conn.execute(insert, {"id": ids["m2"], "g": ids["g3"], "le": le, "kind": gsz, "now": now, "cur": True})
|
||||
# закрытая версия в другой ГСК — допустима
|
||||
await conn.execute(insert, {"id": ids["m3"], "g": ids["g2"], "le": le, "kind": gsk, "now": now, "cur": False})
|
||||
with pytest.raises(IntegrityError):
|
||||
async with engine.begin() as conn: # второе текущее членство того же ЮЛ — нарушение
|
||||
await conn.execute(insert, {"id": ids["m3"], "g": ids["g2"], "le": ids["le"], "now": now, "cur": True})
|
||||
async with engine.begin() as conn: # второе текущее членство того же ЮЛ в ГСК — нарушение
|
||||
await conn.execute(
|
||||
insert, {"id": ids["m4"], "g": ids["g2"], "le": le, "kind": gsk, "now": now, "cur": True}
|
||||
)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""ORM частей 04–06 DBML v5.13 (файлы, ОСВ, отчётность): чётность с DBML без БД и create_all на PostgreSQL."""
|
||||
"""ORM частей 04–06 DBML v5.14 (файлы, ОСВ, отчётность): чётность с DBML без БД и create_all на PostgreSQL."""
|
||||
|
||||
import datetime
|
||||
import decimal
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""ORM частей 07–09 DBML v5.13 (консолидация, КФО, аудит/ЕХ/прочее) и ИТОГОВАЯ полнота всех 43 таблиц."""
|
||||
"""ORM частей 07–09 DBML v5.14 (консолидация, КФО, аудит/ЕХ/прочее) и ИТОГОВАЯ полнота всех 43 таблиц."""
|
||||
|
||||
import datetime
|
||||
|
||||
@ -112,7 +112,7 @@ def test_ex_snapshot_has_no_fk_to_consolidation() -> None:
|
||||
|
||||
|
||||
def test_all_43_tables_registered_and_match_dbml_names() -> None:
|
||||
"""Состав metadata == таблицы DBML v5.13 (имена буквально), всего 43, все в схеме dkb."""
|
||||
"""Состав metadata == таблицы DBML v5.14 (имена буквально), всего 43, все в схеме dkb."""
|
||||
tables = list(Base.metadata.sorted_tables)
|
||||
assert len(tables) == EXPECTED_TABLE_COUNT
|
||||
assert all(t.schema == "dkb" for t in tables)
|
||||
|
||||
@ -50,16 +50,16 @@
|
||||
**Interfaces:**
|
||||
- Produces: `RelatedGroupMember.kind: Mapped[RelatedGroupKind]` (колонка между `legal_entity_id` и `valid_from`, как в DBML); `COMPANY_GROUP_CODE_SEQ = Sequence("company_group_code_seq", schema=SCHEMA, metadata=Base.metadata)`, `RELATED_GROUP_CODE_SEQ = Sequence("related_group_code_seq", …)` в `src/models/directories.py`; `AuditAction.LEGAL_ENTITY_CHANGE`, `AuditAction.USER_CHANGE`.
|
||||
|
||||
- [ ] **Step 1: Обновить тест уникальности членства (падает на старой схеме)**
|
||||
- [x] **Step 1: Обновить тест уникальности членства (падает на старой схеме)**
|
||||
|
||||
В `_check_related_group_member_uniqueness` (`tests/test_models_part_01_03.py`): INSERT в `related_group_member` получает колонку `kind`; сценарий: `g1` (gsk) текущее — ок; `g3` (gsz, создать третью группу) текущее для того же ЮЛ — **ок** (v5.14); `g2` (gsk) текущее — `IntegrityError`; закрытая версия в `g2` — ок. Докстринг: «ЮЛ — в одной ГСК и одной ГСЗ одновременно (v5.14, решение владельца 04.10.2026)».
|
||||
|
||||
- [ ] **Step 2: Запустить — убедиться, что падает**
|
||||
- [x] **Step 2: Запустить — убедиться, что падает**
|
||||
|
||||
Run: `cd api && .venv/bin/pytest -m integration tests/test_models_part_01_03.py -k related_group_member -v`
|
||||
Expected: FAIL (`column "kind" does not exist` или IntegrityError на gsz).
|
||||
|
||||
- [ ] **Step 3: ORM**
|
||||
- [x] **Step 3: ORM**
|
||||
|
||||
`src/models/directories.py`:
|
||||
```python
|
||||
@ -88,7 +88,7 @@ RELATED_GROUP_CODE_SEQ = Sequence("related_group_code_seq", schema=SCHEMA, metad
|
||||
```
|
||||
`src/models/enums.py`: `LEGAL_ENTITY_CHANGE = "LEGAL_ENTITY_CHANGE"` сразу после `GROUP_CHANGE`, `USER_CHANGE = "USER_CHANGE"` сразу после `USER_ROLE_CHANGE`. `src/models/__init__.py`: `DBML_VERSION = "5.14"`. Докстринги модулей `models/*.py`, `scripts/check_dbml_vs_orm.py`: «v5.13» → «v5.14» там, где речь о текущей версии.
|
||||
|
||||
- [ ] **Step 4: Миграция 0002**
|
||||
- [x] **Step 4: Миграция 0002**
|
||||
|
||||
`api/alembic/versions/0002_dbml_v5_14.py`:
|
||||
```python
|
||||
@ -164,17 +164,17 @@ def downgrade() -> None:
|
||||
```
|
||||
`alembic/env.py` менять не нужно. В `tests/test_migrations.py`: `HEAD = "0002_dbml_v5_14"`; добавить тест `test_downgrade_one_step` (сценарий спеки «Откат на один шаг»): `upgrade head` → `downgrade 0001_dbml_v5_13` → в `information_schema.columns` нет `related_group_member.kind`, в `pg_indexes` есть `uq_related_group_member_entity_current`, `enum_range(NULL::dkb.audit_action)` без `LEGAL_ENTITY_CHANGE` → `upgrade head` снова. Если `test_migrated_schema_equals_orm_create_all` сравнивает последовательности — они теперь есть и в metadata, и в миграции.
|
||||
|
||||
- [ ] **Step 5: Пути и версии**
|
||||
- [x] **Step 5: Пути и версии**
|
||||
|
||||
`Makefile:6` → `data-model-v5.14.dbml`; `scripts/dbml_parity.py:59` → `"data-model-v5.14.dbml"`; `tests/test_enums.py` → `"5.14"`; `tests/test_check_dbml_script.py` — фиктивный файл `v5.15`, ожидаемое сообщение с `5.15`/`5.14`.
|
||||
|
||||
- [ ] **Step 6: Проверить**
|
||||
- [x] **Step 6: Проверить**
|
||||
|
||||
Run: `cd api && .venv/bin/ruff check . && .venv/bin/ruff format --check . && .venv/bin/mypy src scripts && .venv/bin/pytest -m "not integration"` → PASS.
|
||||
Run: `make dbml-check` (из корня) → пустой вывод, код 0.
|
||||
Run: `cd api && .venv/bin/pytest -m integration tests/test_migrations.py tests/test_models_part_01_03.py -v` → PASS (цикл 0001→0002→base→head, пустой autogenerate, снимок схемы = create_all, откат на шаг, уникальность gsk+gsz).
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
- [x] **Step 7: Commit**
|
||||
|
||||
`git add -A && git commit -m "feat(model): DBML v5.14 — related_group_member.kind, audit_action +2, миграция 0002, последовательности кодов"` (+ трейлеры).
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ test-integration`). Пути — от корня репозитория. Доп
|
||||
|
||||
## 1. Модель v5.14 и инфраструктура запросов
|
||||
|
||||
- [ ] 1.1 **ORM и миграция 0002.** `api/src/models/directories.py`: `RelatedGroupMember.kind` (enum `related_group_kind`, NOT NULL),
|
||||
- [x] 1.1 **ORM и миграция 0002.** `api/src/models/directories.py`: `RelatedGroupMember.kind` (enum `related_group_kind`, NOT NULL),
|
||||
индекс `uq_related_group_member_entity_kind_current (legal_entity_id, kind) WHERE is_current` вместо `…_entity_current`;
|
||||
`enums.py`: `AuditAction.LEGAL_ENTITY_CHANGE` (после `GROUP_CHANGE`), `USER_CHANGE` (после `USER_ROLE_CHANGE`);
|
||||
`alembic/versions/0002_dbml_v5_14.py` (явный DDL: колонка с заполнением из `related_group`, индексы, `ALTER TYPE … ADD VALUE`,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user