migrations
GitHub管理Drizzle数据库迁移,涵盖生成、审查、应用及冲突解决。规范迁移命名、追踪机制及CI流程,新增向后兼容性检查以预防发布风险,确保数据库架构变更安全有序。
Trigger Scenarios
Install
npx skills add theopenco/llmgateway --skill migrations -g -y
SKILL.md
Frontmatter
{
"name": "migrations",
"description": "Generate, review, edit, apply, or resolve conflicts for Drizzle database migrations in this repo. Use when changing packages\/db\/src\/schema.ts, running pnpm migrations or pnpm migrate, touching packages\/db\/migrations, reviewing migration diffs, or handling migration merge conflicts."
}
Migrations
Use this workflow for database schema changes and migration conflicts.
Default: use generated SQL
Assume Drizzle applies migrations cleanly and tracks which have run. Generate, review, and commit the migration without adapting its SQL by default.
Do not add IF NOT EXISTS, IF EXISTS, existence probes, or duplicate-object
handlers for hypothetical reruns, partial application, or schema drift. The one
exception is index changes on existing tables (see Locking).
Regenerating an unmerged migration after syncing with main does not justify
compatibility with its earlier branch version. A speculative review warning is
not evidence that a migration ran outside the normal workflow.
How Drizzle tracks migrations
- Each migration is a folder
packages/db/migrations/<YYYYMMDDHHMMSS>_<name>/holdingmigration.sqlandsnapshot.json. - The migrator records each applied folder name in
drizzle.__drizzle_migrations.nameand applies every local folder whose name is missing, regardless of timestamp. A branch migration older than migrations already onmainstill runs after merge. - The folder name is the tracking key: never rename or delete a merged migration folder. A renamed folder runs again.
- Each
snapshot.jsonlists its parents inprevIds.drizzle-kitfollows that graph, and an open leaf merges into the next generated diff, so a snapshot with a wrong parent makespnpm migrationsemit destructive DROPs.
Generate migrations
- Run all commands from the repository root.
- CI allows at most one new migration per PR (
scripts/check-new-migrations.sh). For follow-up schema changes, delete and regenerate the PR’s unmerged migration instead of adding another; never delete a merged migration. - Make schema changes in
packages/db/src/schema.ts. Tables usesnakeCase.table, which maps camelCase fields to snake_case columns. - Generate the migration with
pnpm migrations --name <name>. It also runs the commutativity check below. - Name it after the change in snake_case, at most four words:
<table>_<column>for an added column,<table>for a new table,<index_name>for an index, and adrop_orrename_prefix for destructive changes (project_description,api_key_status_idx,drop_transaction_note). Never keep Drizzle's random default name. - If one migration mixes changes, name the destructive one; if that is unclear, split the schema change into separate migrations.
- Review the new
packages/db/migrations/<timestamp>_<name>/folder; confirm the SQL matches the name.
How migrations run
runMigrations (packages/db/src/migrate.ts) applies each pending migration in
its own transaction on one connection with lock_timeout (5s), a
statement_timeout, and an advisory lock that serializes runners. A lock
timeout or deadlock rolls that migration back and retries with backoff, then
fails naming the blocking sessions. pnpm migrate applies; pnpm migrate --check is a read-only preflight. Production runs the same entry point
(node dist/migrate.js) as a job before rollout, with
MIGRATIONS_REQUIRE_PREBUILT_INDEXES=true.
Locking
Every ALTER TABLE takes ACCESS EXCLUSIVE until the migration commits, and
queued app queries wait behind the request. pnpm migrations:lint (CI) checks
migrations created since the lint was added:
- Index changes on existing tables use
CREATE INDEX IF NOT EXISTS/DROP INDEX IF EXISTS. WithMIGRATIONS_REQUIRE_PREBUILT_INDEXES=truethe deploy refuses until the index was built or droppedCONCURRENTLYand prints the commands; list them under "Manual steps" in the PR. Indexes on tables the migration creates are exempt;-- migrate:allow-blocking-indexabove a statement opts out for a small table. - Flagged unless acknowledged: altering more than one existing table in one migration,
ALTER COLUMN ... TYPE,SET NOT NULL, aFOREIGN KEYwithoutNOT VALID, andADD COLUMNwith a volatile default. Prefer expand/contract (add, backfill in batches, then constrain in a later PR). Otherwise acknowledge with-- migrate:reviewed <why this is safe or the manual step>above the statement (anywhere in the file for the multi-table rule).
Backward compatibility
Migrations run before rollout and are never reverted, so the previous release
runs on the new schema during every rollout and after any rollback. Full-row
Drizzle queries (select(), relational queries without columns,
returning()) name every schema.ts column, as does code that references one
directly, so a column the previous release still declares must still exist.
Additive changes (new tables, nullable columns, columns with a DEFAULT) are
safe.
Remove a column or table in two deployed PRs:
- Remove every use and the field from
schema.ts, without a migration. - After step 1 is deployed, run
pnpm migrations --name drop_<table>_<column>and put-- migrate:contract #<step 1 PR>above the DROP. Until it merges, everypnpm migrationsrun emits that DROP.
Rename by adding the new column, writing both, backfilling, switching reads,
then dropping the old one. Change a unique constraint or index by adding the new
one first and dropping the old one once no deployed onConflict targets it.
pnpm migrations:lint flags DROP TABLE/COLUMN/INDEX, RENAME, DROP CONSTRAINT
(except *_fk), DROP DEFAULT, and ADD COLUMN NOT NULL without DEFAULT unless
the statement carries -- migrate:contract <reason>. A -- migrate:reviewed
reason on SET NOT NULL or a type change must also cover what the previous
release writes.
Editing generated migrations
- Do not write a migration by hand from scratch. Generate it first with
pnpm migrations --name <name>. - The operational exception is avoiding locks on huge tables, especially when creating indexes. Treat all history tables as large when reviewing locking behavior.
- If that requires adaptation (including
IF [NOT] EXISTSon index changes and lint markers), edit only the generatedmigration.sql. - Never manually edit any
snapshot.json. - If the TypeScript schema is wrong, fix
packages/db/src/schema.tsand regenerate instead of patching snapshots. - Use snake_case column names in SQL.
For a large-table index change, run CREATE INDEX CONCURRENTLY IF NOT EXISTS
manually, outside a transaction, before the deploy; the migration's
IF NOT EXISTS statement is then a no-op. Keep the generated snapshots exactly
as Drizzle wrote them.
Syncing with main
Migration folders from different branches do not conflict in git. After merging main, run:
pnpm migrations:check
It reports non-commutative migrations: branch migrations that touch the same objects as migrations merged to main since the branch point. If it passes, keep the branch migration as-is. If it fails, regenerate the branch migration on top of main:
- Note any locking adaptation in your branch's
migration.sql; regeneration emits vanilla SQL and drops it. - Delete your branch's migration folder, then run
pnpm migrations --name <same name>. - Re-apply only still-required locking adaptations. Do not carry speculative fallbacks forward.
Never resolve conflicts in migration SQL or snapshot JSON by hand.
Validation
- Inspect
git diff packages/db/src/schema.ts packages/db/migrations/. - Confirm snapshot changes came from
pnpm migrations, not manual edits. - Run
pnpm migrations:lintandpnpm formatafter changes. - Run
pnpm buildafter schema or migration changes.
Version History
-
c6810b7
Current 2026-11-02 20:58
新增迁移Lint检查,拦截破坏性变更并强制要求向后兼容标记;增加“向后兼容性”章节,规范列/表删除及重名的两步部署策略。
-
d41bf19
2026-10-19 04:13
新增迁移跟踪机制说明(基于文件夹名和 snapshot.json prevIds),细化 CI 单 PR 限制及命名规范,明确禁止手动编辑 snapshot.json。
-
bf6473c
2026-09-09 03:58
修正迁移指南中不必要的防御性 SQL,默认信任生成的 Drizzle SQL,拒绝推测性兼容性保护,并文档化大表和历史表的并发索引创建作为操作异常。
- 8fdff5b 2026-08-16 07:59
- 8c03315 2026-07-24 16:53


