Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: CI
on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- run: go build ./...
- run: go vet ./...
- run: go test -race ./...
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Local cache used by cmd/regenerate-parse: downloaded SQLite release
# artifacts and the compiled oracle binary.
.sqlite/
70 changes: 70 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# meyer development guide

meyer is a hand-written, zero-dependency Go parser for the SQLite SQL
dialect. Read PLAN.md first — it defines the architecture, the AST shape,
and what is deliberately out of scope. The grammar ground truth is SQLite's
`parse.y` (Lemon) and `tokenize.c`; the dialect surface is
https://sqlite.org/lang.html.

## Rules

- **Zero dependencies.** `go.mod` must never gain a `require` line.
- **No parser generators.** Everything is hand-written recursive descent.
- **Never edit `parser/testdata/*.test` by hand.** Corpus files are produced
by `cmd/regenerate-parse` from SQLite's own test suite plus a real SQLite
build (the oracle). `*.metadata.json` sidecars are updated by tooling
(`go test ./parser -check-parse`), not by hand.
- Every nontrivial parse function carries a comment naming the `parse.y`
rule(s) it implements.
- Error messages must match SQLite's parser byte-for-byte
(`near "X": syntax error`, `unrecognized token: "X"`, `incomplete input`).

## The corpus

Each `parser/testdata/<name>.test` holds cases extracted from SQLite's
`test/<name>.test` TCL scripts. For every case, the raw oracle results (one
line per statement: prepared OK, or the exact error message and offset) are
stored; the harness derives the expectation from them:

- If any statement failed with a **syntax-family** message (`near "…":
syntax error`, `unrecognized token: "…"`, `incomplete input`), meyer must
reject the case with that first message.
- Otherwise meyer must accept the whole case. This includes statements that
failed **semantically** in SQLite (`no such table: …`) — those parsed
successfully; meyer does no semantic analysis.

Known looseness: messages produced by grammar *actions* (e.g. `ORDER BY
clause should come after UNION not before`) are currently classified as
semantic, so meyer is permitted to accept such statements. The pattern list
lives in `internal/testfile` (`syntaxFamily`) and can be extended without
regenerating the corpus, because the corpus stores raw oracle output.

## The loop

```sh
go run ./cmd/next-test # pick the next todo case to implement
# ... implement in lexer/, ast/, parser/ ...
go test ./parser -run TestParse -check-parse -v 2>&1 | grep "PARSE PASSES NOW"
go test ./... # everything else still green
# commit code + metadata.json changes together
```

`-check-parse` re-runs todo cases and deletes metadata entries for cases
that now pass. Never mark a case done by hand, and never "fix" a failing
case by editing the corpus — if you believe a corpus expectation is wrong,
the fix is in `cmd/regenerate-parse` (or the classification in
`internal/testfile`), followed by a full regeneration and a reviewed diff.

## Regenerating the corpus

```sh
go run ./cmd/regenerate-parse # the full starter set
go run ./cmd/regenerate-parse -files select1 # one file
```

The tool downloads the pinned SQLite release (version + SHA-256 constants in
`cmd/regenerate-parse/main.go`) into `.sqlite/` (gitignored), compiles the
oracle with the system C compiler, and rewrites corpus + metadata (new cases
start as todo; existing case states are preserved). Advancing the pin means
updating the constants and `parser/testdata/README.md`, regenerating
everything, and reviewing the diff.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 The sqlc Authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,33 @@
# meyer
A SQLite SQL parser

A pure Go parser for the SQLite SQL dialect. Zero dependencies, hand-written
recursive descent — no parser generators.

*(a Meyer lemon, for SQLite's [Lemon](https://sqlite.org/lemon.html) parser
generator)*

meyer is being built to replace the ANTLR-generated SQLite parser in
[sqlc](https://github.com/sqlc-dev/sqlc). See [PLAN.md](PLAN.md) for the
architecture and [CLAUDE.md](CLAUDE.md) for the development workflow.

## Status

Under construction: the test corpus and tooling exist; the parser itself is
being implemented against them.

## Conformance

meyer is tested against SQL extracted from SQLite's own test suite, with
expectations produced by a pinned build of SQLite itself: meyer must accept
exactly the statements SQLite's parser accepts, and reproduce SQLite's exact
syntax-error messages. See [parser/testdata/README.md](parser/testdata/README.md)
for provenance and the pinned version.

## Acknowledgments

SQLite and its test suite are public domain — https://sqlite.org/copyright.html.
This project's grammar and test corpus derive from them.

## License

MIT
35 changes: 35 additions & 0 deletions ast/ast.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Package ast declares the syntax tree for the SQLite SQL dialect.
//
// This is the milestone-1 skeleton: the interfaces exist so the parser API
// and test harness are stable, and node types are added as the parser is
// implemented (see PLAN.md for the full intended node set).
package ast

// Node is implemented by every syntax tree node. Positions are byte offsets
// into the original input.
type Node interface {
Pos() int
End() int
Children() []Node
}

// Stmt is implemented by all statement nodes.
type Stmt interface {
Node
stmtNode()
}

// Expr is implemented by all expression nodes.
type Expr interface {
Node
exprNode()
}

// Span carries a node's byte extent and is embedded in every node type.
type Span struct {
Start int `json:"start"`
Stop int `json:"end"`
}

func (s Span) Pos() int { return s.Start }
func (s Span) End() int { return s.Stop }
110 changes: 110 additions & 0 deletions cmd/next-test/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Command next-test picks the next corpus case to implement.
//
// It scans parser/testdata/*.metadata.json for todo cases, chooses the file
// with the fewest remaining todos (ties broken by smaller corpus file, so
// nearly-done and simple files finish first), and prints that file's first
// todo case: its SQL and the expectation derived from the oracle results.
//
// Usage:
//
// go run ./cmd/next-test [-testdata parser/testdata]
package main

import (
"flag"
"fmt"
"os"
"path/filepath"
"sort"

"github.com/sqlc-dev/meyer/internal/testfile"
)

func main() {
testdata := flag.String("testdata", "parser/testdata", "corpus directory")
flag.Parse()

paths, err := filepath.Glob(filepath.Join(*testdata, "*.test"))
if err != nil || len(paths) == 0 {
fmt.Fprintf(os.Stderr, "next-test: no corpus files under %s\n", *testdata)
os.Exit(1)
}
sort.Strings(paths)

type fileState struct {
path string
size int64
todos int
total int
}
var files []fileState
var totalTodo, totalCases int
for _, path := range paths {
meta, err := testfile.ReadMetadata(testfile.MetadataPath(path))
if err != nil {
fatal(err)
}
cases, err := testfile.Read(path)
if err != nil {
fatal(err)
}
st, _ := os.Stat(path)
files = append(files, fileState{path: path, size: st.Size(), todos: len(meta.Todo), total: len(cases)})
totalTodo += len(meta.Todo)
totalCases += len(cases)
}

fmt.Printf("progress: %d/%d cases passing (%d todo across %d files)\n\n",
totalCases-totalTodo, totalCases, totalTodo, len(files))
if totalTodo == 0 {
fmt.Println("No todo cases found!")
return
}

sort.Slice(files, func(i, j int) bool {
if files[i].todos != files[j].todos {
return files[i].todos < files[j].todos
}
return files[i].size < files[j].size
})
var pick fileState
for _, f := range files {
if f.todos > 0 {
pick = f
break
}
}

meta, err := testfile.ReadMetadata(testfile.MetadataPath(pick.path))
if err != nil {
fatal(err)
}
cases, err := testfile.Read(pick.path)
if err != nil {
fatal(err)
}
for _, c := range cases {
if !meta.Todo[c.Name] {
continue
}
exp := c.Expected()
fmt.Printf("file: %s (%d todo of %d cases)\ncase: %s\n\nsql:\n%s\n", pick.path, pick.todos, pick.total, c.Name, c.SQL)
if exp.OK {
fmt.Println("expect: parse success")
} else {
fmt.Printf("expect: parse error %q", exp.Message)
if exp.Offset >= 0 {
fmt.Printf(" at offset %d", exp.Offset)
}
fmt.Println()
}
return
}
fmt.Fprintf(os.Stderr, "next-test: %s: metadata lists todos not present in corpus; rerun cmd/regenerate-parse\n", pick.path)
os.Exit(1)
}

func fatal(err error) {
fmt.Fprintln(os.Stderr, "next-test:", err)
os.Exit(1)
}
Loading
Loading