Skip to content

Commit fe764e3

Browse files
kyleconroyclaude
andcommitted
feat: add GoogleSQL support to sqlc parse
Add a googlesql dialect backed by github.com/sqlc-dev/zetajones, a pure-Go GoogleSQL/ZetaSQL parser that passes the entire upstream googlesql parser golden-test corpus. New files in internal/engine/googlesql/: - parse.go: Parser implementation using zetajones - convert.go: AST converter from zetajones to sqlc AST - utils.go: helpers (identifiers, string unquoting, type rendering) - catalog.go, stdlib.go: catalog initialization - reserved.go: reserved keywords (delegates to zetajones/token) Covers SELECT (joins, subqueries, CTEs, set ops, window functions), INSERT/UPDATE/DELETE (incl. THEN RETURN), CREATE/DROP/TRUNCATE TABLE, and expressions incl. named @params (mapped to the same A_Expr shape the PostgreSQL engine produces, so the named-parameter rewriter consumes them). Unsupported constructs convert to ast.TODO without panicking. Usage: sqlc parse --dialect googlesql queries.sql Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 79af742 commit fe764e3

25 files changed

Lines changed: 2498 additions & 4 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require (
2222
github.com/spf13/pflag v1.0.10
2323
github.com/sqlc-dev/doubleclick v1.0.0
2424
github.com/sqlc-dev/marino v0.1.0
25+
github.com/sqlc-dev/zetajones v0.1.0
2526
github.com/tetratelabs/wazero v1.12.0
2627
github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07
2728
github.com/xeipuuv/gojsonschema v1.2.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ github.com/sqlc-dev/doubleclick v1.0.0 h1:2/OApfQ2eLgcfa/Fqs8WSMA6atH0G8j9hHbQIg
7171
github.com/sqlc-dev/doubleclick v1.0.0/go.mod h1:ODHRroSrk/rr5neRHlWMSRijqOak8YmNaO3VAZCNl5Y=
7272
github.com/sqlc-dev/marino v0.1.0 h1:8Fn13vFhx7OUcmDFfRZdf3zARAbNl04Lcy74211ZpIw=
7373
github.com/sqlc-dev/marino v0.1.0/go.mod h1:mQxC2dgDE0DWHMb2B5jZNk7KToJuS6wnxnffBfYnq08=
74+
github.com/sqlc-dev/zetajones v0.1.0 h1:VeG0atx6lNABr9V2bSI5vL9DvOKTHX0XjMqWUE/rv40=
75+
github.com/sqlc-dev/zetajones v0.1.0/go.mod h1:dU1DxwqC6Cahbpnw16KpH1J2waWRDMdwyDSvovMZR4I=
7476
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
7577
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
7678
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

internal/cmd/cmd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func init() {
3232
initCmd.Flags().BoolP("v1", "", false, "generate v1 config yaml file")
3333
initCmd.Flags().BoolP("v2", "", true, "generate v2 config yaml file")
3434
initCmd.MarkFlagsMutuallyExclusive("v1", "v2")
35-
parseCmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, or sqlite)")
35+
parseCmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, sqlite, clickhouse, or googlesql)")
3636
}
3737

3838
// Do runs the command logic.

internal/cmd/parse.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/sqlc-dev/sqlc/internal/engine/clickhouse"
1212
"github.com/sqlc-dev/sqlc/internal/engine/dolphin"
13+
"github.com/sqlc-dev/sqlc/internal/engine/googlesql"
1314
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
1415
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
1516
"github.com/sqlc-dev/sqlc/internal/sql/ast"
@@ -31,15 +32,18 @@ Examples:
3132
sqlc parse --dialect sqlite queries.sql
3233
3334
# Parse ClickHouse SQL
34-
sqlc parse --dialect clickhouse queries.sql`,
35+
sqlc parse --dialect clickhouse queries.sql
36+
37+
# Parse GoogleSQL (BigQuery, Spanner)
38+
sqlc parse --dialect googlesql queries.sql`,
3539
Args: cobra.MaximumNArgs(1),
3640
RunE: func(cmd *cobra.Command, args []string) error {
3741
dialect, err := cmd.Flags().GetString("dialect")
3842
if err != nil {
3943
return err
4044
}
4145
if dialect == "" {
42-
return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, or clickhouse)")
46+
return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, clickhouse, or googlesql)")
4347
}
4448

4549
// Determine input source
@@ -78,8 +82,11 @@ Examples:
7882
case "clickhouse":
7983
parser := clickhouse.NewParser()
8084
stmts, err = parser.Parse(input)
85+
case "googlesql":
86+
parser := googlesql.NewParser()
87+
stmts, err = parser.Parse(input)
8188
default:
82-
return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, or clickhouse)", dialect)
89+
return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, clickhouse, or googlesql)", dialect)
8390
}
8491
if err != nil {
8592
return fmt.Errorf("parse error: %w", err)

internal/endtoend/parse_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
10+
"github.com/google/go-cmp/cmp"
11+
12+
"github.com/sqlc-dev/sqlc/internal/cmd"
13+
)
14+
15+
// TestParseCommand runs `sqlc parse --dialect <dialect> query.sql` for every
16+
// case under testdata/parse and compares stdout against the stdout.json
17+
// golden. The dialect is the case directory's name up to the first "_"
18+
// (e.g. googlesql_select uses --dialect googlesql).
19+
//
20+
// To regenerate the goldens after an intentional AST change, run:
21+
//
22+
// SQLC_PARSE_UPDATE=1 go test ./internal/endtoend -run TestParseCommand
23+
func TestParseCommand(t *testing.T) {
24+
t.Parallel()
25+
26+
dirs, err := filepath.Glob(filepath.Join("testdata", "parse", "*"))
27+
if err != nil {
28+
t.Fatal(err)
29+
}
30+
if len(dirs) == 0 {
31+
t.Fatal("no testdata/parse cases found")
32+
}
33+
34+
for _, dir := range dirs {
35+
tc := filepath.Base(dir)
36+
dialect, _, ok := strings.Cut(tc, "_")
37+
if !ok {
38+
t.Fatalf("case %q: directory name must be <dialect>_<name>", tc)
39+
}
40+
t.Run(tc, func(t *testing.T) {
41+
// Subtests run sequentially: the parse command is a shared
42+
// package-level cobra.Command, so concurrent cmd.Do calls would
43+
// race on its flag and output state.
44+
query := filepath.Join(dir, "query.sql")
45+
golden := filepath.Join(dir, "stdout.json")
46+
47+
var stdout, stderr bytes.Buffer
48+
code := cmd.Do([]string{"parse", "--dialect", dialect, query}, strings.NewReader(""), &stdout, &stderr)
49+
if code != 0 {
50+
t.Fatalf("sqlc parse exited %d: %s", code, stderr.String())
51+
}
52+
53+
if os.Getenv("SQLC_PARSE_UPDATE") != "" {
54+
if err := os.WriteFile(golden, stdout.Bytes(), 0644); err != nil {
55+
t.Fatal(err)
56+
}
57+
return
58+
}
59+
60+
want, err := os.ReadFile(golden)
61+
if err != nil {
62+
t.Fatalf("reading golden (set SQLC_PARSE_UPDATE=1 to create it): %v", err)
63+
}
64+
if diff := cmp.Diff(string(want), stdout.String(), lineEndings()); diff != "" {
65+
t.Errorf("stdout mismatch (-want +got):\n%s", diff)
66+
}
67+
})
68+
}
69+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SELECT id, name FROM users WHERE status = 'active' ORDER BY id;
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
{
2+
"Stmt": {
3+
"DistinctClause": null,
4+
"IntoClause": null,
5+
"TargetList": {
6+
"Items": [
7+
{
8+
"Name": null,
9+
"Indirection": null,
10+
"Val": {
11+
"Name": "",
12+
"Fields": {
13+
"Items": [
14+
{
15+
"Str": "id"
16+
}
17+
]
18+
},
19+
"Location": 8
20+
},
21+
"Location": 8
22+
},
23+
{
24+
"Name": null,
25+
"Indirection": null,
26+
"Val": {
27+
"Name": "",
28+
"Fields": {
29+
"Items": [
30+
{
31+
"Str": "name"
32+
}
33+
]
34+
},
35+
"Location": 12
36+
},
37+
"Location": 12
38+
}
39+
]
40+
},
41+
"FromClause": {
42+
"Items": [
43+
{
44+
"Catalogname": null,
45+
"Schemaname": "",
46+
"Relname": "users",
47+
"Inh": false,
48+
"Relpersistence": 0,
49+
"Alias": null,
50+
"Location": 0
51+
}
52+
]
53+
},
54+
"WhereClause": {
55+
"Kind": 0,
56+
"Name": {
57+
"Items": [
58+
{
59+
"Str": "="
60+
}
61+
]
62+
},
63+
"Lexpr": {
64+
"Name": "",
65+
"Fields": {
66+
"Items": [
67+
{
68+
"Str": "status"
69+
}
70+
]
71+
},
72+
"Location": 34
73+
},
74+
"Rexpr": {
75+
"Val": {
76+
"Str": "active"
77+
},
78+
"Location": 43
79+
},
80+
"Location": 41
81+
},
82+
"GroupClause": null,
83+
"HavingClause": null,
84+
"WindowClause": null,
85+
"ValuesLists": null,
86+
"SortClause": {
87+
"Items": [
88+
{
89+
"Node": {
90+
"Name": "",
91+
"Fields": {
92+
"Items": [
93+
{
94+
"Str": "id"
95+
}
96+
]
97+
},
98+
"Location": 61
99+
},
100+
"SortbyDir": 2,
101+
"SortbyNulls": 0,
102+
"UseOp": null,
103+
"Location": 61
104+
}
105+
]
106+
},
107+
"LimitOffset": null,
108+
"LimitCount": null,
109+
"LockingClause": null,
110+
"WithClause": null,
111+
"Op": 0,
112+
"All": false,
113+
"Larg": null,
114+
"Rarg": null
115+
},
116+
"StmtLocation": 1,
117+
"StmtLen": 0
118+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE TABLE users (
2+
id INT64 NOT NULL,
3+
name STRING,
4+
score FLOAT64,
5+
created_at TIMESTAMP
6+
);

0 commit comments

Comments
 (0)