Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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 <noreply@anthropic.com>
  • Loading branch information
claude committed Dec 24, 2025
commit e3deb27a27fa5f039899a0329c372b361fc8a687
11 changes: 11 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,14 @@ services:
POSTGRES_DB: postgres
POSTGRES_PASSWORD: mysecretpassword
POSTGRES_USER: postgres

clickhouse:
image: "clickhouse/clickhouse-server:latest"
ports:
- "9000:9000"
- "8123:8123"
restart: always
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: default
CLICKHOUSE_PASSWORD: mysecretpassword
28 changes: 21 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
module github.com/sqlc-dev/sqlc

go 1.24.0

toolchain go1.24.1
go 1.24.7

require (
github.com/ClickHouse/clickhouse-go/v2 v2.42.0
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/cubicdaiya/gonp v1.0.4
github.com/davecgh/go-spew v1.1.1
Expand All @@ -22,6 +21,7 @@ require (
github.com/riza-io/grpc-go v0.2.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/sqlc-dev/doubleclick v0.0.0-20251223195122-0076eee94506
github.com/tetratelabs/wazero v1.10.1
github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07
github.com/xeipuuv/gojsonschema v1.2.0
Expand All @@ -34,6 +34,12 @@ require (
require (
cel.dev/expr v0.24.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/ClickHouse/ch-go v0.69.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
Expand All @@ -43,23 +49,31 @@ require (
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/ncruces/julianday v1.0.0 // indirect
github.com/paulmach/orb v0.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb // indirect
github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
github.com/pingcap/log v1.1.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
Expand Down
92 changes: 75 additions & 17 deletions go.sum

Large diffs are not rendered by default.

240 changes: 240 additions & 0 deletions internal/codegen/golang/clickhouse_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package golang

import (
"log"
"strings"

"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
"github.com/sqlc-dev/sqlc/internal/debug"
"github.com/sqlc-dev/sqlc/internal/plugin"
)

func clickhouseType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
dt := strings.ToLower(sdk.DataType(col.Type))
notNull := col.NotNull || col.IsArray
emitPointersForNull := options.EmitPointersForNullTypes

// Handle Nullable wrapper
if strings.HasPrefix(dt, "nullable(") && strings.HasSuffix(dt, ")") {
dt = dt[9 : len(dt)-1]
notNull = false
}

// Handle LowCardinality wrapper
if strings.HasPrefix(dt, "lowcardinality(") && strings.HasSuffix(dt, ")") {
dt = dt[15 : len(dt)-1]
}

switch dt {
// Integer types
case "int8":
if notNull {
return "int8"
}
if emitPointersForNull {
return "*int8"
}
return "sql.NullInt16" // No sql.NullInt8, use Int16

case "int16":
if notNull {
return "int16"
}
if emitPointersForNull {
return "*int16"
}
return "sql.NullInt16"

case "int32":
if notNull {
return "int32"
}
if emitPointersForNull {
return "*int32"
}
return "sql.NullInt32"

case "int64":
if notNull {
return "int64"
}
if emitPointersForNull {
return "*int64"
}
return "sql.NullInt64"

case "uint8":
if notNull {
return "uint8"
}
if emitPointersForNull {
return "*uint8"
}
return "sql.NullInt16" // No sql.NullUint8

case "uint16":
if notNull {
return "uint16"
}
if emitPointersForNull {
return "*uint16"
}
return "sql.NullInt32" // No sql.NullUint16

case "uint32":
if notNull {
return "uint32"
}
if emitPointersForNull {
return "*uint32"
}
return "sql.NullInt64" // No sql.NullUint32

case "uint64":
if notNull {
return "uint64"
}
if emitPointersForNull {
return "*uint64"
}
// Note: uint64 doesn't fit in sql.NullInt64 for large values
return "sql.NullInt64"

// Float types
case "float32":
if notNull {
return "float32"
}
if emitPointersForNull {
return "*float32"
}
return "sql.NullFloat64"

case "float64":
if notNull {
return "float64"
}
if emitPointersForNull {
return "*float64"
}
return "sql.NullFloat64"

// String types
case "string":
if notNull {
return "string"
}
if emitPointersForNull {
return "*string"
}
return "sql.NullString"

// Boolean type
case "bool", "boolean":
if notNull {
return "bool"
}
if emitPointersForNull {
return "*bool"
}
return "sql.NullBool"

// Date and time types
case "date", "date32":
if notNull {
return "time.Time"
}
if emitPointersForNull {
return "*time.Time"
}
return "sql.NullTime"

case "datetime", "datetime64":
if notNull {
return "time.Time"
}
if emitPointersForNull {
return "*time.Time"
}
return "sql.NullTime"

// UUID type
case "uuid":
if notNull {
return "uuid.UUID"
}
if emitPointersForNull {
return "*uuid.UUID"
}
return "uuid.NullUUID"

// JSON type
case "json":
return "json.RawMessage"

// Any type (for unknown types)
case "any":
return "interface{}"

default:
// Handle FixedString(N)
if strings.HasPrefix(dt, "fixedstring") {
if notNull {
return "string"
}
if emitPointersForNull {
return "*string"
}
return "sql.NullString"
}

// Handle Decimal types
if strings.HasPrefix(dt, "decimal") {
if notNull {
return "float64"
}
if emitPointersForNull {
return "*float64"
}
return "sql.NullFloat64"
}

// Handle Array types
if strings.HasPrefix(dt, "array(") && strings.HasSuffix(dt, ")") {
innerType := dt[6 : len(dt)-1]
innerCol := &plugin.Column{
Type: &plugin.Identifier{Name: innerType},
NotNull: true,
}
return "[]" + clickhouseType(req, options, innerCol)
}

// Handle Enum types
if strings.HasPrefix(dt, "enum8") || strings.HasPrefix(dt, "enum16") {
if notNull {
return "string"
}
if emitPointersForNull {
return "*string"
}
return "sql.NullString"
}

// Handle Map types
if strings.HasPrefix(dt, "map(") {
return "map[string]interface{}"
}

// Handle Tuple types
if strings.HasPrefix(dt, "tuple(") {
return "interface{}"
}

if debug.Active {
log.Printf("unknown ClickHouse type: %s\n", dt)
}

return "interface{}"
}
}
2 changes: 2 additions & 0 deletions internal/codegen/golang/go_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ func goInnerType(req *plugin.GenerateRequest, options *opts.Options, col *plugin
return postgresType(req, options, col)
case "sqlite":
return sqliteType(req, options, col)
case "clickhouse":
return clickhouseType(req, options, col)
default:
return "interface{}"
}
Expand Down
30 changes: 30 additions & 0 deletions internal/compiler/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"github.com/sqlc-dev/sqlc/internal/analyzer"
"github.com/sqlc-dev/sqlc/internal/config"
"github.com/sqlc-dev/sqlc/internal/dbmanager"
"github.com/sqlc-dev/sqlc/internal/engine/clickhouse"
clickhouseanalyze "github.com/sqlc-dev/sqlc/internal/engine/clickhouse/analyzer"
"github.com/sqlc-dev/sqlc/internal/engine/dolphin"
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
pganalyze "github.com/sqlc-dev/sqlc/internal/engine/postgresql/analyzer"
Expand Down Expand Up @@ -111,6 +113,34 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts
)
}
}
case config.EngineClickHouse:
// ClickHouse requires the clickhouse experiment flag
if !parserOpts.Experiment.ClickHouse {
return nil, fmt.Errorf("clickhouse engine requires SQLCEXPERIMENT=clickhouse")
}
// ClickHouse requires database-only mode
if !conf.Analyzer.Database.IsOnly() {
return nil, fmt.Errorf("clickhouse engine requires analyzer.database: only")
}
if conf.Database == nil {
return nil, fmt.Errorf("clickhouse engine requires database configuration")
}
if conf.Database.URI == "" && !conf.Database.Managed {
return nil, fmt.Errorf("clickhouse engine requires database.uri or database.managed")
}

parser := clickhouse.NewParser()
c.parser = parser
c.catalog = clickhouse.NewCatalog()
c.selector = newDefaultSelector()
c.databaseOnlyMode = true

// Create the ClickHouse analyzer
chAnalyzer := clickhouseanalyze.New(*conf.Database)
c.analyzer = analyzer.Cached(chAnalyzer, combo.Global, *conf.Database)
// Create the expander using the analyzer as the column getter
c.expander = expander.New(c.analyzer, parser, parser)

default:
return nil, fmt.Errorf("unknown engine: %s", conf.Engine)
}
Expand Down
7 changes: 4 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ func (p *Paths) UnmarshalYAML(unmarshal func(interface{}) error) error {
}

const (
EngineMySQL Engine = "mysql"
EnginePostgreSQL Engine = "postgresql"
EngineSQLite Engine = "sqlite"
EngineMySQL Engine = "mysql"
EnginePostgreSQL Engine = "postgresql"
EngineSQLite Engine = "sqlite"
EngineClickHouse Engine = "clickhouse"
)

type Config struct {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"contexts": ["managed-db"],
"env": {
"SQLCEXPERIMENT": "clickhouse,analyzerv2"
}
}
Loading