Skip to content

sqlite: replace the ANTLR parser with meyer - #4535

Open
kyleconroy wants to merge 4 commits into
mainfrom
claude/sqlite-parser-meyer-migration-c3g043
Open

sqlite: replace the ANTLR parser with meyer#4535
kyleconroy wants to merge 4 commits into
mainfrom
claude/sqlite-parser-meyer-migration-c3g043

Conversation

@kyleconroy

Copy link
Copy Markdown
Collaborator

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 direct dependency, and replaces a grammar that only approximated SQLite with one that accepts what SQLite accepts.

This is step 8 of meyer's PLAN.md.

What changed

  • parse.go calls meyer and slices statement extents out of the node spans. A parse failure becomes a sqlerr.Error positioned from meyer's byte offset, so syntax errors now report a line and column instead of a bare message.
  • convert.go rewritten to map meyer's tree onto sql/ast.
  • reserved.go defers to meyer's keyword table, which the hand-maintained list was missing MATERIALIZED and RETURNING from.
  • internal/engine/sqlite/parser/ deleted. antlr4-go/antlr/v4 stays in go.mod as an indirect dependency of cel-go.

sqlc.arg() and friends

SQLite has no schema-qualified function call, so sqlc.arg(), sqlc.narg(), sqlc.slice() and sqlc.embed() are not SQL meyer will parse — the ANTLR grammar accepted them because it had a qualified_function_name rule SQLite does not.

The parser folds sqlc.name( to sqlc_name( before parsing and the converter puts the schema back. The fold walks meyer's token stream rather than the raw text, so it cannot reach inside a string literal or a comment; it substitutes one byte for another, so every offset in the tree still points at the same place in the original source, which is what rewrite/parameters.go and rewrite/embeds.go edit against; and it records the offsets it touched, so a user-defined sqlc_arg function is not mistaken for one.

Behavior the old grammar got wrong

  • col TEXT NULL was read as a column whose type was TEXTNULL, because ANTLR's type_name swallowed the constraint. The column generated as interface{} rather than a nullable string. One golden file changes.
  • GROUP BY was dropped whenever the query also had a WHERE clause, from an off-by-one over the ANTLR expression list. It only worked on queries with no WHERE.
  • Comparison operators other than = were all recorded as = (there was a // TODO: add actual comparison), and IS, ISNULL, NOTNULL and LIKE reached the AST as = as well.
  • 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 — which the expander relies on.
  • Quoted identifiers other than "..." kept their quotes, and ""-escaped quotes inside one produced the empty string. Float literals became the integer 0.

Two changes outside the engine follow from these:

  • internal/compiler/output_columns.go recognizes SQLite's implicit rowid / _rowid_ / oid. GROUP BY reaching the compiler for the first time means its column references are validated, and GROUP BY transactions.rowid in testdata/table_function would otherwise fail.
  • lang.IsComparisonOperator learned the keyword-spelled comparisons (IS, IS NOT, LIKE, ISNULL, ...) so SELECT x IS NULL still types as bool now that the operator carries its own name. These are written upper case, which is how SQLite's parser records them; PostgreSQL and MySQL use distinct node types or their own operator spellings, so the strings do not overlap.

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, but anyone running against a build with that option compiled in would regress. Restoring it means a change in meyer, not here.

Also worth flagging: TRUE and FALSE are now column references rather than integer constants, because that is what SQLite's grammar does with them — they are resolved at name-resolution time, not tokenized as literals. Nothing in the test suite exercises it.

Testing

gofmt, go vet ./... and go test --tags=examples ./... all clean, against native PostgreSQL and MySQL: endtoend (TestReplay, TestFormat, TestExamples, TestExamplesVet, TestJsonSchema), the expander, and the sqlite examples. The endtoend suite is the acceptance gate; the sqlite engine's unit tests were removed.


Generated by Claude Code

claude added 4 commits July 31, 2026 02:20
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
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
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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants