DevOps & Security
Migration Safety Auditor
Analyzes database migrations for destructive operations, lock escalation risks, missing rollback scripts, and production runtime estimates. Useful for catching high-risk migrations before they ship. Backend engineers and DBAs shipping schema changes, SRE teams reviewing migrations before deploy, and startup CTOs whose teams do not have a dedicated database specialist. A single unsafe migration can take a service offline, corrupt data, or require hours of recovery work. AI-generated migrations frequently miss engine-specific hazards: Postgres rewrites a table when you add a NOT NULL column without a default, MySQL cannot add a column without a full table copy under some versions, and a missing rollback script turns a failed deploy into a manual recovery exercise. The cost of catching these issues before a deploy is minutes; the cost of catching them after is measured in incidents.
One-Time Purchase
$19.99
Migration Safety Audit — add_email_verified.sql
Engine: Postgres 15 · Table: users (estimated 12M rows from prior context)
Verdict
UNSAFE — do not deploy as written. The migration acquires long-held locks against the live users table at the index-build step and validates a CHECK constraint against every existing row under AccessExclusiveLock. At 12M rows the lock window is large enough to cause connection-pool exhaustion and 500-level errors. A three-deploy replacement is below.
Risk dimensions
Migration Under Review
-- add_email_verified.sql
ALTER TABLE users
ADD COLUMN email_verified BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE users
ADD COLUMN email_verified_at TIMESTAMPTZ;
CREATE INDEX users_email_verified_idx
ON users (email_verified);
ALTER TABLE users
ADD CONSTRAINT users_verified_at_present_when_verified
CHECK (email_verified = FALSE OR email_verified_at IS NOT NULL);
Findings
Finding 1: ADD COLUMN ... NOT NULL DEFAULT FALSE — historically a full rewrite
Severity: Low (PG 15) Operation: ALTER TABLE ADD COLUMN
In Postgres versions earlier than 11, adding a column with a non-null default required rewriting every row in the table. Postgres 11+ optimizes this for constant defaults (FALSE, literal values) by storing the default in the table's pg_attribute row instead of touching the heap — meaning this specific case is fast, not the catastrophe it would have been on Postgres 10.
However:
- The optimization applies only to literal defaults. If the default were a function call (e.g.
gen_random_uuid()), the rewrite kicks back in. - The operation still acquires
AccessExclusiveLockon the table for the duration of the metadata update — short, but during that window all reads and writes block. On a 12M-row table at peak traffic this is typically sub-second, but the lock blocks queries that are already running on the table, including long-running analytical queries.
Risk classification: Low-to-Medium for this specific literal-default case on Postgres 15. Would be HIGH if the default were a function call.
Finding 2: CREATE INDEX without CONCURRENTLY blocks writes
Severity: High Operation: CREATE INDEX
CREATE INDEX without CONCURRENTLY acquires ShareLock on the target table for the entire duration of the index build. On a 12M-row table the build is estimated at 45 seconds to 2 minutes depending on hardware and current load. During that window:
- All
INSERT,UPDATE, andDELETEagainstusersblock. SELECTqueries continue to run, but new write transactions queue up behind the lock.- If write throughput exceeds the connection pool size during the block, the application sees connection-pool exhaustion and returns 500s to users.
Additional concern: an index on a low-cardinality boolean column (email_verified: only TRUE or FALSE) is rarely useful. The planner will almost always choose a sequential scan over a 2-value index unless the column is very skewed (e.g. 99.9% one value). If the intent is to find unverified users, a partial index is the right shape:
CREATE INDEX CONCURRENTLY users_email_unverified_idx
ON users (id)
WHERE email_verified = FALSE;
Risk classification: HIGH for the locking; the index itself is probably the wrong shape regardless.
Finding 3: ADD CONSTRAINT ... CHECK validates against every existing row
Severity: Medium Operation: ADD CONSTRAINT (CHECK)
Adding a CHECK constraint validates every existing row in the table by default. On 12M rows this is a sequential scan under AccessExclusiveLock — estimated 8 to 25 seconds. Like Finding 2, this blocks writes.
The safe pattern is the two-step NOT VALID / VALIDATE CONSTRAINT dance:
-- Step 1: add the constraint, mark NOT VALID — fast, no row scan
ALTER TABLE users
ADD CONSTRAINT users_verified_at_present_when_verified
CHECK (email_verified = FALSE OR email_verified_at IS NOT NULL)
NOT VALID;
-- Step 2: validate in a separate transaction — takes a ShareUpdateExclusiveLock,
-- which does NOT block writes (only blocks other DDL)
ALTER TABLE users
VALIDATE CONSTRAINT users_verified_at_present_when_verified;
Risk classification: MEDIUM. Won't cause an outage on a healthy 12M-row table but will visibly degrade write latency for the duration.
Finding 4: No rollback script provided
Severity: Medium Operation: All four
Best practice for any production migration is a corresponding rollback script committed alongside it. The rollback for this migration is straightforward (drop the index, the constraint, and the columns in reverse order), but if the deploy fails partway through, the operator should not be discovering the rollback path on a 2 AM Pager Duty page.
Recommended Replacement
Split into three deploys, each idempotent and reversible:
Deploy 1: Add the columns (fast, online)
-- add_email_verified__part1_columns.sql
ALTER TABLE users
ADD COLUMN email_verified BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE users
ADD COLUMN email_verified_at TIMESTAMPTZ;
Rollback:
ALTER TABLE users DROP COLUMN email_verified_at;
ALTER TABLE users DROP COLUMN email_verified;
Deploy 2: Add the constraint (NOT VALID first, then validate)
-- add_email_verified__part2_constraint.sql
ALTER TABLE users
ADD CONSTRAINT users_verified_at_present_when_verified
CHECK (email_verified = FALSE OR email_verified_at IS NOT NULL)
NOT VALID;
Then, after the application has been deployed and is writing data consistent with the constraint, run in a separate transaction:
ALTER TABLE users
VALIDATE CONSTRAINT users_verified_at_present_when_verified;
Rollback:
ALTER TABLE users DROP CONSTRAINT users_verified_at_present_when_verified;
Deploy 3: Add the partial index (concurrent, no lock)
-- add_email_verified__part3_index.sql
CREATE INDEX CONCURRENTLY users_email_unverified_idx
ON users (id)
WHERE email_verified = FALSE;
Notes: CREATE INDEX CONCURRENTLY cannot run inside a transaction. Most migration tooling (Flyway, Liquibase, Drizzle, Prisma) supports flagging a migration as non-transactional; verify the tool's syntax before deploying. If the build fails partway, the resulting index is INVALID and must be dropped and re-created.
Rollback:
DROP INDEX CONCURRENTLY IF EXISTS users_email_unverified_idx;
Pre-Deploy Checklist
- Confirm Postgres version is 11+ (so the literal-default fast path applies). Below 11, Deploy 1 needs to be restructured to use
ADD COLUMN+UPDATE+SET NOT NULLin stages. - Confirm the application code reads
email_verifieddefensively. Between Deploy 1 and Deploy 2 application restart, some app instances will returnFALSEfor all users (including ones who'd previously been treated as verified by other means). Behavior must be acceptable during that window. - Confirm
CREATE INDEX CONCURRENTLYis supported by the migration tooling and that the migration is flagged as non-transactional. - Run all three on a staging environment first to confirm timings and lock behavior against representative data volume.
Post-Deploy Verification
After all three deploys land:
-- 1. Confirm the column exists and the default is captured
SELECT column_name, data_type, column_default, is_nullable
FROM information_schema.columns
WHERE table_name = 'users' AND column_name LIKE 'email_verified%';
-- 2. Confirm the constraint is VALID (not just NOT VALID)
SELECT conname, convalidated
FROM pg_constraint
WHERE conrelid = 'users'::regclass
AND conname = 'users_verified_at_present_when_verified';
-- 3. Confirm the index is valid and has the expected shape
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'users' AND indexname = 'users_email_unverified_idx';
Expect:
is_nullable = NOforemail_verified,YESforemail_verified_atconvalidated = truefor the check constraint- The partial index
WHERE email_verified = FALSEappears inindexdef
Summary
| Operation | Original Risk | Replacement Risk |
|---|---|---|
Add email_verified column | LOW (PG 11+ fast path) | LOW (unchanged) |
Add email_verified_at column | LOW | LOW |
Add CHECK constraint | MEDIUM (full-table validation under lock) | LOW (NOT VALID + later VALIDATE) |
Add index on email_verified | HIGH (blocks writes; wrong index shape) | LOW (CONCURRENTLY; partial index targeting actual query) |
| Rollback path | None | Documented per deploy |
| Calendar time to ship | 1 deploy | 3 deploys over a few days |
Recommendation: Do not deploy the original migration as written. Use the three-part replacement above. Total engineering effort to restructure is approximately 30 minutes; the alternative is a high-confidence incident window during deploy.
This audit reviews a hypothetical migration for illustration. Always run the recommended changes on a representative staging environment before production deploy.
This sample illustrates the skill's output format. Names, metrics, and operational details are illustrative unless the artifact explicitly analyzes public information.
View full sample →
All sales final. No refunds on digital products.
Includes support for Claude Code, Codex, OpenClaw, and Google Antigravity in the same license.
Also in Database Reliability
Bundle price: $55. Compare this skill with the full workflow bundle or Pro access.
Best for
Backend engineering teams shipping schema changes without a dedicated database specialist, SRE teams reviewing migrations before high-traffic deploys, and AI-assisted development workflows where the migration was generated rather than hand-written. Most valuable on Postgres and MySQL workloads with meaningful row counts (>100K rows) where a single unsafe migration translates directly into downtime.
Not ideal for
Greenfield databases where schema is still in flux and lock behavior on small tables does not matter, or for engines outside Postgres / MySQL / SQLite where the rule set differs significantly. Also a poor fit as a substitute for an on-call review when shipping irreversible operations like dropping a column with active reads.
Included in this purchase
- Claude Code, Codex, OpenClaw, and Google Antigravity skill files.
- Setup guidance for the right adapter in your workspace.
- One-time license for the purchased skill version.
Setup
Plan for a short setup in the repository or workspace where the skill will run. Some coding familiarity helps for implementation-heavy outputs.
Related Skills
$19.99
One-time license
$19.99
One-time license
$19.99
One-time license
Future Updates
This purchase includes the current version of the skill. If you want future adapter updates — meaning compatibility and packaging updates as supported platforms evolve — plus new catalog additions included automatically, upgrade to Pro.