diff --git a/go.mod b/go.mod index 1da5cb6bc2..c89cfa38f8 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/spf13/pflag v1.0.10 github.com/sqlc-dev/doubleclick v1.0.0 github.com/sqlc-dev/marino v0.1.0 + github.com/sqlc-dev/zetajones v0.1.0 github.com/tetratelabs/wazero v1.12.0 github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 github.com/xeipuuv/gojsonschema v1.2.0 diff --git a/go.sum b/go.sum index 349663d4a3..017fc2647b 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,8 @@ github.com/sqlc-dev/doubleclick v1.0.0 h1:2/OApfQ2eLgcfa/Fqs8WSMA6atH0G8j9hHbQIg github.com/sqlc-dev/doubleclick v1.0.0/go.mod h1:ODHRroSrk/rr5neRHlWMSRijqOak8YmNaO3VAZCNl5Y= github.com/sqlc-dev/marino v0.1.0 h1:8Fn13vFhx7OUcmDFfRZdf3zARAbNl04Lcy74211ZpIw= github.com/sqlc-dev/marino v0.1.0/go.mod h1:mQxC2dgDE0DWHMb2B5jZNk7KToJuS6wnxnffBfYnq08= +github.com/sqlc-dev/zetajones v0.1.0 h1:VeG0atx6lNABr9V2bSI5vL9DvOKTHX0XjMqWUE/rv40= +github.com/sqlc-dev/zetajones v0.1.0/go.mod h1:dU1DxwqC6Cahbpnw16KpH1J2waWRDMdwyDSvovMZR4I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/internal/cmd/parse.go b/internal/cmd/parse.go index a68ad1bee8..2fc4c763e8 100644 --- a/internal/cmd/parse.go +++ b/internal/cmd/parse.go @@ -11,6 +11,7 @@ import ( "github.com/sqlc-dev/sqlc/internal/engine/clickhouse" "github.com/sqlc-dev/sqlc/internal/engine/dolphin" + "github.com/sqlc-dev/sqlc/internal/engine/googlesql" "github.com/sqlc-dev/sqlc/internal/engine/postgresql" "github.com/sqlc-dev/sqlc/internal/engine/sqlite" "github.com/sqlc-dev/sqlc/internal/metadata" @@ -55,7 +56,10 @@ Examples: sqlc parse --dialect sqlite queries.sql # Parse ClickHouse SQL - sqlc parse --dialect clickhouse queries.sql`, + sqlc parse --dialect clickhouse queries.sql + + # Parse GoogleSQL (BigQuery, Spanner) + sqlc parse --dialect googlesql queries.sql`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { dialect, err := cmd.Flags().GetString("dialect") @@ -63,7 +67,7 @@ Examples: return err } if dialect == "" { - return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, or clickhouse)") + return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, clickhouse, or googlesql)") } // Determine input source @@ -98,8 +102,10 @@ Examples: parser = sqlite.NewParser() case "clickhouse": parser = clickhouse.NewParser() + case "googlesql": + parser = googlesql.NewParser() default: - return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, or clickhouse)", dialect) + return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, clickhouse, or googlesql)", dialect) } // Read the full source so each statement's name and command can be @@ -143,6 +149,6 @@ Examples: return nil }, } - cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, sqlite, or clickhouse)") + cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, sqlite, clickhouse, or googlesql)") return cmd } diff --git a/internal/endtoend/testdata/parse_basic/googlesql/exec.json b/internal/endtoend/testdata/parse_basic/googlesql/exec.json new file mode 100644 index 0000000000..6931f13662 --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/googlesql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "parse", + "args": ["--dialect", "googlesql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/parse_basic/googlesql/query.sql b/internal/endtoend/testdata/parse_basic/googlesql/query.sql new file mode 100644 index 0000000000..11dff59f08 --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/googlesql/query.sql @@ -0,0 +1,2 @@ +-- name: GetValue :one +SELECT 1; diff --git a/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt b/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt new file mode 100644 index 0000000000..db7128d126 --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt @@ -0,0 +1,42 @@ +[ + { + "ast": { + "Stmt": { + "DistinctClause": null, + "IntoClause": null, + "TargetList": { + "Items": [ + { + "Name": null, + "Indirection": null, + "Val": { + "Val": { + "Ival": 1 + }, + "Location": 30 + }, + "Location": 30 + } + ] + }, + "FromClause": null, + "WhereClause": null, + "GroupClause": null, + "HavingClause": null, + "WindowClause": null, + "ValuesLists": null, + "SortClause": null, + "LimitOffset": null, + "LimitCount": null, + "LockingClause": null, + "WithClause": null, + "Op": 0, + "All": false, + "Larg": null, + "Rarg": null + }, + "StmtLocation": 23, + "StmtLen": 8 + } + } +] diff --git a/internal/engine/googlesql/catalog.go b/internal/engine/googlesql/catalog.go new file mode 100644 index 0000000000..33dd9398a2 --- /dev/null +++ b/internal/engine/googlesql/catalog.go @@ -0,0 +1,16 @@ +package googlesql + +import ( + "github.com/sqlc-dev/sqlc/internal/sql/catalog" +) + +func NewCatalog() *catalog.Catalog { + def := "main" // GoogleSQL has no implicit schema; use "main" as the default + return &catalog.Catalog{ + DefaultSchema: def, + Schemas: []*catalog.Schema{ + defaultSchema(def), + }, + Extensions: map[string]struct{}{}, + } +} diff --git a/internal/engine/googlesql/convert.go b/internal/engine/googlesql/convert.go new file mode 100644 index 0000000000..ae84839a89 --- /dev/null +++ b/internal/engine/googlesql/convert.go @@ -0,0 +1,960 @@ +package googlesql + +import ( + "strconv" + "strings" + + zjast "github.com/sqlc-dev/zetajones/ast" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +type cc struct { + paramCount int +} + +func (c *cc) convert(node zjast.Node) ast.Node { + switch n := node.(type) { + case *zjast.QueryStatement: + return c.convertQuery(n.Query) + case *zjast.InsertStatement: + return c.convertInsertStatement(n) + case *zjast.UpdateStatement: + return c.convertUpdateStatement(n) + case *zjast.DeleteStatement: + return c.convertDeleteStatement(n) + case *zjast.CreateTableStatement: + return c.convertCreateTableStatement(n) + case *zjast.DropStatement: + return c.convertDropStatement(n) + case *zjast.TruncateStatement: + return c.convertTruncateStatement(n) + case *zjast.HintedStatement: + if n.Statement == nil { + return todo(n) + } + return c.convert(n.Statement) + default: + return todo(n) + } +} + +func (c *cc) convertQuery(n *zjast.Query) ast.Node { + if n == nil { + return todo(n) + } + // Pipe operators (|>) have no sqlc AST equivalent. + if len(n.PipeOperators) > 0 { + return todo(n) + } + node := c.convertQueryExpr(n.QueryExpr) + stmt, ok := node.(*ast.SelectStmt) + if !ok { + return node + } + if n.WithClause != nil { + stmt.WithClause = c.convertWithClause(n.WithClause) + } + if n.OrderBy != nil { + stmt.SortClause = c.convertOrderBy(n.OrderBy) + } + if n.Limit != nil { + if n.Limit.Limit != nil { + if _, all := n.Limit.Limit.Expr.(*zjast.LimitAll); !all { + stmt.LimitCount = c.convertExpr(n.Limit.Limit.Expr) + } + } + if n.Limit.Offset != nil { + stmt.LimitOffset = c.convertExpr(n.Limit.Offset) + } + } + return stmt +} + +func (c *cc) convertQueryExpr(node zjast.Node) ast.Node { + switch n := node.(type) { + case *zjast.Select: + return c.convertSelect(n) + case *zjast.SetOperation: + return c.convertSetOperation(n) + case *zjast.Query: + // Parenthesized query. + return c.convertQuery(n) + case *zjast.AliasedQueryExpression: + return c.convertQuery(n.Query) + default: + return todo(n) + } +} + +func (c *cc) convertSetOperation(n *zjast.SetOperation) ast.Node { + if len(n.Inputs) == 0 { + return todo(n) + } + var entries []*zjast.SetOperationMetadata + if n.Metadata != nil { + entries = n.Metadata.Entries + } + result, ok := c.convertQueryExpr(n.Inputs[0]).(*ast.SelectStmt) + if !ok { + return todo(n) + } + for i, input := range n.Inputs[1:] { + rarg, ok := c.convertQueryExpr(input).(*ast.SelectStmt) + if !ok { + return todo(n) + } + op := ast.Union + all := false + if i < len(entries) && entries[i] != nil { + meta := entries[i] + if meta.OpType != nil { + switch strings.ToUpper(meta.OpType.Op) { + case "UNION": + op = ast.Union + case "INTERSECT": + op = ast.Intersect + case "EXCEPT": + op = ast.Except + } + } + if meta.AllOrDistinct != nil { + all = strings.ToUpper(meta.AllOrDistinct.Value) == "ALL" + } + } + result = &ast.SelectStmt{ + Op: op, + All: all, + Larg: result, + Rarg: rarg, + } + } + return result +} + +func (c *cc) convertSelect(n *zjast.Select) *ast.SelectStmt { + stmt := &ast.SelectStmt{} + + if n.SelectList != nil { + stmt.TargetList = &ast.List{} + for _, col := range n.SelectList.Columns { + if col == nil { + continue + } + stmt.TargetList.Items = append(stmt.TargetList.Items, c.convertSelectColumn(col)) + } + } + + if n.From != nil { + stmt.FromClause = &ast.List{ + Items: []ast.Node{c.convertTableExpr(n.From.TableExpression)}, + } + } + + if n.Where != nil { + stmt.WhereClause = c.convertExpr(n.Where.Expr) + } + + if n.GroupBy != nil { + stmt.GroupClause = &ast.List{} + for _, item := range n.GroupBy.Items { + if item == nil { + continue + } + if item.Expr != nil { + stmt.GroupClause.Items = append(stmt.GroupClause.Items, c.convertExpr(item.Expr)) + } else { + // ROLLUP, CUBE, GROUPING SETS, or the empty grouping item. + stmt.GroupClause.Items = append(stmt.GroupClause.Items, todo(item)) + } + } + } + + if n.Having != nil { + stmt.HavingClause = c.convertExpr(n.Having.Expr) + } + + if n.Distinct { + stmt.DistinctClause = &ast.List{} + } + + return stmt +} + +func (c *cc) convertSelectColumn(n *zjast.SelectColumn) *ast.ResTarget { + res := &ast.ResTarget{ + Location: n.Pos(), + } + if n.Alias != nil && n.Alias.Identifier != nil { + name := identifier(n.Alias.Identifier.Name) + res.Name = &name + } + res.Val = c.convertExpr(n.Expr) + return res +} + +func (c *cc) convertTableExpr(node zjast.Node) ast.Node { + switch n := node.(type) { + case *zjast.TablePathExpression: + return c.convertTablePathExpression(n) + case *zjast.TableSubquery: + if len(n.PostfixOperators) > 0 { + return todo(n) + } + return &ast.RangeSubselect{ + Lateral: n.IsLateral, + Subquery: c.convertQuery(n.Query), + Alias: convertAlias(n.Alias), + } + case *zjast.Join: + return c.convertJoin(n) + case *zjast.ParenthesizedJoin: + if len(n.PostfixOperators) > 0 { + return todo(n) + } + return c.convertTableExpr(n.Join) + case *zjast.TVF: + return c.convertTVF(n) + default: + return todo(n) + } +} + +func (c *cc) convertTablePathExpression(n *zjast.TablePathExpression) ast.Node { + if len(n.PostfixOperators) > 0 { + return todo(n) + } + if n.UnnestExpr != nil { + return &ast.RangeFunction{ + Functions: &ast.List{ + Items: []ast.Node{c.convertUnnestExpression(n.UnnestExpr)}, + }, + Alias: convertAlias(n.Alias), + } + } + rv := parseRangeVar(n.Path) + rv.Alias = convertAlias(n.Alias) + return rv +} + +func (c *cc) convertUnnestExpression(n *zjast.UnnestExpression) *ast.FuncCall { + fc := &ast.FuncCall{ + Func: &ast.FuncName{Name: "unnest"}, + Funcname: &ast.List{Items: []ast.Node{&ast.String{Str: "unnest"}}}, + Args: &ast.List{}, + Location: n.Pos(), + } + for _, e := range n.Expressions { + if e == nil { + continue + } + fc.Args.Items = append(fc.Args.Items, c.convertExpr(e.Expr)) + } + return fc +} + +func (c *cc) convertTVF(n *zjast.TVF) ast.Node { + if len(n.PostfixOperators) > 0 { + return todo(n) + } + parts := pathParts(n.Name) + if len(parts) == 0 { + return todo(n) + } + fc := c.newFuncCall(parts, n.Pos()) + for _, arg := range n.Args { + if arg == nil || arg.Expr == nil { + continue + } + fc.Args.Items = append(fc.Args.Items, c.convertExpr(arg.Expr)) + } + return &ast.RangeFunction{ + Functions: &ast.List{Items: []ast.Node{fc}}, + Alias: convertAlias(n.Alias), + } +} + +func (c *cc) convertJoin(n *zjast.Join) ast.Node { + join := &ast.JoinExpr{ + Larg: c.convertTableExpr(n.Lhs), + Rarg: c.convertTableExpr(n.Rhs), + IsNatural: n.Natural, + } + + switch strings.ToUpper(n.JoinType) { + case "", "INNER", "CROSS", "COMMA": + join.Jointype = ast.JoinTypeInner + case "LEFT": + join.Jointype = ast.JoinTypeLeft + case "RIGHT": + join.Jointype = ast.JoinTypeRight + case "FULL": + join.Jointype = ast.JoinTypeFull + default: + join.Jointype = ast.JoinTypeInner + } + + switch clause := n.OnOrUsingClause.(type) { + case *zjast.OnClause: + join.Quals = c.convertExpr(clause.Expr) + case *zjast.UsingClause: + join.UsingClause = &ast.List{} + for _, key := range clause.Keys { + if key == nil { + continue + } + join.UsingClause.Items = append(join.UsingClause.Items, NewIdentifier(key.Name)) + } + } + + return join +} + +func (c *cc) convertWithClause(n *zjast.WithClause) *ast.WithClause { + wc := &ast.WithClause{ + Recursive: n.Recursive, + Ctes: &ast.List{}, + Location: n.Pos(), + } + for _, entry := range n.Entries { + if entry == nil || entry.AliasedQuery == nil { + // "name() AS GROUP ROWS" entries are not supported. + continue + } + aq := entry.AliasedQuery + cte := &ast.CommonTableExpr{ + Location: aq.Pos(), + Ctequery: c.convertQuery(aq.Query), + } + if aq.Identifier != nil { + name := identifier(aq.Identifier.Name) + cte.Ctename = &name + } + wc.Ctes.Items = append(wc.Ctes.Items, cte) + } + return wc +} + +func (c *cc) convertOrderBy(n *zjast.OrderBy) *ast.List { + list := &ast.List{} + for _, item := range n.Items { + if item == nil { + continue + } + sortBy := &ast.SortBy{ + Node: c.convertExpr(item.Expr), + Location: item.Pos(), + } + switch { + case item.Descending: + sortBy.SortbyDir = ast.SortByDirDesc + case item.HasAsc: + sortBy.SortbyDir = ast.SortByDirAsc + default: + sortBy.SortbyDir = ast.SortByDirDefault + } + if item.NullOrder != nil { + if item.NullOrder.NullsFirst { + sortBy.SortbyNulls = ast.SortByNullsFirst + } else { + sortBy.SortbyNulls = ast.SortByNullsLast + } + } + list.Items = append(list.Items, sortBy) + } + return list +} + +func (c *cc) convertExpr(node zjast.Node) ast.Node { + if node == nil { + return nil + } + switch n := node.(type) { + case *zjast.Identifier: + return &ast.ColumnRef{ + Fields: &ast.List{Items: []ast.Node{NewIdentifier(n.Name)}}, + Location: n.Pos(), + } + case *zjast.PathExpression: + return c.convertPathExpression(n) + case *zjast.DotIdentifier: + if fields, ok := columnRefFields(n); ok { + return &ast.ColumnRef{ + Fields: &ast.List{Items: fields}, + Location: n.Pos(), + } + } + return todo(n) + case *zjast.IntLiteral: + return c.convertIntLiteral(n) + case *zjast.FloatLiteral: + return &ast.A_Const{ + Val: &ast.Float{Str: n.Image}, + Location: n.Pos(), + } + case *zjast.StringLiteral: + return &ast.A_Const{ + Val: &ast.String{Str: stringLiteralValue(n)}, + Location: n.Pos(), + } + case *zjast.BooleanLiteral: + return &ast.A_Const{ + Val: &ast.Boolean{Boolval: n.Value}, + Location: n.Pos(), + } + case *zjast.NullLiteral: + return &ast.A_Const{ + Val: &ast.Null{}, + Location: n.Pos(), + } + case *zjast.ParameterExpr: + return c.convertParameterExpr(n) + case *zjast.UnaryExpression: + return c.convertUnaryExpression(n) + case *zjast.BinaryExpression: + return c.convertBinaryExpression(n) + case *zjast.BitwiseShiftExpression: + op := ">>" + if n.IsLeftShift { + op = "<<" + } + return &ast.A_Expr{ + Name: &ast.List{Items: []ast.Node{&ast.String{Str: op}}}, + Lexpr: c.convertExpr(n.Lhs), + Rexpr: c.convertExpr(n.Rhs), + Location: n.Pos(), + } + case *zjast.AndExpr: + return c.convertBoolExpr(ast.BoolExprTypeAnd, n.Conjuncts, n.Pos()) + case *zjast.OrExpr: + return c.convertBoolExpr(ast.BoolExprTypeOr, n.Disjuncts, n.Pos()) + case *zjast.BetweenExpression: + return &ast.BetweenExpr{ + Expr: c.convertExpr(n.Lhs), + Left: c.convertExpr(n.Low), + Right: c.convertExpr(n.High), + Not: n.IsNot, + Location: n.Pos(), + } + case *zjast.InExpression: + return c.convertInExpression(n) + case *zjast.CaseValueExpression: + if len(n.Arguments) == 0 { + return todo(n) + } + return c.buildCaseExpr(c.convertExpr(n.Arguments[0]), n.Arguments[1:], n.Pos()) + case *zjast.CaseNoValueExpression: + return c.buildCaseExpr(nil, n.Arguments, n.Pos()) + case *zjast.CastExpression: + return &ast.TypeCast{ + Arg: c.convertExpr(n.Expr), + TypeName: &ast.TypeName{Name: typeName(n.Type)}, + Location: n.Pos(), + } + case *zjast.ExpressionSubquery: + return c.convertExpressionSubquery(n) + case *zjast.FunctionCall: + return c.convertFunctionCall(n) + case *zjast.AnalyticFunctionCall: + return c.convertAnalyticFunctionCall(n) + case *zjast.Star: + return &ast.ColumnRef{ + Fields: &ast.List{Items: []ast.Node{&ast.A_Star{}}}, + Location: n.Pos(), + } + case *zjast.DotStar: + return c.convertDotStar(n) + case *zjast.ArrayElement: + return &ast.A_Indirection{ + Arg: c.convertExpr(n.Array), + Indirection: &ast.List{ + Items: []ast.Node{ + &ast.A_Indices{Uidx: c.convertExpr(n.Position)}, + }, + }, + } + case *zjast.NamedArgument: + return c.convertExpr(n.Value) + case *zjast.ExpressionWithAlias: + return c.convertExpr(n.Expression) + default: + return todo(n) + } +} + +func (c *cc) convertPathExpression(n *zjast.PathExpression) ast.Node { + fields, ok := columnRefFields(n) + if !ok { + return todo(n) + } + return &ast.ColumnRef{ + Fields: &ast.List{Items: fields}, + Location: n.Pos(), + } +} + +func (c *cc) convertIntLiteral(n *zjast.IntLiteral) ast.Node { + // Base 0 also accepts hexadecimal literals (0x1a). + ival, err := strconv.ParseInt(n.Image, 0, 64) + if err != nil { + return &ast.A_Const{ + Val: &ast.Float{Str: n.Image}, + Location: n.Pos(), + } + } + return &ast.A_Const{ + Val: &ast.Integer{Ival: ival}, + Location: n.Pos(), + } +} + +// convertParameterExpr converts "@name" and "?" query parameters. +// +// Named parameters are mapped to the shape the PostgreSQL and SQLite engines +// produce for "@name": an A_Expr with the operator "@" and the parameter name +// as the right operand. That is the representation sqlc's named-parameter +// rewriter (internal/sql/rewrite, named.IsParamSign) already understands. +// Positional "?" parameters become ParamRef nodes numbered by their 1-based +// position in the statement. +func (c *cc) convertParameterExpr(n *zjast.ParameterExpr) ast.Node { + if n.Name != nil { + return &ast.A_Expr{ + Name: &ast.List{Items: []ast.Node{&ast.String{Str: "@"}}}, + Rexpr: &ast.String{Str: n.Name.Name}, + Location: n.Pos(), + } + } + c.paramCount++ + number := n.Position + if number == 0 { + number = c.paramCount + } + return &ast.ParamRef{ + Number: number, + Location: n.Pos(), + } +} + +func (c *cc) convertUnaryExpression(n *zjast.UnaryExpression) ast.Node { + if strings.ToUpper(n.Op) == "NOT" { + return &ast.BoolExpr{ + Boolop: ast.BoolExprTypeNot, + Args: &ast.List{ + Items: []ast.Node{c.convertExpr(n.Operand)}, + }, + Location: n.Pos(), + } + } + return &ast.A_Expr{ + Name: &ast.List{Items: []ast.Node{&ast.String{Str: n.Op}}}, + Rexpr: c.convertExpr(n.Operand), + Location: n.Pos(), + } +} + +func (c *cc) convertBinaryExpression(n *zjast.BinaryExpression) ast.Node { + // "x IS NULL" and "x IS NOT NULL" become NullTest nodes. + if strings.ToUpper(n.Op) == "IS" { + if _, isNull := n.Right.(*zjast.NullLiteral); isNull { + nullTest := &ast.NullTest{ + Arg: c.convertExpr(n.Left), + Location: n.Pos(), + } + if n.IsNot { + nullTest.Nulltesttype = ast.NullTestTypeIsNotNull + } else { + nullTest.Nulltesttype = ast.NullTestTypeIsNull + } + return nullTest + } + } + + var expr ast.Node = &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(), + } + if n.IsNot { + // "a NOT LIKE b", "a IS NOT b", etc: wrap the comparison in a NOT. + expr = &ast.BoolExpr{ + Boolop: ast.BoolExprTypeNot, + Args: &ast.List{Items: []ast.Node{expr}}, + Location: n.Pos(), + } + } + return expr +} + +func (c *cc) convertBoolExpr(op ast.BoolExprType, operands []zjast.Node, loc int) *ast.BoolExpr { + expr := &ast.BoolExpr{ + Boolop: op, + Args: &ast.List{}, + Location: loc, + } + for _, operand := range operands { + expr.Args.Items = append(expr.Args.Items, c.convertExpr(operand)) + } + return expr +} + +func (c *cc) convertInExpression(n *zjast.InExpression) ast.Node { + in := &ast.In{ + Expr: c.convertExpr(n.Lhs), + Not: n.IsNot, + Location: n.Pos(), + } + if n.List != nil { + for _, expr := range n.List.Exprs { + in.List = append(in.List, c.convertExpr(expr)) + } + } + if n.UnnestExpr != nil { + in.List = append(in.List, c.convertUnnestExpression(n.UnnestExpr)) + } + if n.Query != nil { + in.Sel = c.convertQuery(n.Query) + } + return in +} + +// buildCaseExpr builds a CaseExpr from the alternating WHEN/THEN expression +// pairs (and optional trailing ELSE expression) of a zetajones CASE node. +func (c *cc) buildCaseExpr(arg ast.Node, rest []zjast.Node, loc int) *ast.CaseExpr { + caseExpr := &ast.CaseExpr{ + Arg: arg, + Args: &ast.List{}, + Location: loc, + } + i := 0 + for ; i+1 < len(rest); i += 2 { + caseExpr.Args.Items = append(caseExpr.Args.Items, &ast.CaseWhen{ + Expr: c.convertExpr(rest[i]), + Result: c.convertExpr(rest[i+1]), + }) + } + if i < len(rest) { + caseExpr.Defresult = c.convertExpr(rest[i]) + } + return caseExpr +} + +func (c *cc) convertExpressionSubquery(n *zjast.ExpressionSubquery) *ast.SubLink { + subLink := &ast.SubLink{ + Subselect: c.convertQuery(n.Query), + Location: n.Pos(), + } + switch strings.ToUpper(n.Modifier) { + case "EXISTS": + subLink.SubLinkType = ast.EXISTS_SUBLINK + case "ARRAY": + subLink.SubLinkType = ast.ARRAY_SUBLINK + default: + subLink.SubLinkType = ast.EXPR_SUBLINK + } + return subLink +} + +// newFuncCall builds a FuncCall from a dotted function path. GoogleSQL +// function names are case-insensitive, so they are lowercased. +func (c *cc) newFuncCall(parts []string, loc int) *ast.FuncCall { + name := strings.ToLower(parts[len(parts)-1]) + var schema string + if len(parts) > 1 { + schema = strings.ToLower(strings.Join(parts[:len(parts)-1], ".")) + } + items := []ast.Node{} + if schema != "" { + items = append(items, &ast.String{Str: schema}) + } + items = append(items, &ast.String{Str: name}) + return &ast.FuncCall{ + Func: &ast.FuncName{Schema: schema, Name: name}, + Funcname: &ast.List{Items: items}, + Args: &ast.List{}, + Location: loc, + } +} + +func (c *cc) convertFunctionCall(n *zjast.FunctionCall) ast.Node { + // Chained calls ("expr.method(...)") have no sqlc AST equivalent. + if n.IsChained { + return todo(n) + } + parts := pathParts(n.Function) + if len(parts) == 0 { + return todo(n) + } + fc := c.newFuncCall(parts, n.Pos()) + fc.AggDistinct = n.Distinct + for _, arg := range n.Args { + if _, ok := arg.(*zjast.Star); ok { + fc.AggStar = true + continue + } + fc.Args.Items = append(fc.Args.Items, c.convertExpr(arg)) + } + if n.OrderBy != nil { + fc.AggOrder = c.convertOrderBy(n.OrderBy) + } + return fc +} + +func (c *cc) convertAnalyticFunctionCall(n *zjast.AnalyticFunctionCall) ast.Node { + base := c.convertExpr(n.Expr) + fc, ok := base.(*ast.FuncCall) + if !ok { + return base + } + over := &ast.WindowDef{Location: n.Pos()} + if spec := n.WindowSpec; spec != nil { + if spec.Name != nil { + name := identifier(spec.Name.Name) + over.Name = &name + } + if spec.PartitionBy != nil { + over.PartitionClause = &ast.List{} + for _, expr := range spec.PartitionBy.Expressions { + over.PartitionClause.Items = append(over.PartitionClause.Items, c.convertExpr(expr)) + } + } + if spec.OrderBy != nil { + over.OrderClause = c.convertOrderBy(spec.OrderBy) + } + } + fc.Over = over + return fc +} + +func (c *cc) convertDotStar(n *zjast.DotStar) ast.Node { + fields, ok := columnRefFields(n.Expr) + if !ok { + return todo(n) + } + fields = append(fields, &ast.A_Star{}) + return &ast.ColumnRef{ + Fields: &ast.List{Items: fields}, + Location: n.Pos(), + } +} + +// targetPath extracts the table path of an INSERT/UPDATE/DELETE target. +func targetPath(node zjast.Node) (*zjast.PathExpression, bool) { + path, ok := node.(*zjast.PathExpression) + return path, ok && path != nil +} + +func (c *cc) convertInsertStatement(n *zjast.InsertStatement) ast.Node { + path, ok := targetPath(n.Target) + if !ok { + return todo(n) + } + stmt := &ast.InsertStmt{ + Relation: parseRangeVar(path), + } + + if n.Columns != nil { + stmt.Cols = &ast.List{} + for _, col := range n.Columns.Identifiers { + if col == nil { + continue + } + name := identifier(col.Name) + stmt.Cols.Items = append(stmt.Cols.Items, &ast.ResTarget{ + Name: &name, + Location: col.Pos(), + }) + } + } + + switch { + case n.Rows != nil: + selectStmt := &ast.SelectStmt{ + ValuesLists: &ast.List{}, + } + for _, row := range n.Rows.Rows { + if row == nil { + continue + } + rowList := &ast.List{} + for _, val := range row.Values { + rowList.Items = append(rowList.Items, c.convertExpr(val)) + } + selectStmt.ValuesLists.Items = append(selectStmt.ValuesLists.Items, rowList) + } + stmt.SelectStmt = selectStmt + case n.Query != nil: + stmt.SelectStmt = c.convertQuery(n.Query) + } + + if n.Returning != nil { + stmt.ReturningList = c.convertReturningClause(n.Returning) + } + + return stmt +} + +// convertReturningClause converts a "THEN RETURN" clause to a returning list. +func (c *cc) convertReturningClause(n *zjast.ReturningClause) *ast.List { + list := &ast.List{} + if n.SelectList != nil { + for _, col := range n.SelectList.Columns { + if col == nil { + continue + } + list.Items = append(list.Items, c.convertSelectColumn(col)) + } + } + return list +} + +func (c *cc) convertUpdateStatement(n *zjast.UpdateStatement) ast.Node { + path, ok := targetPath(n.Target) + if !ok { + return todo(n) + } + rv := parseRangeVar(path) + rv.Alias = convertAlias(n.Alias) + stmt := &ast.UpdateStmt{ + Relations: &ast.List{Items: []ast.Node{rv}}, + TargetList: &ast.List{}, + } + + if n.UpdateItemList != nil { + for _, item := range n.UpdateItemList.Items { + if item == nil || item.SetValue == nil { + // Nested INSERT/UPDATE/DELETE update items are unsupported. + continue + } + stmt.TargetList.Items = append(stmt.TargetList.Items, c.convertUpdateSetValue(item.SetValue)) + } + } + + if n.From != nil { + stmt.FromClause = &ast.List{ + Items: []ast.Node{c.convertTableExpr(n.From.TableExpression)}, + } + } + + if n.Where != nil { + stmt.WhereClause = c.convertExpr(whereExpr(n.Where)) + } + + if n.Returning != nil { + stmt.ReturningList = c.convertReturningClause(n.Returning) + } + + return stmt +} + +func (c *cc) convertUpdateSetValue(n *zjast.UpdateSetValue) *ast.ResTarget { + res := &ast.ResTarget{ + Val: c.convertExpr(n.Value), + Location: n.Pos(), + } + if path, ok := n.Path.(*zjast.PathExpression); ok { + if parts := pathParts(path); len(parts) > 0 { + name := identifier(parts[len(parts)-1]) + res.Name = &name + } + } + return res +} + +func (c *cc) convertDeleteStatement(n *zjast.DeleteStatement) ast.Node { + path, ok := targetPath(n.Target) + if !ok { + return todo(n) + } + rv := parseRangeVar(path) + rv.Alias = convertAlias(n.Alias) + stmt := &ast.DeleteStmt{ + Relations: &ast.List{Items: []ast.Node{rv}}, + } + + if n.Where != nil { + stmt.WhereClause = c.convertExpr(whereExpr(n.Where)) + } + + if n.Returning != nil { + stmt.ReturningList = c.convertReturningClause(n.Returning) + } + + return stmt +} + +func (c *cc) convertCreateTableStatement(n *zjast.CreateTableStatement) ast.Node { + stmt := &ast.CreateTableStmt{ + Name: parseTableName(n.Name), + IfNotExists: n.IsIfNotExists, + } + if n.TableElementList != nil { + for _, elem := range n.TableElementList.Elements { + col, ok := elem.(*zjast.ColumnDefinition) + if !ok { + // Table-level PRIMARY KEY, FOREIGN KEY, and CHECK + // constraints are skipped. + continue + } + stmt.Cols = append(stmt.Cols, c.convertColumnDefinition(col)) + } + } + return stmt +} + +func (c *cc) convertColumnDefinition(n *zjast.ColumnDefinition) *ast.ColumnDef { + col := &ast.ColumnDef{ + Location: n.Pos(), + TypeName: &ast.TypeName{Name: columnSchemaTypeName(n.Schema)}, + } + if n.Name != nil { + col.Colname = identifier(n.Name.Name) + } + + if attrs := columnAttributes(n.Schema); attrs != nil { + for _, attr := range attrs.Attributes { + switch attr.(type) { + case *zjast.NotNullColumnAttribute: + col.IsNotNull = true + case *zjast.PrimaryKeyColumnAttribute: + col.PrimaryKey = true + } + } + } + + if simple, ok := n.Schema.(*zjast.SimpleColumnSchema); ok { + // Type parameters, e.g. STRING(10) or NUMERIC(10, 2). + if simple.TypeParameters != nil { + col.TypeName.Typmods = &ast.List{} + for _, param := range simple.TypeParameters.Parameters { + col.TypeName.Typmods.Items = append(col.TypeName.Typmods.Items, c.convertExpr(param)) + } + } + if simple.DefaultExpression != nil { + col.RawDefault = c.convertExpr(simple.DefaultExpression) + } + } + + return col +} + +func (c *cc) convertDropStatement(n *zjast.DropStatement) ast.Node { + // Only DROP TABLE maps onto the sqlc AST; other object kinds (views, + // functions, schemas, ...) fall through to TODO. + if strings.EqualFold(n.ObjectKind, "TABLE") && n.Path != nil { + return &ast.DropTableStmt{ + IfExists: n.IsIfExists, + Tables: []*ast.TableName{parseTableName(n.Path)}, + } + } + return todo(n) +} + +func (c *cc) convertTruncateStatement(n *zjast.TruncateStatement) ast.Node { + if n.Target == nil { + return todo(n) + } + return &ast.TruncateStmt{ + Relations: &ast.List{Items: []ast.Node{parseRangeVar(n.Target)}}, + } +} diff --git a/internal/engine/googlesql/parse.go b/internal/engine/googlesql/parse.go new file mode 100644 index 0000000000..ea77df7a03 --- /dev/null +++ b/internal/engine/googlesql/parse.go @@ -0,0 +1,61 @@ +package googlesql + +import ( + "bytes" + "context" + "io" + + "github.com/sqlc-dev/zetajones/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 + } + + // zetajones node locations are byte offsets into the input. + loc := stmt.Pos() + stmts = append(stmts, ast.Statement{ + Raw: &ast.RawStmt{ + Stmt: out, + StmtLocation: loc, + StmtLen: stmt.End() - loc, + }, + }) + } + + return stmts, nil +} + +// https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#comments +func (p *Parser) CommentSyntax() source.CommentSyntax { + return source.CommentSyntax{ + Dash: true, // -- comment + SlashStar: true, // /* comment */ + Hash: true, // # comment + } +} diff --git a/internal/engine/googlesql/reserved.go b/internal/engine/googlesql/reserved.go new file mode 100644 index 0000000000..14db13087c --- /dev/null +++ b/internal/engine/googlesql/reserved.go @@ -0,0 +1,11 @@ +package googlesql + +import ( + "github.com/sqlc-dev/zetajones/token" +) + +// IsReservedKeyword reports whether s is a reserved keyword in GoogleSQL; see +// https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#reserved_keywords +func (p *Parser) IsReservedKeyword(s string) bool { + return token.IsReservedKeyword(s) +} diff --git a/internal/engine/googlesql/stdlib.go b/internal/engine/googlesql/stdlib.go new file mode 100644 index 0000000000..e43d9c02c6 --- /dev/null +++ b/internal/engine/googlesql/stdlib.go @@ -0,0 +1,9 @@ +package googlesql + +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/googlesql/utils.go b/internal/engine/googlesql/utils.go new file mode 100644 index 0000000000..002f217c37 --- /dev/null +++ b/internal/engine/googlesql/utils.go @@ -0,0 +1,253 @@ +package googlesql + +import ( + "log" + "strings" + + zjast "github.com/sqlc-dev/zetajones/ast" + + "github.com/sqlc-dev/sqlc/internal/debug" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +func todo(n zjast.Node) *ast.TODO { + if debug.Active { + log.Printf("googlesql.convert: Unknown node type %T\n", n) + } + return &ast.TODO{} +} + +// identifier passes table and column identifiers through unchanged. GoogleSQL +// keywords are case-insensitive, but table names (on BigQuery) are +// case-sensitive, so unlike the MySQL and ClickHouse engines we do not +// lowercase identifiers here. Function and type names, which are +// case-insensitive, are lowercased at their call sites instead. +func identifier(id string) string { + return id +} + +func NewIdentifier(t string) *ast.String { + return &ast.String{Str: identifier(t)} +} + +// pathParts returns the identifier names of a dotted path expression. +func pathParts(n *zjast.PathExpression) []string { + if n == nil { + return nil + } + parts := make([]string, 0, len(n.Names)) + for _, id := range n.Names { + if id == nil { + continue + } + parts = append(parts, id.Name) + } + return parts +} + +func parseTableName(n *zjast.PathExpression) *ast.TableName { + name := &ast.TableName{} + parts := pathParts(n) + switch len(parts) { + case 0: + case 1: + name.Name = identifier(parts[0]) + case 2: + name.Schema = identifier(parts[0]) + name.Name = identifier(parts[1]) + default: + name.Catalog = identifier(parts[0]) + name.Schema = identifier(parts[len(parts)-2]) + name.Name = identifier(parts[len(parts)-1]) + } + return name +} + +func parseRangeVar(n *zjast.PathExpression) *ast.RangeVar { + tn := parseTableName(n) + rv := &ast.RangeVar{} + if n != nil { + rv.Location = n.Pos() + } + if tn.Catalog != "" { + catalog := tn.Catalog + rv.Catalogname = &catalog + } + if tn.Schema != "" { + schema := tn.Schema + rv.Schemaname = &schema + } + if tn.Name != "" { + relname := tn.Name + rv.Relname = &relname + } + return rv +} + +func convertAlias(a *zjast.Alias) *ast.Alias { + if a == nil || a.Identifier == nil { + return nil + } + name := identifier(a.Identifier.Name) + return &ast.Alias{Aliasname: &name} +} + +// whereExpr unwraps a WHERE clause node to its expression. The DELETE and +// UPDATE statement nodes hold either a *WhereClause or a bare expression. +func whereExpr(node zjast.Node) zjast.Node { + if wc, ok := node.(*zjast.WhereClause); ok { + return wc.Expr + } + return node +} + +// stringLiteralValue concatenates and unquotes the components of a string +// literal. +func stringLiteralValue(n *zjast.StringLiteral) string { + var sb strings.Builder + for _, comp := range n.Components { + if comp == nil { + continue + } + sb.WriteString(unquote(comp.Image)) + } + return sb.String() +} + +// unquote strips the optional r/b prefix and the surrounding quotes from a +// string literal component and decodes escape sequences for non-raw strings. +// It is best-effort: malformed input is returned unchanged. +func unquote(image string) string { + raw := false + i := 0 +prefix: + for i < len(image) { + switch image[i] { + case 'r', 'R': + raw = true + i++ + case 'b', 'B': + i++ + default: + break prefix + } + } + s := image[i:] + for _, q := range []string{`'''`, `"""`, `'`, `"`} { + if len(s) >= 2*len(q) && strings.HasPrefix(s, q) && strings.HasSuffix(s, q) { + s = s[len(q) : len(s)-len(q)] + if raw || !strings.ContainsRune(s, '\\') { + return s + } + return decodeEscapes(s) + } + } + return image +} + +func decodeEscapes(s string) string { + var sb strings.Builder + for i := 0; i < len(s); i++ { + ch := s[i] + if ch != '\\' || i+1 >= len(s) { + sb.WriteByte(ch) + continue + } + i++ + switch s[i] { + case 'n': + sb.WriteByte('\n') + case 't': + sb.WriteByte('\t') + case 'r': + sb.WriteByte('\r') + case 'a': + sb.WriteByte('\a') + case 'b': + sb.WriteByte('\b') + case 'f': + sb.WriteByte('\f') + case 'v': + sb.WriteByte('\v') + case '\\', '\'', '"', '?', '`': + sb.WriteByte(s[i]) + default: + // Unknown escape (\x, \u, ...): keep it verbatim. + sb.WriteByte('\\') + sb.WriteByte(s[i]) + } + } + return sb.String() +} + +// typeName renders a zetajones type node (used by CAST) as a lowercased type +// name. Nested types degrade to a readable representation. +func typeName(node zjast.Node) string { + switch t := node.(type) { + case *zjast.SimpleType: + return strings.ToLower(strings.Join(pathParts(t.Name), ".")) + case *zjast.ArrayType: + return "array<" + typeName(t.ElementType) + ">" + case *zjast.StructType: + return "struct" + case *zjast.RangeType: + return "range<" + typeName(t.ElementType) + ">" + case *zjast.MapType: + return "map" + default: + return "" + } +} + +// columnSchemaTypeName renders a CREATE TABLE column schema as a lowercased +// type name. Nested types degrade to a readable representation. +func columnSchemaTypeName(node zjast.Node) string { + switch t := node.(type) { + case *zjast.SimpleColumnSchema: + return strings.ToLower(strings.Join(pathParts(t.Type), ".")) + case *zjast.ArrayColumnSchema: + return "array<" + columnSchemaTypeName(t.ElementSchema) + ">" + case *zjast.StructColumnSchema: + return "struct" + default: + return "" + } +} + +// columnAttributes returns the attribute list (NOT NULL, PRIMARY KEY, ...) of +// a column schema, if any. +func columnAttributes(node zjast.Node) *zjast.ColumnAttributeList { + switch t := node.(type) { + case *zjast.SimpleColumnSchema: + return t.Attributes + case *zjast.ArrayColumnSchema: + return t.Attributes + case *zjast.StructColumnSchema: + return t.Attributes + default: + return nil + } +} + +// columnRefFields flattens an identifier, path expression, or dotted +// identifier chain into ColumnRef fields. +func columnRefFields(node zjast.Node) ([]ast.Node, bool) { + switch n := node.(type) { + case *zjast.Identifier: + return []ast.Node{NewIdentifier(n.Name)}, true + case *zjast.PathExpression: + var fields []ast.Node + for _, part := range pathParts(n) { + fields = append(fields, NewIdentifier(part)) + } + return fields, len(fields) > 0 + case *zjast.DotIdentifier: + base, ok := columnRefFields(n.Expr) + if !ok || n.Name == nil { + return nil, false + } + return append(base, NewIdentifier(n.Name.Name)), true + default: + return nil, false + } +}