Skip to content

Commit 6f3292a

Browse files
committed
Extract the corpus from every pinned test script, and assert error offsets
PLAN.md milestone 2 named a hand-picked starter set of 69 scripts. Taking the whole pinned test suite instead costs 4.4 MB and raises the corpus from 4,685 to 20,971 cases across 943 scripts, with no curation to redo when the pin advances. Scripts that yield no literal-SQL block (the C-level tokenizer tests, the VFS and WAL harnesses) get no corpus file. The harness now also checks the error byte offset against sqlite3_error_offset wherever SQLite reported one, not just the message. Two fixes this shook out of the tooling: - An error message can span lines, because the offending token can: "SELECT X'0102, 1" reports the rest of the input, newline included, as an unrecognized token. runOracle joined nothing and failed the whole run; it now folds continuation lines back into the message so testfile.CheckCase can drop the case the way it already documents. - regenerate-parse wrote an empty corpus file for a script with nothing extractable, and left a stale one behind if a script stopped yielding cases. Corpus: 20971/20971 cases passing, and the parser needed no changes to get there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JzBeCg7rjweVW3uGPg5G7T
1 parent d4bf2d8 commit 6f3292a

1,758 files changed

Lines changed: 136976 additions & 41 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ https://sqlite.org/lang.html.
2222
## The corpus
2323

2424
Each `parser/testdata/<name>.test` holds cases extracted from SQLite's
25-
`test/<name>.test` TCL scripts. For every case, the raw oracle results (one
26-
line per statement: prepared OK, or the exact error message and offset) are
27-
stored; the harness derives the expectation from them:
25+
`test/<name>.test` TCL script; there is one for every script in the pinned
26+
source tree that yields at least one literal-SQL block. For every case, the
27+
raw oracle results (one line per statement: prepared OK, or the exact error
28+
message, offset and parse tail) are stored; the harness derives the
29+
expectation from them:
2830

2931
- If any statement failed with a **syntax-family** message (`near "…":
3032
syntax error`, `unrecognized token: "…"`, `incomplete input`), meyer must
@@ -64,7 +66,7 @@ the fix is in `cmd/regenerate-parse` (or the classification in
6466
## Regenerating the corpus
6567

6668
```sh
67-
go run ./cmd/regenerate-parse # the full starter set
69+
go run ./cmd/regenerate-parse # every script in the pinned tree
6870
go run ./cmd/regenerate-parse -files select1 # one file
6971
```
7072

cmd/regenerate-parse/main.go

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
//
2323
// go run ./cmd/regenerate-parse [-files select1,expr,...] [flags]
2424
//
25-
// With no -files, the starter set below is regenerated.
25+
// With no -files, every test script in the pinned source tree is
26+
// regenerated. Scripts that yield no literal-SQL cases (the tokenizer
27+
// tests written in C, the VFS and WAL harnesses, and so on) produce no
28+
// corpus file, and a stale one left over from an earlier run is removed.
2629
package main
2730

2831
import (
@@ -37,6 +40,7 @@ import (
3740
"os/exec"
3841
"path/filepath"
3942
"runtime"
43+
"sort"
4044
"strconv"
4145
"strings"
4246
"sync"
@@ -60,24 +64,21 @@ const (
6064
srcSHA = "d18fa15aec74d8c17e1463f861095adc01b5ad190256acb4f91d22f0368d232b"
6165
)
6266

63-
// starterSet is the corpus from PLAN.md milestone 2: the dedicated
64-
// parser/tokenizer tests, the grammar-heavy statement suites, and the
65-
// evidence files that map 1:1 to the documented grammar.
66-
var starterSet = []string{
67-
"parser1", "tokenize", "keyword1",
68-
"select1", "select2", "select3", "select4", "select5", "select6",
69-
"select7", "select8", "select9", "selectA", "selectB", "selectC",
70-
"selectD", "selectE", "selectF", "selectG", "selectH",
71-
"expr", "expr2", "in", "in2", "in3", "in4", "in5", "between",
72-
"with1", "with2", "with3", "withM",
73-
"window1", "window2", "window3", "window4", "window5",
74-
"window6", "window7", "window8", "window9", "windowA",
75-
"windowB", "windowC", "windowD", "windowE", "windowerr",
76-
"alter", "alter2", "alter3", "alter4", "altertab", "altertab2", "altertab3",
77-
"alterdropcol", "alterdropcol2",
78-
"trigger1", "upsert1", "upsert2", "upsert3", "upsert4", "upsert5",
79-
"returning1",
80-
"e_select", "e_expr", "e_createtable", "e_insert", "e_update", "e_delete",
67+
// allScripts lists every test/*.test script of the pinned source tree, in
68+
// sorted order. PLAN.md milestone 2 named a hand-picked starter set; taking
69+
// the whole suite instead costs a few megabytes and quadruples the case
70+
// count, and needs no curation when the pin advances.
71+
func allScripts(testDir string) ([]string, error) {
72+
paths, err := filepath.Glob(filepath.Join(testDir, "*.test"))
73+
if err != nil {
74+
return nil, err
75+
}
76+
names := make([]string, len(paths))
77+
for i, path := range paths {
78+
names[i] = strings.TrimSuffix(filepath.Base(path), ".test")
79+
}
80+
sort.Strings(names)
81+
return names, nil
8182
}
8283

8384
func main() {
@@ -89,11 +90,6 @@ func main() {
8990
)
9091
flag.Parse()
9192

92-
names := starterSet
93-
if *files != "" {
94-
names = strings.Split(*files, ",")
95-
}
96-
9793
oracle, err := ensureOracle(*cacheDir)
9894
if err != nil {
9995
fatal(err)
@@ -102,11 +98,18 @@ func main() {
10298
if err != nil {
10399
fatal(err)
104100
}
101+
102+
var names []string
103+
if *files != "" {
104+
names = strings.Split(*files, ",")
105+
} else if names, err = allScripts(testDir); err != nil {
106+
fatal(err)
107+
}
105108
if err := os.MkdirAll(*testdata, 0o755); err != nil {
106109
fatal(err)
107110
}
108111

109-
var totalCases, totalTodo int
112+
var totalCases, totalTodo, usedScripts, emptyScripts int
110113
var totals tclextract.Stats
111114
for _, name := range names {
112115
// Snapshot the previous corpus state before regenerating, so the
@@ -127,6 +130,17 @@ func main() {
127130
if err != nil {
128131
fatal(fmt.Errorf("%s: %w", name, err))
129132
}
133+
totals.Found += stats.Found
134+
totals.SkippedSubst += stats.SkippedSubst
135+
totals.SkippedForm += stats.SkippedForm
136+
if len(cases) == 0 {
137+
// Nothing extractable: leave no corpus file behind, and clear
138+
// away one from an earlier run of a differently pinned tree.
139+
os.Remove(testPath)
140+
os.Remove(testfile.MetadataPath(testPath))
141+
emptyScripts++
142+
continue
143+
}
130144
todo, err := mergeMetadata(testPath, prevCases, prevMeta.Todo, cases)
131145
if err != nil {
132146
fatal(fmt.Errorf("%s: %w", name, err))
@@ -135,12 +149,11 @@ func main() {
135149
name, len(cases), todo, stats.SkippedSubst, stats.SkippedForm)
136150
totalCases += len(cases)
137151
totalTodo += todo
138-
totals.Found += stats.Found
139-
totals.SkippedSubst += stats.SkippedSubst
140-
totals.SkippedForm += stats.SkippedForm
152+
usedScripts++
141153
}
142-
fmt.Printf("\ntotal: %d cases (%d todo) from %d files; %d of %d found blocks skipped (%d substitution, %d malformed/non-literal)\n",
143-
totalCases, totalTodo, len(names),
154+
fmt.Printf("\ntotal: %d cases (%d todo) from %d of %d scripts (%d yielded nothing); "+
155+
"%d of %d found blocks skipped (%d substitution, %d malformed/non-literal)\n",
156+
totalCases, totalTodo, usedScripts, len(names), emptyScripts,
144157
totals.SkippedSubst+totals.SkippedForm, totals.Found,
145158
totals.SkippedSubst, totals.SkippedForm)
146159
}
@@ -196,6 +209,9 @@ func regenerateFile(oracle, testDir, outDir, name string, jobs int) ([]testfile.
196209
}
197210
kept = append(kept, c)
198211
}
212+
if len(kept) == 0 {
213+
return nil, stats, nil
214+
}
199215
if err := testfile.Write(filepath.Join(outDir, name+".test"), kept); err != nil {
200216
return nil, stats, err
201217
}
@@ -214,6 +230,17 @@ func runOracle(oracle, sql string) ([]testfile.StmtResult, error) {
214230
}
215231
var results []testfile.StmtResult
216232
for _, line := range strings.Split(strings.TrimRight(out.String(), "\n"), "\n") {
233+
// An error message can itself contain a newline, because the
234+
// offending token can: "SELECT X'0102, 1" reports the rest of the
235+
// input, newline included, as an unrecognized token. Such a line is
236+
// a continuation of the message before it. The corpus file format
237+
// cannot hold a multi-line message, so testfile.CheckCase drops the
238+
// case later; joining here keeps the observation faithful until
239+
// then instead of failing the whole run.
240+
if !strings.HasPrefix(line, "stmt ") && len(results) > 0 {
241+
results[len(results)-1].Message += "\n" + line
242+
continue
243+
}
217244
if line == "" {
218245
continue
219246
}

parser/parser_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,8 @@ func runFile(t *testing.T, path string) {
7676

7777
// runCase checks one corpus case against the oracle-derived expectation:
7878
// meyer must accept exactly when SQLite's parser accepted, and must produce
79-
// the identical message on rejection. (Error offsets are recorded in the
80-
// corpus but not asserted yet; they become part of this check once the
81-
// parser tracks positions.)
79+
// the identical message and byte offset on rejection. SQLite reports -1 from
80+
// sqlite3_error_offset for errors it cannot place, and those are not checked.
8281
func runCase(c testfile.Case) error {
8382
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
8483
defer cancel()
@@ -108,5 +107,9 @@ func runCase(c testfile.Case) error {
108107
if pe.Message != exp.Message {
109108
return fmt.Errorf("error message mismatch:\n got: %s\n want: %s", pe.Message, exp.Message)
110109
}
110+
if exp.Offset >= 0 && pe.Offset != exp.Offset {
111+
return fmt.Errorf("error offset mismatch for %s: got %d, want %d",
112+
exp.Message, pe.Offset, exp.Offset)
113+
}
111114
return nil
112115
}

parser/testdata/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
The `*.test` files in this directory are generated by
44
`go run ./cmd/regenerate-parse`. **Do not edit them by hand.**
55

6-
- **Source of the SQL**: SQLite's own test suite (`test/*.test` TCL scripts)
7-
from the pinned release below. SQL blocks are extracted verbatim from
8-
`do_execsql_test` / `do_catchsql_test` invocations whose SQL argument is a
9-
literal TCL brace block (cases using TCL substitution are skipped).
6+
- **Source of the SQL**: SQLite's own test suite from the pinned release
7+
below — every `test/*.test` TCL script, not a curated subset. SQL blocks
8+
are extracted verbatim from `do_execsql_test` / `do_catchsql_test`
9+
invocations whose SQL argument is a literal TCL brace block (cases using
10+
TCL substitution are skipped, as are the few whose oracle message spans
11+
more than one line, which the file format cannot hold). Scripts that yield
12+
no such block — the C-level tokenizer tests, the VFS and WAL harnesses —
13+
have no corpus file here.
1014
- **Source of the expectations**: a real SQLite build. Every statement of
1115
every case is prepared independently with `sqlite3_prepare_v2` against an
1216
empty in-memory database (nothing is executed) using the oracle program in

parser/testdata/affinity2.test

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
==== affinity2-100
2+
3+
CREATE TABLE t1(
4+
xi INTEGER,
5+
xr REAL,
6+
xb BLOB,
7+
xn NUMERIC,
8+
xt TEXT
9+
);
10+
INSERT INTO t1(rowid,xi,xr,xb,xn,xt) VALUES(1,1,1,1,1,1);
11+
INSERT INTO t1(rowid,xi,xr,xb,xn,xt) VALUES(2,'2','2','2','2','2');
12+
INSERT INTO t1(rowid,xi,xr,xb,xn,xt) VALUES(3,'03','03','03','03','03');
13+
----
14+
stmt 0 ok
15+
stmt 94 err 1 -1 154 no such table: t1
16+
stmt 154 err 1 -1 224 no such table: t1
17+
stmt 224 err 1 -1 299 no such table: t1
18+
==== affinity2-110
19+
20+
SELECT xi, typeof(xi) FROM t1 ORDER BY rowid;
21+
----
22+
stmt 0 err 1 -1 48 no such table: t1
23+
==== affinity2-120
24+
25+
SELECT xr, typeof(xr) FROM t1 ORDER BY rowid;
26+
----
27+
stmt 0 err 1 -1 48 no such table: t1
28+
==== affinity2-130
29+
30+
SELECT xb, typeof(xb) FROM t1 ORDER BY rowid;
31+
----
32+
stmt 0 err 1 -1 48 no such table: t1
33+
==== affinity2-140
34+
35+
SELECT xn, typeof(xn) FROM t1 ORDER BY rowid;
36+
----
37+
stmt 0 err 1 -1 48 no such table: t1
38+
==== affinity2-150
39+
40+
SELECT xt, typeof(xt) FROM t1 ORDER BY rowid;
41+
----
42+
stmt 0 err 1 -1 48 no such table: t1
43+
==== affinity2-200
44+
45+
SELECT rowid, xi==xt, xi==xb, xi==+xt FROM t1 ORDER BY rowid;
46+
----
47+
stmt 0 err 1 -1 64 no such table: t1
48+
==== affinity2-210
49+
50+
SELECT rowid, xr==xt, xr==xb, xr==+xt FROM t1 ORDER BY rowid;
51+
----
52+
stmt 0 err 1 -1 64 no such table: t1
53+
==== affinity2-220
54+
55+
SELECT rowid, xn==xt, xn==xb, xn==+xt FROM t1 ORDER BY rowid;
56+
----
57+
stmt 0 err 1 -1 64 no such table: t1
58+
==== affinity2-300
59+
60+
SELECT rowid, xt==+xi, xt==xi, xt==xb FROM t1 ORDER BY rowid;
61+
----
62+
stmt 0 err 1 -1 64 no such table: t1
63+
==== 400
64+
65+
CREATE TABLE ttt(c0, c1);
66+
CREATE INDEX ii ON ttt(CAST(c0 AS NUMERIC));
67+
INSERT INTO ttt VALUES('abc', '-1');
68+
----
69+
stmt 0 ok
70+
stmt 28 err 1 -1 75 no such table: main.ttt
71+
stmt 75 err 1 -1 115 no such table: ttt
72+
==== 410
73+
74+
SELECT * FROM ttt WHERE CAST(c0 AS NUMERIC) > c1 GROUP BY rowid;
75+
----
76+
stmt 0 err 1 -1 67 no such table: ttt
77+
==== 420
78+
79+
SELECT * FROM ttt INDEXED BY ii WHERE CAST(c0 AS NUMERIC) > c1 GROUP BY rowid;
80+
----
81+
stmt 0 err 1 -1 81 no such table: ttt
82+
==== 430
83+
84+
CREATE TABLE t3(a, b, c INTEGER);
85+
CREATE INDEX t3ac ON t3(a, c-1);
86+
INSERT INTO t3 VALUES(1, 1, 1);
87+
INSERT INTO t3 VALUES(2, 1, 0);
88+
INSERT INTO t3 VALUES(3, 1, 1);
89+
INSERT INTO t3 VALUES(4, 1, 0);
90+
INSERT INTO t3 VALUES(5, 1, 1);
91+
----
92+
stmt 0 ok
93+
stmt 36 err 1 -1 71 no such table: main.t3
94+
stmt 71 err 1 -1 105 no such table: t3
95+
stmt 105 err 1 -1 139 no such table: t3
96+
stmt 139 err 1 -1 173 no such table: t3
97+
stmt 173 err 1 -1 207 no such table: t3
98+
stmt 207 err 1 -1 241 no such table: t3
99+
==== 440
100+
101+
SELECT * FROM t3 WHERE c='0' ORDER BY a;
102+
----
103+
stmt 0 err 1 -1 43 no such table: t3
104+
==== 500
105+
106+
DROP TABLE IF EXISTS t0;
107+
CREATE TABLE t0(c0 TEXT UNIQUE, c1);
108+
INSERT INTO t0(c0) VALUES (-1);
109+
SELECT quote(- x'ce'), quote(t0.c0), quote(- x'ce' >= t0.c0) FROM t0;
110+
----
111+
stmt 0 ok
112+
stmt 27 ok
113+
stmt 66 err 1 -1 100 no such table: t0
114+
stmt 100 err 1 -1 172 no such table: t0
115+
==== 501
116+
117+
SELECT * FROM t0 WHERE - x'ce' >= t0.c0;
118+
----
119+
stmt 0 err 1 -1 43 no such table: t0
120+
==== 502
121+
122+
SELECT quote(+-+x'ce'), quote(t0.c0), quote(+-+x'ce' >= t0.c0) FROM t0;
123+
----
124+
stmt 0 err 1 -1 74 no such table: t0
125+
==== 503
126+
127+
SELECT * FROM t0 WHERE +-+x'ce' >= t0.c0;
128+
----
129+
stmt 0 err 1 -1 44 no such table: t0
130+
==== 504
131+
132+
SELECT quote(- 'ce'), quote(t0.c0), quote(- 'ce' >= t0.c0) FROM t0;
133+
----
134+
stmt 0 err 1 -1 70 no such table: t0
135+
==== 505
136+
137+
SELECT * FROM t0 WHERE - 'ce' >= t0.c0;
138+
----
139+
stmt 0 err 1 -1 42 no such table: t0
140+
==== 506
141+
142+
SELECT quote(+-+'ce'), quote(t0.c0), quote(+-+'ce' >= t0.c0) FROM t0;
143+
----
144+
stmt 0 err 1 -1 72 no such table: t0
145+
==== 507
146+
147+
SELECT * FROM t0 WHERE +-+'ce' >= t0.c0;
148+
----
149+
stmt 0 err 1 -1 43 no such table: t0
150+
==== 600
151+
152+
DROP TABLE IF EXISTS t0;
153+
CREATE TABLE t0(c0 REAL UNIQUE);
154+
INSERT INTO t0(c0) VALUES (3175546974276630385);
155+
SELECT 3175546974276630385 < c0 FROM t0;
156+
----
157+
stmt 0 ok
158+
stmt 29 ok
159+
stmt 66 err 1 -1 119 no such table: t0
160+
stmt 119 err 1 -1 164 no such table: t0
161+
==== 601
162+
163+
SELECT 1 FROM t0 WHERE 3175546974276630385 < c0;
164+
----
165+
stmt 0 err 1 -1 53 no such table: t0

0 commit comments

Comments
 (0)