Skip to content

SQL read/write classifier fails closed on valid reads (//, typed TIMESTAMPTZ literals, NOT GLOB, CREATE+SELECT) and reports a misleading "user rule" error #1488

Closed as not planned

Description

@anandgupta42

Problem

When the parser cannot read a statement, classifyAndCheck classifies it as a write. The analyst config denies writes, and the model sees only "The user has specified a rule which prevents you from using this specific tool call". Nothing says the statement was actually a parse failure on a valid read, so models rewrite blindly or switch to slower workarounds.

Mechanism

  • classifyAndCheck falls back to classifyFallback, which returns "write" when the parser throws (packages/opencode/src/altimate/tools/sql-classify.ts:131, :172-193). sql_execute then asks for sql_execute_write (sql-execute.ts:113-121), which the analyst agent denies (packages/opencode/src/agent/agent.ts:254).
  • isScratchSafeWrite requires every statement to be a write (sql-classify.ts:226-271), so CREATE TABLE ... AS SELECT ...; SELECT ... in one call is refused.
  • Parser gaps on pure reads (probed against the bundled parser): DuckDB integer division //; SQLite NOT GLOB; FILTER (...) variants; alias names at and match; PIVOT; typed literals such as TIMESTAMPTZ '2023-05-02 00:00:00+00'. The same queries with /, or with ::timestamptz casts, classify as reads.

Evidence

  • 17 of 17 queries containing // were denied; 4 containing GLOB.
  • 90 refusals of pure SELECT/WITH statements in one set of sessions: 87 contain a typed TIMESTAMPTZ '...' literal, 0 of 2,676 allowed queries on the same warehouses do (inferred from this split; not yet reproduced in a unit test).
  • 31 denied scratch CREATE ... AS SELECT batches with a trailing SELECT.
  • 28 of 32 denials in one set of sessions were avoidable, at about 3.4-3.8 s per lost turn (about 600 s total); 23 generations had every call refused.
  • The refusals pushed models onto a scratch-to-scratch sql_materialize path (45 calls, 728 s, including 4 helper crashes) and blocked a session-setting workaround for expression depth limits (see the many-table issue).

Impact

Time and cost; no direct answer loss (affected sessions recovered), but the risk of abandoning a correct approach is real.

Proposed fix

  • Report parser failures as their own message: "could not prove this is read-only: parser error at col N; rewrite or split the statement".
  • Teach the dialect lexer/parser //, GLOB, PIVOT, typed literals (TIMESTAMPTZ, TIMESTAMP, DATE, INTERVAL '...').
  • When the parse fails but the statement starts with SELECT/WITH and masked code has no write keyword, run on a read-only connection.
  • Accept scratch-safe writes followed by reads and return the last result set.

Acceptance check

Regression tests classifying each of the listed constructs as read, a CREATE ...; SELECT ... scratch batch accepted, and a snapshot of the new denial message for a genuinely unparseable statement.

Activity

  1. anandgupta42 commented on Nov 5, 2026

    @anandgupta42
    ContributorAuthor

    Consolidated into #1679, which tracks this together with related issues. The original report here stays linked from that tracker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions