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 |
Аналогично 0003 — tx_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_INVOICE (с ON CONFLICT DO NOTHING) |
2026-05-25 |
Метаданные Drizzle лежат в drizzle/migrations/meta/:
- _journal.json — упорядоченный список миграций с тэгами и временем (используется при migrate);
- NNNN_snapshot.json — снапшот схемы после каждой миграции (используется при generate для diff-а).
Оба файла обязательно коммитятся вместе с .sql-файлом — иначе следующий generate посчитает текущую схему «новой» и сгенерирует фантомные изменения.
Как добавить миграцию¶
Канонический поток — только через Drizzle Kit:
- Правка схемы. Изменить таблицы/колонки/индексы в
src/shared/schema.ts. Это единственный источник правды для типов и диффа. - Генерация SQL.
Drizzle сравнит текущее состояние
schema.tsсо снапшотом вmeta/и создаст файлdrizzle/migrations/NNNN_<random_name>.sql+ обновит снапшот. - Проверка сгенерированного SQL. Открыть файл, убедиться, что нет неожиданных
DROP, что порядок утверждений корректен (statement-breakpointмежду ними). - Ручная доводка (точечно). Допустимо вручную править сгенерированный SQL, если Drizzle что-то не умеет выразить декларативно:
ALTER TYPE … ADD VALUEдля PostgreSQL enum-ов;- сидирующие
INSERTв справочники (см.0009_invoice_payment_route.sql); - performance-настройки (
fillfactor, autovacuum, partial-индексы) — пример в0001_perf-tuning.sql; - переименование колонок (Drizzle по умолчанию даёт
DROP+ADD).
Правка существующего, уже применённого где-либо файла запрещена — только новая миграция. 5. Применение.
Применяет все 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). Стратегия отката:
- Compensating migration. Создать новую миграцию (
NNNN+1_revert_*.sql), которая явнымиDROP COLUMN,ALTER TABLE,DROP INDEX,DROP TABLEотменяет нежелательные изменения. Параллельно отразить откат вsrc/shared/schema.ts, чтобыmeta/-снапшот совпадал — иначе следующийgenerateпопытается «вернуть» откаченные элементы. - Бэкап перед опасной миграцией. Для критичных изменений (
DROP COLUMNс данными, миграции на уже залитой проде) —pg_dump --schema=pmзатронутых таблиц передdrizzle-kit migrate. Хранить бэкап рядом с миграцией вplans/YYYY-MM-DD/. - Восстановление из snapshot. Если миграция привела к потере данных, восстанавливать целевые таблицы из
pg_dumpпосле применения compensating migration — Drizzle сам не вернёт данные. - Никаких ручных
DELETE FROM drizzle.__drizzle_migrations. Drizzle потеряет учёт состояния, и следующий запуск может попытаться повторить уже применённую миграцию. - Для prod-сценариев — двухшаговое развёртывание:
- Шаг 1: миграция, совместимая со старым кодом (например, добавить новую колонку nullable).
- Шаг 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.
Связанные документы¶
- 01-schema-overview.md — общая структура схемы
pm.*. - ../../operations/ — операции с БД в проде (бэкапы, доступы).
CLAUDE.md— NO-GO правила проекта PM.drizzle.config.ts— конфиг Drizzle Kit.