Unknown SQL is a product signal, not a footnote
A migration safety tool earns trust by saying what it could not prove. Unknown statements should show up in coverage, reports, and review policy instead of disappearing behind a green check.
The dangerous failure mode in migration review is not a noisy warning. It is a quiet pass.
If a tool cannot statically analyze part of a migration, that fact has to become visible. The reviewer needs to know how many statements were analyzed, how many statements were not analyzable, and where the gaps live. Otherwise the report teaches the team to trust a green result that was never proven.
That is why pgfence treats unknown SQL as part of the product surface, not as a logging detail.
Coverage is part of the verdict
A good migration report answers two questions:
- What did the analyzer find?
- What could the analyzer not prove?
The second question matters because real migrations are messy. ORMs build SQL with helper methods. Teams interpolate table names. Some frameworks hide SQL inside callbacks. Sometimes a migration has both static SQL and dynamic SQL in the same file.
If the static statements are reviewed but the dynamic statement is skipped, the report should not look like a full pass. It should say something like:
Analyzed: 3 statements | Unanalyzable: 1 (line 42) | Coverage: 75%
That line changes how a reviewer reads the output. A HIGH finding is still a HIGH finding, but a clean result with one unknown statement is not the same as a clean result with full coverage.
Unknown does not mean unsafe
Unknown is not a claim that the migration is dangerous. It is a claim about what the tool knows.
That distinction keeps the workflow honest. Early adopters can keep unknowns as warnings while they learn where dynamic SQL appears in their migration style. Stricter teams can switch unknown handling to block so any unanalyzable statement requires a human review path.
Both modes are valid. What is not valid is silent omission.
Why line numbers matter
Counts are useful, but line numbers are what make a report actionable.
If a reviewer sees “1 dynamic statement not analyzable” with no location, they have to search the file and guess what triggered the caveat. If the report says “line 42”, the reviewer can jump straight to the migration line, decide whether the SQL is benign, and either rewrite it or approve it with context.
That is especially important for ORM files. A TypeORM or Knex migration can contain several static statements and one dynamic branch. The right behavior is to keep analyzing the static statements and surface the dynamic one as an explicit coverage gap.
The policy shape
The policy model should stay simple:
warn: show unknowns in coverage and reports, but do not fail CI by default.block: treat unknowns as a review blocker because the analyzer could not prove the migration safe.
That lets a team adopt pgfence without forcing every migration style to change on day one. It also gives platform teams a clean path to tighten review rules as they find the places where dynamic SQL tends to hide.
The Trust Contract is plain: pgfence must not imply safety where it did not analyze. Unknown statements belong in the report, the editor, the JSON output, and the pull request comment.
What to look for in your own tooling
If you are evaluating a migration safety tool, do not only test whether it catches a known bad statement. Test what happens when it cannot parse one.
Try a dynamic table name. Try a computed SQL string. Try an ORM callback that contains one static raw query and one dynamic raw query. Then look at the report.
If the tool reports full coverage, it is making a stronger claim than it can support.
pgfence is intentionally conservative here. A warning that tells you where the analyzer stopped is better than a pass that quietly skipped the line that mattered.