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/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/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/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..da7b53ab21 --- /dev/null +++ b/internal/engine/clickhouse/stdlib.go @@ -0,0 +1,9 @@ +package clickhouse + +import ( + "github.com/sqlc-dev/sqlc/internal/sql/catalog" +) + +func defaultSchema(name string) *catalog.Schema { + return &catalog.Schema{Name: name} +} 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 +}