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..2798184 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. 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. + // 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 }