Skip to content

Commit fda4d41

Browse files
kyleconroyclaude
andcommitted
clickhouse: wire end-to-end sqlc generate onto the core
Add the EngineClickHouse engine and a dedicated compile path so `sqlc generate` produces Go for ClickHouse entirely through the xqlc core, bypassing the legacy compiler analyze step and the in-memory sql/catalog: - config: add the "clickhouse" engine constant. - compiler: NewCompiler builds a core.Catalog seeded with the ClickHouse dialect; parseCatalog applies schema DDL to it; a new parseQueryCore resolves each query's columns and parameters via core/analyzer and assembles *compiler.Query, reusing only the shared query-metadata parsing. The legacy analyzeQuery/inferQuery/outputColumns path and the analyzer.Analyzer seam are never entered. - codegen: project the core catalog into plugin.Catalog for model/enum generation, and add a ClickHouse -> Go type map (Nullable(T) -> *T, the integer ladder, Float32/64, String, DateTime -> time.Time, ...). - clickhouse parser: compute statement byte-spans with a running offset and a semicolon scan (doubleclick reports statement starts but not ends), so leading "-- name:" annotations fall inside each statement. An endtoend case (clickhouse_select) exercises the full pipeline and its golden Go output is committed. Updating parse_basic/clickhouse's golden reflects the corrected statement spans and now-detected query name/cmd. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTGxNHW6v1S1YyC9FDSgrK
1 parent 071254e commit fda4d41

21 files changed

Lines changed: 577 additions & 62 deletions

File tree

internal/cmd/shim.go

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"github.com/sqlc-dev/sqlc/internal/compiler"
55
"github.com/sqlc-dev/sqlc/internal/config"
66
"github.com/sqlc-dev/sqlc/internal/config/convert"
7+
"github.com/sqlc-dev/sqlc/internal/core"
78
"github.com/sqlc-dev/sqlc/internal/info"
89
"github.com/sqlc-dev/sqlc/internal/plugin"
910
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
@@ -224,10 +225,90 @@ func pluginQueryParam(p compiler.Parameter) *plugin.Parameter {
224225
}
225226

226227
func codeGenRequest(r *compiler.Result, settings config.CombinedSettings) *plugin.GenerateRequest {
228+
// Engines on the xqlc core (ClickHouse) project the codegen catalog
229+
// from the core catalog rather than the in-memory sql/catalog, which is
230+
// nil on that path.
231+
var cat *plugin.Catalog
232+
if r.CoreCatalog != nil {
233+
cat = pluginCatalogFromCore(r.CoreCatalog)
234+
} else {
235+
cat = pluginCatalog(r.Catalog)
236+
}
227237
return &plugin.GenerateRequest{
228238
Settings: pluginSettings(r, settings),
229-
Catalog: pluginCatalog(r.Catalog),
239+
Catalog: cat,
230240
Queries: pluginQueries(r),
231241
SqlcVersion: info.Version,
232242
}
233243
}
244+
245+
// pluginCatalogFromCore projects a core.Catalog (the xqlc SQLite-backed
246+
// catalog) into the plugin.Catalog that codegen consumes to emit models
247+
// and enums. It reads the namespace / class / attribute / type tables
248+
// directly. Projection is best-effort: an unexpected query error against
249+
// the in-memory catalog yields a partial catalog rather than aborting
250+
// generation.
251+
func pluginCatalogFromCore(cc *core.Catalog) *plugin.Catalog {
252+
db := cc.DB()
253+
var schemas []*plugin.Schema
254+
255+
type row struct {
256+
oid int64
257+
name string
258+
}
259+
readRows := func(query string, args ...any) []row {
260+
rows, err := db.Query(query, args...)
261+
if err != nil {
262+
return nil
263+
}
264+
defer rows.Close()
265+
var out []row
266+
for rows.Next() {
267+
var r row
268+
if err := rows.Scan(&r.oid, &r.name); err != nil {
269+
return out
270+
}
271+
out = append(out, r)
272+
}
273+
return out
274+
}
275+
276+
for _, ns := range readRows(`SELECT oid, name FROM sql_namespace ORDER BY oid`) {
277+
var tables []*plugin.Table
278+
for _, cl := range readRows(
279+
`SELECT oid, name FROM sql_class WHERE namespace_oid = ? AND kind = 'r' ORDER BY oid`, ns.oid,
280+
) {
281+
rel := &plugin.Identifier{Schema: ns.name, Name: cl.name}
282+
var columns []*plugin.Column
283+
crows, err := db.Query(
284+
`SELECT a.name, t.name, a.not_null
285+
FROM sql_attribute a JOIN sql_type t ON t.oid = a.type_oid
286+
WHERE a.class_oid = ? ORDER BY a.num`, cl.oid,
287+
)
288+
if err != nil {
289+
continue
290+
}
291+
for crows.Next() {
292+
var name, typeName string
293+
var notNull int
294+
if err := crows.Scan(&name, &typeName, &notNull); err != nil {
295+
break
296+
}
297+
columns = append(columns, &plugin.Column{
298+
Name: name,
299+
Type: &plugin.Identifier{Name: typeName},
300+
NotNull: notNull != 0,
301+
Table: rel,
302+
})
303+
}
304+
crows.Close()
305+
tables = append(tables, &plugin.Table{Rel: rel, Columns: columns})
306+
}
307+
schemas = append(schemas, &plugin.Schema{Name: ns.name, Tables: tables})
308+
}
309+
310+
return &plugin.Catalog{
311+
DefaultSchema: "public",
312+
Schemas: schemas,
313+
}
314+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package golang
2+
3+
import (
4+
"strings"
5+
6+
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
7+
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
8+
"github.com/sqlc-dev/sqlc/internal/plugin"
9+
)
10+
11+
// clickhouseType maps a ClickHouse column type to a Go type. Type names
12+
// arrive lower-cased from the core catalog (e.g. "uint64", "string",
13+
// "datetime"). Nullable columns (NotNull == false) map to a pointer, which
14+
// is how the clickhouse-go driver represents Nullable(T).
15+
func clickhouseType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
16+
dt := strings.ToLower(sdk.DataType(col.Type))
17+
notNull := col.NotNull
18+
19+
switch dt {
20+
case "uint8":
21+
return nullable(notNull, "uint8")
22+
case "uint16":
23+
return nullable(notNull, "uint16")
24+
case "uint32":
25+
return nullable(notNull, "uint32")
26+
case "uint64":
27+
return nullable(notNull, "uint64")
28+
case "int8":
29+
return nullable(notNull, "int8")
30+
case "int16":
31+
return nullable(notNull, "int16")
32+
case "int32":
33+
return nullable(notNull, "int32")
34+
case "int64":
35+
return nullable(notNull, "int64")
36+
case "uint128", "uint256", "int128", "int256":
37+
// Big integers are represented as *big.Int by clickhouse-go.
38+
return "*big.Int"
39+
case "float32", "bfloat16":
40+
return nullable(notNull, "float32")
41+
case "float64":
42+
return nullable(notNull, "float64")
43+
case "bool":
44+
return nullable(notNull, "bool")
45+
case "string", "fixedstring":
46+
return nullable(notNull, "string")
47+
case "date", "date32", "datetime", "datetime64":
48+
return nullable(notNull, "time.Time")
49+
50+
// The following resolve to string for now; richer mappings
51+
// (decimal.Decimal, uuid.UUID, netip.Addr, json.RawMessage) require
52+
// wiring their imports into the Go importer and are a follow-up.
53+
case "decimal", "decimal32", "decimal64", "decimal128", "decimal256",
54+
"uuid", "ipv4", "ipv6", "json", "enum8", "enum16":
55+
return nullable(notNull, "string")
56+
57+
default:
58+
return "interface{}"
59+
}
60+
}
61+
62+
// nullable wraps a base Go type in a pointer when the column is nullable.
63+
func nullable(notNull bool, base string) string {
64+
if notNull {
65+
return base
66+
}
67+
return "*" + base
68+
}

internal/codegen/golang/go_type.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ func goInnerType(req *plugin.GenerateRequest, options *opts.Options, col *plugin
8686
return postgresType(req, options, col)
8787
case "sqlite":
8888
return sqliteType(req, options, col)
89+
case "clickhouse":
90+
return clickhouseType(req, options, col)
8991
default:
9092
return "interface{}"
9193
}

internal/compiler/compile.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"path/filepath"
1010
"strings"
1111

12+
"github.com/sqlc-dev/sqlc/internal/engine/clickhouse"
1213
"github.com/sqlc-dev/sqlc/internal/migrations"
1314
"github.com/sqlc-dev/sqlc/internal/multierr"
1415
"github.com/sqlc-dev/sqlc/internal/opts"
@@ -55,6 +56,18 @@ func (c *Compiler) parseCatalog(schemas []string) error {
5556
continue
5657
}
5758

59+
// ClickHouse populates the core catalog instead of the in-memory
60+
// sql/catalog.
61+
if c.coreCatalog != nil {
62+
for i := range stmts {
63+
if err := clickhouse.Apply(c.coreCatalog, stmts[i].Raw); err != nil {
64+
merr.Add(filename, contents, stmts[i].Pos(), err)
65+
continue
66+
}
67+
}
68+
continue
69+
}
70+
5871
for i := range stmts {
5972
if err := c.catalog.Update(stmts[i], c); err != nil {
6073
merr.Add(filename, contents, stmts[i].Pos(), err)
@@ -135,7 +148,8 @@ func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
135148
}
136149

137150
return &Result{
138-
Catalog: c.catalog,
139-
Queries: q,
151+
Catalog: c.catalog,
152+
CoreCatalog: c.coreCatalog,
153+
Queries: q,
140154
}, nil
141155
}

internal/compiler/engine.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66

77
"github.com/sqlc-dev/sqlc/internal/analyzer"
88
"github.com/sqlc-dev/sqlc/internal/config"
9+
"github.com/sqlc-dev/sqlc/internal/core"
910
"github.com/sqlc-dev/sqlc/internal/dbmanager"
11+
"github.com/sqlc-dev/sqlc/internal/engine/clickhouse"
1012
"github.com/sqlc-dev/sqlc/internal/engine/dolphin"
1113
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
1214
pganalyze "github.com/sqlc-dev/sqlc/internal/engine/postgresql/analyzer"
@@ -27,6 +29,11 @@ type Compiler struct {
2729
client dbmanager.Client
2830
selector selector
2931

32+
// coreCatalog is the xqlc-derived catalog used by engines whose
33+
// analysis runs on the core analyzer (currently ClickHouse) instead of
34+
// the legacy compiler analyze step. It is nil for other engines.
35+
coreCatalog *core.Catalog
36+
3037
schema []string
3138

3239
// databaseOnlyMode indicates that the compiler should use database-only analysis
@@ -111,6 +118,17 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts
111118
)
112119
}
113120
}
121+
case config.EngineClickHouse:
122+
// ClickHouse runs on the xqlc analysis core: its schema and queries
123+
// are resolved against a core.Catalog by the core analyzer, not the
124+
// legacy compiler analyze step or the in-memory sql/catalog.
125+
c.parser = clickhouse.NewParser()
126+
c.selector = newDefaultSelector()
127+
cat, err := core.New(clickhouse.Dialect())
128+
if err != nil {
129+
return nil, fmt.Errorf("clickhouse: init catalog: %w", err)
130+
}
131+
c.coreCatalog = cat
114132
default:
115133
return nil, fmt.Errorf("unknown engine: %s", conf.Engine)
116134
}
@@ -145,4 +163,7 @@ func (c *Compiler) Close(ctx context.Context) {
145163
if c.client != nil {
146164
c.client.Close(ctx)
147165
}
166+
if c.coreCatalog != nil {
167+
c.coreCatalog.Close()
168+
}
148169
}

internal/compiler/parse.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ import (
1919
var debugDumpAST = sqlcdebug.New("dumpast")
2020

2121
func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query, error) {
22+
// ClickHouse resolves types through the core analyzer, entirely
23+
// bypassing the legacy analyze step below.
24+
if c.coreCatalog != nil {
25+
return c.parseQueryCore(stmt, src)
26+
}
27+
2228
ctx := context.Background()
2329

2430
if debugDumpAST.Value() == "1" {

0 commit comments

Comments
 (0)