"Let analysts ask questions in English" is the most-requested AI feature on any data platform, and the naive version — dump the schema into a prompt, hope — fails on every real warehouse because real warehouses have 5,000 tables.
Tier: Advanced · Workflow area: Text-to-SQL & AI-ready serving
The pattern that works in production (popularized by Uber's QueryGPT, which reports ~70% query-authoring time cut — vendor-reported, treat as directional) is a pipeline: intent classification → RAG over schemas and curated example queries → scoped generation → self-correction loop, running against a read-only role. This playbook builds that pipeline small enough to own.
Don't build this before the semantic groundwork exists (see the semantic-layer curation playbook); text-to-SQL over a raw 5,000-table swamp automates the confusion, not the answers.
Scope v1 to one domain workspace. Pick a single subject area (e.g., orders) with 10–30 curated tables. Domain "workspaces" narrow the retrieval search space — this is the single highest-leverage architectural decision in the pattern.
Index two corpora for retrieval. (a) Schema cards: one document per table — columns, types, descriptions, join keys, common filters. (b) Curated example queries: 30–100 known-good question→SQL pairs from your team's actual work, each tagged with the tables it touches. Embed both into pgvector.
Route intent, then select tables. A first LLM call classifies the question into a workspace and picks candidate tables from retrieved schema cards. Keeping this as a separate structured step makes wrong-table failures visible and fixable:
from pydantic import BaseModel
class TableSelection(BaseModel):
workspace: str
tables: list[str]
reasoning: str
Generate SQL with retrieved context. The generation prompt gets: the selected schema cards, the 3–5 most similar example queries, and house rules (dialect, date conventions, always-filter-soft-deletes):
import anthropic
class GeneratedSQL(BaseModel):
sql: str
assumptions: list[str]
client = anthropic.Anthropic()
response = client.messages.parse(
model="claude-opus-4-8",
max_tokens=2048,
thinking={"type": "adaptive"},
messages=[{
"role": "user",
"content": (
f"Write one {DIALECT} SELECT statement answering: {question}\n\n"
f"Schemas:\n{schema_cards}\n\nSimilar known-good examples:\n{examples}\n\n"
"Rules: SELECT only; use exact column names from the schemas; "
"if the question is ambiguous, state the assumption you made."
),
}],
output_format=GeneratedSQL,
)
candidate = response.parsed_output
Run the self-correction loop. Execute with EXPLAIN first (catches syntax and missing-column errors free), then run with a row limit. On error, feed the SQL plus the exact database error back for one repair attempt — cap at 2 rounds, then surface the failure honestly:
for attempt in range(2):
ok, err = try_explain(candidate.sql)
if ok:
break
candidate = repair(client, question, candidate.sql, err) # same parse() shape
Execute inside guardrails. The service's database role is read-only, statement-timeout-limited, row-limited (LIMIT 1000 appended if absent), cost-capped where the warehouse supports it, and blocked from sensitive schemas. Guardrails live in the database role, not in the prompt — prompts are requests, grants are law.
Return SQL + assumptions + results, and log everything. Show users the generated SQL and the model's stated assumptions alongside results — informed skepticism is part of the product. Log question, retrieved context, SQL, and outcome; accepted queries become new curated examples, which is the flywheel that improves accuracy without touching the model.
Retrieval quality and example curation set your accuracy ceiling; the model just climbs to it. When accuracy disappoints, fix the schema cards and add examples for the failing question shapes before touching prompts or models.
hr.salaries is a suggestion; a role grant is a control. Never rely on the former.Schema names, column comments, and example queries all leak business information, so on client engagements the retrieval indexes and logs must live inside the client's environment. Confirm in writing which LLM provider may see schema metadata and example queries (schemas are often approvable where row data is not — this pattern sends metadata, not data, to the model). Run generation against a client-owned read-only role with the guardrails from step 6, and route result sets only within client systems. If no external model is approved, the pipeline runs unchanged against a self-hosted coder model via vLLM — expect lower first-pass accuracy and lean harder on curated examples and the correction loop. Hand off the eval set, the curation process, and the accuracy dashboard so the client can keep the flywheel turning.