Skip to content

Parse UPDATE/DELETE ORDER BY and LIMIT behind an option - #5

Merged
kyleconroy merged 2 commits into
mainfrom
claude/sqlite-update-delete-regression-ppqlcv
Jul 31, 2026
Merged

Parse UPDATE/DELETE ORDER BY and LIMIT behind an option#5
kyleconroy merged 2 commits into
mainfrom
claude/sqlite-update-delete-regression-ppqlcv

Conversation

@kyleconroy

Copy link
Copy Markdown
Contributor

SQLITE_ENABLE_UPDATE_DELETE_LIMIT selects two extra grammar rules, giving UPDATE and DELETE the orderby_opt limit_opt tail that SELECT has. The pinned build defines neither it nor SQLITE_UDL_CAPABLE_PARSER, so meyer rejected those clauses outright — correct for that build, and wrong for anyone pointing sqlc at a database compiled with the option, whose valid SQL meyer could not read.

The shape

parser.Options carries the fork. Its zero value is the pinned build, so the corpus still defines the default behaviour (wherelimit.test expects the rejection, byte offset included), and the same entry points hang off Options as methods for a caller that needs the other side:

stmts, err := parser.Options{UpdateDeleteLimit: true}.ParseString("DELETE FROM t ORDER BY x LIMIT 1")
  • parser/parser.goOptions, with Parse, ParseString and ParseStatement as methods; the package-level functions delegate to Options{}. ParseExpr has no Options form, since no option reaches an expression.
  • parser/parse_dml.goparseUpdateDeleteLimit implements the tail. With the option off it consumes nothing, so ORDER/LIMIT is a token no rule can shift and the caller's end-of-statement check reports it, which is how the default message and offset stay byte-identical.
  • ast/dml.go, ast/render.goOrderBy/Limit on UpdateStmt and DeleteStmt, in Children() and the renderer.
  • internal/roundtripCheckWith, so a tree parsed with an option is re-parsed with it.
  • cmd/debug-parse-update-delete-limit.

Only top-level statements grow the clauses: trigger_cmd has no orderby_opt or limit_opt to gate, in either build.

Verification

The expectations in parser/options_test.go come from a SQLite built with the option, which takes a Lemon run over src/parse.y rather than a -D on the amalgamation — the released amalgamation ships a parse.c generated without the rules, so defining the macro against it changes nothing (confirmed: no call to updateDeleteLimitError survives in it). The recipe is at the top of that file: build tool/lemon.c, regenerate parse.c with the define, splice it into sqlite3.c over the region marked Begin file parse.c, compile with the define. Lemon assigns the same token numbers either way — the generated parse.h is byte-identical — so the splice is sound.

Against that build:

  • every accepted case reaches SQLite's semantic layer (no such table: t1), i.e. it parsed;
  • the rejections that stand in both builds are recorded with the UDL build's message and offset — OFFSET without its LIMIT, RETURNING after a LIMIT (where_opt_ret precedes orderby_opt), and both trigger-body forms;
  • cmd/difftest, pointed at that build with the option on, agreed with meyer on 228,652 mutations across the whole corpus, 0 disagreements (that run needed a local patch to difftest, which is not part of this branch).

go test ./..., gofmt and go vet are clean; the corpus is untouched.

Note for follow-up

SQLITE_ENABLE_ORDERED_SET_AGGREGATES is the only other gate in parse.y that adds grammar (f(x) WITHIN GROUP (ORDER BY y)), so it is the only other way meyer can reject SQL a real build accepts. WITHIN joins the %fallback ID set under the same gate, so it would need no keyword-table change, and percentile.test already carries the "off" side. SQLite's docs call it off by default and omitted from most builds, so it is not urgent. The SQLITE_OMIT_* family only ever subtracts, and is not worth an option each.


Generated by Claude Code

claude added 2 commits July 31, 2026 03:59
SQLITE_ENABLE_UPDATE_DELETE_LIMIT selects two extra grammar rules, giving
UPDATE and DELETE the orderby_opt limit_opt tail that SELECT has. The
pinned build defines neither it nor SQLITE_UDL_CAPABLE_PARSER, so meyer
rejected those clauses outright -- correct for that build, and wrong for
anyone pointing sqlc at a database compiled with the option, whose valid
SQL meyer could not read.

parser.Options carries the fork. Its zero value is the pinned build, so
the corpus still defines the default behaviour (wherelimit.test expects
the rejection, byte offset included), and the same entry points hang off
Options as methods for a caller that needs the other side. Only top-level
statements grow the clauses: trigger_cmd has no orderby_opt or limit_opt
to gate, in either build.

The expectations in parser/options_test.go come from a SQLite built with
the option, which takes a Lemon run over src/parse.y rather than a -D on
the amalgamation -- the released amalgamation ships a parse.c generated
without the rules, so defining the macro against it changes nothing. The
recipe is at the top of that file. cmd/difftest, pointed at that build
with the option on, agreed with meyer on 228,652 mutations of the whole
corpus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kZMPnsTWywB5icVh1wJrA
The doc comment named a specific vendor's build as shipping with
SQLITE_ENABLE_UPDATE_DELETE_LIMIT. I could not verify it, so it goes; the
part that carries the argument -- the option rewrites the parser tables,
so it lives in a build meyer cannot detect from the SQL -- stands on its
own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kZMPnsTWywB5icVh1wJrA
@kyleconroy
kyleconroy merged commit ac0ecd3 into main Jul 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants