Skip to content

Commit f21d767

Browse files
kyleconroyclaude
andcommitted
Add parse benchmarks and a fuzz target
BenchmarkParseSimple/Complex/Corpus measure throughput (21-27 MB/s on M1 Max). FuzzParse drives all five parse modes under NONE and MAXIMUM feature sets, seeded from the golden corpus; a 12M-exec run found no panics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b79816c commit f21d767

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

parser/bench_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package parser_test
2+
3+
import (
4+
"path/filepath"
5+
"strings"
6+
"testing"
7+
8+
"github.com/sqlc-dev/zetajones/internal/testfile"
9+
"github.com/sqlc-dev/zetajones/parser"
10+
)
11+
12+
const benchSimpleSQL = `SELECT id, name FROM users WHERE status = 'active' ORDER BY created_at DESC LIMIT 10`
13+
14+
const benchComplexSQL = `
15+
WITH revenue AS (
16+
SELECT o.customer_id, SUM(oi.quantity * oi.unit_price) AS total,
17+
COUNT(DISTINCT o.order_id) AS orders
18+
FROM orders AS o
19+
JOIN order_items AS oi ON o.order_id = oi.order_id
20+
WHERE o.created_at BETWEEN @start AND @end
21+
AND o.status NOT IN ('cancelled', 'refunded')
22+
GROUP BY o.customer_id
23+
HAVING SUM(oi.quantity * oi.unit_price) > 1000
24+
)
25+
SELECT c.name,
26+
r.total,
27+
r.orders,
28+
RANK() OVER (PARTITION BY c.region ORDER BY r.total DESC) AS region_rank,
29+
CASE WHEN r.total > 10000 THEN 'gold' WHEN r.total > 5000 THEN 'silver' ELSE 'bronze' END AS tier,
30+
(SELECT MAX(created_at) FROM logins l WHERE l.customer_id = c.id) AS last_login
31+
FROM customers AS c
32+
JOIN revenue AS r ON r.customer_id = c.id
33+
LEFT JOIN UNNEST(c.tags) AS tag
34+
WHERE EXISTS (SELECT 1 FROM subscriptions s WHERE s.customer_id = c.id AND s.active)
35+
QUALIFY region_rank <= 100
36+
ORDER BY r.total DESC`
37+
38+
func BenchmarkParseSimple(b *testing.B) {
39+
b.SetBytes(int64(len(benchSimpleSQL)))
40+
for b.Loop() {
41+
if _, err := parser.ParseStatement(benchSimpleSQL); err != nil {
42+
b.Fatal(err)
43+
}
44+
}
45+
}
46+
47+
func BenchmarkParseComplex(b *testing.B) {
48+
b.SetBytes(int64(len(benchComplexSQL)))
49+
for b.Loop() {
50+
if _, err := parser.ParseStatement(benchComplexSQL); err != nil {
51+
b.Fatal(err)
52+
}
53+
}
54+
}
55+
56+
// BenchmarkParseCorpus parses every statement-mode case in the golden corpus
57+
// once per iteration, giving a broad-coverage throughput number across the
58+
// whole grammar (errors included: error paths are part of the workload).
59+
func BenchmarkParseCorpus(b *testing.B) {
60+
files, err := filepath.Glob(filepath.Join("testdata", "*.test"))
61+
if err != nil || len(files) == 0 {
62+
b.Fatal("no testdata files")
63+
}
64+
type unit struct {
65+
sql string
66+
opts parser.Options
67+
}
68+
var units []unit
69+
var totalBytes int64
70+
for _, path := range files {
71+
cases, err := testfile.ParseFile(path)
72+
if err != nil {
73+
b.Fatal(err)
74+
}
75+
for _, c := range cases {
76+
mode := "statement"
77+
var opts parser.Options
78+
opts.Features = parser.ParseFeatureSet("NONE")
79+
for _, opt := range c.Options {
80+
if m, ok := strings.CutPrefix(opt, "mode="); ok {
81+
mode = m
82+
}
83+
if spec, ok := strings.CutPrefix(opt, "language_features="); ok {
84+
opts.Features = parser.ParseFeatureSet(spec)
85+
}
86+
if opt == "reserve_graph_table" {
87+
opts.ReserveGraphTable = true
88+
}
89+
}
90+
if mode != "statement" || c.HasAlternation {
91+
continue
92+
}
93+
units = append(units, unit{sql: c.SQL, opts: opts})
94+
totalBytes += int64(len(c.SQL))
95+
}
96+
}
97+
if len(units) == 0 {
98+
b.Fatal("no benchmark units")
99+
}
100+
b.SetBytes(totalBytes)
101+
b.ResetTimer()
102+
for b.Loop() {
103+
for _, u := range units {
104+
// Errors are expected for negative cases; the parse attempt is
105+
// the work being measured either way.
106+
_, _ = parser.ParseStatementWithOptions(u.sql, u.opts)
107+
}
108+
}
109+
}

parser/fuzz_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package parser_test
2+
3+
import (
4+
"path/filepath"
5+
"testing"
6+
7+
"github.com/sqlc-dev/zetajones/internal/testfile"
8+
"github.com/sqlc-dev/zetajones/parser"
9+
)
10+
11+
// FuzzParse feeds arbitrary input through every parse mode. The parser must
12+
// never panic: invalid input is expected to produce an error, valid input a
13+
// tree. Both feature extremes are exercised (NONE and MAXIMUM with
14+
// reserve_graph_table) since feature gating selects different code paths.
15+
func FuzzParse(f *testing.F) {
16+
// Seed with the golden corpus so the fuzzer starts from inputs that reach
17+
// deep into the grammar.
18+
files, _ := filepath.Glob(filepath.Join("testdata", "*.test"))
19+
seeded := 0
20+
for _, path := range files {
21+
cases, err := testfile.ParseFile(path)
22+
if err != nil {
23+
continue
24+
}
25+
for _, c := range cases {
26+
// Cap the seed corpus; the fuzzer mutates from here.
27+
if seeded >= 2000 {
28+
break
29+
}
30+
f.Add(c.SQL)
31+
seeded++
32+
}
33+
}
34+
f.Add("SELECT 1")
35+
f.Add("graph g match (a)-[e]->(b) return a")
36+
f.Add("`")
37+
f.Add("-- comment\n")
38+
f.Add(`"unterminated`)
39+
40+
maxOpts := parser.Options{
41+
Features: parser.ParseFeatureSet("MAXIMUM"),
42+
ReserveGraphTable: true,
43+
MacroExpansionMode: "lenient",
44+
}
45+
var noneOpts parser.Options
46+
noneOpts.Features = parser.ParseFeatureSet("NONE")
47+
48+
f.Fuzz(func(t *testing.T, sql string) {
49+
for _, opts := range []parser.Options{noneOpts, maxOpts} {
50+
_, _ = parser.ParseStatementWithOptions(sql, opts)
51+
_, _ = parser.ParseScriptWithOptions(sql, opts)
52+
_, _ = parser.ParseExpressionWithOptions(sql, opts)
53+
_, _ = parser.ParseTypeWithOptions(sql, opts)
54+
_, _ = parser.ParseMultipleWithOptions(sql, opts)
55+
}
56+
})
57+
}

0 commit comments

Comments
 (0)