<?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: Yuva Kunaal</title>
    <description>The latest articles on DEV Community by Yuva Kunaal (@yuva_kunaal).</description>
    <link>https://dev.to/yuva_kunaal</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3920570%2F784c7f4b-9f9b-46a9-90bc-4fde7f65dde7.jpg</url>
      <title>DEV Community: Yuva Kunaal</title>
      <link>https://dev.to/yuva_kunaal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yuva_kunaal"/>
    <language>en</language>
    <item>
      <title>"Wrong Answer" is the worst feedback you can give a SQL learner — so I built something better</title>
      <dc:creator>Yuva Kunaal</dc:creator>
      <pubDate>Mon, 18 May 2026 04:16:54 +0000</pubDate>
      <link>https://dev.to/yuva_kunaal/wrong-answer-is-the-worst-feedback-you-can-give-a-sql-learner-so-i-built-something-better-3pka</link>
      <guid>https://dev.to/yuva_kunaal/wrong-answer-is-the-worst-feedback-you-can-give-a-sql-learner-so-i-built-something-better-3pka</guid>
      <description>&lt;p&gt;You submit a query. It's wrong.&lt;/p&gt;

&lt;p&gt;The platform says: &lt;strong&gt;Incorrect result.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;No diff. No hint. No explanation of which rows are wrong or why. Just a red banner telling you to try again.&lt;/p&gt;

&lt;p&gt;I've used enough SQL learning tools to know this is the standard. And I think it's the single biggest reason people get stuck and give up — not because SQL is hard, but because the feedback loop is broken.&lt;/p&gt;

&lt;p&gt;So when I built &lt;a href="https://sqlumina.com" rel="noopener noreferrer"&gt;SQLumina&lt;/a&gt;, I made the debug experience the core feature. Here's exactly how it works under the hood.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with "wrong answer"
&lt;/h2&gt;

&lt;p&gt;When a SQL query returns incorrect results, there are maybe 15 different reasons why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrong JOIN type (INNER when you needed LEFT)&lt;/li&gt;
&lt;li&gt;Joining on the wrong columns (PK to PK instead of FK to PK)&lt;/li&gt;
&lt;li&gt;Missing GROUP BY column&lt;/li&gt;
&lt;li&gt;WHERE clause firing before aggregation&lt;/li&gt;
&lt;li&gt;HAVING threshold off by one&lt;/li&gt;
&lt;li&gt;LIKE missing a wildcard&lt;/li&gt;
&lt;li&gt;NULL comparison written as &lt;code&gt;col = NULL&lt;/code&gt; instead of &lt;code&gt;IS NULL&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these produces a different shape of wrong output. A generic "incorrect" banner treats them all the same. That's useless.&lt;/p&gt;

&lt;p&gt;What a learner actually needs is: &lt;strong&gt;which rows are wrong, and why.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — Building the row-level diff
&lt;/h2&gt;

&lt;p&gt;Every challenge in SQLumina has a reference solution stored server-side. When a user submits, the backend runs both queries concurrently against the live PostgreSQL database:&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;user_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ref_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;run_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_sql&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="nf"&gt;run_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference_sql&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it computes three sets:&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;correct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_rows&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;reference_rows&lt;/span&gt;
&lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reference_rows&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;user_rows&lt;/span&gt;
&lt;span class="n"&gt;extra&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_rows&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;reference_rows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These map directly to the three columns in the debug panel: &lt;strong&gt;Correct&lt;/strong&gt;, &lt;strong&gt;Missing&lt;/strong&gt;, &lt;strong&gt;Extra&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The diff table shows full row values — all columns, no truncation. If a row is missing because your JOIN dropped it, you can see exactly which row it was.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 — Accuracy scoring with Jaccard similarity
&lt;/h2&gt;

&lt;p&gt;Once you have the three sets, you need a single accuracy number.&lt;/p&gt;

&lt;p&gt;I use Jaccard similarity:&lt;br&gt;
accuracy = |correct| / (|correct| + |missing| + |extra|)&lt;/p&gt;

&lt;p&gt;The reason this matters: a simple "percentage correct" would reward over-fetching. If the expected result is 10 rows and you return 10,000, you'd technically have 100% of the correct rows — you'd just also have 9,990 extra ones.&lt;/p&gt;

&lt;p&gt;Jaccard penalises both under-fetching and over-fetching. Getting 15 rows when 10 were expected is scored lower than getting exactly 10 right. This matches how production SQL correctness actually works.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 3 — Parsing the query AST
&lt;/h2&gt;

&lt;p&gt;Showing the diff is useful. Explaining &lt;em&gt;why&lt;/em&gt; the diff exists is what makes it genuinely educational.&lt;/p&gt;

&lt;p&gt;This is where it gets interesting.&lt;/p&gt;

&lt;p&gt;I built a 30+ rule heuristic engine that parses the user's actual SQL to extract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Table names and aliases&lt;/li&gt;
&lt;li&gt;JOIN conditions (specifically the ON clause columns)&lt;/li&gt;
&lt;li&gt;SELECT columns and whether they're wrapped in aggregates&lt;/li&gt;
&lt;li&gt;WHERE predicates and their operators&lt;/li&gt;
&lt;li&gt;GROUP BY columns&lt;/li&gt;
&lt;li&gt;HAVING conditions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then it matches those parsed elements against the diff to produce specific diagnosis cards.&lt;/p&gt;

&lt;p&gt;Example rule — wrong JOIN key detection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_join_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;diff&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;join&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parsed&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;left_col&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;
        &lt;span class="n"&gt;right_col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;
        &lt;span class="n"&gt;left_ref&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_table_pk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;right_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_table_pk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;left_col&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;left_ref&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;right_col&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;right_ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;DiagnosisCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;re joining &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;left_col&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;right_col&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Both are primary keys. You probably want the foreign key instead.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;fix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;generate_corrected_join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The card surfaces in the UI with a severity badge, confidence percentage, the specific columns involved, a corrected SQL fragment, and a plain-language explanation of the underlying concept.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — The zero-rows suite
&lt;/h2&gt;

&lt;p&gt;A separate set of 7 rules fires specifically when the user's query returns zero rows against a non-empty expected result. These are the most frustrating bugs to debug because the diff table is just... empty.&lt;/p&gt;

&lt;p&gt;The rules detect:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;col = NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Should be &lt;code&gt;IS NULL&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LIKE 'foo'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Missing wildcard — should be &lt;code&gt;LIKE '%foo%'&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BETWEEN hi AND lo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bounds are reversed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;NOT IN (...)&lt;/code&gt; with NULL in list&lt;/td&gt;
&lt;td&gt;NULL silently excludes every row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date_col &amp;gt; CURRENT_DATE + INTERVAL '30 days'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Future filter — should subtract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WHERE clause over-filtered&lt;/td&gt;
&lt;td&gt;Too many AND conditions, no rows survive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No WHERE when one is required&lt;/td&gt;
&lt;td&gt;Missing filter entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each zero-row rule names the exact predicate from the user's query and suggests the corrected version. No generic messaging.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5 — The pgBouncer bug that took me days
&lt;/h2&gt;

&lt;p&gt;This one isn't about the debugger directly. It's about the infrastructure that makes sandboxed query execution reliable — and it's the bug that cost me the most time.&lt;/p&gt;

&lt;p&gt;SQLumina runs on Neon's serverless PostgreSQL with asyncpg. Each user query runs inside a schema — &lt;code&gt;ecommerce&lt;/code&gt;, &lt;code&gt;social_media&lt;/code&gt;, or &lt;code&gt;banking&lt;/code&gt; — and sandbox queries run in an isolated &lt;code&gt;sandbox_{user_id}&lt;/code&gt; schema.&lt;/p&gt;

&lt;p&gt;To route queries to the right schema, I was setting the search path before executing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET search_path TO &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In development: worked perfectly.&lt;/p&gt;

&lt;p&gt;In production under any real load: randomly failing with &lt;code&gt;relation "products" does not exist&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Not consistently. Not reproducibly. Just sometimes, on concurrent requests.&lt;/p&gt;

&lt;p&gt;The root cause: &lt;strong&gt;Neon uses pgBouncer in transaction mode.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In transaction mode, pgBouncer doesn't guarantee that two consecutive statements from the same application connection hit the same backend PostgreSQL connection. The &lt;code&gt;SET search_path&lt;/code&gt; runs on connection A. The actual query lands on connection B — which has a fresh search path pointing at &lt;code&gt;public&lt;/code&gt;, where your tables don't exist.&lt;/p&gt;

&lt;p&gt;The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET LOCAL search_path TO &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET LOCAL statement_timeout = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5000&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_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;SET LOCAL&lt;/code&gt; scopes the change to the current transaction. Since the transaction is atomic, both statements are guaranteed to execute on the same backend connection. pgBouncer can't split them.&lt;/p&gt;

&lt;p&gt;Two lines. Took days to find. Now it never fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're building on Neon and hitting "relation does not exist" errors randomly under load — this is your fix.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6 — Sandboxing safely
&lt;/h2&gt;

&lt;p&gt;A few things that keep user queries from doing damage:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comment stripping&lt;/strong&gt; — SQL comments (&lt;code&gt;--&lt;/code&gt; and &lt;code&gt;/* */&lt;/code&gt;) are stripped before parsing so users can't hide keywords inside comments to bypass validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Destructive operation blocking&lt;/strong&gt; — &lt;code&gt;DROP DATABASE&lt;/code&gt;, &lt;code&gt;TRUNCATE&lt;/code&gt; outside sandbox scope, and &lt;code&gt;GRANT&lt;/code&gt; are blocked at the parser level before execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-LIMIT&lt;/strong&gt; — &lt;code&gt;LIMIT 1000&lt;/code&gt; is appended when absent so a &lt;code&gt;SELECT *&lt;/code&gt; on a large table doesn't flood the frontend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeout&lt;/strong&gt; — &lt;code&gt;SET LOCAL statement_timeout = '5000'&lt;/code&gt; kills queries at the DB level after 5 seconds. A Python &lt;code&gt;asyncio.wait_for&lt;/code&gt; at 6 seconds is a belt-and-suspenders fallback — PostgreSQL cancels cleanly before Python times out, which prevents dirty pool connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read-only enforcement&lt;/strong&gt; — The main &lt;code&gt;ecommerce&lt;/code&gt;, &lt;code&gt;social_media&lt;/code&gt;, and &lt;code&gt;banking&lt;/code&gt; schemas are strictly read-only from the Playground. Only the user's own sandbox schema allows DML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-user isolation&lt;/strong&gt; — Every user gets their own &lt;code&gt;sandbox_{user_id}&lt;/code&gt; schema. Other users' tables are completely invisible. Schema names are case-folded and sanitised before interpolation so two Clerk IDs that differ only in case can never collide.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Building this taught me more SQL than taking any course did.&lt;/p&gt;

&lt;p&gt;You can't write a rule that detects "you're joining on two primary keys" without understanding deeply why that's wrong and what the correct foreign key relationship should look like. You can't write the zero-rows suite without knowing every way a query can silently exclude all its data.&lt;/p&gt;

&lt;p&gt;The debug engine is now 30+ rules. It's also the thing I'm most proud of in the entire codebase.&lt;/p&gt;

&lt;p&gt;If you're building anything where users write queries against real data — especially in a learning context — invest in the feedback layer before anything else. The SQL execution part is easy. Telling someone &lt;em&gt;specifically&lt;/em&gt; what they got wrong is the hard problem.&lt;/p&gt;




&lt;p&gt;SQLumina is about to launch...&lt;/p&gt;

</description>
      <category>sql</category>
      <category>python</category>
      <category>beginners</category>
      <category>backend</category>
    </item>
    <item>
      <title>CommitAI — Local AI-Powered Git Assistant Using Gemma 4</title>
      <dc:creator>Yuva Kunaal</dc:creator>
      <pubDate>Fri, 08 May 2026 20:56:09 +0000</pubDate>
      <link>https://dev.to/yuva_kunaal/commitai-local-ai-powered-git-assistant-using-gemma-4-5027</link>
      <guid>https://dev.to/yuva_kunaal/commitai-local-ai-powered-git-assistant-using-gemma-4-5027</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;As developers, we repeatedly write commit messages, maintain changelogs, and switch context during development. I wanted to build a lightweight developer tool that could automate this workflow completely offline without relying on cloud APIs or external AI services.&lt;/p&gt;

&lt;p&gt;So I built CommitAI — a local AI-powered Git assistant using Gemma 4 + Ollama.&lt;/p&gt;

&lt;p&gt;The workflow is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read staged Git diff&lt;/li&gt;
&lt;li&gt;Send it to a local Gemma 4 model&lt;/li&gt;
&lt;li&gt;Generate a clean Conventional Commit message&lt;/li&gt;
&lt;li&gt;Update changelog automatically&lt;/li&gt;
&lt;li&gt;Execute the Git commit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also added Git hook integration so the workflow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the AI handles the rest.&lt;/p&gt;

&lt;p&gt;The project is built entirely in Python and runs locally on my MacBook Air M2 (8GB RAM).&lt;/p&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AI-generated Conventional Commit messages&lt;/li&gt;
&lt;li&gt;Automatic changelog generation&lt;/li&gt;
&lt;li&gt;Git hook integration&lt;/li&gt;
&lt;li&gt;Fully local/offline workflow&lt;/li&gt;
&lt;li&gt;Rich CLI interface using the Rich library&lt;/li&gt;
&lt;li&gt;No external APIs&lt;/li&gt;
&lt;li&gt;Privacy-friendly local inference&lt;/li&gt;
&lt;li&gt;Uses staged Git diffs as context&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://drive.google.com/file/d/1-vuOSi-TYU3a_f5aVssizppuhXGfzslf/view?usp=sharing" rel="noopener noreferrer"&gt;Demo Video&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Yuvakunaal/CommitAI" rel="noopener noreferrer"&gt;CommitAI Repo&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Gemma 4
&lt;/h2&gt;

&lt;p&gt;I used the gemma4:e2b model through Ollama for all AI-powered functionality inside CommitAI.&lt;/p&gt;

&lt;p&gt;I specifically chose the E2B model because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it runs efficiently on local hardware&lt;/li&gt;
&lt;li&gt;lightweight enough for my MacBook Air M2 with 8GB RAM&lt;/li&gt;
&lt;li&gt;low-latency responses for CLI workflows&lt;/li&gt;
&lt;li&gt;strong enough reasoning for summarizing Git diffs&lt;/li&gt;
&lt;li&gt;ideal for privacy-focused offline tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gemma 4 powers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;commit message generation&lt;/li&gt;
&lt;li&gt;changelog summarization&lt;/li&gt;
&lt;li&gt;Git workflow automation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Git Diff
   ↓
CommitAI
   ↓
Ollama API
   ↓
Gemma 4 (E2B)
   ↓
AI-generated commit/changelog
   ↓
Git commit execution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;Ollama&lt;/li&gt;
&lt;li&gt;Gemma 4 (gemma4:e2b)&lt;/li&gt;
&lt;li&gt;Rich&lt;/li&gt;
&lt;li&gt;Git Hooks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Future Improvements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;PR description generation&lt;/li&gt;
&lt;li&gt;Multi-model support&lt;/li&gt;
&lt;li&gt;Better diff summarization&lt;/li&gt;
&lt;li&gt;VSCode extension&lt;/li&gt;
&lt;li&gt;Team changelog modes&lt;/li&gt;
&lt;li&gt;Interactive commit editing&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;One of my biggest goals with this project was proving that useful AI developer tooling can run fully locally without depending on cloud APIs.&lt;/p&gt;

&lt;p&gt;Thanks for reading 🚀&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
      <category>python</category>
    </item>
    <item>
      <title>I built a free AI Flowchart Studio — just launched today! 🚀</title>
      <dc:creator>Yuva Kunaal</dc:creator>
      <pubDate>Fri, 08 May 2026 18:27:40 +0000</pubDate>
      <link>https://dev.to/yuva_kunaal/i-built-a-free-ai-flowchart-studio-just-launched-today-46bl</link>
      <guid>https://dev.to/yuva_kunaal/i-built-a-free-ai-flowchart-studio-just-launched-today-46bl</guid>
      <description>&lt;p&gt;Hey DEV community! 👋&lt;/p&gt;

&lt;p&gt;Just launched AI Flowchart Studio today — a tool that converts &lt;br&gt;
plain English into professional flowcharts using a Multi-Agent &lt;br&gt;
Gemini AI pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Existing tools like Lucidchart are too heavy for quick diagrams.&lt;br&gt;
I wanted something where you just DESCRIBE and it builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the AI Works (4-Stage Pipeline)
&lt;/h2&gt;

&lt;p&gt;This is the part I'm most proud of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Orchestrator Agent&lt;/strong&gt; — Validates if the prompt can 
logically become a flowchart&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logic Parser Agent&lt;/strong&gt; — Breaks text into a structured 
JSON graph (nodes + edges)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generator Agent&lt;/strong&gt; — Converts the graph into Mermaid.js 
syntax&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Syntax Validator&lt;/strong&gt; — Final check to guarantee perfect 
visual output&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pipeline ensures "Garbage In" never becomes "Garbage Out" 🎯&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; Vanilla JS, Mermaid.js, html2canvas, CSS3 &lt;br&gt;
Glassmorphism — deployed on Vercel&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt; FastAPI (Python), Google GenAI SDK, &lt;br&gt;
Server-Sent Events for real-time streaming — deployed on Render&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🤖 Multi-Agent AI generation&lt;/li&gt;
&lt;li&gt;✏️ Manual builder (click &amp;amp; type, no coding)&lt;/li&gt;
&lt;li&gt;🔄 Undo/Redo engine with memory buffer&lt;/li&gt;
&lt;li&gt;📤 Export PNG (3x super-scaled), SVG, or Mermaid code&lt;/li&gt;
&lt;li&gt;💾 5 concurrent saved projects&lt;/li&gt;
&lt;li&gt;📱 Mobile-responsive with bottom-sheet menus&lt;/li&gt;
&lt;li&gt;🔍 Hardware-accelerated canvas zoom (+/- 300%)&lt;/li&gt;
&lt;li&gt;🔑 BYOK — your API key never leaves your browser&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Privacy First (BYOK)
&lt;/h2&gt;

&lt;p&gt;Your Gemini API key is stored only in your browser's &lt;br&gt;
LocalStorage. Our backend acts purely as a secure proxy — &lt;br&gt;
zero data retention.&lt;/p&gt;

&lt;p&gt;🔗 Try it: &lt;a href="https://ai-flowchart-studio.vercel.app/" rel="noopener noreferrer"&gt;AI Flowchart Studio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🚀 Also live on Product Hunt today — support appreciated!&lt;br&gt;
&lt;a href="https://www.producthunt.com/products/ai-flowchart-studio?utm_source=other&amp;amp;utm_medium=social" rel="noopener noreferrer"&gt;Product Hunt Launch&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would love feedback from devs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What would make you use this daily?&lt;/li&gt;
&lt;li&gt;Any architecture improvements you'd suggest?&lt;/li&gt;
&lt;li&gt;Would you pay for a hosted version (no API key needed)?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading! 🙏&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>ai</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
