<?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>Shadow Replay or Read-Only Canary: Two Ways to Ship Agent-Written SQL</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:04:22 +0000</pubDate>
      <link>https://dev.to/dataio_4921/shadow-replay-or-read-only-canary-two-ways-to-ship-agent-written-sql-597k</link>
      <guid>https://dev.to/dataio_4921/shadow-replay-or-read-only-canary-two-ways-to-ship-agent-written-sql-597k</guid>
      <description>&lt;h2&gt;
  
  
  A rewrite that passed every gate and still moved the number
&lt;/h2&gt;

&lt;p&gt;Imagine a team approving an agent-drafted rewrite of a fourteen-table revenue query. The parser gate passed, the read-only guard passed, and the dashboard still drifted by three percent overnight. The defect was one &lt;code&gt;LEFT JOIN&lt;/code&gt; that the draft had promoted into an inner join, which is invisible to syntax checks and obvious to anyone comparing rows.&lt;/p&gt;

&lt;p&gt;That gap defines the real shipping problem. Getting an agent to produce syntactically valid SQL is a solved loop; proving the draft returns the same answers as the query it replaces is the part that still causes incidents. Two credible engineering positions exist for closing it, and they disagree about where equivalence should be measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: shadow replay proves equivalence before any user sees it
&lt;/h2&gt;

&lt;p&gt;The first camp argues that correctness is a property of results, so you should capture real queries, replay them against a disposable copy of the data, and diff the outputs offline. Nothing reaches production until the diff is clean, which makes the risk surface small and the review artifact auditable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Capture a workload sample with normalized parameters instead of literal values.&lt;/li&gt;
&lt;li&gt;Restore a snapshot or seed a disposable schema with representative data.&lt;/li&gt;
&lt;li&gt;Execute the baseline query and the agent draft under identical session settings.&lt;/li&gt;
&lt;li&gt;Normalize both result sets, then diff them with explicit tolerance rules.&lt;/li&gt;
&lt;li&gt;Block the draft when the diff exceeds the threshold, and store the report as evidence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The strength of this position is repeatability. A failing case becomes a regression test you can rerun on every prompt change, and reviewers can read the diff without touching production. The weakness is fidelity: a snapshot goes stale, and replay never exercises concurrency, lock contention, or the query plans your real data volume produces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: a read-only canary measures equivalence on live data
&lt;/h2&gt;

&lt;p&gt;The second camp argues that snapshots lie, because correctness depends on data the snapshot does not have. Instead, route a sampled fraction of real read traffic to the agent draft on a replica, compare the paired results in flight, and alert on divergence.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Grant the drafting path a role with read-only transactions and a hard statement timeout.&lt;/li&gt;
&lt;li&gt;Sample by query fingerprint, not by user, so the same query stays in or out consistently.&lt;/li&gt;
&lt;li&gt;Run baseline and draft for the sampled requests and compare normalized rows.&lt;/li&gt;
&lt;li&gt;Emit a divergence metric per fingerprint and trip a circuit breaker on threshold breach.&lt;/li&gt;
&lt;li&gt;Promote the draft to full traffic only after a clean window you define in advance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The strength here is realism, since skew, nulls, and timezone handling all show up exactly as production presents them. The weakness is noise: partial failures, transient replica lag, and nondeterministic ordering generate diffs that are not semantic bugs, and someone has to triage them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The artifact: a replay harness you can build in an afternoon
&lt;/h2&gt;

&lt;p&gt;The following sketch is a harness, not a finished tool. It assumes read-only queries, a disposable database, and a normalization policy you have already agreed on with the people who own the numbers.&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;# replay.py - sketch; normalize() must match your domain rules
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;NORMALIZE_DECIMALS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&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="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;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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;normalize&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;out&lt;/span&gt; &lt;span class="o"&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;row&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;span class="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&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;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
        &lt;span class="n"&gt;norm&lt;/span&gt; &lt;span class="o"&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;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keys&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;v&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;norm&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="se"&gt;\x00&lt;/span&gt;&lt;span class="s"&gt;NULL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;elif&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;v&lt;/span&gt;&lt;span class="p"&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;norm&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NORMALIZE_DECIMALS&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="n"&gt;norm&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;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;out&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;norm&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;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&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;run&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;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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="mi"&gt;5000&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET TRANSACTION READ ONLY&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 LOCAL 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;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="n"&gt;params&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;normalize&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;fetchall&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;replay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseline_conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;draft_conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_diff_rows&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;report&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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fail&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;error&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="k"&gt;case&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;fid&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="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;baseline_sql&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;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseline_conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;baseline_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft_conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&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;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&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="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;delta&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;symmetric_difference&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;draft&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;bucket&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;delta&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;max_diff_rows&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;fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bucket&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row_delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;delta&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;report&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;cases&lt;/span&gt; &lt;span class="o"&gt;=&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;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&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;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cases.jsonl&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="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="nf"&gt;replay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cases&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="bp"&gt;None&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;The harness is deliberately blunt, because a clever comparator hides the disagreements you most need to see. Pass real connections in place of &lt;code&gt;None&lt;/code&gt;, keep &lt;code&gt;cases.jsonl&lt;/code&gt; under version control, and treat every &lt;code&gt;fail&lt;/code&gt; entry as a future test rather than a one-off investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalization rules are the actual engineering decision
&lt;/h2&gt;

&lt;p&gt;Most debate about replay versus canary is really a debate about what counts as equal. Row order without an &lt;code&gt;ORDER BY&lt;/code&gt; is not a contract, so sort before comparing. Floating point sums need a decimal tolerance, and timestamps need an explicit timezone policy rather than a lucky server default.&lt;/p&gt;

&lt;p&gt;Write those three rules down before you write the harness. Teams that skip this step spend weeks arguing about diffs that are artifacts of comparison, and the agent gets blamed for noise the harness created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a free server and free model access change the math
&lt;/h2&gt;

&lt;p&gt;Replay is cheap to describe and expensive to run, because it multiplies queries by two and adds a drafting call for every captured case. Running the drafting loop and the harness on the same free server keeps the captured cases, the draft outputs, and the diff report in one place, which is the difference between a ritual and a routine. MonkeyCode's free model access and free server option are the parts that make that loop affordable for a small team; check the project's current terms for limits, since free-tier quotas change and restating a number here would age badly.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision rule
&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;Shadow replay first&lt;/th&gt;
&lt;th&gt;Read-only canary first&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Workload volume&lt;/td&gt;
&lt;td&gt;High, repetitive fingerprints&lt;/td&gt;
&lt;td&gt;Low volume, long tail of one-offs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data volatility&lt;/td&gt;
&lt;td&gt;Snapshot can be refreshed nightly&lt;/td&gt;
&lt;td&gt;Business day changes the answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema churn&lt;/td&gt;
&lt;td&gt;Stable tables, few migrations&lt;/td&gt;
&lt;td&gt;Frequent migrations and renames&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-call maturity&lt;/td&gt;
&lt;td&gt;No one to triage live diffs&lt;/td&gt;
&lt;td&gt;Team already triages alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blast radius&lt;/td&gt;
&lt;td&gt;Every draft costs a review&lt;/td&gt;
&lt;td&gt;Only sampled requests are exposed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use replay when you need an auditable artifact and cannot staff live triage. Use a canary when correctness is data-dependent and your alerting is mature enough to absorb noise. Most teams I would advise run replay as the gate and a small canary as the confirmation, in that order.&lt;/p&gt;

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

&lt;p&gt;Neither approach covers writes, so do not extend this pattern to &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt; without a separate rollback design. Replay cannot reproduce lock waits, and a canary cannot prove coverage of paths that receive no traffic during the window.&lt;/p&gt;

&lt;p&gt;Skip both if you have no disposable environment or no agreed definition of equivalence. A harness built on an undefined contract produces confident reports that mean nothing, which is worse than shipping behind a manual review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the diff, not the demo
&lt;/h2&gt;

&lt;p&gt;Pick twenty captured queries, normalize them honestly, and see what your current drafts actually return. If the diffs are clean, you have a regression suite; if they are not, you have found the incident before your users did.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>testing</category>
      <category>ai</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Temperature 0 or Five Samples? A Debate Over Reproducible Agent SQL Drafts</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Tue, 20 Oct 2026 09:43:07 +0000</pubDate>
      <link>https://dev.to/dataio_4921/temperature-0-or-five-samples-a-debate-over-reproducible-agent-sql-drafts-1c2n</link>
      <guid>https://dev.to/dataio_4921/temperature-0-or-five-samples-a-debate-over-reproducible-agent-sql-drafts-1c2n</guid>
      <description>&lt;h1&gt;
  
  
  Temperature 0 or Five Samples? A Debate Over Reproducible Agent SQL Drafts
&lt;/h1&gt;

&lt;p&gt;Consider a composite Tuesday that plays out in many teams: an agent re-drafts a query it produced the day before, a reviewer approves it a second time, and the execution plan quietly flips from an index seek to a full scan. Nobody edited the prompt, the schema diff came back empty, and the provider had announced no model change. Two engineers then lose an afternoon arguing about whether the drafting loop should be pinned to one deterministic output or deliberately sampled several times and filtered.&lt;/p&gt;

&lt;p&gt;That argument deserves a decision rule instead of a taste preference, because the answer determines how you spend a free token allowance, what your CI can honestly assert, and what you are able to say at review time. Below are the two strongest positions I can defend, a reproducible harness for telling them apart on your own schema, and a rule for choosing between them. No provider benchmarks are claimed here; every number you act on should come from your own replay run.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Two credible positions, stated fairly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Position A — pin it.&lt;/strong&gt; Send one call at temperature 0, store the exact SQL text plus a normalized fingerprint, and treat any later drift as a reviewable diff rather than as noise. The argument is economic and procedural: one call per draft keeps a free allowance stretched across far more scenarios, and a single artifact is easy to attach to a pull request, annotate, and re-check after a dialect migration.&lt;/p&gt;

&lt;p&gt;The weakness is that temperature 0 is not a determinism guarantee. Provider-side batching, floating-point non-associativity in sampling, and silent model revisions all produce different outputs for identical inputs, so Position A still needs a fingerprint and a stored plan, not just a stored string.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Position B — sample it.&lt;/strong&gt; Request k candidates, run static checks on each, and keep the survivor whose plan is cheapest under a stated cost model. The argument is coverage: dialect edge cases such as quoting rules, date arithmetic, and window frames show up as variance across samples, and a single greedy decode tends to hide them behind confident-looking syntax that parses but misbehaves.&lt;/p&gt;

&lt;p&gt;The weakness is selection bias plus cost. Static checks cannot detect semantic wrongness, so filtering mostly re-ranks plausible candidates, and five samples against a free tier consume roughly five times the budget for one accepted draft.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What both sides already agree on.&lt;/strong&gt; You need a normalized fingerprint, a captured plan, and an approval record. That agreement is the part worth implementing first, which is exactly what the harness below does.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A replayable draft harness in three steps
&lt;/h2&gt;

&lt;p&gt;The goal is a script that takes a prompt, obtains candidates, records what the database actually does with them, and fails CI when behavior changes without approval. The examples below are illustrative and labeled as such: the code is runnable, but the pass criteria must be tuned to your schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Guard the budget before the next call
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# draft_harness.py (illustrative)
&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="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Budget&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="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;used&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;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completion&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="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Rough guard only: approximate token count, not provider billing.
&lt;/span&gt;        &lt;span class="n"&gt;estimate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;prompt&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;completion&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;estimate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;self&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;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft budget exhausted; stop before the next call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;estimate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Charge after every candidate, not after every prompt, otherwise a k=5 loop overshoots a free allowance by a factor of five on the first bad day. The guard is intentionally crude, and you should log the real reported usage alongside it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Parse, restrict tables, and fingerprint
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sqlglot&lt;/span&gt;

&lt;span class="n"&gt;WRITE_NODES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&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;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Insert&lt;/span&gt;&lt;span class="p"&gt;,&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;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;sqlglot&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;inspect&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;dialect&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;allowed_tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tree&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_one&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="n"&gt;dialect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tables&lt;/span&gt; &lt;span class="o"&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="n"&gt;name&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;for&lt;/span&gt; &lt;span class="n"&gt;t&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;find_all&lt;/span&gt;&lt;span class="p"&gt;(&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;exp&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="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;parses&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown_tables&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="n"&gt;tables&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;allowed_tables&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has_write&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;tree&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;node&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;WRITE_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;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;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;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dialect&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dialect&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;12&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;p&gt;Fingerprinting the normalized tree rather than the raw text absorbs whitespace and comment churn, so a changed fingerprint means a changed query shape. Reject any candidate with unknown tables before you ever execute it, and keep the dialect argument explicit so a Postgres draft is never silently validated as generic SQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Capture the plan and assert on change
&lt;/h3&gt;



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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;plan&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;db_path&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;str&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;sqlite3&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;db_path&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;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&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;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 QUERY PLAN &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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store &lt;code&gt;(prompt_hash, fingerprint, plan, approved_by)&lt;/code&gt; in a small table checked into the repo. CI then replays every stored prompt, compares fingerprints first and plans second, and fails on any unapproved difference. Plans are engine-specific, so treat a SQLite plan as a smoke signal and re-capture on the target engine before you trust it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The selection rule for Position B
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&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="o"&gt;-&amp;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;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candidates&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;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown_tables&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has_write&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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;clean&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;no candidate passed static checks&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clean&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;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&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;c&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;c&lt;/span&gt;&lt;span class="p"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using plan length as a placeholder cost model is defensible only for small fixtures; swap in your engine's cost estimate or a row-count probe before this touches anything that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. What free compute changes, and what it does not
&lt;/h2&gt;

&lt;p&gt;Free capacity moves the cost side of this debate and leaves correctness untouched. A k=5 loop that was previously unaffordable becomes affordable, while the need for fingerprints, plan capture, and human approval stays exactly where it was. The operator-supplied terms for this account describe MonkeyCode's free model access and a free server option, including a free tier of 10 million tokens; treat those as availability claims from the project's outreach rather than as measured throughput, and confirm current terms before you size a CI budget around them.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;A practical arrangement is to run the drafting and replay loop on the free server option so the harness has a stable place to live, and to spend the free model access on replayed prompts rather than on exploratory chat. Keep the harness provider-agnostic regardless: if the endpoint changes, your stored fingerprints and plans are still the source of truth.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Choose&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;Regulated schema, monthly migrations, reviewer signs off&lt;/td&gt;
&lt;td&gt;Position A: temperature 0 plus stored fingerprint&lt;/td&gt;
&lt;td&gt;One artifact per prompt; drift becomes a visible diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema changes weekly, multiple SQL dialects in play&lt;/td&gt;
&lt;td&gt;Position B: k=5 with static filters&lt;/td&gt;
&lt;td&gt;Variance surfaces quoting and date-arithmetic edge cases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free allowance is the hard constraint&lt;/td&gt;
&lt;td&gt;Position A, or k=3 with early exit&lt;/td&gt;
&lt;td&gt;Candidate count is the dominant cost multiplier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency-sensitive interactive tooling&lt;/td&gt;
&lt;td&gt;Position A&lt;/td&gt;
&lt;td&gt;Five calls per keystroke-adjacent action is untenable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No staging replica and no approval process&lt;/td&gt;
&lt;td&gt;Neither, yet&lt;/td&gt;
&lt;td&gt;Without a replay target the harness asserts nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;This harness validates syntax, table access, and one engine's plan shape; it cannot tell you that a JOIN fans out rows or that a filter uses the wrong timezone. Teams without a staging replica, a fixture snapshot, or a reviewer who can approve a fingerprint should not build CI around fingerprints at all, because they will be freezing noise.&lt;/p&gt;

&lt;p&gt;The approach also ages poorly against dialect churn: parsers lag real engines, so a query that fails to parse may still be correct, and your harness should record parse failures as warnings rather than hard blocks during upgrades. Finally, the budget guard is an estimate, and any team spending real money on drafts should log provider-reported usage next to it.&lt;/p&gt;

&lt;p&gt;If you already run a replay suite, the cheapest next step is to take your five most-edited prompts, record fingerprints and plans today, and let CI tell you how often they actually drift before you commit to either position.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>sql</category>
      <category>testing</category>
      <category>database</category>
    </item>
    <item>
      <title>Schema-Only or Row Samples: A Debate Over What an Agent Sees Before Writing SQL</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Mon, 19 Oct 2026 08:25:29 +0000</pubDate>
      <link>https://dev.to/dataio_4921/schema-only-or-row-samples-a-debate-over-what-an-agent-sees-before-writing-sql-47md</link>
      <guid>https://dev.to/dataio_4921/schema-only-or-row-samples-a-debate-over-what-an-agent-sees-before-writing-sql-47md</guid>
      <description>&lt;h1&gt;
  
  
  Schema-Only or Row Samples: A Debate Over What an Agent Sees Before Writing SQL
&lt;/h1&gt;

&lt;h2&gt;
  
  
  A review queue where two reviewers disagree
&lt;/h2&gt;

&lt;p&gt;The scenario repeats in most teams that let an agent draft SQL: a query passes review, runs in staging, and returns something plausible that is still wrong. One reviewer blames the prompt for hiding the data, while another blames the habit of pasting production rows into a model context. Both are describing the same defect from opposite ends of the pipeline, and neither has a measurement that settles the argument.&lt;/p&gt;

&lt;p&gt;An illustrative example: the request is "active customers in the EU last quarter." Schema-only context produces &lt;code&gt;WHERE status = 'active'&lt;/code&gt;, but the column actually stores single characters such as &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;I&lt;/code&gt;, and &lt;code&gt;P&lt;/code&gt;, so the filter matches zero rows. Add five sample rows to the context and the model copies the encoding, but now a customer email and a partial address have also entered the prompt. The disagreement is real, and it is not resolved by intuition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: the schema is the contract
&lt;/h2&gt;

&lt;p&gt;The first position holds that DDL, comments, and constraint definitions are the only context an agent should receive, because they are stable, versionable, and reviewable. A schema can be diffed in a pull request, pinned to a commit, and replayed later, which makes agent output reproducible in a way that a row dump never is. Proponents also argue that if the model needs literal values to answer correctly, the missing knowledge belongs in a governed lookup table rather than in a prompt.&lt;/p&gt;

&lt;p&gt;The privacy argument is equally concrete. Sample rows are the easiest way to move regulated data into a third-party inference call, and most review processes have no record of which rows were sent. Under this view, schema-only grounding is not a limitation but a control, and any accuracy loss is a signal that documentation or reference data needs work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: distributions are part of the contract
&lt;/h2&gt;

&lt;p&gt;The second position holds that many conventions live in values rather than in DDL, and no amount of column comments recovers them. Encoded status flags, soft-delete sentinels, negative quantities for refunds, amounts stored in minor units, timestamps in UTC with a local offset column, and trailing spaces from legacy imports are all invisible to a model reading &lt;code&gt;PRAGMA table_info&lt;/code&gt;. Reviewers who have debugged these failures tend to stop trusting schema-only prompts quickly.&lt;/p&gt;

&lt;p&gt;This camp usually accepts redaction rather than rejection. If samples are generated through a masking function that preserves shape and cardinality while replacing sensitive literals, the accuracy benefit survives and the exposure narrows. The counterargument, of course, is that a masking function is code, and code needs its own tests before anyone trusts it as a boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the disagreement actually sits
&lt;/h2&gt;

&lt;p&gt;Both positions agree that the model needs the information; they disagree about where that information should live and who owns it. Schema-only advocates want conventions promoted into documented reference data, while sample advocates want conventions observed empirically and masked. A team can hold both views at once by treating samples as a diagnostic tool and reference tables as the production fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reproducible harness
&lt;/h2&gt;

&lt;p&gt;The only way out of the argument is a small, boring measurement, so the workflow below scores both contexts against the same frozen evaluation set. Everything runs locally in SQLite, and the harness template is unexecuted here, so treat the numbers you get as yours rather than mine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: freeze a 30-item evaluation set
&lt;/h3&gt;

&lt;p&gt;Each item stores the question, a gold query, and the expected result, never the model output. Thirty items is deliberately small: it is enough to expose value-convention failures and cheap enough to re-run after every prompt change.&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="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="s2"&gt;"eu-active-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Active customers in the EU last quarter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gold_sql"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SELECT COUNT(*) FROM customers WHERE status='A' AND region='EU' AND last_order_at &amp;gt;= '2026-07-01'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"expected_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;filled_by_harness&amp;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;h3&gt;
  
  
  Step 2: build two context builders
&lt;/h3&gt;

&lt;p&gt;One builder emits DDL only; the other emits DDL plus a small, masked sample per table. Sample with an explicit seed so the context is reproducible across runs and reviewers.&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;# harness.py - template, not executed in this article
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;MASK&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;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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="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;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&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="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;v&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;schema_context&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;tables&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="se"&gt;\n&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;conn&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;SELECT sql FROM sqlite_master WHERE name=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&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="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;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tables&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;sample_context&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;tables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;per_table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="o"&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;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&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;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;SELECT * FROM &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ORDER BY rowid LIMIT &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;per_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="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&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;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&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;description&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="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;MASK&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;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;v&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;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cols&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="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;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="n"&gt;blocks&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;-- sample: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rows&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="nf"&gt;schema_context&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;tables&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="se"&gt;\n&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="se"&gt;\n&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;blocks&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: score on result sets, not strings
&lt;/h3&gt;

&lt;p&gt;String comparison punishes formatting and rewards nothing useful, so hash the sorted result rows instead. That single choice removes most reviewer disagreement about whether a query "looks right."&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;result_hash&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;sql&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;conn&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="nf"&gt;fetchall&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;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="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;map&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;rows&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;draft_fn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="o"&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;it&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&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;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;draft_fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;passed&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;result_hash&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;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_hash&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="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;passed&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="bp"&gt;False&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;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passed&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;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: run both arms at k=3 and log failures by cause
&lt;/h3&gt;

&lt;p&gt;Run every item three times per context, because a single pass hides flakiness that reviewers will otherwise discover in staging. Label each failure as &lt;code&gt;encoding&lt;/code&gt;, &lt;code&gt;null_handling&lt;/code&gt;, &lt;code&gt;unit_scale&lt;/code&gt;, or &lt;code&gt;syntax&lt;/code&gt;, and keep the labels in the same repository as the eval set.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python harness.py &lt;span class="nt"&gt;--context&lt;/span&gt; schema &lt;span class="nt"&gt;--k&lt;/span&gt; 3 &lt;span class="nt"&gt;--out&lt;/span&gt; results/schema.jsonl
python harness.py &lt;span class="nt"&gt;--context&lt;/span&gt; samples &lt;span class="nt"&gt;--k&lt;/span&gt; 3 &lt;span class="nt"&gt;--out&lt;/span&gt; results/samples.jsonl
python report.py results/&lt;span class="k"&gt;*&lt;/span&gt;.jsonl &lt;span class="nt"&gt;--group-by&lt;/span&gt; cause
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: decide with a rule, not a preference
&lt;/h3&gt;

&lt;p&gt;Read the failure labels first and the aggregate accuracy second, because a context that wins by two points on syntax while losing every encoding case is the wrong default for a reporting workload. Then apply the rule in the next section.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Observed signal&lt;/th&gt;
&lt;th&gt;Schema-only is the right default&lt;/th&gt;
&lt;th&gt;Row samples are the right default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Failures concentrated in &lt;code&gt;encoding&lt;/code&gt; or &lt;code&gt;unit_scale&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failures concentrated in &lt;code&gt;syntax&lt;/code&gt; or ambiguous joins&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No masking pipeline, regulated data&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stable reference tables exist for coded columns&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legacy tables with undocumented sentinels&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&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;Start with schema-only and run the harness once; if the failure labels contain no value-convention causes, stop and spend the effort on documentation instead. If at least a third of failures are &lt;code&gt;encoding&lt;/code&gt; or &lt;code&gt;unit_scale&lt;/code&gt;, add masked samples for the affected tables only, then re-run the same eval set. Escalate per table and per failure cause, never per project, so the prompt stays as narrow as the evidence allows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the comparison without a budget fight
&lt;/h2&gt;

&lt;p&gt;This matrix multiplies calls: thirty items, three repetitions, two contexts, plus re-runs after each prompt edit. MonkeyCode's free model access removes the cost objection for the drafting side, and the free server option covers running the harness and SQLite fixtures next to the data. Confirm the current token allowance and server terms on the project page before you plan around them, since promotions change and this article does not treat them as permanent.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;I have not audited the MonkeyCode repository or its license independently, so check the license terms yourself before adopting it as a dependency, and treat the free tier as an evaluation path rather than a capacity guarantee. The harness itself is plain Python and runs anywhere, which keeps the measurement portable if your tooling changes.&lt;/p&gt;

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

&lt;p&gt;Thirty items is a smoke test, not a statistically sound benchmark, and it will miss rare conventions that appear once a quarter. Masking that preserves shape can still leak information through cardinality or timestamp patterns, and no sample context should be used where a proper data-processing agreement is absent. Teams without result-set comparison in their pipeline should build that first, because string-diff review will make the harness output unreadable.&lt;/p&gt;

&lt;p&gt;If this eval-set format saves you a review cycle, the most useful thing to send back is the failure taxonomy you ended up with, since that is the part most teams rebuild from scratch.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>ai</category>
      <category>database</category>
      <category>testing</category>
    </item>
    <item>
      <title>Transpile or Re-Ask: Fixing Dialect Breaks in Agent-Written SQL</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Sat, 17 Oct 2026 06:10:25 +0000</pubDate>
      <link>https://dev.to/dataio_4921/transpile-or-re-ask-fixing-dialect-breaks-in-agent-written-sql-2aa5</link>
      <guid>https://dev.to/dataio_4921/transpile-or-re-ask-fixing-dialect-breaks-in-agent-written-sql-2aa5</guid>
      <description>&lt;p&gt;A quarterly reporting job moved from Postgres to MySQL last spring, and every agent-drafted query in the pipeline kept parsing fine while returning the wrong rows. I have already described one loud failure on this account, an AI-generated query that locked a production table, but this quieter class of break costs more engineering hours. The model never sees the target engine, so it keeps writing &lt;code&gt;date_trunc('month', ...)&lt;/code&gt; and &lt;code&gt;ON CONFLICT&lt;/code&gt; long after those idioms stop meaning anything. Below is the debate I ran against my own fixture set, plus the decision rule I now apply before touching a prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where agent SQL actually breaks
&lt;/h2&gt;

&lt;p&gt;Dialect drift looks like a syntax problem and usually is not one. The statements that break loudly are cheap to fix, because the parser refuses them and the loop simply retries with the error text. The expensive ones parse on both engines and disagree about results: &lt;code&gt;ILIKE&lt;/code&gt; against a case-insensitive collation, &lt;code&gt;LIMIT&lt;/code&gt; before &lt;code&gt;OFFSET&lt;/code&gt;, integer division, and &lt;code&gt;jsonb&lt;/code&gt; arrow operators with no direct MySQL equivalent. A query that runs cleanly on both engines and returns 41 rows instead of 44 will pass every check that only looks for exceptions.&lt;/p&gt;

&lt;p&gt;That asymmetry is why the choice between deterministic transpilation and a model re-ask is not a tooling preference. The two approaches fail in different places, and the decision rule has to be driven by which failure you can detect cheaply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: transpile deterministically, then execute
&lt;/h2&gt;

&lt;p&gt;The first position says dialect conversion is a parsing problem and should stay one. &lt;code&gt;sqlglot&lt;/code&gt; reads the Postgres AST and writes MySQL, and the same input always produces the same output, which makes the whole step reviewable in a diff. It is also fast enough to run on every draft, so repair cost stays near zero for the common cases.&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;# dialect_probe.py — illustrative; adjust identifiers to your own schema
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlglot&lt;/span&gt;

&lt;span class="n"&gt;SOURCE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TARGET&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mysql&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;transpile&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Deterministic: same input, same output, diffable in code review.&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;sqlglot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpile&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="n"&gt;SOURCE&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="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pretty&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="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="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;src&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 user_id, date_trunc(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;month&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, created_at) AS m, count(*)
        FROM orders WHERE note ILIKE &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%refund%&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; GROUP BY 1, 2
    &lt;/span&gt;&lt;span class="sh"&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;transpile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The weakness is equally clear: transpilers preserve the semantics they can express and guess at the rest. Window frame defaults, string concatenation and null ordering under &lt;code&gt;ORDER BY&lt;/code&gt; are all places where a translated statement is valid, executable, and quietly wrong. Position A therefore only holds up when it is paired with execution against the target engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: re-ask the model with the target dialect in the prompt
&lt;/h2&gt;

&lt;p&gt;The second position says the model already knows both dialects and only needs to be told which one matters. Free model access makes the extra call cheap enough to sit inside the repair loop, which is exactly where I use it: MonkeyCode's free model access covers the re-ask step, and the free server option gives me a disposable target engine to execute against. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The allowance and limits on that access change over time, so check the current account terms before you budget a loop that runs on every commit.&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;# reask.py — unexecuted example; swap the client if your endpoint differs
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Rewrite this {src} query for {dst}.
Keep the output column names and their order identical.
Do not change filtering semantics. Return SQL only.

{sql}
&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;reask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&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;src&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="n"&gt;dst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mysql&lt;/span&gt;&lt;span class="sh"&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&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;LLM_MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;temperature&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;messages&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;role&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;user&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;content&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dst&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;sql&lt;/span&gt;&lt;span class="p"&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The re-ask handles the cases the transpiler guesses at, including collation-sensitive comparisons and &lt;code&gt;ON DUPLICATE KEY UPDATE&lt;/code&gt; conversions that need a rewrite rather than a symbol swap. Its weakness is the inverse: it is non-deterministic, it can silently drop a &lt;code&gt;WHERE&lt;/code&gt; clause, and a plausible rewrite is indistinguishable from a correct one until you execute it. Position B without execution is a confident guess with extra steps.&lt;/p&gt;

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

&lt;p&gt;Neither position survives alone, and the split is not "simple queries versus complex queries". It is "can I detect the failure with a fixture run", and the fixture is the same artifact either way.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Observation after the step&lt;/th&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transpile raises a parse error&lt;/td&gt;
&lt;td&gt;re-ask, then execute&lt;/td&gt;
&lt;td&gt;grammar-level gap; the model often knows the idiom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transpile parses, target engine errors&lt;/td&gt;
&lt;td&gt;re-ask, then execute&lt;/td&gt;
&lt;td&gt;valid AST, unsupported function or syntax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executes, column names or order differ&lt;/td&gt;
&lt;td&gt;reject the draft&lt;/td&gt;
&lt;td&gt;breaks downstream consumers silently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executes, row count differs&lt;/td&gt;
&lt;td&gt;re-ask, then execute&lt;/td&gt;
&lt;td&gt;semantic difference; never merge on syntax alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executes, counts match, checksum differs&lt;/td&gt;
&lt;td&gt;hand-write, add fixture&lt;/td&gt;
&lt;td&gt;aggregate or rounding drift the model will not catch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executes, counts and checksum match&lt;/td&gt;
&lt;td&gt;accept and cache the rewrite&lt;/td&gt;
&lt;td&gt;stop spending calls on a solved pair&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule underneath the table is straightforward: transpile first because it is free and deterministic, and escalate to a re-ask only when execution on the target engine disagrees with the source. Cache accepted rewrites keyed by normalised SQL text, or the loop re-derives the same conversion on every run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the harness in five steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Capture the source result once, on the source engine, with a read-only role. Store column names, row count, and a checksum over an ordered numeric column.&lt;/li&gt;
&lt;li&gt;Transpile the statement and record whether the parse succeeded before you run anything at all.&lt;/li&gt;
&lt;li&gt;Execute the output against a disposable target engine — I load the free server option with a schema-only dump plus a small fixture, run the probe, then destroy the instance.&lt;/li&gt;
&lt;li&gt;Escalate only the disagreements to the re-ask function, and cap the loop at two attempts so a bad prompt cannot burn a budget overnight.&lt;/li&gt;
&lt;li&gt;Promote every query that needed a hand fix into the fixture set, so the regression is covered the next time the agent proposes that shape.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Row-count equality is necessary and not sufficient, and the checksum only protects the columns you chose to cover. Fixtures that omit NULLs, duplicate keys, timezone boundaries and unicode will pass a rewrite that fails on the first real month-end load. Temperatures above zero make the re-ask step non-reproducible, so keep the repair path at zero and treat accepted rewrites as code rather than as prompt state. Skip this workflow if you have no fixture data, no disposable environment, or a policy that forbids sending schema text to a hosted model — in those cases a deterministic transpiler plus human review is the honest option.&lt;/p&gt;

&lt;p&gt;Keep the model out of the decision about whether the rewrite is correct, and let execution decide. If you want to try the loop without paying for a second engine or a model bill, start with one fixture query against the free server option and let the table above route it.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>ai</category>
      <category>database</category>
      <category>testing</category>
    </item>
    <item>
      <title>Hosted Free Models or a Free Server: Where an Agent's SQL Drafting Loop Should Run</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Fri, 16 Oct 2026 05:07:50 +0000</pubDate>
      <link>https://dev.to/dataio_4921/hosted-free-models-or-a-free-server-where-an-agents-sql-drafting-loop-should-run-51hp</link>
      <guid>https://dev.to/dataio_4921/hosted-free-models-or-a-free-server-where-an-agents-sql-drafting-loop-should-run-51hp</guid>
      <description>&lt;p&gt;Last month I watched an agent spend forty-one attempts drafting one reporting query, and not one of those attempts was slow enough to notice on its own. The loop only looked expensive when I plotted attempts against wall time and prompt tokens per retry. What surprised me was that the placement of the drafting step, not the model quality, explained most of the variance. That observation turned into an argument I keep having with a colleague, so this article states both sides properly and then writes down a rule for choosing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scene, and why placement matters
&lt;/h2&gt;

&lt;p&gt;The task was ordinary: an agent reads a schema summary, drafts SQL, runs it read-only, reads the error, and revises. Each revision resends the growing conversation, so attempt twelve carries far more prompt tokens than attempt one. When the drafting step runs behind a shared hosted endpoint, retry latency and quota pressure are somebody else's scheduling problem. When it runs on a machine you control, latency is your problem but the retry storm is yours to shape.&lt;/p&gt;

&lt;p&gt;I am not going to pretend I have a benchmark that settles this for everyone, because I have not run one against MonkeyCode's endpoints myself. What I can give you is a reproducible harness, the four signals it collects, and a decision rule that a team can argue about using its own numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: borrow the hosted free model layer
&lt;/h2&gt;

&lt;p&gt;The first position is that an agent's drafting loop should default to a hosted free tier, because the cost of getting started is minutes rather than days. You authenticate, point the loop at an endpoint, and the schema summarization, prompt assembly, and retry logic stay exactly where they were. For a solo developer validating whether an agent can even reach a usable query, this is the shortest path to evidence.&lt;/p&gt;

&lt;p&gt;A second argument is operational: nobody has to patch, restart, or monitor anything. If the endpoint degrades, the retry logic you already wrote absorbs it, and you can rerun the same task suite tomorrow. The counterargument, which I happen to hold, is that a shared free tier gives you no control over the retry ceiling, so a runaway loop is invisible until it stops working.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;MonkeyCode is relevant here because it advertises free model access and a free server option, which is exactly the fork this article is about. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I am describing it as a user of the free tier, not as a benchmark authority, and I have treated the free token grant (the operator states ten million tokens at signup) and the free server as current-at-writing claims. Verify both on the project's own pages before you build a plan around them, because free-tier terms change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Position B: own the drafting step on a free server
&lt;/h2&gt;

&lt;p&gt;The second position is that the drafting step belongs on a machine you control, even if that machine arrives free. Once the model runs next to the database, the schema summary never leaves your boundary, and you can cap concurrency, retries, and context growth yourself. For regulated schemas or for loops that touch production metadata, that boundary is often the whole argument.&lt;/p&gt;

&lt;p&gt;Ownership also makes the failure mode legible. When a retry storm starts, you see memory, queue depth, and per-request latency on a dashboard you built, not in someone else's status page. The cost is real, though: someone must own the image, the model weights, the restart policy, and the capacity ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The artifact: a placement harness you can actually run
&lt;/h2&gt;

&lt;p&gt;Here is a proposed harness, not a validated benchmark. It writes one JSONL row per drafting attempt so that placement decisions rest on your data rather than on my opinions. The token counter is a cheap proxy and the backend is a dry-run echo, so nothing here depends on invented provider numbers.&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;# bench_placement.py — proposed harness; I have not executed this against MonkeyCode.
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&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="n"&gt;statistics&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;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;asdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dataclass&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;Attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;task_id&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;placement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;      &lt;span class="c1"&gt;# "hosted-free" | "free-server"
&lt;/span&gt;    &lt;span class="n"&gt;attempt_no&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;prompt_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;completion_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;wall_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;error_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="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;sql_sha256&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="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;estimate_tokens&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;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Proxy only. Swap in the tokenizer of whichever model you actually call.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&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="nf"&gt;len&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="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;4&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;draft_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&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;history&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;dry_run&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;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="nb"&gt;str&lt;/span&gt; &lt;span class="o"&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="n"&gt;dry_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&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;SELECT 1 /* &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="si"&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="bp"&gt;None&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;Wire your real backend here; keep the return contract.&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;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tasks&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;placements&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;max_attempts&lt;/span&gt;&lt;span class="p"&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;out&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="bp"&gt;None&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="n"&gt;Attempt&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;for&lt;/span&gt; &lt;span class="n"&gt;placement&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;placements&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;task&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;history&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;for&lt;/span&gt; &lt;span class="n"&gt;n&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_attempts&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="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&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;history&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                &lt;span class="n"&gt;t0&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="k"&gt;try&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;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;draft_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dry_run&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;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="c1"&gt;# classify, never swallow silently
&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;err&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="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="n"&gt;wall_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;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;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;placement&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;placement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt_no&lt;/span&gt;&lt;span class="o"&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;prompt_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;estimate_tokens&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;completion_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;estimate_tokens&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;wall_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;wall_ms&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="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="n"&gt;error_class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;sql_sha256&lt;/span&gt;&lt;span class="o"&gt;=&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;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;12&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;sql&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&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;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;break&lt;/span&gt;
                &lt;span class="n"&gt;history&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;attempt &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; failed: &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;with&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;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&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;fh&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;row&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;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&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="nf"&gt;asdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&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="se"&gt;\n&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;placement&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;placements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lat&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wall_ms&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;rows&lt;/span&gt; &lt;span class="k"&gt;if&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;placement&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;placement&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;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;p95&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&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;lat&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="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.95&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;lat&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;placement&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: n=&lt;/span&gt;&lt;span class="si"&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;lat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; p50=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;ms p95=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p95&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;ms&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;__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;ap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tasks.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--placements&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hosted-free,free-server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&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-attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&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;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;placement.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&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;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&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;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;line&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;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&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;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tasks&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="n"&gt;placements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_attempts&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="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Steps to run the comparison
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Write twenty tasks into &lt;code&gt;tasks.txt&lt;/code&gt;, mixing trivial lookups with two or three joins, and keep the same file for both placements.&lt;/li&gt;
&lt;li&gt;Wire your backend into &lt;code&gt;draft_sql&lt;/code&gt;, keeping the return contract so the JSONL schema does not change between runs.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;python bench_placement.py --placements hosted-free --out hosted.jsonl&lt;/code&gt;, then repeat with &lt;code&gt;--placements free-server&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Compare attempts per task and p95 wall time, not average latency, because retry storms hide in the tail.&lt;/li&gt;
&lt;li&gt;Gate every generated statement before execution, regardless of placement, using a read-only transaction with a timeout.
&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;-- Cheap guard: never let a draft touch data until it has been planned 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;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;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&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;'3s'&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="cm"&gt;/* generated statement goes here */&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;
  
  
  The signals that actually decide it
&lt;/h2&gt;

&lt;p&gt;Four signals from the JSONL file carry most of the decision weight. Attempts per task tells you how much context resending you are paying for. The ratio of prompt to completion tokens tells you whether your loop is dominated by conversation history rather than by generation. Tail latency tells you how badly a shared tier handles your retry pattern. Sensitivity class of the schema tells you whether the boundary argument overrides every number above.&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;Hosted free tier wins when&lt;/th&gt;
&lt;th&gt;Free server wins when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Attempts per task&lt;/td&gt;
&lt;td&gt;Under roughly four&lt;/td&gt;
&lt;td&gt;Above eight, and you can cap concurrency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema sensitivity&lt;/td&gt;
&lt;td&gt;Public, fixture, or synthetic only&lt;/td&gt;
&lt;td&gt;Internal or regulated metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tail latency (p95)&lt;/td&gt;
&lt;td&gt;You can tolerate queueing&lt;/td&gt;
&lt;td&gt;You need a stable ceiling you control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ops capacity&lt;/td&gt;
&lt;td&gt;Nobody available to own a box&lt;/td&gt;
&lt;td&gt;Someone owns restarts and capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Failure analysis on both sides
&lt;/h2&gt;

&lt;p&gt;On the hosted side, the failure I see most is silent quota exhaustion mid-loop: the agent keeps retrying, the endpoint keeps refusing, and the loop reports a schema error. Instrument error classes separately or you will misread a rate limit as a bad join. The second is context bloat, where attempt fifteen carries so much history that the useful instruction is a rounding error.&lt;/p&gt;

&lt;p&gt;On the server side, the failure is usually resource exhaustion rather than refusal. A warm model plus three concurrent agents can exceed memory, and the process dies without a useful message. Cold starts also distort early latency, so discard the first task in each run or you will compare startup cost with steady state. Neither placement fixes an incorrect query, and that is the failure people most want placement to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision rule I would defend
&lt;/h2&gt;

&lt;p&gt;Start every new agent loop on the hosted free tier, with the harness running, because evidence beats architecture debate. Move the drafting step to a free server once attempts per task cross eight, or once the schema becomes sensitive, or once tail latency is the constraint users complain about. Keep the harness in version control either way, since the numbers that justified the move are the numbers that will justify moving back.&lt;/p&gt;

&lt;p&gt;a decision like this is only as good as the alternatives you actually tried, so if you want to test both placements on the same task suite, MonkeyCode's free model access and free server option give you two runnable configurations without a procurement conversation.&lt;/p&gt;

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

&lt;p&gt;This harness measures placement, not correctness, and the token counter is a proxy rather than a real tokenizer, so treat the numbers as relative. The free token grant and the free server are operator-stated availability claims that I have not independently benchmarked, and self-hosting on a free server still requires someone to own the image. Skip the debate entirely if you need deterministic sub-second p99 latency or if your compliance rules forbid third-party inference, because in both cases the decision was made before the benchmark ran. Also skip it if your agent has never produced a query a reviewer approved, since placement will not rescue a loop that has not yet proven it can work.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>sql</category>
    </item>
    <item>
      <title>Free Draft Volume or Paid First Passes: A Debate for Agent SQL Budgets</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Thu, 15 Oct 2026 11:22:56 +0000</pubDate>
      <link>https://dev.to/dataio_4921/free-draft-volume-or-paid-first-passes-a-debate-for-agent-sql-budgets-1n2p</link>
      <guid>https://dev.to/dataio_4921/free-draft-volume-or-paid-first-passes-a-debate-for-agent-sql-budgets-1n2p</guid>
      <description>&lt;p&gt;A nightly analytics job in my lab reproduction turns plain-English questions into SQL, and every draft has to survive a rehearsal database before a human sees it. After three weeks of running that loop, the argument inside the team was never about prompt design. It was about where the budget should go: more drafts on a cheap tier, or fewer, better drafts on an expensive one.&lt;/p&gt;

&lt;p&gt;The numbers below are not a product benchmark, because I cannot measure your schema or your tasks. Everything here is a harness you run yourself, and the only claims I make are about the method and the decision rule it produces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: draft breadth is the bottleneck
&lt;/h2&gt;

&lt;p&gt;The first position says generation is cheap relative to review, so a loop should buy as many independent drafts as it can afford. If a task has a 12% chance of a correct first attempt, ten attempts give you roughly a 72% chance that at least one passes, and that arithmetic is the whole argument. Breadth turns a lucky single shot into an engineering process, but it only works when each attempt costs almost nothing.&lt;/p&gt;

&lt;p&gt;This is where free model access and a free server option stop being a discount and become a technical argument. A free draft tier plus a free rehearsal host means the marginal cost of attempt eleven is zero, so your accept@k curve is bounded by wall-clock time rather than by an invoice. MonkeyCode, which its maintainers present as an open-source project, advertises that combination: free model access and a free server option, with a token allowance quoted to me as ten million tokens at the time of writing.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;Treat any published quota as a snapshot rather than a contract, because quotas, model versions, and availability change without negotiation. Whatever free capacity you have, pin it in your logs by model identifier and response hash, so a silent model swap appears as a step change in your accept rate instead of as a mystery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: verification is the bottleneck, so cheap drafts create debt
&lt;/h2&gt;

&lt;p&gt;The second position is less flattering. It says the real constraint is how many changes a human can verify per day, and that cheap drafts simply move cost from the invoice to the reviewer. A draft that fails execution is free to discard, but a draft that parses, executes, and returns the wrong answer is expensive, because only a person notices it.&lt;/p&gt;

&lt;p&gt;Under this reading, cost per token is a vanity metric and cost per accepted draft is a distraction. The number that matters is the total cost of one merged change, including the minutes a reviewer spends rejecting plausible SQL. Breadth makes this worse in a specific way: it multiplies the number of semantically wrong queries that pass a structural verifier and reach a human queue.&lt;/p&gt;

&lt;p&gt;The counter-evidence for Position A is easy to find in your own logs, and it looks like a distribution of failure reasons. If failures cluster at parse and execution, the verifier absorbs them and breadth is nearly free. If failures cluster at "valid SQL, wrong grain, wrong join, wrong filter", then breadth is multiplying review work rather than reducing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The artifact: a harness that measures both positions
&lt;/h2&gt;

&lt;p&gt;The harness below is illustrative and was not executed for this article; run it against your own task set and your own endpoints. It needs Python with the &lt;code&gt;sqlglot&lt;/code&gt; and &lt;code&gt;requests&lt;/code&gt; packages, plus Docker for the rehearsal database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 - freeze a task set with an oracle.&lt;/strong&gt; Write 25 tasks and give each one a machine-checkable expectation, such as an exact row count or a checksum over the sorted result. Without an oracle you cannot separate execution failures from semantic failures, and the whole decision collapses back into opinion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 - provision a disposable rehearsal database.&lt;/strong&gt; Start a throwaway Postgres container, load schema and fixtures, and run every draft inside &lt;code&gt;BEGIN ... ROLLBACK&lt;/code&gt; so one fixture serves all attempts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; rehearsal &lt;span class="nt"&gt;-p&lt;/span&gt; 55432:5432 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres postgres:16-alpine
psql postgresql://postgres:postgres@127.0.0.1:55432/postgres &lt;span class="nt"&gt;-f&lt;/span&gt; schema.sql &lt;span class="nt"&gt;-f&lt;/span&gt; fixtures.sql
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;REHEARSAL_DSN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresql://postgres:postgres@127.0.0.1:55432/postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3 - describe your backends in one config file.&lt;/strong&gt; A free tier and a paid endpoint usually speak the same chat-completions shape, so a single function can drive both. Replace the identifiers and prices with your own; the values below are placeholders, not product facts.&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"free-hosted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;host&amp;gt;/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key_env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FREE_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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;model-id&amp;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;"usd_per_mtok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"paid-endpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;host&amp;gt;/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key_env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAID_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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;model-id&amp;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;"usd_per_mtok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&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;Set &lt;code&gt;usd_per_mtok&lt;/code&gt; from your own invoice for the paid endpoint, and keep it at zero only after you have confirmed the free tier's terms and quota for your usage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 - draft, verify, and log one JSONL row per attempt.&lt;/strong&gt; The verifier does three checks in order: parse to a single statement, execute under a short statement timeout, then compare against the task oracle.&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;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;subprocess&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="n"&gt;sqlglot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_card&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;attempt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;t0&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;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&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;Authorization&lt;/span&gt;&lt;span class="sh"&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;Bearer &lt;/span&gt;&lt;span class="si"&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="n"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;key_env&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]]&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;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;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&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;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&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;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;system&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;content&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_card&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;role&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;user&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;content&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="p"&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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&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="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&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;json&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;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choices&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;body&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;usage&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;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify&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;task&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="n"&gt;tree&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;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;tree&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="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shape&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="n"&gt;wrapped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BEGIN; 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="s"&gt;; &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="nf"&gt;rstrip&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;; ROLLBACK;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&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;psql&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="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-v&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;ON_ERROR_STOP=1&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;-Atc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wrapped&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;capture_output&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;text&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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&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;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&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;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="sh"&gt;"&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;stderr&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="mi"&gt;160&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&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;stdout&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;splitlines&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;line&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="nf"&gt;len&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="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_rows&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;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&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;ok&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;oracle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5 - reduce attempts into one comparison table.&lt;/strong&gt; Group by backend and compute accept rate, cost per accepted draft, and p95 latency, so both positions can be argued with numbers instead of adjectives.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize&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;stats&lt;/span&gt; &lt;span class="o"&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;row&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;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend&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;drafts&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.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;lat&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;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;drafts&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="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&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;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usd&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;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lat&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency_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;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accept_rate&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;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&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;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;drafts&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usd_per_accepted&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;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usd&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;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p95_latency_s&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="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.95&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;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lat&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="mi"&gt;1&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Run both backends over the same 25 tasks and apply one rule, written as a formula you can defend in a review.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tco_per_merged_change =
    (draft_usd + verify_usd + review_minutes * review_usd_per_min) / accepted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;review_minutes&lt;/code&gt; comes from your pull-request history rather than from a guess, and in most teams it is the largest term in the equation. Then apply the branches below to pick a configuration instead of an ideology.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Harness signal&lt;/th&gt;
&lt;th&gt;Cheapest correct choice&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free tier's accept rate is within ~30% of paid, failures at parse/exec&lt;/td&gt;
&lt;td&gt;Free draft tier, 10-20 attempts&lt;/td&gt;
&lt;td&gt;The verifier absorbs failures, so breadth stays cheap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Over a third of failures are "valid SQL, wrong answer"&lt;/td&gt;
&lt;td&gt;Paid endpoint, 1-3 attempts, plus an oracle&lt;/td&gt;
&lt;td&gt;Review minutes dominate; breadth multiplies debt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every task needs fresh fixtures&lt;/td&gt;
&lt;td&gt;Free server for the rehearsal database&lt;/td&gt;
&lt;td&gt;Throwaway rehearsal is the cheapest part of the loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema under data-residency or client-contract limits&lt;/td&gt;
&lt;td&gt;Self-hosted only&lt;/td&gt;
&lt;td&gt;Any hosted free tier is still a third-party processor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule is not "free beats paid" or the reverse. Breadth wins when your verifier is strong and your oracle is per-task; precision wins when your verifier is structural and your oracle is a human in a queue.&lt;/p&gt;

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

&lt;p&gt;Skip the breadth-first loop if you cannot write a per-task oracle, because you will be multiplying exactly the failures you are unable to detect. Skip any hosted free tier if schema cards or sample rows fall under a data-processing agreement that does not cover that provider, and check the terms before the first request rather than after the first audit.&lt;/p&gt;

&lt;p&gt;Do not plan a budget around a published quota, and do not assume a free tier is permanent. Re-run the harness after any provider change, because your accept-rate history is the only early warning you will get.&lt;/p&gt;

&lt;p&gt;This workflow is also a poor fit for one-shot schema migrations. There, review is the only gate you have, and ten cheap drafts simply hand a reviewer ten ways to be wrong.&lt;/p&gt;

&lt;p&gt;If you want to test the breadth-first branch without paying per draft, the free tier is the cheapest way to get the harness above running - measure it with the same oracle and the same decision rule before you commit to it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>sql</category>
      <category>testing</category>
      <category>database</category>
    </item>
    <item>
      <title>Masked Extracts or Local Draft Models: A Debate for Agent SQL Placement</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Tue, 13 Oct 2026 01:04:27 +0000</pubDate>
      <link>https://dev.to/dataio_4921/masked-extracts-or-local-draft-models-a-debate-for-agent-sql-placement-28o8</link>
      <guid>https://dev.to/dataio_4921/masked-extracts-or-local-draft-models-a-debate-for-agent-sql-placement-28o8</guid>
      <description>&lt;p&gt;A platform team watched a checkout report stall after an agent rewrote a revenue query during a release review. The statement joined three large tables and omitted a selective predicate, so the planner chose a broad scan. An engineer cancelled the statement before it held locks, then recorded the draft path rather than the model brand. The remaining decision was where the next draft should run, and which data could leave the production boundary.&lt;/p&gt;

&lt;p&gt;That incident is a composite scenario for this debate, not a measured outage report from a named company. It still matches a pattern that shows up whenever agents draft SQL against live catalogs without a placement rule. Better language models do not, by themselves, decide whether schema text may cross a network boundary. Software controls around placement, redaction, and rehearsal still determine whether a fluent draft is acceptable to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two credible positions
&lt;/h2&gt;

&lt;p&gt;Position A sends a masked catalog extract to a free hosted model, then rehearses the returned SQL on a free disposable server. Position B keeps all draft generation on a local model that remains inside the team trust boundary. That position uses an external server only as an execution witness that is loaded with synthetic rows. They disagree about which artifact may move, and about which system may see real object names.&lt;/p&gt;

&lt;p&gt;Position A argues that hosted models often draft more fluent SQL when prompts include current table names and key relationships. A masked extract can remove row samples, comments, and high-sensitivity column names before that hosted call. A free server then provides an isolated planner and a permission set that production roles do not share. The claim is that separation of draft generation from production execution reduces lock risk more than an unrehearsed local draft.&lt;/p&gt;

&lt;p&gt;Position B argues that column names, check constraints, and relationship graphs can still reveal product structure even after row samples are removed. A local model can read the live catalog through an internal tool and never transmit that text. The free server, if used at all, should receive only synthetic identifiers and the candidate statement. The claim is that an execution witness does not require the drafting model to sit outside the boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence that can actually be checked
&lt;/h2&gt;

&lt;p&gt;Neither position is universally cheaper, safer, or more accurate, and this article does not report a benchmark. The useful evidence is qualitative and checkable by each team against its own written catalog policy. Hosted draft quality matters only when the team is allowed to export a defined schema subset. Local draft quality matters only when the team can actually run a model beside the catalog.&lt;/p&gt;

&lt;p&gt;Rehearsal value depends on whether the disposable server has similar indexes, not on whether the server was free. The table below compares those placement inputs without ranking any vendor, model family, or hosting plan. Availability of a free hosted model and a free server can make Position A practical for unclassified practice catalogs. That availability does not change a contractual ban, and it does not prove that masked names are harmless.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision input&lt;/th&gt;
&lt;th&gt;Favors masked hosted draft&lt;/th&gt;
&lt;th&gt;Favors local draft model&lt;/th&gt;
&lt;th&gt;Reject both external hops&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Schema classification&lt;/td&gt;
&lt;td&gt;Names are internal but not regulated&lt;/td&gt;
&lt;td&gt;Names reveal unreleased product scope&lt;/td&gt;
&lt;td&gt;Any catalog text is barred from export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt contents&lt;/td&gt;
&lt;td&gt;Row samples already stripped&lt;/td&gt;
&lt;td&gt;Catalog text never leaves the host&lt;/td&gt;
&lt;td&gt;A useful draft would require samples&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Planner fidelity&lt;/td&gt;
&lt;td&gt;Synthetic server can mirror indexes&lt;/td&gt;
&lt;td&gt;Local database already mirrors indexes&lt;/td&gt;
&lt;td&gt;No safe mirror exists this week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bad-join failure cost&lt;/td&gt;
&lt;td&gt;Rehearsal can catch it before production&lt;/td&gt;
&lt;td&gt;Local explain can catch it before review&lt;/td&gt;
&lt;td&gt;Write access cannot be prevented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operational constraint&lt;/td&gt;
&lt;td&gt;Hosted call and server are available now&lt;/td&gt;
&lt;td&gt;Local model is maintained by the team&lt;/td&gt;
&lt;td&gt;External terms are unacceptable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A placement workflow with numbered checks
&lt;/h2&gt;

&lt;p&gt;The workflow below is a concrete placement procedure for the debate, and it can be run on an unclassified catalog. Each numbered step produces an artifact that a reviewer can inspect without trusting the agent's explanation. Commands are patterns for PostgreSQL-style rehearsal, and they should be adapted to the server version in use.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Classify the catalog extract before any prompt is built, and mark each table as exportable, mask-required, or local-only.&lt;/li&gt;
&lt;li&gt;Build the extract from catalog metadata only, keeping names, types, and declared keys while dropping comments and sample values.&lt;/li&gt;
&lt;li&gt;Score the extract with an automated gate that fails on missing labels, sensitive classes, or literals copied from logs.&lt;/li&gt;
&lt;li&gt;Choose hosted placement only when the gate passes and a reviewer can read the exact payload before the call.&lt;/li&gt;
&lt;li&gt;Rehearse on a disposable server with synthetic rows, a read-only role, and an explain plan captured before execution.&lt;/li&gt;
&lt;li&gt;Record the model placement, extract hash, server identity, and explain output beside the statement hash for later review.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Artifact: a gate, a role, and four checks
&lt;/h2&gt;

&lt;p&gt;The following Python is a proposal for the gate in step 3, and it is not a production policy engine. It has not been executed against a private catalog for this article, so teams should treat the output as a starting check. A passing result means the payload avoided three obvious failures, not that the schema is safe to export.&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;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&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;sha256&lt;/span&gt;

&lt;span class="n"&gt;SENSITIVE&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;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;phone&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;ssn&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;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&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;[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}&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="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;columns&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;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="k"&gt;if&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="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;leaked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&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;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;SENSITIVE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;literals&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;findall&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;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="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;leaked&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;literals&lt;/span&gt;
    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&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;payload&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="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;hexdigest&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;ok&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;leaked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;leaked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;digest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A passing gate is necessary but not sufficient, because the rehearsal role still needs a database-side boundary. On PostgreSQL, a reviewer can create a read-only role and inspect the plan without granting write rights. The statements below are a pattern for a disposable server, not a claim that every host uses the same defaults. Replace the dates and table names with fixtures that contain no production values before anyone runs them.&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;agent_reader&lt;/span&gt; &lt;span class="n"&gt;NOLOGIN&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;rehearsal&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;agent_reader&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;agent_reader&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;agent_reader&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;ROLE&lt;/span&gt; &lt;span class="n"&gt;agent_reader&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="nb"&gt;TEXT&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="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;payments&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&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;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-10-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL documents &lt;a href="https://www.postgresql.org/docs/current/sql-explain.html" rel="noopener noreferrer"&gt;EXPLAIN&lt;/a&gt; and &lt;a href="https://www.postgresql.org/docs/current/ddl-priv.html" rel="noopener noreferrer"&gt;privilege rules&lt;/a&gt; in the current official reference, which is the primary source for those commands. A team should read that version's pages before copying the grants, because default privileges can differ by release. If the disposable server cannot host a faithful index set, label the plan as a syntax witness rather than a cost forecast.&lt;/p&gt;

&lt;p&gt;Run four checks before anyone trusts the helper, and record the returned dictionary rather than a screenshot of a chat. These checks are expected behaviors for the proposal, not a transcript of an executed private run. Keep the fixture columns small enough that a reviewer can compare the dictionary with the source list by eye.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pass a payload that lists only exportable columns with classes present, and expect the &lt;code&gt;ok&lt;/code&gt; flag to be true.&lt;/li&gt;
&lt;li&gt;Fail a payload that includes a column classed as email, and expect that column name to appear under leaked.&lt;/li&gt;
&lt;li&gt;Fail a payload that contains an email literal in the text, and expect the gate to reject that payload.&lt;/li&gt;
&lt;li&gt;Fail a column entry that omits its class field, and expect that column name to appear under missing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where a free practice surface fits
&lt;/h2&gt;

&lt;p&gt;Some teams lack a maintained local model, yet they still need a place to practice agent SQL without touching production. In that setting, MonkeyCode's free model access and free server option can serve as the practice surfaces for that gated path. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Readers should confirm current terms before relying on either surface, because this article does not treat availability as permanent.&lt;/p&gt;

&lt;p&gt;This article does not name models, quotas, hardware, or duration, because those facts were not supplied as stable specifications. A free option can lower the cost of trying the workflow on an unclassified catalog that already passed the gate. It cannot replace the classification gate, and it should not be treated as a permanent control boundary. If the gate fails, the reviewer keeps the draft local and still uses an isolated database for the explain step.&lt;/p&gt;

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

&lt;p&gt;Masking column names does not anonymize a schema, because unique table names can reveal a launch, a partnership, or a fraud rule. Position A is therefore unsafe for catalogs whose names are themselves confidential, even when row samples never appear in the prompt. A free server with empty statistics will mislead cost comparisons, so its plan cost should not be treated as a production forecast. The sample gate also misses many secret shapes, including internal identifiers that do not resemble email addresses.&lt;/p&gt;

&lt;p&gt;Teams should not use the hosted path when contracts forbid external inference or when classification labels are incomplete. They should also skip that path when the agent can obtain write credentials on any reachable database. This debate is the wrong tool if a human acceptance step cannot be staffed for every promoted statement. An automated promotion from a fluent draft to production sits outside the decision rule that follows.&lt;/p&gt;

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

&lt;p&gt;Reviewers should choose the masked hosted draft only when three conditions hold together for that candidate statement. Every referenced object must be classified exportable before the payload is assembled for a hosted call. The gate report must show no sensitive columns and no literals, and the rehearsal server must stay isolated from production. Choose the local draft model when any object is local-only, or when the team cannot review the exact payload before the call.&lt;/p&gt;

&lt;p&gt;Reject both external hops when write access cannot be prevented or when no faithful non-production mirror exists. The rule prefers the stricter placement whenever the evidence is mixed, incomplete, or still waiting on classification. Fluency is not evidence of safety, and a free practice surface is not evidence of an acceptable boundary. The recorded extract hash, role grant, and explain output are the acceptance package that later reviewers should trust.&lt;/p&gt;

&lt;p&gt;If review notes already capture plans but not placement, add the extract hash before the next agent SQL change. That single field makes the debate auditable, whether the draft model ran locally or on a free hosted practice path. Teams with an export clearance can compare both placements on fixtures and keep the stricter result when the plans disagree.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>postgres</category>
      <category>database</category>
      <category>security</category>
    </item>
    <item>
      <title>Grant Matrices or Query Rewrites: A Debate for Agent SQL Least Privilege</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Sun, 11 Oct 2026 21:22:04 +0000</pubDate>
      <link>https://dev.to/dataio_4921/grant-matrices-or-query-rewrites-a-debate-for-agent-sql-least-privilege-3db9</link>
      <guid>https://dev.to/dataio_4921/grant-matrices-or-query-rewrites-a-debate-for-agent-sql-least-privilege-3db9</guid>
      <description>&lt;p&gt;This account previously published a night-shift story about an agent query that locked a production table. The lock was the visible failure, but the missing debate was which privilege path allowed the statement to run at all. A reporting agent can still ask for a temporary superuser role when a narrower view would answer the question. Reviewers need an explicit rule before they choose between a new grant and a query rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why least privilege is the debate
&lt;/h2&gt;

&lt;p&gt;Recent developer discussion has stressed that model quality can improve while the surrounding software controls stay unchanged. That signal fits agent SQL, because a sharper model still runs inside roles, views, and grants that humans own. This article does not cite vendor benchmark scores, since no primary measurement was supplied for this draft. The practical question is which control should reject a dangerous statement before it touches shared data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position one: expand a grant matrix
&lt;/h2&gt;

&lt;p&gt;A grant matrix treats the agent as an untrusted author and the role catalog as the system of record. Each workflow receives a dedicated login, a narrow schema usage grant, and explicit table or view rights. The matrix records who requested the right, which statement shape needs it, and when the grant should expire. Evidence for this position comes from ordinary privilege errors rather than from model confidence scores or prose explanations.&lt;/p&gt;

&lt;p&gt;Supporters argue that query rewrites can hide missing rights until a later report needs one more column. A reviewed matrix makes that gap visible in role table grants before the agent is allowed to run. The operational cost is real, because every new column, sequence, or function can demand another reviewed grant. Teams that keep reporting schemas stable usually absorb that review cost better than teams shipping daily migrations.&lt;/p&gt;

&lt;p&gt;PostgreSQL already exposes the evidence through catalog views, so the matrix does not depend on a new engine. A reviewer can compare requested relations with information_schema.role_table_grants and reject any right that lacks an owner. Sequences and functions need separate checks, because table grants do not cover nextval or routine execution. Primary documentation for GRANT and privilege types lives in the current PostgreSQL docs, which should be read for the exact major version in use.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-grant.html" rel="noopener noreferrer"&gt;GRANT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/ddl-priv.html" rel="noopener noreferrer"&gt;Privilege system&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/infoschema-role-table-grants.html" rel="noopener noreferrer"&gt;role_table_grants&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Position two: rewrite onto an approved surface
&lt;/h2&gt;

&lt;p&gt;A rewrite position refuses new grants and forces the agent to target views, functions, or procedures that already exist. The database owner defines those objects, and the agent receives only EXECUTE or SELECT on the approved surface. This approach reduces the chance that a generated statement invents a new path through a base table. The main tradeoff is flexibility, because a useful business question may have no approved object yet.&lt;/p&gt;

&lt;p&gt;Evidence here is the diff between the drafted SQL and the allowed surface, which a reviewer can inspect. If the rewrite still needs a new column, the correct response is a schema change, not a silent grant. Rewrites fail closed when the catalog lacks an object, which is often safer than a broad role. They can fail open in practice when someone later adds a security definer routine without a fresh review.&lt;/p&gt;

&lt;p&gt;View design matters, because a view can leak rows if the owner bypasses the caller's privileges. PostgreSQL documents security_invoker views, and teams should confirm that option on the version they actually run. A rewrite that ignores owner rights is not least privilege, even when the agent login looks narrow. The safer rewrite is boring: stable names, explicit column lists, and no dynamic schema search path.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-createview.html" rel="noopener noreferrer"&gt;CREATE VIEW&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Evidence that can actually decide the debate
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision input&lt;/th&gt;
&lt;th&gt;Favors grant matrix&lt;/th&gt;
&lt;th&gt;Favors query rewrite&lt;/th&gt;
&lt;th&gt;Do not treat as proof&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same report shape runs daily&lt;/td&gt;
&lt;td&gt;Rights can be reviewed once and expired on a calendar&lt;/td&gt;
&lt;td&gt;An approved view already matches the shape&lt;/td&gt;
&lt;td&gt;Model says the SQL looks safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema migrations land daily&lt;/td&gt;
&lt;td&gt;Matrix churn may exceed review capacity&lt;/td&gt;
&lt;td&gt;Rewrite waits for a reviewed object&lt;/td&gt;
&lt;td&gt;A passing local syntax check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Statement needs nextval or EXECUTE&lt;/td&gt;
&lt;td&gt;Matrix must name sequence and routine rights&lt;/td&gt;
&lt;td&gt;Rewrite should call a procedure instead&lt;/td&gt;
&lt;td&gt;Table SELECT alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent asks for superuser or BYPASSRLS&lt;/td&gt;
&lt;td&gt;Reject and record the request&lt;/td&gt;
&lt;td&gt;Reject and keep the approved surface&lt;/td&gt;
&lt;td&gt;Free-tier availability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviewer cannot name the data owner&lt;/td&gt;
&lt;td&gt;Stop, because a grant has no accountable owner&lt;/td&gt;
&lt;td&gt;Stop, because a view owner is also unclear&lt;/td&gt;
&lt;td&gt;A green dry run on empty tables&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 measured benchmark, and every cell should be checked against the target catalog. Empty-table success is weak evidence, because missing privileges sometimes appear only after a sequence or a policy is touched. A language model can fill the first draft of this table, but it cannot certify the catalog. Server evidence still means the grants and the rewritten objects were applied on an isolated instance and inspected there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbered rehearsal on a disposable database
&lt;/h2&gt;

&lt;p&gt;The following workflow is a proposed test plan, and it was not executed against a production server for this article. Run it only on a disposable database that you can drop without notice or ticket approval. Keep the agent login unable to create roles, and apply every grant from a separate admin session. Record the SQL diff, the grant diff, and the final privilege query in the same change ticket.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create the narrow role and the approved view
&lt;/h3&gt;

&lt;p&gt;Create the role without superuser, createdb, or createrole, then point the agent at a view you own. Set security invoker only after you confirm that the option exists on the server version you run. Grant schema usage and view select, and do not grant the base table in this step. If view creation fails, stop and fix the owner path before you debate any extra rights.&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;-- Proposed example, not executed for this article.&lt;/span&gt;
&lt;span class="c1"&gt;-- Replace the password outside version control. Do not reuse this placeholder.&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;agent_reports&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'replace-me'&lt;/span&gt;
  &lt;span class="n"&gt;NOSUPERUSER&lt;/span&gt; &lt;span class="k"&gt;NOCREATEDB&lt;/span&gt; &lt;span class="n"&gt;NOCREATEROLE&lt;/span&gt; &lt;span class="n"&gt;NOINHERIT&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;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;reporting&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;VIEW&lt;/span&gt; &lt;span class="n"&gt;reporting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;daily_orders&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;security_invoker&lt;/span&gt; &lt;span class="o"&gt;=&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;AS&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;created_on&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;status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;.&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;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_on&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;7&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="n"&gt;reporting&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;agent_reports&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="n"&gt;reporting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;daily_orders&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;agent_reports&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Stop drafts that ask for a privileged surface
&lt;/h3&gt;

&lt;p&gt;Scan the drafted text for privilege verbs before you spend any review time on a rewrite. This scan is a heuristic filter, and it will miss obfuscated statements or comments that hide keywords. Treat a match as a stop condition, then ask the agent for a narrower statement shape. Do not treat a clean scan as permission to run the draft against any shared database.&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;# Proposed local check. It does not parse SQL and it is not a security boundary.&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DRAFT_SQL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'pg_read_file|pg_sleep|bypassrls|security definer|copy |grant |revoke |alter role'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'stop: draft asks for a privileged surface'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Collect catalog evidence for both positions
&lt;/h3&gt;

&lt;p&gt;Apply the view path first, then run the two catalog checks from an admin session and from the agent role. A successful view count supports the rewrite position only for that object and that role. A base-table error supports least privilege only if the error is insufficient privilege, not a missing relation caused by search path. Save both outputs beside the drafted SQL so the debate has server evidence rather than model prose.&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;-- Position A: rights the granting admin can see because that admin is the grantor.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;privilege_type&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;role_table_grants&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;grantee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'agent_reports'&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Position B: connect as agent_reports. SET ROLE works only for a member or a superuser.&lt;/span&gt;
&lt;span class="c1"&gt;-- Expect the view to succeed and the base table to fail with insufficient_privilege.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;reporting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;daily_orders&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;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;sales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Time-box a grant only after the rewrite loses
&lt;/h3&gt;

&lt;p&gt;Choose the matrix only when the missing right is stable, owned, and narrower than the agent's original request. Put the grant and the matching revoke in one ticket, even if the revoke runs later by schedule. If you cannot name an owner or an expiry, the decision rule below says to stop. A query rewrite remains preferable when an existing approved object already answers the stated business question.&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;-- Proposed ticket pair. Do not leave the grant without the matching removal.&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="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;agent_reports&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;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;agent_reports&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Use the rewrite position when an approved view, function, or procedure already returns the required columns and the agent can access it. Use the grant matrix when no such object exists, a named owner accepts the right, and the grant is narrower than superuser or bypassrls. Stop when the draft asks to create roles, read server files, copy data to a client path, or disable row security. Also stop when the only passing test used empty tables, because that result does not exercise sequences, policies, or default privileges.&lt;/p&gt;

&lt;p&gt;If both positions seem viable, prefer the rewrite for one-off questions and the matrix for a report that will run unchanged for weeks. Revisit the choice after each migration, since a new column can invalidate either a view or a grant list. The rule is intentionally conservative, and it will block some useful drafts that an agent could otherwise run. That delay is an accepted cost, because a wrong privilege typically lasts longer than one delayed report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a free model and a free server fit
&lt;/h2&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;MonkeyCode is included here as an open-source project with operator-stated free model access and a free server option. Those two availability claims are operator-supplied, and this draft does not verify a quota, model list, hardware shape, or duration. A free model can draft the grant matrix, propose the view, and explain which decision-table row it chose. A free server can host the disposable database in the numbered plan, so the privilege checks do not land on production.&lt;/p&gt;

&lt;p&gt;Use the model output as a proposal, then require the catalog queries above before anyone accepts a grant. Do not paste production connection strings into a hosted prompt, and do not reuse the sample password from the code block. If current project terms no longer include free model access or a free server, the workflow still stands on local PostgreSQL. Readers who want to rehearse the matrix can confirm today's MonkeyCode terms and repository before they copy any statement into a shared environment.&lt;/p&gt;

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

&lt;p&gt;This approach does not replace a SQL parser, a migration framework, or a formal access review. The text scan misses encoded statements, dynamic SQL inside functions, and rights inherited through group roles. security_invoker is version-dependent, and older servers need a different view design that you must verify locally. No latency, cost, or accuracy number in this article was measured against a live database service.&lt;/p&gt;

&lt;p&gt;Skip the pattern when you cannot create a disposable database, cannot separate admin and agent logins, or cannot name a data owner. Also skip it for break-glass maintenance that legitimately needs a superuser, because hiding that work inside an agent prompt is worse. Teams that already generate grants from infrastructure code should extend that code, not add a parallel matrix in a chat log. If your only goal is to spend a free quota, this debate will not make the resulting SQL safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the choice
&lt;/h2&gt;

&lt;p&gt;Least privilege for agent SQL is a catalog problem with two honest answers, not a prompt-quality contest. Grant matrices make repeated rights reviewable, and query rewrites keep one-off questions on objects you already trust. The decision rule prefers the existing surface, allows a named expiring grant only when that surface is missing, and stops on superuser requests. Server evidence from the numbered checks should outrank a fluent explanation produced by any language model.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>database</category>
      <category>ai</category>
    </item>
    <item>
      <title>Fixture Databases or Disposable Servers: A Debate for Agent SQL Rehearsal</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Sat, 10 Oct 2026 17:47:20 +0000</pubDate>
      <link>https://dev.to/dataio_4921/fixture-databases-or-disposable-servers-a-debate-for-agent-sql-rehearsal-42fp</link>
      <guid>https://dev.to/dataio_4921/fixture-databases-or-disposable-servers-a-debate-for-agent-sql-rehearsal-42fp</guid>
      <description>&lt;p&gt;A checkout service merged an agent-written refund update after a single-schema fixture returned three affected rows. The production planner resolved the unqualified table name through a session search path that the fixture never set. The statement updated a staging copy that shared the table name, and the refund job reported success. The finance auditors noticed the mismatch only after the nightly settlement export failed its reconciliation checks.&lt;/p&gt;

&lt;p&gt;This scenario is a composite reconstruction, not a named customer incident, and it isolates one failure mode. Local fixtures proved that the statement could change three rows under a simplified catalog and role. They did not prove that the engine version, privileges, and search path matched the production session. The debate below asks where an agent-written mutation should be allowed to fail before any promotion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question the gate has to answer
&lt;/h2&gt;

&lt;p&gt;Agent SQL tools often draft mutations faster than reviewers can replay them on a faithful engine. A fixture database is cheap to host, deterministic to reload, and easy to reset between tests. A disposable server is slower to provision, but it can expose planner, extension, and privilege behavior that a reduced fixture hides. The useful question is not which side sounds stricter, but which evidence the promotion gate can actually collect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: keep rehearsal inside fixture databases
&lt;/h2&gt;

&lt;p&gt;Fixture advocates want every agent draft to fail inside versioned schema files and a local container. The catalog is small, the data is synthetic, and the test does not depend on a network or a shared host. Reviewers can diff the fixture files and the expected row counts in the same pull request. That position is strongest when the workload is read-only, the dialect is narrow, and production extensions are already mirrored in the fixture.&lt;/p&gt;

&lt;p&gt;The weakness of this position is catalog fidelity, not the convenience of resetting a local container. A single-schema SQLite file will not reproduce PostgreSQL search path resolution, partial indexes, or role grants. A container that loads only the happy-path tables can still hide a view, a rule, or a trigger. Passing a fixture then becomes evidence about the fixture, not evidence about the engine that will execute the statement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: rehearse on a disposable server before promotion
&lt;/h2&gt;

&lt;p&gt;Server advocates want the same statement to run against an engine that matches the production major version and the required extensions. The server should be disposable, loaded only with synthetic rows, and destroyed after the rehearsal run. Network distance and setup time are accepted costs because a wrong schema resolution is more expensive than a slower check. This position is strongest for data mutations, dynamic search paths, and catalogs that fixtures routinely simplify.&lt;/p&gt;

&lt;p&gt;The weakness is operational scope, because a hosted server can leak secrets if connection strings are copied from production. It can also create false confidence if its version, extensions, or roles still differ from the target. A green run on the wrong major version is not stronger evidence than a green run on a fixture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence worth scoring
&lt;/h2&gt;

&lt;p&gt;The decision rule needs evidence columns that either side can satisfy without stating a product preference. The table below is a proposed review rubric, not a benchmark taken from a measured lab. Scores in that table are ordinal labels for a review meeting, not percentages from a production study. A missing cell means the promotion gate cannot claim that class of evidence for the current draft.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;th&gt;Fixture databases&lt;/th&gt;
&lt;th&gt;Disposable servers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catalog objects under test&lt;/td&gt;
&lt;td&gt;Only what the fixture file creates&lt;/td&gt;
&lt;td&gt;Whatever the rehearsal role can see&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search path and role grants&lt;/td&gt;
&lt;td&gt;Often omitted or hard-coded&lt;/td&gt;
&lt;td&gt;Set in the session before execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extension and version match&lt;/td&gt;
&lt;td&gt;Easy to skip&lt;/td&gt;
&lt;td&gt;Required before the run counts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reset and reproducibility&lt;/td&gt;
&lt;td&gt;Strong, because files are versioned&lt;/td&gt;
&lt;td&gt;Weaker, unless the load script is versioned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data-handling risk&lt;/td&gt;
&lt;td&gt;Low if rows are synthetic&lt;/td&gt;
&lt;td&gt;Higher if any production extract is used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to first failure&lt;/td&gt;
&lt;td&gt;Usually shorter&lt;/td&gt;
&lt;td&gt;Usually longer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A numbered rehearsal workflow
&lt;/h2&gt;

&lt;p&gt;The numbered steps below are an unexecuted example for a PostgreSQL mutation rehearsal gate, not a timed study. They do not claim measured timings, and they assume a reviewer can run Docker on a local machine. Replace the database names, host ports, and rehearsal credentials before any real use of these commands.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pin the engine and refuse to continue when the server version does not match the agreed major version.&lt;/li&gt;
&lt;li&gt;Load only synthetic rows from a versioned script, and fail if the script contains a production connection string.&lt;/li&gt;
&lt;li&gt;Set the rehearsal role, the search path, and a statement timeout before the agent statement is submitted.&lt;/li&gt;
&lt;li&gt;Execute inside a transaction, capture row counts and notices, then roll back unless a later human step promotes the change.&lt;/li&gt;
&lt;li&gt;Record which evidence columns were actually satisfied, and block promotion when any required column is missing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A local fixture can still own the load step and the rollback step when the catalog is already complete. A disposable server should own the version pin and the session contract when the statement is a mutation. The gate should store the rollback output next to the SQL draft so the reviewer sees engine evidence, not only model commentary. Comments in the examples mark them as unexecuted, so nobody should treat the sample password as a real secret policy.&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;-- Unexecuted example: session contract before any agent mutation.&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;statement_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="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;public&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="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;rehearsal_writer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Paste agent SQL below this contract, never before it.&lt;/span&gt;
&lt;span class="c1"&gt;-- ROLLBACK is the default end state for rehearsal.&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Unexecuted example. Replace MAJOR with the production major version before running.&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; agent-sql-rehearsal &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rehearsal-only &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rehearsal &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 55432:5432 &lt;span class="s2"&gt;"postgres:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MAJOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
psql &lt;span class="s1"&gt;'postgresql://postgres:rehearsal-only@localhost:55432/rehearsal'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'SHOW server_version;'&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;-- Unexecuted fixture that makes the composite failure visible.&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;app&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;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;staging&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;refunds&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;int&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;status&lt;/span&gt; &lt;span class="nb"&gt;text&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;staging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;refunds&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;int&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;status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;refunds&lt;/span&gt; &lt;span class="k"&gt;VALUES&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="s1"&gt;'open'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'open'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'open'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;refunds&lt;/span&gt; &lt;span class="k"&gt;VALUES&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="s1"&gt;'open'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'open'&lt;/span&gt;&lt;span class="p"&gt;),&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="s1"&gt;'open'&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;# Unexecuted example: list leading mutation statements for a manual qualifier check.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s1"&gt;'^(UPDATE|DELETE FROM|INSERT INTO) '&lt;/span&gt; draft.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to read a failed rehearsal
&lt;/h2&gt;

&lt;p&gt;A failed run is useful only when the log shows the session state that produced it. Treat a missing notice, a missing role name, or a missing rollback marker as an incomplete test rather than a pass. Do not widen the statement timeout to hide a lock, because that change answers a different debate about budgets and caps. Record the failing statement text exactly as submitted, including any schema qualifier the agent omitted before execution.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm the server version before you interpret a syntax error, because a version mismatch can mimic a dialect bug.&lt;/li&gt;
&lt;li&gt;Print the search path and current user immediately after the failure, and store both lines with the SQL draft.&lt;/li&gt;
&lt;li&gt;Compare the affected row count with the fixture expectation only after you know which schema resolved the table name.&lt;/li&gt;
&lt;li&gt;Roll the transaction back before you retry, so a diagnostic query does not leave a partial mutation in the rehearsal database.
&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;-- Unexecuted example: capture session state after a rehearsal failure.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_schema&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'search_path'&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;search_path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where free model access fits
&lt;/h2&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;MonkeyCode is described in this outreach as an open-source project with free model access and a free server option. This article treats free model access and a free server option as availability claims supplied for the draft, not as a measured capacity sheet. A reviewer can use free model access to draft the mutation and to list assumptions, then ignore that commentary unless the server output agrees. The free server option is relevant only when a local container is unavailable and the documentation still confirms the offer.&lt;/p&gt;

&lt;p&gt;The model should not be the acceptance witness for a mutation that can touch more than one schema. Ask it for a short assumption list: target schema, required indexes, roles, and statements it refused to qualify. Compare that assumption list with the session contract and with the rolled-back server output before anyone promotes the draft. If the model says the table is unique and the server shows two schemas with the same name, the server evidence wins.&lt;/p&gt;

&lt;p&gt;That comparison is the same discipline whether the draft came from the assistant under test or from another tool. Reviewers should keep the product name out of the acceptance log and keep the server transcript in it. The draft source matters less than whether the rolled-back engine contradicted an assumption in the list.&lt;/p&gt;

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

&lt;p&gt;Choose fixture databases when the statement is read-only and the fixture already creates every object it can touch. Choose a disposable server when the statement mutates data or uses unqualified names under a role the fixture never grants. Require both when the change is promoted to a shared environment, because each side covers a different failure. Reject the change if the server major version is unpinned, the rows are not synthetic, or the transaction does not roll back by default.&lt;/p&gt;

&lt;p&gt;A practical tie-break helps when reviewers disagree about which missing column is acceptable for that draft. If a missing evidence column could change the target object or the lock footprint, the disposable server is mandatory. If the missing column could only change fixture convenience, the fixture is enough for that draft. Write the chosen column set into the pull request so the next agent run cannot silently drop it.&lt;/p&gt;

&lt;p&gt;The function below is an unexecuted sketch of the tie-break, and it returns a boolean rather than a performance score. Callers must fill the evidence dictionary from server output, not from the model's own summary of that output. A false return means the draft stays in rehearsal, even if the generated SQL looks complete in review.&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;# Unexecuted example: promotion checklist. Labels are review notes, not measurements.
&lt;/span&gt;&lt;span class="n"&gt;REQUIRED_FOR_MUTATION&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;version_pinned&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;synthetic_rows_only&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;search_path_set&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;role_set&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;rolled_back&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;def&lt;/span&gt; &lt;span class="nf"&gt;allow_promotion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evidence&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;statement_mutates&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;bool&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;statement_mutates&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;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;evidence&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;fixture_objects_complete&lt;/span&gt;&lt;span class="sh"&gt;'&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;evidence&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;synthetic_rows_only&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;return&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evidence&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;key&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;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;REQUIRED_FOR_MUTATION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;This workflow does not prove production safety, because a synthetic server is not production traffic, statistics, or concurrency. It does not estimate cost, and it does not replace grants reviews, backups, or a human approval for destructive statements. Quotas, model lists, server regions, and retention rules are outside this article and must be read from current project documentation before use. Anyone who cannot keep sample data synthetic should not point an agent at a hosted server.&lt;/p&gt;

&lt;p&gt;Teams that lack a rollback default, a version pin, and a named rehearsal role should not treat a green model answer as acceptance. Operators who need a measured benchmark, a named model, or a guaranteed capacity number will not find those claims here. Readers who only need a local container should skip the hosted option and keep the session contract anyway. The composite opening is a teaching case, so it should not be cited as evidence that any vendor failed.&lt;/p&gt;

&lt;p&gt;If the gate in your repository still accepts agent SQL from fixture row counts alone, compare that habit with the session contract above. The MonkeyCode project documentation is one place to check whether free model access and a free server option fit that gate today. Do that check before changing the promotion rule, and do not treat this article as a capacity guarantee. The decision remains the evidence columns in the rubric, not the assistant that drafted the statement.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>postgres</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>Model Critique or Server Evidence: A Debate for Agent SQL Acceptance</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Fri, 09 Oct 2026 14:16:57 +0000</pubDate>
      <link>https://dev.to/dataio_4921/model-critique-or-server-evidence-a-debate-for-agent-sql-acceptance-55n8</link>
      <guid>https://dev.to/dataio_4921/model-critique-or-server-evidence-a-debate-for-agent-sql-acceptance-55n8</guid>
      <description>&lt;p&gt;A Thursday review queue held one agent draft that joined orders, payments, and refunds for a weekly finance extract. The author had requested a seven-day window, yet the draft projected every column from all three source tables. Two reviewers debated the draft for several minutes before either would approve it for the scheduled job. One reviewer wanted a second model critique, while the other demanded execution evidence from a disposable server.&lt;/p&gt;

&lt;p&gt;Both positions can be defended with operational evidence, yet they answer different questions about failure and exposure. A written critique can name a missing predicate, but only a server run shows the plan, the row count, and the locks. This article compares those gates, supplies a reproducible evidence worksheet, and ends with a decision rule for promotion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question that should frame the debate
&lt;/h2&gt;

&lt;p&gt;The useful question is not which tool sounds more modern during a crowded afternoon review meeting. The useful question is which failure you fear, and which artifact can actually observe that failure. A missing date predicate is visible in the statement text, while a bad join order appears only after planning. Lock waits, temp file growth, and underestimated row counts appear only when a server executes the statement.&lt;/p&gt;

&lt;p&gt;If your acceptance rule collects the wrong artifact, you will feel confident while the risky behavior remains untested. That mismatch, rather than model brand or server price, is the failure mode this debate should reduce. Headlines about model quality do not change the observation problem, because a fluent explanation is still not a plan. Treat news about newer models as a reason to retest your gate, not as a substitute for the artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position A: a structured critique as the acceptance gate
&lt;/h2&gt;

&lt;p&gt;Position A says a second model pass should accept or reject the draft before any database connection opens. Supporters point to speed, repeatability, and the absence of lock risk while the statement remains plain text. A critique checklist can require named checks for predicates, join keys, selected columns, and explicit limits. The output can be stored beside the draft, diffed across revisions, and reviewed without copying table rows.&lt;/p&gt;

&lt;p&gt;The evidence for this position is real, but it is evidence about the text rather than evidence about execution. A careful critique often catches a cartesian join, an unbounded scan, or a write hidden inside a function call. It cannot tell you whether the planner chooses a nested loop, a hash join, or a sequential scan. It also cannot show whether a selective predicate matches zero rows because of a type or timezone mistake.&lt;/p&gt;

&lt;p&gt;Position A becomes weak when the same model family drafts the statement and then reviews its own work. Shared blind spots travel from the draft into the critique, especially around business calendars and late-arriving facts. A second pass remains useful as a prefilter, yet it should not be renamed into proof that the query is safe. Keep the critique artifact, and label it as a text review rather than as an execution result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position B: server evidence as the acceptance gate
&lt;/h2&gt;

&lt;p&gt;Position B says no agent statement is accepted until a disposable server returns a plan and a bounded result. Supporters argue that production-shaped statistics reveal mistakes that a fluent critique will describe and then miss. A rehearsal session can set a statement timeout, a lock timeout, and a read-only transaction before the draft runs. Those settings do not make the draft correct, but they make a bad draft fail where you can afford it.&lt;/p&gt;

&lt;p&gt;The evidence for this position is the plan, the estimates, the bounded output, and the error text from the session. A sequential scan on a fixture of known size is a fact, while a model warning about scans is only a hypothesis. Server evidence also exposes search path surprises, missing indexes, and casts that prevent index use. Those facts are difficult to invent from the SQL text alone, even when the critique sounds specific and confident.&lt;/p&gt;

&lt;p&gt;Position B becomes weak when the fixture is tiny, stale, or shaped differently from the production tables. A clean plan on a small synthetic sample can hide a hash spill that appears on a much larger month. A hosted rehearsal server also does not grant permission to upload regulated rows, secrets, or unmasked customer attributes. If the fixture cannot legally leave your network, this position must be satisfied on infrastructure you control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a free model pass and a free server participate
&lt;/h2&gt;

&lt;p&gt;MonkeyCode offers free model access for the critique pass and a free server option for the rehearsal database.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This draft states neither model names, token quotas, hardware size, nor how long either option remains available. Confirm the current terms in the product documentation before any promotion gate depends on them. The same worksheet still works if you replace that model access with another API and that server with local Postgres. Removing the product name should not remove the debate, the session limits, or the decision rule below.&lt;/p&gt;

&lt;p&gt;Use the free options only for masked fixtures and for statement text, not for raw extracts from production. Record which option produced each artifact so a later review can see the boundary you actually tested. A hosted target is a collector of evidence, not a quieter copy of the production role. If the documented terms are narrower than this worksheet, follow the terms and shrink the fixture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbered workflow: collect both artifacts, then decide
&lt;/h2&gt;

&lt;p&gt;The following commands are a proposal for a local rehearsal, and they were not executed for this article. If you use a hosted free server, keep the SQL and replace only the connection target after you confirm its terms. Do not point this workflow at production, even with a read-only role, until a separate change process allows it. Start from an empty database so leftover objects from an earlier experiment cannot silently change the plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Start a disposable database
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Local throwaway password. Do not reuse it. Pin a supported image tag before relying on this.&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; agent-sql-rehearsal &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rehearsal &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rehearsal &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 55432:5432 &lt;span class="se"&gt;\&lt;/span&gt;
  postgres:16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image tag above is an example for a local container, not a description of any hosted free server. Wait for the port to accept connections, then load a masked fixture you are allowed to copy. Keep the fixture row counts in a small file so later steps can detect drift before you trust a plan. Stop the container when the review ends so the data does not linger on a laptop.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Record fixture volume before planning
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'orders'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;relname&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;fixture_rows&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;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'payments'&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;payments&lt;/span&gt;
&lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'refunds'&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;refunds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare those three counts with the ranges published by the team that owns the weekly finance extract. A fixture that is empty, duplicated, or missing a month will make both the plan and the critique look calmer than production. Write the counts into the evidence record before you open a model prompt or run &lt;code&gt;EXPLAIN&lt;/code&gt;. If the drift exceeds the agreed threshold, rebuild the fixture and do not promote the draft.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Run the draft inside a read-only, time-bounded transaction
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="k"&gt;READ&lt;/span&gt; &lt;span class="k"&gt;ONLY&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;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1500ms'&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;'400ms'&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="n"&gt;COSTS&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="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;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;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;payments&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&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;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-10-02'&lt;/span&gt;
  &lt;span class="k"&gt;AND&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;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-10-09'&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&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;&lt;code&gt;BEGIN READ ONLY&lt;/code&gt; rejects writes in that transaction, even if a later statement tries to insert rows. The timeouts bound waiting and execution, but they do not certify that the selected rows are the rows the author wanted. Save the JSON plan, the SQLSTATE if the statement fails, and the wall time reported by &lt;code&gt;psql&lt;/code&gt;. If you need actual row counts, run a second statement with &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; only after the read-only plan is understood.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Ask for a critique without sending row samples
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review this SQL as text only. Do not request sample rows.
Checks: date predicate present, join keys named, selected columns minimal,
no DDL, no DML, no unbounded scan language, limit present.
Return a JSON object with pass, findings, and the fear each finding addresses.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send the statement text and the checklist, not a CSV extract and not the JSON plan's private filter values if those values are sensitive. A critique that asks for example rows is a data-handling event, and it should fail the review even if the SQL looks sound. Store the model output next to the plan, using separate fields so neither artifact overwrites the other. If the critique and the plan disagree, keep both and let the decision rule below pick the stricter gate.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Score the record with an explicit function
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Proposal only: not executed against a live database or model for this article.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;promotion_allowed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&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;writes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&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_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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dml&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;ddl&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;writes&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;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;server_evidence&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="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fear&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;locks&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;row_estimate&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;server_evidence&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="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&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;regulated_columns&lt;/span&gt;&lt;span class="sh"&gt;"&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;record&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;rows_sent_to_model&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="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&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;fixture_drift_ratio&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;record&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;drift_limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&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;record&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;human_reviewed&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="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function encodes the debate instead of hiding it inside a prompt that a later editor cannot diff. A &lt;code&gt;0.2&lt;/code&gt; drift limit is a placeholder threshold for the example, not a measured standard from a production incident. Change that placeholder only when the owning team publishes a numeric range for the masked rehearsal fixture. Run the function in your test suite against the cases in the next section before you trust it in review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence map and proposed cases
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feared failure&lt;/th&gt;
&lt;th&gt;Critique can observe it&lt;/th&gt;
&lt;th&gt;Server can observe it&lt;/th&gt;
&lt;th&gt;Acceptance rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Missing date predicate&lt;/td&gt;
&lt;td&gt;Yes, from the text&lt;/td&gt;
&lt;td&gt;Yes, as an unexpectedly large scan&lt;/td&gt;
&lt;td&gt;Either artifact may block; do not accept on silence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bad join method&lt;/td&gt;
&lt;td&gt;Only as a guess&lt;/td&gt;
&lt;td&gt;Yes, in the plan nodes&lt;/td&gt;
&lt;td&gt;Require server evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hidden write&lt;/td&gt;
&lt;td&gt;Sometimes, if the syntax is visible&lt;/td&gt;
&lt;td&gt;Yes, read-only mode errors&lt;/td&gt;
&lt;td&gt;Require server evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lock wait&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, when &lt;code&gt;lock_timeout&lt;/code&gt; fires&lt;/td&gt;
&lt;td&gt;Require server evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rows copied into a prompt&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Fail closed; neither gate excuses the copy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixture drift&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Only if you counted rows first&lt;/td&gt;
&lt;td&gt;Discard the plan and rebuild&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use the table as a test oracle for &lt;code&gt;promotion_allowed&lt;/code&gt;, not as a slogan to paste into a model prompt. The following cases are proposed inputs, and they illustrate the rule without claiming a measured pass rate. A read with fear &lt;code&gt;plan&lt;/code&gt;, server evidence present, no regulated columns, drift &lt;code&gt;0.05&lt;/code&gt;, and human review returns true. A write without server evidence returns false, even when the critique field says the statement looks safe.&lt;/p&gt;

&lt;p&gt;A regulated extract that sent sample rows to the model returns false, regardless of how clean the plan looks. An empty fixture with drift above the limit returns false, because the plan would describe the wrong data set. These cases are small on purpose, so a reviewer can implement them in minutes and argue about the rule rather than about a framework. Add a case from your own incident archive only when that archive can be shared inside the team.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Name the feared failure before you choose a tool, and write that name into the evidence record.&lt;/li&gt;
&lt;li&gt;If the statement writes, changes objects, or calls a function you have not classified, require server evidence on non-production data.&lt;/li&gt;
&lt;li&gt;If the fear is a plan shape, a lock, or a row estimate, a critique may prefilter the draft but cannot accept it.&lt;/li&gt;
&lt;li&gt;If any column is regulated, do not upload those rows to a third-party server and do not paste samples into a model prompt.&lt;/li&gt;
&lt;li&gt;If fixture drift exceeds the published threshold, discard both the plan and the critique's confidence language.&lt;/li&gt;
&lt;li&gt;Promote only when the collected artifact matches the fear, the session limits match the script, and a human has read the record.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Apply the six checks in order, and stop at the first failed check instead of averaging them into a soft score. A soft score hides a regulated-data failure behind a fluent critique and a fast rehearsal plan. The rule is intentionally strict about copies and writes, because those failures are expensive relative to a delayed review. Textual doubts can still be resolved by editing the draft and collecting a new pair of artifacts.&lt;/p&gt;

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

&lt;p&gt;This approach does not estimate production latency, and it should not be cited as a benchmark of any model or server. A masked fixture can still leak rare identifiers, so legal review remains necessary even when the SQL session is read-only. Timeouts can abort the statement before the needed plan is printed, so a timeout is not itself a safe plan. Shared rehearsal servers can also be noisy, so a slow plan there is not automatic proof of a slow plan in production.&lt;/p&gt;

&lt;p&gt;Skip the hosted free server when the fixture cannot leave a private network, or when your policy forbids third-party compute. Skip the model critique path when the statement contains secrets, customer keys, or unpublished financial figures in literal predicates. Skip the whole worksheet if the only available credentials can also reach production, because a pasted connection string defeats the rehearsal boundary. Teams that need a change-advisory approval for every query should keep this method in a lab until that process explicitly allows it.&lt;/p&gt;

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

&lt;p&gt;Before the next promotion meeting, run one unsettled draft through the six checks and keep the artifact that matches the feared failure. If those free collectors are currently available to you, use them only as described above after you confirm the live terms. That comparison is useful when it changes a decision, and it is noise when it merely adds another confident paragraph. Leave the production role untouched until the written record shows which gate actually observed the risk.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>ai</category>
      <category>testing</category>
    </item>
    <item>
      <title>Session Budgets or Driver Caps: A Debate for Agent SQL Exploration</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Thu, 08 Oct 2026 09:49:39 +0000</pubDate>
      <link>https://dev.to/dataio_4921/session-budgets-or-driver-caps-a-debate-for-agent-sql-exploration-2p0</link>
      <guid>https://dev.to/dataio_4921/session-budgets-or-driver-caps-a-debate-for-agent-sql-exploration-2p0</guid>
      <description>&lt;p&gt;A composite warehouse incident, rather than a claimed personal outage, is enough to frame this operational choice. An exploratory agent drafted a wide analytical query against a shared reporting database during an ordinary afternoon. The statement sorted a large fact table and held one pooled connection much longer than the dashboard jobs expected. Waiting sessions alone exhausted the pool, so a production lock was not required for the queue to stall.&lt;/p&gt;

&lt;p&gt;Teams that let agents explore SQL face the same narrow question after that kind of stall. Should the database session carry the budget, or should the client driver refuse excess work first? Both positions are credible, and both fail in specific, observable ways that a small fixture can expose. The rest of this note compares those positions, then gives a measurement workflow you can rerun.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two positions, stated without slogans
&lt;/h2&gt;

&lt;p&gt;A session budget is a set of PostgreSQL parameters applied to the connection before the agent statement starts. Typical controls include statement_timeout, lock_timeout, and idle_in_transaction_session_timeout, and each value is a millisecond bound on the session. The server cancels the statement, or the idle transaction, when a bound is crossed, regardless of which client sent the work. That independence is the main evidence for this position, because agents often bypass a careful application wrapper.&lt;/p&gt;

&lt;p&gt;A driver cap is a limit enforced in the client before, or while, rows are fetched from the server. Examples include a read-only transaction flag, a client-side query timeout, and a hard maximum on fetched rows. The appeal is precision, because the tool can stop a result stream without waiting for a server timeout to elapse. The weakness is coverage, because any second client, console, or retry path can skip the same wrapper.&lt;/p&gt;

&lt;p&gt;These positions are not cosmetic variants of the same control, and the difference shows up in timing. A session budget still lets the server start the work, and only then aborts the statement. A driver cap can refuse further fetches, yet the server may already have produced a batch of rows. Measuring that split matters more than preferring whichever control happens to be easier to configure locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence that should settle the argument
&lt;/h2&gt;

&lt;p&gt;Evidence should come from a fixture you control, not from a vendor latency chart or a single anecdote. The useful observations are which bound fires, how many rows leave the server, and whether the pool slot is released. Cost estimates alone do not settle this debate, and neither does a successful run against a tiny sample. A query can be cheap to plan and still pin a connection while a client slowly drains a wide result.&lt;/p&gt;

&lt;p&gt;PostgreSQL documents statement_timeout as a client-connection setting that aborts any statement exceeding the configured millisecond limit. The same manual chapter documents lock_timeout and idle_in_transaction_session_timeout as separate bounds that use different failure triggers. A single timeout therefore does not cover lock waits, long statements, and abandoned transactions at once. I am using that manual as the primary description of server behavior, and I am not claiming a hosted benchmark.&lt;/p&gt;

&lt;p&gt;Primary source: &lt;a href="https://www.postgresql.org/docs/current/runtime-config-client.html" rel="noopener noreferrer"&gt;Client Connection Defaults&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a free draft host fits
&lt;/h2&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;The operator describes MonkeyCode as an open-source project that offers free model access and a free server option. I am not restating a token quota, a model name, a hardware size, or a duration in this note. Those details were not attached as a primary source, and product terms of that kind change without notice. Check the current project documentation before you plan any workload around a number you saw elsewhere.&lt;/p&gt;

&lt;p&gt;In this workflow, free model access is only a drafting aid for two candidate statements, not a safety judge. The free server option is only a disposable PostgreSQL host for the fixture, useful when a local container is inconvenient. Neither availability claim replaces the session budget or the driver cap, and neither claim decides the debate. If those options are unavailable, run the same steps on any Postgres instance you are allowed to reset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measurement workflow
&lt;/h2&gt;

&lt;p&gt;The examples below are unexecuted proposals, and they are labeled so they are not mistaken for measured results. They use a tiny table so you can see a cancellation path without scanning a real warehouse table. Replace the connection string with a database you are allowed to reset, and never point the script at production. A passing local run still needs the decision table, because one green script does not rank the two positions.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create a resettable fixture
&lt;/h3&gt;

&lt;p&gt;Create the fixture table, load twenty thousand wide rows, and collect planner statistics before either probe starts. The payload column is intentionally wide, so a careless projection costs memory even when the row count looks modest. Identity keys keep the insert simple, and the region column gives the grouped query a small, stable cardinality. Run ANALYZE so the planner is not the accidental source of a timeout you meant to attribute to data volume.&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;-- Unexecuted example. Run only on a disposable database.&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;explore_events&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;explore_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&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;region&lt;/span&gt; &lt;span class="nb"&gt;text&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;payload&lt;/span&gt; &lt;span class="nb"&gt;text&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;occurred_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="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;explore_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&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;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&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="s1"&gt;'east'&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'west'&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&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;generate_series&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="mi"&gt;20000&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;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;explore_events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Record a session budget on a fresh connection
&lt;/h3&gt;

&lt;p&gt;Open a fresh connection, apply three local timeouts, and expect a cancellation rather than a quiet success. The sleep call is a synthetic delay, so the bound can fire without depending on your disk or CPU speed. A completed statement is not evidence that the budget works, so keep adjusting until you observe the error. The error you want, when the bound is actually hit, is SQLSTATE 57014, which means query_canceled.&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;-- Unexecuted example. Expect query_canceled if the sleep exceeds the budget.&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="k"&gt;READ&lt;/span&gt; &lt;span class="k"&gt;ONLY&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;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'150ms'&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;'100ms'&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;idle_in_transaction_session_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2s'&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;region&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="n"&gt;pg_sleep&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="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;explore_events&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;region&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;region&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Record a driver cap on another connection
&lt;/h3&gt;

&lt;p&gt;This client stops after fifty fetched rows, which is the concrete driver-cap claim under test today. It does not prove the server avoided work, because a named cursor may already have filled one batch. Capture pg_stat_activity during the fetch if you need to separate client silence from server idle time. Close the cursor by leaving the block, so an abandoned hold does not imitate a pool leak.&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;# Unexecuted example. Requires psycopg 3 and a disposable DSN.
&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;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;EXPLORE_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;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 id, region, payload
FROM explore_events
ORDER BY id
&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;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="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 statement_timeout = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;30s&lt;/span&gt;&lt;span class="sh"&gt;'"&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="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;agent_probe&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;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="n"&gt;itersize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&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="n"&gt;rows&lt;/span&gt; &lt;span class="o"&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;row&lt;/span&gt; &lt;span class="ow"&gt;in&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;rows&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="n"&gt;row&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;rows&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;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&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;len&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Compare one shell observation
&lt;/h3&gt;

&lt;p&gt;Run the activity query while each probe is in flight, using a role that can read pg_stat_activity. Note whether the session remains active, or idle in transaction, after the client has stopped reading rows. In practice, that server-side row often reverses a preference formed entirely from the client application logs. Repeat the check for both probes, because a single snapshot can miss a session that already exited.&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;# Unexecuted example. Replace the DSN, and do not use a production role.&lt;/span&gt;
psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXPLORE_DSN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT pid, state, wait_event_type, query_start
FROM pg_stat_activity
WHERE datname = current_database()
  AND pid &amp;lt;&amp;gt; pg_backend_pid();"&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;Observation on the fixture&lt;/th&gt;
&lt;th&gt;Favors session budgets&lt;/th&gt;
&lt;th&gt;Favors driver caps&lt;/th&gt;
&lt;th&gt;Practical reading&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A second client can open a session&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;The server bound is mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A statement ends quickly but streams many rows&lt;/td&gt;
&lt;td&gt;Weak alone&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Add a fetch cap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A client dies and leaves a transaction open&lt;/td&gt;
&lt;td&gt;Yes, if the idle timeout is set&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Set the idle-transaction timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You need the error in the server log&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Usually no&lt;/td&gt;
&lt;td&gt;Keep the server bound&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The wrapper is the only permitted client&lt;/td&gt;
&lt;td&gt;Still useful as a backstop&lt;/td&gt;
&lt;td&gt;Yes for row stops&lt;/td&gt;
&lt;td&gt;Use both, with the server last&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;Adopt session budgets whenever any client other than your wrapper can reach the exploration database at all. Add driver caps when the tool streams rows and a finished statement can still exhaust memory or pool time. Do not treat a driver cap as a substitute for statement_timeout, lock_timeout, or an idle-transaction timeout. If the fixture cannot show a cancellation or a stopped fetch, do not promote the control into a shared tool.&lt;/p&gt;

&lt;p&gt;Use the server budget as the backstop even when the driver cap looks sufficient on one happy script. Retry paths, SQL consoles, and later agents are new clients, and they will not inherit yesterday's wrapper settings. A read-only transaction reduces write risk, but it does not by itself limit CPU, locks, or result size. Keep the three server settings even if the fetch loop already stops at a small row count.&lt;/p&gt;

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

&lt;p&gt;This fixture uses twenty thousand rows, so it will not reproduce production planner choices or cache behavior. A statement_timeout that fires here may never fire on a selective query with a huge projected payload. Named cursors reduce client memory, yet the server can still perform substantial work before the first fetch. Free model drafts can propose unsafe SQL, so a human should read both candidates before any execution.&lt;/p&gt;

&lt;p&gt;I did not run these statements against a hosted service for this draft, and I am not reporting timings. I am also not reporting quotas, hardware sizes, or permanence for the free server option mentioned above. Availability of that option can change, and a shared host is the wrong place for private data or load generation. Roles that can write, or that can change global configuration, sit outside this read-only exploration workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should skip this approach
&lt;/h2&gt;

&lt;p&gt;Skip this approach if the workload is latency-sensitive OLTP and a canceled statement is worse than a slow one. Skip it if policy forbids session-level settings and you also cannot wrap every client that reaches the database. Skip it if the rows are production records and the only available host is a shared evaluation server. Skip it if the agent must apply migrations, because write probes need controls beyond read exploration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the choice
&lt;/h2&gt;

&lt;p&gt;Session budgets and driver caps answer different failure modes, and the fixture above makes that split visible. Prefer the server bound when you cannot enumerate every client, and add the driver bound when row volume is the risk. Re-run the four steps after any change to pool size, timeout values, or the agent tool's fetch loop. If a local database is inconvenient, confirm the current free-server terms, then run only this disposable fixture there.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>ai</category>
      <category>database</category>
    </item>
    <item>
      <title>Schema Cards or Isolated Rehearsal: A Debate for Agent SQL Drafts</title>
      <dc:creator>Morgan Li</dc:creator>
      <pubDate>Thu, 24 Sep 2026 20:15:23 +0000</pubDate>
      <link>https://dev.to/dataio_4921/schema-cards-or-isolated-rehearsal-a-debate-for-agent-sql-drafts-3jf0</link>
      <guid>https://dev.to/dataio_4921/schema-cards-or-isolated-rehearsal-a-debate-for-agent-sql-drafts-3jf0</guid>
      <description>&lt;p&gt;The staging failure below is a reconstructed example, not a measured incident from this account's history. A checkout service asked an agent to draft a backfill after refunds.amount became a stored generated column. The prompt still held an older schema card that described amount as a writable numeric field. The draft update looked legal on that card and survived a short human skim in review.&lt;/p&gt;

&lt;p&gt;Shared staging rejected the statement only after the agent had already consumed a full review cycle. The channel then split between people who wanted stricter cards and people who wanted a disposable database. Neither side had a written rule for when a paper review of the card was enough. This debate keeps both positions and ends with a rule you can apply before any promotion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two positions that both survive contact with SQL
&lt;/h2&gt;

&lt;p&gt;A schema-card position treats the attached card as the only schema the agent is allowed to trust. The card lists tables, columns, types, nullability, generated flags, and the privileges of the drafting role. Reviewers compare the SQL text with that card and refuse execution while the draft remains open. The blast radius stays small, because a wrong statement never opens a live database connection at all.&lt;/p&gt;

&lt;p&gt;An isolated-rehearsal position treats the same card as a hypothesis rather than as proof of executability. The agent may run the draft only on a database that holds synthetic rows and no production secrets. Every trial sits inside a transaction that ends with rollback, together with a short statement timeout. The useful output is an error class, not a guess about how the target server will reject the text.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this debate refuses to rerun
&lt;/h2&gt;

&lt;p&gt;This split is not a rerun of the contract-versus-catalog tool debate, which asks how an agent learns table shape. Here the card is already in hand, and the open question is whether text review is sufficient before promotion. Execution on shared staging is excluded from both winning paths in the decision rule that follows. The artifact is a hash plus a rolled-back SQLSTATE, not a preference for one metadata reading API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence you can check without a benchmark
&lt;/h2&gt;

&lt;p&gt;PostgreSQL rejects an ordinary update that assigns to a stored generated column, which a thin card can hide. In PostgreSQL's documented error codes, that rejection is SQLSTATE 428C9, and you should confirm it on your major version. A card that omits the generated flag will approve text that a matching server is required to refuse. A rehearsal on a different major version can hide the same fault, so the server version must be pinned in the run record.&lt;/p&gt;

&lt;p&gt;Catalog reads are not a substitute for that pin, because information_schema reflects the database you actually connected to. If the card was exported yesterday and the rehearsal catalog was built last month, the hashes will disagree. Disagreement is a hard stop condition, not a warning you silently override inside the agent prompt. The rest of this workflow is an unexecuted proposal you should run on disposable data before you trust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Export a schema card and hash it
&lt;/h2&gt;

&lt;p&gt;Start from a database role that can read metadata and cannot read any customer table rows. The export below keeps only the columns a reviewer needs in order to judge a write target. Pipe the copy to a hash so later runs can prove they used the same card. Store the hash beside the draft instead of pasting the whole catalog into every new prompt.&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;-- Unexecuted proposal. Confirm column names on your PostgreSQL version before use.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;column_name&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="n"&gt;is_nullable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;is_generated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;generation_expression&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;WHERE&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;table_name&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;/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;# Unexecuted proposal. REHEARSAL_URL must point at synthetic data only.&lt;/span&gt;
psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REHEARSAL_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;-At&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; card_export.sql | &lt;span class="nb"&gt;sha256sum&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Build a synthetic catalog that matches the hash
&lt;/h2&gt;

&lt;p&gt;Apply only the DDL that the card describes, using invented keys and amounts rather than a production sample. Skip extensions, collations, and row-level policies that you have not copied on purpose into this catalog. After the load, recompute the hash and stop if it differs from the card hash in the pull request. A mismatched hash means you are rehearsing a different database than the one the agent was shown.&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;-- Unexecuted proposal. Synthetic rows only.&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;refunds&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;refund_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;order_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;net_amount&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;12&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;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;fee_amount&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;12&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;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&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;12&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;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net_amount&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;fee_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;STORED&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;refunds&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;refund_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;net_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fee_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&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="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Run the draft inside a transaction that always rolls back
&lt;/h2&gt;

&lt;p&gt;Give the agent a rehearsal role that can write the synthetic tables and nothing else at all. Set a local statement timeout so a bad join cannot occupy the session for the rest of the review. Execute the candidate text, capture the SQLSTATE, and roll back even when the statement succeeds cleanly. Success inside this wrapper means the text parsed and ran on this catalog, not that it is safe for production.&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;-- Unexecuted proposal. Always end in ROLLBACK.&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;statement_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="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pg_temp&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;refunds&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;refund_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&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;On the table above, that update should fail because amount is generated always as an expression. Record 428C9 if your version returns it, and record the exact message if it does not. Do not edit the draft until the recorded class matches the failure you intended to catch. A timeout, a missing table, or a permission error is a different defect and needs its own row in the log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Score the two positions with the same run record
&lt;/h2&gt;

&lt;p&gt;Use one row per draft so the debate does not depend on memory of the review thread. The columns below are deliberately boring, because a promotion meeting should be able to read them aloud. Fill them from the hash command and the rolled-back session, not from the agent's own summary. If a cell is unknown, the decision rule later in this article treats the draft as not ready.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Schema-card only&lt;/th&gt;
&lt;th&gt;Isolated rehearsal&lt;/th&gt;
&lt;th&gt;Stop if&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Card hash stored with the draft&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;Hash missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generated and identity flags present&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;Flag omitted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write executed somewhere&lt;/td&gt;
&lt;td&gt;Forbidden&lt;/td&gt;
&lt;td&gt;Allowed on synthetic data&lt;/td&gt;
&lt;td&gt;Shared staging was used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLSTATE captured&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;td&gt;Required for writes&lt;/td&gt;
&lt;td&gt;Class unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version pin recorded&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;Rehearsal version differs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback proven&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;Session committed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A decision rule for promotion
&lt;/h2&gt;

&lt;p&gt;Choose the schema-card position when the statement is read-only and names only columns on the hashed card. Also require that the statement does not call a function with side effects or a volatile helper. In that narrow case, execution adds little evidence and a banned connection is the safer default. Require isolated rehearsal when the statement writes or targets a generated or identity column at all.&lt;/p&gt;

&lt;p&gt;Also require rehearsal when the statement joins tables whose row counts are absent from the card. Refuse both options when the only reachable database is shared staging or production, then return the draft to a named owner. A named owner is a person who can change grants, not the agent that produced the SQL text.&lt;/p&gt;

&lt;p&gt;Read-only does not mean harmless, because a select can still lock rows if the agent adds a locking clause. Treat FOR UPDATE, FOR SHARE, and any explicit lock statement as a write for this rule. Also treat DDL, COPY, and calls that insert into audit tables as writes even when the main verb looks like a query. The rule is conservative on purpose, and it will send some safe drafts through rehearsal that a senior reviewer might have waived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a free drafting setup fits
&lt;/h2&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode, supplied here as an open-source option, offers free model access and a free server for rehearsal. Use that pair only as the rehearsal role described here, with the hashed card attached to the prompt. Keep rollback as the default ending, and do not send production connection strings, dumps, or customer keys.&lt;/p&gt;

&lt;p&gt;It does not replace the decision rule, and it should never receive a credential that can reach shared staging. The model can propose the update text, while the free server is only useful if it can run your pinned engine. Keep the agent on the rehearsal role so a confident review note cannot open a broader grant. If the free server cannot load an extension you depend on, stop and record that limitation instead of using shared staging.&lt;/p&gt;

&lt;p&gt;A missing extension is a catalog mismatch, and the hash check is already designed to catch that class of drift. If your synthetic catalogs already exist, the free model access and the free server option can host the next write rehearsal. Do not treat that hosting convenience as evidence that the generated SQL is correct on your production major version. Confirm the version pin in the run record before anyone promotes the text beyond the rehearsal role.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations that should stay visible
&lt;/h2&gt;

&lt;p&gt;This workflow does not estimate cost, lock duration, or rows touched on a production-sized refund table. A three-second timeout on synthetic data says nothing about a sequential scan against a much larger refund table. Generated-column rules and identity rules vary across major versions, so a pin from last year is not current evidence. Free server capacity can also differ in collation, timezone, and installed extensions, which is why the hash remains mandatory.&lt;/p&gt;

&lt;p&gt;The reconstructed checkout story is only a teaching frame, and it is not a customer result or a measured outage count. No latency, token, or accuracy figure is claimed here, because this account did not run a comparative benchmark for this draft. If your operator later supplies versioned product limits, cite those primary notes beside the step that depends on them. Until then, treat availability of a free server as a convenience, not as a guarantee that the option remains unchanged.&lt;/p&gt;

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

&lt;p&gt;Skip the rehearsal half if policy forbids sending schema names to an external model, even when the rows are synthetic. Skip it if the workload needs an extension, foreign-data wrapper, or collation that the free server cannot load. Skip the card-only half if reviewers cannot reliably see generated flags, identity flags, and grants on the attachment. Skip the whole debate if the agent already holds a write credential on a shared database, because that credential breaks the rule.&lt;/p&gt;

&lt;p&gt;Teams that promote SQL through a locked migration tool may already have a stronger gate than either position. They can still borrow the hash and the SQLSTATE log without adopting the full agent loop. Everyone else should keep the stop row in the table, since an unknown cell is cheaper than a committed staging trial. The next useful change is a stricter card, or a cleaner synthetic catalog, not a broader database role.&lt;/p&gt;

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