<?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: yanou16</title>
    <description>The latest articles on DEV Community by yanou16 (@yanou16).</description>
    <link>https://dev.to/yanou16</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%2F2275021%2F1c94a13d-23ac-4b51-b9b4-d62f533cdde1.png</url>
      <title>DEV Community: yanou16</title>
      <link>https://dev.to/yanou16</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yanou16"/>
    <language>en</language>
    <item>
      <title>How I Built a Production AI Query Engine on 28 Tables — And Why I Used Both Text-to-SQL and Function Calling</title>
      <dc:creator>yanou16</dc:creator>
      <pubDate>Thu, 19 Mar 2026 15:32:46 +0000</pubDate>
      <link>https://dev.to/yanou16/how-i-built-a-production-ai-query-engine-on-28-tables-and-why-i-used-both-text-to-sql-and-3mk1</link>
      <guid>https://dev.to/yanou16/how-i-built-a-production-ai-query-engine-on-28-tables-and-why-i-used-both-text-to-sql-and-3mk1</guid>
      <description>&lt;p&gt;A real production system running on an affiliate marketing ERP — not a demo.&lt;/p&gt;




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

&lt;p&gt;The operations team at an affiliate marketing company I worked with hit a classic wall.&lt;/p&gt;

&lt;p&gt;They had all the data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Revenue by affiliate
&lt;/li&gt;
&lt;li&gt;Fraud flags
&lt;/li&gt;
&lt;li&gt;Campaign ROI
&lt;/li&gt;
&lt;li&gt;Conversion rates by traffic source
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of it live in a 28-table MySQL database.&lt;/p&gt;

&lt;p&gt;But every non-trivial question required SQL.&lt;br&gt;&lt;br&gt;
And the people asking the questions weren’t the ones who could write it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The goal:&lt;/strong&gt;&lt;br&gt;
Let a non-technical operator ask a question in plain English and get a real answer instantly.&lt;/p&gt;

&lt;p&gt;No dashboards. No SQL. No waiting for a dev.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake #1 — I Started with Function Calling
&lt;/h2&gt;

&lt;p&gt;On paper, it looks perfect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured JSON
&lt;/li&gt;
&lt;li&gt;Controlled schema
&lt;/li&gt;
&lt;li&gt;Safe execution
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I tried a real query:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Compare campaign ROI this month vs last month, by traffic source, excluding fraud flags, grouped by affiliate tier”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The schema needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15+ nested parameters
&lt;/li&gt;
&lt;li&gt;Time comparison logic
&lt;/li&gt;
&lt;li&gt;Multi-table joins
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The LLM hallucinated fields.&lt;br&gt;&lt;br&gt;
The result was garbage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reality:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Function Calling breaks fast on analytical queries.&lt;/p&gt;

&lt;p&gt;SQL exists for a reason.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake #2 — Text-to-SQL Alone Terrified Me
&lt;/h2&gt;

&lt;p&gt;So I switched to Text-to-SQL.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Flexible
&lt;/li&gt;
&lt;li&gt;Accurate
&lt;/li&gt;
&lt;li&gt;Handles complexity naturally
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there was a problem.&lt;/p&gt;

&lt;p&gt;This was a &lt;strong&gt;production database&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Financial data
&lt;/li&gt;
&lt;li&gt;Affiliate records
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Letting an LLM generate raw SQL is risky.&lt;/p&gt;

&lt;p&gt;“Only write SELECT” in a prompt is not a guarantee.&lt;br&gt;&lt;br&gt;
It’s a suggestion.&lt;/p&gt;

&lt;p&gt;I needed something stronger.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution — The Router Pattern
&lt;/h2&gt;

&lt;p&gt;Instead of choosing one approach, I built both.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 Text-to-SQL → The Brain
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Handles analytical queries
&lt;/li&gt;
&lt;li&gt;Full SQL flexibility
&lt;/li&gt;
&lt;li&gt;Cross-table logic
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ✋ Function Calling (MCP) → The Hands
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Executes actions
&lt;/li&gt;
&lt;li&gt;Predefined tools only
&lt;/li&gt;
&lt;li&gt;Human approval for sensitive operations
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Router
&lt;/h3&gt;

&lt;p&gt;A lightweight classifier decides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Analytical → Text-to-SQL
&lt;/li&gt;
&lt;li&gt;Action → Function Calling
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The AST Validator — The Real Safety Layer
&lt;/h2&gt;

&lt;p&gt;This is what makes Text-to-SQL usable in production.&lt;/p&gt;

&lt;p&gt;Instead of trusting the LLM, I validate the SQL &lt;strong&gt;before execution&lt;/strong&gt; using an Abstract Syntax Tree.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1 — Regex Filter
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Blocks &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;DROP&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Detects multi-statement injections (&lt;code&gt;;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Executes in &amp;lt;1ms&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Layer 2 — AST Parsing
&lt;/h3&gt;

&lt;p&gt;Using &lt;code&gt;node-sql-parser&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Converts SQL into a structured tree
&lt;/li&gt;
&lt;li&gt;Validates &lt;code&gt;stmt.type === 'select'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Works on semantics, not string matching
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/*DELETE*/ SELECT ...&lt;/code&gt; → passes regex
&lt;/li&gt;
&lt;li&gt;AST still confirms it's a &lt;code&gt;SELECT&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Layer 3 — Table Allowlist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Recursively scans:

&lt;ul&gt;
&lt;li&gt;FROM
&lt;/li&gt;
&lt;li&gt;JOIN
&lt;/li&gt;
&lt;li&gt;Subqueries
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Only allows approved tables
&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Even valid SQL gets blocked if it touches unauthorized tables.&lt;/p&gt;




&lt;h3&gt;
  
  
  Result
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Zero legitimate queries blocked
&lt;/li&gt;
&lt;li&gt;Multiple injection attempts caught during testing
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the difference between a demo and production.&lt;/p&gt;




&lt;h2&gt;
  
  
  The MCP Layer — Giving the AI Hands
&lt;/h2&gt;

&lt;p&gt;For actions, I don’t let the LLM generate SQL.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It calls predefined tools
&lt;/li&gt;
&lt;li&gt;Each tool is a controlled Node.js function
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;suspend_affiliate(affiliate_id)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flag_transaction(transaction_id)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Human-in-the-loop
&lt;/h3&gt;

&lt;p&gt;For sensitive actions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;LLM proposes
&lt;/li&gt;
&lt;li&gt;Manager approves (Slack)
&lt;/li&gt;
&lt;li&gt;Tool executes
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No direct execution from the model.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Looks Like in Production
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Analytical Query
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Operator:&lt;br&gt;&lt;br&gt;
“Show me affiliates with EPC drop &amp;gt; 30% this week”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Router → Text-to-SQL  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;LLM generates SQL  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AST Validator passes  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;MySQL executes  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Results → dashboard  &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Fraud Workflow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;n8n detects anomaly
&lt;/li&gt;
&lt;li&gt;Risk classified
&lt;/li&gt;
&lt;li&gt;High risk → MCP tool triggered
&lt;/li&gt;
&lt;li&gt;Telegram alert sent
&lt;/li&gt;
&lt;li&gt;Human approval
&lt;/li&gt;
&lt;li&gt;Affiliate suspended + logged
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;👉 18 workflows running this pattern in production&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Worked
&lt;/h3&gt;

&lt;p&gt;The 3-layer validation looked over-engineered.&lt;br&gt;&lt;br&gt;
In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regex = fast
&lt;/li&gt;
&lt;li&gt;AST = precise
&lt;/li&gt;
&lt;li&gt;Allowlist = strict
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each layer matters.&lt;/p&gt;




&lt;h3&gt;
  
  
  What I Underestimated
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Schema semantics.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bad:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;affiliate_tier&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Better:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;affiliate_tier (Platinum/Gold/Silver/AtRisk — performance classification)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This alone improves SQL quality a lot.&lt;/p&gt;




&lt;h3&gt;
  
  
  What’s Missing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Eval pipeline (automated accuracy tracking)
&lt;/li&gt;
&lt;li&gt;Semantic table routing (pgvector)
&lt;/li&gt;
&lt;li&gt;Dynamic context reduction
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Honest Take
&lt;/h2&gt;

&lt;p&gt;Most “production AI” demos skip security entirely.&lt;/p&gt;

&lt;p&gt;The AST validator is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;not flashy
&lt;/li&gt;
&lt;li&gt;not viral
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it’s the only reason this system is safe to run on real data.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture in One Line
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Brain&lt;/strong&gt; = Text-to-SQL + AST Validator → read-only analytics
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hands&lt;/strong&gt; = Function Calling (MCP) → actions + control
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s not about choosing one.&lt;/p&gt;

&lt;p&gt;It’s about routing correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;If you're building something similar, the hardest part isn’t generation.&lt;/p&gt;

&lt;p&gt;It’s control.&lt;/p&gt;

&lt;p&gt;Curious how others are handling this in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connect
&lt;/h2&gt;

&lt;p&gt;I share more real-world AI system breakdowns here:&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://www.linkedin.com/in/rayane-louzazna-b7752b224/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/rayane-louzazna-b7752b224/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>mcp</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
