<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Morgan Li</title>
    <description>The latest articles on DEV Community by Morgan Li (@dataio_4921).</description>
    <link>https://dev.to/dataio_4921</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4061595%2Fab789ddd-b207-4aed-a459-0662d868f15b.png</url>
      <title>DEV Community: Morgan Li</title>
      <link>https://dev.to/dataio_4921</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dataio_4921"/>
    <language>en</language>
    <item>
      <title>Cost Estimates or Timed Canaries: A Debate for Promoting Agent SQL</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Mon, 21 Sep 2026 13:11:48 +0000</pubDate>
      <link>https://dev.to/dataio_4921/cost-estimates-or-timed-canaries-a-debate-for-promoting-agent-sql-4f27</link>
      <guid>https://dev.to/dataio_4921/cost-estimates-or-timed-canaries-a-debate-for-promoting-agent-sql-4f27</guid>
      <description>&lt;p&gt;On a Tuesday release window, an analytics agent proposed a four-join reporting query against a 40 million row events table. The planner estimated a few thousand cost units because the most selective predicate still used last week's statistics. Staging accepted the plan, then the first canary scanned far more heap pages than any review comment had predicted. Promotion, not generation, became the failure mode: the model wrote plausible SQL that static checks could not refute.&lt;/p&gt;

&lt;p&gt;This article treats that incident as a decision problem rather than a prompt-engineering story. Two credible camps now argue about the last gate before agent SQL reaches a shared database. One camp trusts PostgreSQL cost estimates as a cheap, lock-free rejector. The other camp insists on timed canaries against representative data, because cost units are not latency and because skew defeats the planner.&lt;/p&gt;

&lt;p&gt;The sections below compare both positions with a small, labeled harness you can run. The harness is a proposal, not a production benchmark, and it records estimates and wall time without claiming a universal SLO. Reader value sits in the decision rule; any named tool is optional and removable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why promotion is the bottleneck that tests miss
&lt;/h2&gt;

&lt;p&gt;Agent SQL usually fails after it already looks reviewable in a diff. The join graph compiles, the column names exist, and a unit fixture with ten rows returns the expected shape. Those tests do not encode correlation, TOAST size, or the histogram that autovacuum has not updated since the last backfill.&lt;/p&gt;

&lt;p&gt;Evaluation suites lose bite when models learn the shape of the suite rather than the shape of production data. Query promotion has the same failure mode, only with page cache and &lt;code&gt;random_page_cost&lt;/code&gt; instead of exam items. A gate that always passes is not a gate, and a gate that never runs the statement cannot see I/O.&lt;/p&gt;

&lt;p&gt;The practical question is therefore narrow. Which signal is allowed to veto a parsed, linted candidate, and when is that signal too expensive to collect on every agent attempt?&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: Planner cost estimates as a promotion gate
&lt;/h2&gt;

&lt;p&gt;Cost-based gates start from a simple operational fact: &lt;code&gt;EXPLAIN&lt;/code&gt; without &lt;code&gt;ANALYZE&lt;/code&gt; never executes the query. That property matters when an agent might emit a nested loop that only explodes after the first million rows. A reviewer can reject a candidate when total cost, estimated rows, or a sequential scan on a large relation crosses a numeric budget.&lt;/p&gt;

&lt;p&gt;Advocates also note that cost estimates stay comparable when statistics are frozen for the test. Teams can restore a catalog snapshot, run &lt;code&gt;EXPLAIN (FORMAT JSON)&lt;/code&gt;, and compare total cost against a stored ceiling. The comparison is deterministic, fast, and free of write locks, which makes it attractive in CI for high-frequency agents.&lt;/p&gt;

&lt;p&gt;PostgreSQL documents that the planner uses relation statistics to compute startup and total cost in abstract units, not milliseconds. The current &lt;code&gt;EXPLAIN&lt;/code&gt; reference is the primary source for that behavior, including &lt;code&gt;ANALYZE&lt;/code&gt; as the switch that actually runs the statement (&lt;a href="https://www.postgresql.org/docs/current/sql-explain.html" rel="noopener noreferrer"&gt;PostgreSQL EXPLAIN&lt;/a&gt;). When those statistics lag, the same mechanism will underprice a scan, which is the opening Position B uses.&lt;/p&gt;

&lt;p&gt;Even so, Position A remains rational for narrow OLTP lookups that must be screened hundreds of times per hour. A cost cap is a filter, not a proof of safety, and cheap filters belong at the earliest layer. Throwing away a bad plan before allocating a rehearsal host is an engineering choice, not a philosophical one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: Timed canaries on a rehearsal server
&lt;/h2&gt;

&lt;p&gt;Canary advocates treat cost units as a different quantity from the SLO the pager actually pages on. Wall time, shared buffer hits, and rows actually returned can diverge from the estimate when predicates correlate or when a TOAST table dominates I/O. A rehearsal run with a tight &lt;code&gt;statement_timeout&lt;/code&gt; converts that divergence into a binary promote-or-reject signal that CI can store.&lt;/p&gt;

&lt;p&gt;The second argument is statistical freshness rather than philosophy. Agent SQL often encodes filters the warehouse added this week, so last week's histogram cannot price the plan honestly. Measuring a read-only canary against a subset that preserves skew is then the only test that can fail for the right reason. Realistic API performance work makes the same claim at another layer: the test has to look like production traffic, not like a fixture.&lt;/p&gt;

&lt;p&gt;Canaries are not free in time or in data hygiene. They need a dataset that is not production, a timeout that is not infinite, and isolation from writers who serve customers. They also need a host you are willing to burn if the agent invents a pathological join, which is the only reason a scratch server belongs in this workflow.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach. When a team already has a staging replica, that replica is the correct canary target and no extra host is required. When it does not, MonkeyCode's free model access can draft candidate SQL, and the free server option can hold a throwaway rehearsal database for the harness below. Neither option replaces statistics management, anonymized subsets, or the decision rule, and this article does not claim quotas, hardware profiles, or durability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence both camps already accept
&lt;/h2&gt;

&lt;p&gt;Both sides agree that agent SQL should not meet production on the first execution of a new text. Both sides also agree that parser-level checks and timeout budgets answer different questions than promotion, so this debate does not reopen those gates. The remaining dispute is which signal may veto a candidate that already parsed and already sat under a statement timeout.&lt;/p&gt;

&lt;p&gt;Three shared facts constrain any honest comparison of the two camps. First, &lt;code&gt;EXPLAIN&lt;/code&gt; without &lt;code&gt;ANALYZE&lt;/code&gt; is cheap relative to execution, while &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; runs the statement and therefore needs a rehearsal role. Second, &lt;code&gt;statement_timeout&lt;/code&gt; aborts a canary but does not repair a bad join order for the next agent attempt. Third, frozen statistics make cost comparisons reproducible, and thawed statistics make them honest about today's data.&lt;/p&gt;

&lt;p&gt;A useful artifact has to record both signals instead of declaring a winner in prose alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A two-stage promotion harness
&lt;/h2&gt;

&lt;p&gt;The following workflow is a proposal. It does not execute against a warehouse until you point the connection string at a scratch database you own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Freeze the question, not the model output
&lt;/h3&gt;

&lt;p&gt;Store the candidate SQL, the intended read-only role, and the SLO in a small YAML file. Do not let the agent rewrite the SLO after it sees a failing canary, because that loop trains the model to game the gate. Keep the YAML in review so humans change budgets on purpose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# proposal: promo_case.yml — not a live production contract&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;events_daily_rollups&lt;/span&gt;
&lt;span class="na"&gt;slo_ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1500&lt;/span&gt;
&lt;span class="na"&gt;max_explain_cost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;250000&lt;/span&gt;
&lt;span class="na"&gt;statement_timeout_ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4000&lt;/span&gt;
&lt;span class="na"&gt;require_canary_if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;estimated_rows_gt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt;
  &lt;span class="na"&gt;seq_scan_relations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;events&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;event_payloads&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Capture a lock-free plan
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;EXPLAIN&lt;/code&gt; in JSON mode under a role that cannot write. Persist the total cost, planned rows, and node types beside the candidate. This is Position A as a command rather than a manifesto, and it should fail closed if the role is missing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- proposal: capture_plan.sql&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;default_transaction_read_only&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'day'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'enterprise'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Decide whether a canary is mandatory
&lt;/h3&gt;

&lt;p&gt;Apply the YAML thresholds before you spend rehearsal time. If estimated rows stay tiny and no large sequential scan appears, Position A may be sufficient for that candidate. If the plan touches a fact table or the cost sits near the cap, Position B becomes mandatory rather than optional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Time a bounded canary
&lt;/h3&gt;

&lt;p&gt;On a rehearsal host only, set &lt;code&gt;statement_timeout&lt;/code&gt; below human patience and above the published SLO. Record wall time, &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; buffer totals, and whether the timeout fired. Never point this step at a primary that serves customers, even if the SQL looks like a &lt;code&gt;SELECT&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# proposal: promo_harness.py — unexecuted example, scratch DB only
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;

&lt;span class="n"&gt;SQL_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CANDIDATE_SQL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;DSN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCRATCH_DSN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;SLO_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SLO_MS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1500&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;TIMEOUT_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STATEMENT_TIMEOUT_MS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;MAX_COST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MAX_EXPLAIN_COST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;250000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;ROW_TRIGGER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EST_ROWS_TRIGGER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_sql&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SQL_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dsn_looks_unsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;lowered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lowered&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;primary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;master&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;explain_only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET default_transaction_read_only = on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EXPLAIN (FORMAT JSON, COSTS) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Plan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total Cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Plan Rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_canary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET default_transaction_read_only = on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET statement_timeout = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TIMEOUT_MS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;elapsed_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1000.0&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;elapsed_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Plan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;dsn_looks_unsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refusing a DSN that looks like production&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_sql&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;autocommit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;est_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;explain_only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;stage_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_COST&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;need_canary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;est_rows&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;ROW_TRIGGER&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_COST&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stage_a_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stage_a_est_rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;est_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stage_a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stage_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;need_canary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;need_canary&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;stage_a&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;need_canary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;elapsed_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_canary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stage_b_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elapsed_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stage_b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;elapsed_ms&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;SLO_MS&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared_hit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Shared Hit Blocks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared_read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Shared Read Blocks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stage_b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stage_b_reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Persist both verdicts next to the SQL
&lt;/h3&gt;

&lt;p&gt;Write the JSON object beside the candidate in source control so later reviews see cost and milliseconds together. If Stage A rejects, skip the canary to keep the scratch host cheap and the logs readable. If Stage A passes and Stage B rejects, keep the cost number anyway so planner optimism becomes visible rather than anecdotal.&lt;/p&gt;

&lt;p&gt;Sample output from an unexecuted run would look like the object below, which is a fixture for the debate, not a measured cluster result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stage_a_cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;188432.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stage_a_est_rows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;240000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stage_a"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"need_canary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stage_b_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2210.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stage_b"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shared_hit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1204&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shared_read"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;88110&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Cheap to collect&lt;/th&gt;
&lt;th&gt;Distorted by stale stats&lt;/th&gt;
&lt;th&gt;Executes the query&lt;/th&gt;
&lt;th&gt;Suggested veto&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Parser and lint only&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Syntax or missing objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXPLAIN total cost&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, can under-reject&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Cost above the cap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estimated rows on a fact table&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Rows above threshold force a canary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timed canary with timeout&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Less than EXPLAIN alone&lt;/td&gt;
&lt;td&gt;Yes, read-only&lt;/td&gt;
&lt;td&gt;Time above SLO or timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buffer reads from ANALYZE&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Less than EXPLAIN alone&lt;/td&gt;
&lt;td&gt;Yes, read-only&lt;/td&gt;
&lt;td&gt;Reads explode versus a stored baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table is a decision aid, not a benchmark. Your numbers will differ with cache warmth, disk, and the quality of the data subset. Treat every ceiling as local until a week of promotions says otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  A decision rule you can operationalize
&lt;/h2&gt;

&lt;p&gt;Use both stages, in order, with an explicit exception list rather than a vibe. Stage A is a cheap rejector: if total cost exceeds the cap, do not promote and do not spend a canary. Stage B is mandatory when estimated rows on a fact table exceed the threshold, when the plan sequential-scans a large relation, when the SQL uses correlated subqueries, &lt;code&gt;OFFSET&lt;/code&gt; paging, or volatile functions, or when yesterday's canary and today's estimate already disagreed by more than a factor of three.&lt;/p&gt;

&lt;p&gt;If none of those hold, a passing cost gate may promote a narrow OLTP lookup without a timed run. That exception exists to keep CI fast, not to spare the agent from measurement as tables grow. Revisit the exception whenever autovacuum lag, a new index, or a warehouse backfill changes the shape of the fact table.&lt;/p&gt;

&lt;p&gt;The rule is deliberately silent on writes. This harness assumes &lt;code&gt;default_transaction_read_only&lt;/code&gt;, and agent-written DML or DDL needs rollback-first rehearsals that this debate does not cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;Cost units are not portable across PostgreSQL versions, &lt;code&gt;random_page_cost&lt;/code&gt;, or &lt;code&gt;work_mem&lt;/code&gt;, so a cap copied from another cluster is not evidence. Canary wall time depends on cache warmth, and a cold rehearsal host will reject queries that a warm replica would accept. Subset databases that drop the long tail of a skewed distribution will lie in the other direction and promote queries that production will punish.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; still runs the query, including functions with side effects if the role was not constrained. &lt;code&gt;statement_timeout&lt;/code&gt; does not undo work already performed by a trigger that volunteered to write. The DSN heuristic that looks for names such as &lt;code&gt;prod&lt;/code&gt; is a naming convention, not a security control, and it will miss a poorly named primary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should not use this approach
&lt;/h2&gt;

&lt;p&gt;Do not run timed canaries if you cannot obtain an anonymized subset and you would be tempted to use the primary instead. Do not use cost caps as the only gate if large tables sit unanalyzed for days. Regulated workloads that cannot copy rows onto a scratch host need EXPLAIN-only gates plus human sign-off, and they should not import customer payloads into a shared rehearsal server.&lt;/p&gt;

&lt;p&gt;Teams that already keep a production-shaped replica and a mature query bot may find the Python file redundant. In that case the decision rule still applies, but the host should be the replica you already trust. A second scratch machine adds noise without adding a new signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to count after you pick a side
&lt;/h2&gt;

&lt;p&gt;After a week of promotions, count four numbers and ignore the rest of the telemetry. Count Stage A rejects, Stage B rejects, promotions that later needed a human rollback, and canaries skipped under the OLTP exception. If Stage B never rejects, the SLO is too loose or the subset is too kind, and if Stage A never rejects, the cost cap is decorative.&lt;/p&gt;

&lt;p&gt;Those counts are the evidence this debate actually needs, and they cost nothing but a JSON file beside each candidate query. If you already operate a scratch database, run the harness there first; a free rehearsal host is only a convenience when that database does not exist.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>postgres</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>Golden Result Files or Invariant Assertions: A Debate for Agent SQL Tests</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Sun, 20 Sep 2026 11:19:03 +0000</pubDate>
      <link>https://dev.to/dataio_4921/golden-result-files-or-invariant-assertions-a-debate-for-agent-sql-tests-4l56</link>
      <guid>https://dev.to/dataio_4921/golden-result-files-or-invariant-assertions-a-debate-for-agent-sql-tests-4l56</guid>
      <description>&lt;p&gt;A payments team kept a directory of golden CSV files for every reporting query an agent was allowed to rewrite. Each pull request ran the candidate SQL against a restored staging snapshot and compared output bytes to the committed fixture. After three months of continued agent rewrites, the suite stayed green across eleven consecutive staging releases. Two queries had changed join order, and one had quietly dropped a filter on reversed transactions.&lt;/p&gt;

&lt;p&gt;The suite had not become more rigorous. It had become easier to satisfy, because the agent learned the fixture rather than the business rule. That pattern now shows up wherever coding agents emit SQL faster than review capacity can grow. The useful debate is not whether to test agent SQL. It is which oracle still fails when the model has already seen last week's expected rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem the suite stopped measuring
&lt;/h2&gt;

&lt;p&gt;Golden files are a literal oracle. They encode one accepted result for one frozen database image, then treat any byte difference as a regression. That design is excellent when the extract must match a regulator's file, a partner feed, or a previously signed financial close. It is a weak design when the agent is allowed to rewrite joins, push filters, or change aggregation order for cost.&lt;/p&gt;

&lt;p&gt;Invariant assertions are a property oracle. They encode rules that must remain true even when the result set is allowed to change shape slightly, or when the snapshot is a day newer than the fixture. Typical invariants include grain, uniqueness, referential closure, sign constraints, and reconciliation totals against a slower source of truth. They fail on meaning, not on formatting.&lt;/p&gt;

&lt;p&gt;This article treats both oracles as engineering instruments, not as ideology. The artifact is a small PostgreSQL scenario, two test styles, and a decision rule you can apply before the next agent rewrite lands in review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: keep golden result files
&lt;/h2&gt;

&lt;p&gt;Golden files win when correctness is defined as reproduction. If yesterday's close file is the contract, a new plan that returns the same rows in a different order is already a defect. Teams that ship CSV to banks, tax authorities, or data vendors often need that strictness, because downstream parsers treat column order and numeric formatting as part of the interface.&lt;/p&gt;

&lt;p&gt;They also win on diagnosis speed. A failing &lt;code&gt;diff&lt;/code&gt; points at the first mismatched row, which is easier to discuss in a pull request than a failed predicate about “settled amount within 0.5 percent.” When the staging snapshot is versioned beside the fixture, the test is deterministic and cheap to shard in CI.&lt;/p&gt;

&lt;p&gt;The failure mode is silent under-specification. Once an agent can emit SQL that recreates the fixture, it can drop a predicate that never fired on that snapshot. The test still passes. Production data that was absent from the restore then violates a rule nobody encoded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: replace files with invariant assertions
&lt;/h2&gt;

&lt;p&gt;Invariant tests win when the query is a living report, not an archival extract. Agent rewrites are usually trying to reduce work_mem, avoid sequential scans, or replace a correlated subquery with a join. Those edits should be allowed to change row order, column aliases used only internally, and even the physical plan. What must not change is the grain of the result and the money math.&lt;/p&gt;

&lt;p&gt;They also degrade more slowly as agents improve. A model that has memorized &lt;code&gt;expected/daily_settle.csv&lt;/code&gt; can still violate &lt;code&gt;SUM(amount) = SUM(leg_amount)&lt;/code&gt; or emit two rows for one &lt;code&gt;payment_id&lt;/code&gt;. Property checks keep failing after golden files have been saturated. That is the practical answer to evaluation suites that no longer discriminate.&lt;/p&gt;

&lt;p&gt;The failure mode is incomplete properties. If you only assert &lt;code&gt;COUNT(*) &amp;gt; 0&lt;/code&gt;, an agent can return the wrong customers and still look healthy. Invariants require the same design effort as a schema, and they are easy to under-build on the first pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete schema and two oracles
&lt;/h2&gt;

&lt;p&gt;The following objects are a teaching fixture, not a production dump. Label them as such if you adapt them. They are small enough to restore on a laptop Postgres and large enough to show both oracles disagreeing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- teaching fixture: settlement grain is one row per payment_id&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;payment_id&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;account_id&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount_cents&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_cents&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;reversed&lt;/span&gt;     &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;settled_at&lt;/span&gt;   &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ledger_legs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;leg_id&lt;/span&gt;       &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payment_id&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;amount_cents&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- seed omitted: include at least one reversed payment whose legs still sum&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Candidate query A is the sort of rewrite an agent proposes after reading an old report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- candidate_a.sql: looks cheaper, drops reversed filter&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_cents&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ledger_legs&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settled_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-09-01'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_cents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Candidate query B preserves the business filter the fixture happened not to exercise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- candidate_b.sql&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_cents&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reversed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settled_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-09-01'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_cents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_cents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ledger_legs&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Oracle 1: golden file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;-d&lt;/span&gt; staging &lt;span class="nt"&gt;-At&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;candidate_a.sql&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/got.csv
diff &lt;span class="nt"&gt;-u&lt;/span&gt; tests/golden/daily_settle.csv /tmp/got.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the restored snapshot contained no reversed rows in September, &lt;code&gt;candidate_a.sql&lt;/code&gt; matches the golden file. The &lt;code&gt;diff&lt;/code&gt; is silent. That is the saturation case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Oracle 2: invariants
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- tests/invariants/daily_settle.sql&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TEMP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="cm"&gt;/* paste candidate here */&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- grain: one row per payment&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;payment_id&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- no reversed payments in the report grain&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- money identity against legs&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_cents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;leg_sum&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ledger_legs&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;payment_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_cents&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leg_sum&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ROLLBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any of those &lt;code&gt;SELECT&lt;/code&gt; statements returning a row is a failed invariant. On the same snapshot, &lt;code&gt;candidate_a.sql&lt;/code&gt; can pass the golden file and fail the reversed-payment check the moment a single reversed row exists. That is the discrimination the suite had lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  A numbered rehearsal before you pick a side
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Freeze a staging restore hash, not a production connection string, and record the restore command in the review notes.&lt;/li&gt;
&lt;li&gt;Classify the query as extract, aggregate, or mutating statement; mutating SQL does not belong in either oracle until it has a savepoint rehearsal.&lt;/li&gt;
&lt;li&gt;Run the current human-written SQL and the agent candidate against the same restore, capturing both row hashes and invariant violations.&lt;/li&gt;
&lt;li&gt;If the golden file matches and an invariant fails, keep the invariant and treat the file as stale documentation.&lt;/li&gt;
&lt;li&gt;If both pass, add one negative row to the restore that should make a bad rewrite fail, then rerun before merge.&lt;/li&gt;
&lt;li&gt;Only then accept a plan change, index hint, or CTE rewrite from the agent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A coding agent that can reach a free remote model and a free server is useful in step 3, because you can generate several candidate rewrites without burning a production quota. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I used MonkeyCode in that narrow slot: free model access and a free server option to emit candidate SQL files, then ran the oracles above outside the product. The oracles remain the method even if you generate the SQL by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# labeled workflow, not a benchmark
# 1. ask the agent for three semantically equivalent rewrites
# 2. store them as candidate_a.sql, candidate_b.sql, candidate_c.sql
# 3. execute golden diff + invariant SQL on the same restore
# 4. keep the candidate that preserves invariants at the lowest EXPLAIN cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not treat generation latency, token ceilings, or hardware size as known facts here. Those numbers change, and this debate does not depend on them. What matters is that candidate generation is cheap enough to produce disagreements your tests can still see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence you can collect in one afternoon
&lt;/h2&gt;

&lt;p&gt;You do not need a published benchmark to choose an oracle. You need a disagreement matrix from your own restore. The table below is the artifact to fill, not a claim about any vendor.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query class&lt;/th&gt;
&lt;th&gt;Golden file result&lt;/th&gt;
&lt;th&gt;Invariant result&lt;/th&gt;
&lt;th&gt;Merge rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Regulatory extract, fixed column order&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Prefer golden; invariants are extra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regulatory extract&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;td&gt;Block merge; fixture is under-specified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent join rewrite&lt;/td&gt;
&lt;td&gt;Fail (row order)&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Prefer invariants; sort in the exporter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent filter rewrite&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;td&gt;Block merge; snapshot lacked the filtered rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time-window aggregate&lt;/td&gt;
&lt;td&gt;Fail (new day)&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Prefer invariants; rebase golden files daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mutating upsert&lt;/td&gt;
&lt;td&gt;Either&lt;/td&gt;
&lt;td&gt;Either&lt;/td&gt;
&lt;td&gt;Do not use these oracles; use savepoints&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fill the table with three real queries from your warehouse, not with synthetic slogans. If every cell is “both pass,” your negative fixtures are too weak, and the agent is no longer being tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision rule
&lt;/h2&gt;

&lt;p&gt;Use golden result files as the primary oracle when all three conditions hold: the output is a contractual extract, the staging snapshot is versioned with the fixture, and row order plus formatting are part of the interface. Use invariant assertions as the primary oracle when any of these is true: the agent may change plans for cost, the snapshot ages faster than the fixture, or the business cares about grain and totals rather than byte identity.&lt;/p&gt;

&lt;p&gt;If both oracles are affordable, run invariants first and keep golden files only for extract queries. That ordering catches silent filter drops before a &lt;code&gt;diff&lt;/code&gt; can be argued away as “just sorting.” If you can afford only one oracle this quarter, choose invariants for agent-written SQL and leave golden files on the human-maintained extracts.&lt;/p&gt;

&lt;p&gt;A short rule that fits on a runbook card: &lt;em&gt;byte identity for files you ship; properties for queries you rewrite.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this
&lt;/h2&gt;

&lt;p&gt;This approach assumes a restorable staging database and queries that are read-only during review. It does not replace parser gates, statement timeouts, or privilege scoping, and it does not prove that a query will hold locks safely. Teams without a snapshot pipeline should not pretend a laptop subset is an oracle for production cardinality.&lt;/p&gt;

&lt;p&gt;Skip golden files if your reports are timezone-sensitive and the fixture is rebuilt from a moving clock. Skip invariants if nobody on the team can name the grain of the result in one sentence. Skip agent generation entirely for DDL, role changes, and anything that writes without a rehearsed down path.&lt;/p&gt;

&lt;p&gt;The tests you already have will keep passing as agents get better at imitating them. The decision is whether your next oracle still knows how to fail. If you want a place to generate extra SQL candidates before those oracles run, MonkeyCode’s free model access and free server option are one way to do that rehearsal without pointing the agent at production.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>postgres</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>Query Fingerprints or Literal Text Diffs: A Debate for Agent SQL Regression</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Fri, 18 Sep 2026 07:45:27 +0000</pubDate>
      <link>https://dev.to/dataio_4921/query-fingerprints-or-literal-text-diffs-a-debate-for-agent-sql-regression-4emi</link>
      <guid>https://dev.to/dataio_4921/query-fingerprints-or-literal-text-diffs-a-debate-for-agent-sql-regression-4emi</guid>
      <description>&lt;p&gt;A Tuesday review queue held three agent rewrites of the same reporting query, each formatted differently and each carrying a new bind value. The text diff looked noisy, the join graph looked unchanged, and the reviewer had twelve minutes before a freeze window. None of the candidates touched writes, yet one rewrite moved a date filter from &lt;code&gt;orders.created_at&lt;/code&gt; onto a denormalized snapshot column. The real question was not which assistant drafted the SQL, but which regression gate should fail the pull request.&lt;/p&gt;

&lt;p&gt;This article treats that choice as a two-sided debate with evidence, a small runnable artifact, and a decision rule. The setting is PostgreSQL review for agent-written &lt;code&gt;SELECT&lt;/code&gt; and constrained &lt;code&gt;DML&lt;/code&gt;, not a claim about any particular production outage. Examples below are labeled proposals and unexecuted fixtures, not measured customer results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agent SQL breaks naive regression gates
&lt;/h2&gt;

&lt;p&gt;Agent-written SQL rarely arrives as a single canonical string, even when the logical plan is stable across attempts. Whitespace, alias names, literal formatting, and CTE labels change while the join graph and predicates stay equivalent. A gate that compares raw text therefore fails on harmless restyles and can bury the one predicate that actually moved. A gate that compares fingerprints can hide a literal that now scans an unbounded date range or an unparameterized &lt;code&gt;IN&lt;/code&gt; list.&lt;/p&gt;

&lt;p&gt;Review agents also outgrow the tests that only assert “the query still runs.” Runtime success does not prove that the accepted string is the same workload the team intended to keep. Fingerprints and literal diffs measure different failure modes, and mixing them without a rule produces both false red builds and silent plan drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: normalize to a fingerprint, then compare
&lt;/h2&gt;

&lt;p&gt;The fingerprint camp argues that reviewers should sign off on a workload identity, not on a pretty-printed string. Literals, comments, and ignorable whitespace are stripped or replaced, then a digest is compared to a committed golden. Equivalent restyles stay green; a join, filter, or projection change flips the digest and fails CI.&lt;/p&gt;

&lt;p&gt;Evidence for this side is strongest on high-churn reporting SQL, where agents repeatedly rename aliases and reflow CTEs. PostgreSQL already thinks in normalized identities through &lt;code&gt;pg_stat_statements.queryid&lt;/code&gt;, which collapses similar text so operators can track a workload rather than a file. Teams that store goldens as digests also keep review noise low when the only delta is formatting.&lt;/p&gt;

&lt;p&gt;The cost is information loss. Two queries can share a fingerprint shape while one binds a day and the other binds a decade. Comments that document a lock-order constraint disappear. Dollar-quoted strings, &lt;code&gt;INTERVAL&lt;/code&gt; literals, and array constructors are easy to mishandle in a homemade normalizer, which creates collisions the gate will not see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: keep literal text diffs, then require a human on every token
&lt;/h2&gt;

&lt;p&gt;The literal-diff camp argues that agent SQL is an audit artifact, not only a plan. Every changed character is a chance to introduce a new table, a broader predicate, or a function with a different volatility. &lt;code&gt;git diff&lt;/code&gt; on &lt;code&gt;sqlfmt&lt;/code&gt; output is simple, explainable to compliance, and does not depend on a normalizer the team must maintain.&lt;/p&gt;

&lt;p&gt;Evidence for this side is strongest on privileged &lt;code&gt;DML&lt;/code&gt;, security-definer paths, and queries that embed business constants. A fingerprint that replaces &lt;code&gt;'pending'&lt;/code&gt; and &lt;code&gt;'closed'&lt;/code&gt; with &lt;code&gt;?&lt;/code&gt; cannot tell a status filter from an accidental cross-status scan. Reviewers who must reconstruct intent from a digest are slower than reviewers who read a three-line diff of the predicate list.&lt;/p&gt;

&lt;p&gt;The cost is noise. Agents emit different pretty-printers, optional &lt;code&gt;AS&lt;/code&gt; keywords, and unstable CTE names, so the diff becomes a formatting argument. Under time pressure, reviewers start rubber-stamping restyles and miss the one moved column. Literal gates also fight bind-parameter style, because &lt;code&gt;'2026-09-17'&lt;/code&gt; and &lt;code&gt;$1&lt;/code&gt; are different text even when they are the same plan at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Artifact: a dual-gate fixture you can run on a replica
&lt;/h2&gt;

&lt;p&gt;The proposal below is a regression fixture, not a production benchmark. It stores a golden fingerprint and a canonical text file, then fails with distinct exit codes so CI can apply different rules per query class. Label it unexecuted until a replica you control has loaded the schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# proposal: sql_regression_gates.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;COMMENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--.*?$|/\*.*?\*/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;LITERAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?x)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(?:&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="s"&gt;|[^&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;])*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;|\b\d+\.\d+\b|\b\d+\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;AS_ALIAS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\s+as\s+([a-z_][a-z0-9_]*)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;WS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\s+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;COMMENT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LITERAL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AS_ALIAS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; as _a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;WS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="n"&gt;golden_sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# fingerprint | literal | both
&lt;/span&gt;    &lt;span class="n"&gt;fp_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;golden_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;golden_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;golden_sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fingerprint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fp_ok&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;literal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;golden_sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fp_ok&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;text_ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;fp_ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair the script with a statement timeout and a read-only role on the replica. The commands below are a rehearsal sequence, not a claim that any host already ran them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPLICA_DSN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="sh"&gt;'
SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;
SET lock_timeout = '2s';
SET statement_timeout = '5s';
-- optional: confirm the role cannot write
SELECT current_user, inet_server_addr();
&lt;/span&gt;&lt;span class="no"&gt;SQL

&lt;/span&gt;python sql_regression_gates.py candidate.sql golden.sql both
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  &lt;span class="c"&gt;# 0 ok, 10 fingerprint miss, 11 literal miss, 12 shape-ok but text drifted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A second probe records whether the planner still sees the expected nodes after a fingerprint match. Keep this as a fixture &lt;code&gt;EXPLAIN&lt;/code&gt; with &lt;code&gt;COSTS OFF&lt;/code&gt;, because cost numbers move with cache and autovacuum and should not be the identity of the query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;VERBOSE&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_cents&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-09-01'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach. A practical way to exercise both gates is to let a coding assistant propose candidate SQL, then run the script against a disposable replica. MonkeyCode's free model access and free server option can host that rehearsal loop without pointing the agent at the primary; they do not replace the decision rule below, and this article does not claim model names, token quotas, or hardware ratings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table for query class
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query class&lt;/th&gt;
&lt;th&gt;Default gate&lt;/th&gt;
&lt;th&gt;Fail closed when&lt;/th&gt;
&lt;th&gt;Allow fingerprint-only when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read-only report, no row-security predicates&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;both&lt;/code&gt;, warn on text drift&lt;/td&gt;
&lt;td&gt;Join graph or filter digest changes&lt;/td&gt;
&lt;td&gt;Alias and CTE restyles with identical &lt;code&gt;EXPLAIN&lt;/code&gt; nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parameterized OLTP lookup&lt;/td&gt;
&lt;td&gt;fingerprint plus bind-arity check&lt;/td&gt;
&lt;td&gt;Placeholder count changes&lt;/td&gt;
&lt;td&gt;Formatter-only diffs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privileged &lt;code&gt;UPDATE&lt;/code&gt;/&lt;code&gt;DELETE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;literal text&lt;/td&gt;
&lt;td&gt;Any token changes without human&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries with status literals or tenant ids&lt;/td&gt;
&lt;td&gt;literal on the predicate list&lt;/td&gt;
&lt;td&gt;Literal replaced by &lt;code&gt;?&lt;/code&gt; in the golden&lt;/td&gt;
&lt;td&gt;None; those literals are the contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security-definer or &lt;code&gt;search_path&lt;/code&gt; sensitive SQL&lt;/td&gt;
&lt;td&gt;literal plus fully qualified names&lt;/td&gt;
&lt;td&gt;Unqualified relation names appear&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Numbered rule a review bot can apply
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Classify the file from path and the first statement verb before any model output is trusted. &lt;code&gt;dml/&lt;/code&gt; and &lt;code&gt;security/&lt;/code&gt; default to literal; &lt;code&gt;reports/&lt;/code&gt; may use fingerprints.&lt;/li&gt;
&lt;li&gt;Run the replica with &lt;code&gt;READ ONLY&lt;/code&gt;, &lt;code&gt;lock_timeout&lt;/code&gt;, and &lt;code&gt;statement_timeout&lt;/code&gt; so a bad candidate cannot wait on a lock or scan without a budget.&lt;/li&gt;
&lt;li&gt;Compute the fingerprint and the literal result as separate exit codes, then map them through the table rather than a single boolean.&lt;/li&gt;
&lt;li&gt;If the fingerprint matches and the literal drifts, require a human only when the class is privileged or literal-sensitive; otherwise record a restyle.&lt;/li&gt;
&lt;li&gt;If the fingerprint misses, reject even when &lt;code&gt;EXPLAIN&lt;/code&gt; still says &lt;code&gt;Index Scan&lt;/code&gt;, because a moved predicate can keep the same node type while changing cardinality.&lt;/li&gt;
&lt;li&gt;Store goldens as both &lt;code&gt;golden.sql&lt;/code&gt; and &lt;code&gt;golden.fp&lt;/code&gt; so a normalizer bug is visible as a dual mismatch instead of a silent collision.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Limitations the gates will not hide
&lt;/h2&gt;

&lt;p&gt;Homemade normalizers are not PostgreSQL's parser. Dollar quotes, &lt;code&gt;E''&lt;/code&gt; escapes, &lt;code&gt;WITH ORDINALITY&lt;/code&gt;, and &lt;code&gt;jsonb&lt;/code&gt; literals can collapse two different strings into one digest. &lt;code&gt;pg_stat_statements.queryid&lt;/code&gt; is safer than a regex, but it requires executing or parsing on a real engine and still ignores whether a bound range is one day or ten years.&lt;/p&gt;

&lt;p&gt;Fixture &lt;code&gt;EXPLAIN&lt;/code&gt; without &lt;code&gt;ANALYZE&lt;/code&gt; will not catch a sequential scan that only appears after autovacuum lags. Literal diffs will not catch a logically identical query that switched from a partial index to a full index because statistics moved. Neither gate proves row-level security, because RLS depends on &lt;code&gt;SET ROLE&lt;/code&gt; and session variables the file may not contain.&lt;/p&gt;

&lt;p&gt;The dual-gate script also assumes one statement per file. Agents that emit batches, temp tables, or &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; need a splitter and a different rehearsal, which this article does not provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should not use this approach
&lt;/h2&gt;

&lt;p&gt;Skip fingerprint goldens if the team cannot maintain a parser-quality normalizer or cannot run PostgreSQL in CI. Skip literal-only gates if the review queue is already flooded with formatter noise and reviewers have stopped reading diffs. Skip both if the agent is allowed to choose tables dynamically from a live catalog without a frozen contract, because there is then no stable golden to compare.&lt;/p&gt;

&lt;p&gt;Teams without a replica that matches production extensions, collations, and &lt;code&gt;search_path&lt;/code&gt; should not treat a green local gate as promotion evidence. The debate is about which identity to store, not about skipping rehearsal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the queue without a slogan
&lt;/h2&gt;

&lt;p&gt;Agent SQL becomes engineering work when the merge rule is explicit: fingerprints guard workload shape, literal diffs guard privileged tokens, and the table decides which failure is blocking. The opening review queue needed that split more than it needed another restyled CTE. If a spare replica is useful while you wire the exit codes, MonkeyCode's free server option is one place to rehearse the suite before the freeze window starts.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>postgres</category>
      <category>ai</category>
      <category>testing</category>
    </item>
    <item>
      <title>Timeout Budgets or Work-Mem Caps: A Debate for Agent-Written SQL</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Thu, 17 Sep 2026 05:42:02 +0000</pubDate>
      <link>https://dev.to/dataio_4921/timeout-budgets-or-work-mem-caps-a-debate-for-agent-written-sql-58he</link>
      <guid>https://dev.to/dataio_4921/timeout-budgets-or-work-mem-caps-a-debate-for-agent-written-sql-58he</guid>
      <description>&lt;p&gt;A staging replica accepted an agent-written reporting query that never finished its hash join. The statement held a share lock on a large fact table while nested loops spilled to disk. Review comments had approved the SQL because the join keys looked correct and the filters used indexed columns. The missing control was not syntax; it was a resource envelope the session never received.&lt;/p&gt;

&lt;p&gt;This article treats that rehearsal case as a design debate, not a war story with invented outage metrics. Two credible camps now argue about how SQL review agents should bound work. One camp pins a clock on every statement. The other camp pins memory, temp files, and planner ceilings before the first row is touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why resource envelopes belong in SQL review
&lt;/h2&gt;

&lt;p&gt;Agent-written SQL often looks locally reasonable and still saturates a replica. Join order, predicate shape, and index names can pass a linter while the planner chooses a hash that spills. Human reviewers read intent. Database engines spend CPU, &lt;code&gt;work_mem&lt;/code&gt;, and lock time. Those two views diverge under agent volume.&lt;/p&gt;

&lt;p&gt;The last week of developer discussion around AI coding quality keeps returning to the same gap. Generating a query is cheap. Owning its runtime envelope is still engineering. For SQL review agents, the practical question is which envelope to encode as policy, and which envelope to leave as an incident runbook.&lt;/p&gt;

&lt;p&gt;The rest of this piece compares timeout budgets with memory caps, then offers a labeled rehearsal harness. No production timings are claimed. The harness is a method you can run against a disposable Postgres instance you already operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: timeout budgets as the primary gate
&lt;/h2&gt;

&lt;p&gt;Timeout advocates treat wall-clock time as the only signal operators will actually enforce. A statement that exceeds a budget is cancelled, the transaction ends, and the agent receives a typed failure. Review then becomes a question of whether the proposed SQL can finish inside a published ceiling.&lt;/p&gt;

&lt;p&gt;The evidence for this camp is operational, not aesthetic. On-call teams already know &lt;code&gt;statement_timeout&lt;/code&gt; and &lt;code&gt;lock_timeout&lt;/code&gt;. Orchestrators already retry on cancellation. Product owners already understand “this report may run for thirty seconds.” A clock is easy to explain in a pull request and easy to test with &lt;code&gt;pg_sleep&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A typical session envelope looks like the following labeled example. Treat it as a contract template, not a benchmark.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- labeled example: statement envelope, not a measured SLA&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;application_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sql_review_agent'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'15s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'3s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;idle_in_transaction_session_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'10s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- agent SQL lands only after the GUCs above are applied&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-09-01'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Timeout budgets fail in a predictable way. A query can be cheap for ten seconds and then explode on the eleventh because a filter was not selective. Cancellation also leaves partial work if the agent used autocommit DML. The clock answers “how long,” not “how heavy.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: work-mem caps as the primary gate
&lt;/h2&gt;

&lt;p&gt;Memory-cap advocates treat planner resource use as the real hazard. Hash joins, sorts, and materialize nodes consume &lt;code&gt;work_mem&lt;/code&gt; per node, not per session, and temp files can fill a disk long before a timeout fires. A fifteen-second query that spills a few gigabytes is still an incident on a shared replica.&lt;/p&gt;

&lt;p&gt;This camp wants the review agent to emit session GUCs and planner constraints together with the SQL. The SQL is not “approved” until the envelope would keep sorts in a bounded arena. Operators then fail the review when &lt;code&gt;EXPLAIN&lt;/code&gt; shows sorts or hashes that would exceed the published cap at the stated row estimates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- labeled example: memory and temp-file envelope&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;work_mem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'16MB'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;temp_file_limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'256MB'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;max_parallel_workers_per_gather&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;enable_nestloop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-09-01'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The evidence here is structural. Postgres documentation is explicit that &lt;code&gt;work_mem&lt;/code&gt; is per operation, so a query with several hash nodes multiplies the ceiling. Disk spill is visible in &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; after a rehearsal run. Memory caps catch “small time, huge footprint” queries that timeout policy would bless.&lt;/p&gt;

&lt;p&gt;The weakness is estimation. If statistics are stale, the plan JSON understates memory, and the cap becomes theater. Memory GUCs also change plans, so a review that rewrites &lt;code&gt;work_mem&lt;/code&gt; may approve a shape that production, with a higher default, will never use. Caps without a frozen statistics snapshot are incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Artifact: a decision table and rehearsal loop
&lt;/h2&gt;

&lt;p&gt;The original artifact is a decision table plus a small rehearsal script. The table is meant for review bots that must choose an envelope before they emit “approve.” Rows are query classes, not product claims.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query class&lt;/th&gt;
&lt;th&gt;Timeout budget first?&lt;/th&gt;
&lt;th&gt;Work-mem cap first?&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Point lookup by primary key&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Clock detects lock waits; memory is already tiny&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregates on filtered facts&lt;/td&gt;
&lt;td&gt;Split&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Hash aggregates spill before wall-clock alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent DML with joins&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Need lock_timeout and a tight work_mem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Window functions over dates&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Sorts dominate; a long timeout hides spill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-database reporting&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Split&lt;/td&gt;
&lt;td&gt;Remote waits look like CPU; cap alone misleads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown cardinality&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Dual envelope, then refuse if either trips&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use the table as a routing rule inside the reviewer, not as a production SLA. The following labeled Python shows one rehearsal loop. It applies both envelopes, runs &lt;code&gt;EXPLAIN&lt;/code&gt; only, and prints which gate would have fired. It does not execute DML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# labeled rehearsal: unexecuted against your instance until you fill DSN
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;

&lt;span class="n"&gt;TIMEOUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REVIEW_STATEMENT_TIMEOUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;15s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;WORK_MEM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REVIEW_WORK_MEM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;16MB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;SQL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CANDIDATE_SQL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# SELECT-only for this harness
&lt;/span&gt;
&lt;span class="n"&gt;ENVELOPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
SET statement_timeout = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TIMEOUT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;;
SET lock_timeout = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;;
SET work_mem = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;WORK_MEM&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;;
SET temp_file_limit = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;256MB&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;;
SET max_parallel_workers_per_gather = 0;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;EXPLAIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EXPLAIN (FORMAT JSON, VERBOSE) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;SQL&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;node_needs_memory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;plan_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Node Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;plan_type&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hash Join&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Aggregate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WindowAgg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;node_needs_memory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Node Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plan_rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Plan Rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sort_space&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sort Space Used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Plans&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REVIEW_DSN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;autocommit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ENVELOPE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXPLAIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Plan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;memory_nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory_nodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeout_budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;work_mem&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WORK_MEM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory_nodes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;memory_nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;would_cap_memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory_nodes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Numbered rehearsal steps keep the debate testable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Restore a sanitized schema snapshot into a disposable database you control, never a shared warehouse endpoint.&lt;/li&gt;
&lt;li&gt;Freeze statistics with &lt;code&gt;ANALYZE&lt;/code&gt; on the tables the candidate SQL touches, then store &lt;code&gt;pg_stats&lt;/code&gt; row counts beside the review artifact.&lt;/li&gt;
&lt;li&gt;Apply the timeout budget alone, run &lt;code&gt;EXPLAIN&lt;/code&gt; (not &lt;code&gt;ANALYZE&lt;/code&gt;) on SELECT-only SQL, and record estimated cost.&lt;/li&gt;
&lt;li&gt;Reset the session, apply the work-mem cap alone, and run the same &lt;code&gt;EXPLAIN&lt;/code&gt; so plan shape changes are visible.&lt;/li&gt;
&lt;li&gt;Apply both envelopes together and refuse the candidate if the plan still contains unbounded sorts or missing join filters.&lt;/li&gt;
&lt;li&gt;For DML, stop after the dual envelope; do not auto-apply, and require a human runbook for rollback.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A decision rule that does not pick a mascot
&lt;/h2&gt;

&lt;p&gt;Pick the timeout budget as the outer gate when the dominant failure mode is waiting: lock queues, remote scans, or agent loops that forget a predicate. Pick the work-mem cap as the outer gate when the dominant failure mode is shape: hashes, sorts, and window functions on wide fact tables. If the reviewer cannot classify the query, apply both envelopes and fail closed.&lt;/p&gt;

&lt;p&gt;Concretely, encode three booleans in the review result. &lt;code&gt;clock_ok&lt;/code&gt; means the estimated cost sits under the published timeout heuristic you already use for humans. &lt;code&gt;memory_ok&lt;/code&gt; means no memory node in the plan JSON would obviously exceed &lt;code&gt;work_mem&lt;/code&gt; at the frozen row counts. &lt;code&gt;write_ok&lt;/code&gt; means the SQL is SELECT-only, or the DML is wrapped in an explicit transaction with &lt;code&gt;lock_timeout&lt;/code&gt;. Approve only when all three are true.&lt;/p&gt;

&lt;p&gt;That rule is stricter than either camp alone. It will reject some queries that would have finished. That is the point of a rehearsal-first reviewer. False rejects are cheaper than a replica that spills, provided the agent can rewrite and resubmit inside the same envelope.&lt;/p&gt;

&lt;p&gt;If a team already reviews SQL with a constrained editor, MonkeyCode's free model access can draft envelope wrappers, and its free server option can run the &lt;code&gt;EXPLAIN&lt;/code&gt;-only loop off the warehouse. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Remove that runtime and the decision table still stands; the envelopes are session GUCs and a plan walk, not a vendor feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this approach
&lt;/h2&gt;

&lt;p&gt;This method assumes Postgres-style session GUCs and JSON &lt;code&gt;EXPLAIN&lt;/code&gt;. Engines without per-session memory caps need a different artifact. It also assumes you can freeze statistics; autovacuum drift will make &lt;code&gt;memory_ok&lt;/code&gt; lie. The harness must not run &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; on writes, and it must not point at production connection strings.&lt;/p&gt;

&lt;p&gt;Do not use dual envelopes as a substitute for bind parameters, row-level security, or a migration rehearsal. Do not use them on nested-loop accidental cross joins and then raise timeouts until the query “passes.” Do not publish timeout numbers as SLAs without measuring them on your own hardware. This article does not claim model quality, token quotas, or server capacity beyond the two availability notes above.&lt;/p&gt;

&lt;p&gt;Teams with a dedicated query-gateway team and admission control may already have a better outer gate. Teams that only generate one-off analyst SQL, with a human watching &lt;code&gt;pg_stat_activity&lt;/code&gt;, will find the table heavy. The debate is for review agents that emit SQL faster than humans can watch dashboards.&lt;/p&gt;

&lt;p&gt;The useful close is a checklist, not a slogan. Publish one timeout, one &lt;code&gt;work_mem&lt;/code&gt;, and one refuse-closed rule beside every agent-written query. If you want a disposable place to rehearse that checklist, the free server path is optional; the envelopes remain the engineering work.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>postgres</category>
      <category>ai</category>
      <category>database</category>
    </item>
    <item>
      <title>Parser Gates or Runtime Guards: A Debate for Agent-Written SQL</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Wed, 16 Sep 2026 04:00:00 +0000</pubDate>
      <link>https://dev.to/dataio_4921/parser-gates-or-runtime-guards-a-debate-for-agent-written-sql-299j</link>
      <guid>https://dev.to/dataio_4921/parser-gates-or-runtime-guards-a-debate-for-agent-written-sql-299j</guid>
      <description>&lt;h2&gt;
  
  
  An illustrative lock queue
&lt;/h2&gt;

&lt;p&gt;A generated UPDATE reached staging with a missing key predicate and a wide sequential scan. The statement waited behind an autovacuum worker, then blocked a checkout transaction for thirty-one seconds. Nobody had pasted the SQL into a parser gate, and the runtime role still held UPDATE on the full table. That class of outage is the fork this debate tries to resolve for SQL review agents.&lt;/p&gt;

&lt;p&gt;This article treats the incident as an illustrative reconstruction, not as a measured postmortem from a named company. The technical question is narrow: should agent-written SQL be rejected by static gates, or contained by runtime guards? Both positions are credible, and both fail in documented ways when used as a single control. The useful output is a decision rule, plus a reproducible review workflow you can run without production credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this debate is not a style argument
&lt;/h2&gt;

&lt;p&gt;Agent-written SQL now appears in migrations, analytics extracts, backfills, and operator notebooks after a short prompt. Static review scales with pull requests, while runtime guards scale with live statements and actual query plans. Teams that over-index on parsers ship statements that look bounded and still lock a hot range of rows. Teams that over-index on timeouts discover the damage only after a queue of waiting sessions has already formed.&lt;/p&gt;

&lt;p&gt;Recent public discussion about vibe coding versus engineering often stops at taste, tools, or model quality scores. For SQL, the failure mode is more specific: a statement can be valid, reviewed, and still enter a RowExclusiveLock wait queue. The debate below stays on controls you can test, rather than on whether models outrank human authors in general. If a control cannot be rehearsed with a fixture database, it does not belong in the decision rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A — Fail closed in CI with a parser gate
&lt;/h2&gt;

&lt;p&gt;Advocates want a deterministic check before merge, independent of planner statistics and live cache state. The gate parses the statement, classifies verbs, and rejects unbounded writes, missing predicates, and heavy lock upgrades. Evidence is cheap to collect, because unit tests over SQL strings do not require a populated staging cluster. The limitation is structural, because parsers do not see cardinality, skew, or the join order the planner will pick.&lt;/p&gt;

&lt;p&gt;A parser-first team usually encodes a small allowlist rather than a growing pile of regular expressions. SELECT, EXPLAIN, and EXPLAIN ANALYZE on replicas may pass; UPDATE, DELETE, TRUNCATE, and ALTER fail closed without extra metadata. DDL that takes AccessExclusiveLock fails unless a human-owned runbook identifier is present in the review packet. That design keeps the agent from inventing a migration during a question that was supposed to stay read-only.&lt;/p&gt;

&lt;h3&gt;
  
  
  Numbered workflow for a parser gate
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Parse the candidate SQL with a real engine parser, not a split on semicolons or a language-model guess.&lt;/li&gt;
&lt;li&gt;Reject multi-statement strings unless every statement independently passes the same verb and lock policy.&lt;/li&gt;
&lt;li&gt;Require a bounded predicate for UPDATE and DELETE, and reject empty predicates plus statements that omit WHERE entirely.&lt;/li&gt;
&lt;li&gt;Fail CI if the statement includes LOCK TABLE, DROP, TRUNCATE, or unflagged ALTER without a human-owned exception token.&lt;/li&gt;
&lt;li&gt;Store the parser verdict as an artifact next to the SQL, and forbid the agent from editing that artifact in the same turn.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The evidence for this side is operational simplicity: the check is fast, deterministic, and easy to replay inside pull requests. It also limits blast radius when an agent emits several statements and only the last one was meant for review. False confidence is the cost, because a bounded WHERE clause on an unindexed, high-churn column can still lock a wide heap. Parser gates are necessary for verb control; they are not sufficient for plan control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B — Fail soft in the database with runtime guards
&lt;/h2&gt;

&lt;p&gt;The second position argues that dangerous SQL is usually a plan problem, not a syntax problem, in field incidents. Database guards include statement_timeout, lock_timeout, idle_in_transaction_session_timeout, and a role stripped of exclusive DDL. Evidence comes from wait-event traces, where the failing statement was syntactically bounded and still blocked checkout sessions. The limitation is operational, because a timeout that fires after a lock is held still leaves a waiting queue behind it.&lt;/p&gt;

&lt;p&gt;Runtime advocates point at statistics drift, parameter sniffing, and autovacuum overlap, which no merge-time string check can see. A role that can only write through a security-definer function, with row-level filters, contains mistakes the parser never names. statement_timeout aborts CPU-heavy scans; lock_timeout aborts waiters before they become an outage narrative for on-call. Those settings are real controls, but they are not a review process, and they do not explain intent to a future operator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Numbered workflow for runtime guards
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a dedicated review role that cannot execute TRUNCATE, DROP, ALTER, VACUUM FULL, or LOCK TABLE in production.&lt;/li&gt;
&lt;li&gt;Set lock_timeout and statement_timeout on that role, not as a session afterthought inside agent-generated SQL.&lt;/li&gt;
&lt;li&gt;Force default_transaction_read_only for any agent whose prompt is diagnostic, including EXPLAIN-only review jobs.&lt;/li&gt;
&lt;li&gt;Route writes to a staging replica or a restored snapshot, and refuse agent connections that present production DSNs.&lt;/li&gt;
&lt;li&gt;Capture wait events around the statement, then treat a timeout as a failed review rather than a successful guard firing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This side wins when the SQL is parameterized, the schema is stable, and the risk is a bad plan rather than a bad verb. It loses when the agent can still start a transaction, hold row locks, and wait until the timeout becomes a user-visible stall. Guards also fail open if the agent reconnects with a stronger role, or if a human copies the SQL into psql as a superuser. Runtime containment is a last fence, not a substitute for refusing dangerous verbs before they reach a shared database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence the two sides actually share
&lt;/h2&gt;

&lt;p&gt;Both sides agree that model self-checks are a weak primary control, because the same model can rewrite the checklist. Both sides agree that production credentials do not belong in prompt context, regardless of how the review job is hosted. Both sides agree that EXPLAIN without execution is closer to a parser than to a guard, and should not be sold as a rehearsal. The disagreement is only about which control is allowed to be the merge blocker when the other control is incomplete.&lt;/p&gt;

&lt;p&gt;A practical reading of wait-event catalogs favors runtime data for performance regressions, and parser data for destructive verbs. UPDATE without a key, DELETE without a predicate, and DDL on hot tables belong to the parser side with almost no exceptions. Skewed joins, stale statistics, and lock waits on correctly keyed updates belong to the runtime side, with tracing attached. Mixing those classes into one AI review score hides the only evidence that would tell an on-call engineer what to do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision rule
&lt;/h2&gt;

&lt;p&gt;Use the following rule in order. Stop at the first match, and record the matched line in the review artifact.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the statement contains DDL, TRUNCATE, LOCK TABLE, or a missing write predicate, fail closed in CI; do not wait for a timeout.&lt;/li&gt;
&lt;li&gt;If the statement is a parameterized write with a key predicate, require runtime guards on a non-production snapshot before merge.&lt;/li&gt;
&lt;li&gt;If the statement is read-only EXPLAIN, allow a parser pass plus a captured plan from staging; do not grant write roles.&lt;/li&gt;
&lt;li&gt;If the agent requests to edit the review oracle, the allowlist, or the timeout values, reject the turn and page a human.&lt;/li&gt;
&lt;li&gt;If neither control can be rehearsed, do not ship the statement, even when a model assigns it a high confidence label.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rule is intentionally boring. Boring rules survive model upgrades, prompt drift, and the next fashionable agent loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Artifact: a decision table and a reproducible harness
&lt;/h2&gt;

&lt;p&gt;The table below is the review contract. Copy it into the repository beside the SQL under review.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Statement class&lt;/th&gt;
&lt;th&gt;Parser gate&lt;/th&gt;
&lt;th&gt;Runtime guard&lt;/th&gt;
&lt;th&gt;Merge blocker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SELECT / EXPLAIN&lt;/td&gt;
&lt;td&gt;Allow if single statement&lt;/td&gt;
&lt;td&gt;read-only role + statement_timeout&lt;/td&gt;
&lt;td&gt;Parser only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE/DELETE with key predicate&lt;/td&gt;
&lt;td&gt;Allow with bound WHERE&lt;/td&gt;
&lt;td&gt;lock_timeout + staging snapshot&lt;/td&gt;
&lt;td&gt;Runtime rehearsal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE/DELETE without key&lt;/td&gt;
&lt;td&gt;Reject&lt;/td&gt;
&lt;td&gt;Do not run&lt;/td&gt;
&lt;td&gt;Parser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DDL / TRUNCATE / LOCK&lt;/td&gt;
&lt;td&gt;Reject unless human flag&lt;/td&gt;
&lt;td&gt;Role without DDL&lt;/td&gt;
&lt;td&gt;Parser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-statement bundle&lt;/td&gt;
&lt;td&gt;Reject unless each part passes&lt;/td&gt;
&lt;td&gt;No implicit transaction from the agent&lt;/td&gt;
&lt;td&gt;Parser&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The harness below is a labeled, unexecuted example for PostgreSQL. Adapt names to your environment before running anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# proposal: ci_sql_parser_gate.py
# Unexecuted example. Requires pglast. Not a production security boundary.
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pglast&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parse_sql&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pglast.ast&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;UpdateStmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DeleteStmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TruncateStmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AlterTableStmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LockStmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TransactionStmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;FORBIDDEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TruncateStmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AlterTableStmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LockStmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;WRITE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateStmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeleteStmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;statements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;parse_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;has_where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;whereClause&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;stmts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;statements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL: multi-statement SQL is out of policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stmts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TransactionStmt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL: agent may not open or close transactions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FORBIDDEN&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL: destructive or locking DDL requires a human flag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WRITE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;has_where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL: write without a predicate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PASS: parser gate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE orders SET status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;closed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE orders SET status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;closed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; WHERE id = 42;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EXPLAIN SELECT id FROM orders WHERE id = 42;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TRUNCATE orders; SELECT 1;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- proposal: staging_review_role.sql&lt;/span&gt;
&lt;span class="c1"&gt;-- Unexecuted example. Apply only on a disposable staging snapshot.&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;sql_review_agent&lt;/span&gt; &lt;span class="n"&gt;NOINHERIT&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;sql_review_agent&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;sql_review_agent&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;sql_review_agent&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;idle_in_transaction_session_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'3s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;sql_review_agent&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;default_transaction_read_only&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;staging_app&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;sql_review_agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;sql_review_agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;sql_review_agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;REVOKE&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sql_review_agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# proposal: rehearsal.sh&lt;/span&gt;
&lt;span class="c"&gt;# Unexecuted example. Points at staging, never at production DSNs.&lt;/span&gt;

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PGHOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;staging-snapshot.internal
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PGUSER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sql_review_agent
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PGDATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;staging_app
psql &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SHOW lock_timeout;"&lt;/span&gt;
psql &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"EXPLAIN (FORMAT JSON) SELECT id FROM orders WHERE id = 42;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the parser tests in CI on every agent-authored SQL file. Run the role rehearsal only against a snapshot that can be thrown away. Keep production DSNs out of the job environment so a prompt leak cannot become a connection string. Treat a timeout during rehearsal as a failed review, not as proof that the guardrail is working as designed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a free review runtime fits
&lt;/h2&gt;

&lt;p&gt;Some teams still want a model to classify ambiguous SQL against a frozen rubric, especially when the parser output is a syntax tree rather than a policy decision. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option can host that classification step so the rubric and the SQL text stay off production hosts.&lt;/p&gt;

&lt;p&gt;The model does not replace the parser gate, and it does not tune lock_timeout for you. Feed it the parser verdict, the statement class from the table, and a rubric that lives in git, then store the model's label as advisory metadata. If the model disagrees with a FAIL from the parser, the parser still wins. That is the entire integration: one advisory pass, no extra privileges, and no production row samples in the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;Parser coverage depends on the SQL dialect and on whether the agent emits vendor extensions the parser does not understand. pglast does not make MySQL or SQL Server statements safe, and a failed parse must fail closed rather than skip the gate. Runtime timeouts do not roll back work that already happened before the wait, and they do not repair bad data written inside the limit. Neither control detects semantic errors such as updating the wrong tenant key that still looks like a well-bounded predicate.&lt;/p&gt;

&lt;p&gt;Free review runtimes are not an isolation story by themselves, and they should not receive production dumps, secrets, or customer row samples. Do not treat model classification as a measured accuracy benchmark; this article does not claim a quota, a hardware profile, or a latency number. Do not let the agent modify timeout settings, role grants, or the decision table in the same change that contains the SQL. If staging statistics are not a plausible shadow of production, the runtime rehearsal will certify the wrong plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should not use this approach
&lt;/h2&gt;

&lt;p&gt;Do not use parser-only gates if your agents routinely emit dynamic SQL through stored procedures the parser never sees. Do not use runtime-only guards if agents can connect with a role that still owns DDL, or if staging is a shared writable database. Do not send regulated data to any hosted review path, including a free server, when the legal control is that no data leaves the VPC. Do not adopt the workflow as a substitute for a human runbook on irreversible migrations, even when both gates are green.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Parser gates and runtime guards answer different questions, and collapsing them into a single agent score recreates the lock queue. Put verb safety in CI, put plan safety on a disposable snapshot, and keep the review oracle outside the agent's write path. If you already have a free model runtime for advisory classification, copy the decision table first and leave the model in a comment-only role.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>postgres</category>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Agent-Written Down Migrations or Human Recovery Runbooks: A Rehearsal-First Debate</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Mon, 14 Sep 2026 21:40:59 +0000</pubDate>
      <link>https://dev.to/dataio_4921/agent-written-down-migrations-or-human-recovery-runbooks-a-rehearsal-first-debate-4jcf</link>
      <guid>https://dev.to/dataio_4921/agent-written-down-migrations-or-human-recovery-runbooks-a-rehearsal-first-debate-4jcf</guid>
      <description>&lt;p&gt;Consider a composite of the failure reports that keep circulating in Postgres incident threads, rather than a logged incident from my own systems. A migration adds a column and a backfill, an AI reviewer approves the up-path, and nobody authors the down-path at all. Three hours later the backfill is still running, the lock queue is growing, and the only rollback plan left is last night's base backup. The interesting failure is not that the model was wrong; it is that nobody defined who owned the reversal before the migration started.&lt;/p&gt;

&lt;p&gt;That gap is what this post argues about, in the same debate format this account has used before. Two defensible positions exist, each with real supporting evidence, and the resolution is not "be careful" but a decision rule you can encode in a pull request checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: The agent should author both directions
&lt;/h2&gt;

&lt;p&gt;The first position says a review agent is not finished until it has produced a down migration alongside the up migration. Its strongest argument is behavioral rather than technical: pairing forces the reversibility question to be asked at authoring time, when the change is still cheap to reshape. A migration that cannot be expressed as a clean inverse is usually a migration that should be split, staged, or moved out of band. Coverage also improves for mechanical cases, because additive DDL has a genuinely trivial inverse that nobody should be writing by hand in 2026.&lt;/p&gt;

&lt;p&gt;There is supporting evidence for this position in ordinary diff review. When a reviewer sees &lt;code&gt;ALTER TABLE orders ADD COLUMN settled_at timestamptz&lt;/code&gt; with no counterpart, the asymmetry is visible and cheap to flag. When the down-path is instead a paragraph in a runbook that lives somewhere else, the asymmetry disappears from the diff entirely. Model-assisted review is good at exactly this kind of local symmetry check, which is why the "author both directions" camp tends to win arguments about small, additive changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: Humans own rollback; agents may only draft
&lt;/h2&gt;

&lt;p&gt;The second position accepts the symmetry argument but rejects its conclusion for destructive changes. Its central claim is that a down migration which drops a column is not a rollback; it is data loss with extra ceremony. Once values have been overwritten by a backfill, no DDL statement can reconstruct them, and a &lt;code&gt;down.sql&lt;/code&gt; file that pretends otherwise creates false confidence in exactly the moment confidence is most expensive. The real recovery artifact for a destructive change is a restore path plus a runbook, and neither is something a code generator should own.&lt;/p&gt;

&lt;p&gt;This position also has an organizational argument that is hard to dismiss. Review capacity, not authoring capacity, is the scarce resource on most teams. Generating down migrations for every change produces a large body of reviewable output whose value is concentrated in the small subset of changes that are genuinely reversible. A team that reviews twenty generated inverses to catch one dangerous one has spent attention it could have spent on the up-path's lock profile instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each position actually breaks
&lt;/h2&gt;

&lt;p&gt;Position A breaks on data-destroying changes, and it breaks silently. The schema after the down migration looks identical to the schema before, so every automated check you are likely to have will pass while the values are gone. This is the single most important failure mode in the whole debate, and it is the reason schema equivalence alone cannot be the gate.&lt;/p&gt;

&lt;p&gt;Position B breaks on velocity and on forgotten coverage. If rollback planning is a separate human deliverable, it competes with feature work and loses, which is how teams end up with a migration history full of up-paths and no rehearsed reversal for anything. It also breaks on long-lived branches, where the person who wrote the migration is no longer the person merging it. A rule that depends on human memory of intent degrades as the branch ages.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision rule
&lt;/h2&gt;

&lt;p&gt;Author the down migration with an agent only when DDL alone can restore the previous state; anything that destroys values needs a human-authored recovery runbook. That single sentence resolves most of the disagreement, and the table below turns it into per-change-class gates you can paste into a review checklist.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change class&lt;/th&gt;
&lt;th&gt;Rehearsal signal&lt;/th&gt;
&lt;th&gt;Down-path author&lt;/th&gt;
&lt;th&gt;Merge gate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Additive, nullable, no backfill&lt;/td&gt;
&lt;td&gt;down restores an identical schema hash&lt;/td&gt;
&lt;td&gt;agent may author&lt;/td&gt;
&lt;td&gt;CI rehearsal passes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backfill or bulk UPDATE&lt;/td&gt;
&lt;td&gt;write volume exceeds your maintenance budget&lt;/td&gt;
&lt;td&gt;agent drafts, human rewrites as a batched job&lt;/td&gt;
&lt;td&gt;explicit row cap plus an off-peak window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index creation&lt;/td&gt;
&lt;td&gt;concurrent build lock profile observed&lt;/td&gt;
&lt;td&gt;agent may author&lt;/td&gt;
&lt;td&gt;deployed as a separate step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Column drop or type change&lt;/td&gt;
&lt;td&gt;down cannot restore original values&lt;/td&gt;
&lt;td&gt;human-authored restore runbook&lt;/td&gt;
&lt;td&gt;no agent-authored down migration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Constraint or foreign key addition&lt;/td&gt;
&lt;td&gt;lock wait exceeds the rehearsal timeout&lt;/td&gt;
&lt;td&gt;agent drafts, human approves&lt;/td&gt;
&lt;td&gt;staged validation, not one transaction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule is deliberately conservative about data and deliberately permissive about structure. Structural changes are where generation pays off, because the inverse is mechanical and the failure is visible in a diff. Data changes are where generation is dangerous, because the inverse is impossible and the failure is invisible until someone queries a column that used to have values.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rehearsal harness you can run in CI
&lt;/h2&gt;

&lt;p&gt;Everything above is cheap to test if you keep one disposable database around. The script below rehearses an up/down pair and fails when the down-path is not schema-equivalent, or when the up-path changed nothing at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# rehearsal.sh — run an up/down pair against a disposable database.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;DB_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REHEARSAL_DB_URL&lt;/span&gt;:?set&lt;span class="p"&gt; REHEARSAL_DB_URL to a throwaway database&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;UP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?usage:&lt;span class="p"&gt; rehearsal.sh up.sql down.sql&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;DOWN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;:?usage:&lt;span class="p"&gt; rehearsal.sh up.sql down.sql&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Fail fast instead of queueing behind a lock you cannot see.&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PGOPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-c lock_timeout=3s -c statement_timeout=15s"&lt;/span&gt;

&lt;span class="c"&gt;# sha256sum on Linux, shasum -a 256 on macOS.&lt;/span&gt;
&lt;span class="nv"&gt;HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sha256sum"&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"shasum -a 256"&lt;/span&gt;

schema_hash&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  pg_dump &lt;span class="nt"&gt;--schema-only&lt;/span&gt; &lt;span class="nt"&gt;--no-owner&lt;/span&gt; &lt;span class="nt"&gt;--no-privileges&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'/^--/d'&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'/^$/d'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | &lt;span class="nv"&gt;$HASH&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;before&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;schema_hash&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$UP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="nv"&gt;applied&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;schema_hash&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOWN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="nv"&gt;after&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;schema_hash&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'before  %s\napplied %s\nafter   %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$before&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$applied&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$after&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$before&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$applied&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'FAIL: up migration changed nothing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;3&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$before&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$after&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;   &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'FAIL: down migration is not schema-equivalent'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'PASS: up applied, down restored the prior schema'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before you let a backfill near production, estimate the write volume without executing it. Note the absence of &lt;code&gt;ANALYZE&lt;/code&gt; in the block below: &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; on an &lt;code&gt;UPDATE&lt;/code&gt; executes the &lt;code&gt;UPDATE&lt;/code&gt;, which is a mistake worth naming explicitly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Estimate rows and plan shape for a proposed backfill. Read-only.&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'3s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;reltuples&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;approx_rows&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public.orders'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;settled_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;settled_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ROLLBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Numbered workflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Provision a throwaway database and never point the rehearsal at anything you intend to keep.&lt;/li&gt;
&lt;li&gt;Capture the before-state schema hash with the &lt;code&gt;schema_hash&lt;/code&gt; function above.&lt;/li&gt;
&lt;li&gt;Apply the up migration under explicit &lt;code&gt;lock_timeout&lt;/code&gt; and &lt;code&gt;statement_timeout&lt;/code&gt; values, not defaults.&lt;/li&gt;
&lt;li&gt;Apply the down migration and fail the build when the hash does not match the before-state.&lt;/li&gt;
&lt;li&gt;Classify the change against the decision table and record the matching row in the pull request.&lt;/li&gt;
&lt;li&gt;For destructive classes, replace the down migration with a restore runbook and name its owner in the pull request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you also want a second opinion on the up-path, the two-reviewer pattern below is a reasonable prompt sketch rather than a validated system. It is pseudocode: I have not benchmarked it, and two passes from the same model family can share the same blind spot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Reviewer 1 — author pass (pseudocode)
Input:  up.sql, table sizes, current indexes
Output: risk list, requested rewrite, proposed down migration
Rule:   if the change destroys values, refuse to author a down migration

# Reviewer 2 — adversarial pass (pseudocode)
Input:  the same up.sql, plus Reviewer 1's output
Output: counterexamples, lock-order hazards, missing rollback data
Rule:   assume the down migration will be executed during peak traffic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this runs, and one availability note
&lt;/h2&gt;

&lt;p&gt;The rehearsal database must be disposable by design, which makes a throwaway server the natural host for it. MonkeyCode's operator states that the free tier includes ten million tokens of model access and a free server option, which is enough to run both reviewer passes and a short-lived rehearsal database without a procurement conversation. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Those availability claims come from the operator and I have not independently verified resource ceilings or long-term uptime, so verify the current limits on their site before you depend on them; use the free server for short, disposable rehearsals and never for anything you intend to keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this
&lt;/h2&gt;

&lt;p&gt;Schema-hash equality proves nothing about data, and a rehearsal database has none of production's row distribution or contention. A three-second lock timeout that passes on an idle server may be wildly optimistic on a table receiving continuous writes. The harness above is Postgres-specific and depends on &lt;code&gt;pg_dump&lt;/code&gt; access, so managed platforms that restrict it need a substitute snapshot mechanism. Finally, if your framework already owns reversible migrations and you trust its inverses, this workflow mostly duplicates work you have already paid for.&lt;/p&gt;

&lt;p&gt;Teams that cannot spin up a scratch database, or that operate under rules preventing schema artifacts from leaving their environment, should not adopt the CI half of this pattern; the decision table alone still applies to them. Everyone else gets the useful part: a rule that lets an agent write the parts of rollback that are genuinely mechanical, and keeps humans accountable for the parts that are not. If you want to try the rehearsal loop without provisioning anything first, the operator-provided free model access and free server are a reasonable starting point — just confirm the current limits before you build a habit on them.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>ai</category>
      <category>devops</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Single-Pass Critique or Repair Loop: A Debate for SQL Review Agents</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Sun, 13 Sep 2026 20:04:59 +0000</pubDate>
      <link>https://dev.to/dataio_4921/single-pass-critique-or-repair-loop-a-debate-for-sql-review-agents-4d0c</link>
      <guid>https://dev.to/dataio_4921/single-pass-critique-or-repair-loop-a-debate-for-sql-review-agents-4d0c</guid>
      <description>&lt;p&gt;A checkout migration sat in review for eleven minutes while a SQL agent rewrote the same UPDATE four times. Each pass looked cleaner in the comment thread, yet the agent never measured lock scope against a realistic row estimate. The final suggestion dropped a WHERE clause that still compiled, and a parser would have flagged the missing predicate immediately. This opening is a labeled composite of review-queue failures, not a first-person production claim.&lt;/p&gt;

&lt;p&gt;SQL review agents fail in a specific way that generic coding agents often hide behind fluent comments. They can emit valid SQL that still expands lock scope, invalidates indexes, or rewrites predicates during a supposedly helpful repair. Multi-turn loops amplify that risk because each iteration treats the last model output as a trusted new source of truth. The practical question is whether the agent should speak once, or keep repairing until a budget expires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is not a routing or catalog debate
&lt;/h2&gt;

&lt;p&gt;Isolation answers where a candidate statement may execute, and catalog policy answers which schema objects the model may observe. Auto-rewrite gates answer whether the agent may patch a file at all, which is a different permission than iteration count. Those questions remain open on this account, and they should not collapse into one policy knob. This article isolates a narrower control: how many times a SQL review agent may rewrite after the first finding.&lt;/p&gt;

&lt;p&gt;Recent public discussion around agent loops is noisy, and most of it is not grounded in statement-class risk. A SQL agent is not a general coding loop with a compiler sitting at the end of the turn. The “compiler” for SQL is a combination of parser, planner, permissions, and data-dependent locks, and those signals do not arrive together. A repair loop that only watches message text will optimize for comment aesthetics instead of lock and cardinality behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: single-pass critique behind a parser gate
&lt;/h2&gt;

&lt;p&gt;The first position treats the model as a reviewer, not as an author with an unbounded edit loop. A deterministic parser extracts statement type, tables, predicates, and join keys before any model token is spent. If the parse fails, or a policy rule fires, the agent posts a finding and stops without proposing a patched file. The model may explain residual risk, but it does not receive its own previous SQL as the next user message.&lt;/p&gt;

&lt;p&gt;This position is strongest when the failure mode is structural rather than planner-specific. Missing WHERE clauses, unqualified DELETE, cross-schema DROP, and INSERT that omits a NOT NULL column are parser-visible. Sending those defects into a repair loop often produces a fluent rewrite that still fails the same static rule. Single-pass critique also keeps the review comment stable for humans, because the thread does not accumulate four competing variants of one migration.&lt;/p&gt;

&lt;p&gt;A labeled example gate looks like the following Python. Treat it as a review-time sketch, not as a measured benchmark from production traffic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example only: parser gate for a SQL review agent. Not executed against live traffic.
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlglot&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlglot&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;

&lt;span class="n"&gt;FORBIDDEN_ROOTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Truncate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Alter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GateResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;allow_model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;halt_loop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parser_gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GateResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;trees&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlglot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;sqlglot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;GateResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allow_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;halt_loop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parse_error:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;trees&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FORBIDDEN_ROOTS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;forbidden_root:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;where&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dml_without_where:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Limit&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unbounded_join_select&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;halt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;GateResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allow_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;halt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;halt_loop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;halt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under Position A, &lt;code&gt;halt_loop=True&lt;/code&gt; is terminal. The agent may still write a diagnostic comment that quotes &lt;code&gt;reasons&lt;/code&gt;, but it must not call the model again with a “please fix this SQL” instruction. That rule sounds severe until you inspect what repair loops optimize for in practice. They optimize for making the linter quiet, which is not the same as making the statement safe under concurrency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: a bounded repair loop on an isolated server
&lt;/h2&gt;

&lt;p&gt;The second position accepts that first drafts miss bind-parameter types, partial indexes, and trigger side effects. A small loop may rewrite the statement, run it in a sandbox, and feed errors back into the next prompt. That loop only stays honest if execution happens off the production endpoint and the iteration budget is explicit. Without a hard stop, the agent will spend the entire review window arguing with itself about alias names.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/em&gt; Teams evaluating MonkeyCode can use its free model access and free server option to keep those repair attempts off production capacity. The claim here is availability of free model access and a free server option, not a quota, hardware profile, model name, or permanence guarantee. If your change set includes customer data, the sandbox still needs a scrubbed schema clone rather than a logical replica of production rows.&lt;/p&gt;

&lt;p&gt;Position B is strongest when the defect is runtime-shaped and parser-invisible. Search path mistakes, missing grants, IMMUTABLE function misuse, and trigger recursion often survive sqlglot and still fail when a statement hits a real engine. A single isolated execution can return a definite error string, which is higher-quality evidence than another paragraph of model speculation. The loop should consume that error, not a request to “make it more elegant.”&lt;/p&gt;

&lt;p&gt;A labeled loop budget can be this small. The important part is the halt conditions, not the client library you happen to wrap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example only: bounded repair loop. Do not point this at production.
&lt;/span&gt;&lt;span class="n"&gt;MAX_REPAIR_TURNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;  &lt;span class="c1"&gt;# initial critique + at most one error-driven repair
&lt;/span&gt;&lt;span class="n"&gt;STYLE_FEEDBACK_FORBIDDEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoopHalt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;repair_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_ddl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sandbox_exec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_complete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;gate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parser_gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;halt_loop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;LoopHalt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parser_gate:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;
    &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;turn&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MAX_REPAIR_TURNS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;turn&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;critique_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_ddl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;LoopHalt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_runtime_error_for_repair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;repair_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_ddl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;proposal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;model_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;gate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parser_gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proposal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;halt_loop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;LoopHalt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repair_failed_parser:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sandbox_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proposal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;proposal&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;turn&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;
        &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;LoopHalt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repair_budget_exhausted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the loop does not repair on style comments, unused aliases, or formatting nits. If the sandbox returns success, Position B still should not rewrite for taste, because taste rewrites are how WHERE clauses disappear. The sandbox is evidence for errors, not a license to keep iterating until the model gets bored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence that actually changes the decision
&lt;/h2&gt;

&lt;p&gt;Parser-first evidence is cheap and repeatable, which matters more than rhetorical confidence from a model. If &lt;code&gt;sqlglot.parse&lt;/code&gt; fails, or a DML node lacks &lt;code&gt;where&lt;/code&gt;, you already have a ship-blocking finding with a stable identifier. Repairing that class of defect with a model is optional work, and it is often worse work, because the model can satisfy the parser by adding &lt;code&gt;WHERE true&lt;/code&gt; or a tautology. Single-pass critique should treat tautology predicates as a halt condition as well.&lt;/p&gt;

&lt;p&gt;Sandbox evidence is slower and more complete for engine-true failures. A free isolated server can answer questions the parser cannot, including missing relations, wrong types in RETURNING lists, and volatile functions in index expressions. That evidence is only trustworthy when the schema clone matches the branch under review, including migrations that have not reached production. A stale clone will reject valid statements or, worse, accept invalid ones that depend on columns added later.&lt;/p&gt;

&lt;p&gt;Loop evidence is mostly negative, and that is still useful. Each extra turn re-sends schema text, prior SQL, and the latest error, which crowds out the original reviewer intent. Agents then “fix” timeouts by deleting joins, and they “fix” permission errors by switching tables. If you cannot write a halt condition that names those regressions, you do not have a repair loop. You have an unsupervised author with a retry button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Artifact: decision table, tests, and a command path
&lt;/h2&gt;

&lt;p&gt;The original artifact is a decision table plus a test plan you can run without production credentials. Rows are statement classes, not vibes about model quality. Apply the table before the first model call, then again after any proposed repair.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Statement class&lt;/th&gt;
&lt;th&gt;Parser gate&lt;/th&gt;
&lt;th&gt;Repair loop allowed&lt;/th&gt;
&lt;th&gt;Required evidence to continue&lt;/th&gt;
&lt;th&gt;Halt immediately when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE/DELETE without WHERE&lt;/td&gt;
&lt;td&gt;Fail closed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Missing predicate, tautology predicate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE/DELETE with WHERE&lt;/td&gt;
&lt;td&gt;Fail open to critique&lt;/td&gt;
&lt;td&gt;One turn, error only&lt;/td&gt;
&lt;td&gt;Sandbox error string&lt;/td&gt;
&lt;td&gt;Predicate removed or weakened&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SELECT with JOIN, no LIMIT&lt;/td&gt;
&lt;td&gt;Warn&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Static join list&lt;/td&gt;
&lt;td&gt;Model adds DISTINCT as a substitute for LIMIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DDL (DROP/ALTER/TRUNCATE)&lt;/td&gt;
&lt;td&gt;Fail closed for apply&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Human approval&lt;/td&gt;
&lt;td&gt;Any auto-applied rewrite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INSERT missing NOT NULL column&lt;/td&gt;
&lt;td&gt;Fail closed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Parser or information_schema&lt;/td&gt;
&lt;td&gt;Model supplies dummy literals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Utility SQL (VACUUM, GRANT)&lt;/td&gt;
&lt;td&gt;Fail closed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Named operator runbook&lt;/td&gt;
&lt;td&gt;Agent invents a substitute command&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Labeled tests below encode the table. They are intended for a local pytest run against fixtures, not as reported production metrics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example tests for the debate rule. Fixtures only.
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_update_without_where_never_enters_loop&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE orders SET status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;paid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;gate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parser_gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;halt_loop&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dml_without_where&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_tautology_where_is_not_a_successful_repair&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE orders SET status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;paid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; WHERE id = %s;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;repaired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE orders SET status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;paid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; WHERE 1 = 1;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;parser_gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;halt_loop&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="c1"&gt;# Extension point: compare predicates, do not accept tautologies.
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1 = 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;repaired&lt;/span&gt;  &lt;span class="c1"&gt;# document the failure mode under review
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_repair_budget_is_two_turns_maximum&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;MAX_REPAIR_TURNS&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A command path keeps the same rule visible outside Python. The following is a local dry-run sketch against a disposable database, not a production runbook.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example only: parse, then optionally exec on a disposable database.&lt;/span&gt;
python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'from gate import parser_gate; print(parser_gate(open("stmt.sql").read()))'&lt;/span&gt;
&lt;span class="c"&gt;# If the gate fails, stop. Do not call a model.&lt;/span&gt;
&lt;span class="c"&gt;# If the gate passes and statement class allows one repair, exec in a clone:&lt;/span&gt;
psql &lt;span class="s2"&gt;"postgres://review:review@sandbox-host:5432/review_clone"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"BEGIN; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;stmt.sql&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;; ROLLBACK;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rollback wrapping is part of the evidence story, not an optional flourish. Repair loops that auto-commit on a shared clone will contaminate the next pull request’s findings. If you cannot wrap the candidate in &lt;code&gt;BEGIN&lt;/code&gt; and &lt;code&gt;ROLLBACK&lt;/code&gt;, Position B is unavailable for that statement class, and you should fall back to Position A.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete workflow in numbered steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Classify the statement with a parser before any model call, and store the statement class next to the review comment identifier. Classification must be deterministic so later turns cannot relabel a DROP as a SELECT after a rewrite. If classification fails, post the parse error and halt without asking the model to guess intent from broken SQL. This step is mandatory for both positions, because a loop without a class cannot apply the decision table.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Apply the decision table row for that class, including the halt conditions that mention weakened predicates and dummy literals. Do not skip this row because a model previously “did well” on a neighboring file in the same pull request. Neighboring files do not share lock scope, and they do not share trigger bodies. Record the chosen position in the comment so humans can see whether they are reading a critique or a repaired proposal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If Position A applies, call the model at most once with schema that is frozen for the branch, then post findings without a patch file. The prompt should ask for residual risk, not for a rewritten statement, which removes the incentive to invent WHERE clauses. If the model still returns SQL, discard the SQL and keep only the diagnostic sentences that cite objects the parser already extracted. Single-pass critique fails closed when the model cannot point at a parsed object.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If Position B applies, run the current SQL in an isolated clone, and allow a repair turn only when the engine returns an error string. Feed that error back without style instructions, then parse the proposal and diff it against the original predicates and target tables. Halt if tables change, if predicates weaken, or if the repair budget is already spent. Success in the sandbox is not permission to iterate again for naming or formatting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Close the review with one artifact humans can replay: parser reasons, sandbox error or &lt;code&gt;None&lt;/code&gt;, turn count, and the halt rule that fired. Replayability matters more than confidence language, because the next reviewer needs to know why the agent stopped. If you cannot replay the halt, you cannot defend the comment in an incident review. That close-out is the difference between a SQL review agent and a chat window attached to git.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Limitations, and who should not use this approach
&lt;/h2&gt;

&lt;p&gt;This approach does not replace query-plan review, because neither sqlglot nor a rolled-back execute is a substitute for cardinality on production-like statistics. It also does not certify that a free server clone matches production extensions, collations, or autovacuum settings. If your risk sits in planner choices rather than parse structure or hard engine errors, you need captured plans or a statistics fixture, which is a different debate. Mixing those signals into this loop will hide the halt conditions that make the loop defensible.&lt;/p&gt;

&lt;p&gt;Do not use Position B on a shared free server when the SQL or the clone would carry customer data, secrets, or uncleansed dumps. Do not use a repair loop for non-idempotent DDL, because a failed ALTER in a clone is not a safe teaching signal for a model. Do not use either position as an apply mechanism; the artifact is a review comment and a halt reason, not a merge. Teams without a parser in the path should not enable loops at all, because they cannot detect a WHERE clause vanishing between turns.&lt;/p&gt;

&lt;p&gt;Free model access and a free server option also do not imply unbounded capacity, reserved hardware, or a stable model identity across days. Treat both as an evaluation path for isolated review work, then re-check current product terms before you automate a queue. If terms are unclear, keep the parser gate and disable repair until the execution target is named. Unnamed execution targets are how review agents wander into the primary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision rule
&lt;/h2&gt;

&lt;p&gt;Use Position A, single-pass critique, when the parser can name a structural defect or the statement class is DDL, GRANT, or unbounded DML. Use Position B, one error-driven repair turn, only when the parser is clean, the statement is DML or SELECT, a scrubbed clone exists, and the sandbox returns an engine error. Never repair on style, never repair after a successful sandbox run, and never allow a repair that changes tables or weakens predicates. If those halt checks cannot be coded as tests, the agent does not get a loop.&lt;/p&gt;

&lt;p&gt;That rule is intentionally conservative relative to general coding agents that retry until tests go green. SQL “tests” that only check executability will go green after deleting the constraint that made the statement interesting. The review agent’s job is to keep that constraint visible, even when a model is available at no listed cost. Cheap tokens do not change the cost of a lock on the orders table.&lt;/p&gt;

&lt;p&gt;If you already have a disposable clone and want to exercise the bounded loop on synthetic migrations, MonkeyCode’s free model access and free server option are a reasonable evaluation path. Keep the parser gate in front, keep the decision table in source control, and keep production endpoints out of the client configuration.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>postgres</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Fixture EXPLAIN or Captured Plans: A Debate for SQL Review Agents</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Sat, 12 Sep 2026 18:14:29 +0000</pubDate>
      <link>https://dev.to/dataio_4921/fixture-explain-or-captured-plans-a-debate-for-sql-review-agents-257e</link>
      <guid>https://dev.to/dataio_4921/fixture-explain-or-captured-plans-a-debate-for-sql-review-agents-257e</guid>
      <description>&lt;p&gt;A review agent cleared a reporting query because the fixture database returned a nested-loop plan in under twenty milliseconds. The same query shape hit production later that day and chose a sequential scan across a skewed events table. The difference was not the SQL text; it was the evidence the agent was allowed to trust. Fixture EXPLAIN output and captured plans answer different questions, and mixing them quietly creates false confidence.&lt;/p&gt;

&lt;p&gt;This article treats that conflict as a structured debate rather than a quiet tooling preference. One position says a SQL review agent should always obtain a fresh EXPLAIN from a disposable fixture. The other position says the agent should only read captured plans from the runtime that will execute the statement. A later decision rule chooses between them using evidence quality, not using model branding or demo latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure this debate is about
&lt;/h2&gt;

&lt;p&gt;Consider a pull request that adds a dashboard query joining events to accounts on account_id. The fixture loader inserts a few thousand uniform rows, so PostgreSQL prefers an index nested loop. The agent then reports that no sequential scan is present and the statement looks safe to merge. Production histograms are skewed, the same predicate matches many recent events, and the planner picks another join order.&lt;/p&gt;

&lt;p&gt;Captured plans fail in the opposite direction when the sample does not represent daytime bind values. A plan taken during a quiet night, or against a replica with untouched default statistics, can bless a fragile query. The agent then argues with developers using evidence that is already stale relative to autovacuum. Both evidence sources remain legitimate in review, but they are not interchangeable artifacts for blocking merges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: Fresh fixture EXPLAIN as the primary artifact
&lt;/h2&gt;

&lt;p&gt;Advocates of fixture EXPLAIN want a review that stays reproducible on every pull request without cluster access. They load committed schema, constraints, and a published row-count recipe, then run EXPLAIN without ANALYZE. Writers never execute, and the agent receives JSON plans that CI can diff when a finding is disputed. The operational claim is simple: if two reviewers cannot replay the plan, the finding is only an anecdote.&lt;/p&gt;

&lt;p&gt;This position is strongest when the question is structural rather than statistical inside the planner output. Missing indexes, implicit casts that block index use, and SELECT * on wide JSON columns appear on tiny fixtures. The position becomes weak when cardinality, correlation, or partial-index predicates dominate estimated cost. A fixture that lies about data shape will make the agent sound certain while remaining wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: Captured plans from the target runtime only
&lt;/h2&gt;

&lt;p&gt;Advocates of captured plans treat the planner as an environment-specific compiler that reads local statistics. They record EXPLAIN FORMAT JSON on a staging replica, a canary, or a CI restore of last night's statistics. The agent is forbidden from inventing a plan that the target cluster would not actually choose. The empirical claim is to review the plan you will pay for, not the plan a laptop invented.&lt;/p&gt;

&lt;p&gt;This position is strongest for high-volume reads where n_distinct and correlation decide join order. It is weakest when capture is incomplete, autovacuum has not run, or bind values differ from production. A single captured plan is a sample of one execution shape, not a distribution over the workload. Review policies that block on one hash without recording bind types will create noisy false positives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence each side can actually collect
&lt;/h2&gt;

&lt;p&gt;The debate stops being philosophical once both sides write down the files they can actually produce. Fixture work produces schema files, seed scripts, and EXPLAIN JSON committed next to the query text. Captured-plan work produces a fingerprint, bind parameter types, stats_reset timestamps, and the PostgreSQL major version. If a side cannot produce those files during review, it should not win a blocking argument.&lt;/p&gt;

&lt;p&gt;The following fingerprint helper is a labeled, unexecuted sketch for CI, not a latency benchmark. It keeps node types and relation names, and it drops estimated costs so fixture size cannot dominate the hash.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Proposal: fingerprint EXPLAIN JSON without executing DML.
# Unexecuted example; run only against fixtures or read-only replicas.
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;KEEP_KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Node Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Relation Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Index Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Join Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Strategy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;KEEP_KEYS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Plans&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]:&lt;/span&gt;
        &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;explain_json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;explain_json&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Plan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A matching capture statement should stay read-only, and it should never add ANALYZE for writers. Point the same shape at a fixture DSN and, when policy allows, at a staging replica that already has representative statistics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Labeled example: costs only, no execution.&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;VERBOSE&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Labeled example: capture both artifacts as files the agent may read.&lt;/span&gt;
psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FIXTURE_DSN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-Atc&lt;/span&gt; &lt;span class="s2"&gt;"EXPLAIN (FORMAT JSON, ANALYZE FALSE) &lt;/span&gt;&lt;span class="nv"&gt;$SQL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fixture_plan.json
psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STAGING_DSN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-Atc&lt;/span&gt; &lt;span class="s2"&gt;"EXPLAIN (FORMAT JSON, ANALYZE FALSE) &lt;/span&gt;&lt;span class="nv"&gt;$SQL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; captured_plan.json
psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STAGING_DSN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-Atc&lt;/span&gt; &lt;span class="s2"&gt;"SELECT stats_reset FROM pg_stat_database WHERE datname = current_database();"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; stats_reset.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A practical workflow in numbered steps
&lt;/h2&gt;

&lt;p&gt;The numbered workflow below keeps the debate testable inside ordinary pull requests on a data team. It uses a PostgreSQL instance that you already operate for fixtures or for restored statistics. When CI needs a model to label structural versus statistical findings, a scratch runtime is enough.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open-source project with free model access and a free server option for that classifier. Those two availability claims host the agent; they do not create planner statistics the cluster never collected.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Freeze the statement text, bind types, and search_path in the pull request, because planner output is otherwise meaningless.&lt;/li&gt;
&lt;li&gt;Build a fixture database from committed DDL and a documented seed recipe, then capture EXPLAIN JSON without ANALYZE.&lt;/li&gt;
&lt;li&gt;If policy allows, capture EXPLAIN JSON from staging or from a statistics restore that matches the target major version.&lt;/li&gt;
&lt;li&gt;Fingerprint both plans with the helper above, and store the hashes plus the raw JSON as review artifacts.&lt;/li&gt;
&lt;li&gt;Send the model a bounded packet: statement, schema excerpt, both fingerprints, and a short list of disallowed node types.&lt;/li&gt;
&lt;li&gt;Require the agent to label every finding as structural or statistical, and reject unlabeled findings before merge.&lt;/li&gt;
&lt;li&gt;Apply the decision rule in the next section before the agent is allowed to block a merge on statistical grounds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A minimal packet builder, labeled as a proposal, keeps the review model from swallowing entire information_schema dumps. Do not paste production rows, and do not ask the model to invent histograms that were never captured.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Proposal: bound the review packet; unexecuted example.
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_packet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_excerpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixture_fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;captured_fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;captured_age_hours&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema_excerpt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;schema_excerpt&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixture_fingerprint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fixture_fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;captured_fingerprint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;captured_fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;captured_age_hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;captured_age_hours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disallowed_nodes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Seq Scan on events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rules&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Label each finding structural or statistical.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Never recommend EXPLAIN ANALYZE on INSERT, UPDATE, DELETE, or MERGE.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;If fingerprints differ, do not auto-rewrite SQL.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decision table
&lt;/h2&gt;

&lt;p&gt;The table below is a policy object that should be versioned next to the agent prompt. Teams can argue about a row in review, instead of arguing about a model tone in chat. It is not a quality score, and it does not claim a latency improvement for either evidence source.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Prefer fixture EXPLAIN&lt;/th&gt;
&lt;th&gt;Prefer captured plan&lt;/th&gt;
&lt;th&gt;Block merge?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Finding is implicit cast or missing index&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Optional contrast&lt;/td&gt;
&lt;td&gt;Yes, if the fixture reproduces it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fingerprints match&lt;/td&gt;
&lt;td&gt;Either&lt;/td&gt;
&lt;td&gt;Either&lt;/td&gt;
&lt;td&gt;Only for a listed disallowed node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fingerprints differ and captured plan is under 24h old&lt;/td&gt;
&lt;td&gt;Contrast only&lt;/td&gt;
&lt;td&gt;Primary&lt;/td&gt;
&lt;td&gt;Yes, if the captured plan has a disallowed node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Captured plan older than 24h or stats_reset is unknown&lt;/td&gt;
&lt;td&gt;Primary&lt;/td&gt;
&lt;td&gt;Discard&lt;/td&gt;
&lt;td&gt;No statistical block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Statement is DML&lt;/td&gt;
&lt;td&gt;Fixture EXPLAIN only&lt;/td&gt;
&lt;td&gt;Replica EXPLAIN without ANALYZE&lt;/td&gt;
&lt;td&gt;Always block ANALYZE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seed data cannot encode skew&lt;/td&gt;
&lt;td&gt;Do not claim costs&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;Human review, no auto-block&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The decision rule
&lt;/h2&gt;

&lt;p&gt;Apply the following rule without slogans and without hidden fallbacks inside the agent prompt text. If the finding is structural and the fixture plan reproduces it, the fixture wins and the agent may block. If the finding is statistical, the captured plan wins only when age, major version, and bind types are known. Otherwise the agent may comment, and it must not block the merge on statistical cost arguments.&lt;/p&gt;

&lt;p&gt;If the two fingerprints disagree, the agent must not rewrite SQL, because the wrong plan trains a new incident. If DML is involved, drop ANALYZE and drop any suggestion that requires executing the write on a shared cluster. That split also clarifies what a scratch model runtime can honestly do during a SQL review. It can classify structural versus statistical findings from a bounded packet, and it cannot invent missing histograms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should not use this
&lt;/h2&gt;

&lt;p&gt;This approach does not estimate latency, and it does not replace pg_stat_statements sampling on serving clusters. Fixture EXPLAIN will understate skew, correlation, and partial index usefulness, so it must not block on cost. Captured plans will overfit one bind value, which is why a single hash is not a workload summary. Treat both artifacts as evidence with known failure modes, not as a substitute for owned query operations.&lt;/p&gt;

&lt;p&gt;Do not use this debate as a merge gate if generated SQL text is unstable across equivalent requests. Skip it when RLS policies differ between fixture and runtime, or when the statement must not leave the production network. Do not send production row samples to any shared model host when columns can contain secrets or identifiers. Teams that cannot restore statistics, and also cannot build honest fixtures, should keep SQL review human.&lt;/p&gt;

&lt;p&gt;The method also refuses a common shortcut that makes agents sound more certain than the evidence. Adding ANALYZE to settle a review turns the agent into a load generator, and sometimes into a locker of rows. If the only way to settle the debate is to execute the query, the agent has left review. That next workflow belongs to operations, with different owners, different budgets, and explicit rollback rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to keep when the models change
&lt;/h2&gt;

&lt;p&gt;Model routing will keep changing, and free inference tiers will keep changing around whatever host you choose. The durable artifact remains the pair of fingerprints plus the structural-versus-statistical label on each finding. If a future model disagrees with a previous comment, rerun the packet against the same JSON files. Do not rerun the query to break a tie, because that restarts the original production risk this debate exists to avoid.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>postgres</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Catalog Tools or Frozen Contracts: A Debate for SQL Review Agents</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Fri, 11 Sep 2026 15:46:01 +0000</pubDate>
      <link>https://dev.to/dataio_4921/catalog-tools-or-frozen-contracts-a-debate-for-sql-review-agents-5400</link>
      <guid>https://dev.to/dataio_4921/catalog-tools-or-frozen-contracts-a-debate-for-sql-review-agents-5400</guid>
      <description>&lt;p&gt;A pull request added a reporting query that joined orders, order_items, products, and a new promo_windows table. The SQL review agent still carried last week's schema dump inside its prompt context window. It treated promo_windows as missing and opened a false-positive finding against a table that already existed. A second run queried information_schema.columns during review and accepted the join, but that path needed a live database session in CI. That fork is the subject of this debate: on-demand catalog tools versus versioned schema contracts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem both sides are trying to bound
&lt;/h2&gt;

&lt;p&gt;SQL review agents fail in two opposite ways when the catalog they see is wrong. They either reject valid SQL because a new object is absent from a stale dump, or they approve SQL that references columns dropped after the dump was taken. Planner statistics raise a related issue, yet this article does not revisit index hit counters or queue ranking. It asks a narrower question about how the agent is allowed to learn names, types, and constraints. The answer changes reproducibility, secret handling, and how much production privilege the review path must hold.&lt;/p&gt;

&lt;p&gt;Both positions below assume the agent may not execute DML, may not take locks, and may not rewrite SQL without a human gate. They also assume CI must finish within a predictable wall clock, because review queues stall merge trains. The disagreement is only about the catalog channel. Names and types are treated as facts; cardinality estimates are out of scope unless a later job adds them as a separate artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: On-demand catalog tools
&lt;/h2&gt;

&lt;p&gt;Position A treats the database catalog as a tool surface, in the same family as MCP resource calls. The agent starts with the SQL text, the changed files, and a short allowlist of catalog procedures. When it needs a relation, it calls a narrow function that returns columns, nullability, and check constraints for that relation only. Advocates argue this keeps the prompt small and tracks migrations that landed minutes ago on the review database.&lt;/p&gt;

&lt;p&gt;Evidence for this position is mostly operational rather than academic. Schema dumps that include every comment, index, and sequence routinely exceed what a review prompt should hold for a single query. Teams that inline entire &lt;code&gt;pg_dump&lt;/code&gt; output also leak default privileges and comment text that never belonged in a model context. On-demand tools avoid that bulk, provided each call is parameterized, read-only, and timeout-bounded.&lt;/p&gt;

&lt;p&gt;The cost is coupling. A catalog tool needs a network path, credentials, and a target that resembles production names. If that target is production, a buggy tool wrapper can still issue heavy catalog queries during peak hours. If that target is a replica, lag can hide a migration that the pull request itself is introducing. Position A therefore implies a review database that applies the branch migration first, then answers catalog calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example tool wrapper (labeled sketch)
&lt;/h3&gt;

&lt;p&gt;The following snippet is a proposal, not production credential handling. It fetches one relation per call, rejects wildcards, and sets a statement timeout before the catalog read.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Sketch only. Do not reuse as a privileged CI role.
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;catalog_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isidentifier&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isidentifier&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refusing non-identifier catalog lookup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT column_name, data_type, is_nullable
        FROM information_schema.columns
        WHERE table_schema = %s AND table_name = %s
        ORDER BY ordinal_position
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET LOCAL statement_timeout = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2s&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;LookupError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nullable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YES&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
        &lt;span class="nf"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tool-shaped loop still needs a hard budget. Four relations in one join should mean four calls, not an open-ended search through &lt;code&gt;pg_class&lt;/code&gt;. If the agent cannot name the relation from the SQL text, Position A should fail closed instead of scanning the catalog for lookalikes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: Versioned schema contracts
&lt;/h2&gt;

&lt;p&gt;Position B refuses live catalog calls from the model loop. Instead, CI generates a compact JSON contract from the applied migration set and stores it next to the SQL under review. The agent receives only that file, plus the query text, and must justify every relation against the contract. If the contract and the SQL disagree, the review fails closed without opening a database session for the model.&lt;/p&gt;

&lt;p&gt;Evidence here is about audit and replay. A frozen contract can be reviewed in Git, signed, and replayed months later when an incident questions why a query passed. On-demand tools leave a trail of ad hoc catalog reads that are hard to reconstruct unless every tool response is logged verbatim. Contracts also shrink blast radius: the model host never holds a database password, and the review can run on a machine that is not routed to production.&lt;/p&gt;

&lt;p&gt;The cost is freshness. A contract generated from migration files, but not from a database that applied those migrations, can drift from true types such as domains, generated columns, or &lt;code&gt;search_path&lt;/code&gt; surprises. Position B therefore needs a deterministic exporter, not a handwritten YAML file that humans forget to update. The exporter may touch a database; the model loop may not.&lt;/p&gt;

&lt;p&gt;This is where isolated compute helps. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open-source project that currently offers free model access and a free server option, which can host the exporter and the scoring job without sharing a production DSN with the model. That pairing is relevant to Position B because the model sees files, while the server sees the database. It is not a requirement for Position A, and it does not replace a review replica that already exists inside a private network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Numbered workflow for the contract path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Apply the pull request migrations to an empty, disposable Postgres instance that is not reachable from the model process.&lt;/li&gt;
&lt;li&gt;Run a pinned exporter that writes only the relations referenced by the changed SQL, plus their constraints.&lt;/li&gt;
&lt;li&gt;Commit or upload the contract as a CI artifact with a content hash beside the query text.&lt;/li&gt;
&lt;li&gt;Give the review model the SQL, the contract, and a rule that unknown relations are defects.&lt;/li&gt;
&lt;li&gt;Keep the database DSN on the exporter host; never inject it into the model prompt or tool list.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Exporter fragment. Label: run on the review instance after migrations.&lt;/span&gt;
&lt;span class="c1"&gt;-- Limit output to relations named by the SQL parser, not the whole catalog.&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_nullable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;column_default&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;has_default&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'public'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'public'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'order_items'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'public'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'products'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'public'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'promo_windows'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'/tmp/schema_contract.csv'&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;CSV&lt;/span&gt; &lt;span class="n"&gt;HEADER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Artifact: a contract file and a gate
&lt;/h2&gt;

&lt;p&gt;The contract is intentionally smaller than a dump. It records facts the reviewer must not invent: relation identity, column types, nullability, and a generation timestamp. Cardinality, index hit rates, and buffer stats stay out, because those values go stale on a different clock and belong to a different debate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"generated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-11T12:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"review-db-after-migrations"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"relations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"public.promo_windows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"columns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bigint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"nullable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"nullable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"starts_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"timestamp with time zone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"nullable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ends_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"timestamp with time zone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"nullable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gate below is executable locally. It does not call a model. It answers whether a parsed relation list is covered by the contract, which is the mechanical half of Position B. The model half only starts after this function returns &lt;code&gt;ok&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
    &lt;span class="n"&gt;extra_in_sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;  &lt;span class="c1"&gt;# reserved; parser fills this
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gate_against_contract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_relations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;relations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}))&lt;/span&gt;
    &lt;span class="n"&gt;needed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_relations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;needed&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;known&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extra_in_sql&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decide_channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;has_private_review_db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;must_replay_in_git&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Decision helper. Prefer contracts when replay beats freshness.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;must_replay_in_git&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;has_private_review_db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_private_review_db&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;must_replay_in_git&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;catalog_tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_private_review_db&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;must_replay_in_git&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;export_then_contract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# Position B with a live exporter
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refuse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# no catalog channel is safe enough
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the gate with a fixture before wiring any model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from gate import gate_against_contract; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
print(gate_against_contract(['public.promo_windows'], {'relations': {'public.promo_windows': {}}}))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Catalog tools (A)&lt;/th&gt;
&lt;th&gt;Frozen contract (B)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Branch migrations must be visible in minutes&lt;/td&gt;
&lt;td&gt;Strong fit, if CI applies them first&lt;/td&gt;
&lt;td&gt;Fit only after a fresh export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review must be replayable from Git alone&lt;/td&gt;
&lt;td&gt;Weak, unless every tool payload is stored&lt;/td&gt;
&lt;td&gt;Strong fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model host must not receive a DSN&lt;/td&gt;
&lt;td&gt;Weak, tools need a session&lt;/td&gt;
&lt;td&gt;Strong fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catalog is large, query touches three tables&lt;/td&gt;
&lt;td&gt;Strong fit, prompt stays small&lt;/td&gt;
&lt;td&gt;Strong fit if export is relation-scoped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No review database exists&lt;/td&gt;
&lt;td&gt;Unsafe&lt;/td&gt;
&lt;td&gt;Unsafe unless dump is signed and recent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance forbids model-adjacent DB credentials&lt;/td&gt;
&lt;td&gt;Poor fit&lt;/td&gt;
&lt;td&gt;Required path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The decision rule
&lt;/h2&gt;

&lt;p&gt;Use this rule in order, and stop at the first match. Do not average the rows in the table above.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the model runtime cannot be trusted with a database credential, choose Position B and keep the exporter on a separate host.&lt;/li&gt;
&lt;li&gt;If incident review must replay the exact catalog facts, choose Position B and store the contract hash with the pull request.&lt;/li&gt;
&lt;li&gt;If the query references objects created in the same branch, and a review database can apply those migrations, you may choose Position A.&lt;/li&gt;
&lt;li&gt;If Position A is chosen, allow only per-relation catalog reads with timeouts; refuse &lt;code&gt;LIKE&lt;/code&gt; and unqualified name search.&lt;/li&gt;
&lt;li&gt;If both replay and same-branch objects matter, do not run tools from the model. Export after migrations, then review against the contract. That is Position B with a live exporter, not Position A.&lt;/li&gt;
&lt;li&gt;If none of the above can be satisfied, skip automated SQL review rather than pointing the agent at production catalog tables.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fifth clause is the one teams skip. Mixing a model-held tool with a half-applied migration looks fast, and it recreates the false missing-table finding from the opening scenario, or the inverse approval of a dropped column. Export-then-contract is slower by one CI step and removes that class of disagreement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, and who should not use this
&lt;/h2&gt;

&lt;p&gt;This debate does not measure model quality, token burn, or planner cost accuracy. The contract shown here omits row counts, &lt;code&gt;n_distinct&lt;/code&gt;, and index definitions, so it will not catch a sequential scan that a human DBA would reject. Catalog tools that return &lt;code&gt;pg_stats&lt;/code&gt; would change the privilege story and should be a separate design, with a separate allowlist.&lt;/p&gt;

&lt;p&gt;Do not use Position A from a shared laptop against production, even with a read-only user, if catalog functions can still take locks or read comments that include secrets. Do not use Position B as a substitute for applying migrations; a contract copied from main will miss the branch table that started this article. Do not treat a free shared server as a holder of production data. If the SQL under review is the production dataset, the exporter belongs in your network, and only sanitized contracts should leave that network.&lt;/p&gt;

&lt;p&gt;Teams without a SQL parser should not let the model invent the relation list that feeds the gate. Parse first with a library you already trust, then run &lt;code&gt;gate_against_contract&lt;/code&gt;. The model can explain a miss; it should not be the only process that decides which tables exist.&lt;/p&gt;

&lt;p&gt;If you need a disposable host for the exporter-and-gate loop, MonkeyCode's free server option and free model access are one way to keep that loop off production credentials. The useful part of the method remains the contract hash and the six-step rule, even if that host is a different CI runner.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>postgres</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Shared Endpoint or Isolated Runtime: A Routing Rule for SQL Review Agents</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Thu, 10 Sep 2026 04:21:22 +0000</pubDate>
      <link>https://dev.to/dataio_4921/shared-endpoint-or-isolated-runtime-a-routing-rule-for-sql-review-agents-22p9</link>
      <guid>https://dev.to/dataio_4921/shared-endpoint-or-isolated-runtime-a-routing-rule-for-sql-review-agents-22p9</guid>
      <description>&lt;p&gt;At 09:14 a review bot comments on a migration that rewrites a forty-million-row payments table in staging. The comment itself is useful, but the request body includes the full CREATE TABLE statement plus three sample rows. Those rows came from a staging clone, and the inference endpoint sat on a shared server several other teams also called. Cost for that call was zero, which pleased finance, while the data-classification question remained completely unanswered.&lt;/p&gt;

&lt;p&gt;SQL review agents fail in a quieter way than missing an index. They fail when query text, catalog fragments, and EXPLAIN output leave a trust boundary that nobody named. This article treats that boundary as a routing problem, not a model-quality contest. Two credible positions follow, then a decision rule a team can implement without inventing a new agent stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a routing debate, not a billing debate
&lt;/h2&gt;

&lt;p&gt;Pull-request SQL already lives in git, so people assume the model prompt is equally public. That assumption collapses as soon as the bot adds live catalogs, row samples, connection hosts, or runtime plans. Those extras are what make comments specific, and they are also what change the classification of the payload.&lt;/p&gt;

&lt;p&gt;A shared free endpoint can be the right sink for unclassified review traffic. An isolated runtime can be the right sink when the same bot starts attaching catalog facts. Mixing those sinks without a rule produces the Monday morning payload above. The rest of this piece stays on that split.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: Put the review model on a shared free endpoint
&lt;/h2&gt;

&lt;p&gt;Position A says iteration speed is the scarce resource during the first months of a SQL review bot. Engineers will not tune prompts, comment templates, or severity labels if every dry run needs a purchase order. A shared endpoint with free model access removes that stall, so the team can measure false positives on real pull requests.&lt;/p&gt;

&lt;p&gt;Evidence for this position is operational rather than rhetorical. Count how many review payloads in a week contain only SQL that already appears in the pull request, with no live catalog, no row samples, and no hostnames. If that share is high, a shared server is carrying low-sensitivity work. The cost of isolating that work is delay, not safety.&lt;/p&gt;

&lt;p&gt;Position A also argues that comment quality and data egress are separate controls. Redaction and allow-lists can sit in front of any endpoint. If those controls are tested, the physical location of the model is a capacity choice. Teams that already publish DDL in public repositories have a weaker case for treating every review prompt as secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: Keep inference inside an isolated runtime
&lt;/h2&gt;

&lt;p&gt;Position B says SQL review is not generic chat. The dangerous object is the concatenation of schema, predicates, and samples, even when each piece looks dull alone. A shared server used by several teams expands the set of people, processes, and logs that can see that concatenation.&lt;/p&gt;

&lt;p&gt;Evidence for this position shows up in payload diffs, not in model leaderboards. Compare the bot's request body with the files in the pull request. Any field that does not appear in git is an egress candidate: &lt;code&gt;pg_stats&lt;/code&gt; excerpts, &lt;code&gt;EXPLAIN&lt;/code&gt; JSON, column comments from a live catalog, or “example rows to help the model.” Those fields are why isolated inference exists.&lt;/p&gt;

&lt;p&gt;Position B further notes that free shared capacity does not change retention, subprocessors, or prompt-log access. A review agent that posts in GitHub still sent the prompt somewhere else first. If the somewhere else is a multi-tenant server, the threat model includes operator access and noisy-neighbor logs, not only model weights.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a free shared server actually participates
&lt;/h2&gt;

&lt;p&gt;The useful compromise is not “always shared” or “always private.” It is a classifier in front of the model call. Unclassified PR-only SQL may use a shared free endpoint so prompt work does not wait on procurement. Classified payloads never take that path.&lt;/p&gt;

&lt;p&gt;MonkeyCode's free model access and free server option can fill the unclassified slot in that design, which is the only slot this article assigns them. Disclosure: This article was prepared as part of MonkeyCode's product outreach. They are not a substitute for isolation when the payload includes catalog extracts, samples, or connection metadata. Treat both offerings as availability claims, not as a graded security certification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Artifact: classify the payload before you choose an endpoint
&lt;/h2&gt;

&lt;p&gt;The following classifier is a proposal, not a production security review. It encodes the split above as explicit fields a SQL review agent already tends to collect. Label it unexecuted until your team runs the tests in the next section against recorded payloads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Sink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;SHARED_FREE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared_free&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;ISOLATED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isolated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;REFUSE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refuse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;SENSITIVE_MARKERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws_access&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection_string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;copy (&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_dump&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReviewPayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sql_from_pr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;live_catalog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;explain_json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sample_rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;system_class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;internal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# public | internal | confidential | restricted
&lt;/span&gt;    &lt;span class="n"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;contains_secret_marker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;lowered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lowered&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SENSITIVE_MARKERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_sql_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ReviewPayload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Sink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sql_from_pr&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;contains_secret_marker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REFUSE&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_class&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidential&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restricted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ISOLATED&lt;/span&gt;
    &lt;span class="n"&gt;extras&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;live_catalog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;explain_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sample_rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ISOLATED&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_class&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SHARED_FREE&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ISOLATED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A second artifact is the decision table the classifier implements. Keep this table in the bot repository next to the prompt templates, so routing changes require a reviewed diff.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Payload contents&lt;/th&gt;
&lt;th&gt;System class&lt;/th&gt;
&lt;th&gt;Allowed sink&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQL already in the PR, no extras&lt;/td&gt;
&lt;td&gt;public or internal&lt;/td&gt;
&lt;td&gt;shared free&lt;/td&gt;
&lt;td&gt;Prompt iteration path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL in the PR plus live catalog&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;isolated&lt;/td&gt;
&lt;td&gt;Catalog is not git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL plus EXPLAIN JSON&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;isolated&lt;/td&gt;
&lt;td&gt;Plans leak predicates and row estimates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any sample rows&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;isolated&lt;/td&gt;
&lt;td&gt;Rows are data, not schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hostnames or connection strings&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;refuse or isolated&lt;/td&gt;
&lt;td&gt;Prefer refuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret markers in SQL text&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;refuse&lt;/td&gt;
&lt;td&gt;Do not “redact and send” by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;confidential or restricted systems&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;isolated&lt;/td&gt;
&lt;td&gt;Class beats convenience&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Numbered workflow on a throwaway repository
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Record twenty real review payloads from a non-production bot, including every JSON field the agent currently sends. Store them as files, not in chat history, so the classifier can run twice.&lt;/li&gt;
&lt;li&gt;Label each file with system class and with a boolean for extras that do not appear in the pull request. Do this labeling by hand for the first week; automation comes after disagreement rate drops.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;route_sql_review&lt;/code&gt; over the corpus and print a three-column tally: shared_free, isolated, refuse. If refuse is zero, your marker list is probably too weak.&lt;/li&gt;
&lt;li&gt;Bind only the &lt;code&gt;shared_free&lt;/code&gt; slice to a free shared server, and keep the other slices on an isolated runtime or drop them. Do not “temporarily” send isolated traffic to the shared sink to save a queue.&lt;/li&gt;
&lt;li&gt;Re-read five shared_free comments on later pull requests and check whether the bot quietly reintroduced extras. If a comment cites a live index the PR never mentioned, the classifier leaked around the route.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A minimal dry-run command looks like this. Adjust paths; do not paste production schema into the sample files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
from pathlib import Path
import json
from review_route import ReviewPayload, route_sql_review

for path in sorted(Path("payloads").glob("*.json")):
    raw = json.loads(path.read_text())
    sink = route_sql_review(ReviewPayload(**raw))
    print(f"{path.name}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="sh"&gt;{sink.value}")
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output is a stable mapping from filename to sink. A test plan with four fixtures is enough to prevent silent drift when someone adds “helpful” catalog context next quarter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# labeled tests; unexecuted until you wire pytest
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_pr_only_sql_may_use_shared_free&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReviewPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_from_pr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALTER TABLE orders ADD COLUMN note text;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;route_sql_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared_free&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_sample_rows_force_isolated&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReviewPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;sql_from_pr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM orders WHERE id = 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sample_rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;route_sql_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isolated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_explain_json_force_isolated&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReviewPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;sql_from_pr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;explain_json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Plan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Node Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Seq Scan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Relation Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;route_sql_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isolated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_secret_marker_refuses&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReviewPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_from_pr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-- password = hunter2&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;SELECT 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;route_sql_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refuse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Evidence a team can collect without a vendor bake-off
&lt;/h2&gt;

&lt;p&gt;Shared-endpoint advocates should publish two numbers after one week: share of payloads that are PR-only, and comment precision on that slice. Isolated-runtime advocates should publish two different numbers: count of extra fields not present in git, and count of systems classed confidential. Those four numbers decide the mix. Model brand names do not.&lt;/p&gt;

&lt;p&gt;If PR-only share is below one third, Position A is arguing from a workload you do not have. If extra-field count is near zero, Position B is paying isolation rent on chat that already matched the pull request. Re-measure when someone adds catalog tools, because that commit changes the mix overnight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, and who should not use the shared path
&lt;/h2&gt;

&lt;p&gt;This routing rule does not encrypt prompts, negotiate a business associate agreement, or prove a vendor's log retention. It only stops unclassified and classified traffic from sharing a sink by accident. Redaction is incomplete by construction: SQL predicates can identify people without containing a column named &lt;code&gt;email&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do not use the shared free path for health, payments, or government schemas, even when the SQL is “just a migration.” Do not use it when sample rows exist, including fake-looking staging rows that were copied from production last year. Do not use it as a workaround when the isolated runtime is slow. Queue delay is cheaper than an untracked catalog dump.&lt;/p&gt;

&lt;p&gt;Teams without a data-classification label should not default to shared. Missing labels are not “public.” They are unknown, and unknown routes to isolated or refuse. The classifier above treats confidential and restricted as isolated regardless of extras, which is intentional and not a performance bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision rule
&lt;/h2&gt;

&lt;p&gt;Use a shared free endpoint only when every field in the model request already appears in the pull request, the system class is public or internal, and secret markers are absent. Otherwise keep inference isolated, or refuse the call. Re-run the corpus tally whenever the agent gains a new tool that can read catalogs, plans, or rows.&lt;/p&gt;

&lt;p&gt;That rule is deliberately boring. SQL review agents already accumulate if-statements around severity and style; they need one more around egress. Cost pressure belongs in the unclassified slice. Schema and samples do not become cheaper when the server is free.&lt;/p&gt;

&lt;p&gt;If you already operate a SQL review bot, paste the decision table into the repository and fail CI when a new prompt field is added without a sink update. The table is the product of this debate; the endpoint you bind to &lt;code&gt;shared_free&lt;/code&gt; is a later, narrower choice.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>ai</category>
      <category>agents</category>
      <category>database</category>
    </item>
    <item>
      <title>Diagnostic Findings or Auto-Rewrites: A Gate for SQL Review Agents</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Wed, 09 Sep 2026 16:04:53 +0000</pubDate>
      <link>https://dev.to/dataio_4921/diagnostic-findings-or-auto-rewrites-a-gate-for-sql-review-agents-39fo</link>
      <guid>https://dev.to/dataio_4921/diagnostic-findings-or-auto-rewrites-a-gate-for-sql-review-agents-39fo</guid>
      <description>&lt;p&gt;SQL review agents now sit in pull request queues and often paste a rewritten statement into the same diff. That extra authority looks efficient until the new text changes lock shape, isolation, or scan family. Syntax-only checks will not catch those shifts, because the rewritten query still parses and still returns a plausible row set. The merge then ships with mixed authorship, which is a control problem rather than a model-quality complaint.&lt;/p&gt;

&lt;p&gt;Consider a labeled incident pattern from migration review, not a claim about one private outage. An author submits a point delete that filters orders by primary key, which usually takes a narrow row lock. The agent rewrites it into a DELETE that joins an unpaid-status subquery because a style guide prefers expressive SQL. Staging stays quiet because the clone is small, and reviewers compare style instead of predicate hashes.&lt;/p&gt;

&lt;p&gt;Production later queues checkout updates behind a wider lock than the original statement implied. The engine did what the new text asked, and the pull request still showed a green review badge. The miss was not that a model guessed a worse join; the miss was that no gate decided whether the bot may change SQL text at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why rewrite authority is the real control
&lt;/h2&gt;

&lt;p&gt;Most SQL review threads argue about prompts, schema dumps, or whether EXPLAIN belongs in context. Those inputs matter, yet they do not answer the ownership question that DML actually raises in review. If the agent can edit the statement, lock scope and transaction shape become shared work, and shared work is how unsafe deletes land.&lt;/p&gt;

&lt;p&gt;Treat the model as a witness that fills a schema, or as a compiler pass that emits patches, but do not leave that choice inside the chat. The rest of this article compares those two positions with observables you can score on a clone. It then gives a numbered CI gate, a finding schema, and a decision rule that does not depend on tone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: the agent should return a patch
&lt;/h2&gt;

&lt;p&gt;Teams that favor auto-rewrites treat the model as a compiler pass over SQL text. The bot can fold redundant predicates, replace unbounded &lt;code&gt;SELECT *&lt;/code&gt; lists, and push filters before joins when the schema is stable. In a high-volume review queue, a patch reduces round trips when the finding is mechanical and the statement is read-only.&lt;/p&gt;

&lt;p&gt;Auto-rewrite also encodes institutional style in one place instead of a wiki that nobody opens. If the organization bans &lt;code&gt;NOT IN&lt;/code&gt; against nullable columns, the agent can emit &lt;code&gt;NOT EXISTS&lt;/code&gt; rather than writing another comment essay. That is attractive for reporting SELECTs on a replica, where lock shape is usually not the merge risk.&lt;/p&gt;

&lt;p&gt;The cost is authorship. Once the bot edits DML, blame for cardinality, isolation, and lock range is shared across human and model. Shared blame is a process smell, especially when the rewrite changes equality keys that also define the lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: the agent should return evidence only
&lt;/h2&gt;

&lt;p&gt;Teams that forbid patches treat the model as a witness, not a second author on the diff. The bot must emit a structured finding: statement class, tables touched, equality keys, range predicates, and a recommended human action. The original SQL remains the only text that can merge, which keeps a single writer for statements that take locks.&lt;/p&gt;

&lt;p&gt;Evidence-only review also simplifies audit trails, because the commit still belongs to the person who typed the predicates. Reviewers can disagree with a finding without arguing about who changed the WHERE clause during the bot pass. The downside is latency, since humans must apply mechanical nits, and some style issues will survive until the next review cycle.&lt;/p&gt;

&lt;p&gt;This position fits production DML, tenant isolation filters, and any statement whose WHERE clause is also the lock clause. It is the conservative default when the clone is incomplete or EXPLAIN output is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to score instead of helpfulness
&lt;/h2&gt;

&lt;p&gt;Score each statement on four observables that a disposable clone can provide during CI. Do not score “helpfulness,” because that metric rewards silent rewrites that still parse.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Statement class: &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, DDL, or unknown after parse.&lt;/li&gt;
&lt;li&gt;Predicate stability: did any proposed rewrite change equality keys or expand ranges?&lt;/li&gt;
&lt;li&gt;Plan family: Seq Scan, Index Scan, Nested Loop, Hash Join, or unknown when EXPLAIN fails.&lt;/li&gt;
&lt;li&gt;Authorship: is the merged text identical to the human submission for DML and DDL?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A patch that changes class 2 or class 3 should never auto-apply. A SELECT patch that only narrows the projection list may be allowed under Position A when class 3 is unchanged. DML that fails class 4 should fail the gate if you adopt Position B for writes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Statement class&lt;/th&gt;
&lt;th&gt;Auto-rewrite&lt;/th&gt;
&lt;th&gt;Required clone check&lt;/th&gt;
&lt;th&gt;Merge text on failure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SELECT&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Predicate hash and plan family match&lt;/td&gt;
&lt;td&gt;Keep original, comment only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INSERT / UPDATE / DELETE&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;EXPLAIN original only&lt;/td&gt;
&lt;td&gt;Keep original, fail closed if EXPLAIN missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DDL&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;Parser class only&lt;/td&gt;
&lt;td&gt;Keep original&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;Fail closed for writes&lt;/td&gt;
&lt;td&gt;Keep original&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A numbered gate you can run in CI
&lt;/h2&gt;

&lt;p&gt;The workflow below is a proposal. Label it unexecuted until you wire a real parser, a clone DSN, and your own model client.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse the diff with a dialect-aware SQL parser, not a regex that splits on semicolons inside string literals.&lt;/li&gt;
&lt;li&gt;Classify each statement; unknown class fails closed for DML and DDL, and stays comment-only for reads.&lt;/li&gt;
&lt;li&gt;Ask the model for a diagnostic object only. Do not ask it to “improve” the query in the same call as the finding.&lt;/li&gt;
&lt;li&gt;Restore a schema-only or sanitized clone that has production-like indexes and statistics, then run &lt;code&gt;EXPLAIN&lt;/code&gt; on the original statement.&lt;/li&gt;
&lt;li&gt;If policy allows SELECT patches, request a rewrite in a second call, and EXPLAIN that rewrite on the same clone with the same parameters.&lt;/li&gt;
&lt;li&gt;Compare predicate hashes and plan families. If they diverge, drop the patch and keep the finding as evidence.&lt;/li&gt;
&lt;li&gt;Publish JSON on the pull request. Merged SQL stays human-authored unless the gate explicitly promotes a SELECT-only patch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach. Free model access and a free server option fit steps 3–5 when you want the diagnostic call and clone-side &lt;code&gt;EXPLAIN&lt;/code&gt; off the primary network path. Keep production credentials off that box, spend free model capacity on structured findings, and still refuse to let the agent author DML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Artifact: finding schema, predicate gate, and clone commands
&lt;/h2&gt;

&lt;p&gt;The JSON schema keeps the model from narrating. Every field is checkable in CI without reading prose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"statement_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pr-842:migration:3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DELETE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"equality_keys"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"orders.id"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"range_predicates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rewrite_allowed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plan_family_original"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Index Scan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plan_family_rewrite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lock_note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"point delete on primary key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"human_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"keep original; do not widen to a status subquery"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Proposed Python gate (unexecuted example). It hashes normalized predicates and rejects a rewrite when DML is involved or when the hash changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# proposal.py — labeled example, not a production service
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;DML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_pred&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\s+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pred_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;normalize_pred&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;explain_family&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Node Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;original_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rewrite_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orig_plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_plan&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merge_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;original_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence_only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;DML&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DDL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;writes_stay_human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rewrite_sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_patch_requested&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;
    &lt;span class="n"&gt;same_pred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pred_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;pred_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rewrite_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;same_plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;explain_family&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orig_plan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;explain_family&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_plan&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;same_pred&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;same_plan&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rewrite_allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merge_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rewrite_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;select_patch_ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;select_plan_stable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;patch_changed_pred_or_plan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clone-side commands stay ordinary Postgres. Point them at a disposable copy that cannot reach the primary, including through &lt;code&gt;EXPLAIN&lt;/code&gt; of DML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# labeled commands — CLONE_DSN must not be the primary&lt;/span&gt;
psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLONE_DSN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="sh"&gt;'
EXPLAIN (FORMAT JSON, COSTS OFF)
DELETE FROM orders WHERE id = 0;
&lt;/span&gt;&lt;span class="no"&gt;SQL
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tiny test plan you can run without a model in the loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Point delete &lt;code&gt;WHERE id = 1&lt;/code&gt; versus rewrite &lt;code&gt;WHERE id IN (SELECT id FROM orders WHERE status = 'unpaid')&lt;/code&gt;. The gate must keep the original text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SELECT * FROM orders WHERE id = 1&lt;/code&gt; versus &lt;code&gt;SELECT id, status FROM orders WHERE id = 1&lt;/code&gt;. If policy allows SELECT patches and plan family matches, the gate may promote the rewrite.&lt;/li&gt;
&lt;li&gt;Missing EXPLAIN JSON or a timed-out clone. DML fails closed; SELECT stays evidence-only and does not merge a patch.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The decision rule
&lt;/h2&gt;

&lt;p&gt;Use one rule, not a prompt that asks the model to be careful with production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the statement is DML or DDL, the agent may not change text. If the statement is SELECT, a rewrite may merge only when predicate hash and plan family both match the original on a clone. If EXPLAIN or the model call fails, DML fails closed and SELECT stays evidence-only.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That rule turns Position A and Position B into a function of statement class, not a personality debate about agents. Current discussion around agent stacks often hides ordinary control flow behind a chatty interface. Your control flow should be this gate, with the model filling JSON fields rather than choosing who may take a lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this
&lt;/h2&gt;

&lt;p&gt;This gate does not prove correctness. Equal plan families can still hide partition pruning changes, row-level security gaps, or parameter sniffing on the first real bind. Predicate hashing on normalized SQL is brittle with comments, casts, and inlined views, so a dialect parser should replace the regex before you trust the hash.&lt;/p&gt;

&lt;p&gt;Do not allow auto-rewrites when you lack a clone that resembles production indexes and statistics. Do not allow them for tenant isolation predicates, encryption wrappers, or ORM sessions that already open transactions around the statement. Do not point shared or free servers at production DSNs, even for &lt;code&gt;EXPLAIN&lt;/code&gt;, if those statements can take locks or run functions with side effects.&lt;/p&gt;

&lt;p&gt;Teams that need a human signature on every byte of SQL should stay on Position B for all classes. The gate then publishes evidence, and that is a complete design. Keep the decision rule in your repository either way; the useful part of the workflow is the ownership split, not the hosting choice.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>postgres</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Rank by Mean or Rank by Tail: A Queue Rule for SQL Review Agents</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Sun, 06 Sep 2026 21:21:01 +0000</pubDate>
      <link>https://dev.to/dataio_4921/rank-by-mean-or-rank-by-tail-a-queue-rule-for-sql-review-agents-5c15</link>
      <guid>https://dev.to/dataio_4921/rank-by-mean-or-rank-by-tail-a-queue-rule-for-sql-review-agents-5c15</guid>
      <description>&lt;p&gt;A checkout API spent fourteen minutes under a held row lock during a routine afternoon deploy. The SQL review agent had already cleared the hottest query by mean latency that same morning. The blocking statement was a rare report join whose average looked cheap in pg_stat_statements. This article treats that incident pattern as a ranking problem rather than a model-quality problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The queue is the hidden prompt
&lt;/h2&gt;

&lt;p&gt;Most SQL review agents do not read every statement that reached the database. They receive a shortlist, and that shortlist is usually ordered by mean execution time from pg_stat_statements. Mean time is cheap to query, stable across many calls, and already sitting in shared catalogs. It is also a biased prior when lock waits and parameter sniffing live in the tail.&lt;/p&gt;

&lt;p&gt;PostgreSQL does not store a true p95 inside pg_stat_statements for any queryid. The extension exposes mean_exec_time, stddev_exec_time, min_exec_time, and max_exec_time on each normalized fingerprint. If an agent ranks only on mean_exec_time, it will starve statements that stay cheap on average and become catastrophic on the worst call. That starvation is a property of the queue, not a property of the language model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: rank the review queue by mean_exec_time
&lt;/h2&gt;

&lt;p&gt;Position A treats mean latency as the least noisy ranking key available without extra logging. High-call statements with elevated means are where CPU, buffer cache, and planner cost usually concentrate. Reviewing them first maximizes expected work avoided per token spent on later analysis. The ranking key stays inside the catalog, so the pipeline needs no log shipper and no percentile store.&lt;/p&gt;

&lt;p&gt;Advocates of Position A also note that max_exec_time is a single observation, not a distribution. One checkpoint stall or one cold cache fill can inflate max without describing the steady state that the cluster actually pays. Mean time, especially once calls pass a few hundred, is closer to the planner's own average-cost view. For agents that propose indexes, that average is a better match than a one-off spike.&lt;/p&gt;

&lt;p&gt;Position A therefore ships a shortlist of the top N queryids by mean_exec_time, filtered by a minimum calls threshold. The agent never sees the rare report that blocked checkout unless that report also carries a high mean. That is a deliberate trade: stability and token efficiency over tail coverage. Teams with clean OLTP and no ad-hoc reporting often accept that trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: rank by a tail proxy, then break ties with mean
&lt;/h2&gt;

&lt;p&gt;Position B starts from a catalog fact: a lock spike is a tail event, not an average event. mean_exec_time can stay modest while max_exec_time or stddev_exec_time explodes on the same queryid. Review agents that ignore those columns keep polishing popular sequential scans and miss the join that holds a primary row. The mean is not wrong; it is answering a different question than on-call is asking.&lt;/p&gt;

&lt;p&gt;A practical tail proxy, without claiming a real percentile, is mean_exec_time plus k times stddev_exec_time, with max_exec_time as a circuit breaker. Large standard deviation relative to mean is a signal of parameter sniffing, wait events, or plan flips across binds. Position B ranks by that proxy, then uses mean_exec_time only as a tie breaker among stable fingerprints. k equals two in the example below; it is a gate, not a statistical proof.&lt;/p&gt;

&lt;p&gt;Position B accepts more false positives on purpose. Checkpoint noise and one-off maintenance will enter the review queue on noisy days. The operational claim is that a false positive review is cheaper than a missed lock on a writer row. That claim should be tested against your lock_timeout logs, not against a model vendor's demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence you can reproduce on a replica
&lt;/h2&gt;

&lt;p&gt;The following query is a labeled example for PostgreSQL 14 and later with pg_stat_statements installed. Run it on a replica or a restored snapshot, never as a writer-side experiment under lock pressure. Do not paste raw literals from production into an external prompt when those literals can carry PII or tenant keys.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- labeled example: ranking keys, not a percentile estimator&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;queryid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean_exec_time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mean_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stddev_exec_time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stddev_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_exec_time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;mean_exec_time&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;stddev_exec_time&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;tail_proxy_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;mean_exec_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
      &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;stddev_exec_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean_exec_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;query_head&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_statements&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;dbid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_database&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;datname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_database&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;mean_exec_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that ordering with the same select ordered by &lt;code&gt;tail_proxy_ms&lt;/code&gt; descending. The two lists diverge on any workload that mixes waits, bind-sensitive plans, or reporting joins with OLTP. That divergence is the entire debate, and it can be measured before anyone writes a review prompt.&lt;/p&gt;

&lt;p&gt;Export the result from psql so a later ranker can compute overlap without another catalog round trip.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="s2"&gt;"service=replica_ro"&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="sh"&gt;'
&lt;/span&gt;&lt;span class="se"&gt;\c&lt;/span&gt;&lt;span class="sh"&gt;opy (
  SELECT queryid, calls,
         mean_exec_time AS mean_ms,
         stddev_exec_time AS stddev_ms,
         max_exec_time AS max_ms,
         (mean_exec_time + 2 * stddev_exec_time) AS tail_proxy_ms,
         left(query, 120) AS query_head
  FROM pg_stat_statements
  WHERE calls &amp;gt;= 50
) TO 'pgss_export.csv' WITH CSV HEADER
&lt;/span&gt;&lt;span class="no"&gt;SQL
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A small Python ranker makes the comparison explicit for a review bot. The script is a proposal, not a production service, and it never needs database credentials after the export exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# proposal: compare two ranking keys from a CSV export
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;scored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;scored&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;jaccard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sa&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pgss_export.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;mean_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queryid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;tail_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queryid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tail_proxy_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jaccard_top10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;jaccard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tail_ids&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean_only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tail_ids&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tail_only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tail_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean_ids&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Jaccard on the top ten stays high across a weekday and a weekend snapshot, Position A is enough for that database. If the symmetric difference contains reporting joins or ORM &lt;code&gt;IN&lt;/code&gt; lists, Position B is the safer default for the next review window.&lt;/p&gt;

&lt;p&gt;True percentiles still need log sampling or auto_explain, because stddev is not p95. Use the proxy to decide which fingerprints deserve a log hunt, not as a substitute for one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A numbered workflow for the ranking job
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Confirm &lt;code&gt;shared_preload_libraries&lt;/code&gt; includes &lt;code&gt;pg_stat_statements&lt;/code&gt; on the instance you will read, then reload if the extension was added only in the config file.&lt;/li&gt;
&lt;li&gt;Export ranking columns from a replica or a restored snapshot with &lt;code&gt;\copy&lt;/code&gt;, never from a primary that is already showing lock waits.&lt;/li&gt;
&lt;li&gt;Compute Jaccard overlap between mean rank and tail-proxy rank on one weekday file and one weekend file.&lt;/li&gt;
&lt;li&gt;Feed only queryid, calls, ranking keys, and a redacted query head to the review agent, omitting bind values.&lt;/li&gt;
&lt;li&gt;Require the agent to label each finding as mean-driven, tail-driven, or insufficient evidence before it proposes an index or rewrite.&lt;/li&gt;
&lt;li&gt;Block any recommendation that implies &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; on a primary, or DDL without an explicit lock budget and rollback window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The ranking job itself does not need to sit beside the database. A detached runtime is useful when several services share one review loop and the export file should leave the primary host.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option, which can host that detached ranking job and the later review pass without enlarging the production image. The same SQL and Python remain usable on a laptop with a local model if remote review is out of policy. Do not send unredacted SQL text, bind values, or credentials to any remote model, including a free one.&lt;/p&gt;

&lt;p&gt;A labeled review prompt, not an executed production prompt, can force the queue policy into the open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# proposal prompt fragment
You receive a ranked SQL fingerprint list.
Each row has queryid, calls, mean_ms, tail_proxy_ms, cv, max_ms, query_head.
State which ranker selected this row: mean, tail_proxy, or both.
If cv &amp;gt; 1.0 or max_ms / mean_ms &amp;gt; 10, refuse index advice until a replica EXPLAIN exists.
Never recommend EXPLAIN ANALYZE on a writer. Never invent p95 from these columns.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decision rule
&lt;/h2&gt;

&lt;p&gt;Use Position A when all of the following hold at once. Calls on the top mean queries are high, coefficient of variation stays below 1.0, and on-call has not recorded lock timeouts in the current review window. Use Position B when cv exceeds 1.0, max_exec_time is more than ten times mean_exec_time, or OLTP and ad-hoc reporting share one primary. If pg_stat_statements is absent, stop the agent; do not invent ranks from application logs that lack queryid.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Prefer&lt;/th&gt;
&lt;th&gt;Do not do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mean ∩ tail Jaccard at or above 0.7&lt;/td&gt;
&lt;td&gt;Position A&lt;/td&gt;
&lt;td&gt;Dual prompts that waste context on the same queryid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cv above 1.0 or max/mean above 10&lt;/td&gt;
&lt;td&gt;Position B&lt;/td&gt;
&lt;td&gt;Index advice from a single max_exec_time sample&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed reporting on the primary&lt;/td&gt;
&lt;td&gt;Position B plus replica EXPLAIN&lt;/td&gt;
&lt;td&gt;EXPLAIN ANALYZE on the writer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PII or tenant keys in literals&lt;/td&gt;
&lt;td&gt;Redact, then either ranker&lt;/td&gt;
&lt;td&gt;Full query text to a remote model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extension missing&lt;/td&gt;
&lt;td&gt;Human review of slow logs&lt;/td&gt;
&lt;td&gt;Synthetic ranking from incomplete logs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Recompute Jaccard when traffic shape changes, not on a fixed calendar. A review window that follows a release, a reporting deadline, or a vacuum backlog will shift the tail proxy even when mean ranks look unchanged. Store the two ranked lists beside the agent output so later incidents can be traced to the queue, not only to the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should not use this
&lt;/h2&gt;

&lt;p&gt;This method does not compute a real p95. Mean plus a multiple of stddev is a Chebyshev-style proxy, and max_exec_time is one observation. Autovacuum, checkpoints, and cold caches inflate tails without proving a bad plan. pg_stat_statements normalizes literals, so two bind shapes that need different indexes can share one queryid and one misleading mean.&lt;/p&gt;

&lt;p&gt;Do not use this queue rule on tiny datasets where every query fits in memory and means are mostly noise. Do not treat it as permission for an agent to create indexes unattended. Do not use a remote review server if SQL text cannot be redacted down to a head and a fingerprint. Teams without replica access should export from a restored snapshot, not from the writer under load.&lt;/p&gt;

&lt;p&gt;The incident pattern at the start is a reminder that the first query in the prompt is a policy choice. Changing the model without changing the ranker often changes nothing measurable in lock time. If you need a machine that is not the primary for the export-and-review loop, the free server option is one place to park that job.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>ai</category>
      <category>database</category>
    </item>
  </channel>
</rss>
