Db migration safety
Migration-safety lint (M22) — the static contract the migration-safety auditor (MSA) and db-migration-writer enforce on a candidate migration BEFORE it runs. Checks reversibility (down migration present / expand-contract), lock level (does it take ACCESS EXCLUSIVE on a hot table), table rewrite (full-table rewrites on type/default changes), destructive ops (DROP COLUMN/TABLE, TRUNCATE), unsafe enum mutation, NOT NULL without default on a large table, and schema drift between the declarative artifact and migration history. Feeds the Performance & Scale score.From its SKILL.md
npx -y skills add Hainrixz/claude-db --skill db-migration-safetyAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 19 stars19 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
5.7 KB, as published. Nobody here has run it
db-migration-safety (M22) — the migration lint contract
M22 lints a candidate migration as a static artifact before execution. It is the contract the migration-safety auditor and the db-migration-writer agent both honor. Feeds Performance & Scale (Almacenamiento/operability category). A destructive irreversible migration is the sev-5 cap on the performance axis.
What it checks (the lint contract)
- Reversibility — a usable
down/rollback exists, or the change follows expand-contract (add-new → backfill → switch → drop-old in separate deploys). A one-shot destructive step with no down is the highest-severity finding here. - Lock level — does the statement take a blocking lock on a hot table? In Postgres: adding a column
with a volatile default (pre-11),
ALTER COLUMN TYPE, adding aCHECK/FK withoutNOT VALID, non-CONCURRENTLYindex creation all takeACCESS EXCLUSIVE/ block writes. PreferCREATE INDEX CONCURRENTLY,ADD CONSTRAINT ... NOT VALIDthenVALIDATE CONSTRAINT. - Table rewrite — changes that force a full-table rewrite (certain
ALTER COLUMN TYPE, adding aNOT NULLcolumn with a volatile default on old engines) → slow + long lock on large tables. - Destructive ops —
DROP COLUMN,DROP TABLE,TRUNCATE,DROP NOT NULLreversals, dropping a column still read by deployed code. Destructive + irreversible = sev 5 (caps performance). - NOT NULL without default added to a populated table → fails or locks; require a backfill plan.
- Enum mutation — the durable concern: enum values cannot be removed or reordered (removal is
unsupported; renaming/reordering breaks dependents), so a changing set should be a lookup table,
not a native enum.
ALTER TYPE ... ADD VALUEis append-only and runs in a transaction on PG12+ (only older PG forbade it mid-txn). Flag enum changes; suggest lookup tables. - Schema drift — the declarative artifact (schema.prisma / structure.sql / snapshot) disagrees with the migration history (a column exists in one but not the other) → directional warn.
Tier-0 static checks
Parse migration SQL / ORM migration files (scripts/parse-schema.mjs, parse-orm-python.py). Scan for:
DROP TABLE|DROP COLUMN|TRUNCATE; ALTER COLUMN .* TYPE; ADD COLUMN .* NOT NULL without DEFAULT;
CREATE INDEX lacking CONCURRENTLY; ADD CONSTRAINT (FK/CHECK) lacking NOT VALID;
ALTER TYPE .* ADD VALUE / enum value removal or reorder; absence of a down/rollback block; drift between the
authoritative artifact (references/detection-signals.md precedence) and the latest migration.
Tier-1 verification query
- Affected-table size (does the lock matter?):
SELECT pg_size_pretty(pg_total_relation_size('<t>')), reltuples FROM pg_class WHERE relname='<t>'; - Lock the statement would take:
EXPLAIN/pg_locksinspection, or adry_run_migrationagainst a throwaway copy / shadow DB. Useverification.method: dry_run_migrationwhen a shadow DB is available; otherwisemigration_linton the static SQL. When live size is needed to decide severity but no DB is reachable →needs_api, never a silent pass.
Findings
Emit per schema/finding.schema.json. Example ids:
M22.20240115_drop_users_legacy.destructive_irreversible—DROP COLUMNwith no down and column still referenced (fail, severity 5, axisperformance, fixableproposed→ expand-contract plan).M22.add_index.lock_blocking—CREATE INDEXwithoutCONCURRENTLYon a hot table (warn, severity 4, axisperformance, fixableauto→ addCONCURRENTLY).M22.add_email_notnull.notnull_no_default—ADD COLUMN ... NOT NULLno default on a populated table (fail, severity 4, axisperformance, fixableproposed).M22.status_enum.enum_value_removed— enum value removed/reordered (unsupported; breaks dependents) — suggest a lookup table (warn, severity 3, axisperformance).M22.drift.column_in_artifact_not_migration— schema drift (warn, severity 2, axisperformance, confidencedirectional). Each finding:evidence.observedquotes the offending migration statement verbatim (secrets redacted);verification.reproduceis a runnable command above using$DATABASE_URL;verification.methodismigration_lintordry_run_migration;fix_previewcarries the safer rewrite (e.g.CONCURRENTLY,NOT VALID+VALIDATE, an expand-contract sequence);expected_impactcarries{axis, confidence, magnitude, rationale}— banded, never a naked %.
Honesty
- Severity of a lock depends on table size and write rate — without live evidence, a blocking op on an
unknown-size table is
directional(medium), not an established sev-5. Don't claim a downtime duration in seconds; magnitude banded only. - Only
DROP/TRUNCATE-class destructive and irreversible migrations cap performance at sev 5. Reversible or expand-contract changes are warns. Drift is directional and never caps. - Read-only: M22 lints and proposes; it never executes a migration. Execution is the writer agent's job behind explicit user accept.