Dronk
All posts
Engineering

Keeping generated SQL safe by default

Letting an agent write queries against a production database sounds terrifying. Here's the layered way we make it boring instead.

RC

Ryan Chandler

3 min read

The first reaction people have to "an AI writes SQL and runs it against your database" is, reasonably, alarm. A stray DELETE or an accidental scan over a billion-row table is nobody's idea of a good time.

So safety isn't something we bolted on at the end. It's the default posture, and it doesn't depend on the model behaving itself. Here's how the layers stack up.

Layer one: parse it before you trust it

Every query the agent produces goes through a safety check before it runs. The checker first strips out comments, string literals, and quoted identifiers, so a value like where note = 'please drop everything' can't fool the keyword scan. Then it enforces two rules: there can only be one statement, and that statement has to start with SELECT, WITH, or EXPLAIN.

That alone blocks every top-level write. The one gap left is a write smuggled inside a CTE (Postgres lets you put INSERT or UPDATE inside a WITH), so we also reject a short list of write keywords anywhere in the query:

insert  update  delete  merge
create  alter   drop    grant   revoke   into

Anything that trips the checker never reaches your database. We record it as a rejected query instead, so we can see what the agent tried and why it was stopped.

Layer two: a read-only session

Parsing is good, but we don't want it to be the only thing standing between you and a bad day. So the connection Dronk uses to run a query is hardened at the session level before the query executes:

-- Postgres
SET default_transaction_read_only = on;
SET statement_timeout = 15000;          -- 15 seconds, in milliseconds

-- MySQL
SET SESSION transaction_read_only = 1;
SET SESSION max_execution_time = 15000;

Even if a write somehow slipped past the parser, the database itself would refuse it. And a query that runs too long gets cancelled by the database rather than tying up a connection.

Layer three: a read-only credential

The strongest version of all this is to never hand Dronk a writeable login in the first place. We recommend connecting with a role that can only read:

CREATE ROLE dronk_readonly LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE app TO dronk_readonly;
GRANT USAGE ON SCHEMA public TO dronk_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO dronk_readonly;

Now there are three independent things that all have to fail before a write could happen: the parser, the session settings, and the database grants. That's the point. Safety that depends on the model being right isn't safety. Safety that holds even when the model is wrong is the bar we build to.