Skip to content

Commit 98ef75d

Browse files
kyleconroyclaude
andauthored
sqlite: replace the ANTLR parser with meyer (#4535)
* sqlite: replace the ANTLR parser with meyer meyer is a hand-written recursive descent parser for SQLite's dialect, ported from src/parse.y and src/tokenize.c and checked against SQLite itself. Moving to it deletes 1.2 MB of generated ANTLR code and the antlr4-go dependency, and replaces a grammar that only approximated SQLite with one that accepts what SQLite accepts. parse.go calls meyer and slices statement extents out of the node spans; convert.go maps meyer's tree onto sqlc's AST; reserved.go now defers to meyer's keyword table, which the hand-maintained list was missing MATERIALIZED and RETURNING from. SQLite has no schema-qualified function call, so sqlc.arg() and its siblings are not SQL meyer will parse. The parser folds "sqlc.name(" to "sqlc_name(" over the token stream -- one byte for another, so every offset still points into the original source, and string literals and comments are untouched -- and the converter puts the schema back. Behavior the old grammar got wrong and this corrects: - "col TEXT NULL" was read as a column of type "TEXTNULL", so the column generated as interface{} rather than a nullable string. This changes one golden file. - GROUP BY was dropped whenever the query also had a WHERE clause, from an off-by-one over the ANTLR expression list. Now that GROUP BY reaches the compiler, its column references are validated, which SQLite's implicit rowid would have failed; the compiler now recognizes rowid, _rowid_ and oid for SQLite. - Comparison operators other than "=" were all recorded as "=", and IS, ISNULL, NOTNULL and LIKE reached the AST as "=" too. They now carry their own spelling, and lang.IsComparisonOperator learned the keyword-spelled comparisons so they still type as bool. - ORDER BY, DISTINCT, WITH RECURSIVE, ON CONFLICT, OVER and FILTER were parsed and then discarded, so ast.Format dropped them from the SQL it rebuilds. All are carried through now. - Quoted identifiers other than "..." kept their quotes; float literals became the integer 0. One capability is lost: the pinned SQLite build meyer targets defines neither SQLITE_ENABLE_UPDATE_DELETE_LIMIT nor SQLITE_UDL_CAPABLE_PARSER, so UPDATE and DELETE no longer accept ORDER BY or LIMIT. No test covered it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RkrYmzwTktB2CA5Bvqg6z5 * sqlite: drop parse_test.go The end-to-end tests are the acceptance gate for the parser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RkrYmzwTktB2CA5Bvqg6z5 * sqlite: drop catalog_test.go The end-to-end tests cover the catalog the schema builds. Removing the test leaves newTestCatalog with no callers, so it goes too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RkrYmzwTktB2CA5Bvqg6z5 * sqlite: drop the analyzer test The end-to-end tests exercise the analyzer against a real database. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RkrYmzwTktB2CA5Bvqg6z5 * sqlite: drop the sqlc.arg fold, now that preprocessing handles it #4537 rewrites sqlc's syntax to native placeholders above the engines, and SQLite is one of the preprocessed dialects, so sqlc.arg() and @name never reach the parser. The fold this branch carried is dead. One gap follows from the pass's rule that a statement whose sqlc syntax did not validate is copied through for the engine to parse: that assumes the engine can parse it. SQLite cannot, so an invalid sqlc.arg() surfaced as `near "(": syntax error` and the preprocessor's own diagnostics were never reported -- from parseCatalog, since a schema file and a query file are often the same file. On a parse failure the compiler now reports the sqlc syntax errors recorded for that file, if any, in place of the failure they produced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RkrYmzwTktB2CA5Bvqg6z5 * sqlite: update to meyer v0.1.1, restoring UPDATE/DELETE LIMIT v0.1.1 adds parser.Options, whose UpdateDeleteLimit field compiles in the grammar SQLITE_ENABLE_UPDATE_DELETE_LIMIT does. The option selects between two forms of SQLite's own rules, so meyer cannot tell which one a caller wants from the SQL alone; sqlc has always accepted ORDER BY and LIMIT on UPDATE and DELETE, so it asks for them. UpdateStmt and DeleteStmt grow OrderBy and Limit fields to match. sqlc's AST has somewhere to put the LIMIT and nowhere to put the ORDER BY, which is what the ANTLR converter did with them too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RkrYmzwTktB2CA5Bvqg6z5 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5ce31fc commit 98ef75d

25 files changed

Lines changed: 1087 additions & 40444 deletions

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ go 1.26.0
55
toolchain go1.26.5
66

77
require (
8-
github.com/antlr4-go/antlr/v4 v4.13.1
98
github.com/cubicdaiya/gonp v1.0.4
109
github.com/davecgh/go-spew v1.1.1
1110
github.com/fatih/structtag v1.2.0
@@ -22,6 +21,7 @@ require (
2221
github.com/spf13/pflag v1.0.10
2322
github.com/sqlc-dev/doubleclick v1.0.0
2423
github.com/sqlc-dev/marino v0.1.0
24+
github.com/sqlc-dev/meyer v0.1.1
2525
github.com/sqlc-dev/zetajones v0.1.0
2626
github.com/tetratelabs/wazero v1.12.0
2727
github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07
@@ -36,6 +36,7 @@ require (
3636
require (
3737
cel.dev/expr v0.25.2 // indirect
3838
filippo.io/edwards25519 v1.2.0 // indirect
39+
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
3940
github.com/dustin/go-humanize v1.0.1 // indirect
4041
github.com/google/uuid v1.6.0 // indirect
4142
github.com/inconshreveable/mousetrap v1.1.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ github.com/sqlc-dev/doubleclick v1.0.0 h1:2/OApfQ2eLgcfa/Fqs8WSMA6atH0G8j9hHbQIg
8383
github.com/sqlc-dev/doubleclick v1.0.0/go.mod h1:ODHRroSrk/rr5neRHlWMSRijqOak8YmNaO3VAZCNl5Y=
8484
github.com/sqlc-dev/marino v0.1.0 h1:8Fn13vFhx7OUcmDFfRZdf3zARAbNl04Lcy74211ZpIw=
8585
github.com/sqlc-dev/marino v0.1.0/go.mod h1:mQxC2dgDE0DWHMb2B5jZNk7KToJuS6wnxnffBfYnq08=
86+
github.com/sqlc-dev/meyer v0.1.1 h1:BAeZcfgLyTnk9f90DyGEKXPrHxtgvVD/DTM6awq2kUY=
87+
github.com/sqlc-dev/meyer v0.1.1/go.mod h1:pS4USCRf/SLjWtaMcnTo4YrEEFKBj8CyyqlxcVUJQH8=
8688
github.com/sqlc-dev/zetajones v0.1.0 h1:VeG0atx6lNABr9V2bSI5vL9DvOKTHX0XjMqWUE/rv40=
8789
github.com/sqlc-dev/zetajones v0.1.0/go.mod h1:dU1DxwqC6Cahbpnw16KpH1J2waWRDMdwyDSvovMZR4I=
8890
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

internal/compiler/compile.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ func (c *Compiler) parseCatalog(schemas []string) error {
4848
// but don't update the catalog - the database will be the source of truth
4949
stmts, err := c.parser.Parse(strings.NewReader(contents))
5050
if err != nil {
51-
merr.Add(filename, contents, 0, err)
51+
// A schema file and a query file are often the same file, so a
52+
// query's sqlc syntax can fail here. Look for an explanation
53+
// before reporting the syntax error it caused.
54+
if reported := addSyntaxErrors(merr, filename, contents, preprocess.File(c.conf.Engine, contents)); !reported {
55+
merr.Add(filename, contents, 0, err)
56+
}
5257
continue
5358
}
5459

@@ -111,7 +116,9 @@ func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
111116

112117
stmts, err := c.parser.Parse(strings.NewReader(pp.Text))
113118
if err != nil {
114-
merr.Add(filename, src, 0, err)
119+
if reported := addSyntaxErrors(merr, filename, src, pp); !reported {
120+
merr.Add(filename, src, 0, err)
121+
}
115122
continue
116123
}
117124
for _, stmt := range stmts {
@@ -162,3 +169,32 @@ func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
162169
Queries: q,
163170
}, nil
164171
}
172+
173+
// addSyntaxErrors reports every sqlc syntax error the preprocessor recorded
174+
// for a file, in source order, and says whether there were any. Locations come
175+
// back in the rewritten text's coordinates, so they are mapped through Origin
176+
// to point at what the user wrote.
177+
//
178+
// A statement whose sqlc syntax did not validate is copied through for the
179+
// engine to parse, which assumes the engine can parse it. SQLite cannot: it
180+
// has no schema-qualified function call, so a bad sqlc.arg() is a syntax error
181+
// there rather than a call the preprocessor's message can be attached to.
182+
// These messages name the cause, so they are reported in place of the failure
183+
// they produced; anything else wrong with the file surfaces on the next run.
184+
func addSyntaxErrors(merr *multierr.Error, filename, src string, pp *preprocess.Result) bool {
185+
var found bool
186+
for _, stmt := range pp.Statements() {
187+
if stmt.Err == nil {
188+
continue
189+
}
190+
found = true
191+
loc := pp.Origin(stmt.Start)
192+
var e *sqlerr.Error
193+
if errors.As(stmt.Err, &e) && e.Location != 0 {
194+
loc = pp.Origin(e.Location)
195+
e.Location = loc
196+
}
197+
merr.Add(filename, src, loc, stmt.Err)
198+
}
199+
return found
200+
}

internal/compiler/output_columns.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66

7+
"github.com/sqlc-dev/sqlc/internal/config"
78
"github.com/sqlc-dev/sqlc/internal/sql/ast"
89
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
910
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
@@ -69,7 +70,7 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er
6970

7071
if n.GroupClause != nil {
7172
for _, item := range n.GroupClause.Items {
72-
if err := findColumnForNode(item, tables, targets); err != nil {
73+
if err := c.findColumnForNode(item, tables, targets); err != nil {
7374
return nil, err
7475
}
7576
}
@@ -85,7 +86,7 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er
8586
if !ok {
8687
continue
8788
}
88-
if err := findColumnForNode(sb.Node, tables, targets); err != nil {
89+
if err := c.findColumnForNode(sb.Node, tables, targets); err != nil {
8990
return nil, fmt.Errorf("%v: if you want to skip this validation, set 'strict_order_by' to false", err)
9091
}
9192
}
@@ -101,7 +102,7 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er
101102
if !ok {
102103
continue
103104
}
104-
if err := findColumnForNode(caseExpr.Xpr, tables, targets); err != nil {
105+
if err := c.findColumnForNode(caseExpr.Xpr, tables, targets); err != nil {
105106
return nil, fmt.Errorf("%v: if you want to skip this validation, set 'strict_order_by' to false", err)
106107
}
107108
}
@@ -713,14 +714,36 @@ func outputColumnRefs(res *ast.ResTarget, tables []*Table, node *ast.ColumnRef)
713714
return cols, nil
714715
}
715716

716-
func findColumnForNode(item ast.Node, tables []*Table, targetList *ast.List) error {
717+
func (c *Compiler) findColumnForNode(item ast.Node, tables []*Table, targetList *ast.List) error {
717718
ref, ok := item.(*ast.ColumnRef)
718719
if !ok {
719720
return nil
720721
}
722+
if c.isImplicitColumnRef(ref) {
723+
return nil
724+
}
721725
return findColumnForRef(ref, tables, targetList)
722726
}
723727

728+
// isImplicitColumnRef reports whether ref names a column that every table in
729+
// the dialect has without declaring it, and which therefore never appears in
730+
// a catalog built from the schema. SQLite gives each ordinary table a rowid,
731+
// reachable under three spellings.
732+
func (c *Compiler) isImplicitColumnRef(ref *ast.ColumnRef) bool {
733+
if c.conf.Engine != config.EngineSQLite {
734+
return false
735+
}
736+
parts := stringSlice(ref.Fields)
737+
if len(parts) == 0 {
738+
return false
739+
}
740+
switch parts[len(parts)-1] {
741+
case "rowid", "_rowid_", "oid":
742+
return true
743+
}
744+
return false
745+
}
746+
724747
func findColumnForRef(ref *ast.ColumnRef, tables []*Table, targetList *ast.List) error {
725748
parts := stringSlice(ref.Fields)
726749
var alias, name string

internal/endtoend/testdata/case_named_params/sqlite/go/models.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/engine/sqlite/analyzer/analyze_test.go

Lines changed: 0 additions & 123 deletions
This file was deleted.

internal/engine/sqlite/catalog.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,3 @@ func NewCatalog() *catalog.Catalog {
1212
Extensions: map[string]struct{}{},
1313
}
1414
}
15-
16-
func newTestCatalog() *catalog.Catalog {
17-
return catalog.New("main")
18-
}

0 commit comments

Comments
 (0)