Схема БД конфигурируемая: DB_SCHEMA (#38) #53

Merged
aep merged 1 commits from feat/38-db-schema-setting into main 2026-10-07 18:33:45 +03:00
21 changed files with 345 additions and 135 deletions

View File

@ -40,7 +40,7 @@
управляется только `NATS_ENABLED` (от `DEBUG` не зависит); при `DEBUG=false` шаблонный импорт
`raisa_fastapi_protected_api` в `app.py` условный (пакета нет вне банка — не ошибка); middleware РАИСА в Срезе 0
не подключается.
- **БД** — схема **`dkb`** (MetaData, `search_path`, Alembic); PK `uuid` через `UUIDPrimaryMixin`; ревизии в
- **БД** — схема **`dkb`** по умолчанию, имя конфигурируемо настройкой `DB_SCHEMA` (`OPENBAO__APP__DB_SCHEMA` в контуре; MetaData, `search_path`, Alembic читают `models.base.SCHEMA`, в миграциях — `from src.models.base import SCHEMA`, не литерал); PK `uuid` через `UUIDPrimaryMixin`; ревизии в
`api/alembic/versions/`; автогенерация — черновик, финал сверяется с DBML скриптом.
- **Ошибки** — `common/exceptions.py` → `core/exception_handlers.py`. Доменные ошибки (`DomainError`: `NotFoundError` 404
`NOT_FOUND`, `ConflictError` 409, `DomainValidationError` 422; код и доп. поля задаёт вызывающий) отдаются как

View File

@ -98,7 +98,7 @@ cp api/.env.example api/.env
### 4. Миграции и справочники
```bash
make migrate # alembic upgrade head: схема dkb, 27 enum-типов, 43 таблицы
make migrate # alembic upgrade head: схема `DB_SCHEMA` (по умолчанию dkb), 27 enum-типов, 43 таблицы
make seed-dev # справочники продукта + dev-пользователи (по одному на каждую роль) + dev-ГК под документы fixtures
```
@ -879,6 +879,7 @@ email без учёта регистра проверяет только сер
| `OPENBAO__APP__<ИМЯ>` | — | **контур платформы:** туда передаются только секреты OpenBao (`OPENBAO__*`), обычные переменные окружения задать нельзя. Любая настройка верхнего уровня из этой таблицы (`NATS_ENABLED`, `DEBUG`, `AUTH_DEV_ISSUER`, `FORWARDED_ALLOW_IPS`, `CORS_ORIGINS`, …) может прийти как `OPENBAO__APP__<ИМЯ>`, например `OPENBAO__APP__NATS_ENABLED=true`. Значения разбираются как из обычного env (bool, числа, списки). Приоритет: обычный env → `OPENBAO__APP__*` → `.env` → значение по умолчанию. Неизвестное имя — ошибка конфигурации при старте (опечатка не игнорируется); `OPENBAO__APP__OPENBAO__…` не допускается. Читается только из окружения процесса (в `.env` не действует). Реализация — `api/src/core/settings_sources.py` |
| `ENV_FILE` | `.env` | файл окружения (относительно `api/`) |
| `DEBUG` | `false` | режим разработки. `true` необходим dev-эмитенту токенов и `make seed-dev`, включает документацию API (`/docs`, `/redoc`, `/openapi.json`) и CORS `*`; при `false` документации нет (404), CORS — из `CORS_ORIGINS`, пакеты РАИСА подгружаются условно |
| `DB_SCHEMA` | `dkb` | схема PostgreSQL модели данных: MetaData ORM, `search_path` движка приложения, `CREATE SCHEMA` и таблица версий Alembic, схема в миграциях. Имя — строчный идентификатор `[a-z_][a-z0-9_]{0,62}`, не `pg_*` (подставляется в DDL без кавычек; иное — ошибка при старте). **Одно значение для приложения и для `alembic upgrade`:** сменить схему у уже мигрированной БД нельзя (таблицы остаются в старой); в контуре — `OPENBAO__APP__DB_SCHEMA`, в миграционном задании тоже. Читается при импорте моделей, поэтому скрипты и тесты видят его через те же `.env`/окружение |
| `FORWARDED_ALLOW_IPS` | `127.0.0.1` | доверенные прокси для `X-Forwarded-For`/`X-Forwarded-Proto` (`ProxyHeadersMiddleware`): адреса через запятую или `*`. Адрес клиента (`audit_event.ip_address`) берётся из заголовка только от них, от остальных заголовок игнорируется. На платформе — адреса ingress/балансировщика (OQ-65) |
| `CORS_ORIGINS` | пусто | при `DEBUG=false` — источники, которым разрешены кросс-доменные запросы: строка через запятую (`https://a.example.ru,https://b.example.ru`) или JSON-список. Пусто — кросс-доменные запросы запрещены (middleware CORS не подключается); `allow_credentials` — только для явного списка без `*`. При `DEBUG=true` не используется (всегда `*` без credentials: токен передаётся заголовком, cookie не нужны) |
| `SERVER__HOST`, `SERVER__PORT` | `0.0.0.0`, `8000` | адрес и порт сервера (`run_server.py`); в `.env.example` — `127.0.0.1` (dev-запуск только на loopback) |
@ -1020,7 +1021,7 @@ api/
api/dependencies/ зависимости FastAPI (current_user, проверка прав)
core/ config, auth (JWT), clients (S3), nats (подключение, publish, подписка),
logger, exception_handlers
db/ движок и сессия (схема dkb)
db/ движок и сессия (схема из `DB_SCHEMA`, по умолчанию dkb)
models/ ORM-модели, 1:1 с DBML
repositories/ доступ к данным
schemas/ pydantic-схемы запросов и ответов

View File

@ -4,7 +4,12 @@
# Контур платформы: обычные переменные окружения там задать нельзя, передаются только секреты OpenBao (OPENBAO__*).
# Любая настройка верхнего уровня ниже (не вложенные OPENBAO__<ГРУППА>__*) может прийти как OPENBAO__APP__<ИМЯ>, например:
# OPENBAO__APP__NATS_ENABLED=true
# OPENBAO__APP__DB_SCHEMA=dkb
# OPENBAO__APP__DEBUG=true
# Схема PostgreSQL модели данных (по умолчанию dkb). Одно значение для приложения и Alembic; идентификатор
# [a-z_][a-z0-9_]{0,62}, не pg_*. В контуре платформы: OPENBAO__APP__DB_SCHEMA
DB_SCHEMA=dkb
# OPENBAO__APP__AUTH_DEV_ISSUER=true
# OPENBAO__APP__FORWARDED_ALLOW_IPS=10.0.0.1,10.0.0.2
# Приоритет: обычный env -> OPENBAO__APP__* -> .env -> значение по умолчанию. Неизвестное имя — ошибка при старте.

View File

@ -4,6 +4,9 @@
DEBUG=true
# Схема БД: как в .env.example (тесты используют ту же настройку, что и приложение)
DB_SCHEMA=dkb
# Как в .env.example (тесты сервер не запускают)
SERVER__HOST=127.0.0.1
SERVER__PORT=8000

View File

@ -1,4 +1,4 @@
"""Окружение Alembic: схема `dkb`, async-движок, URL БД из настроек или из вызывающего кода.
"""Окружение Alembic: схема `settings.DB_SCHEMA` (по умолчанию `dkb`), async-движок, URL БД из настроек или вызывающего.
URL БД (по убыванию приоритета):
1. `alembic -x dburl=<url> ...` — для тестов и разовых запусков против другой БД (временной, scratch);
@ -6,7 +6,7 @@ URL БД (по убыванию приоритета):
шаблонной заглушки `driver://...` из `alembic.ini`;
3. `settings.OPENBAO.DATABASE.DATABASE_URL` (`.env` / `ENV_FILE`) — обычный `alembic upgrade head` / `make migrate`.
Схема `dkb` создаётся до таблицы версий `dkb.alembic_version` и до миграций — только если её нет: PostgreSQL проверяет
Схема создаётся до таблицы версий `<схема>.alembic_version` и до миграций — только если её нет: PostgreSQL проверяет
право CREATE на базу раньше, чем `IF NOT EXISTS`, а платформа может выдать роли миграций готовую схему без этого права.
"""
@ -19,8 +19,8 @@ from sqlalchemy.ext.asyncio import create_async_engine
from alembic import context
from src.core.config import settings
from src.models import Base # модели регистрируются в src/models/__init__.py
from src.models.base import SCHEMA
SCHEMA = "dkb"
#: Значение `sqlalchemy.url` в шаблонном `alembic.ini` — признак «URL не задан вызывающим кодом».
INI_PLACEHOLDER_PREFIX = "driver://"
#: Офлайн-вариант условного создания схемы: условие проверяется при применении SQL-скрипта, а не при его генерации.
@ -49,7 +49,7 @@ def database_url() -> str:
def include_name(name: str | None, type_: str, parent_names: object) -> bool:
"""Autogenerate видит только схему `dkb` (схема по умолчанию `public` и прочие — не наши)."""
"""Autogenerate видит только схему модели (`SCHEMA`) (схема по умолчанию `public` и прочие — не наши)."""
if type_ == "schema":
return name == SCHEMA
return True
@ -82,7 +82,7 @@ def run_migrations_offline() -> None:
def do_run_migrations(connection: Connection) -> None:
# Схема нужна до того, как alembic создаст `dkb.alembic_version`. CREATE — только если схемы нет (см. docstring
# Схема нужна до того, как alembic создаст `<схема>.alembic_version`. CREATE — только если схемы нет (см. docstring
# модуля). Транзакцию закрываем сразу: иначе autobegin оставил бы соединение во «внешней» транзакции и alembic
# не стал бы коммитить миграции.
exists = connection.execute(text("SELECT 1 FROM pg_namespace WHERE nspname = :name"), {"name": SCHEMA}).scalar()

View File

@ -1,4 +1,4 @@
"""DBML v5.13: вся модель данных АРМ КА с нуля (схема dkb, 27 enum-типов, 43 таблицы, ограничения, индексы).
"""DBML v5.13: вся модель данных АРМ КА с нуля (схема БД, 27 enum-типов, 43 таблицы, ограничения, индексы).
Ревизия заморожена: явный DDL, модели `src.models` не импортируются. Имена ограничений и индексов совпадают с ORM
(naming_convention `src/models/base.py` + явные имена); `alembic check` после upgrade не находит расхождений.
@ -16,13 +16,13 @@ import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
from src.models.base import SCHEMA
revision: str = "0001_dbml_v5_13"
down_revision: str | Sequence[str] | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
SCHEMA = "dkb"
FK_CYCLE = "fk_stored_file_current_recognition_job_id_recognition_job"
#: Схема создаётся только при отсутствии (и без env.py, и в офлайн-SQL): PostgreSQL проверяет право CREATE на базу
#: раньше, чем `IF NOT EXISTS`, а роли миграций платформа может выдать готовую схему без этого права.
@ -33,7 +33,7 @@ CREATE_SCHEMA_IF_MISSING_SQL = (
def _enum(name: str, *values: str) -> postgresql.ENUM:
"""Нативный PG-enum `dkb.<name>`; тип создаётся явно в upgrade(), а не неявно из create_table."""
"""Нативный PG-enum `<схема>.<name>`; тип создаётся явно в upgrade(), а не неявно из create_table."""
return postgresql.ENUM(*values, name=name, schema=SCHEMA, create_type=False)
@ -177,7 +177,7 @@ TABLES: tuple[str, ...] = (
def upgrade() -> None:
"""Схема dkb (если её нет) -> 27 enum-типов -> 43 таблицы с их индексами в порядке зависимостей FK.
"""Схема БД (если её нет) -> 27 enum-типов -> 43 таблицы с их индексами в порядке зависимостей FK.
FK цикла `stored_file.current_recognition_job_id` добавляется сразу после создания `recognition_job`.
"""
@ -195,7 +195,9 @@ def upgrade() -> None:
sa.Column("row_type", ENUMS["account_row_type"], nullable=False),
sa.Column("is_off_balance", sa.Boolean(), server_default=sa.text("false"), nullable=False),
sa.ForeignKeyConstraint(
["parent_id"], ["dkb.chart_of_account.id"], name=op.f("fk_chart_of_account_parent_id_chart_of_account")
["parent_id"],
[f"{SCHEMA}.chart_of_account.id"],
name=op.f("fk_chart_of_account_parent_id_chart_of_account"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_chart_of_account")),
sa.UniqueConstraint("code", name=op.f("uq_chart_of_account_code")),
@ -296,7 +298,7 @@ def upgrade() -> None:
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
["regional_office_id"],
["dkb.regional_office.id"],
[f"{SCHEMA}.regional_office.id"],
name=op.f("fk_app_user_regional_office_id_regional_office"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_app_user")),
@ -313,11 +315,13 @@ def upgrade() -> None:
sa.Column("is_excluded", sa.Boolean(), server_default=sa.text("false"), nullable=False),
sa.ForeignKeyConstraint(
["account_id"],
["dkb.chart_of_account.id"],
[f"{SCHEMA}.chart_of_account.id"],
name=op.f("fk_elimination_rule_account_account_id_chart_of_account"),
),
sa.ForeignKeyConstraint(
["rule_id"], ["dkb.elimination_rule.id"], name=op.f("fk_elimination_rule_account_rule_id_elimination_rule")
["rule_id"],
[f"{SCHEMA}.elimination_rule.id"],
name=op.f("fk_elimination_rule_account_rule_id_elimination_rule"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_elimination_rule_account")),
schema=SCHEMA,
@ -331,7 +335,7 @@ def upgrade() -> None:
sa.Column("is_mandatory", sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(
["account_id"],
["dkb.chart_of_account.id"],
[f"{SCHEMA}.chart_of_account.id"],
name=op.f("fk_required_document_rule_account_id_chart_of_account"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_required_document_rule")),
@ -352,7 +356,7 @@ def upgrade() -> None:
sa.Column("text", sa.Text(), nullable=False),
sa.Column("author_id", sa.UUID(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["author_id"], ["dkb.app_user.id"], name=op.f("fk_comment_author_id_app_user")),
sa.ForeignKeyConstraint(["author_id"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_comment_author_id_app_user")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_comment")),
schema=SCHEMA,
)
@ -373,10 +377,12 @@ def upgrade() -> None:
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
["base_regional_office_id"],
["dkb.regional_office.id"],
[f"{SCHEMA}.regional_office.id"],
name=op.f("fk_company_group_base_regional_office_id_regional_office"),
),
sa.ForeignKeyConstraint(["updated_by"], ["dkb.app_user.id"], name=op.f("fk_company_group_updated_by_app_user")),
sa.ForeignKeyConstraint(
["updated_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_company_group_updated_by_app_user")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_company_group")),
sa.UniqueConstraint("code", name=op.f("uq_company_group_code")),
sa.UniqueConstraint("crm_id", name=op.f("uq_company_group_crm_id")),
@ -389,12 +395,12 @@ def upgrade() -> None:
sa.Column("granted_by", sa.UUID(), nullable=True),
sa.Column("granted_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
["granted_by"], ["dkb.app_user.id"], name=op.f("fk_role_permission_granted_by_app_user")
["granted_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_role_permission_granted_by_app_user")
),
sa.ForeignKeyConstraint(
["permission_id"], ["dkb.permission.id"], name=op.f("fk_role_permission_permission_id_permission")
["permission_id"], [f"{SCHEMA}.permission.id"], name=op.f("fk_role_permission_permission_id_permission")
),
sa.ForeignKeyConstraint(["role_id"], ["dkb.role.id"], name=op.f("fk_role_permission_role_id_role")),
sa.ForeignKeyConstraint(["role_id"], [f"{SCHEMA}.role.id"], name=op.f("fk_role_permission_role_id_role")),
sa.PrimaryKeyConstraint("role_id", "permission_id", name=op.f("pk_role_permission")),
schema=SCHEMA,
)
@ -403,8 +409,8 @@ def upgrade() -> None:
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("user_id", sa.UUID(), nullable=False),
sa.Column("role_id", sa.UUID(), nullable=False),
sa.ForeignKeyConstraint(["role_id"], ["dkb.role.id"], name=op.f("fk_user_role_role_id_role")),
sa.ForeignKeyConstraint(["user_id"], ["dkb.app_user.id"], name=op.f("fk_user_role_user_id_app_user")),
sa.ForeignKeyConstraint(["role_id"], [f"{SCHEMA}.role.id"], name=op.f("fk_user_role_role_id_role")),
sa.ForeignKeyConstraint(["user_id"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_user_role_user_id_app_user")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_user_role")),
schema=SCHEMA,
)
@ -420,11 +426,11 @@ def upgrade() -> None:
sa.Column("valid_to", sa.DateTime(timezone=True), nullable=True),
sa.Column("changed_by", sa.UUID(), nullable=True),
sa.ForeignKeyConstraint(
["changed_by"], ["dkb.app_user.id"], name=op.f("fk_company_group_history_changed_by_app_user")
["changed_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_company_group_history_changed_by_app_user")
),
sa.ForeignKeyConstraint(
["company_group_id"],
["dkb.company_group.id"],
[f"{SCHEMA}.company_group.id"],
name=op.f("fk_company_group_history_company_group_id_company_group"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_company_group_history")),
@ -452,14 +458,18 @@ def upgrade() -> None:
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
["company_group_id"], ["dkb.company_group.id"], name=op.f("fk_legal_entity_company_group_id_company_group")
["company_group_id"],
[f"{SCHEMA}.company_group.id"],
name=op.f("fk_legal_entity_company_group_id_company_group"),
),
sa.ForeignKeyConstraint(
["regional_office_id"],
["dkb.regional_office.id"],
[f"{SCHEMA}.regional_office.id"],
name=op.f("fk_legal_entity_regional_office_id_regional_office"),
),
sa.ForeignKeyConstraint(["updated_by"], ["dkb.app_user.id"], name=op.f("fk_legal_entity_updated_by_app_user")),
sa.ForeignKeyConstraint(
["updated_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_legal_entity_updated_by_app_user")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_legal_entity")),
sa.UniqueConstraint("crm_id", name=op.f("uq_legal_entity_crm_id")),
sa.UniqueConstraint("inn", name=op.f("uq_legal_entity_inn")),
@ -474,7 +484,9 @@ def upgrade() -> None:
sa.Column("name", sa.String(), nullable=False),
sa.Column("created_date", sa.Date(), nullable=True),
sa.ForeignKeyConstraint(
["company_group_id"], ["dkb.company_group.id"], name=op.f("fk_related_group_company_group_id_company_group")
["company_group_id"],
[f"{SCHEMA}.company_group.id"],
name=op.f("fk_related_group_company_group_id_company_group"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_related_group")),
sa.UniqueConstraint("code", name=op.f("uq_related_group_code")),
@ -490,10 +502,14 @@ def upgrade() -> None:
sa.Column("reporting_date", sa.Date(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
["company_group_id"], ["dkb.company_group.id"], name=op.f("fk_consolidation_company_group_id_company_group")
["company_group_id"],
[f"{SCHEMA}.company_group.id"],
name=op.f("fk_consolidation_company_group_id_company_group"),
),
sa.ForeignKeyConstraint(
["related_group_id"], ["dkb.related_group.id"], name=op.f("fk_consolidation_related_group_id_related_group")
["related_group_id"],
[f"{SCHEMA}.related_group.id"],
name=op.f("fk_consolidation_related_group_id_related_group"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_consolidation")),
schema=SCHEMA,
@ -524,17 +540,23 @@ def upgrade() -> None:
sa.Column("archived_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("data_lake_ref", sa.String(), nullable=True),
sa.ForeignKeyConstraint(
["company_group_id"], ["dkb.company_group.id"], name=op.f("fk_ex_snapshot_company_group_id_company_group")
["company_group_id"],
[f"{SCHEMA}.company_group.id"],
name=op.f("fk_ex_snapshot_company_group_id_company_group"),
),
sa.ForeignKeyConstraint(
["related_group_id"], ["dkb.related_group.id"], name=op.f("fk_ex_snapshot_related_group_id_related_group")
["related_group_id"],
[f"{SCHEMA}.related_group.id"],
name=op.f("fk_ex_snapshot_related_group_id_related_group"),
),
sa.ForeignKeyConstraint(
["superseded_by_snapshot_id"],
["dkb.ex_snapshot.id"],
[f"{SCHEMA}.ex_snapshot.id"],
name=op.f("fk_ex_snapshot_superseded_by_snapshot_id_ex_snapshot"),
),
sa.ForeignKeyConstraint(["written_by"], ["dkb.app_user.id"], name=op.f("fk_ex_snapshot_written_by_app_user")),
sa.ForeignKeyConstraint(
["written_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_ex_snapshot_written_by_app_user")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_ex_snapshot")),
sa.UniqueConstraint("source_version_id", name=op.f("uq_ex_snapshot_source_version_id")),
schema=SCHEMA,
@ -562,11 +584,11 @@ def upgrade() -> None:
sa.Column("created_by", sa.UUID(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
["created_by"], ["dkb.app_user.id"], name=op.f("fk_legal_entity_alias_created_by_app_user")
["created_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_legal_entity_alias_created_by_app_user")
),
sa.ForeignKeyConstraint(
["legal_entity_id"],
["dkb.legal_entity.id"],
[f"{SCHEMA}.legal_entity.id"],
name=op.f("fk_legal_entity_alias_legal_entity_id_legal_entity"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_legal_entity_alias")),
@ -603,11 +625,11 @@ def upgrade() -> None:
sa.Column("valid_to", sa.DateTime(timezone=True), nullable=True),
sa.Column("changed_by", sa.UUID(), nullable=True),
sa.ForeignKeyConstraint(
["changed_by"], ["dkb.app_user.id"], name=op.f("fk_legal_entity_history_changed_by_app_user")
["changed_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_legal_entity_history_changed_by_app_user")
),
sa.ForeignKeyConstraint(
["legal_entity_id"],
["dkb.legal_entity.id"],
[f"{SCHEMA}.legal_entity.id"],
name=op.f("fk_legal_entity_history_legal_entity_id_legal_entity"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_legal_entity_history")),
@ -630,16 +652,16 @@ def upgrade() -> None:
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")
["created_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_related_group_member_created_by_app_user")
),
sa.ForeignKeyConstraint(
["legal_entity_id"],
["dkb.legal_entity.id"],
[f"{SCHEMA}.legal_entity.id"],
name=op.f("fk_related_group_member_legal_entity_id_legal_entity"),
),
sa.ForeignKeyConstraint(
["related_group_id"],
["dkb.related_group.id"],
[f"{SCHEMA}.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")),
@ -678,17 +700,19 @@ def upgrade() -> None:
sa.Column("uploaded_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
["legal_entity_id"], ["dkb.legal_entity.id"], name=op.f("fk_stored_file_legal_entity_id_legal_entity")
["legal_entity_id"], [f"{SCHEMA}.legal_entity.id"], name=op.f("fk_stored_file_legal_entity_id_legal_entity")
),
sa.ForeignKeyConstraint(
["s3_object_id"], ["dkb.s3_object.id"], name=op.f("fk_stored_file_s3_object_id_s3_object")
["s3_object_id"], [f"{SCHEMA}.s3_object.id"], name=op.f("fk_stored_file_s3_object_id_s3_object")
),
sa.ForeignKeyConstraint(
["superseded_by_file_id"],
["dkb.stored_file.id"],
[f"{SCHEMA}.stored_file.id"],
name=op.f("fk_stored_file_superseded_by_file_id_stored_file"),
),
sa.ForeignKeyConstraint(["uploaded_by"], ["dkb.app_user.id"], name=op.f("fk_stored_file_uploaded_by_app_user")),
sa.ForeignKeyConstraint(
["uploaded_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_stored_file_uploaded_by_app_user")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_stored_file")),
schema=SCHEMA,
)
@ -726,34 +750,38 @@ def upgrade() -> None:
sa.Column("updated_by", sa.UUID(), nullable=True),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
["approved_in_rf_by"], ["dkb.app_user.id"], name=op.f("fk_consolidation_version_approved_in_rf_by_app_user")
["approved_in_rf_by"],
[f"{SCHEMA}.app_user.id"],
name=op.f("fk_consolidation_version_approved_in_rf_by_app_user"),
),
sa.ForeignKeyConstraint(
["consolidation_id"],
["dkb.consolidation.id"],
[f"{SCHEMA}.consolidation.id"],
name=op.f("fk_consolidation_version_consolidation_id_consolidation"),
),
sa.ForeignKeyConstraint(
["created_by"], ["dkb.app_user.id"], name=op.f("fk_consolidation_version_created_by_app_user")
["created_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_consolidation_version_created_by_app_user")
),
sa.ForeignKeyConstraint(
["initiator_id"], ["dkb.app_user.id"], name=op.f("fk_consolidation_version_initiator_id_app_user")
["initiator_id"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_consolidation_version_initiator_id_app_user")
),
sa.ForeignKeyConstraint(
["initiator_office_id"],
["dkb.regional_office.id"],
[f"{SCHEMA}.regional_office.id"],
name=op.f("fk_consolidation_version_initiator_office_id_regional_office"),
),
sa.ForeignKeyConstraint(
["previous_version_id"],
["dkb.consolidation_version.id"],
[f"{SCHEMA}.consolidation_version.id"],
name="fk_consolidation_version_previous_version_id",
),
sa.ForeignKeyConstraint(
["updated_by"], ["dkb.app_user.id"], name=op.f("fk_consolidation_version_updated_by_app_user")
["updated_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_consolidation_version_updated_by_app_user")
),
sa.ForeignKeyConstraint(
["verified_in_go_by"], ["dkb.app_user.id"], name=op.f("fk_consolidation_version_verified_in_go_by_app_user")
["verified_in_go_by"],
[f"{SCHEMA}.app_user.id"],
name=op.f("fk_consolidation_version_verified_in_go_by_app_user"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_consolidation_version")),
schema=SCHEMA,
@ -784,12 +812,14 @@ def upgrade() -> None:
sa.Column("confirmed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(["confirmed_by"], ["dkb.app_user.id"], name=op.f("fk_document_confirmed_by_app_user")),
sa.ForeignKeyConstraint(
["legal_entity_id"], ["dkb.legal_entity.id"], name=op.f("fk_document_legal_entity_id_legal_entity")
["confirmed_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_document_confirmed_by_app_user")
),
sa.ForeignKeyConstraint(
["stored_file_id"], ["dkb.stored_file.id"], name=op.f("fk_document_stored_file_id_stored_file")
["legal_entity_id"], [f"{SCHEMA}.legal_entity.id"], name=op.f("fk_document_legal_entity_id_legal_entity")
),
sa.ForeignKeyConstraint(
["stored_file_id"], [f"{SCHEMA}.stored_file.id"], name=op.f("fk_document_stored_file_id_stored_file")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_document")),
schema=SCHEMA,
@ -816,11 +846,13 @@ def upgrade() -> None:
sa.Column("inn", sa.String(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.ForeignKeyConstraint(
["ex_snapshot_id"], ["dkb.ex_snapshot.id"], name=op.f("fk_ex_snapshot_member_ex_snapshot_id_ex_snapshot")
["ex_snapshot_id"],
[f"{SCHEMA}.ex_snapshot.id"],
name=op.f("fk_ex_snapshot_member_ex_snapshot_id_ex_snapshot"),
),
sa.ForeignKeyConstraint(
["legal_entity_id"],
["dkb.legal_entity.id"],
[f"{SCHEMA}.legal_entity.id"],
name=op.f("fk_ex_snapshot_member_legal_entity_id_legal_entity"),
),
sa.PrimaryKeyConstraint("ex_snapshot_id", "legal_entity_id", name=op.f("pk_ex_snapshot_member")),
@ -856,10 +888,12 @@ def upgrade() -> None:
sa.Column("fields_low_confidence", sa.Integer(), nullable=True),
sa.Column("fields_unrecognized", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["file_id"], ["dkb.stored_file.id"], name=op.f("fk_recognition_job_file_id_stored_file")
["file_id"], [f"{SCHEMA}.stored_file.id"], name=op.f("fk_recognition_job_file_id_stored_file")
),
sa.ForeignKeyConstraint(
["result_s3_object_id"], ["dkb.s3_object.id"], name=op.f("fk_recognition_job_result_s3_object_id_s3_object")
["result_s3_object_id"],
[f"{SCHEMA}.s3_object.id"],
name=op.f("fk_recognition_job_result_s3_object_id_s3_object"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_recognition_job")),
sa.UniqueConstraint("request_msg_id", name=op.f("uq_recognition_job_request_msg_id")),
@ -899,10 +933,10 @@ def upgrade() -> None:
sa.Column("user_agent", sa.String(), nullable=True),
sa.ForeignKeyConstraint(
["consolidation_version_id"],
["dkb.consolidation_version.id"],
[f"{SCHEMA}.consolidation_version.id"],
name=op.f("fk_audit_event_consolidation_version_id_consolidation_version"),
),
sa.ForeignKeyConstraint(["user_id"], ["dkb.app_user.id"], name=op.f("fk_audit_event_user_id_app_user")),
sa.ForeignKeyConstraint(["user_id"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_audit_event_user_id_app_user")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_audit_event")),
schema=SCHEMA,
)
@ -937,11 +971,11 @@ def upgrade() -> None:
sa.Column("entered_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
["consolidation_version_id"],
["dkb.consolidation_version.id"],
[f"{SCHEMA}.consolidation_version.id"],
name="fk_client_kfo_line_consolidation_version_id",
),
sa.ForeignKeyConstraint(
["entered_by"], ["dkb.app_user.id"], name=op.f("fk_client_kfo_line_entered_by_app_user")
["entered_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_client_kfo_line_entered_by_app_user")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_client_kfo_line")),
schema=SCHEMA,
@ -960,11 +994,11 @@ def upgrade() -> None:
sa.Column("document_id", sa.UUID(), nullable=False),
sa.ForeignKeyConstraint(
["consolidation_version_id"],
["dkb.consolidation_version.id"],
[f"{SCHEMA}.consolidation_version.id"],
name="fk_consolidation_document_consolidation_version_id",
),
sa.ForeignKeyConstraint(
["document_id"], ["dkb.document.id"], name=op.f("fk_consolidation_document_document_id_document")
["document_id"], [f"{SCHEMA}.document.id"], name=op.f("fk_consolidation_document_document_id_document")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_consolidation_document")),
schema=SCHEMA,
@ -987,14 +1021,14 @@ def upgrade() -> None:
sa.Column("takeover_reason", sa.Text(), nullable=True),
sa.ForeignKeyConstraint(
["consolidation_version_id"],
["dkb.consolidation_version.id"],
[f"{SCHEMA}.consolidation_version.id"],
name="fk_consolidation_lock_consolidation_version_id",
),
sa.ForeignKeyConstraint(
["locked_by"], ["dkb.app_user.id"], name=op.f("fk_consolidation_lock_locked_by_app_user")
["locked_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_consolidation_lock_locked_by_app_user")
),
sa.ForeignKeyConstraint(
["taken_over_from"], ["dkb.app_user.id"], name=op.f("fk_consolidation_lock_taken_over_from_app_user")
["taken_over_from"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_consolidation_lock_taken_over_from_app_user")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_consolidation_lock")),
schema=SCHEMA,
@ -1015,12 +1049,12 @@ def upgrade() -> None:
sa.Column("is_head_entity", sa.Boolean(), server_default=sa.text("false"), nullable=False),
sa.ForeignKeyConstraint(
["consolidation_version_id"],
["dkb.consolidation_version.id"],
[f"{SCHEMA}.consolidation_version.id"],
name="fk_consolidation_member_consolidation_version_id",
),
sa.ForeignKeyConstraint(
["legal_entity_id"],
["dkb.legal_entity.id"],
[f"{SCHEMA}.legal_entity.id"],
name=op.f("fk_consolidation_member_legal_entity_id_legal_entity"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_consolidation_member")),
@ -1043,10 +1077,10 @@ def upgrade() -> None:
sa.Column("changed_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("comment", sa.Text(), nullable=True),
sa.ForeignKeyConstraint(
["changed_by"], ["dkb.app_user.id"], name=op.f("fk_consolidation_status_history_changed_by_app_user")
["changed_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_consolidation_status_history_changed_by_app_user")
),
sa.ForeignKeyConstraint(
["version_id"], ["dkb.consolidation_version.id"], name="fk_consolidation_status_history_version_id"
["version_id"], [f"{SCHEMA}.consolidation_version.id"], name="fk_consolidation_status_history_version_id"
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_consolidation_status_history")),
schema=SCHEMA,
@ -1078,33 +1112,39 @@ def upgrade() -> None:
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
["consolidation_version_id"],
["dkb.consolidation_version.id"],
[f"{SCHEMA}.consolidation_version.id"],
name=op.f("fk_correction_consolidation_version_id_consolidation_version"),
),
sa.ForeignKeyConstraint(
["counterparty_id"], ["dkb.legal_entity.id"], name=op.f("fk_correction_counterparty_id_legal_entity")
["counterparty_id"], [f"{SCHEMA}.legal_entity.id"], name=op.f("fk_correction_counterparty_id_legal_entity")
),
sa.ForeignKeyConstraint(
["created_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_correction_created_by_app_user")
),
sa.ForeignKeyConstraint(["created_by"], ["dkb.app_user.id"], name=op.f("fk_correction_created_by_app_user")),
sa.ForeignKeyConstraint(
["credit_account_id"],
["dkb.chart_of_account.id"],
[f"{SCHEMA}.chart_of_account.id"],
name=op.f("fk_correction_credit_account_id_chart_of_account"),
),
sa.ForeignKeyConstraint(
["debit_account_id"],
["dkb.chart_of_account.id"],
[f"{SCHEMA}.chart_of_account.id"],
name=op.f("fk_correction_debit_account_id_chart_of_account"),
),
sa.ForeignKeyConstraint(
["legal_entity_id"], ["dkb.legal_entity.id"], name=op.f("fk_correction_legal_entity_id_legal_entity")
["legal_entity_id"], [f"{SCHEMA}.legal_entity.id"], name=op.f("fk_correction_legal_entity_id_legal_entity")
),
sa.ForeignKeyConstraint(
["parent_correction_id"], ["dkb.correction.id"], name=op.f("fk_correction_parent_correction_id_correction")
["parent_correction_id"],
[f"{SCHEMA}.correction.id"],
name=op.f("fk_correction_parent_correction_id_correction"),
),
sa.ForeignKeyConstraint(
["rule_id"], ["dkb.elimination_rule.id"], name=op.f("fk_correction_rule_id_elimination_rule")
["rule_id"], [f"{SCHEMA}.elimination_rule.id"], name=op.f("fk_correction_rule_id_elimination_rule")
),
sa.ForeignKeyConstraint(
["updated_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_correction_updated_by_app_user")
),
sa.ForeignKeyConstraint(["updated_by"], ["dkb.app_user.id"], name=op.f("fk_correction_updated_by_app_user")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_correction")),
schema=SCHEMA,
)
@ -1132,13 +1172,17 @@ def upgrade() -> None:
sa.Column("payload", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column("payload_schema_version", sa.String(), nullable=True),
sa.ForeignKeyConstraint(
["document_id"], ["dkb.document.id"], name=op.f("fk_ex_snapshot_item_document_id_document")
["document_id"], [f"{SCHEMA}.document.id"], name=op.f("fk_ex_snapshot_item_document_id_document")
),
sa.ForeignKeyConstraint(
["ex_snapshot_id"], ["dkb.ex_snapshot.id"], name=op.f("fk_ex_snapshot_item_ex_snapshot_id_ex_snapshot")
["ex_snapshot_id"],
[f"{SCHEMA}.ex_snapshot.id"],
name=op.f("fk_ex_snapshot_item_ex_snapshot_id_ex_snapshot"),
),
sa.ForeignKeyConstraint(
["legal_entity_id"], ["dkb.legal_entity.id"], name=op.f("fk_ex_snapshot_item_legal_entity_id_legal_entity")
["legal_entity_id"],
[f"{SCHEMA}.legal_entity.id"],
name=op.f("fk_ex_snapshot_item_legal_entity_id_legal_entity"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_ex_snapshot_item")),
schema=SCHEMA,
@ -1187,19 +1231,19 @@ def upgrade() -> None:
sa.Column("checks_accepted_comment", sa.Text(), nullable=True),
sa.ForeignKeyConstraint(
["checks_accepted_by"],
["dkb.app_user.id"],
[f"{SCHEMA}.app_user.id"],
name=op.f("fk_financial_statement_meta_checks_accepted_by_app_user"),
),
sa.ForeignKeyConstraint(
["document_id"], ["dkb.document.id"], name=op.f("fk_financial_statement_meta_document_id_document")
["document_id"], [f"{SCHEMA}.document.id"], name=op.f("fk_financial_statement_meta_document_id_document")
),
sa.ForeignKeyConstraint(
["recognition_job_id"],
["dkb.recognition_job.id"],
[f"{SCHEMA}.recognition_job.id"],
name=op.f("fk_financial_statement_meta_recognition_job_id_recognition_job"),
),
sa.ForeignKeyConstraint(
["reviewed_by"], ["dkb.app_user.id"], name=op.f("fk_financial_statement_meta_reviewed_by_app_user")
["reviewed_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_financial_statement_meta_reviewed_by_app_user")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_financial_statement_meta")),
sa.UniqueConstraint("document_id", name=op.f("uq_financial_statement_meta_document_id")),
@ -1225,21 +1269,25 @@ def upgrade() -> None:
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
["consolidation_version_id"],
["dkb.consolidation_version.id"],
[f"{SCHEMA}.consolidation_version.id"],
name=op.f("fk_goodwill_consolidation_version_id_consolidation_version"),
),
sa.ForeignKeyConstraint(["created_by"], ["dkb.app_user.id"], name=op.f("fk_goodwill_created_by_app_user")),
sa.ForeignKeyConstraint(
["created_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_goodwill_created_by_app_user")
),
sa.ForeignKeyConstraint(
["head_legal_entity_id"],
["dkb.legal_entity.id"],
[f"{SCHEMA}.legal_entity.id"],
name=op.f("fk_goodwill_head_legal_entity_id_legal_entity"),
),
sa.ForeignKeyConstraint(
["subsidiary_legal_entity_id"],
["dkb.legal_entity.id"],
[f"{SCHEMA}.legal_entity.id"],
name=op.f("fk_goodwill_subsidiary_legal_entity_id_legal_entity"),
),
sa.ForeignKeyConstraint(["updated_by"], ["dkb.app_user.id"], name=op.f("fk_goodwill_updated_by_app_user")),
sa.ForeignKeyConstraint(
["updated_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_goodwill_updated_by_app_user")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_goodwill")),
schema=SCHEMA,
)
@ -1269,7 +1317,7 @@ def upgrade() -> None:
sa.Column("has_deviations", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["consolidation_version_id"],
["dkb.consolidation_version.id"],
[f"{SCHEMA}.consolidation_version.id"],
name=op.f("fk_kfo_result_consolidation_version_id_consolidation_version"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_kfo_result")),
@ -1307,24 +1355,28 @@ def upgrade() -> None:
sa.Column("modified_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("is_excisable", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["account_id"], ["dkb.chart_of_account.id"], name=op.f("fk_osv_entry_account_id_chart_of_account")
["account_id"], [f"{SCHEMA}.chart_of_account.id"], name=op.f("fk_osv_entry_account_id_chart_of_account")
),
sa.ForeignKeyConstraint(
["counterparty_id"], ["dkb.legal_entity.id"], name=op.f("fk_osv_entry_counterparty_id_legal_entity")
["counterparty_id"], [f"{SCHEMA}.legal_entity.id"], name=op.f("fk_osv_entry_counterparty_id_legal_entity")
),
sa.ForeignKeyConstraint(
["document_id"], [f"{SCHEMA}.document.id"], name=op.f("fk_osv_entry_document_id_document")
),
sa.ForeignKeyConstraint(
["modified_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_osv_entry_modified_by_app_user")
),
sa.ForeignKeyConstraint(["document_id"], ["dkb.document.id"], name=op.f("fk_osv_entry_document_id_document")),
sa.ForeignKeyConstraint(["modified_by"], ["dkb.app_user.id"], name=op.f("fk_osv_entry_modified_by_app_user")),
sa.ForeignKeyConstraint(
["owner_legal_entity_id"],
["dkb.legal_entity.id"],
[f"{SCHEMA}.legal_entity.id"],
name=op.f("fk_osv_entry_owner_legal_entity_id_legal_entity"),
),
sa.ForeignKeyConstraint(
["parent_entry_id"], ["dkb.osv_entry.id"], name=op.f("fk_osv_entry_parent_entry_id_osv_entry")
["parent_entry_id"], [f"{SCHEMA}.osv_entry.id"], name=op.f("fk_osv_entry_parent_entry_id_osv_entry")
),
sa.ForeignKeyConstraint(
["recognition_job_id"],
["dkb.recognition_job.id"],
[f"{SCHEMA}.recognition_job.id"],
name=op.f("fk_osv_entry_recognition_job_id_recognition_job"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_osv_entry")),
@ -1350,7 +1402,7 @@ def upgrade() -> None:
sa.Column("is_current", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["meta_id"],
["dkb.financial_statement_meta.id"],
[f"{SCHEMA}.financial_statement_meta.id"],
name=op.f("fk_financial_statement_column_meta_id_financial_statement_meta"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_financial_statement_column")),
@ -1377,7 +1429,7 @@ def upgrade() -> None:
sa.Column("line_name_modified", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["meta_id"],
["dkb.financial_statement_meta.id"],
[f"{SCHEMA}.financial_statement_meta.id"],
name=op.f("fk_financial_statement_line_meta_id_financial_statement_meta"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_financial_statement_line")),
@ -1408,7 +1460,7 @@ def upgrade() -> None:
sa.Column("comment", sa.Text(), nullable=True),
sa.Column("ordinal", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["kfo_result_id"], ["dkb.kfo_result.id"], name=op.f("fk_kfo_result_line_kfo_result_id_kfo_result")
["kfo_result_id"], [f"{SCHEMA}.kfo_result.id"], name=op.f("fk_kfo_result_line_kfo_result_id_kfo_result")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_kfo_result_line")),
schema=SCHEMA,
@ -1435,16 +1487,16 @@ def upgrade() -> None:
sa.Column("modified_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
["column_id"],
["dkb.financial_statement_column.id"],
[f"{SCHEMA}.financial_statement_column.id"],
name="fk_financial_statement_value_column_id_fs_column",
),
sa.ForeignKeyConstraint(
["line_id"],
["dkb.financial_statement_line.id"],
[f"{SCHEMA}.financial_statement_line.id"],
name=op.f("fk_financial_statement_value_line_id_financial_statement_line"),
),
sa.ForeignKeyConstraint(
["modified_by"], ["dkb.app_user.id"], name=op.f("fk_financial_statement_value_modified_by_app_user")
["modified_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_financial_statement_value_modified_by_app_user")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_financial_statement_value")),
schema=SCHEMA,
@ -1464,12 +1516,12 @@ def upgrade() -> None:
sa.Column("share_percent", sa.Numeric(precision=7, scale=4), nullable=True),
sa.ForeignKeyConstraint(
["kfo_result_line_id"],
["dkb.kfo_result_line.id"],
[f"{SCHEMA}.kfo_result_line.id"],
name=op.f("fk_kfo_result_line_company_kfo_result_line_id_kfo_result_line"),
),
sa.ForeignKeyConstraint(
["legal_entity_id"],
["dkb.legal_entity.id"],
[f"{SCHEMA}.legal_entity.id"],
name=op.f("fk_kfo_result_line_company_legal_entity_id_legal_entity"),
),
sa.PrimaryKeyConstraint("kfo_result_line_id", "legal_entity_id", name=op.f("pk_kfo_result_line_company")),
@ -1478,7 +1530,7 @@ def upgrade() -> None:
def downgrade() -> None:
"""Полный откат: FK цикла -> таблицы в обратном порядке (с индексами) -> enum-типы. Схема dkb остаётся."""
"""Полный откат: FK цикла -> таблицы в обратном порядке (с индексами) -> enum-типы. Схема БД остаётся."""
op.drop_constraint(FK_CYCLE, "stored_file", type_="foreignkey", schema=SCHEMA)
for table in reversed(TABLES):
op.drop_table(table, schema=SCHEMA)

View File

@ -20,18 +20,18 @@ from sqlalchemy.dialects import postgresql
from sqlalchemy.schema import CreateSequence, DropSequence
from alembic import op
from src.models.base import SCHEMA
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 у роли миграций может не быть).
#: Временная копия строк на время пересоздания таблицы (в схеме БД: прав на TEMP у роли миграций может не быть).
COPY_TABLE = "related_group_member_v5_13"
SEQUENCES = ("company_group_code_seq", "related_group_code_seq")
AUDIT_ACTION_V5_13 = (
@ -56,16 +56,16 @@ def _create_member_table() -> None:
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")
["created_by"], [f"{SCHEMA}.app_user.id"], name=op.f("fk_related_group_member_created_by_app_user")
),
sa.ForeignKeyConstraint(
["legal_entity_id"],
["dkb.legal_entity.id"],
[f"{SCHEMA}.legal_entity.id"],
name=op.f("fk_related_group_member_legal_entity_id_legal_entity"),
),
sa.ForeignKeyConstraint(
["related_group_id"],
["dkb.related_group.id"],
[f"{SCHEMA}.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")),

View File

@ -17,13 +17,13 @@ from collections.abc import Sequence
from sqlalchemy.dialects import postgresql
from alembic import op
from src.models.base import SCHEMA
revision: str = "0003_dbml_v5_15"
down_revision: str | Sequence[str] | None = "0002_dbml_v5_14"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
SCHEMA = "dkb"
OSV_DATA_TYPE_V5_14 = ("BU", "CURRENCY", "QUANTITY", "SUM")
# v5.15 п.4: (имя, таблица, колонка) — обычные btree-индексы, имена как в ORM
FK_LOOKUP_INDEXES = (

View File

@ -13,13 +13,13 @@ Create Date: 2026-10-06
from collections.abc import Sequence
from alembic import op
from src.models.base import SCHEMA
revision: str = "0004_dbml_v5_16"
down_revision: str | Sequence[str] | None = "0003_dbml_v5_15"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
SCHEMA = "dkb"
# порядок значений — как в DBML v5.16 (сверка enum'ов сравнивает упорядоченные списки)
MATCH_KINDS = ("INN", "NAME", "ALIAS", "MANUAL", "PROPAGATED")

View File

@ -12,13 +12,13 @@ Create Date: 2026-10-07
from collections.abc import Sequence
from alembic import op
from src.models.base import SCHEMA
revision: str = "0005_dbml_v5_17"
down_revision: str | Sequence[str] | None = "0004_dbml_v5_16"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
SCHEMA = "dkb"
TABLES = ("legal_entity", "legal_entity_history")

View File

@ -33,6 +33,9 @@ if str(_API_DIR) not in sys.path:
sys.path.insert(0, str(_API_DIR))
if "ENV_FILE" not in os.environ and not Path(".env").is_file():
os.environ["ENV_FILE"] = str(_API_DIR / ".env.example")
# Сверка идёт в эталонной схеме `dkb`: имя схемы в БД конфигурируемо (`DB_SCHEMA`), на совпадение имён, типов и
# ограничений с DBML оно не влияет. Обычная переменная окружения приоритетнее `OPENBAO__APP__DB_SCHEMA` и `.env`.
os.environ["DB_SCHEMA"] = "dkb"
from sqlalchemy import MetaData # noqa: E402

View File

@ -55,6 +55,7 @@ from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.sql.elements import TextClause
from sqlalchemy.sql.functions import FunctionElement
#: Эталонная схема модели. Имя схемы в БД конфигурируемо (`DB_SCHEMA`), CLI сверяет ORM, загруженную с этим значением.
SCHEMA = "dkb"
DBML_FILE_NAME = "data-model-v5.17.dbml"
ORM = "ORM"

View File

@ -102,7 +102,7 @@ async def _run_cli(seed_dir: Path, *, dev: bool, dev_groups: bool) -> SeedReport
from src.core.config import settings # noqa: PLC0415
engine = create_async_engine(
settings.OPENBAO.DATABASE.DATABASE_URL, connect_args={"server_settings": {"search_path": "dkb"}}
settings.OPENBAO.DATABASE.DATABASE_URL, connect_args={"server_settings": {"search_path": settings.DB_SCHEMA}}
)
try:
async with async_sessionmaker(engine, expire_on_commit=False)() as session, session.begin():

View File

@ -1,5 +1,6 @@
import json
import os
import re
from typing import Annotated, Any
from pydantic import NonNegativeInt, PositiveInt, field_validator, model_validator
@ -7,6 +8,8 @@ from pydantic_settings import BaseSettings, NoDecode, PydanticBaseSettingsSource
from src.core.settings_sources import OpenBaoAppSettingsSource
DB_SCHEMA_RE = re.compile(r"[a-z_][a-z0-9_]{0,62}")
class DBSettings(BaseSettings):
DATABASE_URL: str
@ -69,6 +72,9 @@ class Settings(BaseSettings):
DEBUG: bool = False
server: ServerSettings = ServerSettings()
ROOT_PATH: str = "/"
# Схема PostgreSQL модели данных (MetaData ORM, search_path движка, Alembic). В контуре — OPENBAO__APP__DB_SCHEMA.
# Имя подставляется в DDL, поэтому валидируется как простой идентификатор (см. `_check_db_schema`)
DB_SCHEMA: str = "dkb"
VERSION: str = "1.0.0"
# CORS при DEBUG=false: разрешённые источники (JSON-список или строка через запятую); пусто — кросс-доменные
# запросы запрещены. При DEBUG=true — всегда `*` (OQ-65)
@ -124,6 +130,17 @@ class Settings(BaseSettings):
file_secret_settings,
)
@field_validator("DB_SCHEMA")
@classmethod
def _check_db_schema(cls, value: str) -> str:
"""Имя схемы — строчный SQL-идентификатор до 63 символов, не `pg_*` (зарезервировано PostgreSQL): безопасно
для подстановки в DDL и `search_path` без кавычек."""
if not DB_SCHEMA_RE.fullmatch(value) or value.startswith("pg_"):
raise ValueError(
f"DB_SCHEMA={value!r}: нужен идентификатор [a-z_][a-z0-9_]{{0,62}}, не начинающийся с 'pg_'"
)
return value
@field_validator("CORS_ORIGINS", mode="before")
@classmethod
def _split_origins(cls, value: Any) -> Any:

View File

@ -5,9 +5,10 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_asyn
# from fastapi_users.db import SQLAlchemyUserDatabase, SQLAlchemyBaseUserTableUUID
from src.core.config import settings
from src.models.base import SCHEMA
engine = create_async_engine(
settings.OPENBAO.DATABASE.DATABASE_URL, connect_args={"server_settings": {"search_path": "dkb"}}
settings.OPENBAO.DATABASE.DATABASE_URL, connect_args={"server_settings": {"search_path": SCHEMA}}
)
async_session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

View File

@ -1,4 +1,4 @@
"""Базовый класс ORM: схема `dkb`, детерминированные имена ограничений и первичный ключ uuid."""
"""Базовый класс ORM: схема из `settings.DB_SCHEMA` (по умолчанию `dkb`), имена ограничений и первичный ключ uuid."""
import uuid
@ -6,8 +6,11 @@ from sqlalchemy import MetaData
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, declarative_mixin, mapped_column
#: Схема PostgreSQL, в которой живёт вся модель данных (DBML v5.17).
SCHEMA = "dkb"
from src.core.config import settings
#: Схема PostgreSQL, в которой живёт вся модель данных (DBML v5.17); единственный источник — `settings.DB_SCHEMA`.
#: Читается при импорте: MetaData, enum и sequence привязываются к схеме сразу (см. R-1 PR #38).
SCHEMA = settings.DB_SCHEMA
#: Стандартная схема имён SQLAlchemy. Нужна, чтобы ORM, миграция 0001 и `alembic autogenerate` давали одни и те же
#: имена ограничений и `downgrade` был детерминирован. Для составных/partial-индексов, где конвенция даёт

View File

@ -342,7 +342,7 @@ ENUMS: dict[str, type[enum.Enum]] = {
def pg_enum(name: str) -> SAEnum:
"""Тип колонки для enum DBML `name`: нативный PG-enum `dkb.<name>`, значения — `.value` членов.
"""Тип колонки для enum DBML `name`: нативный PG-enum `<схема>.<name>`, значения — `.value` членов.
Создание типа — поведение SQLAlchemy по умолчанию (`create_type=True`, `checkfirst`): `Base.metadata.create_all`
создаёт типы сам; миграция 0001 создаёт их явно.

View File

@ -40,6 +40,7 @@ from src.core.config import get_settings
from src.core.exception_handlers import register_exception_handlers
from src.core.storage import ObjectNotFoundError
from src.db.db import get_async_session, session_transaction
from src.models.base import SCHEMA
from src.services.session_audit import SessionAudit
from tests.fixtures_helpers import fixtures_skip_reason, resolve_fixtures_dir
from tests.pg_helpers import ( # scratch_db_url — реэкспорт фикстуры, чтобы pytest её нашёл
@ -96,8 +97,8 @@ async def migrated_db_url() -> AsyncIterator[str]:
@pytest_asyncio.fixture(scope="session")
async def db_engine(migrated_db_url: str) -> AsyncIterator[AsyncEngine]:
"""Async-движок на мигрированную БД сессии; `search_path = dkb`."""
engine = create_async_engine(migrated_db_url, connect_args={"server_settings": {"search_path": "dkb"}})
"""Async-движок на мигрированную БД сессии; `search_path` = схема из настроек."""
engine = create_async_engine(migrated_db_url, connect_args={"server_settings": {"search_path": SCHEMA}})
try:
yield engine
finally:
@ -236,7 +237,7 @@ async def committed_sessionmaker() -> AsyncIterator[async_sessionmaker[AsyncSess
"""Отдельная временная БД: `alembic upgrade head` и seed (dev) ЗАФИКСИРОВАНЫ; удаляется после теста."""
name = f"armcore_import_{uuid.uuid4().hex[:12]}"
url = (await create_scratch_db(name)).render_as_string(hide_password=False)
engine = create_async_engine(url, connect_args={"server_settings": {"search_path": "dkb"}})
engine = create_async_engine(url, connect_args={"server_settings": {"search_path": SCHEMA}})
try:
await alembic_async(url, "upgrade", "head")
maker = async_sessionmaker(engine, expire_on_commit=False)

View File

@ -7,10 +7,12 @@
"""
import asyncio
import os
import subprocess
import sys
import uuid
from collections.abc import AsyncIterator
from functools import partial
from pathlib import Path
import asyncpg
@ -40,9 +42,10 @@ def _asyncpg_dsn(url: URL) -> str:
return url.set(drivername="postgresql").render_as_string(hide_password=False)
def run_alembic(url: str, *args: str) -> str:
def run_alembic(url: str, *args: str, env: dict[str, str] | None = None) -> str:
"""`alembic -x dburl=<url> <args>` из каталога api/ отдельным процессом; вернуть stdout+stderr.
`env` — дополнительные переменные окружения процесса (например `DB_SCHEMA`).
`env.py` вызывает `asyncio.run(...)` — внутри event loop pytest-asyncio это невозможно. При ошибке —
`AssertionError` с выводом процесса (`ENV_FILE`/`APP_ENV` наследуются из окружения pytest-env).
"""
@ -52,15 +55,16 @@ def run_alembic(url: str, *args: str) -> str:
capture_output=True,
text=True,
check=False,
env={**os.environ, **(env or {})},
)
output = result.stdout + result.stderr
assert result.returncode == 0, f"alembic {' '.join(args)} завершился с кодом {result.returncode}:\n{output}"
return output
async def alembic_async(url: str, *args: str) -> str:
async def alembic_async(url: str, *args: str, env: dict[str, str] | None = None) -> str:
"""`run_alembic` в потоке, чтобы не блокировать event loop."""
return await asyncio.to_thread(run_alembic, url, *args)
return await asyncio.to_thread(partial(run_alembic, url, *args, env=env))
async def create_scratch_db(name: str) -> URL:

View File

@ -22,7 +22,15 @@ REQUIRED_ENV = {
# Плоские настройки, которые тесты ниже задают/проверяют
POLL = 7
PORT = 9000
FLAT_KEYS = {"DEBUG", "NATS_ENABLED", "AUTH_DEV_ISSUER", "FORWARDED_ALLOW_IPS", "CORS_ORIGINS", "OUTBOX_POLL_SECONDS"}
FLAT_KEYS = {
"DEBUG",
"NATS_ENABLED",
"AUTH_DEV_ISSUER",
"FORWARDED_ALLOW_IPS",
"CORS_ORIGINS",
"OUTBOX_POLL_SECONDS",
"DB_SCHEMA",
}
@pytest.fixture
@ -50,6 +58,21 @@ def test_app_value_applied(env: pytest.MonkeyPatch) -> None:
assert s.OPENBAO.NATS.NATS_URL == "nats://localhost:4222"
def test_db_schema_default_and_app(env: pytest.MonkeyPatch) -> None:
"""`DB_SCHEMA`: по умолчанию `dkb`; в контуре задаётся как `OPENBAO__APP__DB_SCHEMA`."""
assert _settings().DB_SCHEMA == "dkb"
env.setenv("OPENBAO__APP__DB_SCHEMA", "dkb_alt")
assert _settings().DB_SCHEMA == "dkb_alt"
@pytest.mark.parametrize("bad", ["", "Dkb", "1dkb", "dkb-alt", 'dkb"; DROP SCHEMA x; --', "pg_dkb", "a" * 64, "схема"])
def test_db_schema_rejects_unsafe_identifier(env: pytest.MonkeyPatch, bad: str) -> None:
"""Имя подставляется в DDL: не идентификатор `[a-z_][a-z0-9_]{0,62}` или `pg_*` отвергается при старте."""
env.setenv("DB_SCHEMA", bad)
with pytest.raises(ValidationError):
_settings()
def test_default_without_app(env: pytest.MonkeyPatch) -> None:
assert _settings().NATS_ENABLED is False

View File

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