Skip to content

Commit 3c2546a

Browse files
authored
Check analyze cases against a real ClickHouse in goldeneye, and make sqlc agree with it (#4603)
1 parent 23e357a commit 3c2546a

80 files changed

Lines changed: 4785 additions & 850 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: 11 additions & 6 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 `stdout.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
@@ -147,8 +147,11 @@ the run early. Run a subset to get past one (`-run 'TestReplay/core/^select'`).
147147

148148
The dialect seeds under `/internal/engine/<engine>/dialect/` are generated
149149
from a live database by `/internal/goldeneye`, a nested module, and its tests
150-
verify the committed files against one byte for byte. Engines whose database
151-
is not available skip.
150+
verify the committed files against one byte for byte. The same module checks
151+
the `analyze_*` cases under `/internal/endtoend/testdata/` against what the
152+
database itself reports for them, so a `fixture.sql` next to a case's schema
153+
gives the queries rows to run against. Engines whose database is not
154+
available skip.
152155

153156
```bash
154157
cd internal/goldeneye
@@ -240,8 +243,10 @@ MYSQL_SERVER_URI="root:mysecretpassword@tcp(127.0.0.1:3306)/mysql?multiStatement
240243
JSONL read by `/internal/core/seed`; the generated parts come from
241244
`/internal/goldeneye`
242245
- `/internal/goldeneye/` - Nested module that generates the dialect seeds
243-
under `/internal/engine/<engine>/dialect/` from a live database and checks
244-
the committed ones against it, one package per engine; see its README
246+
under `/internal/engine/<engine>/dialect/` from a live database, checks
247+
the committed ones against it, and checks the analyze cases under
248+
`/internal/endtoend/testdata/` against what the database reports, one
249+
package per engine; see its README
245250
- `/internal/core/` - The analysis core: catalog, analyzer and dialect seeds
246251
- `/internal/compiler/` - Query compilation logic
247252
- `/internal/codegen/` - Code generation for different languages

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: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/sqlc-dev/sqlc/internal/compiler"
1313
"github.com/sqlc-dev/sqlc/internal/config"
14+
"github.com/sqlc-dev/sqlc/internal/core"
1415
"github.com/sqlc-dev/sqlc/internal/multierr"
1516
"github.com/sqlc-dev/sqlc/internal/opts"
1617
"github.com/sqlc-dev/sqlc/internal/sql/ast"
@@ -204,11 +205,9 @@ type analyzedQuery struct {
204205
}
205206

206207
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"`
208+
Name string `json:"name"`
209+
Type *core.TypeExpr `json:"type,omitempty"`
210+
Table string `json:"table,omitempty"`
212211
}
213212

214213
type analyzedParam struct {
@@ -243,13 +242,34 @@ func newAnalyzedColumn(col *compiler.Column) analyzedColumn {
243242
return analyzedColumn{}
244243
}
245244
ac := analyzedColumn{
246-
Name: col.Name,
247-
DataType: col.DataType,
248-
NotNull: col.NotNull,
249-
IsArray: col.IsArray,
245+
Name: col.Name,
246+
Type: newAnalyzedType(col),
250247
}
251248
if col.Table != nil {
252249
ac.Table = col.Table.Name
253250
}
254251
return ac
255252
}
253+
254+
// newAnalyzedType is the column's type as an expression: the one the
255+
// analysis core wrote when it did, otherwise the flat description the
256+
// compiler holds, which is the data type wrapped in one array node per
257+
// dimension with the column's nullability on the outermost node.
258+
func newAnalyzedType(col *compiler.Column) *core.TypeExpr {
259+
if col.TypeExpr != nil {
260+
return col.TypeExpr
261+
}
262+
if col.DataType == "" {
263+
return nil
264+
}
265+
t := core.ParseTypeExpr(col.DataType)
266+
dims := col.ArrayDims
267+
if col.IsArray && dims == 0 {
268+
dims = 1
269+
}
270+
for i := 0; i < dims; i++ {
271+
t = &core.TypeExpr{Name: "array", Args: []core.TypeArg{{Type: t}}}
272+
}
273+
t.Nullable = !col.NotNull
274+
return t
275+
}

internal/compiler/parse_core.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ func coreColumn(c core.Column) *Column {
105105
DataType: c.DataType,
106106
NotNull: c.NotNull,
107107
IsArray: c.IsArray,
108+
TypeExpr: c.Type,
108109
}
109110
// The core reports arrays without dimensions, and codegen renders one
110111
// "[]" per dimension.
@@ -129,6 +130,7 @@ func coreParamColumn(p core.Parameter, params *named.ParamSet) *Column {
129130
DataType: p.DataType,
130131
NotNull: p.NotNull,
131132
IsArray: p.IsArray,
133+
TypeExpr: p.Type,
132134
}
133135
if p.IsArray {
134136
col.ArrayDims = 1

internal/compiler/query.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package compiler
22

33
import (
4+
"github.com/sqlc-dev/sqlc/internal/core"
45
"github.com/sqlc-dev/sqlc/internal/metadata"
56
"github.com/sqlc-dev/sqlc/internal/sql/ast"
67
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
@@ -37,6 +38,11 @@ type Column struct {
3738
Type *ast.TypeName
3839
EmbedTable *ast.TableName
3940

41+
// TypeExpr is the type as the analysis core wrote it, with the
42+
// arguments and nesting DataType and IsArray flatten away. It is unset
43+
// on the legacy path.
44+
TypeExpr *core.TypeExpr
45+
4046
IsSqlcSlice bool // is this sqlc.slice()
4147

4248
skipTableRequiredCheck bool

internal/core/analysis.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,11 @@ type ColumnSource struct {
5353
}
5454

5555
type Column struct {
56-
Name string `json:"name"`
57-
DataType string `json:"data_type"`
56+
Name string `json:"name"`
57+
DataType string `json:"data_type"`
58+
// Type is the column's type as an expression, carrying what DataType
59+
// and IsArray flatten away: arguments, nesting and inner nullability.
60+
Type *TypeExpr `json:"type,omitempty"`
5861
TypeOID int64 `json:"type_oid,omitempty"`
5962
NotNull bool `json:"not_null"`
6063
IsArray bool `json:"is_array,omitempty"`
@@ -73,6 +76,7 @@ type Parameter struct {
7376
Number int `json:"number"`
7477
Name string `json:"name,omitempty"`
7578
DataType string `json:"data_type,omitempty"`
79+
Type *TypeExpr `json:"type,omitempty"`
7680
TypeOID int64 `json:"type_oid,omitempty"`
7781
NotNull bool `json:"not_null"`
7882
IsArray bool `json:"is_array,omitempty"`

internal/core/analyzer/analyzer.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,20 @@ func derivedRel(alias string, cols []core.Column) scopeRel {
119119
}
120120

121121
func (a *analyzer) result() core.PrepareResult {
122+
// A placeholder nothing constrained takes the dialect's type for one, when
123+
// the dialect has such a type.
124+
if oid, ok := a.cat.UntypedTypeOID(); ok {
125+
for n, p := range a.params {
126+
if p.TypeOID == 0 && p.DataType == "" {
127+
t := exprType{typeOID: oid, nullable: true}
128+
p.TypeOID = oid
129+
p.DataType, p.IsArray = a.typeNameOf(t)
130+
p.NotNull = false
131+
p.Type = a.typeExprOf(t, "")
132+
a.params[n] = p
133+
}
134+
}
135+
}
122136
res := core.PrepareResult{
123137
Command: a.command,
124138
Columns: a.columns,
@@ -213,9 +227,39 @@ func (a *analyzer) analyzeSelect(s *ast.SelectStmt) error {
213227
return err
214228
}
215229
}
230+
for _, item := range listItems(s.SortClause) {
231+
if sb, ok := item.(*ast.SortBy); ok {
232+
if _, err := a.typeExpr(sb.Node); err != nil {
233+
return fmt.Errorf("order by: %w", err)
234+
}
235+
}
236+
}
237+
for _, n := range []ast.Node{s.LimitCount, s.LimitOffset} {
238+
if err := a.typeLimit(n); err != nil {
239+
return fmt.Errorf("limit: %w", err)
240+
}
241+
}
216242
return nil
217243
}
218244

245+
// typeLimit types a LIMIT or OFFSET count. A bare placeholder there holds
246+
// whatever the dialect counts rows in.
247+
func (a *analyzer) typeLimit(n ast.Node) error {
248+
if n == nil {
249+
return nil
250+
}
251+
if pr, ok := n.(*ast.ParamRef); ok {
252+
oid, err := a.cat.LimitTypeOID()
253+
if err != nil {
254+
return err
255+
}
256+
a.inferParam(pr.Number, exprType{typeOID: oid})
257+
return nil
258+
}
259+
_, err := a.typeExpr(n)
260+
return err
261+
}
262+
219263
func (a *analyzer) typeValuesLists(l *ast.List) error {
220264
for _, row := range listItems(l) {
221265
if _, err := a.typeExpr(row); err != nil {

0 commit comments

Comments
 (0)