How To · DomainAdvanced

How to Build a Text-to-SQL Agent

How to build a text-to-SQL (NL2SQL) agent: schema packing, dialect, read-only DB user, dry-run, and evals on golden queries that catch silent wrong SQL results.

Schema packerDialect templatesRead-only DB roleEXPLAIN dry-runGolden query evals17 min · 6 steps · Updated 2026-08-25
The short answer

To build a text-to-SQL agent, pack only the relevant schema into the model, generate dialect-correct SELECT statements, execute them as a read-only database user after a dry-run/EXPLAIN and row/cost guards, and score the agent on golden questions whose result hashes you already know. Natural language to SQL is unsafe until authorization and evals sit outside the model.

What you’ll build

  • A schema-packing layer that sends only relevant tables, columns, and sample values per question
  • Dialect-correct SQL (Snowflake, BigQuery, Postgres, T-SQL) with identifiers quoted as required
  • Execution only as a read-only user, after EXPLAIN/dry-run and a cost/row guard
  • Query-time ACL filters injected server-side so the model cannot omit a tenant predicate
  • Execution accuracy and exact-SQL match on a golden set, gated in CI

Before you start

  • 01A warehouse or OLTP replica you are allowed to query, with a documented schema
  • 02A read-only database role with column-level grants that match user groups
  • 0350+ golden natural-language questions with verified SQL and result hashes
  • 04An LLM that supports constrained SQL or tool calling, pinned by version
  • 05Query timeout, row cap, and a place to log SQL before it runs

Key takeaways

  • 01

    NL2SQL quality is mostly schema packing and gold evals; a larger model will still join the wrong table if you dump 400 relations into context.

  • 02

    The database role is the security boundary: read-only, column grants, timeout, row cap, and server-injected tenant predicates.

  • 03

    Dry-run with EXPLAIN (or equivalent) catches full scans and parse errors; it does not catch wrong business grain — golden result hashes do.

  • 04

    Hybrid retrieval over table/column descriptions (and synonym maps) is how the agent finds `arr` when the user says “revenue.”

  • 05

    Ship behind shadow mode: generate and score SQL without writing results to the user until execution accuracy holds.

01

What a text-to-SQL agent is (and is not)

A text-to-SQL, or NL2SQL, agent turns a natural-language question into a database query, runs it, and returns results. It is not a general analyst: it should refuse questions the schema cannot support, and it should not UPDATE. Compared with a RAG chatbot, the failure mode is worse — a fluent wrong number looks like a metric, not like a bad paragraph.

Treat SQL generation as function calling: the model proposes, the executor authorizes and runs. If you paste the question into a notebook with admin credentials, you have a demo, not an agent.

02

Schema packing and synonym retrieval

Models write better SQL when they see the right 4 tables, not 400. Store descriptions and sample values; retrieve with hybrid search so “NPS” hits `nps_score` and “customer” does not hit every table with `customer_id`. Include join paths among the retrieved tables. If two tables share a name pattern, show the grain in the description (`orders` is one row per checkout, `order_items` is one row per SKU).

Refresh the catalog on schema change. Stale columns produce confident SQL against missing fields. For large warehouses, maintain a verified subset for the agent; ungoverned sandbox tables do not belong in retrieval until an owner writes a description.

03

Dialect, read-only users, and dry-run

Generate for the engine you run. Date functions, identifier quoting, JSON operators, and LIMIT/TOP differ enough to make SQLite few-shots harmful. Keep a dialect template and a lint that rejects vendor-wrong syntax before the round-trip.

Create a dedicated read-only role: SELECT on the allowed views, no INSERT/UPDATE/DELETE/DDL, statement timeout, and a row cap. Prefer exposing views that already encode grain and PII policy. EXPLAIN or dry-run before fetch; kill queries over a bytes or slot-ms budget. The model must not set the timeout to zero.

04

Row-level security the model cannot skip

Query-time ACLs for SQL mean the executor attaches predicates from the authenticated user: tenant id, region, and column grants. Database RLS is the strong form; if you cannot use it, wrap SQL in a subquery that applies the filter and reject statements that reference disallowed catalogs. Treat “forget the filter” as a tested attack, same as prompt injection.

Do not return errors that reveal forbidden tables (“relation hr_salary does not exist” vs “permission denied”) in a way that maps the schema for an unauthorized user. Log those attempts.

05

Golden queries: execution accuracy beats pretty SQL

Exact SQL match is brittle (aliases, CTE vs subquery). Execution accuracy compares result sets (sorted, typed) to a gold hash. Keep both: exact match for regressions on a pin, execution accuracy for semantic equivalence. Include questions that must refuse: missing grain, write requests, and ambiguous metrics without a definition.

A four-week path: week 1 catalog and gold set, week 2 packer plus read-only executor, week 3 dialect guards and shadow queries, week 4 CI gate and runbooks. The client owns the golden queries, views, and evals. No token markup; warehouse cost is usually the surprise bill — budget it next to the model.

  • Pack: retrieve tables/columns/samples, do not dump the warehouse
  • Generate: SELECT/CTE only, dialect templates, SQL as a tool
  • Execute: read-only role, ACL inject, EXPLAIN, timeout, row cap
  • Eval: result hashes, refuse cases, shadow mode, CI on the real engine
06

When not to build NL2SQL

If the warehouse has no descriptions, no primary keys, and five competing “revenue” tables, you will generate plausible fiction. Fix the semantic layer first, or restrict the agent to a handful of certified views. If users need writes, that is a different product with change tickets — not this agent.

If questions are policy and runbook text, use document RAG instead. Text-to-SQL is for questions whose answer is a result set. Mixing both in one unconstrained agent is how SQL ends up in a wiki tool and wiki text ends up in a JOIN.

Step-by-step build

  1. 1

    Catalog schema with business names

    Export tables, columns, types, primary/foreign keys, and owners. Add a short business description and 3–5 sample values per column people filter on. Record dialect (Snowflake, BigQuery, Postgres, T-SQL) and quoting rules. This catalog is the retrieval corpus, not a dump into every prompt.

  2. 2

    Pack schema per question

    Embed table and column descriptions; retrieve the top tables with hybrid search (names and synonyms are BM25 work). Cap context: typically a handful of tables, their columns, join keys, and samples. If retrieval misses, ask a clarifying question instead of guessing a table.

  3. 3

    Generate SQL as a tool, not as chat

    Expose `propose_sql` with a schema for the statement string and a rationale. Constrain to a single SELECT/CTE. Include dialect few-shots (date trunc, identifier quoting). The model never gets a connection string.

  4. 4

    Validate, dry-run, and inject ACLs

    Parse the SQL. Reject anything that is not read-only. Inject tenant/user predicates from the session. Run EXPLAIN (or BigQuery dry run) with a bytes/cost ceiling. Cap rows and timeout. Log the final SQL before execution.

  5. 5

    Execute and present with citations

    Run as the read-only role. Return a truncated result grid plus the SQL the user can inspect. Cite tables used. On error, feed the engine message back once; if it still fails, refuse. Do not auto-loop into a write.

  6. 6

    Evaluate on golden queries and shadow

    Keep 50–200 questions with gold SQL and result hashes, including dialect traps and “should refuse” items. Score exact match, execution accuracy (result equivalence), and ACL-omission attempts. Shadow on live questions; promote when both exact-enough SQL and safe rejects hold.

Common pitfalls

The mistakes that show up in real deployments — each one costs a week if you learn it the hard way.

Dumping the whole warehouse schema into the prompt

Token limits fill with unused tables and the model joins the wrong `user_id`. Retrieve relevant tables first (names, descriptions, sample values), then generate SQL.

Running as a writer or as a superuser

NL2SQL plus DROP is a horror story, not a joke. A dedicated read-only role, revoked DDL/DML, and a statement filter that allows only SELECT (or WITH…SELECT) are mandatory.

Trusting EXPLAIN-looking SQL without running gold results

Valid SQL can still join on the wrong grain and return a plausible number. Score result hashes and execution accuracy, not only “the query parsed.”

Letting the model write the tenant filter

If the agent “forgets” `org_id = …`, you have a cross-tenant leak. Inject row-level predicates in the executor from the session, then reject SQL that tries to overwrite them.

No dialect tests

Postgres LIMIT vs T-SQL TOP vs BigQuery date functions will fail in production after a demo on SQLite. Golden queries must run on the real engine.

Frequently asked questions

How do you build a text-to-SQL agent?

Retrieve only the relevant schema, have the model propose a SELECT in your warehouse dialect, execute it as a read-only user after EXPLAIN and ACL injection, and score result hashes on a golden set. The database role and evals — not the prompt — make NL2SQL production-safe.

What is NL2SQL?

NL2SQL (natural language to SQL) is generating a database query from a user question, then running it. A text-to-SQL agent wraps that loop with schema packing, a read-only executor, and evals. Valid SQL is not enough; wrong joins produce confident, wrong numbers.

How do I stop a text-to-SQL agent from changing data?

Use a database role that can only SELECT, parse and reject non-read statements, skip any path that sends user text to a writer credential, and log SQL before execution. Confirmation is not enough if the role can DELETE. Put the boundary in the warehouse, then again in the executor.

How do you evaluate a text-to-SQL agent?

Run a frozen set of natural-language questions against the real engine and compare result sets to gold hashes (execution accuracy), plus exact-SQL match on a pin. Include dialect traps, ACL-omission attempts, and questions that must refuse. CI should fail when execution accuracy drops.

When is text-to-SQL the wrong design?

When the question is answered by documents, when the schema is undocumented, or when users need writes. Also skip it if you cannot inject tenant filters — a skipped WHERE is a data leak. Build a certified view layer first, or use RAG for prose and keep SQL off the table.

Want this built for your team?

From architecture review to a deployed, evaluated system your engineers own — we ship it with you.

FAQ

Working with us

How soon can this build work start?

Typically within a week or two of a scope being agreed. The first delivery is deliberately a small batch so you can check the output against your expectations before volume ramps.

What do you need from our team?

One process owner who knows the workflow, one engineer with access to the systems involved, and a weekly 45-minute review. No standing committee, and no requirement for an ML specialist on your side.

Who owns the output and the data?

You do. Datasets, labels, weights, evaluation suites and runbooks are yours and are handed over at the end. Your data trains your models only, with zero-retention provider settings by default.

Can you scale volume up quickly if we need it?

Yes, and the quality bar holds because the rubric and gold set are already agreed by that point. Ramping is a staffing question, not a re-scoping one, so it usually takes days rather than a new engagement.

We already have a vendor for this. Why switch?

Often you should not. The cases where teams move to us are when they cannot get a quality number out of their current vendor, or when the work is delivered as an opaque batch with no trace of how disagreements were resolved.

What happens after the engagement ends?

We stay on-call for 30 days at no extra cost, then move to an optional support retainer. Most teams also keep a quarterly evaluation review with us to catch drift early.

Copyright © 2026
ReinforcedX, Inc.
All rights reserved