From f887bbe404851a333d1042121298f71b24d576b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 03:59:47 +0000 Subject: [PATCH 1/2] Parse UPDATE/DELETE ORDER BY and LIMIT behind an option 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 Claude-Session: https://claude.ai/code/session_019kZMPnsTWywB5icVh1wJrA --- CLAUDE.md | 6 + PLAN.md | 10 ++ README.md | 12 ++ ast/dml.go | 25 +++- ast/render.go | 15 +++ cmd/debug-parse/main.go | 5 +- internal/roundtrip/roundtrip.go | 9 +- parser/options_test.go | 199 ++++++++++++++++++++++++++++++++ parser/parse_dml.go | 34 ++++++ parser/parser.go | 61 ++++++++-- 10 files changed, 359 insertions(+), 17 deletions(-) create mode 100644 parser/options_test.go diff --git a/CLAUDE.md b/CLAUDE.md index 4426469..ff1660a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,6 +23,12 @@ https://sqlite.org/lang.html. side: no nonterminal of the vendored grammar may go unmentioned. - Error messages must match SQLite's parser byte-for-byte (`near "X": syntax error`, `unrecognized token: "X"`, `incomplete input`). +- Grammar that a build option turns on — `SQLITE_ENABLE_UPDATE_DELETE_LIMIT` + so far — is a `parser.Options` field, off by default: the corpus is + generated from the pinned build, so the default has to stay exactly where + that build is. Expectations for an option come from a SQLite compiled with + it, which takes a Lemon run rather than a `-D` on the amalgamation; the + recipe is at the top of `parser/options_test.go`. ## The corpus diff --git a/PLAN.md b/PLAN.md index ffa6eea..1e4fd1a 100644 --- a/PLAN.md +++ b/PLAN.md @@ -212,6 +212,16 @@ them — meyer implements exactly what that build accepts, including CONSTRAINT` forms if (and only if) the pinned build has them. When the pin advances, `cmd/regenerate` re-derives expectations and diffs are reviewed. +A gate that real deployments turn on is a second matter, because the pinned +build is not the only SQLite sqlc is ever pointed at. Those get a field on +`parser.Options`, defaulting to the pinned build's answer so conformance is +untouched, and are opt-in per parse: +`SQLITE_ENABLE_UPDATE_DELETE_LIMIT` (`Options.UpdateDeleteLimit`) is the +first — ORDER BY and LIMIT on UPDATE and DELETE. Each one has to be a fork +in SQLite's own grammar, never a dialect meyer invents, and has to be +verified the same way everything else is: against a build with the option +on. + ### 4. Positions are load-bearing (sqlc integration) teesql has no positions at all; doubleclick's `End()` is vestigial. meyer diff --git a/README.md b/README.md index 525e5ce..61decd8 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,18 @@ _, err := parser.ParseString("SELECT FROM t") fmt.Println(err) // 1:8: near "FROM": syntax error ``` +Those entry points parse what the pinned SQLite build parses. Where SQLite's +own grammar forks on a build option, `parser.Options` has the same entry +points as methods, so a caller aimed at a database built differently can +parse the SQL that database accepts: + +```go +// ORDER BY and LIMIT on UPDATE and DELETE, as SQLITE_ENABLE_UPDATE_DELETE_LIMIT +// compiles in. Without it -- the default, and the pinned build -- they are a +// syntax error, which is what SQLite reports too. +stmts, err := parser.Options{UpdateDeleteLimit: true}.ParseString("DELETE FROM t ORDER BY x LIMIT 1") +``` + Every node embeds `ast.Span`, so `Pos()` and `End()` give byte offsets into the original input — sqlc slices the source with them to find `-- name:` comments and to report errors, so they are load-bearing rather than diff --git a/ast/dml.go b/ast/dml.go index a87b67b..f493f40 100644 --- a/ast/dml.go +++ b/ast/dml.go @@ -90,10 +90,13 @@ func (n *SetPair) Children() []Node { // // cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from // where_opt_ret. +// cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from +// where_opt_ret orderby_opt limit_opt. // -// The pinned build defines neither SQLITE_ENABLE_UPDATE_DELETE_LIMIT nor -// SQLITE_UDL_CAPABLE_PARSER, so UPDATE has no ORDER BY or LIMIT at all: they -// are a plain syntax error rather than a grammar-action message. +// The second form is the one SQLITE_ENABLE_UPDATE_DELETE_LIMIT compiles in. +// The pinned build defines neither it nor SQLITE_UDL_CAPABLE_PARSER, so +// OrderBy and Limit are only ever set for a parse that asked for them with +// parser.Options.UpdateDeleteLimit; without it they are a syntax error. type UpdateStmt struct { Span With *With `json:"with,omitempty"` @@ -106,6 +109,8 @@ type UpdateStmt struct { From []*TableRef `json:"from,omitempty"` Where Expr `json:"where,omitempty"` Returning []*ResultColumn `json:"returning,omitempty"` + OrderBy []*OrderingTerm `json:"orderBy,omitempty"` + Limit *Limit `json:"limit,omitempty"` } func (*UpdateStmt) stmtNode() {} @@ -115,14 +120,19 @@ func (n *UpdateStmt) Children() []Node { out = appendNodes(out, n.From) out = append(out, nodes(n.Where)...) out = appendNodes(out, n.Returning) - return out + out = appendNodes(out, n.OrderBy) + return append(out, nodes(n.Limit)...) } // DeleteStmt is a DELETE statement. // // cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret. +// cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret orderby_opt +// limit_opt. // -// As for UPDATE, the pinned build's DELETE takes no ORDER BY or LIMIT. +// As for UPDATE, the second form is the one +// SQLITE_ENABLE_UPDATE_DELETE_LIMIT compiles in, and OrderBy and Limit are +// set only for a parse made with parser.Options.UpdateDeleteLimit. type DeleteStmt struct { Span With *With `json:"with,omitempty"` @@ -132,11 +142,14 @@ type DeleteStmt struct { NotIndexed bool `json:"notIndexed,omitempty"` Where Expr `json:"where,omitempty"` Returning []*ResultColumn `json:"returning,omitempty"` + OrderBy []*OrderingTerm `json:"orderBy,omitempty"` + Limit *Limit `json:"limit,omitempty"` } func (*DeleteStmt) stmtNode() {} func (n *DeleteStmt) Children() []Node { out := nodes(n.With, n.Table, n.Alias, n.IndexedBy, n.Where) out = appendNodes(out, n.Returning) - return out + out = appendNodes(out, n.OrderBy) + return append(out, nodes(n.Limit)...) } diff --git a/ast/render.go b/ast/render.go index 7de17ed..6d1e19a 100644 --- a/ast/render.go +++ b/ast/render.go @@ -945,6 +945,7 @@ func writeUpdate(b *strings.Builder, t *UpdateStmt) { write(b, t.Where) } writeReturning(b, t.Returning) + writeUpdateDeleteLimit(b, t.OrderBy, t.Limit) } func writeDelete(b *strings.Builder, t *DeleteStmt) { @@ -964,6 +965,20 @@ func writeDelete(b *strings.Builder, t *DeleteStmt) { write(b, t.Where) } writeReturning(b, t.Returning) + writeUpdateDeleteLimit(b, t.OrderBy, t.Limit) +} + +// writeUpdateDeleteLimit renders the ORDER BY and LIMIT an UPDATE or DELETE +// carries only when it was parsed with parser.Options.UpdateDeleteLimit. The +// rendering is re-parseable under the same option, and under no other. +func writeUpdateDeleteLimit(b *strings.Builder, order []*OrderingTerm, limit *Limit) { + if len(order) > 0 { + w{b}.kw("ORDER BY") + writeOrdering(b, order) + } + if limit != nil { + write(b, limit) + } } func writeIndexedBy(b *strings.Builder, by *Ident, notIndexed bool) { diff --git a/cmd/debug-parse/main.go b/cmd/debug-parse/main.go index 3d23593..ee67799 100644 --- a/cmd/debug-parse/main.go +++ b/cmd/debug-parse/main.go @@ -10,6 +10,7 @@ // go run ./cmd/debug-parse 'SELECT 1' // go run ./cmd/debug-parse -tokens 'SELECT 1' // go run ./cmd/debug-parse -f query.sql +// go run ./cmd/debug-parse -update-delete-limit 'DELETE FROM t LIMIT 1' // echo 'SELECT 1' | go run ./cmd/debug-parse // // A parse error is printed with a caret under the offending byte, in the @@ -36,6 +37,8 @@ func main() { tokens = flag.Bool("tokens", false, "print the token stream instead of the tree") render = flag.Bool("render", false, "print the tree rendered back to SQL") positions = flag.Bool("pos", true, "include byte spans in the tree") + udl = flag.Bool("update-delete-limit", false, + "accept ORDER BY and LIMIT on UPDATE and DELETE, as SQLITE_ENABLE_UPDATE_DELETE_LIMIT does") ) flag.Parse() @@ -51,7 +54,7 @@ func main() { return } - stmts, err := parser.ParseString(src) + stmts, err := parser.Options{UpdateDeleteLimit: *udl}.ParseString(src) if err != nil { var pe *parser.Error if errors.As(err, &pe) { diff --git a/internal/roundtrip/roundtrip.go b/internal/roundtrip/roundtrip.go index 86fb564..018ec79 100644 --- a/internal/roundtrip/roundtrip.go +++ b/internal/roundtrip/roundtrip.go @@ -32,9 +32,14 @@ type Result struct { // Equality is structural: byte spans and the Raw fields differ after a round // trip by construction, and the renderer promises nothing about them, so // dump.Structure leaves both out. -func Check(stmts []ast.Stmt) Result { +func Check(stmts []ast.Stmt) Result { return CheckWith(parser.Options{}, stmts) } + +// CheckWith is Check for a tree that was parsed with opts. The rendering is +// re-parsed with the same options, because a clause an option unlocked -- +// an UPDATE's LIMIT, say -- can only be read back with that option on. +func CheckWith(opts parser.Options, stmts []ast.Stmt) Result { rendered := ast.Statements(stmts) - again, err := parser.ParseString(rendered) + again, err := opts.ParseString(rendered) if err != nil { return Result{Rendered: rendered, Reason: "rendered SQL does not parse: " + err.Error()} } diff --git a/parser/options_test.go b/parser/options_test.go new file mode 100644 index 0000000..49d0001 --- /dev/null +++ b/parser/options_test.go @@ -0,0 +1,199 @@ +package parser_test + +import ( + "errors" + "strings" + "testing" + + "github.com/sqlc-dev/meyer/ast" + "github.com/sqlc-dev/meyer/internal/roundtrip" + "github.com/sqlc-dev/meyer/parser" +) + +// The expectations in this file are the oracle's, like every other error +// expectation in the package, but they need an oracle the corpus tooling +// does not build: SQLITE_ENABLE_UPDATE_DELETE_LIMIT selects grammar rules, +// so defining it while compiling the released amalgamation changes nothing +// -- the amalgamation ships a parse.c that Lemon already generated without +// them. The build that produced these lines was, from the pinned release's +// two artifacts: +// +// cc -o lemon sqlite-src-/tool/lemon.c +// ./lemon -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT -S src/parse.y +// +// with the resulting parse.c spliced into sqlite3.c over the region the +// amalgamation marks "Begin file parse.c", and the whole compiled with +// -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT. Lemon assigns the same token numbers +// either way -- the generated parse.h is byte-identical -- so the splice is +// sound. cmd/difftest run against that build, with the option on, agreed +// with meyer on 228,652 mutations of the whole corpus. + +// udlAccepted is SQL that a build with SQLITE_ENABLE_UPDATE_DELETE_LIMIT +// parses and the pinned build rejects. Every case reaches SQLite's semantic +// layer there ("no such table: t1"), which is how the oracle reports a +// statement that parsed. +var udlAccepted = []string{ + `DELETE FROM t1 ORDER BY x`, + `DELETE FROM t1 WHERE x=1 ORDER BY x`, + `DELETE FROM t1 WHERE x>0 LIMIT 5`, + `DELETE FROM t1 WHERE x>0 ORDER BY x LIMIT 5 OFFSET 2`, + `DELETE FROM t1 LIMIT 2, 3`, + `DELETE FROM t1 INDEXED BY i1 WHERE x=1 LIMIT 1`, + `DELETE FROM t1 AS a WHERE a.x=1 ORDER BY a.x LIMIT 1`, + `DELETE FROM t1 ORDER BY x COLLATE nocase DESC NULLS LAST LIMIT 1`, + `WITH c(x) AS (SELECT 1) DELETE FROM t1 WHERE x IN (SELECT x FROM c) LIMIT 1`, + `UPDATE t1 SET y=1 LIMIT 5`, + `UPDATE t1 SET y=1 WHERE x=1 ORDER BY x`, + `UPDATE OR REPLACE t1 SET y=1 WHERE x=1 ORDER BY x DESC LIMIT 5 OFFSET 2`, + `UPDATE t1 SET y=1 FROM t2 WHERE t1.x=t2.x LIMIT 1`, + // where_opt_ret comes before orderby_opt, so RETURNING precedes LIMIT. + `UPDATE t1 SET y=1 WHERE x=1 RETURNING x, y, '|' LIMIT 5`, + `UPDATE t1 SET (a,b)=(SELECT 1,2) WHERE x=1 ORDER BY x LIMIT 1`, +} + +// udlRejected is SQL both builds reject, with the message and offset the +// UDL build reports. The clauses are only ever the tail of a top-level +// UPDATE or DELETE: OFFSET needs its LIMIT, RETURNING belongs to +// where_opt_ret and so cannot follow one, and trigger_cmd never grew either +// clause, whatever the build. +var udlRejected = []struct { + sql string + msg string + offset int +}{ + {`DELETE FROM t1 WHERE x=1 OFFSET 2`, `near "OFFSET": syntax error`, 25}, + {`UPDATE t1 SET y=1 WHERE x=1 OFFSET 2`, `near "OFFSET": syntax error`, 28}, + {`DELETE FROM t1 LIMIT 1, 2 OFFSET 3`, `near "OFFSET": syntax error`, 26}, + {`UPDATE t1 SET y=1 LIMIT 5 RETURNING x`, `near "RETURNING": syntax error`, 26}, + {`DELETE FROM t1 LIMIT 5 RETURNING x`, `near "RETURNING": syntax error`, 23}, + { + `CREATE TRIGGER r AFTER INSERT ON t1 BEGIN DELETE FROM t1 LIMIT 1; END`, + `near "LIMIT": syntax error`, 57, + }, + { + `CREATE TRIGGER r AFTER INSERT ON t1 BEGIN UPDATE t1 SET y=1 ORDER BY x LIMIT 1; END`, + `near "ORDER": syntax error`, 60, + }, +} + +// udlOff is what the pinned build makes of the same clauses: the statement +// ended at the token before, so ORDER or LIMIT is a token no rule can +// shift. These offsets are the corpus's own, from wherelimit.test. +var udlOff = []struct { + sql string + msg string + offset int +}{ + {`DELETE FROM t1 ORDER BY x`, `near "ORDER": syntax error`, 15}, + {`DELETE FROM t1 WHERE x=1 ORDER BY x`, `near "ORDER": syntax error`, 25}, + {`DELETE FROM t1 WHERE x>0 LIMIT 5`, `near "LIMIT": syntax error`, 25}, + {`UPDATE t1 SET y=1 WHERE x=1 ORDER BY x`, `near "ORDER": syntax error`, 28}, + {`UPDATE t1 SET y=1 WHERE x=1 RETURNING x, y, '|' LIMIT 5`, `near "LIMIT": syntax error`, 48}, + {`WITH c(x) AS (SELECT 1) DELETE FROM t1 WHERE x IN (SELECT x FROM c) LIMIT 1`, `near "LIMIT": syntax error`, 68}, +} + +var udl = parser.Options{UpdateDeleteLimit: true} + +// TestUpdateDeleteLimit checks that the option accepts the clauses SQLite +// accepts with SQLITE_ENABLE_UPDATE_DELETE_LIMIT, that the tree survives a +// round trip through the renderer, and that the same SQL is still a syntax +// error without the option -- the corpus is generated from a build without +// it, so the default has to stay where it is. +func TestUpdateDeleteLimit(t *testing.T) { + for _, sql := range udlAccepted { + t.Run(sql, func(t *testing.T) { + stmts, err := udl.ParseString(sql) + if err != nil { + t.Fatalf("expected the input to parse with the option, got: %v", err) + } + if r := roundtrip.CheckWith(udl, stmts); !r.Ok { + t.Errorf("round trip: %s\nrendered: %s", r.Reason, r.Rendered) + } + if _, err := parser.ParseString(sql); err == nil { + t.Error("the default options accepted an UPDATE/DELETE LIMIT") + } + }) + } +} + +func TestUpdateDeleteLimitErrors(t *testing.T) { + for _, tt := range udlRejected { + t.Run(tt.sql, func(t *testing.T) { + checkError(t, udl, tt.sql, tt.msg, tt.offset) + }) + } + for _, tt := range udlOff { + t.Run(tt.sql, func(t *testing.T) { + checkError(t, parser.Options{}, tt.sql, tt.msg, tt.offset) + }) + } +} + +func checkError(t *testing.T, opts parser.Options, sql, msg string, offset int) { + t.Helper() + _, err := opts.ParseString(sql) + var pe *parser.Error + if !errors.As(err, &pe) { + t.Fatalf("expected %q, got %v", msg, err) + } + if pe.Message != msg { + t.Errorf("message:\n got: %s\n want: %s", pe.Message, msg) + } + if pe.Offset != offset { + t.Errorf("offset: got %d, want %d", pe.Offset, offset) + } +} + +// TestUpdateDeleteLimitTree checks that the clauses land on the statement +// rather than being consumed and dropped, which accept/reject cannot see. +func TestUpdateDeleteLimitTree(t *testing.T) { + stmt, err := udl.ParseStatement(`DELETE FROM t1 WHERE x>0 ORDER BY y DESC LIMIT 5 OFFSET 2`) + if err != nil { + t.Fatalf("parse: %v", err) + } + del, ok := stmt.(*ast.DeleteStmt) + if !ok { + t.Fatalf("parsed to %T, want *ast.DeleteStmt", stmt) + } + if len(del.OrderBy) != 1 || del.OrderBy[0].Order != ast.SortDesc { + t.Errorf("ORDER BY is %+v, want one descending term", del.OrderBy) + } + if del.Limit == nil || del.Limit.Count == nil || del.Limit.Offset == nil { + t.Fatalf("LIMIT is %+v, want a count and an offset", del.Limit) + } + if got, want := ast.String(del), `DELETE FROM t1 WHERE x > 0 ORDER BY y DESC LIMIT 5 OFFSET 2`; got != want { + t.Errorf("rendered\n got: %s\n want: %s", got, want) + } + + // "LIMIT x, y" swaps the operands, as it does in a SELECT, and the + // renderer has to put them back the way they were written. + stmt, err = udl.ParseStatement(`UPDATE t1 SET y=1 LIMIT 2, 3`) + if err != nil { + t.Fatalf("parse: %v", err) + } + up := stmt.(*ast.UpdateStmt) + if up.Limit == nil || !up.Limit.Comma { + t.Fatalf("LIMIT is %+v, want the comma spelling", up.Limit) + } + if got, want := ast.String(up), `UPDATE t1 SET y = 1 LIMIT 2, 3`; got != want { + t.Errorf("rendered\n got: %s\n want: %s", got, want) + } +} + +// TestOptionsEntryPoints checks that the option reaches every entry point, +// and that the package-level ones are unaffected by it. +func TestOptionsEntryPoints(t *testing.T) { + const sql = `DELETE FROM t1 LIMIT 1` + if _, err := udl.ParseStatement(sql); err != nil { + t.Errorf("Options.ParseStatement: %v", err) + } + if _, err := udl.Parse(t.Context(), strings.NewReader(sql)); err != nil { + t.Errorf("Options.Parse: %v", err) + } + if _, err := parser.ParseStatement(sql); err == nil { + t.Error("ParseStatement accepted a DELETE ... LIMIT") + } + if _, err := (parser.Options{}).ParseString(sql); err == nil { + t.Error("the zero Options accepted a DELETE ... LIMIT") + } +} diff --git a/parser/parse_dml.go b/parser/parse_dml.go index cc6e022..3309416 100644 --- a/parser/parse_dml.go +++ b/parser/parse_dml.go @@ -170,10 +170,13 @@ func (p *parser) parseSetList() []*ast.SetPair { // // cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from // where_opt_ret. +// cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from +// where_opt_ret orderby_opt limit_opt. func (p *parser) parseUpdate(with *ast.With, start int) ast.Stmt { n := &ast.UpdateStmt{With: with} p.updateCore(n) n.Where, n.Returning = p.parseWhereOptRet() + n.OrderBy, n.Limit = p.parseUpdateDeleteLimit() n.Span = p.span(start) return n } @@ -196,14 +199,45 @@ func (p *parser) updateCore(n *ast.UpdateStmt) { // parseDelete implements the DELETE command. // // cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret. +// cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret orderby_opt +// limit_opt. func (p *parser) parseDelete(with *ast.With, start int) ast.Stmt { n := &ast.DeleteStmt{With: with} p.deleteCore(n) n.Where, n.Returning = p.parseWhereOptRet() + n.OrderBy, n.Limit = p.parseUpdateDeleteLimit() n.Span = p.span(start) return n } +// parseUpdateDeleteLimit implements the "orderby_opt limit_opt" tail that +// UPDATE and DELETE grow when SQLite is compiled with +// SQLITE_ENABLE_UPDATE_DELETE_LIMIT. Without Options.UpdateDeleteLimit the +// pinned build's rules are in force, where the statement simply ends: ORDER +// or LIMIT is then a token no rule can shift, and the caller's own end-of- +// statement check reports it. +// +// A build with SQLITE_UDL_CAPABLE_PARSER but not the option itself is a +// third behaviour -- it parses the clauses and then rejects them from a +// grammar action, "syntax error near \"ORDER BY\"" -- which meyer does not +// model: it accepts exactly the SQL its caller's database accepts, and no +// database accepts less than one of these two. +// +// orderby_opt ::= . / orderby_opt ::= ORDER BY sortlist. +// limit_opt ::= . / LIMIT expr. / LIMIT expr OFFSET expr. / LIMIT expr COMMA expr. +func (p *parser) parseUpdateDeleteLimit() ([]*ast.OrderingTerm, *ast.Limit) { + if !p.opts.UpdateDeleteLimit { + return nil, nil + } + var order []*ast.OrderingTerm + if p.at(token.ORDER) { + p.advance() + p.expect(token.BY) + order = p.parseSortList() + } + return order, p.parseLimit() +} + // deleteCore reads "DELETE FROM xfullname indexed_opt", everything a DELETE // shares with a trigger body's delete. func (p *parser) deleteCore(n *ast.DeleteStmt) { diff --git a/parser/parser.go b/parser/parser.go index db19469..4420e6f 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -76,6 +76,36 @@ func LineCol(src string, offset int) (line, col int) { return line, offset - start + 1 } +// Options turns on grammar that SQLite itself has only when it is compiled +// with the matching build option. +// +// The zero value is the pinned build the corpus is generated from, and is +// what the package-level entry points parse with, so conformance is +// unaffected by anything here. A field is only ever a fork in SQLite's own +// grammar, never a dialect meyer invents: the point is that a caller aimed +// at a database built with the option can parse the SQL that database +// accepts. +// +// stmts, err := parser.Options{UpdateDeleteLimit: true}.ParseString(src) +type Options struct { + // UpdateDeleteLimit accepts ORDER BY and LIMIT on UPDATE and DELETE, + // as SQLITE_ENABLE_UPDATE_DELETE_LIMIT does. The pinned build defines + // neither it nor SQLITE_UDL_CAPABLE_PARSER, so those clauses are a + // syntax error by default -- but the option is common enough (Android's + // SQLite ships with it, and it is what updateDeleteLimitError in + // parse.y exists to complain about) that the clauses have to be + // parseable on request. + // + // cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret + // orderby_opt limit_opt. + // cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from + // where_opt_ret orderby_opt limit_opt. + // + // Trigger bodies are unaffected in either build: trigger_cmd has no + // orderby_opt or limit_opt to gate. + UpdateDeleteLimit bool +} + // Parse reads SQL from r and returns one ast.Stmt per statement. // // The context is accepted so the signature matches the sibling parsers @@ -83,15 +113,23 @@ func LineCol(src string, offset int) (line, col int) { // pass over the input, and the recursion limit keeps even hostile input // from taking long enough to be worth cancelling. func Parse(ctx context.Context, r io.Reader) ([]ast.Stmt, error) { + return Options{}.Parse(ctx, r) +} + +// Parse is Parse with these options. +func (o Options) Parse(ctx context.Context, r io.Reader) ([]ast.Stmt, error) { src, err := io.ReadAll(r) if err != nil { return nil, err } - return ParseString(string(src)) + return o.ParseString(string(src)) } // ParseString parses a complete SQL script. -func ParseString(src string) (stmts []ast.Stmt, err error) { +func ParseString(src string) ([]ast.Stmt, error) { return Options{}.ParseString(src) } + +// ParseString is ParseString with these options. +func (o Options) ParseString(src string) (stmts []ast.Stmt, err error) { // newParser is inside the recover: it reads the first token, and an // input that starts with an illegal one fails there. defer func() { @@ -103,12 +141,15 @@ func ParseString(src string) (stmts []ast.Stmt, err error) { stmts, err = nil, b.err } }() - return newParser(src).parseScript(), nil + return newParser(src, o).parseScript(), nil } // ParseStatement parses exactly one statement and rejects trailing input. -func ParseStatement(src string) (ast.Stmt, error) { - stmts, err := ParseString(src) +func ParseStatement(src string) (ast.Stmt, error) { return Options{}.ParseStatement(src) } + +// ParseStatement is ParseStatement with these options. +func (o Options) ParseStatement(src string) (ast.Stmt, error) { + stmts, err := o.ParseString(src) if err != nil { return nil, err } @@ -119,6 +160,9 @@ func ParseStatement(src string) (ast.Stmt, error) { } // ParseExpr parses a single expression. It exists for tests and tooling. +// +// There is no Options form: no option reaches an expression, since the +// grammar they fork is always a statement's. func ParseExpr(src string) (expr ast.Expr, err error) { defer func() { if r := recover(); r != nil { @@ -129,7 +173,7 @@ func ParseExpr(src string) (expr ast.Expr, err error) { expr, err = nil, b.err } }() - p := newParser(src) + p := newParser(src, Options{}) e := p.parseExpr(precLowest) if !p.at(token.EOF) { p.syntaxError() @@ -145,6 +189,7 @@ type parser struct { src string toks []token.Token i int + opts Options nVar int // bind parameters seen so far varNums map[string]int // named parameters already assigned a number, if any @@ -182,8 +227,8 @@ func (p *parser) enter() { func (p *parser) leave() { p.depth-- } -func newParser(src string) *parser { - p := &parser{src: src, toks: lexer.Lex(src)} +func newParser(src string, opts Options) *parser { + p := &parser{src: src, toks: lexer.Lex(src), opts: opts} p.checkIllegal() return p } From fddb223484bb6a16973d34df91f13a20c481ca51 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 04:25:51 +0000 Subject: [PATCH 2/2] Drop an unverified claim about who enables the option 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 Claude-Session: https://claude.ai/code/session_019kZMPnsTWywB5icVh1wJrA --- parser/parser.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/parser/parser.go b/parser/parser.go index 4420e6f..2798184 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -91,10 +91,10 @@ type Options struct { // UpdateDeleteLimit accepts ORDER BY and LIMIT on UPDATE and DELETE, // as SQLITE_ENABLE_UPDATE_DELETE_LIMIT does. The pinned build defines // neither it nor SQLITE_UDL_CAPABLE_PARSER, so those clauses are a - // syntax error by default -- but the option is common enough (Android's - // SQLite ships with it, and it is what updateDeleteLimitError in - // parse.y exists to complain about) that the clauses have to be - // parseable on request. + // syntax error by default. The option selects grammar rules, so a + // database built with it can only be built from canonical sources -- + // which also means meyer cannot tell from the SQL alone, and the caller + // has to say. // // cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret // orderby_opt limit_opt.