Database Migration Skill
Activates on schema-change requests. Generates paired forward + reverse migrations, validates safety (transaction-wrapped, idempotent, backward-compatible during rolling deploys), and refuses dangerous operations without explicit confirmation.
When it triggers
- "Add a column to X"
- "Change Y to nullable" / "Make Z required"
- "Migrate the schema"
- "Rename table/column"
- "Add an index"
- "Drop the unused column"
- "Create a new table for X"
- "Backfill data for Y"
Why a skill (and a strict one)
Migrations are dangerous. A bad migration in production = an outage, data loss, or both. The conventions for safe migrations (online schema changes, backward compatibility during deploys, paired up/down) are non-obvious and easy to skip when in a hurry.
This skill enforces them.
Installation
- Copy this skill folder to
~/.claude/skills/database-migration/ - Restart Claude Code
- Try: "Add a
phone_numbercolumn to the customers table"
SKILL.md content
---
name: database-migration
description: |
Use this skill when the user wants to change the database schema in any way:
add/remove columns, change types, add/remove constraints, create/drop
tables, add/remove indexes, rename anything, or backfill data.
Triggers on: "migrate", "add column", "drop column", "change type", "make
nullable", "rename table/column", "add index", "schema change", "alter
table", "create table", "backfill".
Generates paired forward + reverse migrations with safety checks.
Do NOT use for: data fixes (use a one-off script), reading the schema (use
database tools directly), or designing a fresh schema for a new project
(use the Postgres Schema Designer template).
---
# Database Migration
You generate safe, paired migrations. You refuse dangerous operations without
explicit confirmation. You always think about what happens during a rolling
deploy where old code and new code run simultaneously.
## Operating principles
1. **Always paired.** Every forward migration has a matching reverse.
2. **Always wrapped.** All DDL inside a transaction (where the DB supports it).
3. **Always idempotent.** Running twice should not fail.
4. **Always backward compatible during deploys.** New code must work with old
schema; old code must survive new schema briefly.
5. **Refuse dangerous operations.** DROP TABLE, DROP COLUMN with data, type
narrowing — confirm before generating.
6. **Online schema changes** for tables likely to be large.
## Process when activated
### Step 1: Understand the change
Ask up to 3 clarifying questions if needed:
- **Target table size.** "Roughly how many rows in this table? <1K, ~10K-1M, >1M?"
Affects whether we need online (concurrent) operations.
- **Deploy model.** "Rolling deploy or full downtime window?" Affects backward
compat strategy.
- **Production data?** "Is there existing data to consider, or is this a new
table?"
For trivial changes (adding nullable column to small table), skip questions.
### Step 2: Classify the change
| Type | Risk | Default approach |
|------|------|------------------|
| Add nullable column | LOW | Add directly |
| Add NOT NULL column with default | MEDIUM | Add nullable → backfill → set NOT NULL (3 migrations) |
| Add NOT NULL column no default | HIGH | Refuse for tables with data; force a default |
| Drop column | HIGH | Two-deploy: stop using → drop in next migration |
| Rename column | HIGH | Two-deploy: add new → backfill → switch code → drop old |
| Change type (compat) | MEDIUM | USING clause + verify |
| Change type (incompat) | HIGH | New column → backfill → swap |
| Add CHECK constraint | MEDIUM | Add NOT VALID → VALIDATE separately |
| Add foreign key | MEDIUM | Add NOT VALID → VALIDATE separately |
| Add index | LOW (small) / MEDIUM (large) | CREATE INDEX CONCURRENTLY for large tables |
| Add unique constraint | MEDIUM | Add unique index CONCURRENTLY → ALTER TABLE constraint |
| Drop NOT NULL | LOW | Direct |
| Add NOT NULL to existing column | HIGH | Verify no NULLs first → add as NOT VALID → validate |
| Drop table | CRITICAL | Refuse without explicit confirmation |
| Truncate table | CRITICAL | Refuse |
### Step 3: Generate paired migrations
Use the project's existing migration tool. Detect by looking for:
- `migrations/` folder + bare SQL files → straight SQL migrations
- `prisma/` folder → Prisma migrations
- `alembic.ini` → Alembic (Python)
- `knexfile.*` → Knex
- `node_modules/typeorm` → TypeORM
- `flyway.conf` → Flyway
If unclear, ASK: "Which migration tool — bare SQL, Prisma, Alembic, Knex,
TypeORM, Flyway, or other?"
Match the project's naming convention. Common patterns:
- Sequential: `0001_initial.sql`, `0002_add_phone.sql`
- Timestamped: `20240126_120000_add_phone.sql`
Generate BOTH:
- **Forward migration** (e.g., `0042_add_phone_to_customers.sql`)
- **Reverse migration** (e.g., `0042_add_phone_to_customers.down.sql`)
### Step 4: Apply safety patterns
#### Always: wrap in transaction
```sql
BEGIN;
-- changes here
COMMIT;EXCEPTION: Operations that don't run in transactions (Postgres):
CREATE INDEX CONCURRENTLYALTER TYPE ... ADD VALUE(in some versions)VACUUM
For these, omit BEGIN/COMMIT and add a comment explaining why.
Always: make idempotent
-- Safe to re-run
ALTER TABLE customers ADD COLUMN IF NOT EXISTS phone_number TEXT;
CREATE INDEX IF NOT EXISTS idx_customers_phone ON customers(phone_number);
-- For data backfill: scoped to NULL only, so re-runs are no-ops
UPDATE customers SET tier = 'standard' WHERE tier IS NULL;Online operations for large tables
-- BAD on large tables (locks table)
CREATE INDEX idx_orders_customer ON orders(customer_id);
-- GOOD on large tables (no full table lock)
-- Note: cannot run in transaction
CREATE INDEX CONCURRENTLY idx_orders_customer ON orders(customer_id);-- BAD: rewrites the entire table
ALTER TABLE orders ADD COLUMN status TEXT NOT NULL DEFAULT 'pending';
-- GOOD: split into 3 migrations
-- Migration 1: add nullable
ALTER TABLE orders ADD COLUMN IF NOT EXISTS status TEXT;
-- (deploy code that writes to status)
-- Migration 2: backfill in batches (separate script)
-- Migration 3: set NOT NULL with default
ALTER TABLE orders ALTER COLUMN status SET DEFAULT 'pending';
ALTER TABLE orders ALTER COLUMN status SET NOT NULL;Always: validate constraints separately
-- Step 1: add as NOT VALID (fast, doesn't scan existing rows)
ALTER TABLE orders ADD CONSTRAINT chk_amount_positive
CHECK (amount > 0) NOT VALID;
-- Step 2: validate later (in next migration or off-hours)
ALTER TABLE orders VALIDATE CONSTRAINT chk_amount_positive;Foreign keys: same pattern
-- NOT VALID first
ALTER TABLE orders ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id) NOT VALID;
-- VALIDATE later
ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_customer;Step 5: Output the migration set
Format:
Forward migration
migrations/NNNN_brief_description.sql
[SQL]Reverse migration
migrations/NNNN_brief_description.down.sql
[SQL]Why this approach
[2-3 sentences explaining the safety rationale]
Deploy sequence
If multi-step (rolling deploy needed):
-
Deploy 1: Run forward migration.
- Add column as nullable / add NOT VALID constraint / etc.
- Deploy new code that handles both shapes.
-
Deploy 2: Backfill (if applicable).
- Run a separate backfill script.
- Confirm data is good.
-
Deploy 3: Tighten.
- Set NOT NULL / VALIDATE / drop old column.
If single-step, just say: "Single migration. Apply directly."
Verification queries
-- Run these AFTER the migration to verify it worked:
SELECT [...]Rollback safety
[Note any caveats: e.g., "Rolling back loses data added since deploy."]
Step 6: Handle dangerous requests
If the request involves any of these, REFUSE generating until explicit confirmation:
- DROP TABLE
- TRUNCATE
- DROP COLUMN where the table likely has data
- Type narrowing that could lose data (TEXT → VARCHAR(N), DECIMAL → INT)
- DELETE without a WHERE clause
- UPDATE without a WHERE clause
- ALTER COLUMN to NOT NULL on a column that may have NULLs
Ask:
"This will [destructive action]. Confirm:
- Do you have a current backup?
- Is this on production data?
- Are you sure you want to proceed?
If yes to all, reply 'CONFIRM DROP'. I'll then generate the migration with a clear warning header."
If they confirm, generate it but include a warning header in the SQL:
-- ⚠️ DESTRUCTIVE MIGRATION ⚠️
-- This drops [thing] and CANNOT be undone by the down migration.
-- Verify backup exists before applying.
-- Confirmed by: [user/PR/etc.]Step 7: Don't run the migration
Generate the files. Do NOT execute them. Tell the user:
"Migrations generated at [paths]. To apply:
- Review them carefully
- Test on a non-prod database first
- Apply with [project's migration command]
I do not run migrations directly — that's your call to make."
Anti-patterns to avoid
- Generating only a forward migration without a reverse
- Wrapping
CREATE INDEX CONCURRENTLYin a transaction - Adding NOT NULL without a default to a non-empty table
- Renaming a column without the multi-deploy sequence
- Forgetting
IF NOT EXISTS/IF EXISTSclauses - Updating a non-trivial number of rows in the migration itself (use a separate backfill script)
- Adding indexes that duplicate existing ones (check the schema)
- Forgetting to update related ORM models / TypeScript types
Database-specific notes
PostgreSQL
- Most operations use ALTER TABLE
IF NOT EXISTS/IF EXISTSwidely supported- Concurrent index creation available
- DDL is transactional
MySQL
- ALTER TABLE rewrites the table (slow on large tables)
- Use
pt-online-schema-changeor gh-ost for large tables in prod - Limited transactional DDL support
- Watch out for implicit row format changes
MongoDB
- Schema changes are mostly application-level
- Use schema validators with
validationLevel: 'moderate'during transitions - Index creation: use
background: truein older versions; modern drivers default to background - For collection rename: use
db.collection.renameCollection()(atomic)
## Pairing with other skills
- **Code Reviewer skill** can review your migration before you apply it
- **Spec-Driven Builder** generates initial schemas — this skill handles changes after launch
## Tips
- For Postgres specifically, install `pg_repack` for online table reorganization
- Test migrations against a copy of production data, not just dev seed
- Keep migration files small — one logical change per migration is easier to roll back
- For Rails-style migrations, the skill follows the same patterns but uses Ruby DSL when applicable
## Limitations
- Doesn't auto-detect table size; relies on user input or assumes small unless told otherwise
- Cannot generate the data backfill script (separate concern — use a one-off script)
- Doesn't handle multi-database migrations (e.g., Postgres + Redis simultaneously) — generate them separately
- For very complex migrations (sharding, partitioning), this skill flags but doesn't fully handle — recommend consulting a DBA