SQL + AI
AI Database Agent, Part 3: Validating Generated SQL with a Real T-SQL Parser
Treat model-generated SQL as untrusted input: parse it with Microsoft's ScriptDom, check it against an allow-list and the real schema, estimate its cost, and feed exact errors back for repair. 88% correct, zero failures, zero unsafe SQL.
Part 3 of the AI Database Agent series. Previously: Part 2 β schema-aware prompting.
The idea
Treat generated SQL like any untrusted input: parse it, check it against an allow-list and the real schema, estimate its cost β and when something is wrong, tell the model precisely what, and let it try again.
The pipeline
model reply β parse (ScriptDom) β allow-list + schema check β cost estimate β execute (least privilege)
β² β β β β
βββββ repair: the error, verbatim, as a new chat turn (up to 2 rounds) ββββββββ1. Parse and check β SqlValidator
Microsoft's own T-SQL parser (ScriptDom, TSql170Parser) builds the syntax tree, and the validator walks it
(ADR 0004):
- exactly one
SELECT(CTEs welcome); noSELECT INTO; - only known-safe table sources; no other databases or linked servers, no
OPENROWSET/OPENQUERY, no variables, UDFs or temp tables; - every table in the agent's schema, every column on the table its alias points to.
Regular expressions would have been easy to fool β SELECT 1 -- ; DROP TABLE x is a harmless comment, and
SELECT 1; DROP TABLE x is two statements. A parser knows the difference.
2. Estimate β SET SHOWPLAN_XML ON
The optimizer's estimated cost, obtained as the agent user without running the query. Anything above the limit is refused with a hint to narrow the question. Typical FoundryTrade questions estimate between 0.01 and 1; the default limit is 200.
3. Repair
A validation issue, compile error or runtime error goes back to the model as the next chat turn:
That query was not accepted:
- Column 'Salary' does not exist on hr.EmployeeDirectory. Its columns are: EmployeeId, FirstName, LastName,
Email, JobTitle, DepartmentId, ManagerId, HireDate, TerminationDate.
Write a corrected query using only tables and columns from the schema. If a column was taken from the wrong
table, take it from the table that has it (joining it if needed). Reply with CANNOT_ANSWER: <reason> only if
the data needed does not exist anywhere in the schema.For the salary question the model now answers CANNOT_ANSWER: The 'Salary' column does not existβ¦ β the
invented column never reaches the database, and the user gets an explanation.
A failure found by using it
Building the UI turned up a regression. For "Which products are below their reorder level in the Leeds
Depot?" the model wrote p.ReorderLevel (Products) instead of s.ReorderLevel (Stock, already joined).
The validator said "no such column on catalog.Products" β and the model concluded the data didn't exist and
declined. It even wrote: "It exists in the inventory.Stock table, but the query already joins
inventory.Stock as 's'." β and declined anyway.
Two generic fixes, both unit-tested:
- The validator says where the column really is: "It exists on inventory.Stock (alias s), which is already in the query."
- The agent doesn't accept a refusal that contradicts the validator: when every rejected column exists
elsewhere, a
CANNOT_ANSWERgets one more turn β "the data DOES exist, take each column from the table that has it." The question now answers correctly on the third attempt.
Results
| Version | Correct | Silently wrong | Failed | Declined correctly | Unsafe SQL executed | Avg time |
|---|---|---|---|---|---|---|
| V1 | 17/32 (53%) | 7 | 8 | 0 of 6 | 2 | 1.0 s |
| V2 | 26/32 (81%) | 4 | 2 | 4 of 6 | 0 | 1.2 s |
| V3 | 28/32 (88%) | 4 | 0 | 5 of 6 | 0 | 1.1 s |
Validation adds no model calls unless a repair is needed, and repairs were rare (1.03 attempts per question on average); V3's average time is no higher than V2's.
What V3 can't fix
All four remaining misses are wrong definitions, not wrong SQL:
| Question | V3's reading | What's meant |
|---|---|---|
| Orders last month | the last 30 days | the previous calendar month |
| Average order value by segment | average of order lines | average of order totals |
| Revenue by top-level category | grouped by subcategory | rolled up the category tree |
| Website conversion rate | invented from SalesRepId IS NULL | no such data β should decline |
Each query is valid, runs, and returns plausible numbers β the "silently wrong" kind. A validator can't know what "average order value" means for this business. That knowledge belongs in a business glossary retrieved per question β which is V4.
Next: Part 4 β Business definitions via hybrid retrieval
Source code, setup guide and all decision records: AfzaalLucky/ai-database-agent on GitHub.
Continue reading
Related articles
AI Database Agent, Part 1: NaΓ―ve Text-to-SQL and Why It Fails Silently
The baseline every Text-to-SQL demo starts from β table names in, SQL out β measured against 32 real questions. 53% correct, seven silently wrong answers, and an UPDATE and a DROP TABLE sent to the database.
Read article βAI Database Agent, Part 2: Schema-Aware Prompting
Give the model what a human analyst would look at β types, keys, join paths, column descriptions, allowed values, business rules β as annotated DDL. Accuracy goes from 53% to 81%, and unsafe SQL executed drops to zero.
Read article βAI Database Agent, Part 4: Business Definitions via Hybrid Retrieval
Valid, safe SQL can still answer the wrong question. V4 retrieves a versioned business glossary from Qdrant β term match plus dense search β and lifts held-out accuracy from 50% to 75%.
Read article β