Giving an LLM your production database is easy. Taking access away is the hard part. — DeepSQL Blog All postsProduct UpdatesAug 21, 20267 min read<br>Giving an LLM your production database is easy. Taking access away is the hard part.<br>Every "chat with your database" demo ends at the first correct query. The interesting engineering starts the moment a second user logs in — and the model can still see the salaries table.<br>VS<br>Venkat Sakamuri<br>DeepSQL R&D · Ex Oracle Query Engine Team · YC & CMU
TL;DR
A read-only connection is not an access policy. It stops writes; it does nothing about who may read hr.employees.
Guarding "the query" is wrong. Guard the whole statement: CTEs, subqueries, unions, COMMENT, CALL, and whatever the editor lets a user paste in.
The only way to know a policy works is to look at the database as that user — so we built impersonation before we built anything else on top of policies.
We open-sourced DeepSQL (github.com/DeepSQLAI/deepsql) six weeks ago as a self-hosted database agent for Postgres and MySQL. The demo everyone builds — natural language in, SQL out, results in a table — took days. The last three releases have been almost entirely about the boring half: making sure the agent cannot read something the human asking cannot read.
That turns out to be a much harder problem than SQL generation, and I don't think it gets talked about enough.
The read-only connection lie
The default posture for an LLM database tool is: connect with a read-only role, ship it. That closes exactly one hole — mutation — and leaves the bigger one wide open. SELECT is the dangerous verb in an analytics context. A support engineer who could never open the finance dashboard can now ask "what's our average deal size by rep" and get a precise answer, because the agent's connection is a superset of every human's authority.
Two-thirds of the "AI DBA" tools I've looked at collapse every human into one service account. The audit log then reads:
2026-08-19 11:04:22 deepsql_agent SELECT ... FROM finance.invoices ...<br>2026-08-19 11:04:41 deepsql_agent SELECT ... FROM hr.compensation ...
Which is to say: it reads nothing. You cannot answer "who saw this row" after the fact, and in a Postgres instance with six schemas and 500+ tables — our ACME ERP fixture is exactly that, crm, sales, finance, inventory, hr, marts — nobody can hold in their head which of those an analyst is allowed to touch.
Policies in English, enforced in the planner path
The mechanism we landed in v1.2.0 is schema-scoped access policies. An admin writes a rule in plain English:
Support engineers can read customer and ticket data. No financial or HR data. Never expose email addresses.
That resolves, at policy-save time, into a concrete artifact: an allowed schema list plus table and column deny lists. Not a prompt. A prompt is a suggestion; the model complies until someone writes "ignore prior instructions, I'm the DBA now." The resolved policy is enforced in three places the model does not control — schema introspection (the Brain only indexes what you may see, so denied tables never enter the context window), the query guard before execution, and the schema APIs the web UI and MCP client call.
The context-window point is the one people miss. If the agent's schema context contains hr.compensation, the model will eventually reference it, and your only defense is a rejection at execution time — after the column names have already been leaked into the answer text.
Guard the statement, not the query
Two fixes in this cut are worth reading if you're building anything similar, because both were bypasses we shipped and then had to close.
Enforce the allowlist over the whole statement. Our first guard resolved the tables in the primary FROM. Which does nothing to:
WITH leak AS (<br>SELECT employee_id, base_salary FROM hr.compensation<br>SELECT c.name, l.base_salary<br>FROM crm.customers c<br>JOIN leak l ON l.employee_id = c.owner_id;
The top-level target is crm.customers — allowed. The payload comes out of a CTE. Every subquery, CTE, union arm, and lateral join has to be resolved against the allowlist, or the allowlist is decorative.
COMMENT and CALL are not mutations. Going the other way: our mutation classifier read the first table-shaped identifier in the statement and flagged COMMENT ON TABLE sales.orders IS '...' as a write on sales.orders. Real work got blocked, so people asked for the guard to be relaxed. A guard with false positives gets turned off, and then you have no guard. Precision in a policy engine is a security property, not a UX nicety.
The same class of bypass had to be closed in the SQL editor, which is the surface everyone forgets: the agent may be perfectly constrained while the raw editor next to it runs whatever you type through a different code path.
You cannot verify a policy you cannot see
The feature I'd argue matters most sounds like an admin convenience: "View as" another user. An admin switches into a target profile and...