Text-to-SQL in Production, Guardrails and When to Refuse

Pull-quote: “A text-to-SQL system that cannot say ‘I do not know which revenue you mean’ is not an analyst. It is a confident intern with database credentials.”
Why this matters
Text-to-SQL demos are the easiest sale in enterprise AI: type a question, watch a query appear, applaud. Production is where the thousand-table schema lives, where “revenue” has four definitions, and where a wrong-but-plausible number travels into a deck and becomes a fact. The gap between demo and production is not model quality. Current models write competent SQL. The gap is grounding, validation, and the discipline to refuse.
Ground the schema before you generate
Generation quality is bounded by what the model knows about your data, so put the effort where the leverage is.
Scope to curated views, not raw tables. A few dozen governed, documented views beat a thousand raw tables in every dimension that matters: less ambiguity, fewer join traps, smaller prompts. Define the metrics once: what “on-time” means, which date column is truth, how currency conversions happen. Feed the model column descriptions and representative sample values, because half of text-to-SQL failure is entity linking, the mapping from the words in the question to the columns and literal values in the warehouse. For large estates, retrieve relevant schema documentation per question instead of stuffing the whole catalog into context.
Guardrails between draft and execution
| Failure | Cause | Guardrail |
|---|---|---|
| Plausible SQL over the wrong table | Duplicate and stale tables in scope | Allowlisted views, one owner per domain |
| Wrong join, confident tone | Undocumented relationships | Modeled joins only; reject paths outside the model |
| Runaway scans | Unbounded generation | Static checks: parse, allowlist, row limits, timeouts |
| Write attempts | Over-privileged connection | Read-only principal; DDL and DML denied at the database |
| Guessed metrics | Two definitions of the same word | Refusal policy: clarify or decline, never improvise |
Question
│ entity + metric resolution ── unknown or ambiguous ──► clarify or refuse
▼
Schema grounding (curated views, metric definitions, modeled joins)
│ SQL draft
▼
Static validation ── parse fails, table off-allowlist, no limits ──► repair or refuse
│ read-only execution, timeout enforced
▼
Answer + the SQL that ran + the tables it touched
Refusal is a feature
The strongest production systems decline more than the demos do. Refuse, or ask one clarifying question, when the entity cannot be resolved to the schema, when a metric is ambiguous, when the requested join path is not in the model, or when the question is out of scope entirely. Every answer ships with the SQL that produced it and the tables it touched, because a number nobody can audit is a rumor with formatting. Build the evaluation set from real questions the analytics queue actually receives, and track the refusal rate alongside accuracy: a falling refusal rate with rising correction rates means the system has started guessing.
Where this shows up in the field
Domain analytics systems put the whole pattern on display. A question about carriers or lanes has to resolve its entities against a modeled schema before any SQL is drafted, questions that cannot be grounded in the model get declined rather than improvised, and the generated SQL ships as part of the answer, not as a hidden implementation detail. None of that is glamorous, and all of it is what separates an analyst from an oracle.
Closing
Text-to-SQL earns trust the way a good analyst does: by defining terms, by showing the work, and by saying “I do not know” at the right moments. Ground the schema, validate before execution, run read-only, and treat refusal as a designed outcome with its own metrics. The teams that ship this successfully are not the ones with the cleverest prompts. They are the ones who did the data modeling.
