Skip to content

Commit 78d7394

Browse files
committed
Make sqlc's ClickHouse analysis agree with ClickHouse
testcheck showed sqlc disagreeing with ClickHouse on every analyze case that went beyond plain column references. This makes the six ClickHouse cases match the database byte for byte, and moves the testcheck command under cmd/testcheck. Types are carried as expressions. The ClickHouse converter keeps a column's full spelling in the type name's Spelling, the schema stores it as the attribute's declared type, and the analysis core writes each column and parameter a TypeExpr from it, so Array(Nullable(String)), Map(String, Nullable(UInt8)), Tuple(lat Float64, lon Float64) and Decimal(10, 2) survive intact. The analyze command prints that expression when the core produced one. Functions are typed. A ClickHouse function seed of 581 signatures replaces the single count() entry, with "$n" naming the type of the nth argument and never_null marking results that stay non-null. The dialect declares that functions propagate nullability, that comparisons yield UInt8 while true is Bool, that LIMIT counts are UInt64, and that an unconstrained placeholder is Nothing. The analyzer scores overloads, types ORDER BY, LIMIT and OFFSET, follows IN (SELECT ...) into the subquery, makes COALESCE null only when every argument is, and names a placeholder compared with a function call after the function. The converter gives unaliased expression columns ClickHouse's own names, such as sum(amount) and plus(id, 1), captures aliases on every node kind, converts a scalar subquery as a value rather than EXISTS, converts coalesce and ifNull to COALESCE, handles WITH elements as the parser produces them, and counts positions from zero so star expansion and parameter renumbering line up. ClickHouse joins the preprocessed engines, so sqlc.arg() and sqlc.narg() work with ? binding. A dialect that names the second id of a join e.id says so in its seed and the analyzer qualifies such columns. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6XkyWnx7iJFEb8q3AnYps
1 parent 42907fd commit 78d7394

27 files changed

Lines changed: 1964 additions & 202 deletions

File tree

internal/cmd/analyze.go

Lines changed: 14 additions & 29 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,36 +205,16 @@ type analyzedQuery struct {
204205
}
205206

206207
type analyzedColumn struct {
207-
Name string `json:"name"`
208-
Type *analyzedType `json:"type,omitempty"`
209-
Table string `json:"table,omitempty"`
208+
Name string `json:"name"`
209+
Type *core.TypeExpr `json:"type,omitempty"`
210+
Table string `json:"table,omitempty"`
210211
}
211212

212213
type analyzedParam struct {
213214
Number int `json:"number"`
214215
Column analyzedColumn `json:"column"`
215216
}
216217

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-
237218
func newAnalyzedQuery(q *compiler.Query, includeAST bool) analyzedQuery {
238219
aq := analyzedQuery{
239220
Name: q.Metadata.Name,
@@ -270,20 +251,24 @@ func newAnalyzedColumn(col *compiler.Column) analyzedColumn {
270251
return ac
271252
}
272253

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 {
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+
}
277262
if col.DataType == "" {
278263
return nil
279264
}
280-
t := &analyzedType{Name: col.DataType}
265+
t := core.ParseTypeExpr(col.DataType)
281266
dims := col.ArrayDims
282267
if col.IsArray && dims == 0 {
283268
dims = 1
284269
}
285270
for i := 0; i < dims; i++ {
286-
t = &analyzedType{Name: "array", Args: []analyzedArg{{Type: t}}}
271+
t = &core.TypeExpr{Name: "array", Args: []core.TypeArg{{Type: t}}}
287272
}
288273
t.Nullable = !col.NotNull
289274
return t

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)