Talk to your data does the obvious thing. An admin types a question in plain English, somewhere in the back office, and gets an answer drawn from their own company's data, with a chart where a chart helps.
That half is easy now. A capable model writes decent SQL against a schema it has been shown, and has done for a while.
The half worth writing about is the other one: what the query it writes is physically unable to reach. Because the moment you let a model compose SQL against a real database, the question stops being "can it answer" and becomes "what happens when it writes something nobody intended".
What it actually does
One typed question becomes one read-only SELECT, which runs in a sandbox, and the result is explained back in plain language. It cannot change anything. The database is attached read-only, so a write is refused by SQLite itself rather than by an instruction in a prompt.
That distinction is the whole design, and it is worth being precise about. A prompt that says "only write SELECT statements" is a request. A database attached in read-only mode returns "attempt to write a readonly database" no matter how the query was phrased, who phrased it, or what the model believed it was doing.

Why a list of forbidden words is the wrong instinct
The obvious way to keep an AI-written query in bounds is to scan the SQL before running it and reject anything containing the words you do not like. We had that. It is the approach almost everyone reaches for first, and it does not hold.
The reason is simple once you see it: a regular expression over SQL text is a guess about what a parser will do, and the parser is the one that decides. Over three days, three different quoting tricks walked straight around ours. Double quotes went first, then single quotes, and brackets and backticks would have been next, because SQLite accepts all of them and a text scan has to anticipate every one.
You cannot win that race by adding patterns. Each fix is a new guess, and the guesses are checked by whoever is trying hardest to get past them.
Build the fence, do not inspect for violations
What replaced it is a different shape of answer: rather than checking a query for forbidden things, construct a database in which the forbidden things do not exist. Three layers, and each does a job the others cannot.
- The company's database is attached read-only to an empty in-memory one. SQLite refuses every write itself. That is the write boundary, and no prompt, jailbreak or clever phrasing is upstream of it.
- Every table the analyst may use is exposed as a temporary view carrying only the allowed columns, and only the allowed rows where a table needs that. A denied column does not come back blank or null. Asking for it returns "no such column", because in this database there genuinely is not one. That is also what makes it safe to let the model write SELECT star.
- An SQLite authorizer sits inside the compiler and sees the resolved query, after every name has been matched to a real object. How a name was spelled or quoted is already behind it by then. Its rule is that the attached company schema may be read only through one of those views, never by naming a real table, and that the internal sqlite tables, the pragma table functions and PRAGMA itself are refused outright.
The list of banned words still exists, and it is no longer load-bearing
It was not deleted, which deserves an explanation, because keeping a defence you have just declared inadequate looks like indecision.
It stays for one job it is genuinely good at: turning an obvious write or PRAGMA into a plain sentence the model can read and correct, before anything runs. That is a better experience than a raw engine error, and it costs nothing.
What changed is what the safety depends on. The authorizer is the guarantee. The word list is manners. Any defence you keep should be clear about which of those two it is, because a team that cannot say which layer is load-bearing will eventually remove the wrong one in a refactor.
What the fence is actually protecting is not data, it is promises
The interesting part of the deny lists is not that they contain credentials. Of course they do: password reset tokens and one-time admin handoff tokens are on it, and a leak of either is unrecoverable.
The entries that matter more are the ones protecting commitments the product makes elsewhere. A private feedback note is reachable only through an audited moderation screen, which is a promise to the person who wrote it. A level-up expert's write-up is readable by the chair alone, and that is not squeamishness: it is what makes a panel of four produce four independent opinions rather than three agreements with whoever posted first.
Both promises are enforced carefully in the code that owns them. A chat able to select those columns would make both false in the one place nobody would think to check. That is the real risk a natural-language data feature introduces: not a new leak, but a back door into guarantees you already made and tested somewhere else.
So the question to ask of any feature like this is not "is the data secured". It is: which promises made elsewhere in this product does this new path quietly bypass.

Names are not stripped out of the answers, deliberately
There is a tempting safety feature we turned off for this one: redacting people's names from what the model says.
It is the wrong layer. The whole job of this agent is to answer questions like who has the most verified skills, and an answer with the names removed is not an answer. More importantly, filtering the output is protection applied after the model has already read the thing. If a column should not be readable, the fix is that it is not in the database the query runs against, not that we tidy up the sentence afterwards.
Redaction at the end is what you build when you do not trust the boundary at the start. Build the boundary instead.
The test that had to exist
A fence nobody tests is a fence until the first refactor. This one is checked two ways, and they catch different failures.
The structural half is driven by the deny lists themselves: every denied table and column must be absent from the sandbox and refused when asked for by name. Adding an entry tests it automatically, so the test cannot drift away from the thing it is testing.
The end-to-end half exists because of a real case. A structural check proves a column is hidden. It cannot prove the same text was not also copied into some other column that is visible. So a marker is planted in a protected write-up and then searched for across every column the analyst can see. That check was written after exactly that leak was found, and it runs the real pipeline rather than a reimplementation of it, because a test that rebuilds the thing it is testing proves only that you can write the bug twice.
What it still will not do for you
The fence decides what the query can reach. It does nothing about whether the answer is right.
A model can write a query that runs perfectly, returns a number, and answers a subtly different question from the one you asked. Joining the wrong way, counting rows where you meant people, silently dropping everyone with a null. The result arrives with a chart and a confident sentence attached, and nothing about it looks uncertain.
That is the same problem this blog keeps arriving at from different directions: the output has stopped indicating whether the work was done correctly. The honest framing for a feature like this is that it removes the delay between a question and a number, which is genuinely valuable, and leaves entirely intact the job of knowing whether the number means what you think. Anyone selling it as the end of needing to understand your own data is selling something else.
Which is why it is worth spot-checking early answers against a figure you already know. Not because the SQL is usually wrong, but because finding out how it is wrong when it is wrong is the only way to calibrate how much to trust the next one.




