Skip to content

Commit 42907fd

Browse files
committed
Move ClickHouse testgen into internal/testcheck and verify end-to-end cases
testcheck replaces testgen. It generates nothing: each engine package finds the analyze cases under internal/endtoend/testdata, loads a case's schema, fixture and queries into a real database, and compares what the database reports with the output.json the case committed, byte for byte. The ClickHouse package is the testgen code moved over; other engines get their own package alongside it. sqlc analyze now prints each column's type as a call expression instead of data_type, not_null and is_array, so its output and the database's answer share one format. The compiler's flat column description maps onto it as the data type wrapped in one array node per dimension with the column's nullability on the outermost node. Every analyze case's expected output is renamed from stdout.txt to output.json, which the end-to-end harness now reads first, and regenerated. The cases from testgen's testdata become analyze_types, analyze_expressions, analyze_subqueries and analyze_exec under the ClickHouse dialect, with fixture.sql next to the schema, and the existing analyze_basic and analyze_params ClickHouse cases gain fixtures. Two queries from the subqueries case, a CTE and a SELECT * over a join, are left out because sqlc cannot analyze them yet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6XkyWnx7iJFEb8q3AnYps
1 parent 696eba2 commit 42907fd

73 files changed

Lines changed: 1383 additions & 1588 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ A case is a directory holding the inputs and the expected output. `exec.json`
120120
names the command and its arguments — omit it and the case runs `generate`,
121121
comparing the generated files against the ones committed alongside; give it
122122
`{"command": "analyze", "args": [...]}` and the case compares the command's
123-
stdout against `stdout.txt`. A case that is expected to fail commits its
124-
`stderr.txt`. Regenerate a golden by running the command in its directory and
123+
stdout against `output.json` (or `stdout.txt` for a command that does not
124+
print JSON). A case that is expected to fail commits its `stderr.txt`. Regenerate a golden by running the command in its directory and
125125
writing the output back over the committed file.
126126

127127
`TestReplay` runs the whole corpus once per *context*. `base` runs each case as
@@ -215,9 +215,6 @@ MYSQL_SERVER_URI="root:mysecretpassword@tcp(127.0.0.1:3306)/mysql?multiStatement
215215
- `/postgresql/` - PostgreSQL parser and converter
216216
- `/dolphin/` - MySQL parser (uses TiDB parser)
217217
- `/sqlite/` - SQLite parser
218-
- `/clickhouse/testgen/` - Nested module that records what a real
219-
ClickHouse reports about a schema, fixture and queries, in the shape of
220-
`sqlc analyze` output with types as call expressions; see its README
221218
- `/duckdb/` - DuckDB 2.0 parser (uses darkwing, the pure Go port of
222219
DuckDB's PEG parser); its dialect seeds are generated by
223220
`/internal/tools/sqlc-duckdb-gen` from a live DuckDB CLI
@@ -228,6 +225,9 @@ MYSQL_SERVER_URI="root:mysecretpassword@tcp(127.0.0.1:3306)/mysql?multiStatement
228225
- `/internal/codegen/` - Code generation for different languages
229226
- `/internal/config/` - Configuration file parsing
230227
- `/internal/endtoend/` - End-to-end tests
228+
- `/internal/testcheck/` - Nested module that verifies the analyze cases under
229+
`/internal/endtoend/testdata/` against a real database, one package per
230+
engine; see its README
231231
- `/internal/sqltest/` - Test database setup (Docker, native, local detection)
232232
- `/examples/` - Example projects for testing
233233

docs/howto/analyze.md

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,23 +70,24 @@ reports the result columns and parameters:
7070
"columns": [
7171
{
7272
"name": "id",
73-
"data_type": "bigserial",
74-
"not_null": true,
75-
"is_array": false,
73+
"type": {
74+
"name": "bigserial"
75+
},
7676
"table": "authors"
7777
},
7878
{
7979
"name": "name",
80-
"data_type": "text",
81-
"not_null": true,
82-
"is_array": false,
80+
"type": {
81+
"name": "text"
82+
},
8383
"table": "authors"
8484
},
8585
{
8686
"name": "bio",
87-
"data_type": "text",
88-
"not_null": false,
89-
"is_array": false,
87+
"type": {
88+
"name": "text",
89+
"nullable": true
90+
},
9091
"table": "authors"
9192
}
9293
],
@@ -95,9 +96,9 @@ reports the result columns and parameters:
9596
"number": 1,
9697
"column": {
9798
"name": "id",
98-
"data_type": "bigserial",
99-
"not_null": true,
100-
"is_array": false,
99+
"type": {
100+
"name": "bigserial"
101+
},
101102
"table": "authors"
102103
}
103104
}
@@ -106,6 +107,13 @@ reports the result columns and parameters:
106107
]
107108
```
108109

110+
A column's `type` is written as a call expression: a `name` applied to
111+
`args`, each of which carries an optional `label` and exactly one of `type`,
112+
`int`, `bool` or `string`, with `nullable` set at whatever depth it applies.
113+
An array of text is `array` applied to `text`; a `Map(String, Nullable(UInt8))`
114+
in ClickHouse is `map` applied to `string` and a nullable `uint8`. Names are
115+
recorded as the engine reports them.
116+
109117
Pass `--ast` to also include each statement's parsed AST under an `ast` key. It
110118
has the same shape as the output of [`parse`](parse.md), with every node tagged
111119
by type.

internal/cmd/analyze.go

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -204,18 +204,36 @@ type analyzedQuery struct {
204204
}
205205

206206
type analyzedColumn struct {
207-
Name string `json:"name"`
208-
DataType string `json:"data_type"`
209-
NotNull bool `json:"not_null"`
210-
IsArray bool `json:"is_array"`
211-
Table string `json:"table,omitempty"`
207+
Name string `json:"name"`
208+
Type *analyzedType `json:"type,omitempty"`
209+
Table string `json:"table,omitempty"`
212210
}
213211

214212
type analyzedParam struct {
215213
Number int `json:"number"`
216214
Column analyzedColumn `json:"column"`
217215
}
218216

217+
// analyzedType writes a type as a call expression: a name applied to
218+
// arguments that are other types, integers, booleans or strings, each with
219+
// an optional label, and a nullable flag at whatever depth it applies. An
220+
// array of text is array(text); a nullable column of it has nullable set on
221+
// the array node. Names are recorded as the engine reports them and resolve
222+
// against the catalog afterwards.
223+
type analyzedType struct {
224+
Name string `json:"name"`
225+
Nullable bool `json:"nullable,omitempty"`
226+
Args []analyzedArg `json:"args,omitempty"`
227+
}
228+
229+
type analyzedArg struct {
230+
Label string `json:"label,omitempty"`
231+
Type *analyzedType `json:"type,omitempty"`
232+
Int *int64 `json:"int,omitempty"`
233+
Bool *bool `json:"bool,omitempty"`
234+
String *string `json:"string,omitempty"`
235+
}
236+
219237
func newAnalyzedQuery(q *compiler.Query, includeAST bool) analyzedQuery {
220238
aq := analyzedQuery{
221239
Name: q.Metadata.Name,
@@ -243,13 +261,30 @@ func newAnalyzedColumn(col *compiler.Column) analyzedColumn {
243261
return analyzedColumn{}
244262
}
245263
ac := analyzedColumn{
246-
Name: col.Name,
247-
DataType: col.DataType,
248-
NotNull: col.NotNull,
249-
IsArray: col.IsArray,
264+
Name: col.Name,
265+
Type: newAnalyzedType(col),
250266
}
251267
if col.Table != nil {
252268
ac.Table = col.Table.Name
253269
}
254270
return ac
255271
}
272+
273+
// newAnalyzedType builds the type expression the compiler's flat column
274+
// description amounts to: the data type wrapped in one array node per
275+
// dimension, with the column's nullability on the outermost node.
276+
func newAnalyzedType(col *compiler.Column) *analyzedType {
277+
if col.DataType == "" {
278+
return nil
279+
}
280+
t := &analyzedType{Name: col.DataType}
281+
dims := col.ArrayDims
282+
if col.IsArray && dims == 0 {
283+
dims = 1
284+
}
285+
for i := 0; i < dims; i++ {
286+
t = &analyzedType{Name: "array", Args: []analyzedArg{{Type: t}}}
287+
}
288+
t.Nullable = !col.NotNull
289+
return t
290+
}

internal/endtoend/case_test.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,22 @@ func parseStderr(t *testing.T, dir, testctx string) []byte {
5252
return nil
5353
}
5454

55+
// parseStdout reads the command's expected output: output.json for a
56+
// command that prints JSON, so editors highlight it, otherwise stdout.txt.
5557
func parseStdout(t *testing.T, dir string) []byte {
5658
t.Helper()
57-
path := filepath.Join(dir, "stdout.txt")
58-
if _, err := os.Stat(path); os.IsNotExist(err) {
59-
return nil
60-
}
61-
blob, err := os.ReadFile(path)
62-
if err != nil {
63-
t.Fatal(err)
59+
for _, name := range []string{"output.json", "stdout.txt"} {
60+
path := filepath.Join(dir, name)
61+
if _, err := os.Stat(path); os.IsNotExist(err) {
62+
continue
63+
}
64+
blob, err := os.ReadFile(path)
65+
if err != nil {
66+
t.Fatal(err)
67+
}
68+
return blob
6469
}
65-
return blob
70+
return nil
6671
}
6772

6873
// hasSQLCConfig reports whether dir contains an sqlc configuration file.

internal/endtoend/testdata/analyze_ast/postgresql/stdout.txt renamed to internal/endtoend/testdata/analyze_ast/postgresql/output.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
"columns": [
66
{
77
"name": "name",
8-
"data_type": "text",
9-
"not_null": true,
10-
"is_array": false,
8+
"type": {
9+
"name": "text"
10+
},
1111
"table": "authors"
1212
}
1313
],
@@ -16,9 +16,9 @@
1616
"number": 1,
1717
"column": {
1818
"name": "id",
19-
"data_type": "bigserial",
20-
"not_null": true,
21-
"is_array": false,
19+
"type": {
20+
"name": "bigserial"
21+
},
2222
"table": "authors"
2323
}
2424
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INSERT INTO events (id, name, tag, amount, created) VALUES (1, 'signup', NULL, 9.5, '2024-01-01 00:00:00');

internal/endtoend/testdata/analyze_basic/clickhouse/stdout.txt renamed to internal/endtoend/testdata/analyze_basic/clickhouse/output.json

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,38 @@
55
"columns": [
66
{
77
"name": "id",
8-
"data_type": "uint64",
9-
"not_null": true,
10-
"is_array": false,
8+
"type": {
9+
"name": "uint64"
10+
},
1111
"table": "events"
1212
},
1313
{
1414
"name": "name",
15-
"data_type": "string",
16-
"not_null": true,
17-
"is_array": false,
15+
"type": {
16+
"name": "string"
17+
},
1818
"table": "events"
1919
},
2020
{
2121
"name": "tag",
22-
"data_type": "string",
23-
"not_null": false,
24-
"is_array": false,
22+
"type": {
23+
"name": "string",
24+
"nullable": true
25+
},
2526
"table": "events"
2627
},
2728
{
2829
"name": "amount",
29-
"data_type": "float64",
30-
"not_null": true,
31-
"is_array": false,
30+
"type": {
31+
"name": "float64"
32+
},
3233
"table": "events"
3334
},
3435
{
3536
"name": "created",
36-
"data_type": "datetime",
37-
"not_null": true,
38-
"is_array": false,
37+
"type": {
38+
"name": "datetime"
39+
},
3940
"table": "events"
4041
}
4142
],

internal/endtoend/testdata/analyze_basic/duckdb/stdout.txt renamed to internal/endtoend/testdata/analyze_basic/duckdb/output.json

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,38 @@
55
"columns": [
66
{
77
"name": "id",
8-
"data_type": "bigint",
9-
"not_null": true,
10-
"is_array": false,
8+
"type": {
9+
"name": "bigint"
10+
},
1111
"table": "authors"
1212
},
1313
{
1414
"name": "name",
15-
"data_type": "text",
16-
"not_null": true,
17-
"is_array": false,
15+
"type": {
16+
"name": "text"
17+
},
1818
"table": "authors"
1919
},
2020
{
2121
"name": "bio",
22-
"data_type": "text",
23-
"not_null": false,
24-
"is_array": false,
22+
"type": {
23+
"name": "text",
24+
"nullable": true
25+
},
2526
"table": "authors"
2627
},
2728
{
2829
"name": "royalties",
29-
"data_type": "decimal",
30-
"not_null": true,
31-
"is_array": false,
30+
"type": {
31+
"name": "decimal"
32+
},
3233
"table": "authors"
3334
},
3435
{
3536
"name": "created",
36-
"data_type": "timestamp",
37-
"not_null": true,
38-
"is_array": false,
37+
"type": {
38+
"name": "timestamp"
39+
},
3940
"table": "authors"
4041
}
4142
],

internal/endtoend/testdata/analyze_basic/googlesql/stdout.txt renamed to internal/endtoend/testdata/analyze_basic/googlesql/output.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55
"columns": [
66
{
77
"name": "id",
8-
"data_type": "int64",
9-
"not_null": true,
10-
"is_array": false,
8+
"type": {
9+
"name": "int64"
10+
},
1111
"table": "users"
1212
},
1313
{
1414
"name": "name",
15-
"data_type": "string",
16-
"not_null": true,
17-
"is_array": false,
15+
"type": {
16+
"name": "string"
17+
},
1818
"table": "users"
1919
}
2020
],
@@ -23,9 +23,9 @@
2323
"number": 1,
2424
"column": {
2525
"name": "id",
26-
"data_type": "int64",
27-
"not_null": true,
28-
"is_array": false,
26+
"type": {
27+
"name": "int64"
28+
},
2929
"table": "users"
3030
}
3131
}

0 commit comments

Comments
 (0)