From c215af286f19ef33800684b32b7af8ba83776907 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 16 Jan 2026 20:39:27 -0800 Subject: [PATCH 1/4] feat: add ClickHouse database engine support Add ClickHouse support using the github.com/sqlc-dev/doubleclick parser library. New files in internal/engine/clickhouse/: - parse.go: Parser implementation using doubleclick - convert.go: AST converter from doubleclick to sqlc AST - format.go: ClickHouse-specific SQL formatting - catalog.go: Catalog initialization - stdlib.go: Standard library functions - reserved.go: Reserved keywords - utils.go: Helper functions - parse_test.go: Unit tests Supported SQL operations: - SELECT with JOINs, subqueries, CTEs, window functions - INSERT with VALUES and SELECT subquery - UPDATE and DELETE - CREATE TABLE, ALTER TABLE, DROP TABLE, TRUNCATE Co-Authored-By: Claude --- go.mod | 5 +- go.sum | 2 + internal/compiler/engine.go | 5 + internal/config/config.go | 7 +- internal/engine/clickhouse/catalog.go | 16 + internal/engine/clickhouse/convert.go | 1020 ++++++++++++++++++++++ internal/engine/clickhouse/format.go | 35 + internal/engine/clickhouse/parse.go | 64 ++ internal/engine/clickhouse/parse_test.go | 91 ++ internal/engine/clickhouse/reserved.go | 150 ++++ internal/engine/clickhouse/stdlib.go | 168 ++++ internal/engine/clickhouse/utils.go | 59 ++ 12 files changed, 1616 insertions(+), 6 deletions(-) create mode 100644 internal/engine/clickhouse/catalog.go create mode 100644 internal/engine/clickhouse/convert.go create mode 100644 internal/engine/clickhouse/format.go create mode 100644 internal/engine/clickhouse/parse.go create mode 100644 internal/engine/clickhouse/parse_test.go create mode 100644 internal/engine/clickhouse/reserved.go create mode 100644 internal/engine/clickhouse/stdlib.go create mode 100644 internal/engine/clickhouse/utils.go diff --git a/go.mod b/go.mod index 44ecebecb4..87d57139a0 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/sqlc-dev/sqlc -go 1.24.0 - -toolchain go1.24.1 +go 1.24.7 require ( github.com/antlr4-go/antlr/v4 v4.13.1 @@ -48,6 +46,7 @@ require ( 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/sqlc-dev/doubleclick v1.0.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 diff --git a/go.sum b/go.sum index 0fb994c119..bc1987fe3c 100644 --- a/go.sum +++ b/go.sum @@ -157,6 +157,8 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/sqlc-dev/doubleclick v1.0.0 h1:2/OApfQ2eLgcfa/Fqs8WSMA6atH0G8j9hHbQIgMfAXI= +github.com/sqlc-dev/doubleclick v1.0.0/go.mod h1:ODHRroSrk/rr5neRHlWMSRijqOak8YmNaO3VAZCNl5Y= github.com/sqlc-dev/mysql v0.0.0-20251129233104-d81e1cac6db2 h1:kmCAKKtOgK6EXXQX9oPdEASIhgor7TCpWxD8NtcqVcU= github.com/sqlc-dev/mysql v0.0.0-20251129233104-d81e1cac6db2/go.mod h1:TrDMWzjNTKvJeK2GC8uspG+PWyPLiY9QKvwdWpAdlZE= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= diff --git a/internal/compiler/engine.go b/internal/compiler/engine.go index 64fdf3d5c7..0f8d5ca0f9 100644 --- a/internal/compiler/engine.go +++ b/internal/compiler/engine.go @@ -7,6 +7,7 @@ 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" "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" @@ -82,6 +83,10 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts c.parser = dolphin.NewParser() c.catalog = dolphin.NewCatalog() c.selector = newDefaultSelector() + case config.EngineClickHouse: + c.parser = clickhouse.NewParser() + c.catalog = clickhouse.NewCatalog() + c.selector = newDefaultSelector() case config.EnginePostgreSQL: parser := postgresql.NewParser() c.parser = parser diff --git a/internal/config/config.go b/internal/config/config.go index d3e610ef05..59e31ae233 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 { diff --git a/internal/engine/clickhouse/catalog.go b/internal/engine/clickhouse/catalog.go new file mode 100644 index 0000000000..fb0511f72e --- /dev/null +++ b/internal/engine/clickhouse/catalog.go @@ -0,0 +1,16 @@ +package clickhouse + +import ( + "github.com/sqlc-dev/sqlc/internal/sql/catalog" +) + +func NewCatalog() *catalog.Catalog { + def := "default" // ClickHouse default database + return &catalog.Catalog{ + DefaultSchema: def, + Schemas: []*catalog.Schema{ + defaultSchema(def), + }, + Extensions: map[string]struct{}{}, + } +} diff --git a/internal/engine/clickhouse/convert.go b/internal/engine/clickhouse/convert.go new file mode 100644 index 0000000000..ba2817e2bb --- /dev/null +++ b/internal/engine/clickhouse/convert.go @@ -0,0 +1,1020 @@ +package clickhouse + +import ( + "strconv" + "strings" + + chast "github.com/sqlc-dev/doubleclick/ast" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +type cc struct { + paramCount int +} + +func (c *cc) convert(node chast.Node) ast.Node { + switch n := node.(type) { + case *chast.SelectWithUnionQuery: + return c.convertSelectWithUnionQuery(n) + case *chast.SelectQuery: + return c.convertSelectQuery(n) + case *chast.InsertQuery: + return c.convertInsertQuery(n) + case *chast.CreateQuery: + return c.convertCreateQuery(n) + case *chast.UpdateQuery: + return c.convertUpdateQuery(n) + case *chast.DeleteQuery: + return c.convertDeleteQuery(n) + case *chast.DropQuery: + return c.convertDropQuery(n) + case *chast.AlterQuery: + return c.convertAlterQuery(n) + case *chast.TruncateQuery: + return c.convertTruncateQuery(n) + default: + return todo(n) + } +} + +func (c *cc) convertSelectWithUnionQuery(n *chast.SelectWithUnionQuery) ast.Node { + if len(n.Selects) == 0 { + return &ast.TODO{} + } + + // Single select without union + if len(n.Selects) == 1 { + return c.convert(n.Selects[0]) + } + + // Build a chain of SelectStmt with UNION operations + var result *ast.SelectStmt + for i, sel := range n.Selects { + stmt, ok := c.convert(sel).(*ast.SelectStmt) + if !ok { + continue + } + if i == 0 { + result = stmt + } else { + unionMode := ast.Union + if i-1 < len(n.UnionModes) { + switch strings.ToUpper(n.UnionModes[i-1]) { + case "ALL": + unionMode = ast.Union + case "DISTINCT": + unionMode = ast.Union + } + } + result = &ast.SelectStmt{ + Op: unionMode, + All: n.UnionAll || (i-1 < len(n.UnionModes) && strings.ToUpper(n.UnionModes[i-1]) == "ALL"), + Larg: result, + Rarg: stmt, + } + } + } + return result +} + +func (c *cc) convertSelectQuery(n *chast.SelectQuery) *ast.SelectStmt { + stmt := &ast.SelectStmt{} + + // Convert target list (SELECT columns) + if len(n.Columns) > 0 { + stmt.TargetList = &ast.List{} + for _, col := range n.Columns { + target := c.convertToResTarget(col) + if target != nil { + stmt.TargetList.Items = append(stmt.TargetList.Items, target) + } + } + } + + // Convert FROM clause + if n.From != nil { + stmt.FromClause = c.convertTablesInSelectQuery(n.From) + } + + // Convert WHERE clause + if n.Where != nil { + stmt.WhereClause = c.convertExpr(n.Where) + } + + // Convert GROUP BY clause + if len(n.GroupBy) > 0 { + stmt.GroupClause = &ast.List{} + for _, expr := range n.GroupBy { + stmt.GroupClause.Items = append(stmt.GroupClause.Items, c.convertExpr(expr)) + } + } + + // Convert HAVING clause + if n.Having != nil { + stmt.HavingClause = c.convertExpr(n.Having) + } + + // Convert ORDER BY clause + if len(n.OrderBy) > 0 { + stmt.SortClause = &ast.List{} + for _, orderBy := range n.OrderBy { + stmt.SortClause.Items = append(stmt.SortClause.Items, c.convertOrderByElement(orderBy)) + } + } + + // Convert LIMIT clause + if n.Limit != nil { + stmt.LimitCount = c.convertExpr(n.Limit) + } + + // Convert OFFSET clause + if n.Offset != nil { + stmt.LimitOffset = c.convertExpr(n.Offset) + } + + // Convert DISTINCT clause + if n.Distinct { + stmt.DistinctClause = &ast.List{} + } + + // Convert DISTINCT ON clause + if len(n.DistinctOn) > 0 { + stmt.DistinctClause = &ast.List{} + for _, expr := range n.DistinctOn { + stmt.DistinctClause.Items = append(stmt.DistinctClause.Items, c.convertExpr(expr)) + } + } + + // Convert WITH clause (CTEs) + if len(n.With) > 0 { + stmt.WithClause = &ast.WithClause{ + Ctes: &ast.List{}, + } + for _, cte := range n.With { + if aliased, ok := cte.(*chast.AliasedExpr); ok { + cteNode := &ast.CommonTableExpr{ + Ctename: &aliased.Alias, + } + // CTE expression may be a Subquery containing the actual SELECT + if subq, ok := aliased.Expr.(*chast.Subquery); ok { + cteNode.Ctequery = c.convert(subq.Query) + } else { + // Fallback: treat the expression itself as the query + cteNode.Ctequery = c.convertExpr(aliased.Expr) + } + stmt.WithClause.Ctes.Items = append(stmt.WithClause.Ctes.Items, cteNode) + } + } + } + + return stmt +} + +func (c *cc) convertToResTarget(expr chast.Expression) *ast.ResTarget { + res := &ast.ResTarget{ + Location: expr.Pos().Offset, + } + + switch e := expr.(type) { + case *chast.Asterisk: + if e.Table != "" { + // table.* + res.Val = &ast.ColumnRef{ + Fields: &ast.List{ + Items: []ast.Node{ + NewIdentifier(e.Table), + &ast.A_Star{}, + }, + }, + } + } else { + // Just * + res.Val = &ast.ColumnRef{ + Fields: &ast.List{ + Items: []ast.Node{&ast.A_Star{}}, + }, + } + } + case *chast.AliasedExpr: + res.Name = &e.Alias + res.Val = c.convertExpr(e.Expr) + case *chast.Identifier: + if e.Alias != "" { + res.Name = &e.Alias + } + res.Val = c.convertIdentifier(e) + case *chast.FunctionCall: + if e.Alias != "" { + res.Name = &e.Alias + } + res.Val = c.convertFunctionCall(e) + default: + res.Val = c.convertExpr(expr) + } + + return res +} + +func (c *cc) convertTablesInSelectQuery(n *chast.TablesInSelectQuery) *ast.List { + if n == nil || len(n.Tables) == 0 { + return nil + } + + result := &ast.List{} + + for i, elem := range n.Tables { + if elem.Table != nil { + tableExpr := c.convertTableExpression(elem.Table) + if i == 0 { + result.Items = append(result.Items, tableExpr) + } else if elem.Join != nil { + // This element has a join + joinExpr := c.convertTableJoin(elem.Join, result.Items[len(result.Items)-1], tableExpr) + result.Items[len(result.Items)-1] = joinExpr + } else { + result.Items = append(result.Items, tableExpr) + } + } else if elem.Join != nil && len(result.Items) > 0 { + // Join without table (should not happen normally) + continue + } + } + + return result +} + +func (c *cc) convertTableExpression(n *chast.TableExpression) ast.Node { + var result ast.Node + + switch t := n.Table.(type) { + case *chast.TableIdentifier: + rv := parseTableIdentifierToRangeVar(t) + if n.Alias != "" { + alias := n.Alias + rv.Alias = &ast.Alias{Aliasname: &alias} + } + result = rv + case *chast.Subquery: + subselect := &ast.RangeSubselect{ + Subquery: c.convert(t.Query), + } + alias := n.Alias + if alias == "" && t.Alias != "" { + alias = t.Alias + } + if alias != "" { + subselect.Alias = &ast.Alias{Aliasname: &alias} + } + result = subselect + case *chast.FunctionCall: + // Table function like file(), url(), etc. + rf := &ast.RangeFunction{ + Functions: &ast.List{ + Items: []ast.Node{c.convertFunctionCall(t)}, + }, + } + if n.Alias != "" { + alias := n.Alias + rf.Alias = &ast.Alias{Aliasname: &alias} + } + result = rf + default: + result = &ast.TODO{} + } + + return result +} + +func (c *cc) convertTableJoin(n *chast.TableJoin, left, right ast.Node) *ast.JoinExpr { + join := &ast.JoinExpr{ + Larg: left, + Rarg: right, + } + + // Convert join type + switch n.Type { + case chast.JoinInner: + join.Jointype = ast.JoinTypeInner + case chast.JoinLeft: + join.Jointype = ast.JoinTypeLeft + case chast.JoinRight: + join.Jointype = ast.JoinTypeRight + case chast.JoinFull: + join.Jointype = ast.JoinTypeFull + case chast.JoinCross: + join.Jointype = ast.JoinTypeInner + join.IsNatural = false + default: + join.Jointype = ast.JoinTypeInner + } + + // Convert ON clause + if n.On != nil { + join.Quals = c.convertExpr(n.On) + } + + // Convert USING clause + if len(n.Using) > 0 { + join.UsingClause = &ast.List{} + for _, u := range n.Using { + if id, ok := u.(*chast.Identifier); ok { + join.UsingClause.Items = append(join.UsingClause.Items, NewIdentifier(id.Name())) + } + } + } + + return join +} + +func (c *cc) convertExpr(expr chast.Expression) ast.Node { + if expr == nil { + return nil + } + + switch e := expr.(type) { + case *chast.Identifier: + return c.convertIdentifier(e) + case *chast.Literal: + return c.convertLiteral(e) + case *chast.BinaryExpr: + return c.convertBinaryExpr(e) + case *chast.FunctionCall: + return c.convertFunctionCall(e) + case *chast.AliasedExpr: + return c.convertExpr(e.Expr) + case *chast.Parameter: + return c.convertParameter(e) + case *chast.Asterisk: + return c.convertAsterisk(e) + case *chast.CaseExpr: + return c.convertCaseExpr(e) + case *chast.CastExpr: + return c.convertCastExpr(e) + case *chast.BetweenExpr: + return c.convertBetweenExpr(e) + case *chast.InExpr: + return c.convertInExpr(e) + case *chast.IsNullExpr: + return c.convertIsNullExpr(e) + case *chast.LikeExpr: + return c.convertLikeExpr(e) + case *chast.Subquery: + return c.convertSubquery(e) + case *chast.ArrayAccess: + return c.convertArrayAccess(e) + case *chast.UnaryExpr: + return c.convertUnaryExpr(e) + case *chast.Lambda: + // Lambda expressions are ClickHouse-specific, return as-is for now + return &ast.TODO{} + default: + return &ast.TODO{} + } +} + +func (c *cc) convertIdentifier(n *chast.Identifier) *ast.ColumnRef { + fields := &ast.List{} + for _, part := range n.Parts { + fields.Items = append(fields.Items, NewIdentifier(part)) + } + return &ast.ColumnRef{ + Fields: fields, + Location: n.Pos().Offset, + } +} + +func (c *cc) convertLiteral(n *chast.Literal) *ast.A_Const { + switch n.Type { + case chast.LiteralString: + str := n.Value.(string) + return &ast.A_Const{ + Val: &ast.String{Str: str}, + Location: n.Pos().Offset, + } + case chast.LiteralInteger: + var ival int64 + switch v := n.Value.(type) { + case int64: + ival = v + case int: + ival = int64(v) + case float64: + ival = int64(v) + case string: + ival, _ = strconv.ParseInt(v, 10, 64) + } + return &ast.A_Const{ + Val: &ast.Integer{Ival: ival}, + Location: n.Pos().Offset, + } + case chast.LiteralFloat: + var fval float64 + switch v := n.Value.(type) { + case float64: + fval = v + case string: + fval, _ = strconv.ParseFloat(v, 64) + } + str := strconv.FormatFloat(fval, 'f', -1, 64) + return &ast.A_Const{ + Val: &ast.Float{Str: str}, + Location: n.Pos().Offset, + } + case chast.LiteralBoolean: + // ClickHouse booleans are typically 0/1 + bval := n.Value.(bool) + if bval { + return &ast.A_Const{ + Val: &ast.Integer{Ival: 1}, + Location: n.Pos().Offset, + } + } + return &ast.A_Const{ + Val: &ast.Integer{Ival: 0}, + Location: n.Pos().Offset, + } + case chast.LiteralNull: + return &ast.A_Const{ + Val: &ast.Null{}, + Location: n.Pos().Offset, + } + default: + return &ast.A_Const{ + Location: n.Pos().Offset, + } + } +} + +func (c *cc) convertBinaryExpr(n *chast.BinaryExpr) ast.Node { + op := strings.ToUpper(n.Op) + + // Handle logical operators + if op == "AND" || op == "OR" { + var boolop ast.BoolExprType + if op == "AND" { + boolop = ast.BoolExprTypeAnd + } else { + boolop = ast.BoolExprTypeOr + } + return &ast.BoolExpr{ + Boolop: boolop, + Args: &ast.List{ + Items: []ast.Node{ + c.convertExpr(n.Left), + c.convertExpr(n.Right), + }, + }, + Location: n.Pos().Offset, + } + } + + // Handle other operators + return &ast.A_Expr{ + Name: &ast.List{ + Items: []ast.Node{&ast.String{Str: n.Op}}, + }, + Lexpr: c.convertExpr(n.Left), + Rexpr: c.convertExpr(n.Right), + Location: n.Pos().Offset, + } +} + +func (c *cc) convertFunctionCall(n *chast.FunctionCall) *ast.FuncCall { + fc := &ast.FuncCall{ + Funcname: &ast.List{ + Items: []ast.Node{&ast.String{Str: n.Name}}, + }, + Location: n.Pos().Offset, + AggDistinct: n.Distinct, + } + + // Convert arguments + if len(n.Arguments) > 0 { + fc.Args = &ast.List{} + for _, arg := range n.Arguments { + fc.Args.Items = append(fc.Args.Items, c.convertExpr(arg)) + } + } + + // Convert window function + if n.Over != nil { + fc.Over = &ast.WindowDef{} + if len(n.Over.PartitionBy) > 0 { + fc.Over.PartitionClause = &ast.List{} + for _, p := range n.Over.PartitionBy { + fc.Over.PartitionClause.Items = append(fc.Over.PartitionClause.Items, c.convertExpr(p)) + } + } + if len(n.Over.OrderBy) > 0 { + fc.Over.OrderClause = &ast.List{} + for _, o := range n.Over.OrderBy { + fc.Over.OrderClause.Items = append(fc.Over.OrderClause.Items, c.convertOrderByElement(o)) + } + } + } + + return fc +} + +func (c *cc) convertParameter(n *chast.Parameter) ast.Node { + c.paramCount++ + // Use the parameter name if available + name := n.Name + if name == "" { + name = strconv.Itoa(c.paramCount) + } + return &ast.ParamRef{ + Number: c.paramCount, + Location: n.Pos().Offset, + } +} + +func (c *cc) convertAsterisk(n *chast.Asterisk) *ast.ColumnRef { + fields := &ast.List{} + if n.Table != "" { + fields.Items = append(fields.Items, NewIdentifier(n.Table)) + } + fields.Items = append(fields.Items, &ast.A_Star{}) + return &ast.ColumnRef{ + Fields: fields, + Location: n.Pos().Offset, + } +} + +func (c *cc) convertCaseExpr(n *chast.CaseExpr) *ast.CaseExpr { + ce := &ast.CaseExpr{ + Location: n.Pos().Offset, + } + + // Convert test expression (CASE expr WHEN ...) + if n.Operand != nil { + ce.Arg = c.convertExpr(n.Operand) + } + + // Convert WHEN clauses + if len(n.Whens) > 0 { + ce.Args = &ast.List{} + for _, when := range n.Whens { + caseWhen := &ast.CaseWhen{ + Expr: c.convertExpr(when.Condition), + Result: c.convertExpr(when.Result), + } + ce.Args.Items = append(ce.Args.Items, caseWhen) + } + } + + // Convert ELSE clause + if n.Else != nil { + ce.Defresult = c.convertExpr(n.Else) + } + + return ce +} + +func (c *cc) convertCastExpr(n *chast.CastExpr) *ast.TypeCast { + tc := &ast.TypeCast{ + Arg: c.convertExpr(n.Expr), + Location: n.Pos().Offset, + } + + if n.Type != nil { + tc.TypeName = &ast.TypeName{ + Name: n.Type.Name, + } + } + + return tc +} + +func (c *cc) convertBetweenExpr(n *chast.BetweenExpr) *ast.BetweenExpr { + return &ast.BetweenExpr{ + Expr: c.convertExpr(n.Expr), + Left: c.convertExpr(n.Low), + Right: c.convertExpr(n.High), + Not: n.Not, + Location: n.Pos().Offset, + } +} + +func (c *cc) convertInExpr(n *chast.InExpr) *ast.In { + in := &ast.In{ + Expr: c.convertExpr(n.Expr), + Not: n.Not, + Location: n.Pos().Offset, + } + + // Convert the list + if len(n.List) > 0 { + in.List = make([]ast.Node, 0, len(n.List)) + for _, item := range n.List { + in.List = append(in.List, c.convertExpr(item)) + } + } + + // Handle subquery + if n.Query != nil { + in.Sel = c.convert(n.Query) + } + + return in +} + +func (c *cc) convertIsNullExpr(n *chast.IsNullExpr) *ast.NullTest { + nullTest := &ast.NullTest{ + Arg: c.convertExpr(n.Expr), + Location: n.Pos().Offset, + } + if n.Not { + nullTest.Nulltesttype = ast.NullTestTypeIsNotNull + } else { + nullTest.Nulltesttype = ast.NullTestTypeIsNull + } + return nullTest +} + +func (c *cc) convertLikeExpr(n *chast.LikeExpr) *ast.A_Expr { + kind := ast.A_Expr_Kind(0) + opName := "~~" + if n.CaseInsensitive { + opName = "~~*" + } + if n.Not { + opName = "!~~" + if n.CaseInsensitive { + opName = "!~~*" + } + } + + return &ast.A_Expr{ + Kind: kind, + Name: &ast.List{ + Items: []ast.Node{&ast.String{Str: opName}}, + }, + Lexpr: c.convertExpr(n.Expr), + Rexpr: c.convertExpr(n.Pattern), + Location: n.Pos().Offset, + } +} + +func (c *cc) convertSubquery(n *chast.Subquery) *ast.SubLink { + return &ast.SubLink{ + SubLinkType: ast.EXISTS_SUBLINK, + Subselect: c.convert(n.Query), + } +} + +func (c *cc) convertArrayAccess(n *chast.ArrayAccess) *ast.A_Indirection { + return &ast.A_Indirection{ + Arg: c.convertExpr(n.Array), + Indirection: &ast.List{ + Items: []ast.Node{ + &ast.A_Indices{ + Uidx: c.convertExpr(n.Index), + }, + }, + }, + } +} + +func (c *cc) convertUnaryExpr(n *chast.UnaryExpr) ast.Node { + op := strings.ToUpper(n.Op) + + if op == "NOT" { + return &ast.BoolExpr{ + Boolop: ast.BoolExprTypeNot, + Args: &ast.List{ + Items: []ast.Node{c.convertExpr(n.Operand)}, + }, + Location: n.Pos().Offset, + } + } + + return &ast.A_Expr{ + Name: &ast.List{ + Items: []ast.Node{&ast.String{Str: n.Op}}, + }, + Rexpr: c.convertExpr(n.Operand), + Location: n.Pos().Offset, + } +} + +func (c *cc) convertOrderByElement(n *chast.OrderByElement) *ast.SortBy { + sortBy := &ast.SortBy{ + Node: c.convertExpr(n.Expression), + Location: n.Expression.Pos().Offset, + } + + if n.Descending { + sortBy.SortbyDir = ast.SortByDirDesc + } else { + sortBy.SortbyDir = ast.SortByDirAsc + } + + if n.NullsFirst != nil { + if *n.NullsFirst { + sortBy.SortbyNulls = ast.SortByNullsFirst + } else { + sortBy.SortbyNulls = ast.SortByNullsLast + } + } + + return sortBy +} + +func (c *cc) convertInsertQuery(n *chast.InsertQuery) *ast.InsertStmt { + stmt := &ast.InsertStmt{ + Relation: &ast.RangeVar{ + Relname: &n.Table, + }, + } + + if n.Database != "" { + stmt.Relation.Schemaname = &n.Database + } + + // Convert column list + if len(n.Columns) > 0 { + stmt.Cols = &ast.List{} + for _, col := range n.Columns { + name := col.Name() + stmt.Cols.Items = append(stmt.Cols.Items, &ast.ResTarget{ + Name: &name, + }) + } + } + + // Convert SELECT subquery if present + if n.Select != nil { + stmt.SelectStmt = c.convert(n.Select) + } + + // Convert VALUES clause + if len(n.Values) > 0 { + selectStmt := &ast.SelectStmt{ + ValuesLists: &ast.List{}, + } + for _, row := range n.Values { + rowList := &ast.List{} + for _, val := range row { + rowList.Items = append(rowList.Items, c.convertExpr(val)) + } + selectStmt.ValuesLists.Items = append(selectStmt.ValuesLists.Items, rowList) + } + stmt.SelectStmt = selectStmt + } + + return stmt +} + +func (c *cc) convertCreateQuery(n *chast.CreateQuery) ast.Node { + // Handle CREATE DATABASE + if n.CreateDatabase { + return &ast.CreateSchemaStmt{ + Name: &n.Database, + IfNotExists: n.IfNotExists, + } + } + + // Handle CREATE TABLE + if n.Table != "" { + stmt := &ast.CreateTableStmt{ + Name: &ast.TableName{ + Name: identifier(n.Table), + }, + IfNotExists: n.IfNotExists, + } + + if n.Database != "" { + stmt.Name.Schema = identifier(n.Database) + } + + // Convert columns + for _, col := range n.Columns { + colDef := c.convertColumnDeclaration(col) + stmt.Cols = append(stmt.Cols, colDef) + } + + // Convert AS SELECT + if n.AsSelect != nil { + // This is a CREATE TABLE ... AS SELECT + // The AsSelect field contains the SELECT statement + } + + return stmt + } + + // Handle CREATE VIEW + if n.View != "" { + return &ast.ViewStmt{ + View: &ast.RangeVar{ + Relname: &n.View, + }, + Query: c.convert(n.AsSelect), + Replace: n.OrReplace, + } + } + + return &ast.TODO{} +} + +func (c *cc) convertColumnDeclaration(n *chast.ColumnDeclaration) *ast.ColumnDef { + colDef := &ast.ColumnDef{ + Colname: identifier(n.Name), + IsNotNull: isNotNull(n), + } + + if n.Type != nil { + colDef.TypeName = &ast.TypeName{ + Name: n.Type.Name, + } + // Handle type parameters (e.g., Decimal(10, 2)) + if len(n.Type.Parameters) > 0 { + colDef.TypeName.Typmods = &ast.List{} + for _, param := range n.Type.Parameters { + colDef.TypeName.Typmods.Items = append(colDef.TypeName.Typmods.Items, c.convertExpr(param)) + } + } + } + + // Handle PRIMARY KEY constraint + if n.PrimaryKey { + colDef.PrimaryKey = true + } + + // Handle DEFAULT + if n.Default != nil { + // colDef.RawDefault = c.convertExpr(n.Default) + } + + // Handle comment + if n.Comment != "" { + colDef.Comment = n.Comment + } + + return colDef +} + +func (c *cc) convertUpdateQuery(n *chast.UpdateQuery) *ast.UpdateStmt { + rv := &ast.RangeVar{ + Relname: &n.Table, + } + if n.Database != "" { + rv.Schemaname = &n.Database + } + stmt := &ast.UpdateStmt{ + Relations: &ast.List{ + Items: []ast.Node{rv}, + }, + } + + // Convert assignments + if len(n.Assignments) > 0 { + stmt.TargetList = &ast.List{} + for _, assign := range n.Assignments { + name := identifier(assign.Column) + stmt.TargetList.Items = append(stmt.TargetList.Items, &ast.ResTarget{ + Name: &name, + Val: c.convertExpr(assign.Value), + }) + } + } + + // Convert WHERE clause + if n.Where != nil { + stmt.WhereClause = c.convertExpr(n.Where) + } + + return stmt +} + +func (c *cc) convertDeleteQuery(n *chast.DeleteQuery) *ast.DeleteStmt { + rv := &ast.RangeVar{ + Relname: &n.Table, + } + if n.Database != "" { + rv.Schemaname = &n.Database + } + stmt := &ast.DeleteStmt{ + Relations: &ast.List{ + Items: []ast.Node{rv}, + }, + } + + // Convert WHERE clause + if n.Where != nil { + stmt.WhereClause = c.convertExpr(n.Where) + } + + return stmt +} + +func (c *cc) convertDropQuery(n *chast.DropQuery) ast.Node { + // Handle DROP TABLE + if n.Table != "" { + tableName := &ast.TableName{ + Name: identifier(n.Table), + } + if n.Database != "" { + tableName.Schema = identifier(n.Database) + } + return &ast.DropTableStmt{ + IfExists: n.IfExists, + Tables: []*ast.TableName{tableName}, + } + } + + // Handle DROP TABLE with multiple tables + if len(n.Tables) > 0 { + tables := make([]*ast.TableName, 0, len(n.Tables)) + for _, t := range n.Tables { + tables = append(tables, parseTableName(t)) + } + return &ast.DropTableStmt{ + IfExists: n.IfExists, + Tables: tables, + } + } + + // Handle DROP DATABASE - return TODO for now + // Handle DROP VIEW - return TODO for now + return &ast.TODO{} +} + +func (c *cc) convertAlterQuery(n *chast.AlterQuery) ast.Node { + alt := &ast.AlterTableStmt{ + Table: &ast.TableName{ + Name: identifier(n.Table), + }, + Cmds: &ast.List{}, + } + + if n.Database != "" { + alt.Table.Schema = identifier(n.Database) + } + + for _, cmd := range n.Commands { + switch cmd.Type { + case chast.AlterAddColumn: + if cmd.Column != nil { + name := cmd.Column.Name + alt.Cmds.Items = append(alt.Cmds.Items, &ast.AlterTableCmd{ + Name: &name, + Subtype: ast.AT_AddColumn, + Def: c.convertColumnDeclaration(cmd.Column), + }) + } + case chast.AlterDropColumn: + name := cmd.ColumnName + alt.Cmds.Items = append(alt.Cmds.Items, &ast.AlterTableCmd{ + Name: &name, + Subtype: ast.AT_DropColumn, + MissingOk: cmd.IfExists, + }) + case chast.AlterModifyColumn: + if cmd.Column != nil { + name := cmd.Column.Name + // Drop and re-add to simulate modify + alt.Cmds.Items = append(alt.Cmds.Items, &ast.AlterTableCmd{ + Name: &name, + Subtype: ast.AT_DropColumn, + }) + alt.Cmds.Items = append(alt.Cmds.Items, &ast.AlterTableCmd{ + Name: &name, + Subtype: ast.AT_AddColumn, + Def: c.convertColumnDeclaration(cmd.Column), + }) + } + case chast.AlterRenameColumn: + oldName := cmd.ColumnName + newName := cmd.NewName + return &ast.RenameColumnStmt{ + Table: alt.Table, + Col: &ast.ColumnRef{Name: oldName}, + NewName: &newName, + } + } + } + + return alt +} + +func (c *cc) convertTruncateQuery(n *chast.TruncateQuery) *ast.TruncateStmt { + stmt := &ast.TruncateStmt{ + Relations: &ast.List{}, + } + + tableName := n.Table + schemaName := n.Database + + rv := &ast.RangeVar{ + Relname: &tableName, + } + if schemaName != "" { + rv.Schemaname = &schemaName + } + + stmt.Relations.Items = append(stmt.Relations.Items, rv) + + return stmt +} diff --git a/internal/engine/clickhouse/format.go b/internal/engine/clickhouse/format.go new file mode 100644 index 0000000000..c103c7803f --- /dev/null +++ b/internal/engine/clickhouse/format.go @@ -0,0 +1,35 @@ +package clickhouse + +// QuoteIdent returns a quoted identifier if it needs quoting. +// ClickHouse uses backticks or double quotes for quoting identifiers. +func (p *Parser) QuoteIdent(s string) string { + // For now, don't quote - can be extended to quote when necessary + return s +} + +// TypeName returns the SQL type name for the given namespace and name. +func (p *Parser) TypeName(ns, name string) string { + if ns != "" { + return ns + "." + name + } + return name +} + +// Param returns the parameter placeholder for the given number. +// ClickHouse uses {name:Type} for named parameters, but for positional +// parameters we use ? which is supported by the clickhouse-go driver. +func (p *Parser) Param(n int) string { + return "?" +} + +// NamedParam returns the named parameter placeholder for the given name. +// ClickHouse uses {name:Type} syntax for named parameters. +func (p *Parser) NamedParam(name string) string { + return "{" + name + ":String}" +} + +// Cast returns a type cast expression. +// ClickHouse uses CAST(expr AS type) syntax, same as MySQL. +func (p *Parser) Cast(arg, typeName string) string { + return "CAST(" + arg + " AS " + typeName + ")" +} diff --git a/internal/engine/clickhouse/parse.go b/internal/engine/clickhouse/parse.go new file mode 100644 index 0000000000..282089f31d --- /dev/null +++ b/internal/engine/clickhouse/parse.go @@ -0,0 +1,64 @@ +package clickhouse + +import ( + "bytes" + "context" + "io" + + "github.com/sqlc-dev/doubleclick/parser" + + "github.com/sqlc-dev/sqlc/internal/source" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +func NewParser() *Parser { + return &Parser{} +} + +type Parser struct{} + +func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) { + blob, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + ctx := context.Background() + stmtNodes, err := parser.Parse(ctx, bytes.NewReader(blob)) + if err != nil { + return nil, err + } + + var stmts []ast.Statement + for _, stmt := range stmtNodes { + converter := &cc{} + out := converter.convert(stmt) + if _, ok := out.(*ast.TODO); ok { + continue + } + + // Get position information from the statement + pos := stmt.Pos() + end := stmt.End() + stmtLen := end.Offset - pos.Offset + + stmts = append(stmts, ast.Statement{ + Raw: &ast.RawStmt{ + Stmt: out, + StmtLocation: pos.Offset, + StmtLen: stmtLen, + }, + }) + } + + return stmts, nil +} + +// https://clickhouse.com/docs/en/sql-reference/syntax#comments +func (p *Parser) CommentSyntax() source.CommentSyntax { + return source.CommentSyntax{ + Dash: true, // -- comment + SlashStar: true, // /* comment */ + Hash: true, // # comment (ClickHouse supports this) + } +} diff --git a/internal/engine/clickhouse/parse_test.go b/internal/engine/clickhouse/parse_test.go new file mode 100644 index 0000000000..2edf3c117b --- /dev/null +++ b/internal/engine/clickhouse/parse_test.go @@ -0,0 +1,91 @@ +package clickhouse + +import ( + "strings" + "testing" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + sql string + count int // expected statement count + }{ + { + name: "simple select", + sql: "SELECT id, name FROM users", + count: 1, + }, + { + name: "select with where", + sql: "SELECT id, name FROM users WHERE id = 1", + count: 1, + }, + { + name: "create table", + sql: "CREATE TABLE users (id UInt64, name String) ENGINE = MergeTree() ORDER BY id", + count: 1, + }, + { + name: "insert", + sql: "INSERT INTO users (id, name) VALUES (1, 'test')", + count: 1, + }, + { + name: "multiple statements", + sql: "SELECT 1; SELECT 2", + count: 2, + }, + } + + p := NewParser() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stmts, err := p.Parse(strings.NewReader(tt.sql)) + if err != nil { + t.Fatalf("Parse() error = %v", err) + } + if len(stmts) != tt.count { + t.Errorf("Parse() returned %d statements, want %d", len(stmts), tt.count) + } + }) + } +} + +func TestCommentSyntax(t *testing.T) { + p := NewParser() + cs := p.CommentSyntax() + if !cs.Dash { + t.Error("expected Dash comment to be supported") + } + if !cs.SlashStar { + t.Error("expected SlashStar comment to be supported") + } + if !cs.Hash { + t.Error("expected Hash comment to be supported") + } +} + +func TestIsReservedKeyword(t *testing.T) { + p := NewParser() + tests := []struct { + word string + reserved bool + }{ + {"select", true}, + {"from", true}, + {"where", true}, + {"table", true}, + {"engine", true}, + {"foobar", false}, + {"mycolumn", false}, + } + + for _, tt := range tests { + t.Run(tt.word, func(t *testing.T) { + if got := p.IsReservedKeyword(tt.word); got != tt.reserved { + t.Errorf("IsReservedKeyword(%q) = %v, want %v", tt.word, got, tt.reserved) + } + }) + } +} diff --git a/internal/engine/clickhouse/reserved.go b/internal/engine/clickhouse/reserved.go new file mode 100644 index 0000000000..1a9ac45f3a --- /dev/null +++ b/internal/engine/clickhouse/reserved.go @@ -0,0 +1,150 @@ +package clickhouse + +import "strings" + +// https://clickhouse.com/docs/en/sql-reference/syntax#keywords +func (p *Parser) IsReservedKeyword(s string) bool { + switch strings.ToLower(s) { + case "add": + case "after": + case "alias": + case "all": + case "alter": + case "and": + case "anti": + case "any": + case "array": + case "as": + case "asc": + case "asof": + case "between": + case "both": + case "by": + case "case": + case "cast": + case "check": + case "cluster": + case "collate": + case "column": + case "comment": + case "constraint": + case "create": + case "cross": + case "cube": + case "database": + case "databases": + case "default": + case "delete": + case "desc": + case "describe": + case "detach": + case "distinct": + case "distributed": + case "drop": + case "else": + case "end": + case "engine": + case "exists": + case "explain": + case "expression": + case "extract": + case "false": + case "fetch": + case "final": + case "first": + case "for": + case "format": + case "from": + case "full": + case "function": + case "global": + case "grant": + case "group": + case "having": + case "if": + case "ilike": + case "in": + case "index": + case "inner": + case "insert": + case "interpolate": + case "interval": + case "into": + case "is": + case "join": + case "key": + case "kill": + case "last": + case "leading": + case "left": + case "like": + case "limit": + case "live": + case "local": + case "logs": + case "materialized": + case "modify": + case "natural": + case "not": + case "null": + case "nulls": + case "offset": + case "on": + case "optimize": + case "or": + case "order": + case "outer": + case "outfile": + case "over": + case "partition": + case "paste": + case "populate": + case "prewhere": + case "primary": + case "projection": + case "rename": + case "replace": + case "right": + case "rollup": + case "sample": + case "select": + case "semi": + case "set": + case "settings": + case "show": + case "storage": + case "substring": + case "sync": + case "system": + case "table": + case "tables": + case "temporary": + case "test": + case "then": + case "ties": + case "to": + case "top": + case "totals": + case "trailing": + case "trim": + case "true": + case "truncate": + case "ttl": + case "type": + case "union": + case "update": + case "use": + case "using": + case "uuid": + case "values": + case "view": + case "watch": + case "when": + case "where": + case "window": + case "with": + default: + return false + } + return true +} diff --git a/internal/engine/clickhouse/stdlib.go b/internal/engine/clickhouse/stdlib.go new file mode 100644 index 0000000000..edb8056912 --- /dev/null +++ b/internal/engine/clickhouse/stdlib.go @@ -0,0 +1,168 @@ +package clickhouse + +import ( + "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/catalog" +) + +func defaultSchema(name string) *catalog.Schema { + s := &catalog.Schema{Name: name} + s.Funcs = []*catalog.Function{ + // Aggregate functions + { + Name: "count", + Args: []*catalog.Argument{}, + ReturnType: &ast.TypeName{Name: "UInt64"}, + }, + { + Name: "sum", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "any"}}, + }, + ReturnType: &ast.TypeName{Name: "any"}, + }, + { + Name: "avg", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "any"}}, + }, + ReturnType: &ast.TypeName{Name: "Float64"}, + }, + { + Name: "min", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "any"}}, + }, + ReturnType: &ast.TypeName{Name: "any"}, + }, + { + Name: "max", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "any"}}, + }, + ReturnType: &ast.TypeName{Name: "any"}, + }, + // Type conversion functions + { + Name: "toInt32", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "any"}}, + }, + ReturnType: &ast.TypeName{Name: "Int32"}, + }, + { + Name: "toInt64", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "any"}}, + }, + ReturnType: &ast.TypeName{Name: "Int64"}, + }, + { + Name: "toUInt32", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "any"}}, + }, + ReturnType: &ast.TypeName{Name: "UInt32"}, + }, + { + Name: "toUInt64", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "any"}}, + }, + ReturnType: &ast.TypeName{Name: "UInt64"}, + }, + { + Name: "toString", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "any"}}, + }, + ReturnType: &ast.TypeName{Name: "String"}, + }, + { + Name: "toFloat64", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "any"}}, + }, + ReturnType: &ast.TypeName{Name: "Float64"}, + }, + // Date/time functions + { + Name: "now", + Args: []*catalog.Argument{}, + ReturnType: &ast.TypeName{Name: "DateTime"}, + }, + { + Name: "today", + Args: []*catalog.Argument{}, + ReturnType: &ast.TypeName{Name: "Date"}, + }, + { + Name: "toDate", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "any"}}, + }, + ReturnType: &ast.TypeName{Name: "Date"}, + }, + { + Name: "toDateTime", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "any"}}, + }, + ReturnType: &ast.TypeName{Name: "DateTime"}, + }, + // String functions + { + Name: "concat", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "String"}}, + {Type: &ast.TypeName{Name: "String"}}, + }, + ReturnType: &ast.TypeName{Name: "String"}, + }, + { + Name: "lower", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "String"}}, + }, + ReturnType: &ast.TypeName{Name: "String"}, + }, + { + Name: "upper", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "String"}}, + }, + ReturnType: &ast.TypeName{Name: "String"}, + }, + { + Name: "length", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "String"}}, + }, + ReturnType: &ast.TypeName{Name: "UInt64"}, + }, + // Conditional functions + { + Name: "if", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "UInt8"}}, + {Type: &ast.TypeName{Name: "any"}}, + {Type: &ast.TypeName{Name: "any"}}, + }, + ReturnType: &ast.TypeName{Name: "any"}, + }, + // Array functions + { + Name: "array", + Args: []*catalog.Argument{}, + ReturnType: &ast.TypeName{Name: "Array"}, + }, + { + Name: "arrayJoin", + Args: []*catalog.Argument{ + {Type: &ast.TypeName{Name: "Array"}}, + }, + ReturnType: &ast.TypeName{Name: "any"}, + }, + } + return s +} diff --git a/internal/engine/clickhouse/utils.go b/internal/engine/clickhouse/utils.go new file mode 100644 index 0000000000..9e52f4d5a7 --- /dev/null +++ b/internal/engine/clickhouse/utils.go @@ -0,0 +1,59 @@ +package clickhouse + +import ( + "log" + "strings" + + chast "github.com/sqlc-dev/doubleclick/ast" + + "github.com/sqlc-dev/sqlc/internal/debug" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +func todo(n chast.Node) *ast.TODO { + if debug.Active { + log.Printf("clickhouse.convert: Unknown node type %T\n", n) + } + return &ast.TODO{} +} + +func identifier(id string) string { + return strings.ToLower(id) +} + +func NewIdentifier(t string) *ast.String { + return &ast.String{Str: identifier(t)} +} + +func parseTableName(n *chast.TableIdentifier) *ast.TableName { + return &ast.TableName{ + Schema: identifier(n.Database), + Name: identifier(n.Table), + } +} + +func parseTableIdentifierToRangeVar(n *chast.TableIdentifier) *ast.RangeVar { + schemaname := identifier(n.Database) + relname := identifier(n.Table) + return &ast.RangeVar{ + Schemaname: &schemaname, + Relname: &relname, + } +} + +func isNotNull(n *chast.ColumnDeclaration) bool { + if n.Type == nil { + return false + } + // Check if type is wrapped in Nullable() + // If it's Nullable, it can be null, so return false + // If it's not Nullable, it's NOT NULL by default in ClickHouse + if n.Type.Name != "" && strings.ToLower(n.Type.Name) == "nullable" { + return false + } + // Also check if Nullable field is explicitly set + if n.Nullable != nil && *n.Nullable { + return false + } + return true +} From c90b86cfc1ce8a4a326f9dfce16d19128696cc42 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 16 Jan 2026 20:44:01 -0800 Subject: [PATCH 2/4] chore: remove test file Co-Authored-By: Claude --- internal/engine/clickhouse/parse_test.go | 91 ------------------------ 1 file changed, 91 deletions(-) delete mode 100644 internal/engine/clickhouse/parse_test.go diff --git a/internal/engine/clickhouse/parse_test.go b/internal/engine/clickhouse/parse_test.go deleted file mode 100644 index 2edf3c117b..0000000000 --- a/internal/engine/clickhouse/parse_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package clickhouse - -import ( - "strings" - "testing" -) - -func TestParse(t *testing.T) { - tests := []struct { - name string - sql string - count int // expected statement count - }{ - { - name: "simple select", - sql: "SELECT id, name FROM users", - count: 1, - }, - { - name: "select with where", - sql: "SELECT id, name FROM users WHERE id = 1", - count: 1, - }, - { - name: "create table", - sql: "CREATE TABLE users (id UInt64, name String) ENGINE = MergeTree() ORDER BY id", - count: 1, - }, - { - name: "insert", - sql: "INSERT INTO users (id, name) VALUES (1, 'test')", - count: 1, - }, - { - name: "multiple statements", - sql: "SELECT 1; SELECT 2", - count: 2, - }, - } - - p := NewParser() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - stmts, err := p.Parse(strings.NewReader(tt.sql)) - if err != nil { - t.Fatalf("Parse() error = %v", err) - } - if len(stmts) != tt.count { - t.Errorf("Parse() returned %d statements, want %d", len(stmts), tt.count) - } - }) - } -} - -func TestCommentSyntax(t *testing.T) { - p := NewParser() - cs := p.CommentSyntax() - if !cs.Dash { - t.Error("expected Dash comment to be supported") - } - if !cs.SlashStar { - t.Error("expected SlashStar comment to be supported") - } - if !cs.Hash { - t.Error("expected Hash comment to be supported") - } -} - -func TestIsReservedKeyword(t *testing.T) { - p := NewParser() - tests := []struct { - word string - reserved bool - }{ - {"select", true}, - {"from", true}, - {"where", true}, - {"table", true}, - {"engine", true}, - {"foobar", false}, - {"mycolumn", false}, - } - - for _, tt := range tests { - t.Run(tt.word, func(t *testing.T) { - if got := p.IsReservedKeyword(tt.word); got != tt.reserved { - t.Errorf("IsReservedKeyword(%q) = %v, want %v", tt.word, got, tt.reserved) - } - }) - } -} From dbd5583986c3c834f6cdab238d3cf69ebef58702 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 16 Jan 2026 20:44:37 -0800 Subject: [PATCH 3/4] chore: return empty function set from stdlib Co-Authored-By: Claude --- internal/engine/clickhouse/stdlib.go | 161 +-------------------------- 1 file changed, 1 insertion(+), 160 deletions(-) diff --git a/internal/engine/clickhouse/stdlib.go b/internal/engine/clickhouse/stdlib.go index edb8056912..da7b53ab21 100644 --- a/internal/engine/clickhouse/stdlib.go +++ b/internal/engine/clickhouse/stdlib.go @@ -1,168 +1,9 @@ package clickhouse import ( - "github.com/sqlc-dev/sqlc/internal/sql/ast" "github.com/sqlc-dev/sqlc/internal/sql/catalog" ) func defaultSchema(name string) *catalog.Schema { - s := &catalog.Schema{Name: name} - s.Funcs = []*catalog.Function{ - // Aggregate functions - { - Name: "count", - Args: []*catalog.Argument{}, - ReturnType: &ast.TypeName{Name: "UInt64"}, - }, - { - Name: "sum", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "any"}}, - }, - ReturnType: &ast.TypeName{Name: "any"}, - }, - { - Name: "avg", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "any"}}, - }, - ReturnType: &ast.TypeName{Name: "Float64"}, - }, - { - Name: "min", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "any"}}, - }, - ReturnType: &ast.TypeName{Name: "any"}, - }, - { - Name: "max", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "any"}}, - }, - ReturnType: &ast.TypeName{Name: "any"}, - }, - // Type conversion functions - { - Name: "toInt32", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "any"}}, - }, - ReturnType: &ast.TypeName{Name: "Int32"}, - }, - { - Name: "toInt64", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "any"}}, - }, - ReturnType: &ast.TypeName{Name: "Int64"}, - }, - { - Name: "toUInt32", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "any"}}, - }, - ReturnType: &ast.TypeName{Name: "UInt32"}, - }, - { - Name: "toUInt64", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "any"}}, - }, - ReturnType: &ast.TypeName{Name: "UInt64"}, - }, - { - Name: "toString", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "any"}}, - }, - ReturnType: &ast.TypeName{Name: "String"}, - }, - { - Name: "toFloat64", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "any"}}, - }, - ReturnType: &ast.TypeName{Name: "Float64"}, - }, - // Date/time functions - { - Name: "now", - Args: []*catalog.Argument{}, - ReturnType: &ast.TypeName{Name: "DateTime"}, - }, - { - Name: "today", - Args: []*catalog.Argument{}, - ReturnType: &ast.TypeName{Name: "Date"}, - }, - { - Name: "toDate", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "any"}}, - }, - ReturnType: &ast.TypeName{Name: "Date"}, - }, - { - Name: "toDateTime", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "any"}}, - }, - ReturnType: &ast.TypeName{Name: "DateTime"}, - }, - // String functions - { - Name: "concat", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "String"}}, - {Type: &ast.TypeName{Name: "String"}}, - }, - ReturnType: &ast.TypeName{Name: "String"}, - }, - { - Name: "lower", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "String"}}, - }, - ReturnType: &ast.TypeName{Name: "String"}, - }, - { - Name: "upper", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "String"}}, - }, - ReturnType: &ast.TypeName{Name: "String"}, - }, - { - Name: "length", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "String"}}, - }, - ReturnType: &ast.TypeName{Name: "UInt64"}, - }, - // Conditional functions - { - Name: "if", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "UInt8"}}, - {Type: &ast.TypeName{Name: "any"}}, - {Type: &ast.TypeName{Name: "any"}}, - }, - ReturnType: &ast.TypeName{Name: "any"}, - }, - // Array functions - { - Name: "array", - Args: []*catalog.Argument{}, - ReturnType: &ast.TypeName{Name: "Array"}, - }, - { - Name: "arrayJoin", - Args: []*catalog.Argument{ - {Type: &ast.TypeName{Name: "Array"}}, - }, - ReturnType: &ast.TypeName{Name: "any"}, - }, - } - return s + return &catalog.Schema{Name: name} } From 4b880fd7dda357fa63ef36313a01878ce67d316c Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 16 Jan 2026 20:47:43 -0800 Subject: [PATCH 4/4] refactor: move ClickHouse support to parse command only Remove ClickHouse from compiler/engine.go and config.go. Add --dialect clickhouse support to the sqlc parse command. Co-Authored-By: Claude --- internal/cmd/parse.go | 13 ++++++++++--- internal/compiler/engine.go | 5 ----- internal/config/config.go | 7 +++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/internal/cmd/parse.go b/internal/cmd/parse.go index b9e26c072e..aca01511f1 100644 --- a/internal/cmd/parse.go +++ b/internal/cmd/parse.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" + "github.com/sqlc-dev/sqlc/internal/engine/clickhouse" "github.com/sqlc-dev/sqlc/internal/engine/dolphin" "github.com/sqlc-dev/sqlc/internal/engine/postgresql" "github.com/sqlc-dev/sqlc/internal/engine/sqlite" @@ -27,7 +28,10 @@ Examples: echo "SELECT * FROM users" | sqlc parse --dialect mysql # Parse SQLite SQL - sqlc parse --dialect sqlite queries.sql`, + sqlc parse --dialect sqlite queries.sql + + # Parse ClickHouse SQL + sqlc parse --dialect clickhouse queries.sql`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { dialect, err := cmd.Flags().GetString("dialect") @@ -35,7 +39,7 @@ Examples: return err } if dialect == "" { - return fmt.Errorf("--dialect flag is required (postgresql, mysql, or sqlite)") + return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, or clickhouse)") } // Determine input source @@ -71,8 +75,11 @@ Examples: case "sqlite": parser := sqlite.NewParser() stmts, err = parser.Parse(input) + case "clickhouse": + parser := clickhouse.NewParser() + stmts, err = parser.Parse(input) default: - return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, or sqlite)", dialect) + return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, or clickhouse)", dialect) } if err != nil { return fmt.Errorf("parse error: %w", err) diff --git a/internal/compiler/engine.go b/internal/compiler/engine.go index 0f8d5ca0f9..64fdf3d5c7 100644 --- a/internal/compiler/engine.go +++ b/internal/compiler/engine.go @@ -7,7 +7,6 @@ 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" "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" @@ -83,10 +82,6 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts c.parser = dolphin.NewParser() c.catalog = dolphin.NewCatalog() c.selector = newDefaultSelector() - case config.EngineClickHouse: - c.parser = clickhouse.NewParser() - c.catalog = clickhouse.NewCatalog() - c.selector = newDefaultSelector() case config.EnginePostgreSQL: parser := postgresql.NewParser() c.parser = parser diff --git a/internal/config/config.go b/internal/config/config.go index 59e31ae233..d3e610ef05 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -51,10 +51,9 @@ func (p *Paths) UnmarshalYAML(unmarshal func(interface{}) error) error { } const ( - EngineMySQL Engine = "mysql" - EnginePostgreSQL Engine = "postgresql" - EngineSQLite Engine = "sqlite" - EngineClickHouse Engine = "clickhouse" + EngineMySQL Engine = "mysql" + EnginePostgreSQL Engine = "postgresql" + EngineSQLite Engine = "sqlite" ) type Config struct {