Query safety: how to stop an LLM from destroying your database
An LLM writing queries against your database is a liability unless you build defence in depth — read-only credentials, static validation, execution limits, and tenant filters the model never touches.
Give a language model the ability to query your database and, sooner or later, it will generate a DROP, an unbounded table scan, or a query that returns another customer's data. Not because it's malicious — because it's probabilistic. So you don't trust it; you build defence in depth, where no single failure is catastrophic.
Defence in depth
- Read-only credentials — the connection the app uses is physically incapable of writing, dropping, or altering. This is the load-bearing layer: even a perfect injection can't destroy what it can't write to. Everything else is backup.
- Static validation — parse the generated query and reject anything that isn't a single read statement: no DDL, no DML, no multiple statements, no stacked queries or comment tricks.
- Execution limits — enforce row limits, statement timeouts, and query-cost caps, so a runaway scan degrades gracefully instead of taking the database down.
Tenant isolation must never be LLM-generated
This is the one that turns a bug into a breach. The filter that scopes results to the current tenant or user must be injected by your code — never written by the model. An LLM-generated 'WHERE tenant_id = ...' is one hallucination away from serving customer A's data to customer B. Enforce it outside the model: row-level security in the database, or a mandatory server-side filter the query is wrapped in.
Never let the model write the boundary that separates one customer's data from another. That filter belongs in your code, enforced by the database — not in a prompt.