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.