Migration Safety Skill
Lock risk scoring for DDL scripts on PostgreSQL 18.4 - agent and human workflow to classify Flyway/Liquibase migrations before they block production traffic.
Recipe
-- Lock watch (run during staging migration)
SELECT pid, wait_event_type, wait_event, state, left(query, 80)
FROM pg_stat_activity
WHERE datname = current_database() AND pid <> pg_backend_pid();
SELECT l.mode, c.relname, a.query
FROM pg_locks l
JOIN pg_class c ON c.oid = l.relation
JOIN pg_stat_activity a ON a.pid = l.pid
WHERE NOT l.granted;## Risk scoring (summary)
| DDL | Default risk | Safer pattern |
|-----|--------------|---------------|
| CREATE INDEX | HIGH on >1M rows | CREATE INDEX CONCURRENTLY |
| ADD COLUMN NOT NULL no default | HIGH | add nullable → backfill → SET NOT NULL |
| ALTER TYPE | HIGH | expand-contract + new column |
| DROP COLUMN | MEDIUM | expand-contract deprecate first |
| ADD FK inline | HIGH | ADD NOT VALID → VALIDATE CONSTRAINT |When to invoke:
- Every PR touching
db/migrations/ - Advisor or agent generated DDL
- Hotfix migration during incident
Working Example
Migration file: V20260709__orders_priority.sql
ALTER TABLE orders ADD COLUMN priority text NOT NULL DEFAULT 'normal';
CREATE INDEX idx_orders_priority ON orders (priority);Skill analysis:
## Lock risk: HIGH
### Findings
1. `CREATE INDEX` without CONCURRENTLY takes ShareLock, blocks writes on large orders table
2. `ADD COLUMN ... DEFAULT` is metadata-only in PG 11+ for static default - OK
3. Index build is main blocker (~45M rows)
### Safer migration (split files)
-- V20260709_1__orders_priority_column.sql
ALTER TABLE orders ADD COLUMN IF NOT EXISTS priority text NOT NULL DEFAULT 'normal';
-- V20260709_2__orders_priority_index.sql (run in change window)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_priority ON orders (priority);-- Rollback
DROP INDEX CONCURRENTLY IF EXISTS idx_orders_priority;
ALTER TABLE orders DROP COLUMN IF EXISTS priority;Deep Dive
Scoring Matrix
| Factor | Low | High |
|---|---|---|
| Table rows | < 100k | > 10M |
| Traffic | staging | peak prod OLTP |
| Lock mode | ShareUpdateExclusive | AccessExclusive |
| Duration estimate | < 1s metadata | index build minutes |
Row Count Input
SELECT c.relname, c.reltuples::bigint AS est_rows
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public' AND c.relname = 'orders';CONCURRENTLY Caveats
-- CONCURRENTLY cannot run inside flyway transaction block
-- Use flyway outOfOrder + executeInTransaction=false pattern per runner docs
CREATE INDEX CONCURRENTLY idx_orders_priority ON orders (priority);Gotchas
- Flyway wraps in transaction -
CREATE INDEX CONCURRENTLYfails. Fix: separate migration withexecuteInTransaction=false. - VALIDATE CONSTRAINT still needs share lock - shorter than full table scan for FK verify, but plan window.
- DROP COLUMN in one step - breaks old app version during deploy. Fix: expand-contract across releases.
- Renaming column with TYPE change - rewrite table. Fix: new column, backfill, cutover.
- Missing rollback - forward-only teams still document manual rollback SQL for sev-1.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| expand-contract | Zero-downtime column changes | Brand new table |
| pg_repack | Bloat-only | Schema change |
| Logical replication cutover | Major rewrite | Simple index add |
FAQs
Is ADD COLUMN always low risk?
Static DEFAULT is metadata-only on modern PG. Volatile defaults or GENERATED expressions may rewrite. Read PG 18 release notes for edge cases.
Who approves HIGH risk migrations?
DBA + on-call lead; schedule change window; announce in status channel; prepare lock cancel policy.
Can agent auto-apply LOW risk on staging?
Human still merges PR. CI applies staging after review. Prod never auto-DDL from agent.
Related
- Agent Skills Basics - invocation rules
- Migrations Basics - folder layout
- Zero Downtime DDL - patterns
- Expand-Contract Pattern - column changes
- Large Table Migrations - backfill
Stack versions: This page was written for PostgreSQL 18.4, Flyway 10+, and Liquibase 4+.