Перейти к содержанию

13. Миграции БД

Обзор миграций схемы pm.*: правила применения, порядок, как добавлять новые и чего нельзя делать. Канонический инструмент — drizzle-kit, конфиг drizzle.config.ts (schemaFilter: ['pm']).

Назначение раздела

Payment Manager управляет только схемой pm.* в PostgreSQL. Миграции применяются строго через drizzle-kit migrate — это единственный поддерживаемый способ изменения схемы PM. SQL-файлы хранятся в drizzle/migrations/ и применяются по алфавиту (номер 0000, 0001, …). Drizzle ведёт собственную таблицу __drizzle_migrations (внутри схемы drizzle) для отслеживания применённых миграций — она хранит хэш каждого выполненного файла и метку времени.

Конфиг (drizzle.config.ts):

dialect: 'postgresql',
schema: './src/shared/schema.ts',
out: './drizzle/migrations',
schemaFilter: ['pm'],

Ключевой параметр — schemaFilter: ['pm']: Drizzle при generate и migrate смотрит исключительно на схему pm. Это гарантирует, что соседняя public.* (под управлением Serverpod) никогда не попадёт в diff и не будет случайно «исправлена» PM-ом.

NPM-скрипты:

npm run db:generate   # drizzle-kit generate — создать миграцию из изменений в schema.ts
npm run db:migrate    # drizzle-kit migrate  — применить миграции к БД
npm run db:seed       # tsx drizzle/seed.ts  — наполнить справочники (fee_rule, limit_rule и т.п.)

Внутренний CLI: npm run migrate (= node dist/migrate.js) вызывает тот же drizzle-kit migrate из собранного бандла — используется в Docker-образе при старте контейнера.

Список миграций

Файл Что добавляет Дата
0000 0000_init.sql Создаёт схему pm и базовый набор таблиц: fee_rule, intent, outbox, psp_tx_map, service_key, tb_account_map, tx_history, payment_route, psp и др. 2026-05-05
0001 0001_perf-tuning.sql Performance-настройки: partial-индекс psp_tx_map_active_idx для горячих состояний worker-а, fillfactor=85, тонкие настройки autovacuum для psp_tx_map 2026-05-05
0002 0002_magenta_forgotten_one.sql Таблица limit_rule — правила лимитов (по operation_type, channel, direction, окно, сумма/счёт), плюс смежные индексы 2026-05-06
0003 0003_fix_limit_rule_direction_char.sql Меняет тип limit_rule.direction с char(6) на varchar(6)char правый-padding ломал сравнение 'DEBIT ''DEBIT' 2026-05-11
0004 0004_fix_tx_history_direction_char.sql Аналогично 0003tx_history.direction переводится в varchar(6) 2026-05-11
0005 0005_skinny_skreet.sql Удаляет колонку service_key.secret_hash — HMAC-секрет хранится только в env, не в БД 2026-05-22
0006 0006_rapid_millenium_guard.sql Добавляет tx_history.attributes jsonb и индекс по (attributes->>'nfcTagId')::int, created_at — для NFC-меток 2026-05-22
0007 0007_intents_invoice_fields.sql Поля инвойсов в intent: version, expires_at, reserved_at, canceled_at, redeemed_at, cancel_reason; from_account_name становится nullable 2026-05-25
0008 0008_auth_policies.sql Таблица pm.auth_policies — политики step-up авторизации (PIN, biometric) по контексту транзакции; см. docs/AUTH-POLICIES.md 2026-05-25
0009 0009_invoice_payment_route.sql Сидирующий INSERT в payment_route: маршрут INVOICE_PAYMENT → канал MERCHANT_INVOICEON CONFLICT DO NOTHING) 2026-05-25

Метаданные Drizzle лежат в drizzle/migrations/meta/: - _journal.json — упорядоченный список миграций с тэгами и временем (используется при migrate); - NNNN_snapshot.json — снапшот схемы после каждой миграции (используется при generate для diff-а).

Оба файла обязательно коммитятся вместе с .sql-файлом — иначе следующий generate посчитает текущую схему «новой» и сгенерирует фантомные изменения.

Как добавить миграцию

Канонический поток — только через Drizzle Kit:

  1. Правка схемы. Изменить таблицы/колонки/индексы в src/shared/schema.ts. Это единственный источник правды для типов и диффа.
  2. Генерация SQL.
    npx drizzle-kit generate
    
    Drizzle сравнит текущее состояние schema.ts со снапшотом в meta/ и создаст файл drizzle/migrations/NNNN_<random_name>.sql + обновит снапшот.
  3. Проверка сгенерированного SQL. Открыть файл, убедиться, что нет неожиданных DROP, что порядок утверждений корректен (statement-breakpoint между ними).
  4. Ручная доводка (точечно). Допустимо вручную править сгенерированный SQL, если Drizzle что-то не умеет выразить декларативно:
  5. ALTER TYPE … ADD VALUE для PostgreSQL enum-ов;
  6. сидирующие INSERT в справочники (см. 0009_invoice_payment_route.sql);
  7. performance-настройки (fillfactor, autovacuum, partial-индексы) — пример в 0001_perf-tuning.sql;
  8. переименование колонок (Drizzle по умолчанию даёт DROP+ADD).

Правка существующего, уже применённого где-либо файла запрещена — только новая миграция. 5. Применение.

npx drizzle-kit migrate
Применяет все pending-миграции в порядке номеров. Drizzle помечает их в __drizzle_migrations. 6. Коммит. В одном коммите: - src/shared/schema.ts (изменения); - drizzle/migrations/NNNN_*.sql (новый файл); - drizzle/migrations/meta/_journal.json и meta/NNNN_snapshot.json (обновлены автоматически); - CHANGELOG.md — короткое описание (см. правила PR).

Сообщение коммита по стилю проекта: feat(db): <что добавлено> либо fix(db): <что исправлено>.

Пример: добавление колонки

// src/shared/schema.ts
export const intent = pmSchema.table('intent', {
  // ...
  metadata: jsonb('metadata'),     // ← новая колонка
})
npx drizzle-kit generate   # создаст drizzle/migrations/0010_<name>.sql
npx drizzle-kit migrate    # применит к локальной БД
git add src/shared/schema.ts drizzle/migrations/0010_*.sql drizzle/migrations/meta/
git commit -m "feat(db): add intent.metadata column"

Пример: сидирующий INSERT (миграция-данные)

См. 0009_invoice_payment_route.sql — после generate (если он вообще ничего не сгенерировал, так как схема не менялась) можно создать пустую миграцию вручную через drizzle-kit generate --custom, и добавить в неё INSERT … ON CONFLICT DO NOTHING.

Что нельзя

  • Нельзя писать SQL руками без drizzle-kit generate. Это NO-GO из CLAUDE.md проекта PM:

    PM migrations ONLY via drizzle-kit migrate — never write SQL manually. Исключение — точечная доводка уже сгенерированного файла (см. шаг 4 выше).

  • Нельзя писать в public.*. Это схема Serverpod (Auth Center). PM имеет к ней только read-only доступ через search_path=pm,public — все её таблицы создаются миграциями Serverpod (serverpod generate + dart bin/main.dart --apply-migrations).
  • Нельзя использовать миграции Serverpod для pm.*. Serverpod и Drizzle ведут независимые реестры применённых миграций; смешение приведёт к рассинхронизации.
  • Нельзя править уже применённый файл миграции. На любой среде, где она уже выполнилась, изменение не будет переподобрано — следующий drizzle-kit migrate просто пропустит её по хэшу.
  • Нельзя коммитить миграцию без обновлённого meta/-снапшота — иначе следующий generate создаст «фантомную» миграцию-откат.

Откат

Drizzle не поддерживает down-миграции — это сознательное архитектурное решение (forward-only schema evolution). Стратегия отката:

  1. Compensating migration. Создать новую миграцию (NNNN+1_revert_*.sql), которая явными DROP COLUMN, ALTER TABLE, DROP INDEX, DROP TABLE отменяет нежелательные изменения. Параллельно отразить откат в src/shared/schema.ts, чтобы meta/-снапшот совпадал — иначе следующий generate попытается «вернуть» откаченные элементы.
  2. Бэкап перед опасной миграцией. Для критичных изменений (DROP COLUMN с данными, миграции на уже залитой проде) — pg_dump --schema=pm затронутых таблиц перед drizzle-kit migrate. Хранить бэкап рядом с миграцией в plans/YYYY-MM-DD/.
  3. Восстановление из snapshot. Если миграция привела к потере данных, восстанавливать целевые таблицы из pg_dump после применения compensating migration — Drizzle сам не вернёт данные.
  4. Никаких ручных DELETE FROM drizzle.__drizzle_migrations. Drizzle потеряет учёт состояния, и следующий запуск может попытаться повторить уже применённую миграцию.
  5. Для prod-сценариев — двухшаговое развёртывание:
  6. Шаг 1: миграция, совместимая со старым кодом (например, добавить новую колонку nullable).
  7. Шаг 2: после деплоя приложения — отдельная миграция, ужесточающая constraint (NOT NULL, DROP старой колонки).

Это позволяет откатить код без отката миграции — никогда не возникает ситуации, когда новая колонка/тип ломает старый код.

Запуск в разных средах

Среда Когда применяются миграции Команда
local dev вручную после git pull, если в diff есть drizzle/migrations/*.sql npx drizzle-kit migrate
test (Vitest) автоматически в globalSetup (если настроен) или вручную перед npm test npm run db:migrate
Docker (prod-образ) при старте контейнера, до node dist/server.js node dist/migrate.js
CI в pipeline до e2e-тестов npm run db:migrate

Все среды используют один и тот же реестр drizzle.__drizzle_migrations — это означает, что миграция, успешно прошедшая локально, гарантированно не будет повторно применена на staging/prod.

Troubleshooting

  • error: relation "pm.<table>" already exists — миграция запускается на уже инициализированной БД, но без записи в drizzle.__drizzle_migrations. Проверь, на ту ли БД смотрит DATABASE_URL. Никогда не «чини» это руками — лучше восстановить из бэкапа.
  • generate создал пустую миграцию — снапшот meta/NNNN_snapshot.json рассогласован с schema.ts. Чаще всего причина — забыли закоммитить предыдущий снапшот. Откатить файл, синхронизировать meta/.
  • generate создал лишний DROP — в schema.ts пропущена таблица/колонка, которая существует в meta/. Скорее всего после git pull забыли поднять снапшот. Сверить schema.ts с meta/<последний>_snapshot.json.
  • migrate падает с permission denied for schema pm — у роли БД нет прав на CREATE/ALTER в схеме pm. Гранты выдаются один раз вручную; см. DEPLOYMENT.
  • Конфликт нумерации (2 PR создали 0010_*.sql) — пере-сгенерировать свою миграцию после merge другого PR: удалить свой файл + snapshot, заново drizzle-kit generate.

Связанные документы