"Ask your data a question in plain English" is a good demo and a genuinely useful feature. It is also the kind of feature where the interesting engineering is almost entirely about limiting what the model is allowed to do, not about how well it writes SQL — a model asked to answer questions over a reporting database will write a plausible query for almost anything you ask it, including things it should never be allowed to run.
Start from the database role, not the prompt
The first control is not a prompt instruction telling the model to only read data — instructions are a suggestion the model can be talked out of. The connection the assistant uses should be a database role that is physically incapable of writing: no INSERT, UPDATE, DELETE, or DDL grants, full stop. If a generated query somehow tries to write, the database rejects it before the question of whether the model "should have" ever comes up. Everything after this point is a second layer of defence, not the only one.
The schema the model sees should be smaller than the real one
Handing a model your full schema and letting it query anything is how a report on booking volume ends up joined against a table it had no business seeing. The assistant should be scoped to an explicit, curated set of tables and views — ideally read-only views that already shape the data into what a business question actually needs — rather than the live production schema. That scoping does two things: it shrinks what can go wrong, and it usually improves accuracy, because a smaller, well-named schema is easier for a model to reason about than a sprawling one with legacy columns nobody uses.
Parse the output, do not execute free text
The model's response should not go straight to the database as a string. It should be parsed into a fixed structure — the tables it touches, the filters, the aggregation — and validated against an allowlist before anything runs. A query that references a table outside the approved set, or that does not parse cleanly into the expected shape, gets rejected rather than attempted. This is the same principle as any other untrusted input: validate the shape before you act on it, regardless of how well-behaved it usually looks.
Answers come from retrieved rows, not from the model's memory
The point of running an actual query is that the answer is grounded in real data rather than the model's general knowledge, which means the final response has to be built from the query result, not from the model freely narrating what it thinks the number probably is. If the query returns nothing, or returns something ambiguous, the honest answer is "I couldn't find that" — not a plausible-sounding guess dressed up as a fact.
Log the generated query, not just the answer
Every question asked and every query generated in response should be logged, because that is the record that lets you find where the assistant is misunderstanding the schema or the business terms — a "customer" that means one thing in the sales table and another in the support table is a classic source of a confidently wrong answer that nobody would catch just by reading the final response.
Where the human review gate actually goes
Read-only questions over a reporting database are a reasonable place to let the answer go straight to the user, because the cost of a wrong answer is bounded — someone double-checks a number, at worst. The same pattern used for anything that writes, sends, or acts on someone's behalf needs a person in the loop before it executes, because the cost of being wrong is no longer "an inconvenient answer", it is an action that already happened.