DEV Community

Morgan Li
Morgan Li

Posted on

Parser Gates or Runtime Guards: A Debate for Agent-Written SQL

An illustrative lock queue

A generated UPDATE reached staging with a missing key predicate and a wide sequential scan. The statement waited behind an autovacuum worker, then blocked a checkout transaction for thirty-one seconds. Nobody had pasted the SQL into a parser gate, and the runtime role still held UPDATE on the full table. That class of outage is the fork this debate tries to resolve for SQL review agents.

This article treats the incident as an illustrative reconstruction, not as a measured postmortem from a named company. The technical question is narrow: should agent-written SQL be rejected by static gates, or contained by runtime guards? Both positions are credible, and both fail in documented ways when used as a single control. The useful output is a decision rule, plus a reproducible review workflow you can run without production credentials.

Why this debate is not a style argument

Agent-written SQL now appears in migrations, analytics extracts, backfills, and operator notebooks after a short prompt. Static review scales with pull requests, while runtime guards scale with live statements and actual query plans. Teams that over-index on parsers ship statements that look bounded and still lock a hot range of rows. Teams that over-index on timeouts discover the damage only after a queue of waiting sessions has already formed.

Recent public discussion about vibe coding versus engineering often stops at taste, tools, or model quality scores. For SQL, the failure mode is more specific: a statement can be valid, reviewed, and still enter a RowExclusiveLock wait queue. The debate below stays on controls you can test, rather than on whether models outrank human authors in general. If a control cannot be rehearsed with a fixture database, it does not belong in the decision rule.

Position A — Fail closed in CI with a parser gate

Advocates want a deterministic check before merge, independent of planner statistics and live cache state. The gate parses the statement, classifies verbs, and rejects unbounded writes, missing predicates, and heavy lock upgrades. Evidence is cheap to collect, because unit tests over SQL strings do not require a populated staging cluster. The limitation is structural, because parsers do not see cardinality, skew, or the join order the planner will pick.

A parser-first team usually encodes a small allowlist rather than a growing pile of regular expressions. SELECT, EXPLAIN, and EXPLAIN ANALYZE on replicas may pass; UPDATE, DELETE, TRUNCATE, and ALTER fail closed without extra metadata. DDL that takes AccessExclusiveLock fails unless a human-owned runbook identifier is present in the review packet. That design keeps the agent from inventing a migration during a question that was supposed to stay read-only.

Numbered workflow for a parser gate

  1. Parse the candidate SQL with a real engine parser, not a split on semicolons or a language-model guess.
  2. Reject multi-statement strings unless every statement independently passes the same verb and lock policy.
  3. Require a bounded predicate for UPDATE and DELETE, and reject empty predicates plus statements that omit WHERE entirely.
  4. Fail CI if the statement includes LOCK TABLE, DROP, TRUNCATE, or unflagged ALTER without a human-owned exception token.
  5. Store the parser verdict as an artifact next to the SQL, and forbid the agent from editing that artifact in the same turn.

The evidence for this side is operational simplicity: the check is fast, deterministic, and easy to replay inside pull requests. It also limits blast radius when an agent emits several statements and only the last one was meant for review. False confidence is the cost, because a bounded WHERE clause on an unindexed, high-churn column can still lock a wide heap. Parser gates are necessary for verb control; they are not sufficient for plan control.

Position B — Fail soft in the database with runtime guards

The second position argues that dangerous SQL is usually a plan problem, not a syntax problem, in field incidents. Database guards include statement_timeout, lock_timeout, idle_in_transaction_session_timeout, and a role stripped of exclusive DDL. Evidence comes from wait-event traces, where the failing statement was syntactically bounded and still blocked checkout sessions. The limitation is operational, because a timeout that fires after a lock is held still leaves a waiting queue behind it.

Runtime advocates point at statistics drift, parameter sniffing, and autovacuum overlap, which no merge-time string check can see. A role that can only write through a security-definer function, with row-level filters, contains mistakes the parser never names. statement_timeout aborts CPU-heavy scans; lock_timeout aborts waiters before they become an outage narrative for on-call. Those settings are real controls, but they are not a review process, and they do not explain intent to a future operator.

Numbered workflow for runtime guards

  1. Create a dedicated review role that cannot execute TRUNCATE, DROP, ALTER, VACUUM FULL, or LOCK TABLE in production.
  2. Set lock_timeout and statement_timeout on that role, not as a session afterthought inside agent-generated SQL.
  3. Force default_transaction_read_only for any agent whose prompt is diagnostic, including EXPLAIN-only review jobs.
  4. Route writes to a staging replica or a restored snapshot, and refuse agent connections that present production DSNs.
  5. Capture wait events around the statement, then treat a timeout as a failed review rather than a successful guard firing.

This side wins when the SQL is parameterized, the schema is stable, and the risk is a bad plan rather than a bad verb. It loses when the agent can still start a transaction, hold row locks, and wait until the timeout becomes a user-visible stall. Guards also fail open if the agent reconnects with a stronger role, or if a human copies the SQL into psql as a superuser. Runtime containment is a last fence, not a substitute for refusing dangerous verbs before they reach a shared database.

Evidence the two sides actually share

Both sides agree that model self-checks are a weak primary control, because the same model can rewrite the checklist. Both sides agree that production credentials do not belong in prompt context, regardless of how the review job is hosted. Both sides agree that EXPLAIN without execution is closer to a parser than to a guard, and should not be sold as a rehearsal. The disagreement is only about which control is allowed to be the merge blocker when the other control is incomplete.

A practical reading of wait-event catalogs favors runtime data for performance regressions, and parser data for destructive verbs. UPDATE without a key, DELETE without a predicate, and DDL on hot tables belong to the parser side with almost no exceptions. Skewed joins, stale statistics, and lock waits on correctly keyed updates belong to the runtime side, with tracing attached. Mixing those classes into one AI review score hides the only evidence that would tell an on-call engineer what to do next.

Decision rule

Use the following rule in order. Stop at the first match, and record the matched line in the review artifact.

  1. If the statement contains DDL, TRUNCATE, LOCK TABLE, or a missing write predicate, fail closed in CI; do not wait for a timeout.
  2. If the statement is a parameterized write with a key predicate, require runtime guards on a non-production snapshot before merge.
  3. If the statement is read-only EXPLAIN, allow a parser pass plus a captured plan from staging; do not grant write roles.
  4. If the agent requests to edit the review oracle, the allowlist, or the timeout values, reject the turn and page a human.
  5. If neither control can be rehearsed, do not ship the statement, even when a model assigns it a high confidence label.

The rule is intentionally boring. Boring rules survive model upgrades, prompt drift, and the next fashionable agent loop.

Artifact: a decision table and a reproducible harness

The table below is the review contract. Copy it into the repository beside the SQL under review.

Statement class Parser gate Runtime guard Merge blocker
SELECT / EXPLAIN Allow if single statement read-only role + statement_timeout Parser only
UPDATE/DELETE with key predicate Allow with bound WHERE lock_timeout + staging snapshot Runtime rehearsal
UPDATE/DELETE without key Reject Do not run Parser
DDL / TRUNCATE / LOCK Reject unless human flag Role without DDL Parser
Multi-statement bundle Reject unless each part passes No implicit transaction from the agent Parser

The harness below is a labeled, unexecuted example for PostgreSQL. Adapt names to your environment before running anything.

# proposal: ci_sql_parser_gate.py
# Unexecuted example. Requires pglast. Not a production security boundary.

from pglast import parse_sql
from pglast.ast import (
    UpdateStmt,
    DeleteStmt,
    TruncateStmt,
    AlterTableStmt,
    LockStmt,
    TransactionStmt,
)

FORBIDDEN = (TruncateStmt, AlterTableStmt, LockStmt)
WRITE = (UpdateStmt, DeleteStmt)


def statements(sql: str):
    return [r.stmt for r in parse_sql(sql)]


def has_where(stmt) -> bool:
    return getattr(stmt, "whereClause", None) is not None


def review(sql: str) -> str:
    stmts = statements(sql)
    if len(stmts) != 1:
        return "FAIL: multi-statement SQL is out of policy"
    stmt = stmts[0]
    if isinstance(stmt, TransactionStmt):
        return "FAIL: agent may not open or close transactions"
    if isinstance(stmt, FORBIDDEN):
        return "FAIL: destructive or locking DDL requires a human flag"
    if isinstance(stmt, WRITE) and not has_where(stmt):
        return "FAIL: write without a predicate"
    return "PASS: parser gate"


if __name__ == "__main__":
    samples = [
        "UPDATE orders SET status = 'closed';",
        "UPDATE orders SET status = 'closed' WHERE id = 42;",
        "EXPLAIN SELECT id FROM orders WHERE id = 42;",
        "TRUNCATE orders; SELECT 1;",
    ]
    for sql in samples:
        print(review(sql), "=>", sql)
Enter fullscreen mode Exit fullscreen mode
-- proposal: staging_review_role.sql
-- Unexecuted example. Apply only on a disposable staging snapshot.

CREATE ROLE sql_review_agent NOINHERIT LOGIN;
ALTER ROLE sql_review_agent SET statement_timeout = '5s';
ALTER ROLE sql_review_agent SET lock_timeout = '1s';
ALTER ROLE sql_review_agent SET idle_in_transaction_session_timeout = '3s';
ALTER ROLE sql_review_agent SET default_transaction_read_only = on;

GRANT CONNECT ON DATABASE staging_app TO sql_review_agent;
GRANT USAGE ON SCHEMA public TO sql_review_agent;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO sql_review_agent;
REVOKE UPDATE, INSERT, DELETE, TRUNCATE ON ALL TABLES IN SCHEMA public FROM sql_review_agent;
Enter fullscreen mode Exit fullscreen mode
# proposal: rehearsal.sh
# Unexecuted example. Points at staging, never at production DSNs.

export PGHOST=staging-snapshot.internal
export PGUSER=sql_review_agent
export PGDATABASE=staging_app
psql -v ON_ERROR_STOP=1 -c "SHOW lock_timeout;"
psql -v ON_ERROR_STOP=1 -c "EXPLAIN (FORMAT JSON) SELECT id FROM orders WHERE id = 42;"
Enter fullscreen mode Exit fullscreen mode

Run the parser tests in CI on every agent-authored SQL file. Run the role rehearsal only against a snapshot that can be thrown away. Keep production DSNs out of the job environment so a prompt leak cannot become a connection string. Treat a timeout during rehearsal as a failed review, not as proof that the guardrail is working as designed.

Where a free review runtime fits

Some teams still want a model to classify ambiguous SQL against a frozen rubric, especially when the parser output is a syntax tree rather than a policy decision. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option can host that classification step so the rubric and the SQL text stay off production hosts.

The model does not replace the parser gate, and it does not tune lock_timeout for you. Feed it the parser verdict, the statement class from the table, and a rubric that lives in git, then store the model's label as advisory metadata. If the model disagrees with a FAIL from the parser, the parser still wins. That is the entire integration: one advisory pass, no extra privileges, and no production row samples in the prompt.

Limitations

Parser coverage depends on the SQL dialect and on whether the agent emits vendor extensions the parser does not understand. pglast does not make MySQL or SQL Server statements safe, and a failed parse must fail closed rather than skip the gate. Runtime timeouts do not roll back work that already happened before the wait, and they do not repair bad data written inside the limit. Neither control detects semantic errors such as updating the wrong tenant key that still looks like a well-bounded predicate.

Free review runtimes are not an isolation story by themselves, and they should not receive production dumps, secrets, or customer row samples. Do not treat model classification as a measured accuracy benchmark; this article does not claim a quota, a hardware profile, or a latency number. Do not let the agent modify timeout settings, role grants, or the decision table in the same change that contains the SQL. If staging statistics are not a plausible shadow of production, the runtime rehearsal will certify the wrong plan.

Who should not use this approach

Do not use parser-only gates if your agents routinely emit dynamic SQL through stored procedures the parser never sees. Do not use runtime-only guards if agents can connect with a role that still owns DDL, or if staging is a shared writable database. Do not send regulated data to any hosted review path, including a free server, when the legal control is that no data leaves the VPC. Do not adopt the workflow as a substitute for a human runbook on irreversible migrations, even when both gates are green.

Closing

Parser gates and runtime guards answer different questions, and collapsing them into a single agent score recreates the lock queue. Put verb safety in CI, put plan safety on a disposable snapshot, and keep the review oracle outside the agent's write path. If you already have a free model runtime for advisory classification, copy the decision table first and leave the model in a comment-only role.

Top comments (1)

Collapse
 
jo-do profile image
Jo Do

The strongest boundary here is making the evidence artifact uneditable by the agent that produced the SQL. I would extend that to the execution verdict too: keep parser output, the exact schema revision, EXPLAIN JSON, role settings, and timeout result in one immutable review packet. Otherwise a later retry can pass against different statistics or a stronger role while looking like the same reviewed statement. Syntax gates and runtime guards only compose safely when they are tied to the same candidate and environment.