Skip to content

Commit 071254e

Browse files
kyleconroyclaude
andcommitted
clickhouse: analyze queries through the core catalog + analyzer
Wire ClickHouse onto the merged core: a dialect seed registering the built-in ClickHouse types, and a DDL handler that populates the core catalog from CREATE TABLE using sqlc's existing ClickHouse parser. A smoke test proves the full vertical path — ClickHouse SQL -> sqlc's ClickHouse parser -> internal/sql/ast -> core catalog + analyzer -> PrepareResult — resolving column names, types, nullability, source bindings, and star expansion, with none of the legacy compiler analyze step involved. Also fix the ClickHouse converter to render nested type parameters (the inner type of Nullable(T)/Array(T), Decimal precision, etc.) into TypeName.Name instead of dropping them as TODO nodes, so wrapped types resolve to their effective scalar type. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTGxNHW6v1S1YyC9FDSgrK
1 parent d2837b7 commit 071254e

4 files changed

Lines changed: 450 additions & 8 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package clickhouse_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/sqlc-dev/sqlc/internal/core"
8+
"github.com/sqlc-dev/sqlc/internal/core/analyzer"
9+
"github.com/sqlc-dev/sqlc/internal/engine/clickhouse"
10+
)
11+
12+
// analyzeOne seeds a ClickHouse-dialect catalog, applies ddl, then runs
13+
// the dialect-neutral core analyzer over the trailing query. It proves
14+
// the full ClickHouse vertical path: ClickHouse SQL -> sqlc's ClickHouse
15+
// parser -> internal/sql/ast -> core catalog + analyzer -> PrepareResult,
16+
// with no legacy compiler analyze step involved.
17+
func analyzeOne(t *testing.T, ddl, query string) core.PrepareResult {
18+
t.Helper()
19+
cat, err := core.New(clickhouse.Dialect())
20+
if err != nil {
21+
t.Fatal(err)
22+
}
23+
t.Cleanup(func() { cat.Close() })
24+
25+
if err := clickhouse.LoadSchema(cat, ddl); err != nil {
26+
t.Fatalf("load schema: %v", err)
27+
}
28+
29+
stmts, err := clickhouse.NewParser().Parse(strings.NewReader(query))
30+
if err != nil {
31+
t.Fatalf("parse query: %v", err)
32+
}
33+
if len(stmts) != 1 {
34+
t.Fatalf("expected 1 stmt, got %d", len(stmts))
35+
}
36+
res, err := analyzer.Prepare(cat, stmts[0].Raw)
37+
if err != nil {
38+
t.Fatalf("analyze: %v", err)
39+
}
40+
return res
41+
}
42+
43+
func colByName(res core.PrepareResult, name string) (core.Column, bool) {
44+
for _, c := range res.Columns {
45+
if c.Name == name {
46+
return c, true
47+
}
48+
}
49+
return core.Column{}, false
50+
}
51+
52+
const eventsDDL = `
53+
CREATE TABLE events (
54+
id UInt64,
55+
name String,
56+
tag Nullable(String),
57+
amount Decimal(18, 4)
58+
) ENGINE = MergeTree ORDER BY id
59+
`
60+
61+
func TestClickHouseSelectColumns(t *testing.T) {
62+
res := analyzeOne(t, eventsDDL, `SELECT id, name, tag FROM events`)
63+
64+
if len(res.Columns) != 3 {
65+
t.Fatalf("got %d cols, want 3: %+v", len(res.Columns), res.Columns)
66+
}
67+
68+
id, ok := colByName(res, "id")
69+
if !ok || id.DataType != "uint64" || !id.NotNull {
70+
t.Errorf("id: %+v (ok=%v)", id, ok)
71+
}
72+
name, ok := colByName(res, "name")
73+
if !ok || name.DataType != "string" || !name.NotNull {
74+
t.Errorf("name: %+v (ok=%v)", name, ok)
75+
}
76+
// Nullable(String) must resolve to the inner scalar and be nullable.
77+
tag, ok := colByName(res, "tag")
78+
if !ok || tag.DataType != "string" || tag.NotNull {
79+
t.Errorf("tag: want string/nullable, got %+v (ok=%v)", tag, ok)
80+
}
81+
82+
for _, c := range res.Columns {
83+
if c.SourceClassOID == 0 || c.SourceAttributeOID == 0 {
84+
t.Errorf("col %s missing source binding: %+v", c.Name, c)
85+
}
86+
}
87+
}
88+
89+
func TestClickHouseSelectStar(t *testing.T) {
90+
res := analyzeOne(t, eventsDDL, `SELECT * FROM events`)
91+
if len(res.Columns) != 4 {
92+
t.Fatalf("got %d cols, want 4: %+v", len(res.Columns), res.Columns)
93+
}
94+
want := []string{"id", "name", "tag", "amount"}
95+
for i, w := range want {
96+
if res.Columns[i].Name != w {
97+
t.Errorf("col %d: got %q, want %q", i, res.Columns[i].Name, w)
98+
}
99+
}
100+
}

internal/engine/clickhouse/convert.go

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

33
import (
4+
"fmt"
45
"strconv"
56
"strings"
67

@@ -825,15 +826,14 @@ func (c *cc) convertColumnDeclaration(n *chast.ColumnDeclaration) *ast.ColumnDef
825826
}
826827

827828
if n.Type != nil {
829+
// Render the full type text (including nested type parameters such
830+
// as the inner type of Nullable(T) / Array(T) and the precision of
831+
// Decimal(P, S)) into Name so downstream consumers can recover the
832+
// complete declaration. Nested type parameters are themselves
833+
// *chast.DataType nodes, which the generic expression converter
834+
// cannot represent, so they are rendered here instead.
828835
colDef.TypeName = &ast.TypeName{
829-
Name: n.Type.Name,
830-
}
831-
// Handle type parameters (e.g., Decimal(10, 2))
832-
if len(n.Type.Parameters) > 0 {
833-
colDef.TypeName.Typmods = &ast.List{}
834-
for _, param := range n.Type.Parameters {
835-
colDef.TypeName.Typmods.Items = append(colDef.TypeName.Typmods.Items, c.convertExpr(param))
836-
}
836+
Name: renderDataType(n.Type),
837837
}
838838
}
839839

@@ -855,6 +855,42 @@ func (c *cc) convertColumnDeclaration(n *chast.ColumnDeclaration) *ast.ColumnDef
855855
return colDef
856856
}
857857

858+
// renderDataType renders a ClickHouse type node back to its canonical
859+
// textual form, e.g. Nullable(String), Array(UInt64), Decimal(18, 4), or
860+
// Map(String, Array(Nullable(UInt32))). Nested type parameters recurse.
861+
func renderDataType(dt *chast.DataType) string {
862+
if dt == nil {
863+
return ""
864+
}
865+
if len(dt.Parameters) == 0 {
866+
return dt.Name
867+
}
868+
parts := make([]string, 0, len(dt.Parameters))
869+
for _, p := range dt.Parameters {
870+
parts = append(parts, renderTypeParam(p))
871+
}
872+
return dt.Name + "(" + strings.Join(parts, ", ") + ")"
873+
}
874+
875+
// renderTypeParam renders a single type parameter: a nested type, a
876+
// numeric/string literal (Decimal precision, FixedString length, Enum
877+
// value), or an identifier.
878+
func renderTypeParam(e chast.Expression) string {
879+
switch v := e.(type) {
880+
case *chast.DataType:
881+
return renderDataType(v)
882+
case *chast.Literal:
883+
if v.Source != "" {
884+
return v.Source
885+
}
886+
return fmt.Sprintf("%v", v.Value)
887+
case *chast.Identifier:
888+
return strings.Join(v.Parts, ".")
889+
default:
890+
return ""
891+
}
892+
}
893+
858894
func (c *cc) convertUpdateQuery(n *chast.UpdateQuery) *ast.UpdateStmt {
859895
rv := &ast.RangeVar{
860896
Relname: &n.Table,

0 commit comments

Comments
 (0)