<?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: Eitamos Ring</title>
    <description>The latest articles on DEV Community by Eitamos Ring (@eitamos_ring_0508146ca448).</description>
    <link>https://dev.to/eitamos_ring_0508146ca448</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%2F3395918%2F5ae6659e-ea1d-4142-b982-ec79776600b3.png</url>
      <title>DEV Community: Eitamos Ring</title>
      <link>https://dev.to/eitamos_ring_0508146ca448</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eitamos_ring_0508146ca448"/>
    <language>en</language>
    <item>
      <title>Why Regex Sucks in a Hot Loop</title>
      <dc:creator>Eitamos Ring</dc:creator>
      <pubDate>Wed, 17 Jun 2026 13:41:49 +0000</pubDate>
      <link>https://dev.to/eitamos_ring_0508146ca448/why-regex-sucks-in-a-hot-loop-f4k</link>
      <guid>https://dev.to/eitamos_ring_0508146ca448/why-regex-sucks-in-a-hot-loop-f4k</guid>
      <description>&lt;p&gt;A while back I ripped a regex out of my SQL parser and replaced it with twenty lines of hand-written string scanning. Then I got nervous. Hand-rolling a scanner because you assume regex is slow is exactly the kind of premature optimization I make fun of other people for. So I filed an issue against myself: prove the hand-rolled version is actually faster, or delete it and go back to the regex.&lt;/p&gt;

&lt;p&gt;Months later I sat down to settle it. The honest regex came back about ninety times slower. And the reason had nothing to do with raw matching speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the code does
&lt;/h2&gt;

&lt;p&gt;The function answers one small question: does a specific table alias appear in this piece of SQL, followed by a dot? That's how the parser notices a subquery reaching out to an outer table, like &lt;code&gt;o.id&lt;/code&gt; pointing at an &lt;code&gt;orders o&lt;/code&gt; defined outside it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;containsWordDot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&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="n"&gt;word&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;needle&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"."&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;needle&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;pos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&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="no"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;absPos&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;
        &lt;span class="c"&gt;// the char before the alias must not be part of a longer identifier,&lt;/span&gt;
        &lt;span class="c"&gt;// so alias "a" doesn't match "data."&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;absPos&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;rune&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;absPos&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&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;unicode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsLetter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;unicode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsDigit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sc"&gt;'_'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;absPos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;continue&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="no"&gt;true&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;It's not pretty. The boundary check is the only reason it exists, because a plain &lt;code&gt;strings.Contains&lt;/code&gt; would happily match &lt;code&gt;a.&lt;/code&gt; inside &lt;code&gt;data.&lt;/code&gt; and invent a correlation that was never there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The case for going back to regex
&lt;/h2&gt;

&lt;p&gt;My own argument against the code was simple. Go's regex engine is RE2: deterministic, no catastrophic backtracking, genuinely fast. Compile one pattern once at startup and reuse it everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wordDotPattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;regexp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MustCompile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`\b\w+\.\w+`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Less code. No manual boundary handling to get wrong. If it benchmarked even close to the hand-rolled version, deleting twenty fiddly lines was the right move. I expected to delete them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the plan fell apart
&lt;/h2&gt;

&lt;p&gt;That regex matches &lt;em&gt;any&lt;/em&gt; word-dot-word. My function answers a narrower question: does &lt;em&gt;this specific alias&lt;/em&gt; appear? And the alias is different on every call, because the parser walks every table in the query and asks about each one in turn.&lt;/p&gt;

&lt;p&gt;So the precompiled pattern can't replace the function. It's answering a question nobody asked. The only regex that does the same job has to bake the specific alias into the pattern, which means compiling a fresh regex every single call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;regexp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MustCompile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`\b`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;regexp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QuoteMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&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;MatchString&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line is the whole story. You can't compile a pattern once when the pattern changes every time you run it. "Compile once, reuse forever" quietly dies the moment you notice the thing it depends on isn't a constant. And compiling a regex is not cheap. Doing it in a loop, once per table per query, is a bill you pay on every parse for the rest of the program's life.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;I ran the matrix I'd written into the issue: short input (~20 bytes), medium (~200), long (~2000), each as a match, a miss, and a near-miss. Ten runs each, medians:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input        scanner      regex (compiled per call)
short/miss     21 ns          1,972 ns
short/hit      24 ns          1,990 ns
medium/hit    195 ns          6,252 ns
long/hit    1,566 ns         46,600 ns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six to ninety times slower depending on size, and the scanner did it with zero allocations while the regex allocated on every call.&lt;/p&gt;

&lt;p&gt;For a sanity check I also benchmarked the precompiled regex, the one that can't actually do the job. The scanner still won at every size, because &lt;code&gt;strings.Index&lt;/code&gt; is SIMD-accelerated and allocates nothing, while even a warm &lt;code&gt;MatchString&lt;/code&gt; carries per-call overhead. There was no input length where regex caught up. No crossover at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  So does regex suck?
&lt;/h2&gt;

&lt;p&gt;Not really.&lt;br&gt;
The title is a little unfair, and I'll own that, I needed to catch your eye a bit :)&lt;/p&gt;

&lt;p&gt;Regex is the right tool sometimes, and a state machine I wrote by hand is often the thing that deserves to be deleted&lt;/p&gt;

&lt;p&gt;What actually sucks is reaching for regex without noticing that your pattern isn't constant.&lt;br&gt;
The cost of a regex is split in two: compiling it, and matching with it. Everyone remembers matching is fast.&lt;/p&gt;

&lt;p&gt;People forget compiling is the expensive half, and that you only get to skip it when the pattern is fixed. Put a per-call variable in the pattern and you've moved the expensive half into your hot path without realizing it.&lt;/p&gt;

&lt;p&gt;The benchmark earned its place, but not the way I thought it would. It didn't just tell me which option was faster. Forcing myself to write the genuinely equivalent regex is what revealed there was no equivalent precompiled regex in the first place. Making the comparison fair was where the real answer was hiding.&lt;/p&gt;

&lt;p&gt;I kept the twenty lines and left the benchmark numbers in a comment on top, so the next person who thinks "this should just be a regex" can read the receipts instead of arguing with me about it. Which is exactly what past-me wanted when I filed the issue.&lt;/p&gt;

&lt;p&gt;Measure the thing you'd actually ship. Not the thing that's easy to type into a benchmark.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This came out of &lt;a href="https://github.com/ValkDB/postgresparser/issues/59" rel="noopener noreferrer"&gt;issue #59&lt;/a&gt; on postgresparser, a pure-Go PostgreSQL parser I work on. The full benchmark and verdict live in the issue.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>database</category>
      <category>go</category>
      <category>software</category>
    </item>
    <item>
      <title>Parse, Don’t Guess</title>
      <dc:creator>Eitamos Ring</dc:creator>
      <pubDate>Thu, 11 Jun 2026 22:24:01 +0000</pubDate>
      <link>https://dev.to/eitamos_ring_0508146ca448/parse-dont-guess-3710</link>
      <guid>https://dev.to/eitamos_ring_0508146ca448/parse-dont-guess-3710</guid>
      <description>&lt;p&gt;&lt;em&gt;Three days before going open source, I deleted my parser's smartest feature.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;333 lines. 6 functions. 12 passing tests. All green, all clever, all gone.&lt;/p&gt;

&lt;p&gt;The feature worked. That was the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature that knew too much
&lt;/h2&gt;

&lt;p&gt;My PostgreSQL parser (&lt;a href="https://github.com/ValkDB/postgresparser" rel="noopener noreferrer"&gt;valk-postgres-parser&lt;/a&gt;) extracts structure from SQL text: tables, columns, joins, filters. One of its analysis functions did something more ambitious. Give it a query like this:&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;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and it would tell you the foreign key relationship: &lt;code&gt;orders.customer_id&lt;/code&gt; is a child pointing at parent &lt;code&gt;customers.id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No schema. No catalog access. Just the query text. It felt like magic, and users love magic.&lt;/p&gt;

&lt;p&gt;Here is how the magic worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;isForeignKeyColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetTable&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasSuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"_id"&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&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="no"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c"&gt;// Also check if table contains the prefix (handles prefixed&lt;/span&gt;
        &lt;span class="c"&gt;// tables like fk_customers). This allows customer_id to match&lt;/span&gt;
        &lt;span class="c"&gt;// fk_customers, e2e_customers, etc.&lt;/span&gt;
        &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Naming conventions. &lt;code&gt;customer_id&lt;/code&gt; next to a table called &lt;code&gt;customers&lt;/code&gt;? Must be a foreign key. Ship it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comment that aged badly
&lt;/h2&gt;

&lt;p&gt;The real problem was further down, in the fallback. What happens when neither column matches a naming pattern? The function did this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// If we can't determine from FK naming conventions, return a default&lt;/span&gt;
&lt;span class="c"&gt;// relationship based on table order (left table = parent by convention&lt;/span&gt;
&lt;span class="c"&gt;// in SQL JOINs). This is still a heuristic but is consistent with how&lt;/span&gt;
&lt;span class="c"&gt;// JOINs are typically written.&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;JoinRelationship&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ChildTable&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;rightTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ParentTable&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;leftTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that again. When the function had no idea, it did not say "I have no idea." It returned an answer anyway, based on which table appeared first in the query.&lt;/p&gt;

&lt;p&gt;Every JOIN got an answer. &lt;strong&gt;That was the bug.&lt;/strong&gt; Not a crash, not a parse error. A function that is sometimes right, sometimes wrong, and gives the caller no way to tell which.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong is worse than empty
&lt;/h2&gt;

&lt;p&gt;Think about what downstream code does with a foreign key relationship. In our case it generated test data: parent rows first, then children referencing them. Flip parent and child and you get foreign key violations, or worse, data that inserts fine but means the wrong thing.&lt;/p&gt;

&lt;p&gt;An empty result fails loudly. The caller sees nothing came back and handles it. A wrong result fails quietly, three layers up, a week later, in a system that trusted the library.&lt;/p&gt;

&lt;p&gt;And the heuristic had plenty of ways to be wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;customer_id&lt;/code&gt; happily matched tables named &lt;code&gt;fk_customers&lt;/code&gt; and &lt;code&gt;e2e_customers&lt;/code&gt;, because of a prefix check added for one test environment&lt;/li&gt;
&lt;li&gt;Self-referencing tables (&lt;code&gt;employees.manager_id&lt;/code&gt;) confused the direction logic&lt;/li&gt;
&lt;li&gt;Junction tables, where both joined columns are primary keys, got an arbitrary winner&lt;/li&gt;
&lt;li&gt;And the table-order fallback was a coin flip dressed up as a convention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each bug was fixable. Another pattern, another special case, another test. That is exactly how the function grew to be the smartest 333 lines in the codebase. The line count was not the cost. The confidence was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The replacement: facts or nothing
&lt;/h2&gt;

&lt;p&gt;The fix was not a better heuristic. It was a contract change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;][]&lt;/span&gt;&lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ColumnSchema&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"customers"&lt;/span&gt;&lt;span class="o"&gt;:&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="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PGType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"bigint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IsPrimaryKey&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="o"&gt;:&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="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IsPrimaryKey&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&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="s"&gt;"customer_id"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;joins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExtractJoinRelationshipsWithSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// orders.customer_id -&amp;gt; customers.id, because the schema says id is a PK.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You pass schema metadata, you get relationships derived from actual primary keys. You do not pass schema, you get nothing. If the metadata cannot settle a case (both sides are primary keys), the answer is nil, not a guess.&lt;/p&gt;

&lt;p&gt;Deleting the no-schema path meant deleting tested, working, green code. The 12 tests I removed were not failing. They were carefully asserting that the guesses came out the way the guesses come out. Tests that lock in behavior nobody should rely on are not coverage. They are a fence around a landmine.&lt;/p&gt;

&lt;p&gt;The migration cost turned out to be near zero: every production caller already had schema metadata sitting right there. They had just never been asked for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I did not expect
&lt;/h2&gt;

&lt;p&gt;Last week a stranger opened an issue on the repo. He was analyzing hundreds of queries and hit a case where a WHERE clause column was not qualified by a table name, so the parser left the table field empty.&lt;/p&gt;

&lt;p&gt;He did not ask the parser to guess. He asked for &lt;code&gt;ExtractWhereConditionsWithSchema&lt;/code&gt;, by name, with the same schema-map shape the JOIN function uses. The design had taught him what to ask for.&lt;/p&gt;

&lt;p&gt;That is the moment you find out an API decision was right: when users start requesting extensions to the constraint instead of exceptions from it.&lt;/p&gt;

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

&lt;p&gt;You probably know &lt;a href="https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/" rel="noopener noreferrer"&gt;"Parse, don't validate"&lt;/a&gt;: make illegal states unrepresentable by parsing input into types that carry proof. This is the next clause of the same contract. Parsing tells the truth about what the input &lt;em&gt;is&lt;/em&gt;. It must also tell the truth about what it &lt;em&gt;cannot know&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If your library cannot know, it must say so. An empty result is an answer. A guess is a liability with good marketing.&lt;/p&gt;

&lt;p&gt;We are currently spending billions of dollars teaching language models to say "I don't know" instead of hallucinating a confident answer. Your API can do it for free, in the type system, today: take the inputs that make the answer knowable, and return nothing when it is not.&lt;/p&gt;

&lt;p&gt;The smartest code I ever deleted is the reason people trust what is left.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The parser is open source at &lt;a href="https://github.com/ValkDB/postgresparser" rel="noopener noreferrer"&gt;github.com/ValkDB/postgresparser&lt;/a&gt;, 260 stars and counting. Issues and PRs welcome, especially the ones that ask for facts instead of guesses.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>go</category>
      <category>postgres</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Microsecond Lie: Why your Go timers are lying about the GPU</title>
      <dc:creator>Eitamos Ring</dc:creator>
      <pubDate>Sat, 23 May 2026 19:12:52 +0000</pubDate>
      <link>https://dev.to/eitamos_ring_0508146ca448/the-microsecond-lie-why-your-go-timers-are-lying-about-the-gpu-2gp5</link>
      <guid>https://dev.to/eitamos_ring_0508146ca448/the-microsecond-lie-why-your-go-timers-are-lying-about-the-gpu-2gp5</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I thought my CUDA kernel was running in 160 microseconds. I was wrong. Here is how I used CUDA Events in pure Go to find the real hardware time, and why CPU-side timers are the wrong tool for GPU forensics.&lt;/p&gt;




&lt;p&gt;I wrapped my kernel launch in a standard Go &lt;code&gt;time.Since(start)&lt;/code&gt; block and saw &lt;strong&gt;162 microseconds&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;I thought I had built a speed demon. Then I implemented real GPU Events and found the truth.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Misleading Metric
&lt;/h3&gt;

&lt;p&gt;When you launch a CUDA kernel, it is completely asynchronous. The CPU doesn't wait for the GPU to finish; it just puts the task in a queue (a Stream) and returns control to your Go program immediately. &lt;/p&gt;

&lt;p&gt;My 162-microsecond measurement wasn't measuring the math. It was only measuring how long it took the Go runtime to talk to the NVIDIA driver and enqueue the job. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The GPU hadn't even finished the first row of the matrix before my timer stopped.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Hardware Truth (RTX 4070 Ti)
&lt;/h3&gt;

&lt;p&gt;To find the real numbers, I had to implement &lt;strong&gt;CUDA Events&lt;/strong&gt;. These are markers you place directly into the hardware stream. The GPU itself records a timestamp when it reaches the marker, bypassing the CPU clock entirely.&lt;/p&gt;

&lt;p&gt;I ran a 10M element vector addition on an RTX 4070 Ti. Here is what the hardware actually said:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement Method&lt;/th&gt;
&lt;th&gt;Reported Time&lt;/th&gt;
&lt;th&gt;What it actually measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU &lt;code&gt;time.Since&lt;/code&gt; (Async)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~160 µs&lt;/td&gt;
&lt;td&gt;Time to enqueue the work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GPU &lt;code&gt;cuda.Event&lt;/code&gt; (Actual)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~434 µs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Actual compute time on Silicon&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU &lt;code&gt;time.Since&lt;/code&gt; (with Sync)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~404 µs&lt;/td&gt;
&lt;td&gt;Enqueue + Execution + Runtime overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The hardware compute time was &lt;strong&gt;2.7x slower&lt;/strong&gt; than what my CPU timers led me to believe. &lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation in Pure Go
&lt;/h3&gt;

&lt;p&gt;Measuring this accurately required adding &lt;code&gt;NewEvent&lt;/code&gt;, &lt;code&gt;Record&lt;/code&gt;, and &lt;code&gt;ElapsedTime&lt;/code&gt; to the &lt;code&gt;gocudrv&lt;/code&gt; package. Since we aren't using cgo, I had to bind the &lt;code&gt;cuEventElapsedTime&lt;/code&gt; symbols manually and handle the C-to-Go float32 conversion.&lt;/p&gt;

&lt;p&gt;Here is what the "truth-telling" code looks like now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// 1. Create the hardware stopwatches&lt;/span&gt;
&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewEvent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewEvent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c"&gt;// 2. Place markers in the stream&lt;/span&gt;
&lt;span class="n"&gt;start&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="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LaunchOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&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="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stop&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="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// 3. Wait for the STOP marker to be reached&lt;/span&gt;
&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Synchronize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// 4. Get the hardware duration&lt;/span&gt;
&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Elapsed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Actual GPU time: %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Lesson for AI Infrastructure
&lt;/h3&gt;

&lt;p&gt;As we move toward Go-based AI infrastructure, we have to be careful about &lt;strong&gt;"Measurement Drift."&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;If you are building an inference gateway or a real-time image processor in Go, using CPU timers will make your P99s look incredible on paper while your users experience mysterious latency. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can't optimize what you can't measure.&lt;/strong&gt; If you aren't using hardware events, you are just measuring the speed of your request queue, not the speed of your product.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Next?
&lt;/h3&gt;

&lt;p&gt;Now that I have a microsecond-accurate stopwatch, I can finally start optimizing the data path. I'm currently working on &lt;strong&gt;CUDA Graphs&lt;/strong&gt; to reduce that 160µs enqueuing overhead by bundling complex task topologies into a single hardware command.&lt;/p&gt;

&lt;p&gt;If you're interested in the forensics of low-level Go or want to help build the cgo-free bridge, check out the progress on GitHub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/eitamring/gocudrv" rel="noopener noreferrer"&gt;https://github.com/eitamring/gocudrv&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>go</category>
      <category>cuda</category>
    </item>
    <item>
      <title>Deleting the 8.4GB Python Sidecar: Pure Go + CUDA with `CGO_ENABLED=0`</title>
      <dc:creator>Eitamos Ring</dc:creator>
      <pubDate>Wed, 20 May 2026 04:47:04 +0000</pubDate>
      <link>https://dev.to/eitamos_ring_0508146ca448/deleting-the-84gb-python-sidecar-pure-go-cuda-with-cgoenabled0-3268</link>
      <guid>https://dev.to/eitamos_ring_0508146ca448/deleting-the-84gb-python-sidecar-pure-go-cuda-with-cgoenabled0-3268</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built &lt;code&gt;gocudrv&lt;/code&gt; so Go services can talk directly to NVIDIA GPUs — no cgo, no CUDA toolkit, no bloated Python dependencies. One static binary.&lt;/p&gt;

&lt;p&gt;Last month I was reviewing a production AI service. The core business logic was clean, efficient Go (15MB binary), but GPU access was routed through a Python sidecar.&lt;/p&gt;

&lt;p&gt;The results were painful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;8.4GB Docker images&lt;/strong&gt; — bloated with unused CUDA toolkits and PyTorch dependencies&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;4-minute cold starts&lt;/strong&gt; during autoscaling&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Extra serialization + network hops&lt;/strong&gt; between Go → Python → GPU&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We had accepted this because “GPUs belong to Python.” I decided to challenge that assumption.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Impossible Build: &lt;code&gt;CGO_ENABLED=0&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Most Go developers assume you need cgo for CUDA. Instead, I used the &lt;strong&gt;CUDA Driver API&lt;/strong&gt; (already present wherever an NVIDIA driver is installed) together with &lt;code&gt;purego&lt;/code&gt; to bypass the C compiler entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// internal/platform/platform_linux.go&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;LibraryCandidates&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"libcuda.so.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"/usr/lib/x86_64-linux-gnu/libcuda.so.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"/usr/lib/wsl/lib/libcuda.so.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;// Works seamlessly in WSL2&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;&lt;code&gt;gocudrv&lt;/code&gt; loads &lt;code&gt;libcuda.so&lt;/code&gt; at runtime. Standard &lt;code&gt;go build&lt;/code&gt; works — even when building on a Mac targeting Linux.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Receipts: Size &amp;amp; Build Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Python Sidecar Approach&lt;/th&gt;
&lt;th&gt;gocudrv (Pure Go)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Artifact Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~8,400 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.4 MB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5–10 minutes (Docker)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&amp;lt; 2 seconds&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;External Dependencies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python + PyTorch + CUDA Toolkit&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;NVIDIA Driver only&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment Simplicity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multiple processes + networking&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Single static binary&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Low-Level Kernel Performance (10M element vector add on RTX 4070 Ti)
&lt;/h3&gt;

&lt;p&gt;For a simple vector addition (~114 MB data):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  H→D Copy: &lt;strong&gt;19.3 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  Kernel Launch: &lt;strong&gt;3.4 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  D→H Copy: &lt;strong&gt;25.6 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Total GPU Pipeline:&lt;/strong&gt; &lt;strong&gt;48.3 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These numbers represent the raw GPU work. In the previous Python sidecar setup, we also paid extra for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  JSON/Protobuf serialization&lt;/li&gt;
&lt;li&gt;  Local network socket transfer (Go → Python)&lt;/li&gt;
&lt;li&gt;  Python interpreter + PyTorch overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real win is not necessarily beating PyTorch on micro-benchmarks, but removing the entire sidecar layer and its operational complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Beyond a Simple Wrapper
&lt;/h3&gt;

&lt;p&gt;Pure Go doesn’t mean slow. I focused on &lt;strong&gt;asynchronous overlap&lt;/strong&gt; from the beginning to hide PCIe transfer latency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewStream&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c"&gt;// Start DMA transfer — returns immediately&lt;/span&gt;
&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CopyFromHostAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hostBuffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Go can do useful work while the GPU is computing&lt;/span&gt;
&lt;span class="c"&gt;// ...&lt;/span&gt;

&lt;span class="c"&gt;// Synchronize only when needed&lt;/span&gt;
&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Synchronize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why This Matters in 2026
&lt;/h3&gt;

&lt;p&gt;AI is shifting from research demos to critical infrastructure. Go excels at stability, concurrency, observability, and operational predictability — exactly what production model serving demands.&lt;/p&gt;

&lt;p&gt;Removing the Python sidecar gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Dramatically smaller images&lt;/strong&gt; and faster deploys.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Much better cold start times.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Single language and single binary&lt;/strong&gt; (much simpler observability and debugging).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;No GIL,&lt;/strong&gt; better P99 tail latencies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Current State (Honest)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;gocudrv&lt;/code&gt; is still early and experimental. Core functionality works today (device management, memory management, PTX loading, streams, async copies), but it is not yet ready for complex high-performance inference serving.&lt;/p&gt;

&lt;p&gt;I’m actively working on CUDA Graphs, Events &amp;amp; Timing, and multi-GPU support.&lt;/p&gt;

&lt;p&gt;If you’re a Go engineer tired of carrying heavy Python AI runtimes in production, I’d love your feedback and contributions.&lt;/p&gt;

&lt;p&gt;→ &lt;strong&gt;&lt;a href="https://github.com/eitamring/gocudrv" rel="noopener noreferrer"&gt;link&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
      <category>go</category>
    </item>
    <item>
      <title>Why LLMs Run on GPUs, Not CPUs</title>
      <dc:creator>Eitamos Ring</dc:creator>
      <pubDate>Mon, 18 May 2026 07:15:03 +0000</pubDate>
      <link>https://dev.to/eitamos_ring_0508146ca448/why-llms-run-on-gpus-not-cpus-m6h</link>
      <guid>https://dev.to/eitamos_ring_0508146ca448/why-llms-run-on-gpus-not-cpus-m6h</guid>
      <description>&lt;h2&gt;
  
  
  It’s not because GPUs are magic AI chips
&lt;/h2&gt;

&lt;p&gt;I used to think GPUs were used for LLMs because they were special hardware built for AI.&lt;/p&gt;

&lt;p&gt;That is close, but not really the useful answer.&lt;/p&gt;

&lt;p&gt;The better answer is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;LLMs became GPU-shaped.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A GPU does not understand language.&lt;br&gt;
It does not reason.&lt;br&gt;
It does not know what your prompt means.&lt;/p&gt;

&lt;p&gt;It is just very good at doing the same numeric work across a massive amount of data.&lt;/p&gt;

&lt;p&gt;And that is mostly what modern LLM inference is.&lt;/p&gt;
&lt;h2&gt;
  
  
  From text to numbers
&lt;/h2&gt;

&lt;p&gt;When you send a prompt to an LLM, the model is not reading it like a person.&lt;/p&gt;

&lt;p&gt;Your prompt becomes tokens.&lt;br&gt;
Tokens become numbers.&lt;br&gt;
Those numbers move through many layers.&lt;/p&gt;

&lt;p&gt;At a very simplified level, the model keeps doing this:&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="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is matrix multiplication.&lt;/p&gt;

&lt;p&gt;For a tiny model, a CPU is fine.&lt;/p&gt;

&lt;p&gt;For a 7B, 70B, or 405B parameter model, this becomes a ridiculous amount of repeated numeric work.&lt;/p&gt;

&lt;p&gt;That is where GPUs win.&lt;/p&gt;

&lt;p&gt;A CPU is great at flexible logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAdmin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;loadDashboard&lt;/span&gt;&lt;span class="p"&gt;()&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;loadLimitedView&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;A GPU is great at repeated parallel work:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do the same operation across millions of values.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not because the work is smart.&lt;/p&gt;

&lt;p&gt;Because there is a lot of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottleneck most developers feel: memory
&lt;/h2&gt;

&lt;p&gt;Raw math is only part of the story.&lt;/p&gt;

&lt;p&gt;The bigger issue is often memory movement.&lt;/p&gt;

&lt;p&gt;A 70B model in FP16 is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;70B parameters × 2 bytes = 140GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is just the weights.&lt;/p&gt;

&lt;p&gt;So when the model generates text, it is not only “doing math.”&lt;br&gt;
It also has to keep moving a huge amount of model data fast enough to produce the next token.&lt;/p&gt;

&lt;p&gt;That is why normal developers hit questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why do I need so much VRAM?&lt;/li&gt;
&lt;li&gt;Why is CPU offload so slow?&lt;/li&gt;
&lt;li&gt;Why does quantization help?&lt;/li&gt;
&lt;li&gt;Why does long context get expensive?&lt;/li&gt;
&lt;li&gt;Why does my big GPU look underused with one request?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answer is usually data movement.&lt;/p&gt;

&lt;p&gt;Not just compute.&lt;/p&gt;

&lt;p&gt;If the model fits in GPU memory, life is better.&lt;/p&gt;

&lt;p&gt;If parts of it spill to CPU memory, every generated token can get slower.&lt;/p&gt;

&lt;p&gt;If you reduce the model size with quantization, there is less data to move.&lt;/p&gt;

&lt;p&gt;That is why quantized models often feel faster.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why quantization helps
&lt;/h2&gt;

&lt;p&gt;Quantization reduces how much data the system has to store and move.&lt;/p&gt;

&lt;p&gt;Roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FP16 = 2 bytes per parameter
INT8 = 1 byte per parameter
INT4 = 0.5 bytes per parameter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a 70B model looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FP16  → 140GB
INT8  → 70GB
INT4  → 35GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same basic model shape.&lt;/p&gt;

&lt;p&gt;Much less data to push through memory.&lt;/p&gt;

&lt;p&gt;That is the practical reason quantization matters.&lt;/p&gt;

&lt;p&gt;It is not just “smaller model file.”&lt;/p&gt;

&lt;p&gt;It can change whether the model fits in VRAM at all.&lt;/p&gt;

&lt;p&gt;And fitting in VRAM is often the difference between usable and painful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefill vs decode
&lt;/h2&gt;

&lt;p&gt;LLM inference has two very different phases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefill&lt;/strong&gt; is when the model processes your prompt.&lt;/p&gt;

&lt;p&gt;The prompt is already available, so the GPU can do a lot of work in parallel.&lt;/p&gt;

&lt;p&gt;This phase fits GPUs well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decode&lt;/strong&gt; is when the model generates the response.&lt;/p&gt;

&lt;p&gt;This happens one token at a time.&lt;/p&gt;

&lt;p&gt;Token 1.&lt;br&gt;
Then token 2.&lt;br&gt;
Then token 3.&lt;/p&gt;

&lt;p&gt;The model cannot fully generate token 50 before token 49 exists.&lt;/p&gt;

&lt;p&gt;That is why a powerful GPU can still look underused when serving one request.&lt;/p&gt;

&lt;p&gt;The GPU is huge.&lt;/p&gt;

&lt;p&gt;The work is arriving in small steps.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why batching matters
&lt;/h2&gt;

&lt;p&gt;Batching is how serving systems keep GPUs busy.&lt;/p&gt;

&lt;p&gt;One request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Generate&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Many requests together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&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="n"&gt;promptA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;promptB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;promptC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;promptD&lt;/span&gt;&lt;span class="p"&gt;,&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;GenerateBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the GPU can process many next-token steps together.&lt;/p&gt;

&lt;p&gt;The same model weights can be reused across more work.&lt;/p&gt;

&lt;p&gt;That is the real reason batching matters.&lt;/p&gt;

&lt;p&gt;It is not just:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;More work means busier GPU.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use every expensive memory read for as much useful math as possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is why LLM serving is not just “put model on GPU.”&lt;/p&gt;

&lt;p&gt;It is batching, scheduling, memory layout, and keeping the hardware fed.&lt;/p&gt;

&lt;p&gt;A bad serving system wastes expensive GPU time.&lt;/p&gt;

&lt;p&gt;A good one keeps the GPU busy.&lt;/p&gt;

&lt;h2&gt;
  
  
  So why GPUs?
&lt;/h2&gt;

&lt;p&gt;We use GPUs for LLMs because the workload is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;huge&lt;/li&gt;
&lt;li&gt;numeric&lt;/li&gt;
&lt;li&gt;repetitive&lt;/li&gt;
&lt;li&gt;parallel enough&lt;/li&gt;
&lt;li&gt;very sensitive to memory movement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The GPU is not the brain.&lt;/p&gt;

&lt;p&gt;The model is the brain.&lt;/p&gt;

&lt;p&gt;The GPU is the engine that makes the brain fast enough to use.&lt;/p&gt;

&lt;p&gt;LLMs do not run on GPUs because GPUs are magic AI machines.&lt;/p&gt;

&lt;p&gt;They run there because the workload matches what GPUs are good at.&lt;/p&gt;

&lt;p&gt;Once that clicks, a lot of practical things make more sense:&lt;/p&gt;

&lt;p&gt;Why VRAM matters.&lt;br&gt;
Why batching matters.&lt;br&gt;
Why quantization helps.&lt;br&gt;
Why CPU offload hurts.&lt;br&gt;
Why long context is expensive.&lt;br&gt;
Why “just use a bigger GPU” is not always the full answer.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Calling CUDA from Go without cgo</title>
      <dc:creator>Eitamos Ring</dc:creator>
      <pubDate>Sat, 16 May 2026 14:37:43 +0000</pubDate>
      <link>https://dev.to/eitamos_ring_0508146ca448/calling-cuda-from-go-without-cgo-3kfi</link>
      <guid>https://dev.to/eitamos_ring_0508146ca448/calling-cuda-from-go-without-cgo-3kfi</guid>
      <description>&lt;p&gt;I started &lt;code&gt;gocudrv&lt;/code&gt; with one constraint:&lt;/p&gt;

&lt;p&gt;I wanted Go code to call CUDA without making every build depend on CUDA headers, a C compiler, or &lt;code&gt;cgo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That means loading the NVIDIA driver at runtime instead of linking against CUDA at build time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why avoid cgo?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cgo&lt;/code&gt; is the normal way to call C from Go, and often the right tool. But it also makes builds heavier.&lt;/p&gt;

&lt;p&gt;A package that uses &lt;code&gt;cgo&lt;/code&gt; needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a C compiler&lt;/li&gt;
&lt;li&gt;platform-specific toolchains&lt;/li&gt;
&lt;li&gt;cross-compilers for cross-platform builds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this project, that was exactly the setup I wanted to avoid.&lt;/p&gt;

&lt;p&gt;The goal was a normal Go build:&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="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 go build ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The binary still requires an NVIDIA driver on the machine where it runs.&lt;/p&gt;

&lt;p&gt;It just does not require the CUDA toolkit on the machine where it is built.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Driver API?
&lt;/h2&gt;

&lt;p&gt;CUDA exposes two major APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the higher-level Runtime API&lt;/li&gt;
&lt;li&gt;the lower-level Driver API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;gocudrv&lt;/code&gt; uses the Driver API because it is exposed directly by the NVIDIA driver itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;libcuda.so.1&lt;/code&gt; on Linux/WSL&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nvcuda.dll&lt;/code&gt; on Windows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That allows the program to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;load the driver dynamically at startup&lt;/li&gt;
&lt;li&gt;bind only the symbols it needs&lt;/li&gt;
&lt;li&gt;fail gracefully if the driver is missing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Driver API is also backward compatible, which makes it a better fit for a thin binding layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where purego fits
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gocudrv&lt;/code&gt; uses &lt;a href="https://github.com/ebitengine/purego?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;purego&lt;/a&gt; to open shared libraries and bind native functions without &lt;code&gt;cgo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At the top level, initialization looks pretty ordinary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"log"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/eitamring/gocudrv/cuda"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&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="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&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="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DriverVersion&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="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CUDA driver: %d.%d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;10&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;Underneath that small API, the package:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;locates the driver library&lt;/li&gt;
&lt;li&gt;binds functions like &lt;code&gt;cuInit&lt;/code&gt; and &lt;code&gt;cuDriverGetVersion&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;calls &lt;code&gt;cuInit(0)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;maps CUDA result codes into Go errors&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this does &lt;em&gt;not&lt;/em&gt; buy
&lt;/h2&gt;

&lt;p&gt;Skipping &lt;code&gt;cgo&lt;/code&gt; does not remove the C boundary.&lt;/p&gt;

&lt;p&gt;It just makes the boundary more manual.&lt;/p&gt;

&lt;p&gt;The library still has to define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;function signatures&lt;/li&gt;
&lt;li&gt;pointer types&lt;/li&gt;
&lt;li&gt;struct layouts&lt;/li&gt;
&lt;li&gt;alignment and padding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;exactly as the CUDA ABI expects them.&lt;/p&gt;

&lt;p&gt;If a native function expects a pointer to a struct, the Go side must pass memory with the exact same layout. The compiler will not rescue a bad binding.&lt;/p&gt;

&lt;p&gt;That tradeoff is worth it for this project, but it is still a tradeoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;Loading the driver is only the first step.&lt;/p&gt;

&lt;p&gt;A machine may have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;zero GPUs&lt;/li&gt;
&lt;li&gt;one GPU&lt;/li&gt;
&lt;li&gt;several GPUs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next step is handling devices, contexts, memory, and eventually streams and async execution cleanly from Go.&lt;/p&gt;

&lt;p&gt;The project is still very early, but the vector-add example already works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/eitamring/gocudrv?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;gocudrv on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>go</category>
      <category>cuda</category>
    </item>
    <item>
      <title>Three Rules for Designing a Go SDK Other People Will Actually Use</title>
      <dc:creator>Eitamos Ring</dc:creator>
      <pubDate>Thu, 07 May 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/eitamos_ring_0508146ca448/three-rules-for-designing-a-go-sdk-other-people-will-actually-use-53b4</link>
      <guid>https://dev.to/eitamos_ring_0508146ca448/three-rules-for-designing-a-go-sdk-other-people-will-actually-use-53b4</guid>
      <description>&lt;p&gt;I publish open-source Go libraries. &lt;br&gt;
Not many people use most of them, and I've spent a fair amount of time trying to figure out why. Some of it is distribution. Some of it is the unsexy truth that nobody needed the thing I built. But a real chunk of it — bigger than I want to admit — is that the API was designed for &lt;em&gt;me&lt;/em&gt;, the author, and not for the developer arriving cold from a Google search at 2am with a deadline.&lt;/p&gt;

&lt;p&gt;This post is three rules I now apply when designing a Go SDK. They come from publishing &lt;a href="https://github.com/ValkDB/postgresparser" rel="noopener noreferrer"&gt;postgresparser&lt;/a&gt; — a pure-Go PostgreSQL parser — and watching where new users got stuck. The examples are from that library, but the rules aren't about parsers. They're about what the surface of a Go package should look like if you want strangers to use it.&lt;/p&gt;

&lt;p&gt;I'll also flag one place I broke my own rule, because the post would be dishonest without it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rule 1: Expose answers, not nodes
&lt;/h2&gt;

&lt;p&gt;The single biggest mistake I see in Go SDKs (and that I've made myself) is shipping the &lt;em&gt;internal data model&lt;/em&gt; as the public API. The author has built an AST, or a state machine, or a config tree, and they think: "great, I'll let the caller walk it." The caller does not want to walk it. The caller wants an answer to a specific question.&lt;/p&gt;

&lt;p&gt;Here's what "expose the nodes" looks like in a SQL parsing context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// What other Go SQL parsers tend to give you&lt;/span&gt;
&lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stmt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&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;Statements&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;sel&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;stmt&lt;/span&gt;&lt;span class="o"&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;ast&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectStmt&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;sel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&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;rv&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;from&lt;/span&gt;&lt;span class="o"&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;ast&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RangeVar&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="n"&gt;tables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&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;rv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Relname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="c"&gt;// ...also handle JoinExpr, Subquery, RangeFunction,&lt;/span&gt;
            &lt;span class="c"&gt;// RangeTableSample, RangeTableFunc, CTERef...&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user came to your library to find out which tables a query touches. You handed them a tree-walking exercise and a list of node types they have to learn. Every caller of your library now has to write — and maintain — the same boilerplate, with the same bugs, in slightly different ways.&lt;/p&gt;

&lt;p&gt;Compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// What postgresparser gives you&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;postgresparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseSQL&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tables&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Two lines. CTEs, subqueries, set operations, joins — all flattened into the same field, with aliases preserved. The IR (the actual AST-equivalent) still exists internally, but it's not what the caller binds to.&lt;/p&gt;

&lt;p&gt;The principle: &lt;strong&gt;for every question your SDK answers, there should be a single field or function whose name &lt;em&gt;is&lt;/em&gt; the question.&lt;/strong&gt; "Which tables?" → &lt;code&gt;Tables&lt;/code&gt;. "Which columns are filtered?" → &lt;code&gt;ExtractWhereConditions&lt;/code&gt;. "How is each column used?" → &lt;code&gt;ColumnUsage&lt;/code&gt;. If a user has to traverse three levels of struct to get an answer, the answer wasn't really exposed.&lt;/p&gt;

&lt;p&gt;The objection I hear: &lt;em&gt;but what if the caller wants something custom that we didn't anticipate?&lt;/em&gt; Fine — keep the IR public for the 5% case. But default to answering the 95% case in one line, and only fall back to the IR when the typed accessor doesn't cover the question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 2: Name the common case after the common case, and mark the variants
&lt;/h2&gt;

&lt;p&gt;Most Go SDKs I see treat all of their entry points as peers. &lt;code&gt;Parse&lt;/code&gt;, &lt;code&gt;ParseStrict&lt;/code&gt;, &lt;code&gt;ParseAll&lt;/code&gt;, &lt;code&gt;ParseWithOptions&lt;/code&gt;, &lt;code&gt;ParseFromReader&lt;/code&gt; — all listed in pkg.go.dev with the same visual weight, and the user has to read every one to figure out which they want.&lt;/p&gt;

&lt;p&gt;This is the "tyranny of options" failure. The author thought of every variant; the user has to think about it too.&lt;/p&gt;

&lt;p&gt;The fix is sequencing. Pick the version 80% of users want. Give &lt;em&gt;that&lt;/em&gt; the short name. Make the other variants explicitly named after the thing that makes them different.&lt;/p&gt;

&lt;p&gt;postgresparser's parsing entry points:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// 80% case — parses one statement, gives you a result.&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;postgresparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseSQL&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="c"&gt;// "I might pass multiple statements and want all of them."&lt;/span&gt;
&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;postgresparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseSQLAll&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="c"&gt;// "I want an error if more than one statement was passed."&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;postgresparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseSQLStrict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ParseSQL&lt;/code&gt; is the default. &lt;code&gt;ParseSQLAll&lt;/code&gt; and &lt;code&gt;ParseSQLStrict&lt;/code&gt; are explicitly named after the property that makes them different (handling all statements, strict-on-multi). A user reading the package docs sees &lt;code&gt;ParseSQL&lt;/code&gt; first, tries it, and only goes looking for the variants if they hit a case it doesn't cover.&lt;/p&gt;

&lt;p&gt;The wrong version of the same API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Don't do this&lt;/span&gt;
&lt;span class="n"&gt;postgresparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseSQL&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;ParseOptions&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Strict&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AllStatements&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;postgresparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseSQL&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;ParseOptions&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Strict&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AllStatements&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You've moved the decision from the function name (where it's documented and grep-able) to a config struct (where it's not). New users have to read the options struct just to call the function. Existing code has to be re-read every time someone wants to know what mode it's in.&lt;/p&gt;

&lt;p&gt;The principle: &lt;strong&gt;the most common call should be the shortest call. Variants get names that describe how they differ. Config structs are for things that don't fit in a name, not for things that do.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 3: Return structured data, not strings the caller has to re-parse
&lt;/h2&gt;

&lt;p&gt;This one I see less often in writing about SDK design, but it's the one that bites users hardest in practice.&lt;/p&gt;

&lt;p&gt;If your SDK has done work to extract structured information from unstructured input, &lt;em&gt;don't throw the structure away on the way out&lt;/em&gt;. Returning &lt;code&gt;[]string&lt;/code&gt; when you could have returned &lt;code&gt;[]struct{...}&lt;/code&gt; is a tax you charge every caller forever.&lt;/p&gt;

&lt;p&gt;postgresparser extracts WHERE conditions. The naive return type would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Bad: caller has to re-parse what you already parsed&lt;/span&gt;
&lt;span class="n"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExtractWhereConditions&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="c"&gt;// returns: ["status = 'active'", "total &amp;gt; 100"]&lt;/span&gt;

&lt;span class="c"&gt;// Now every caller writes a regex. They get it wrong.&lt;/span&gt;
&lt;span class="c"&gt;// They handle = and != but forget IS NULL. They miss BETWEEN.&lt;/span&gt;
&lt;span class="c"&gt;// They re-introduce the bug your library was built to solve.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it actually returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Condition&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Column&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Operator&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Value&lt;/span&gt;    &lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExtractWhereConditions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"SELECT * FROM orders WHERE status = 'active' AND total &amp;gt; 100"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s %s %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Operator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// status = active&lt;/span&gt;
&lt;span class="c"&gt;// total &amp;gt; 100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the caller can ask &lt;code&gt;c.Column == "tenant_id"&lt;/code&gt; directly. They can switch on &lt;code&gt;c.Operator&lt;/code&gt;. They can type-assert &lt;code&gt;c.Value&lt;/code&gt;. None of them have to write a regex, and none of them re-introduce parsing bugs at the boundary of your library.&lt;/p&gt;

&lt;p&gt;The principle: &lt;strong&gt;if the structure exists internally, expose the structure. Strings are for things that have no structure, or for things the user is going to print.&lt;/strong&gt; Stringly-typed return values are how libraries become impossible to use correctly at scale.&lt;/p&gt;

&lt;p&gt;The reverse also holds: if you find yourself writing a long regex inside a library you depend on, that library failed Rule 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I broke my own rule
&lt;/h2&gt;

&lt;p&gt;In the spirit of not pretending I have all this figured out: postgresparser violates Rule 2 with &lt;code&gt;ParseSQLWithOptions(sql, opts)&lt;/code&gt;. It exists alongside &lt;code&gt;ParseSQL(sql)&lt;/code&gt;, takes a config struct with extraction flags like &lt;code&gt;IncludeCreateTableFieldComments&lt;/code&gt;, and is exactly the "tyranny of options" pattern I just told you to avoid.&lt;/p&gt;

&lt;p&gt;The honest reason it exists: comment extraction is expensive and most callers don't need it, but I didn't want to design a separate &lt;code&gt;ParseSQLWithComments&lt;/code&gt; function because the option might evolve. So I shipped a &lt;code&gt;WithOptions&lt;/code&gt; escape hatch and told myself it was fine. It's not fine — it's a slow leak that will get bigger as more options accrete. The right move would have been a separate named function for the one option that exists today, and a real opt-in API design when the second option arrives. &lt;/p&gt;

&lt;p&gt;I'm flagging it so you can see what the wrong choice looks like even when the author knew the rule.&lt;/p&gt;

&lt;p&gt;The point of including this isn't self-deprecation. It's that &lt;strong&gt;you will violate your own rules&lt;/strong&gt;. The goal isn't a perfect API on day one; it's noticing the violation, naming it, and fixing it before the wrong shape hardens into a public contract you can't change.&lt;/p&gt;

&lt;h2&gt;
  
  
  TLDR;
&lt;/h2&gt;

&lt;p&gt;If you can't remember three rules, remember the question they all answer: &lt;strong&gt;what does the user have to learn before they can use this library?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rule 1 says: don't make them learn your AST.&lt;/li&gt;
&lt;li&gt;Rule 2 says: don't make them learn your option matrix.&lt;/li&gt;
&lt;li&gt;Rule 3 says: don't make them re-parse what you already parsed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every line of documentation a user has to read before their first successful call is friction. Some of it is unavoidable. A lot of it isn't, and that's where the design work is.&lt;/p&gt;




&lt;p&gt;postgresparser is on GitHub at &lt;a href="https://github.com/ValkDB/postgresparser" rel="noopener noreferrer"&gt;github.com/ValkDB/postgresparser&lt;/a&gt; if you want to see what these rules look like applied (and, per the section above, where they aren't yet). Issues and PRs welcome — particularly the kind that point out a rule I missed.&lt;/p&gt;

</description>
      <category>api</category>
      <category>design</category>
      <category>go</category>
      <category>opensource</category>
    </item>
    <item>
      <title>What I Learned Building a Pure Go PostgreSQL Parser</title>
      <dc:creator>Eitamos Ring</dc:creator>
      <pubDate>Tue, 05 May 2026 06:19:05 +0000</pubDate>
      <link>https://dev.to/eitamos_ring_0508146ca448/a-pure-go-postgresql-parser-passed-200-stars-heres-what-i-learned-5eg6</link>
      <guid>https://dev.to/eitamos_ring_0508146ca448/a-pure-go-postgresql-parser-passed-200-stars-heres-what-i-learned-5eg6</guid>
      <description>&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I needed a PostgreSQL parser that could run inside Go tooling without CGO, external binaries, or runtime dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What made PostgreSQL parsing harder than expected
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SQL is not one grammar&lt;/li&gt;
&lt;li&gt;PostgreSQL has a lot of dialect-specific edge cases&lt;/li&gt;
&lt;li&gt;AST shape matters more than “can it parse”&lt;/li&gt;
&lt;li&gt;Error handling becomes a product feature&lt;/li&gt;
&lt;li&gt;Real-world SQL is uglier than examples&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why pure Go mattered
&lt;/h2&gt;

&lt;p&gt;No CGO, easy installation, works in CI, easy to embed in linters and developer tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 200+ GitHub stars taught me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Developers care about boring installation&lt;/li&gt;
&lt;li&gt;Parser APIs need to be simple&lt;/li&gt;
&lt;li&gt;Good examples matter more than perfect docs&lt;/li&gt;
&lt;li&gt;People want tooling, not academic grammar dumps&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it’s going
&lt;/h2&gt;

&lt;p&gt;This parser is becoming the foundation for Valk Guard, a local-first static analyzer for SQL and ORM usage. No LLM required. It works from ASTs and deterministic rules.&lt;/p&gt;

&lt;p&gt;GitHub repo: &lt;a href="https://github.com/ValkDB/postgresparser" rel="noopener noreferrer"&gt;GitHub repo: https://github.com/ValkDB/postgresparser&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>postgres</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>What every `?` in your SQL is hiding</title>
      <dc:creator>Eitamos Ring</dc:creator>
      <pubDate>Mon, 04 May 2026 18:48:09 +0000</pubDate>
      <link>https://dev.to/eitamos_ring_0508146ca448/what-every-in-your-sql-is-hiding-o4o</link>
      <guid>https://dev.to/eitamos_ring_0508146ca448/what-every-in-your-sql-is-hiding-o4o</guid>
      <description>&lt;p&gt;Take a query that comes out of &lt;code&gt;pg_stat_statements&lt;/code&gt;:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&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_at&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;week&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;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;?&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;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six question marks. Each one means something completely different.&lt;/p&gt;

&lt;p&gt;The first, inside &lt;code&gt;date_trunc&lt;/code&gt;, expects a string like &lt;code&gt;'week'&lt;/code&gt; — it's telling the function which time bucket to use. The second is a timestamp comparing against &lt;code&gt;created_at&lt;/code&gt;. The third is a number comparing against &lt;code&gt;amount&lt;/code&gt;. The fourth is a string joined through to the customers table — it has to match a &lt;code&gt;plan&lt;/code&gt; value over there. The fifth, sitting bare inside &lt;code&gt;GROUP BY&lt;/code&gt;, is a positional integer like &lt;code&gt;1&lt;/code&gt;, pointing back at the first column in the SELECT list. It's not a value, it's an &lt;em&gt;index&lt;/em&gt;. The sixth, after &lt;code&gt;LIMIT&lt;/code&gt;, is a page-size integer.&lt;/p&gt;

&lt;p&gt;Six placeholders, four different value types, two completely different &lt;em&gt;kinds&lt;/em&gt; of integer. There isn't a regex that gets all six right — not without re-implementing a SQL parser inside it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;postgresparser&lt;/code&gt; is the open-source Go/ANTLR PostgreSQL parser we maintain at ValkDB. Until this release, when you got back an AST, every &lt;code&gt;?&lt;/code&gt; was just a leaf node with positional information and nothing else. The parser knew exactly what each &lt;code&gt;?&lt;/code&gt; meant — it had to, in order to parse — but it never told you. So everyone downstream fell back to regular expressions, string scanning, and increasingly elaborate guesswork.&lt;/p&gt;

&lt;p&gt;This week's release tells you what every &lt;code&gt;?&lt;/code&gt; actually is.&lt;/p&gt;




&lt;h2&gt;
  
  
  The new API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AnalyzeSQL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;querySQL&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Placeholders&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"placeholder %d: role=%s column=%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&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;Index&lt;/span&gt;&lt;span class="p"&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;Role&lt;/span&gt;&lt;span class="p"&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;ColumnRef&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;placeholder 1: role=function_arg     column=         (date_trunc, arg 0)
placeholder 2: role=where_value      column=created_at
placeholder 3: role=where_value      column=amount
placeholder 4: role=where_value      column=plan
placeholder 5: role=group_by_ordinal column=
placeholder 6: role=limit            column=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six placeholders, six correct classifications, no string scanning. Switch on the role, fill in the right value.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the old way failed
&lt;/h2&gt;

&lt;p&gt;Without role information, this is the pipeline most tools end up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────┐
│  Normalized SQL     │
│  with ? placeholders│
└──────────┬──────────┘
           │
           ▼
┌─────────────────────────────────┐
│   Regex sweep for "?"           │
│                                 │
│   finds ? in string literals    │
│   finds ? in comments           │
│   can't see GROUP BY context    │
│   mis-IDs JSONB ? operator      │
│   picks same value twice for    │
│   same column on &amp;gt;= and &amp;lt;       │
└──────────┬──────────────────────┘
           │
           ▼
┌─────────────────────┐
│  Hand-written       │
│  per-position guess │
│  (fragile)          │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  Substituted SQL    │
│  often broken       │
└─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The role-aware version skips all of that by walking the parse tree the parser already built. String literals are leaves of their own kind, so question marks inside them are never seen as placeholders. Comments are stripped before tree construction. The JSONB operator is parsed as an operator node, not a placeholder leaf, so it never enters the placeholder list. &lt;code&gt;GROUP BY&lt;/code&gt; and &lt;code&gt;ORDER BY&lt;/code&gt; ordinals carry their own dedicated role. And every placeholder's syntactic role — its actual position in the grammar — comes back attached.&lt;/p&gt;




&lt;h2&gt;
  
  
  The five footguns this release closes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The JSONB &lt;code&gt;?&lt;/code&gt; operator is not a placeholder
&lt;/h3&gt;

&lt;p&gt;PostgreSQL has three jsonb operators that look like placeholder tokens:&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;WHERE&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'key'&lt;/span&gt;                  &lt;span class="c1"&gt;-- "does jsonb contain top-level key?"&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="o"&gt;?|&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'b'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="c1"&gt;-- "any of these keys?"&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="o"&gt;?&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'b'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="c1"&gt;-- "all of these keys?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A regex sweep can't tell these apart from real placeholders. The new placeholder list excludes JSONB operator tokens by construction.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;INTERVAL ?&lt;/code&gt; actually parses
&lt;/h3&gt;

&lt;p&gt;Before this release, &lt;code&gt;INTERVAL ?&lt;/code&gt; was rejected with a syntax error — a real problem if you consume &lt;code&gt;pg_stat_statements&lt;/code&gt;, because every query that uses an interval literal gets normalized to that form. The grammar now accepts a parameter token in interval-operand position.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;?&lt;/code&gt; inside string literals stays inside string literals
&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;WHERE&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'has a ?'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'don&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;t mark me ?'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The collector walks the parse tree, never the raw SQL — so string-literal &lt;code&gt;?&lt;/code&gt; and comment &lt;code&gt;?&lt;/code&gt; simply don't appear in the placeholder list.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;GROUP BY ?&lt;/code&gt; is an ordinal, not a value
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pg_stat_statements&lt;/code&gt; rewrites &lt;code&gt;GROUP BY 1, 2&lt;/code&gt; to &lt;code&gt;GROUP BY ?, ?&lt;/code&gt;. These placeholders need to be substituted with positional integers referring to SELECT-list slots — not with arbitrary values. A dedicated role makes this explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Function-argument placeholders need to know their function
&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="n"&gt;date_trunc&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;created_at&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;created_at&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;t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first &lt;code&gt;?&lt;/code&gt; must be a string like &lt;code&gt;'week'&lt;/code&gt;. The second must be a string like &lt;code&gt;'year'&lt;/code&gt;. Both are function-args, but the function differs — so the right substitution differs. Each placeholder of this kind now carries its parent function name and argument index.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;If you build an ORM or query builder and you've ever wanted to type-check a placeholder before binding to it, this is for you. If you build a SQL linter, a migration tool that rewrites queries between dialects, a monitoring agent that ingests &lt;code&gt;pg_stat_statements&lt;/code&gt;, an AI-assisted SQL generator that emits parameterized queries — same. The common thread is that you have a normalized SQL string with &lt;code&gt;?&lt;/code&gt; placeholders in it, and you need to know what each one means before you can do anything useful.&lt;/p&gt;

&lt;p&gt;If that sounds like work you've done, you've probably written a private placeholder classifier already. With this release, you don't have to.&lt;/p&gt;




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

&lt;p&gt;The parser tells you what the SQL says; type inference belongs a layer up. The API stays narrow on purpose — roles, positions, and the structural context needed to make sense of them. Function-wrapper exposure on column usage is next on the roadmap; lateral-join and recursive-CTE refinements after that.&lt;/p&gt;

&lt;p&gt;The parser knew. Now it tells you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/ValkDB/postgresparser" rel="noopener noreferrer"&gt;postgresparser&lt;/a&gt; — open-source PostgreSQL parser. Go, ANTLR-based. Contributions welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>postgres</category>
      <category>opensource</category>
      <category>development</category>
    </item>
    <item>
      <title>A Protobuf for Database Schemas</title>
      <dc:creator>Eitamos Ring</dc:creator>
      <pubDate>Wed, 18 Mar 2026 07:52:54 +0000</pubDate>
      <link>https://dev.to/eitamos_ring_0508146ca448/a-protobuf-for-database-schemas-pc8</link>
      <guid>https://dev.to/eitamos_ring_0508146ca448/a-protobuf-for-database-schemas-pc8</guid>
      <description>&lt;p&gt;Every serious system has an interface definition for its wire format. gRPC has protobuf. REST has OpenAPI. GraphQL has its SDL. But databases -- the thing everything else is built on top of -- have nothing.&lt;/p&gt;

&lt;p&gt;Your database schema is one of the most important artifacts in your system. It defines every table, column, type, constraint, relationship, and index. It encodes years of domain decisions. And yet there is no standard, portable, machine-readable format for it.&lt;/p&gt;

&lt;p&gt;We built one. We call it &lt;code&gt;ctxexport.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem is older than LLMs
&lt;/h2&gt;

&lt;p&gt;Before you assume this is an AI-context story, consider how many times you have needed your schema outside the database itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Onboarding a new engineer who needs to understand the data model.&lt;/li&gt;
&lt;li&gt;Diffing staging against production to catch drift before a deploy.&lt;/li&gt;
&lt;li&gt;Running a linter in CI to enforce naming conventions or catch missing indexes.&lt;/li&gt;
&lt;li&gt;Generating documentation that is not immediately stale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every time, you end up writing a bespoke script that queries &lt;code&gt;information_schema&lt;/code&gt; or &lt;code&gt;pg_catalog&lt;/code&gt;, parses the output, and feeds it into whatever tool you need. The script is Postgres-specific. It breaks when you add a second schema. Nobody maintains it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pg_dump --schema-only&lt;/code&gt; exists, but it is a restore format, not a consumption format. It is Postgres-specific SQL with &lt;code&gt;SET&lt;/code&gt; statements, ownership clauses, and an ordering designed for replay, not reading. Try parsing it reliably. Try feeding it to a linter. Try diffing two of them without drowning in noise.&lt;/p&gt;

&lt;p&gt;MongoDB is worse. There is no &lt;code&gt;mongodump --schema-only&lt;/code&gt;. Your schema lives in the shape of whatever documents happen to exist. Good luck extracting that into something a tool can reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extract once, use many ways
&lt;/h2&gt;

&lt;p&gt;The core insight behind &lt;code&gt;ctxexport.json&lt;/code&gt; is the same one behind protobuf: &lt;strong&gt;separate the definition from the consumption&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A protobuf &lt;code&gt;.proto&lt;/code&gt; file is written once and compiled to Go structs, Python classes, TypeScript types, gRPC stubs, or REST gateways. The definition is the single source of truth. The consumers are many and varied.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ctxexport.json&lt;/code&gt; works the same way. You extract your schema once -- from Postgres, MongoDB, or whatever backend -- and produce a single canonical JSON file. That file contains entities (tables, views, collections), fields (columns with types, nullability, defaults), edges (foreign keys and inferred references), and access paths (indexes). Everything a tool needs to understand your data model, nothing it does not.&lt;/p&gt;

&lt;p&gt;From that single artifact, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compile to a lighthouse map&lt;/strong&gt; -- a compact table-and-relationship summary that fits in an LLM prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compile to full SQL DDL&lt;/strong&gt; -- standard &lt;code&gt;CREATE TABLE&lt;/code&gt; statements for any subset of tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serve over MCP&lt;/strong&gt; -- give an AI agent schema awareness without database credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff across environments&lt;/strong&gt; -- compare staging and production schemas as structured data, not text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lint offline&lt;/strong&gt; -- check naming conventions, missing indexes, or orphaned foreign keys in CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate in CI&lt;/strong&gt; -- catch schema regressions before they reach production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit to git&lt;/strong&gt; -- your schema becomes a versioned artifact with a real history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these consumers need to know whether the source was Postgres or MongoDB. None of them need a live database connection. The extraction happened once, upstream, and everything downstream reads the same contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sidecar pattern
&lt;/h2&gt;

&lt;p&gt;Databases have never been good at carrying human knowledge alongside the schema. Your &lt;code&gt;users.deleted_at&lt;/code&gt; column is a soft-delete flag, but the database only knows it is a &lt;code&gt;timestamp with time zone&lt;/code&gt;. Your &lt;code&gt;orders.payload&lt;/code&gt; column is JSONB with a specific structure, but the database sees an opaque blob.&lt;/p&gt;

&lt;p&gt;A sidecar file (&lt;code&gt;dbdense.yaml&lt;/code&gt;) layers descriptions and value annotations onto the extracted schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;entities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;payments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authorized"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paid"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refunded"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;deleted_at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Soft&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;delete&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;timestamp.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;NULL&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;active."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This merges at export time. The compiled DDL gets inline comments like &lt;code&gt;-- Values: pending, authorized, paid, failed, refunded&lt;/code&gt;. Every downstream consumer -- linter, LLM, documentation generator -- picks it up automatically. Write it once in a YAML file committed to the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why JSON, not SQL
&lt;/h2&gt;

&lt;p&gt;SQL DDL is human-readable but machine-hostile. Parsing &lt;code&gt;CREATE TABLE&lt;/code&gt; statements reliably across dialects is a nightmare. Defaults are quoted differently. Constraints can be inline or out-of-band. Comments use different syntax. There is no standard way to represent a foreign key relationship as structured data.&lt;/p&gt;

&lt;p&gt;JSON is boring and that is the point. It is a declarative state representation -- you look up a table by name, not by parsing DDL statement order. Every language has a JSON parser. The schema is simple: a version string, an array of entities, and an array of edges. You can validate it with a JSON Schema. You can diff it with &lt;code&gt;jq&lt;/code&gt;. You can read it in any language without a SQL parser.&lt;/p&gt;

&lt;p&gt;A minimal entity looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"payments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="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;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"is_pk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="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;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"not_null"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"pending"&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"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flat, predictable, zero ambiguity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop treating your schema like a black box
&lt;/h2&gt;

&lt;p&gt;The immediate use case is LLM context -- giving AI agents schema awareness without live database access. But the format is deliberately general. If your tool can read JSON, it can read a database schema. That was not true before.&lt;/p&gt;

&lt;p&gt;The project is at &lt;a href="https://github.com/valkdb/dbdense" rel="noopener noreferrer"&gt;github.com/valkdb/dbdense&lt;/a&gt;. The contract is documented in &lt;code&gt;docs/ctxexport-contract.md&lt;/code&gt;. It supports Postgres and MongoDB today. The extractor interface is small enough that adding a new backend is a single file.&lt;/p&gt;

&lt;p&gt;Your database schema is too important to be locked inside the database. Export it. Version it. Build on it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Sending 93K Tokens of Schema to Your LLM Agent!</title>
      <dc:creator>Eitamos Ring</dc:creator>
      <pubDate>Wed, 18 Mar 2026 07:52:06 +0000</pubDate>
      <link>https://dev.to/eitamos_ring_0508146ca448/stop-sending-93k-tokens-of-schema-to-your-llm-agent-5c67</link>
      <guid>https://dev.to/eitamos_ring_0508146ca448/stop-sending-93k-tokens-of-schema-to-your-llm-agent-5c67</guid>
      <description>&lt;p&gt;I've watched agents query &lt;code&gt;information_schema&lt;/code&gt; over and over, spending 4-6 turns just to figure out which tables exist, what columns they have, and how they join. On a 500-table database, the full DDL is around 93,000 tokens. Most questions touch 3-5 tables. On a complex multi-table join, I measured a 64% token reduction by just giving the agent the schema upfront.&lt;/p&gt;

&lt;p&gt;That's what &lt;a href="https://github.com/valkdb/dbdense" rel="noopener noreferrer"&gt;dbdense&lt;/a&gt; does.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/valkdb/dbdense" rel="noopener noreferrer"&gt;dbdense&lt;/a&gt; to fix this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;dbdense is a three-step offline pipeline: &lt;strong&gt;extract&lt;/strong&gt;, &lt;strong&gt;compile&lt;/strong&gt;, &lt;strong&gt;serve&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Extract&lt;/strong&gt; connects to your database once and snapshots the schema into a portable JSON file (&lt;code&gt;ctxexport.json&lt;/code&gt;). Tables, columns, types, primary keys, foreign keys, indexes -- everything an LLM needs to write correct queries.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Compile&lt;/strong&gt; turns that snapshot into two artifacts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;lighthouse&lt;/strong&gt; -- a compact table map (~4K tokens for 500 tables). It looks like this:
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; T:users|J:orders,sessions
 T:orders|E:payload,shipping|J:payments,shipments,users
 T:payments|J:orders
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Every table, its FK neighbors, and embedded docs. 23x smaller than full DDL. This stays in the agent's context so it always knows what's available.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full DDL&lt;/strong&gt; -- standard &lt;code&gt;CREATE TABLE&lt;/code&gt; statements with constraints, rendered on demand only for the specific tables the agent asks about.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serve&lt;/strong&gt; (optional) exposes the lighthouse as an MCP resource and the DDL via an MCP &lt;code&gt;slice&lt;/code&gt; tool. The agent reads the map, picks the tables it needs, and gets back just those definitions.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After the extract, everything runs locally. The compiled artifacts are plain text you can commit to your repo. No database connection needed at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  No credentials in the agent runtime
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;export&lt;/code&gt; step is the only step that touches the database. After that, &lt;code&gt;compile&lt;/code&gt; and &lt;code&gt;serve&lt;/code&gt; work from the local snapshot. Your production database credentials never need to be in the agent's environment. The tool works offline and air-gapped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;I ran an agentic benchmark: n=3, same 5 questions, same seeded Postgres database (20K+ rows, 8 tables), same model (Claude Sonnet 4). One arm had only a Postgres MCP tool. The other had the same tool plus dbdense schema context injected into the prompt.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Without schema context&lt;/th&gt;
&lt;th&gt;With dbdense&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Correct answers&lt;/td&gt;
&lt;td&gt;13/15&lt;/td&gt;
&lt;td&gt;13/15&lt;/td&gt;
&lt;td&gt;equal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg turns&lt;/td&gt;
&lt;td&gt;4.1&lt;/td&gt;
&lt;td&gt;2.2&lt;/td&gt;
&lt;td&gt;-46%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens per run&lt;/td&gt;
&lt;td&gt;285,922&lt;/td&gt;
&lt;td&gt;187,603&lt;/td&gt;
&lt;td&gt;-34%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same accuracy. 34% fewer tokens. 46% fewer turns.&lt;/p&gt;

&lt;p&gt;The savings scale with query complexity. On simple single-table filters, both arms performed about the same. On a complex multi-table join, the baseline agent spent 6+ turns querying &lt;code&gt;information_schema&lt;/code&gt; to discover the schema. dbdense answered in 2 turns, using 64% fewer tokens for that query.&lt;/p&gt;

&lt;p&gt;The two wrong answers (both on the same question, in both arms) returned identical incorrect results, pointing to question ambiguity rather than a schema context issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sidecar enrichment
&lt;/h2&gt;

&lt;p&gt;Databases lie by omission. A column named &lt;code&gt;status&lt;/code&gt; with type &lt;code&gt;text&lt;/code&gt; tells the LLM nothing about what values are valid. The agent either guesses or wastes a &lt;code&gt;SELECT DISTINCT&lt;/code&gt; turn to find out.&lt;/p&gt;

&lt;p&gt;dbdense supports a &lt;code&gt;dbdense.yaml&lt;/code&gt; sidecar file where you annotate columns with descriptions and enum values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;entities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;payments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authorized"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paid"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refunded"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;orders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Order&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lifecycle&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;status."&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confirmed"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shipped"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delivered"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancelled"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These annotations merge into the compiled DDL as inline SQL comments. The LLM sees &lt;code&gt;-- Values: pending, authorized, paid, failed, refunded&lt;/code&gt; right next to the column definition. No extra queries needed.&lt;/p&gt;

&lt;p&gt;This also works for documenting JSONB structures, MongoDB embedded documents, or anything else the raw schema doesn't capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;The snapshot is static. If your schema changes, re-run &lt;code&gt;export&lt;/code&gt;. This is intentional -- schemas are stable; questions change.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;slice&lt;/code&gt; tool still depends on the LLM picking the right tables from the lighthouse. dbdense reduces the context problem; it doesn't solve table selection for the model.&lt;/p&gt;

&lt;p&gt;It's not a &lt;code&gt;pg_dump --schema-only&lt;/code&gt; replacement. The renderer covers columns, PKs, FKs, NOT NULL, defaults, unique constraints, and indexes, but skips triggers, RLS policies, and custom types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/valkdb/dbdense/cmd/dbdense@latest
dbdense &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--driver&lt;/span&gt; postgres &lt;span class="nt"&gt;--db&lt;/span&gt; &lt;span class="s2"&gt;"postgres://user:pass@localhost:5432/mydb"&lt;/span&gt; &lt;span class="nt"&gt;--schemas&lt;/span&gt; public
dbdense compile &lt;span class="nt"&gt;--mode&lt;/span&gt; lighthouse &lt;span class="nt"&gt;--in&lt;/span&gt; ctxexport.json &lt;span class="nt"&gt;--out&lt;/span&gt; lighthouse.txt
dbdense compile &lt;span class="nt"&gt;--in&lt;/span&gt; ctxexport.json &lt;span class="nt"&gt;--out&lt;/span&gt; schema.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now have two files: a lighthouse map and full DDL. Point your agent at them. If you use Claude Code, &lt;code&gt;dbdense init-claude&lt;/code&gt; writes the MCP config for you.&lt;/p&gt;

&lt;p&gt;The project is open source at &lt;a href="https://github.com/valkdb/dbdense" rel="noopener noreferrer"&gt;github.com/valkdb/dbdense&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How does a linter know your column doesn't exist</title>
      <dc:creator>Eitamos Ring</dc:creator>
      <pubDate>Mon, 09 Mar 2026 08:40:04 +0000</pubDate>
      <link>https://dev.to/eitamos_ring_0508146ca448/how-does-a-linter-know-your-column-doesnt-exist-7ff</link>
      <guid>https://dev.to/eitamos_ring_0508146ca448/how-does-a-linter-know-your-column-doesnt-exist-7ff</guid>
      <description>&lt;p&gt;You write a query that SELECTs ghost_status from the orders table. Your code compiles. Your tests pass. But ghost_status was never created in any migration. In production, that query crashes.&lt;br&gt;
Valk Guard catches this at PR time - with no database connection.&lt;br&gt;
This post walks through exactly how. Not hand-waving. The actual code path, from source file to finding.&lt;br&gt;
The setup&lt;br&gt;
Here's a Go file using Goqu to build a query:&lt;br&gt;
func ListBrokenUserOrderStatus(ctx context.Context) error {&lt;br&gt;
    _, _, err := goqu.From("users").&lt;br&gt;
        LeftJoin(&lt;br&gt;
            goqu.T("orders"),&lt;br&gt;
            goqu.On(goqu.I("orders.user_id").Eq(goqu.I("users.id"))),&lt;br&gt;
        ).&lt;br&gt;
        Select("users.id", "users.email", "orders.ghost_status").&lt;br&gt;
        Where(goqu.I("orders.missing_flag").Eq("pending")).&lt;br&gt;
        ToSQL()&lt;br&gt;
    return err&lt;br&gt;
}&lt;br&gt;
And here's the migration that created the orders table:&lt;br&gt;
CREATE TABLE orders (&lt;br&gt;
    id         SERIAL PRIMARY KEY,&lt;br&gt;
    user_id    INTEGER NOT NULL REFERENCES users(id),&lt;br&gt;
    total      NUMERIC(10,2) NOT NULL,&lt;br&gt;
    status     TEXT NOT NULL DEFAULT 'pending',&lt;br&gt;
    created_at TIMESTAMP DEFAULT now()&lt;br&gt;
);&lt;br&gt;
Notice: the query references orders.ghost_status. The migration never created that column. There is no ghost_status. Valk Guard reports:&lt;br&gt;
VG105: projection column "ghost_status" not found in table "orders" schema; check SELECT list and schema/model mappings&lt;br&gt;
How does it know?&lt;br&gt;
Let's walk through each phase.&lt;br&gt;
Phase 1: Query extraction&lt;br&gt;
The Goqu scanner doesn't look for SQL strings. It walks the Go AST looking for method chains rooted in goqu.From().&lt;br&gt;
When it finds one, it flattens the chain into a list of method calls: From("users") → LeftJoin(...) → Select(...) → Where(...). Each method gets parsed: From gives the base table, LeftJoin gives the join target, Select gives the projection columns, Where gives the predicates.&lt;br&gt;
From these parts, the scanner synthesizes a SQL statement:&lt;br&gt;
SELECT users.id, users.email, orders.ghost_status&lt;br&gt;
FROM users LEFT JOIN orders ON orders.user_id = users.id&lt;br&gt;
WHERE orders.missing_flag = 'pending'&lt;br&gt;
This SQL never existed in your source code. Valk Guard constructed it from the AST of your Go code. That's the key difference from regex-based tools - regex can't walk a method chain and reconstruct what the query builder will produce.&lt;br&gt;
Phase 2: Schema&amp;nbsp;snapshot&lt;br&gt;
Separately, Valk Guard finds all&amp;nbsp;.sql files under your migration paths. Each file gets parsed through postgresparser, and every DDL statement gets applied to a Snapshot - an in-memory representation of your schema's current state.&lt;br&gt;
The snapshot builder processes DDL actions in order:&lt;br&gt;
CREATE TABLE orders (id, user_id, total, status, created_at) → registers the table with five columns&lt;br&gt;
ALTER TABLE orders ADD COLUMN shipped_at TIMESTAMP → adds a sixth column&lt;br&gt;
ALTER TABLE orders DROP COLUMN shipped_at → removes it&lt;/p&gt;

&lt;p&gt;The end result is a map of table names to column definitions. For orders, that's: id, user_id, total, status, created_at. Five columns. No ghost_status.&lt;br&gt;
This is the same principle as running all your migrations on an empty database - except it happens in memory, with no database, in microseconds.&lt;br&gt;
Phase 3: Rule evaluation&lt;br&gt;
Now VG105 runs. It takes the synthesized SQL (already parsed into a structured IR by postgresparser) and the schema snapshot, and does a straightforward lookup:&lt;br&gt;
For each column in the SELECT list with usage type "projection", resolve which table it belongs to (using the alias or the single-table shortcut)&lt;br&gt;
Look up that table in the snapshot&lt;br&gt;
Check if the column exists in the table's column map&lt;br&gt;
If not → finding&lt;/p&gt;

&lt;p&gt;For ghost_status, the column usage says it belongs to orders (from the orders.ghost_status qualifier). The snapshot has an orders table. But orders.ghost_status is not in the column map. Finding.&lt;br&gt;
The same logic powers VG106 (unknown filter column - catches WHERE orders.missing_flag = 'pending' from the same query) and VG107 (unknown table reference).&lt;br&gt;
It also works with ORM&amp;nbsp;models&lt;br&gt;
The same snapshot system powers schema-drift rules (VG101–VG104). Instead of checking queries against migrations, these rules check ORM models against migrations.&lt;br&gt;
Say you have a Go struct:&lt;br&gt;
type Order struct {&lt;br&gt;
    ID          int    &lt;code&gt;db:"id"&lt;/code&gt;&lt;br&gt;
    UserID      int    &lt;code&gt;db:"user_id"&lt;/code&gt;&lt;br&gt;
    Total       string &lt;code&gt;db:"total"&lt;/code&gt;&lt;br&gt;
    Status      string &lt;code&gt;db:"status"&lt;/code&gt;&lt;br&gt;
    GhostStatus string &lt;code&gt;db:"ghost_status"&lt;/code&gt;&lt;br&gt;
}&lt;br&gt;
Valk Guard's Go model extractor walks the AST, reads the db struct tags, and produces a ModelDef with columns: id, user_id, total, status, ghost_status.&lt;br&gt;
VG101 then compares each model column against the migration snapshot. ghost_status isn't in the orders table → finding:&lt;br&gt;
VG101: model "orders" references column "ghost_status" not found in table "orders" schema; check migration DDL or update model mapping&lt;br&gt;
Two different rules, two different input paths (query vs. model), same schema snapshot, same answer.&lt;br&gt;
What this means in&amp;nbsp;practice&lt;br&gt;
You don't need a running database. You don't need to run migrations. You don't need to connect to staging. Valk Guard reads your source code and your migration files, builds everything in memory, and cross-references them statically.&lt;br&gt;
This runs in CI in seconds. It catches the kind of bug that usually shows up as a column "ghost_status" does not exist error in your logs at 2am - and moves it to a PR comment at 2pm instead.&lt;br&gt;
go install github.com/valkdb/valk-guard/cmd/valk-guard@latest&lt;br&gt;
valk-guard scan .&lt;br&gt;
Repo: github.com/ValkDB/valk-guard&lt;/p&gt;

</description>
      <category>ai</category>
      <category>postgres</category>
      <category>devops</category>
      <category>go</category>
    </item>
  </channel>
</rss>
