01. What Text-to-SQL Solves
Picture a chef who tastes a dish while cooking, spots a mistake like too much salt, and immediately adjusts the seasoning—then tastes again. This subsystem does the same thing for turning plain English questions into database queries: it automatically writes a first attempt, runs it to see if it works, and if the database spits back an error, it uses that real error message to rewrite the query. Its core job is to fix execution-time mistakes so that the final query actually runs correctly.
Now zoom in: first, the system generates an SQL query, then sends it through a safety gate that checks it is a read-only SELECT statement—otherwise it is rejected. If the query passes, it is executed against the database. When the database returns a syntax error or a "column not found" message, the system captures that exact text and feeds it to a repair_sql node. That node diagnoses the problem and writes a corrected SQL, which then re-enters the safety gate before running again. This loop repeats up to two times, no more. Crucially, the system uses a rule called early-accept: if the query runs successfully on the first try, the loop stops immediately—so a working query is never changed. It also treats an empty result set as success, not a mistake.
The trickiest detail is that emptiness is treated as the right answer, not a bug. Without that rule, a perfectly fine query that simply found no matching records would be wrongly flagged as broken, and the system would waste its limited repair attempts trying to "fix" something that was correct. The loop would burn through its budget, return the last error message, and a user asking "How many orders from last year?" would get a confusing failure instead of the simple truth: "zero orders matched." That's the concrete failure a beginner would feel without this subsystem—a correct question returning an error because the system couldn't distinguish between a real bug and a legitimate empty answer.
Turning a plain language question into an SQL query is a core challenge in data access. Modern large language models make it practical because they can generate candidate SQL code automatically. But their first attempt is often wrong. So systems add an error-driven correction loop. They execute the candidate query against the database. If they get a syntax error or an empty result set, they feed that real feedback back to the model. The model then rewrites the query. This repair cycle boosts accuracy far more than just asking the model to double-check itself. The loop is limited to two repair attempts to keep costs low. Safety is equally important. You cannot trust a raw model output to run directly on production data. The code might drop tables or access other tenants. So engineers enforce a read-only gate. They parse the query into an abstract syntax tree, using a parser like sqlglot. This confirms the statement is only a select query. No insert, delete, or drop is allowed. String matching is too easy to bypass with comments or nesting. The trade-off is between expressiveness and safety. Some systems constrain what users can ask before errors happen. Others let users type freely and fix errors afterward. Which approach yields higher user satisfaction is still an open question. The field is new, with very recent papers and no prior work on Cloudflare D1. Each design choice remains unsettled.
The self-healing loop and SELECT-only gate define how text-to-SQL turns plain questions into corrected, safe queries.
_MAX_REPAIR_ATTEMPTS = 2
_MAX_ROWS = 50
# validate_sql → {execute_sql, repair_sql, END}
# execute_sql → {repair_sql, summarize}
# repair_sql → validate_sql
# summarize → END
# validate_sql runs a SELECT-only gate via sqlglot parse + statement check.
# If it fails (not SELECT), route to END.
# If it passes, route to execute_sql.
# On error from execute_sql, if repair_attempts < _MAX_REPAIR_ATTEMPTS, route to repair_sql.
# repair_sql receives failed SQL + error text, regenerates corrected SELECT.
# Early-accept: successful execution (even empty rows) ends loop.
The subsystem begins at the validate_sql gate, which enforces that only a single read‑only SELECT (verified by AST parsing with sqlglot) reaches execution. Queries that pass are handed to execute_sql against the database. If the database raises a hard error—a missing column, malformed join, or type mismatch—the raw diagnostic message and the failing SQL are captured. Provided the attempt budget has not been exhausted (bounded to 2), the system routes to repair_sql. This node receives the failed SQL and the actual error text, diagnoses the problem, and regenerates a corrected single SELECT. That corrected query re‑enters validate_sql before any execution, so the repair can never widen permissions. The loop ends the moment a query executes successfully, because of the early-accept design choice: a working query is never “repaired” into a different one. An empty result set is treated as success, not a defect to heal—the system considers “no rows matched that filter” to be the true answer. After the attempts are exhausted, the run returns the last error rather than looping forever; the bound, not a hope, is the circuit breaker.
The core invariant is early-accept with success defined as any non‑error execution, including empty results. This guarantees that a correct query is never overwritten by a later repair attempt, and that the system does not mistake a legitimate “no data” answer for a failure requiring intervention. The invariant prevents the loop from burning its budget rewriting a query that already answered the user’s question correctly, even if the answer is zero rows. Additionally, the SELECT‑only gate composes with the repair loop: because repaired SQL re‑validates, the system preserves the invariant that only read‑only statements are ever executed, regardless of how many repairs occur.
The key trade-off is choosing execution‑grounded repair over intrinsic self‑correction. Intrinsic correction—where the model re‑reads its own SQL against a human‑written checklist without running it—contributes only about 1–3 percentage points of accuracy, because the model has no ground truth to react to, so it often “fixes” correct queries or misses real bugs. The execution‑grounded approach rejects that alternative. Instead, it feeds real database exceptions (e.g., “no such column: cust_id”) back to the model, producing substantially better accuracy because the feedback is specific and grounded. The cost avoided is the wasteful rewriting of already‑correct queries and the silent acceptance of bugs that would remain hidden under introspection. The trade-off is that execution‑grounded repair cannot fix queries that run cleanly but answer the wrong question; only a human reading the answer catches that semantic miss.
A concrete failure mode is a semantic miss that produces a successful execution. For example, a user asks “Which customers have overdue invoices?” but the generated query joins the wrong table and returns a list of all customers. The loop sees a non‑error execution, counts it as success, and exits. The system then proceeds to the summarize node, which returns a confident business sentence grounded in the returned rows—in this case, a list that looks plausible but is semantically wrong. The operator would see a polished answer with confidence, the explanation, the source tables, and the executed SQL as evidence, but they would not see any error signal. Only by manually inspecting the answer would they detect the mistake. The system provides provenance, but the responsibility for catching semantic errors falls entirely on the human reader.
Semantically Wrong Query That Executes Successfully
- Trigger — The LLM generates a SQL query that passes
validate_sqland runs without a database error but joins the wrong table or uses a wrong filter, answering a question different from the user’s intent. The self‑healing loop treats any non‑error execution as success. - Guard — No explicit guard in the subsystem for semantic correctness. The
summarizenode will only report rows it actually receives, but it has no way to detect that those rows are irrelevant. The loop’s early‑accept design means a successful execution ends the repair cycle immediately. - Posture — Fail‑soft. The system returns a confident answer with provenance fields (
confidence,reason,source,evidence) that are grounded only in the executed SQL and its result set. The run completes without any error signal, so the operator sees a plausible but wrong answer. - Operator signal — Silent absence: no error field, no exception, no metric like
repair_attemptsincremented. The only sign is a human reading the answer and recognising the semantic miss. The LangSmith metricagentic_sales.text_to_sql.confidencemay be high, misleading the operator. - Recovery — No automated recovery. The operator must manually correct the query or re‑phrase the original question and start a new run. The loop provides no fallback because it never detected a defect.
Repair Attempts Exhausted (Persistent Execution Error)
- Trigger — The initial LLM‑generated SQL fails execution with a database error (e.g., a missing column or type mismatch). The repair node receives the error text and generates a corrected query, but each subsequent repair also fails, up to the maximum of two repair attempts.
- Guard — The loop’s hard bound of two repair attempts (
at most 2 repair attemptsin the cost envelope) acts as the circuit breaker. After the last failed attempt the run does not loop forever; it returns the last error. - Posture — Fail‑hard. The run aborts without a result. The subsystem outputs no rows and the LLM summary is never invoked because there was no successful execution to summarise.
- Operator signal — The LangSmith metric
agentic_sales.text_to_sql.repair_attemptswill be2. The returned object carries the last database error text as thereasonfield, and theevidencefield will contain the last failed SQL. Norow_countmetric is emitted because no rows were fetched. - Recovery — The operator must inspect the reported error and manually correct the query or adjust the schema mapping. The loop does not retry further; the only way forward is a fresh query from the user.
LLM Unavailable During Summarization
- Trigger — The query executed successfully (rows were returned), but when the
summarizenode attempts to call the LLM to produce a business‑language sentence, the LLM is unreachable, times out, or returns an error. - Guard — The
summarizenode contains a deterministic fallback: “LLM unavailable → deterministic ‘The query returned N record(s)’.” No named function is given in the source, but the behaviour is explicitly described. - Posture — Fail‑closed. The summary is replaced with a safe, grounded statement that does not fabricate totals or percentages. The provenance fields (
confidence,reason,source,evidence) are still populated from the executed SQL and result set. - Operator signal — The answer contains a plain, un‑narrated line such as “The query returned 34 record(s)” instead of a natural‑language interpretation. The
confidencemetric may be lowered (if the system sets it conditionally) or unchanged if it was already set from the SQL generation phase. The LangSmith metricagentic_sales.text_to_sql.confidencewould still reflect the SQL‑generation confidence, not the summary. - Recovery — No retry. The run completes with the deterministic fallback. The operator sees that the LLM summary was not produced and can re‑submit the same query to attempt a new summarization; the loop does not automatically re‑try.
Repaired SQL Fails validate_sql (AST Rejection)
- Trigger — The repair node generates a corrected
SELECTstatement. Before execution, this candidate re‑entersvalidate_sql. The AST parser (e.g., sqlglot) rejects it because it contains a DML/DDL statement, multiple statements, or invalid syntax not caught by the database’s own parser. - Guard — No explicit handler is named in the source for a validation rejection within the repair loop. The source states that repaired SQL “always re‑enters
validate_sqlbefore any execution”, but does not define what happens if validation fails—whether it is treated as an execution error and passed back to repair, or whether it immediately exhausts the attempt. The loop’s repair node expects a database error text, not a parse‑tree failure. - Posture — Likely fail‑soft (if the validation failure is treated as an execution error, another repair is triggered) or fail‑hard (if the attempt is counted without a retry because no database error exists). The source is silent, so state: no defined posture.
- Operator signal — Unknown. No metric or log line is described for a
validate_sqlfailure. The operator might see an unexpected error message that does not match typical database syntax errors, or therepair_attemptsmetric might increment without a corresponding execution. - Recovery — Not specified in the source. A manual step is needed to diagnose why the repair produced an invalid AST; the loop may have already consumed an attempt without a database execution.
Empty Result Set Treated as Success (User Expected Data)
- Trigger — The user’s question implies that rows should exist (e.g., “show contacts from California”), but the generated SQL returns zero rows because of an incorrect value‑linking (e.g.,
state = 'California'when the database uses'CA'). The subsystem explicitly counts an empty result set as success (“no rows matched that filter is usually the true answer”). - Guard — The early‑accept rule: “An empty result set counts as success, not a defect to heal.” The
summarizenode handles this by generating “The query ran successfully but matched no records.” - Posture — Fail‑soft. The run completes normally, returning a clear message that no records matched. The operator gets a truthful statement, but if the user expected data, the answer is semantically wrong.
- Operator signal — The answer contains the deterministic phrase “matched no records”. The
row_countLangSmith metric will be0. The provenance fields are still present:confidence(high, because the SQL was valid),reason(the explanation),source(tables_used),evidence(the executed SQL). No error is raised. - Recovery — The operator must recognise that the empty result is due to a value‑linking mistake, not a genuine absence of data. The loop does not attempt repair because it considers the execution successful. The user must re‑phrase the query with correct values.
In What Text-to-SQL Solves, what triggers Semantically Wrong Query That Executes Successfully — and how is it caught?
Show answer
The LLM generates a SQL query that passes `validate_sql` and runs without a database error but joins the wrong table or uses a wrong filter
From the research: Retrieval practice / testing effect — Testing (quizzing) boosts classroom learning: A systematic and meta-analytic review (2021)
In What Text-to-SQL Solves, what triggers Repair Attempts Exhausted (Persistent Execution Error) — and how is it caught?
Show answer
The initial LLM‑generated SQL fails execution with a database error (e.g., a missing column or type mismatch).
From the research: Retrieval practice / testing effect — Testing (quizzing) boosts classroom learning: A systematic and meta-analytic review (2021)