Build1 publisher3 min readPublished
Normalizing agent SQL for review strips exactly the literals a DML reviewer needs
A dev.to walkthrough sets query fingerprints against literal text diffs for agent-written PostgreSQL. Its own fixture keeps both goldens, with distinct exit codes so CI can treat a reporting SELECT differently from privileged DML.
The Engineer · Build desk

What happened
- The review queue in the account held three agent rewrites of one reporting query, all formatted differently, with twelve minutes left before a freeze window.
- Under a fingerprint gate, an equivalent restyle stays green while any join, filter or projection change flips the digest against the committed golden and fails CI.
- The published fixture stores both a golden fingerprint and a canonical text file, and exits with distinct codes so CI can apply different rules to different query classes.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint A fingerprint golden cannot separate a status filter from a cross-status scan, so the gate that keeps high-churn reporting review quiet is the wrong one to put in front of security-definer paths.
- exposure A restyle carrying one changed constant clears either gate on its own: normalization drops the constant, and a text diff buries the moved column among reflowed CTEs and renamed aliases.
- decision Someone now has to label each query file by class, because two goldens only help if CI knows which failure is allowed to block the merge.
The normalizer in the post is regex text-processing, not a parser. One pattern deletes `--` and `/* */` comments. Another matches single-quoted strings and integer or decimal numbers. A third takes `AS` aliases, a fourth collapses runs of whitespace, and what survives is hashed and compared against a committed golden [23][10]. The listing breaks off inside `normalize()`, so the post does not publish the whole function [23].
Run the review queue through that. The rewrite that moved a date filter off `orders.created_at` onto a denormalized snapshot column changes a column in the predicate list, so the digest changes and the pull request fails [3][10]. Each of the three candidates also carried a new bind value, and literals are what the normalizer replaces before hashing [1][10]. So the fingerprint gate fails the moved filter and stays green on all three bind changes [25].
Twelve minutes across three rewrites is four minutes a candidate [24]. The gate has to fit inside that. The post is direct about what happens when it does not: under time pressure reviewers start rubber-stamping restyles and miss the one moved column [19].
The case for reading the text is about the literals themselves. A digest that replaces `'pending'` and `'closed'` with `?` cannot tell a status filter from an accidental cross-status scan [16]. Comments that document a lock-order constraint disappear [12]. Two queries can share a fingerprint while one binds a day and the other binds a decade [12]. The post puts it this way: reviewers who reconstruct intent from a digest are slower than reviewers who read a three-line diff of the predicate list [17].
So the article puts the literal-diff evidence on privileged DML, security-definer paths and queries that embed business constants [15]. `git diff` over `sqlfmt` output is simple, explainable to compliance, and needs no normalizer for the team to maintain [14]. Its cost is noise: different pretty-printers, optional `AS` keywords, unstable CTE names, and a diff that turns into a formatting argument [18]. It also fights bind-parameter style, because `'2026-09-17'` and `$1` are different text even when they are the same plan at runtime [20].
The post's warning about running both gates without a rule is specific: false red builds and silent plan drift [26]. PostgreSQL already takes the fingerprint side for observability, where `pg_stat_statements.queryid` collapses similar text so operators can track a workload [11]. A homemade normalizer is a different thing, and the post lists where the homemade ones break: dollar-quoted strings, `INTERVAL` literals and array constructors are easy to mishandle, and the collisions that follow are ones the gate will not see [13].
For a fingerprint golden to be the right gate in your repo, the churn your agents produce has to be the formatting churn the post describes, and your SQL has to stay clear of those constructs [6][13]. There is no measurement here either way. The author labels the examples proposals and unexecuted fixtures, and says to keep the artifact labeled unexecuted until a replica you control has loaded the schema [5][22].
What to watch
- Whether the author publishes the rest of normalize() and a run against a schema loaded on a replica.
- Whether a CI config appears that maps the fixture's two exit codes to labelled query classes per file.
- Whether teams skip the homemade normalizer and commit pg_stat_statements.queryid values as the golden.