write-safe-migrations
SKILL.md
Write Safe Migrations
Quick Start
Default approach: expand → migrate → contract.
Goal: reduce lock time, keep rollback easy, and prove correctness with checks.
When to use this skill
- any production schema change (especially large tables)
- adding columns/indexes/constraints, renames, backfills, or data moves
- when rollback must be possible without restoring from backup
Before you start (inputs)
- database engine + version
- migration goal (what changes, why)
- table size + write rate + peak hours
- lock tolerance (seconds) and downtime tolerance
- app deploy constraints (can you do multiple deploys?)
Common safe patterns
add column (nullable) + backfill + enforce
- add new nullable column
- deploy app writing both old + new (or only new with fallback)
- backfill in batches
- add constraint / set not null (engine-specific)
- remove old column in a later deploy
add index safely
- postgres:
create index concurrently - mysql: use online ddl when supported; validate it is online for your engine/version
add foreign key / check constraint safely
- validate data first (no orphans, no invalid values)
- add constraint in a way that avoids long blocking where possible
- validate separately if engine supports it
Workflow (default)
- design
- write the migration plan and rollback plan
- prechecks
- confirm table sizes, existing indexes, constraint violations
- dry run
- run on staging with production-like data volume if possible
- execute
- apply schema change, then data movement in batches
- verify
- run correctness queries and app-level smoke checks
- monitor
- lock waits, replication lag, error rates
- contract
- remove old paths only after adoption is complete
PostgreSQL templates
add column:
ALTER TABLE public.your_table
ADD COLUMN new_col text;
backfill in batches (example using id ranges):
UPDATE public.your_table
SET new_col = <expression>
WHERE id >= :min_id AND id < :max_id
AND new_col IS NULL;
add index safely:
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_your_table_new_col
ON public.your_table (new_col);
set not null (do this only after backfill + verification):
ALTER TABLE public.your_table
ALTER COLUMN new_col SET NOT NULL;
MySQL templates
add column:
ALTER TABLE your_table
ADD COLUMN new_col VARCHAR(255) NULL;
backfill in batches (example):
UPDATE your_table
SET new_col = <expression>
WHERE id BETWEEN ? AND ?
AND new_col IS NULL;
add index:
CREATE INDEX idx_your_table_new_col ON your_table (new_col);
Verification queries (examples)
null rate after backfill:
SELECT COUNT(*) AS null_count
FROM your_table
WHERE new_col IS NULL;
row count consistency (when moving data):
SELECT COUNT(*) FROM old_table;
SELECT COUNT(*) FROM new_table;
fk integrity (orphan check):
SELECT COUNT(*) AS orphan_count
FROM child c
LEFT JOIN parent p ON p.id = c.parent_id
WHERE c.parent_id IS NOT NULL AND p.id IS NULL;
Rollback rules of thumb
- prefer additive changes first (new columns/tables/indexes) to keep rollback easy
- avoid destructive changes in the same deploy (drop/rename) unless you can restore quickly
- keep old read path working until you prove new path is correct
Output format (runbook)
## migration: [name]
### objective
- what changes:
- why:
### risks
- locks:
- long running work:
- replication lag:
### plan
- [ ] step 1:
- [ ] step 2:
- [ ] step 3:
### verification
- query checks:
- app checks:
### rollback
- exact rollback steps:
Weekly Installs
3
Repository
docfork/db-skillsGitHub Stars
2
First Seen
Feb 28, 2026
Security Audits
Installed on
opencode3
antigravity2
claude-code2
github-copilot2
codex2
zencoder2