Skip to content

Commit 6f636ce

Browse files
committed
feat(clickhouse): add ClickHouse database engine support
Add initial ClickHouse support with the following components: - Parser using sqlc-dev/doubleclick to parse ClickHouse SQL - Analyzer for database-only mode (requires live database connection) - Go codegen with ClickHouse type mappings - Docker and native sqltest support - End-to-end test case with authors table example The implementation requires the clickhouse experiment flag (SQLCEXPERIMENT=clickhouse) and database-only analyzer mode (analyzer.database: only) since static analysis is not yet supported. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent ba513e7 commit 6f636ce

File tree

20 files changed

+1940
-28
lines changed

20 files changed

+1940
-28
lines changed

docker-compose.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,14 @@ services:
1919
POSTGRES_DB: postgres
2020
POSTGRES_PASSWORD: mysecretpassword
2121
POSTGRES_USER: postgres
22+
23+
clickhouse:
24+
image: "clickhouse/clickhouse-server:latest"
25+
ports:
26+
- "9000:9000"
27+
- "8123:8123"
28+
restart: always
29+
environment:
30+
CLICKHOUSE_DB: default
31+
CLICKHOUSE_USER: default
32+
CLICKHOUSE_PASSWORD: mysecretpassword

go.mod

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
module github.com/sqlc-dev/sqlc
22

3-
go 1.24.0
4-
5-
toolchain go1.24.1
3+
go 1.24.7
64

75
require (
6+
github.com/ClickHouse/clickhouse-go/v2 v2.42.0
87
github.com/antlr4-go/antlr/v4 v4.13.1
98
github.com/cubicdaiya/gonp v1.0.4
109
github.com/davecgh/go-spew v1.1.1
@@ -22,6 +21,7 @@ require (
2221
github.com/riza-io/grpc-go v0.2.0
2322
github.com/spf13/cobra v1.10.2
2423
github.com/spf13/pflag v1.0.10
24+
github.com/sqlc-dev/doubleclick v0.0.0-20251223195122-0076eee94506
2525
github.com/tetratelabs/wazero v1.10.1
2626
github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07
2727
github.com/xeipuuv/gojsonschema v1.2.0
@@ -34,6 +34,12 @@ require (
3434
require (
3535
cel.dev/expr v0.24.0 // indirect
3636
filippo.io/edwards25519 v1.1.0 // indirect
37+
github.com/ClickHouse/ch-go v0.69.0 // indirect
38+
github.com/andybalholm/brotli v1.2.0 // indirect
39+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
40+
github.com/go-faster/city v1.0.1 // indirect
41+
github.com/go-faster/errors v0.7.1 // indirect
42+
github.com/google/uuid v1.6.0 // indirect
3743
github.com/inconshreveable/mousetrap v1.1.0 // indirect
3844
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
3945
github.com/jackc/pgconn v1.14.3 // indirect
@@ -43,23 +49,31 @@ require (
4349
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
4450
github.com/jackc/pgtype v1.14.0 // indirect
4551
github.com/jackc/puddle/v2 v2.2.2 // indirect
52+
github.com/klauspost/compress v1.18.0 // indirect
4653
github.com/ncruces/julianday v1.0.0 // indirect
54+
github.com/paulmach/orb v0.12.0 // indirect
55+
github.com/pierrec/lz4/v4 v4.1.22 // indirect
4756
github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb // indirect
4857
github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
4958
github.com/pingcap/log v1.1.0 // indirect
5059
github.com/rogpeppe/go-internal v1.10.0 // indirect
60+
github.com/segmentio/asm v1.2.1 // indirect
61+
github.com/shopspring/decimal v1.4.0 // indirect
5162
github.com/stoewer/go-strcase v1.2.0 // indirect
5263
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect
5364
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
5465
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
66+
go.opentelemetry.io/otel v1.39.0 // indirect
67+
go.opentelemetry.io/otel/trace v1.39.0 // indirect
5568
go.uber.org/atomic v1.11.0 // indirect
5669
go.uber.org/multierr v1.11.0 // indirect
5770
go.uber.org/zap v1.27.0 // indirect
58-
golang.org/x/crypto v0.45.0 // indirect
71+
go.yaml.in/yaml/v3 v3.0.4 // indirect
72+
golang.org/x/crypto v0.46.0 // indirect
5973
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
60-
golang.org/x/net v0.47.0 // indirect
61-
golang.org/x/sys v0.38.0 // indirect
62-
golang.org/x/text v0.31.0 // indirect
74+
golang.org/x/net v0.48.0 // indirect
75+
golang.org/x/sys v0.39.0 // indirect
76+
golang.org/x/text v0.32.0 // indirect
6377
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
6478
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
6579
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect

go.sum

Lines changed: 75 additions & 17 deletions
Large diffs are not rendered by default.
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
package golang
2+
3+
import (
4+
"log"
5+
"strings"
6+
7+
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
8+
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
9+
"github.com/sqlc-dev/sqlc/internal/debug"
10+
"github.com/sqlc-dev/sqlc/internal/plugin"
11+
)
12+
13+
func clickhouseType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
14+
dt := strings.ToLower(sdk.DataType(col.Type))
15+
notNull := col.NotNull || col.IsArray
16+
emitPointersForNull := options.EmitPointersForNullTypes
17+
18+
// Handle Nullable wrapper
19+
if strings.HasPrefix(dt, "nullable(") && strings.HasSuffix(dt, ")") {
20+
dt = dt[9 : len(dt)-1]
21+
notNull = false
22+
}
23+
24+
// Handle LowCardinality wrapper
25+
if strings.HasPrefix(dt, "lowcardinality(") && strings.HasSuffix(dt, ")") {
26+
dt = dt[15 : len(dt)-1]
27+
}
28+
29+
switch dt {
30+
// Integer types
31+
case "int8":
32+
if notNull {
33+
return "int8"
34+
}
35+
if emitPointersForNull {
36+
return "*int8"
37+
}
38+
return "sql.NullInt16" // No sql.NullInt8, use Int16
39+
40+
case "int16":
41+
if notNull {
42+
return "int16"
43+
}
44+
if emitPointersForNull {
45+
return "*int16"
46+
}
47+
return "sql.NullInt16"
48+
49+
case "int32":
50+
if notNull {
51+
return "int32"
52+
}
53+
if emitPointersForNull {
54+
return "*int32"
55+
}
56+
return "sql.NullInt32"
57+
58+
case "int64":
59+
if notNull {
60+
return "int64"
61+
}
62+
if emitPointersForNull {
63+
return "*int64"
64+
}
65+
return "sql.NullInt64"
66+
67+
case "uint8":
68+
if notNull {
69+
return "uint8"
70+
}
71+
if emitPointersForNull {
72+
return "*uint8"
73+
}
74+
return "sql.NullInt16" // No sql.NullUint8
75+
76+
case "uint16":
77+
if notNull {
78+
return "uint16"
79+
}
80+
if emitPointersForNull {
81+
return "*uint16"
82+
}
83+
return "sql.NullInt32" // No sql.NullUint16
84+
85+
case "uint32":
86+
if notNull {
87+
return "uint32"
88+
}
89+
if emitPointersForNull {
90+
return "*uint32"
91+
}
92+
return "sql.NullInt64" // No sql.NullUint32
93+
94+
case "uint64":
95+
if notNull {
96+
return "uint64"
97+
}
98+
if emitPointersForNull {
99+
return "*uint64"
100+
}
101+
// Note: uint64 doesn't fit in sql.NullInt64 for large values
102+
return "sql.NullInt64"
103+
104+
// Float types
105+
case "float32":
106+
if notNull {
107+
return "float32"
108+
}
109+
if emitPointersForNull {
110+
return "*float32"
111+
}
112+
return "sql.NullFloat64"
113+
114+
case "float64":
115+
if notNull {
116+
return "float64"
117+
}
118+
if emitPointersForNull {
119+
return "*float64"
120+
}
121+
return "sql.NullFloat64"
122+
123+
// String types
124+
case "string":
125+
if notNull {
126+
return "string"
127+
}
128+
if emitPointersForNull {
129+
return "*string"
130+
}
131+
return "sql.NullString"
132+
133+
// Boolean type
134+
case "bool", "boolean":
135+
if notNull {
136+
return "bool"
137+
}
138+
if emitPointersForNull {
139+
return "*bool"
140+
}
141+
return "sql.NullBool"
142+
143+
// Date and time types
144+
case "date", "date32":
145+
if notNull {
146+
return "time.Time"
147+
}
148+
if emitPointersForNull {
149+
return "*time.Time"
150+
}
151+
return "sql.NullTime"
152+
153+
case "datetime", "datetime64":
154+
if notNull {
155+
return "time.Time"
156+
}
157+
if emitPointersForNull {
158+
return "*time.Time"
159+
}
160+
return "sql.NullTime"
161+
162+
// UUID type
163+
case "uuid":
164+
if notNull {
165+
return "uuid.UUID"
166+
}
167+
if emitPointersForNull {
168+
return "*uuid.UUID"
169+
}
170+
return "uuid.NullUUID"
171+
172+
// JSON type
173+
case "json":
174+
return "json.RawMessage"
175+
176+
// Any type (for unknown types)
177+
case "any":
178+
return "interface{}"
179+
180+
default:
181+
// Handle FixedString(N)
182+
if strings.HasPrefix(dt, "fixedstring") {
183+
if notNull {
184+
return "string"
185+
}
186+
if emitPointersForNull {
187+
return "*string"
188+
}
189+
return "sql.NullString"
190+
}
191+
192+
// Handle Decimal types
193+
if strings.HasPrefix(dt, "decimal") {
194+
if notNull {
195+
return "float64"
196+
}
197+
if emitPointersForNull {
198+
return "*float64"
199+
}
200+
return "sql.NullFloat64"
201+
}
202+
203+
// Handle Array types
204+
if strings.HasPrefix(dt, "array(") && strings.HasSuffix(dt, ")") {
205+
innerType := dt[6 : len(dt)-1]
206+
innerCol := &plugin.Column{
207+
Type: &plugin.Identifier{Name: innerType},
208+
NotNull: true,
209+
}
210+
return "[]" + clickhouseType(req, options, innerCol)
211+
}
212+
213+
// Handle Enum types
214+
if strings.HasPrefix(dt, "enum8") || strings.HasPrefix(dt, "enum16") {
215+
if notNull {
216+
return "string"
217+
}
218+
if emitPointersForNull {
219+
return "*string"
220+
}
221+
return "sql.NullString"
222+
}
223+
224+
// Handle Map types
225+
if strings.HasPrefix(dt, "map(") {
226+
return "map[string]interface{}"
227+
}
228+
229+
// Handle Tuple types
230+
if strings.HasPrefix(dt, "tuple(") {
231+
return "interface{}"
232+
}
233+
234+
if debug.Active {
235+
log.Printf("unknown ClickHouse type: %s\n", dt)
236+
}
237+
238+
return "interface{}"
239+
}
240+
}

internal/codegen/golang/go_type.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ func goInnerType(req *plugin.GenerateRequest, options *opts.Options, col *plugin
8989
return postgresType(req, options, col)
9090
case "sqlite":
9191
return sqliteType(req, options, col)
92+
case "clickhouse":
93+
return clickhouseType(req, options, col)
9294
default:
9395
return "interface{}"
9496
}

internal/compiler/engine.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"github.com/sqlc-dev/sqlc/internal/analyzer"
88
"github.com/sqlc-dev/sqlc/internal/config"
99
"github.com/sqlc-dev/sqlc/internal/dbmanager"
10+
"github.com/sqlc-dev/sqlc/internal/engine/clickhouse"
11+
clickhouseanalyze "github.com/sqlc-dev/sqlc/internal/engine/clickhouse/analyzer"
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"
@@ -111,6 +113,34 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts
111113
)
112114
}
113115
}
116+
case config.EngineClickHouse:
117+
// ClickHouse requires the clickhouse experiment flag
118+
if !parserOpts.Experiment.ClickHouse {
119+
return nil, fmt.Errorf("clickhouse engine requires SQLCEXPERIMENT=clickhouse")
120+
}
121+
// ClickHouse requires database-only mode
122+
if !conf.Analyzer.Database.IsOnly() {
123+
return nil, fmt.Errorf("clickhouse engine requires analyzer.database: only")
124+
}
125+
if conf.Database == nil {
126+
return nil, fmt.Errorf("clickhouse engine requires database configuration")
127+
}
128+
if conf.Database.URI == "" && !conf.Database.Managed {
129+
return nil, fmt.Errorf("clickhouse engine requires database.uri or database.managed")
130+
}
131+
132+
parser := clickhouse.NewParser()
133+
c.parser = parser
134+
c.catalog = clickhouse.NewCatalog()
135+
c.selector = newDefaultSelector()
136+
c.databaseOnlyMode = true
137+
138+
// Create the ClickHouse analyzer
139+
chAnalyzer := clickhouseanalyze.New(*conf.Database)
140+
c.analyzer = analyzer.Cached(chAnalyzer, combo.Global, *conf.Database)
141+
// Create the expander using the analyzer as the column getter
142+
c.expander = expander.New(c.analyzer, parser, parser)
143+
114144
default:
115145
return nil, fmt.Errorf("unknown engine: %s", conf.Engine)
116146
}

internal/config/config.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ func (p *Paths) UnmarshalYAML(unmarshal func(interface{}) error) error {
5151
}
5252

5353
const (
54-
EngineMySQL Engine = "mysql"
55-
EnginePostgreSQL Engine = "postgresql"
56-
EngineSQLite Engine = "sqlite"
54+
EngineMySQL Engine = "mysql"
55+
EnginePostgreSQL Engine = "postgresql"
56+
EngineSQLite Engine = "sqlite"
57+
EngineClickHouse Engine = "clickhouse"
5758
)
5859

5960
type Config struct {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"contexts": ["managed-db"],
3+
"env": {
4+
"SQLCEXPERIMENT": "clickhouse,analyzerv2"
5+
}
6+
}

0 commit comments

Comments
 (0)