skills/data-migrations/SKILL.md
Database schema and data migration patterns for zero-downtime production deployments. Use when: writing SQL migrations, changing DynamoDB table structures, running data backfills, planning breaking schema changes, designing rollback strategies, auditing an existing project for migration risks, or coordinating database changes with application deployments. Covers expand-contract migrations, backward-compatible changes, data backfills, rollback plans, and migration testing.
npx skillsauth add michaelsvanbeek/personal-agent-skills data-migrationsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Every migration must work with both the old and new application version running simultaneously. During deployment, both versions coexist. If the new schema breaks the old code, the deploy fails or requires downtime.
The safest approach for breaking schema changes. Three phases, each deployed independently:
Add the new structure alongside the old one. Both old and new code work.
-- Migration: Add new column (nullable, no default required)
ALTER TABLE users ADD COLUMN display_name TEXT;
Application writes to both old and new columns. Reads from old column.
Backfill data from old structure to new. Deploy application update to read from new column.
-- Backfill: Copy existing data
UPDATE users SET display_name = username WHERE display_name IS NULL;
Application reads from new column. Writes to both.
Remove the old structure. Only after confirming all reads use the new column.
-- Migration: Drop old column (only after full rollout verified)
ALTER TABLE users DROP COLUMN username;
Rule: Each phase is a separate migration and a separate deployment. Never combine expand and contract in one step.
| Operation | Safe? | Notes |
|-----------|-------|-------|
| ADD COLUMN (nullable) | Yes | No table rewrite, no lock |
| ADD COLUMN with DEFAULT (PG 11+) | Yes | Stored in catalog, no rewrite |
| ADD COLUMN NOT NULL without default | No | Fails on existing rows |
| DROP COLUMN | Caution | Safe if no code reads it; run contract phase |
| RENAME COLUMN | No | Breaks existing code instantly |
| ALTER COLUMN TYPE | No | May require table rewrite and exclusive lock |
| CREATE INDEX | No | Locks writes unless CONCURRENTLY |
| CREATE INDEX CONCURRENTLY | Yes | Non-blocking; slower but safe |
| DROP INDEX | Yes | Instant |
| ADD CONSTRAINT (CHECK, FK) | No | Scans table, holds lock |
| ADD CONSTRAINT ... NOT VALID + VALIDATE | Yes | Two-step: add without scanning, then validate without lock |
| Operation | Safe? | Notes | |-----------|-------|-------| | Add new attribute to items | Yes | Schema-free; old items unaffected | | Add GSI | Yes | Backfills asynchronously; reads return partial results during build | | Remove GSI | Yes | Instant, no data loss from base table | | Change partition key | No | Requires new table + data migration | | Change sort key | No | Requires new table + data migration | | Change attribute type | No | Old items retain old type; must backfill |
YYYYMMDDHHMMSS_description.sql
Examples:
20250115120000_add_display_name_to_users.sql
20250116090000_backfill_display_name.sql
20250120140000_drop_username_from_users.sql
Each migration file must contain both up and down sections:
-- migrate:up
ALTER TABLE users ADD COLUMN display_name TEXT;
-- migrate:down
ALTER TABLE users DROP COLUMN display_name;
down section).up and down in CI before deploying.Never update all rows in a single transaction — it locks the table and may time out:
import time
BATCH_SIZE = 1000
SLEEP_BETWEEN_BATCHES = 0.1 # seconds — limit load on database
def backfill_display_name(db: Connection) -> int:
"""Backfill display_name from username in batches."""
total_updated = 0
while True:
result = db.execute(
"""
UPDATE users
SET display_name = username
WHERE display_name IS NULL
AND id IN (
SELECT id FROM users
WHERE display_name IS NULL
LIMIT %(batch_size)s
)
""",
{"batch_size": BATCH_SIZE},
)
db.commit()
updated = result.rowcount
total_updated += updated
if updated < BATCH_SIZE:
break
time.sleep(SLEEP_BETWEEN_BATCHES)
return total_updated
import boto3
from typing import Any
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("users")
def backfill_attribute(attribute: str, compute_fn: Any) -> int:
"""Scan and update items missing an attribute."""
total = 0
scan_kwargs: dict[str, Any] = {
"FilterExpression": Attr(attribute).not_exists(),
}
while True:
response = table.scan(**scan_kwargs)
items = response.get("Items", [])
with table.batch_writer() as batch:
for item in items:
item[attribute] = compute_fn(item)
batch.put_item(Item=item)
total += 1
if "LastEvaluatedKey" not in response:
break
scan_kwargs["ExclusiveStartKey"] = response["LastEvaluatedKey"]
return total
--dry-run flag to preview changes without applying.Document the rollback plan:
## Migration: add_display_name_to_users
**Forward**: `ALTER TABLE users ADD COLUMN display_name TEXT`
**Rollback**: `ALTER TABLE users DROP COLUMN display_name`
**Data loss on rollback**: None (column is nullable, no data depends on it yet)
**Rollback window**: Before backfill runs (Phase 2)
| Scenario | Action |
|----------|--------|
| Migration failed partway | Run down migration; fix and retry |
| Migration succeeded but app broken | Run down migration; redeploy old app version |
| Backfill completed but data is wrong | Run corrective backfill; do NOT roll back schema |
| Contract phase removed old column | Cannot roll back — old data is gone. Restore from backup if needed |
Some migrations cannot be rolled back:
Rule: Identify the point of no return before executing. Ensure backups exist before crossing it.
# Example: migration step in deployment pipeline
steps:
- name: run-migrations
commands:
- dbmate up
when:
event: deployment
- name: deploy-application
commands:
- serverless deploy
depends_on:
- run-migrations
Rule: Migrations run before application deployment. Rollback migrations run after application rollback.
# Test migrations against a clean database
dbmate up # Apply all migrations
dbmate rollback # Roll back last migration
dbmate up # Re-apply — must succeed
After migrations, compare the resulting schema against expected state:
pg_dump --schema-only production_db > expected_schema.sql
pg_dump --schema-only test_db > actual_schema.sql
diff expected_schema.sql actual_schema.sql
After backfills, verify:
-- No NULLs remain after backfill
SELECT COUNT(*) FROM users WHERE display_name IS NULL;
-- Expected: 0
-- Row count unchanged
SELECT COUNT(*) FROM users;
-- Expected: same as before backfill
| Anti-Pattern | Problem | Fix |
|-------------|---------|-----|
| Rename column in one step | Breaks running application instantly | Use expand-contract: add new → backfill → drop old |
| ALTER TABLE with NOT NULL on populated table | Fails or locks table for full scan | Add nullable, backfill, then add constraint |
| CREATE INDEX without CONCURRENTLY | Blocks writes for duration of index build | Always use CREATE INDEX CONCURRENTLY in production |
| Backfill inside migration transaction | Locks table, may time out, blocks other queries | Run backfills as separate idempotent scripts |
| No rollback plan | Stuck if migration causes issues | Write down migration and document rollback before running up |
| Editing deployed migrations | Diverges local/CI from production state | Migrations are append-only; create new migration to fix |
| Running untested migrations in production | Schema drift, data loss | Test up and down in CI against real schema |
| Skipping the contract phase | Dead columns and indexes accumulate | Schedule contract cleanup; track in backlog |
When auditing an existing project for migration practices:
down step)ALTER TABLE uses blocking operations without CONCURRENTLY or equivalentdevelopment
TypeScript coding standards and type safety conventions. Use when: creating TypeScript files, defining interfaces and types, writing type-safe code, reviewing TypeScript for type correctness, auditing a codebase for type safety gaps, eliminating any or ts-ignore usage, or improving strict-mode compliance. Covers strict typing, avoiding any and ts-ignore, discriminated unions, Zod runtime validation, immutability patterns, and proper type definitions.
testing
Writing clear, actionable tickets in any issue tracker (Jira, Linear, GitHub Issues, ServiceNow, etc.). Use when: creating epics, stories, tasks, bugs, or spikes; writing acceptance criteria; decomposing work for a sprint; linking dependencies between tickets; auditing backlog items for clarity; or coaching a team on ticket quality. Covers title conventions, description templates, acceptance criteria, decomposition rules, dependency linking, and org-specific pluggable configuration.
development
Testing strategy, patterns, and evaluation for software and LLM/AI systems. Use when: writing tests, choosing test boundaries, designing test data, structuring test suites, evaluating LLM outputs, building evaluation pipelines, setting coverage thresholds, auditing test coverage gaps in existing projects, or improving test quality and structure.
development
Writing effective status updates for different audiences and cadences. Use when: writing a weekly status update, preparing a monthly summary, drafting a quarterly review, sending updates to leadership, sharing progress with stakeholders, or improving the clarity and impact of team communications. Covers weekly, monthly, and quarterly formats tailored for upward, lateral, and downward communication.