Skip to content

Commit ac0ecd3

Browse files
authored
Parse UPDATE/DELETE ORDER BY and LIMIT behind an option (#5)
1 parent 08e24f0 commit ac0ecd3

10 files changed

Lines changed: 359 additions & 17 deletions

File tree

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ https://sqlite.org/lang.html.
2323
side: no nonterminal of the vendored grammar may go unmentioned.
2424
- Error messages must match SQLite's parser byte-for-byte
2525
(`near "X": syntax error`, `unrecognized token: "X"`, `incomplete input`).
26+
- Grammar that a build option turns on — `SQLITE_ENABLE_UPDATE_DELETE_LIMIT`
27+
so far — is a `parser.Options` field, off by default: the corpus is
28+
generated from the pinned build, so the default has to stay exactly where
29+
that build is. Expectations for an option come from a SQLite compiled with
30+
it, which takes a Lemon run rather than a `-D` on the amalgamation; the
31+
recipe is at the top of `parser/options_test.go`.
2632

2733
## The corpus
2834

PLAN.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,16 @@ them — meyer implements exactly what that build accepts, including
212212
CONSTRAINT` forms if (and only if) the pinned build has them. When the pin
213213
advances, `cmd/regenerate` re-derives expectations and diffs are reviewed.
214214
215+
A gate that real deployments turn on is a second matter, because the pinned
216+
build is not the only SQLite sqlc is ever pointed at. Those get a field on
217+
`parser.Options`, defaulting to the pinned build's answer so conformance is
218+
untouched, and are opt-in per parse:
219+
`SQLITE_ENABLE_UPDATE_DELETE_LIMIT` (`Options.UpdateDeleteLimit`) is the
220+
first — ORDER BY and LIMIT on UPDATE and DELETE. Each one has to be a fork
221+
in SQLite's own grammar, never a dialect meyer invents, and has to be
222+
verified the same way everything else is: against a build with the option
223+
on.
224+
215225
### 4. Positions are load-bearing (sqlc integration)
216226
217227
teesql has no positions at all; doubleclick's `End()` is vestigial. meyer

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,18 @@ _, err := parser.ParseString("SELECT FROM t")
7575
fmt.Println(err) // 1:8: near "FROM": syntax error
7676
```
7777

78+
Those entry points parse what the pinned SQLite build parses. Where SQLite's
79+
own grammar forks on a build option, `parser.Options` has the same entry
80+
points as methods, so a caller aimed at a database built differently can
81+
parse the SQL that database accepts:
82+
83+
```go
84+
// ORDER BY and LIMIT on UPDATE and DELETE, as SQLITE_ENABLE_UPDATE_DELETE_LIMIT
85+
// compiles in. Without it -- the default, and the pinned build -- they are a
86+
// syntax error, which is what SQLite reports too.
87+
stmts, err := parser.Options{UpdateDeleteLimit: true}.ParseString("DELETE FROM t ORDER BY x LIMIT 1")
88+
```
89+
7890
Every node embeds `ast.Span`, so `Pos()` and `End()` give byte offsets into
7991
the original input — sqlc slices the source with them to find `-- name:`
8092
comments and to report errors, so they are load-bearing rather than

ast/dml.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,13 @@ func (n *SetPair) Children() []Node {
9090
//
9191
// cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from
9292
// where_opt_ret.
93+
// cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from
94+
// where_opt_ret orderby_opt limit_opt.
9395
//
94-
// The pinned build defines neither SQLITE_ENABLE_UPDATE_DELETE_LIMIT nor
95-
// SQLITE_UDL_CAPABLE_PARSER, so UPDATE has no ORDER BY or LIMIT at all: they
96-
// are a plain syntax error rather than a grammar-action message.
96+
// The second form is the one SQLITE_ENABLE_UPDATE_DELETE_LIMIT compiles in.
97+
// The pinned build defines neither it nor SQLITE_UDL_CAPABLE_PARSER, so
98+
// OrderBy and Limit are only ever set for a parse that asked for them with
99+
// parser.Options.UpdateDeleteLimit; without it they are a syntax error.
97100
type UpdateStmt struct {
98101
Span
99102
With *With `json:"with,omitempty"`
@@ -106,6 +109,8 @@ type UpdateStmt struct {
106109
From []*TableRef `json:"from,omitempty"`
107110
Where Expr `json:"where,omitempty"`
108111
Returning []*ResultColumn `json:"returning,omitempty"`
112+
OrderBy []*OrderingTerm `json:"orderBy,omitempty"`
113+
Limit *Limit `json:"limit,omitempty"`
109114
}
110115

111116
func (*UpdateStmt) stmtNode() {}
@@ -115,14 +120,19 @@ func (n *UpdateStmt) Children() []Node {
115120
out = appendNodes(out, n.From)
116121
out = append(out, nodes(n.Where)...)
117122
out = appendNodes(out, n.Returning)
118-
return out
123+
out = appendNodes(out, n.OrderBy)
124+
return append(out, nodes(n.Limit)...)
119125
}
120126

121127
// DeleteStmt is a DELETE statement.
122128
//
123129
// cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret.
130+
// cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret orderby_opt
131+
// limit_opt.
124132
//
125-
// As for UPDATE, the pinned build's DELETE takes no ORDER BY or LIMIT.
133+
// As for UPDATE, the second form is the one
134+
// SQLITE_ENABLE_UPDATE_DELETE_LIMIT compiles in, and OrderBy and Limit are
135+
// set only for a parse made with parser.Options.UpdateDeleteLimit.
126136
type DeleteStmt struct {
127137
Span
128138
With *With `json:"with,omitempty"`
@@ -132,11 +142,14 @@ type DeleteStmt struct {
132142
NotIndexed bool `json:"notIndexed,omitempty"`
133143
Where Expr `json:"where,omitempty"`
134144
Returning []*ResultColumn `json:"returning,omitempty"`
145+
OrderBy []*OrderingTerm `json:"orderBy,omitempty"`
146+
Limit *Limit `json:"limit,omitempty"`
135147
}
136148

137149
func (*DeleteStmt) stmtNode() {}
138150
func (n *DeleteStmt) Children() []Node {
139151
out := nodes(n.With, n.Table, n.Alias, n.IndexedBy, n.Where)
140152
out = appendNodes(out, n.Returning)
141-
return out
153+
out = appendNodes(out, n.OrderBy)
154+
return append(out, nodes(n.Limit)...)
142155
}

ast/render.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,7 @@ func writeUpdate(b *strings.Builder, t *UpdateStmt) {
945945
write(b, t.Where)
946946
}
947947
writeReturning(b, t.Returning)
948+
writeUpdateDeleteLimit(b, t.OrderBy, t.Limit)
948949
}
949950

950951
func writeDelete(b *strings.Builder, t *DeleteStmt) {
@@ -964,6 +965,20 @@ func writeDelete(b *strings.Builder, t *DeleteStmt) {
964965
write(b, t.Where)
965966
}
966967
writeReturning(b, t.Returning)
968+
writeUpdateDeleteLimit(b, t.OrderBy, t.Limit)
969+
}
970+
971+
// writeUpdateDeleteLimit renders the ORDER BY and LIMIT an UPDATE or DELETE
972+
// carries only when it was parsed with parser.Options.UpdateDeleteLimit. The
973+
// rendering is re-parseable under the same option, and under no other.
974+
func writeUpdateDeleteLimit(b *strings.Builder, order []*OrderingTerm, limit *Limit) {
975+
if len(order) > 0 {
976+
w{b}.kw("ORDER BY")
977+
writeOrdering(b, order)
978+
}
979+
if limit != nil {
980+
write(b, limit)
981+
}
967982
}
968983

969984
func writeIndexedBy(b *strings.Builder, by *Ident, notIndexed bool) {

cmd/debug-parse/main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
// go run ./cmd/debug-parse 'SELECT 1'
1111
// go run ./cmd/debug-parse -tokens 'SELECT 1'
1212
// go run ./cmd/debug-parse -f query.sql
13+
// go run ./cmd/debug-parse -update-delete-limit 'DELETE FROM t LIMIT 1'
1314
// echo 'SELECT 1' | go run ./cmd/debug-parse
1415
//
1516
// A parse error is printed with a caret under the offending byte, in the
@@ -36,6 +37,8 @@ func main() {
3637
tokens = flag.Bool("tokens", false, "print the token stream instead of the tree")
3738
render = flag.Bool("render", false, "print the tree rendered back to SQL")
3839
positions = flag.Bool("pos", true, "include byte spans in the tree")
40+
udl = flag.Bool("update-delete-limit", false,
41+
"accept ORDER BY and LIMIT on UPDATE and DELETE, as SQLITE_ENABLE_UPDATE_DELETE_LIMIT does")
3942
)
4043
flag.Parse()
4144

@@ -51,7 +54,7 @@ func main() {
5154
return
5255
}
5356

54-
stmts, err := parser.ParseString(src)
57+
stmts, err := parser.Options{UpdateDeleteLimit: *udl}.ParseString(src)
5558
if err != nil {
5659
var pe *parser.Error
5760
if errors.As(err, &pe) {

internal/roundtrip/roundtrip.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,14 @@ type Result struct {
3232
// Equality is structural: byte spans and the Raw fields differ after a round
3333
// trip by construction, and the renderer promises nothing about them, so
3434
// dump.Structure leaves both out.
35-
func Check(stmts []ast.Stmt) Result {
35+
func Check(stmts []ast.Stmt) Result { return CheckWith(parser.Options{}, stmts) }
36+
37+
// CheckWith is Check for a tree that was parsed with opts. The rendering is
38+
// re-parsed with the same options, because a clause an option unlocked --
39+
// an UPDATE's LIMIT, say -- can only be read back with that option on.
40+
func CheckWith(opts parser.Options, stmts []ast.Stmt) Result {
3641
rendered := ast.Statements(stmts)
37-
again, err := parser.ParseString(rendered)
42+
again, err := opts.ParseString(rendered)
3843
if err != nil {
3944
return Result{Rendered: rendered, Reason: "rendered SQL does not parse: " + err.Error()}
4045
}

parser/options_test.go

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package parser_test
2+
3+
import (
4+
"errors"
5+
"strings"
6+
"testing"
7+
8+
"github.com/sqlc-dev/meyer/ast"
9+
"github.com/sqlc-dev/meyer/internal/roundtrip"
10+
"github.com/sqlc-dev/meyer/parser"
11+
)
12+
13+
// The expectations in this file are the oracle's, like every other error
14+
// expectation in the package, but they need an oracle the corpus tooling
15+
// does not build: SQLITE_ENABLE_UPDATE_DELETE_LIMIT selects grammar rules,
16+
// so defining it while compiling the released amalgamation changes nothing
17+
// -- the amalgamation ships a parse.c that Lemon already generated without
18+
// them. The build that produced these lines was, from the pinned release's
19+
// two artifacts:
20+
//
21+
// cc -o lemon sqlite-src-<v>/tool/lemon.c
22+
// ./lemon -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT -S src/parse.y
23+
//
24+
// with the resulting parse.c spliced into sqlite3.c over the region the
25+
// amalgamation marks "Begin file parse.c", and the whole compiled with
26+
// -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT. Lemon assigns the same token numbers
27+
// either way -- the generated parse.h is byte-identical -- so the splice is
28+
// sound. cmd/difftest run against that build, with the option on, agreed
29+
// with meyer on 228,652 mutations of the whole corpus.
30+
31+
// udlAccepted is SQL that a build with SQLITE_ENABLE_UPDATE_DELETE_LIMIT
32+
// parses and the pinned build rejects. Every case reaches SQLite's semantic
33+
// layer there ("no such table: t1"), which is how the oracle reports a
34+
// statement that parsed.
35+
var udlAccepted = []string{
36+
`DELETE FROM t1 ORDER BY x`,
37+
`DELETE FROM t1 WHERE x=1 ORDER BY x`,
38+
`DELETE FROM t1 WHERE x>0 LIMIT 5`,
39+
`DELETE FROM t1 WHERE x>0 ORDER BY x LIMIT 5 OFFSET 2`,
40+
`DELETE FROM t1 LIMIT 2, 3`,
41+
`DELETE FROM t1 INDEXED BY i1 WHERE x=1 LIMIT 1`,
42+
`DELETE FROM t1 AS a WHERE a.x=1 ORDER BY a.x LIMIT 1`,
43+
`DELETE FROM t1 ORDER BY x COLLATE nocase DESC NULLS LAST LIMIT 1`,
44+
`WITH c(x) AS (SELECT 1) DELETE FROM t1 WHERE x IN (SELECT x FROM c) LIMIT 1`,
45+
`UPDATE t1 SET y=1 LIMIT 5`,
46+
`UPDATE t1 SET y=1 WHERE x=1 ORDER BY x`,
47+
`UPDATE OR REPLACE t1 SET y=1 WHERE x=1 ORDER BY x DESC LIMIT 5 OFFSET 2`,
48+
`UPDATE t1 SET y=1 FROM t2 WHERE t1.x=t2.x LIMIT 1`,
49+
// where_opt_ret comes before orderby_opt, so RETURNING precedes LIMIT.
50+
`UPDATE t1 SET y=1 WHERE x=1 RETURNING x, y, '|' LIMIT 5`,
51+
`UPDATE t1 SET (a,b)=(SELECT 1,2) WHERE x=1 ORDER BY x LIMIT 1`,
52+
}
53+
54+
// udlRejected is SQL both builds reject, with the message and offset the
55+
// UDL build reports. The clauses are only ever the tail of a top-level
56+
// UPDATE or DELETE: OFFSET needs its LIMIT, RETURNING belongs to
57+
// where_opt_ret and so cannot follow one, and trigger_cmd never grew either
58+
// clause, whatever the build.
59+
var udlRejected = []struct {
60+
sql string
61+
msg string
62+
offset int
63+
}{
64+
{`DELETE FROM t1 WHERE x=1 OFFSET 2`, `near "OFFSET": syntax error`, 25},
65+
{`UPDATE t1 SET y=1 WHERE x=1 OFFSET 2`, `near "OFFSET": syntax error`, 28},
66+
{`DELETE FROM t1 LIMIT 1, 2 OFFSET 3`, `near "OFFSET": syntax error`, 26},
67+
{`UPDATE t1 SET y=1 LIMIT 5 RETURNING x`, `near "RETURNING": syntax error`, 26},
68+
{`DELETE FROM t1 LIMIT 5 RETURNING x`, `near "RETURNING": syntax error`, 23},
69+
{
70+
`CREATE TRIGGER r AFTER INSERT ON t1 BEGIN DELETE FROM t1 LIMIT 1; END`,
71+
`near "LIMIT": syntax error`, 57,
72+
},
73+
{
74+
`CREATE TRIGGER r AFTER INSERT ON t1 BEGIN UPDATE t1 SET y=1 ORDER BY x LIMIT 1; END`,
75+
`near "ORDER": syntax error`, 60,
76+
},
77+
}
78+
79+
// udlOff is what the pinned build makes of the same clauses: the statement
80+
// ended at the token before, so ORDER or LIMIT is a token no rule can
81+
// shift. These offsets are the corpus's own, from wherelimit.test.
82+
var udlOff = []struct {
83+
sql string
84+
msg string
85+
offset int
86+
}{
87+
{`DELETE FROM t1 ORDER BY x`, `near "ORDER": syntax error`, 15},
88+
{`DELETE FROM t1 WHERE x=1 ORDER BY x`, `near "ORDER": syntax error`, 25},
89+
{`DELETE FROM t1 WHERE x>0 LIMIT 5`, `near "LIMIT": syntax error`, 25},
90+
{`UPDATE t1 SET y=1 WHERE x=1 ORDER BY x`, `near "ORDER": syntax error`, 28},
91+
{`UPDATE t1 SET y=1 WHERE x=1 RETURNING x, y, '|' LIMIT 5`, `near "LIMIT": syntax error`, 48},
92+
{`WITH c(x) AS (SELECT 1) DELETE FROM t1 WHERE x IN (SELECT x FROM c) LIMIT 1`, `near "LIMIT": syntax error`, 68},
93+
}
94+
95+
var udl = parser.Options{UpdateDeleteLimit: true}
96+
97+
// TestUpdateDeleteLimit checks that the option accepts the clauses SQLite
98+
// accepts with SQLITE_ENABLE_UPDATE_DELETE_LIMIT, that the tree survives a
99+
// round trip through the renderer, and that the same SQL is still a syntax
100+
// error without the option -- the corpus is generated from a build without
101+
// it, so the default has to stay where it is.
102+
func TestUpdateDeleteLimit(t *testing.T) {
103+
for _, sql := range udlAccepted {
104+
t.Run(sql, func(t *testing.T) {
105+
stmts, err := udl.ParseString(sql)
106+
if err != nil {
107+
t.Fatalf("expected the input to parse with the option, got: %v", err)
108+
}
109+
if r := roundtrip.CheckWith(udl, stmts); !r.Ok {
110+
t.Errorf("round trip: %s\nrendered: %s", r.Reason, r.Rendered)
111+
}
112+
if _, err := parser.ParseString(sql); err == nil {
113+
t.Error("the default options accepted an UPDATE/DELETE LIMIT")
114+
}
115+
})
116+
}
117+
}
118+
119+
func TestUpdateDeleteLimitErrors(t *testing.T) {
120+
for _, tt := range udlRejected {
121+
t.Run(tt.sql, func(t *testing.T) {
122+
checkError(t, udl, tt.sql, tt.msg, tt.offset)
123+
})
124+
}
125+
for _, tt := range udlOff {
126+
t.Run(tt.sql, func(t *testing.T) {
127+
checkError(t, parser.Options{}, tt.sql, tt.msg, tt.offset)
128+
})
129+
}
130+
}
131+
132+
func checkError(t *testing.T, opts parser.Options, sql, msg string, offset int) {
133+
t.Helper()
134+
_, err := opts.ParseString(sql)
135+
var pe *parser.Error
136+
if !errors.As(err, &pe) {
137+
t.Fatalf("expected %q, got %v", msg, err)
138+
}
139+
if pe.Message != msg {
140+
t.Errorf("message:\n got: %s\n want: %s", pe.Message, msg)
141+
}
142+
if pe.Offset != offset {
143+
t.Errorf("offset: got %d, want %d", pe.Offset, offset)
144+
}
145+
}
146+
147+
// TestUpdateDeleteLimitTree checks that the clauses land on the statement
148+
// rather than being consumed and dropped, which accept/reject cannot see.
149+
func TestUpdateDeleteLimitTree(t *testing.T) {
150+
stmt, err := udl.ParseStatement(`DELETE FROM t1 WHERE x>0 ORDER BY y DESC LIMIT 5 OFFSET 2`)
151+
if err != nil {
152+
t.Fatalf("parse: %v", err)
153+
}
154+
del, ok := stmt.(*ast.DeleteStmt)
155+
if !ok {
156+
t.Fatalf("parsed to %T, want *ast.DeleteStmt", stmt)
157+
}
158+
if len(del.OrderBy) != 1 || del.OrderBy[0].Order != ast.SortDesc {
159+
t.Errorf("ORDER BY is %+v, want one descending term", del.OrderBy)
160+
}
161+
if del.Limit == nil || del.Limit.Count == nil || del.Limit.Offset == nil {
162+
t.Fatalf("LIMIT is %+v, want a count and an offset", del.Limit)
163+
}
164+
if got, want := ast.String(del), `DELETE FROM t1 WHERE x > 0 ORDER BY y DESC LIMIT 5 OFFSET 2`; got != want {
165+
t.Errorf("rendered\n got: %s\n want: %s", got, want)
166+
}
167+
168+
// "LIMIT x, y" swaps the operands, as it does in a SELECT, and the
169+
// renderer has to put them back the way they were written.
170+
stmt, err = udl.ParseStatement(`UPDATE t1 SET y=1 LIMIT 2, 3`)
171+
if err != nil {
172+
t.Fatalf("parse: %v", err)
173+
}
174+
up := stmt.(*ast.UpdateStmt)
175+
if up.Limit == nil || !up.Limit.Comma {
176+
t.Fatalf("LIMIT is %+v, want the comma spelling", up.Limit)
177+
}
178+
if got, want := ast.String(up), `UPDATE t1 SET y = 1 LIMIT 2, 3`; got != want {
179+
t.Errorf("rendered\n got: %s\n want: %s", got, want)
180+
}
181+
}
182+
183+
// TestOptionsEntryPoints checks that the option reaches every entry point,
184+
// and that the package-level ones are unaffected by it.
185+
func TestOptionsEntryPoints(t *testing.T) {
186+
const sql = `DELETE FROM t1 LIMIT 1`
187+
if _, err := udl.ParseStatement(sql); err != nil {
188+
t.Errorf("Options.ParseStatement: %v", err)
189+
}
190+
if _, err := udl.Parse(t.Context(), strings.NewReader(sql)); err != nil {
191+
t.Errorf("Options.Parse: %v", err)
192+
}
193+
if _, err := parser.ParseStatement(sql); err == nil {
194+
t.Error("ParseStatement accepted a DELETE ... LIMIT")
195+
}
196+
if _, err := (parser.Options{}).ParseString(sql); err == nil {
197+
t.Error("the zero Options accepted a DELETE ... LIMIT")
198+
}
199+
}

0 commit comments

Comments
 (0)