ORM migration extraction has to fail closed
TypeORM, Knex, Sequelize, Prisma, and Drizzle all expose SQL differently. A migration safety tool has to keep valid statements, warn on dynamic pieces, and never let destructive SQL disappear from coverage.
Most production teams do not write one clean folder of plain SQL migrations.
They write TypeORM files with queryRunner.query(). They use Knex schema builders. They call Sequelize queryInterface helpers. Prisma writes migration.sql. Drizzle can emit SQL snapshots. Some teams mix raw SQL with builder calls because one migration needed a hand-written index or a special constraint.
That is why ORM extraction is part of migration safety, not a convenience feature.
The false-negative trap
Consider a migration that hides destructive SQL behind an alias:
const { manager } = queryRunner;
await manager.query('DROP TABLE users');
If the extractor only recognizes queryRunner.query(...), it misses the statement. The analyzer never sees DROP TABLE. The report can say “0 statements” or “safe” even though the migration would destroy a table.
That is the exact class of bug a Trust Contract has to prevent. The fix is not to pretend every JavaScript program can be statically understood. The fix is to recognize the common safe shapes, emit warnings for dynamic shapes, and avoid silently skipping known ORM aliases.
Keep the good statements
Failing closed does not mean giving up on the whole file.
If a migration has five static SQL statements and one dynamic statement, the report should analyze the five static statements and surface one unknown. That gives reviewers maximum useful signal:
- The known dangerous statements still get real lock and risk analysis.
- The unknown statement is visible and traceable to a source line.
- The file does not get treated as fully safe.
This is the difference between a conservative analyzer and a brittle one. The conservative analyzer says what it knows and what it does not know. The brittle one either skips too much or trusts too much.
Builders are SQL too
Knex and Sequelize add another wrinkle. A migration might not contain a raw SQL string at all:
const { schema } = knex;
await schema.dropTable('users');
That still means DROP TABLE users. It still takes an ACCESS EXCLUSIVE lock and it still deserves a CRITICAL result. A useful extractor has to lower common builder calls into SQL-shaped statements so the same analyzer rules apply.
The same principle applies to createTable, addColumn, dropColumn, foreign keys, indexes, and constraint helpers. The surface is different, but the Postgres lock behavior is the same.
When to warn
There are shapes the extractor should not guess:
- interpolated SQL fragments
- computed table names
- conditional builder paths that cannot be resolved statically
- unsupported callback shapes
- missing
up()migration bodies
Those should become extraction warnings. They are not always dangerous, but they are not proven safe either.
This is where coverage reporting matters. A warning hidden in stderr is not enough. The report itself should show that the analyzer could not cover the whole migration.
The practical review rule
The team rule can stay simple:
If pgfence says a statement is HIGH or CRITICAL, fix the migration or add a reviewed exception. If pgfence says a statement is unknown, make the SQL visible or have a reviewer sign off on that exact line.
That is enough to catch the common disaster paths without forcing every team to rewrite its migration framework.
ORMs are useful because they fit the application workflow. Migration safety tooling has to meet them there, then fail closed when the SQL stops being visible.