|
| 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