diff --git a/docs/howto/analyze.md b/docs/howto/analyze.md index 7da2518770..f14a0e18f8 100644 --- a/docs/howto/analyze.md +++ b/docs/howto/analyze.md @@ -19,7 +19,7 @@ provided. The schema is always read from the `--schema` file. ## Flags - `--dialect`, `-d` - The SQL dialect to use. One of `postgresql`, `mysql`, - `sqlite`, or `clickhouse`. Required. + `sqlite`, `clickhouse`, or `googlesql`. Required. - `--schema`, `-s` - Path to the schema (DDL) file. Required. - `--ast` - Include each statement's AST in the output. Defaults to `false`. diff --git a/docs/howto/parse.md b/docs/howto/parse.md index 406cc12673..28ba9a3d9a 100644 --- a/docs/howto/parse.md +++ b/docs/howto/parse.md @@ -20,7 +20,7 @@ provided. ## Flags - `--dialect`, `-d` - The SQL dialect to use. One of `postgresql`, `mysql`, - `sqlite`, or `clickhouse`. Required. + `sqlite`, `clickhouse`, or `googlesql`. Required. ## Examples diff --git a/internal/cmd/analyze.go b/internal/cmd/analyze.go index e8ef0f2243..11740db8bb 100644 --- a/internal/cmd/analyze.go +++ b/internal/cmd/analyze.go @@ -40,6 +40,9 @@ Examples: # Analyze a ClickHouse query sqlc analyze --dialect clickhouse --schema schema.sql query.sql + # Analyze a GoogleSQL (BigQuery, Spanner) query + sqlc analyze --dialect googlesql --schema schema.sql query.sql + # Analyze a query piped via stdin echo "-- name: GetAuthor :one SELECT * FROM authors WHERE id = $1;" | sqlc analyze --dialect postgresql --schema schema.sql @@ -53,7 +56,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)") } schemaPath, err := cmd.Flags().GetString("schema") @@ -112,8 +115,10 @@ Examples: engine = config.EngineSQLite case "clickhouse": engine = config.EngineClickHouse + case "googlesql": + engine = config.EngineGoogleSQL 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) } sql := config.SQL{ @@ -155,7 +160,7 @@ 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)") cmd.Flags().StringP("schema", "s", "", "path to the schema file") cmd.Flags().BoolP("ast", "", false, "include the statement AST in the output") return cmd diff --git a/internal/compiler/engine.go b/internal/compiler/engine.go index 8b50d30426..071f3458e0 100644 --- a/internal/compiler/engine.go +++ b/internal/compiler/engine.go @@ -10,6 +10,7 @@ import ( "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/googlesql" "github.com/sqlc-dev/sqlc/internal/engine/postgresql" pganalyze "github.com/sqlc-dev/sqlc/internal/engine/postgresql/analyzer" "github.com/sqlc-dev/sqlc/internal/engine/sqlite" @@ -123,6 +124,14 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts return nil, fmt.Errorf("clickhouse: init catalog: %w", err) } c.coreCatalog = cat + case config.EngineGoogleSQL: + c.parser = googlesql.NewParser() + c.selector = newDefaultSelector() + cat, err := core.New(googlesql.Dialect()) + if err != nil { + return nil, fmt.Errorf("googlesql: init catalog: %w", err) + } + c.coreCatalog = cat default: return nil, fmt.Errorf("unknown engine: %s", conf.Engine) } diff --git a/internal/compiler/parse_core.go b/internal/compiler/parse_core.go index 9d79b21709..5cede4356b 100644 --- a/internal/compiler/parse_core.go +++ b/internal/compiler/parse_core.go @@ -9,6 +9,8 @@ import ( "github.com/sqlc-dev/sqlc/internal/metadata" "github.com/sqlc-dev/sqlc/internal/source" "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/named" + "github.com/sqlc-dev/sqlc/internal/sql/rewrite" "github.com/sqlc-dev/sqlc/internal/sql/validate" ) @@ -46,10 +48,21 @@ func (c *Compiler) parseQueryCore(stmt ast.Node, src string) (*Query, error) { return nil, err } + numbers, dollar, err := validate.ParamRef(raw) + if err != nil { + return nil, err + } + rewritten, namedParams, edits := rewrite.NamedParameters(c.conf.Engine, raw, numbers, dollar) + expanded, err := source.Mutate(rawSQL, edits) + if err != nil { + return nil, err + } + var cols []*Column var params []Parameter - if _, ok := raw.Stmt.(*ast.SelectStmt); ok { - res, err := coreanalyzer.Prepare(c.coreCatalog, raw) + switch rewritten.Stmt.(type) { + case *ast.SelectStmt, *ast.InsertStmt, *ast.UpdateStmt, *ast.DeleteStmt: + res, err := coreanalyzer.Prepare(c.coreCatalog, rewritten) if err != nil { return nil, err } @@ -57,11 +70,11 @@ func (c *Compiler) parseQueryCore(stmt ast.Node, src string) (*Query, error) { cols = append(cols, coreColumn(col)) } for _, p := range res.Parameters { - params = append(params, Parameter{Number: p.Number, Column: coreParamColumn(p)}) + params = append(params, Parameter{Number: p.Number, Column: coreParamColumn(p, namedParams)}) } } - trimmed, comments, err := source.StripComments(rawSQL) + trimmed, comments, err := source.StripComments(expanded) if err != nil { return nil, err } @@ -100,7 +113,7 @@ func coreColumn(c core.Column) *Column { return col } -func coreParamColumn(p core.Parameter) *Column { +func coreParamColumn(p core.Parameter, params *named.ParamSet) *Column { col := &Column{ Name: p.Name, DataType: p.DataType, @@ -110,5 +123,12 @@ func coreParamColumn(p core.Parameter) *Column { col.Table = &ast.TableName{Schema: p.Source.Schema, Name: p.Source.Table} col.OriginalName = p.Source.Column } + if col.Name == "" { + if name, ok := params.NameFor(p.Number); ok && name != "" { + col.Name = name + } else if p.Source != nil { + col.Name = p.Source.Column + } + } return col } diff --git a/internal/config/config.go b/internal/config/config.go index 6f0a9bf0eb..b488c5b953 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -55,6 +55,7 @@ const ( EnginePostgreSQL Engine = "postgresql" EngineSQLite Engine = "sqlite" EngineClickHouse Engine = "clickhouse" + EngineGoogleSQL Engine = "googlesql" ) type Config struct { diff --git a/internal/core/analyzer/analyzer.go b/internal/core/analyzer/analyzer.go index d4fcc57bbc..533557e417 100644 --- a/internal/core/analyzer/analyzer.go +++ b/internal/core/analyzer/analyzer.go @@ -21,6 +21,21 @@ func Prepare(cat *core.Catalog, stmt ast.Node) (core.PrepareResult, error) { return core.PrepareResult{}, err } a.command = core.CommandSelect + case *ast.InsertStmt: + if err := a.analyzeInsert(s); err != nil { + return core.PrepareResult{}, err + } + a.command = core.CommandInsert + case *ast.UpdateStmt: + if err := a.analyzeUpdate(s); err != nil { + return core.PrepareResult{}, err + } + a.command = core.CommandUpdate + case *ast.DeleteStmt: + if err := a.analyzeDelete(s); err != nil { + return core.PrepareResult{}, err + } + a.command = core.CommandDelete default: return core.PrepareResult{}, fmt.Errorf("analyzer: unsupported statement %T", stmt) } diff --git a/internal/core/analyzer/dml.go b/internal/core/analyzer/dml.go new file mode 100644 index 0000000000..7ea4285e7e --- /dev/null +++ b/internal/core/analyzer/dml.go @@ -0,0 +1,185 @@ +package analyzer + +import ( + "fmt" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +func (a *analyzer) analyzeInsert(s *ast.InsertStmt) error { + if s.Relation == nil { + return fmt.Errorf("insert: missing relation") + } + rel, err := a.bindRangeVar(s.Relation) + if err != nil { + return err + } + a.scope = &scope{rels: []scopeRel{rel}} + + targets, err := insertTargets(rel, s.Cols) + if err != nil { + return err + } + if err := a.bindInsertValues(s.SelectStmt, rel, targets); err != nil { + return err + } + return a.projectReturning(s.ReturningList) +} + +func (a *analyzer) analyzeUpdate(s *ast.UpdateStmt) error { + sc, err := a.relationScope(s.Relations, s.FromClause) + if err != nil { + return err + } + a.scope = sc + target := sc.rels[0] + + for _, item := range listItems(s.TargetList) { + rt, ok := item.(*ast.ResTarget) + if !ok || rt.Name == nil { + continue + } + col, ok := findColumn(target, *rt.Name) + if !ok { + return fmt.Errorf("unknown column %q", *rt.Name) + } + if err := a.bindValue(target, &col, rt.Val); err != nil { + return fmt.Errorf("set %s: %w", *rt.Name, err) + } + } + + if s.WhereClause != nil { + if _, err := a.typeExpr(s.WhereClause); err != nil { + return fmt.Errorf("where: %w", err) + } + } + return a.projectReturning(s.ReturningList) +} + +func (a *analyzer) analyzeDelete(s *ast.DeleteStmt) error { + sc, err := a.relationScope(s.Relations, s.UsingClause) + if err != nil { + return err + } + a.scope = sc + + if s.WhereClause != nil { + if _, err := a.typeExpr(s.WhereClause); err != nil { + return fmt.Errorf("where: %w", err) + } + } + return a.projectReturning(s.ReturningList) +} + +func (a *analyzer) relationScope(relations, extra *ast.List) (*scope, error) { + sc := &scope{} + for _, item := range listItems(relations) { + if err := a.appendFromItem(sc, item); err != nil { + return nil, err + } + } + for _, item := range listItems(extra) { + if err := a.appendFromItem(sc, item); err != nil { + return nil, err + } + } + if len(sc.rels) == 0 { + return nil, fmt.Errorf("missing target relation") + } + return sc, nil +} + +func insertTargets(rel scopeRel, cols *ast.List) ([]scopeCol, error) { + items := listItems(cols) + if len(items) == 0 { + return rel.cols, nil + } + out := make([]scopeCol, 0, len(items)) + for _, item := range items { + rt, ok := item.(*ast.ResTarget) + if !ok || rt.Name == nil { + return nil, fmt.Errorf("insert: unsupported column target %T", item) + } + col, ok := findColumn(rel, *rt.Name) + if !ok { + return nil, fmt.Errorf("unknown column %q", *rt.Name) + } + out = append(out, col) + } + return out, nil +} + +func (a *analyzer) bindInsertValues(n ast.Node, rel scopeRel, targets []scopeCol) error { + if n == nil { + return nil + } + sel, ok := n.(*ast.SelectStmt) + if !ok { + return fmt.Errorf("insert: unsupported source %T", n) + } + if sel.ValuesLists == nil { + return fmt.Errorf("insert: INSERT ... SELECT is not supported") + } + for _, row := range listItems(sel.ValuesLists) { + values, ok := row.(*ast.List) + if !ok { + continue + } + for i, v := range values.Items { + var target *scopeCol + if i < len(targets) { + target = &targets[i] + } + if err := a.bindValue(rel, target, v); err != nil { + return err + } + } + } + return nil +} + +func (a *analyzer) bindValue(rel scopeRel, target *scopeCol, v ast.Node) error { + if target != nil { + switch value := v.(type) { + case *ast.ParamRef: + a.inferParam(value.Number, columnType(rel, *target)) + return nil + case *ast.A_Const: + return nil + } + } + _, err := a.typeExpr(v) + return err +} + +func (a *analyzer) projectReturning(l *ast.List) error { + for _, item := range listItems(l) { + rt, ok := item.(*ast.ResTarget) + if !ok { + continue + } + if err := a.projectTarget(rt); err != nil { + return err + } + } + return nil +} + +func findColumn(rel scopeRel, name string) (scopeCol, bool) { + for _, col := range rel.cols { + if col.name == name { + return col, true + } + } + return scopeCol{}, false +} + +func columnType(rel scopeRel, col scopeCol) exprType { + return exprType{ + typeOID: col.typeOID, + nullable: !col.notNull, + sourceClassOID: rel.classOID, + sourceAttributeOID: col.attOID, + sourceTableAlias: rel.alias, + } +} diff --git a/internal/core/analyzer/expr.go b/internal/core/analyzer/expr.go index ff9ecd7e9d..037e2f34ad 100644 --- a/internal/core/analyzer/expr.go +++ b/internal/core/analyzer/expr.go @@ -276,6 +276,9 @@ func (a *analyzer) typeFuncCall(f *ast.FuncCall) (exprType, error) { } if f.AggStar && (name == "count" || name == "count.*") { + if overloads, err := a.cat.FindProcs("count", nil); err == nil && len(overloads) > 0 { + return exprType{typeOID: overloads[0].ReturnTypeOID, nullable: overloads[0].ReturnNullable}, nil + } oid, err := a.cat.TypeOID("int8") if err != nil { return exprType{}, err diff --git a/internal/endtoend/testdata/analyze_basic/googlesql/exec.json b/internal/endtoend/testdata/analyze_basic/googlesql/exec.json new file mode 100644 index 0000000000..a53ddddc6d --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/googlesql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "googlesql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_basic/googlesql/query.sql b/internal/endtoend/testdata/analyze_basic/googlesql/query.sql new file mode 100644 index 0000000000..e3aba1cde2 --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/googlesql/query.sql @@ -0,0 +1,2 @@ +-- name: GetUser :one +SELECT id, name FROM users WHERE id = @id; diff --git a/internal/endtoend/testdata/analyze_basic/googlesql/schema.sql b/internal/endtoend/testdata/analyze_basic/googlesql/schema.sql new file mode 100644 index 0000000000..1f45153b37 --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/googlesql/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id INT64 NOT NULL, + name STRING NOT NULL, + bio STRING, +) PRIMARY KEY (id); diff --git a/internal/endtoend/testdata/analyze_basic/googlesql/stdout.txt b/internal/endtoend/testdata/analyze_basic/googlesql/stdout.txt new file mode 100644 index 0000000000..4075f80992 --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/googlesql/stdout.txt @@ -0,0 +1,34 @@ +[ + { + "name": "GetUser", + "cmd": ":one", + "columns": [ + { + "name": "id", + "data_type": "int64", + "not_null": true, + "is_array": false, + "table": "users" + }, + { + "name": "name", + "data_type": "string", + "not_null": true, + "is_array": false, + "table": "users" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "id", + "data_type": "int64", + "not_null": true, + "is_array": false, + "table": "users" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_dml/googlesql/exec.json b/internal/endtoend/testdata/analyze_dml/googlesql/exec.json new file mode 100644 index 0000000000..a53ddddc6d --- /dev/null +++ b/internal/endtoend/testdata/analyze_dml/googlesql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "googlesql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_dml/googlesql/query.sql b/internal/endtoend/testdata/analyze_dml/googlesql/query.sql new file mode 100644 index 0000000000..e35c5f5d02 --- /dev/null +++ b/internal/endtoend/testdata/analyze_dml/googlesql/query.sql @@ -0,0 +1,17 @@ +-- name: CreateUser :exec +INSERT INTO users (id, name, bio) VALUES (@id, @name, @bio); + +-- name: CreateUserReturning :one +INSERT INTO users (id, name) VALUES (@id, @name) THEN RETURN id, name; + +-- name: UpdateBio :exec +UPDATE users SET bio = @bio WHERE id = @id; + +-- name: UpdateNameReturning :one +UPDATE users SET name = @name WHERE id = @id THEN RETURN id, name; + +-- name: DeleteUser :exec +DELETE FROM users WHERE id = @id; + +-- name: DeleteUserReturning :one +DELETE FROM users WHERE id = @id THEN RETURN id; diff --git a/internal/endtoend/testdata/analyze_dml/googlesql/schema.sql b/internal/endtoend/testdata/analyze_dml/googlesql/schema.sql new file mode 100644 index 0000000000..1f45153b37 --- /dev/null +++ b/internal/endtoend/testdata/analyze_dml/googlesql/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id INT64 NOT NULL, + name STRING NOT NULL, + bio STRING, +) PRIMARY KEY (id); diff --git a/internal/endtoend/testdata/analyze_dml/googlesql/stdout.txt b/internal/endtoend/testdata/analyze_dml/googlesql/stdout.txt new file mode 100644 index 0000000000..5148457d6d --- /dev/null +++ b/internal/endtoend/testdata/analyze_dml/googlesql/stdout.txt @@ -0,0 +1,192 @@ +[ + { + "name": "CreateUser", + "cmd": ":exec", + "columns": [], + "params": [ + { + "number": 1, + "column": { + "name": "id", + "data_type": "int64", + "not_null": true, + "is_array": false, + "table": "users" + } + }, + { + "number": 2, + "column": { + "name": "name", + "data_type": "string", + "not_null": true, + "is_array": false, + "table": "users" + } + }, + { + "number": 3, + "column": { + "name": "bio", + "data_type": "string", + "not_null": false, + "is_array": false, + "table": "users" + } + } + ] + }, + { + "name": "CreateUserReturning", + "cmd": ":one", + "columns": [ + { + "name": "id", + "data_type": "int64", + "not_null": true, + "is_array": false, + "table": "users" + }, + { + "name": "name", + "data_type": "string", + "not_null": true, + "is_array": false, + "table": "users" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "id", + "data_type": "int64", + "not_null": true, + "is_array": false, + "table": "users" + } + }, + { + "number": 2, + "column": { + "name": "name", + "data_type": "string", + "not_null": true, + "is_array": false, + "table": "users" + } + } + ] + }, + { + "name": "UpdateBio", + "cmd": ":exec", + "columns": [], + "params": [ + { + "number": 1, + "column": { + "name": "bio", + "data_type": "string", + "not_null": false, + "is_array": false, + "table": "users" + } + }, + { + "number": 2, + "column": { + "name": "id", + "data_type": "int64", + "not_null": true, + "is_array": false, + "table": "users" + } + } + ] + }, + { + "name": "UpdateNameReturning", + "cmd": ":one", + "columns": [ + { + "name": "id", + "data_type": "int64", + "not_null": true, + "is_array": false, + "table": "users" + }, + { + "name": "name", + "data_type": "string", + "not_null": true, + "is_array": false, + "table": "users" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "name", + "data_type": "string", + "not_null": true, + "is_array": false, + "table": "users" + } + }, + { + "number": 2, + "column": { + "name": "id", + "data_type": "int64", + "not_null": true, + "is_array": false, + "table": "users" + } + } + ] + }, + { + "name": "DeleteUser", + "cmd": ":exec", + "columns": [], + "params": [ + { + "number": 1, + "column": { + "name": "id", + "data_type": "int64", + "not_null": true, + "is_array": false, + "table": "users" + } + } + ] + }, + { + "name": "DeleteUserReturning", + "cmd": ":one", + "columns": [ + { + "name": "id", + "data_type": "int64", + "not_null": true, + "is_array": false, + "table": "users" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "id", + "data_type": "int64", + "not_null": true, + "is_array": false, + "table": "users" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_select/googlesql/exec.json b/internal/endtoend/testdata/analyze_select/googlesql/exec.json new file mode 100644 index 0000000000..a53ddddc6d --- /dev/null +++ b/internal/endtoend/testdata/analyze_select/googlesql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "googlesql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_select/googlesql/query.sql b/internal/endtoend/testdata/analyze_select/googlesql/query.sql new file mode 100644 index 0000000000..b4e7756130 --- /dev/null +++ b/internal/endtoend/testdata/analyze_select/googlesql/query.sql @@ -0,0 +1,8 @@ +-- name: ListUsers :many +SELECT * FROM users; + +-- name: CountUsers :one +SELECT COUNT(*) AS total FROM users; + +-- name: ListUserPosts :many +SELECT u.name, p.* FROM users u JOIN posts p ON p.user_id = u.id WHERE u.name = @name; diff --git a/internal/endtoend/testdata/analyze_select/googlesql/schema.sql b/internal/endtoend/testdata/analyze_select/googlesql/schema.sql new file mode 100644 index 0000000000..18ef00e682 --- /dev/null +++ b/internal/endtoend/testdata/analyze_select/googlesql/schema.sql @@ -0,0 +1,12 @@ +CREATE TABLE users ( + id INT64 NOT NULL, + name STRING NOT NULL, + bio STRING, +) PRIMARY KEY (id); + +CREATE TABLE posts ( + id INT64 NOT NULL, + user_id INT64 NOT NULL, + title STRING(255), + created TIMESTAMP NOT NULL, +) PRIMARY KEY (id); diff --git a/internal/endtoend/testdata/analyze_select/googlesql/stdout.txt b/internal/endtoend/testdata/analyze_select/googlesql/stdout.txt new file mode 100644 index 0000000000..b2b19ce73f --- /dev/null +++ b/internal/endtoend/testdata/analyze_select/googlesql/stdout.txt @@ -0,0 +1,96 @@ +[ + { + "name": "ListUsers", + "cmd": ":many", + "columns": [ + { + "name": "id", + "data_type": "int64", + "not_null": true, + "is_array": false, + "table": "users" + }, + { + "name": "name", + "data_type": "string", + "not_null": true, + "is_array": false, + "table": "users" + }, + { + "name": "bio", + "data_type": "string", + "not_null": false, + "is_array": false, + "table": "users" + } + ], + "params": [] + }, + { + "name": "CountUsers", + "cmd": ":one", + "columns": [ + { + "name": "total", + "data_type": "int64", + "not_null": true, + "is_array": false + } + ], + "params": [] + }, + { + "name": "ListUserPosts", + "cmd": ":many", + "columns": [ + { + "name": "name", + "data_type": "string", + "not_null": true, + "is_array": false, + "table": "users" + }, + { + "name": "id", + "data_type": "int64", + "not_null": true, + "is_array": false, + "table": "posts" + }, + { + "name": "user_id", + "data_type": "int64", + "not_null": true, + "is_array": false, + "table": "posts" + }, + { + "name": "title", + "data_type": "string", + "not_null": false, + "is_array": false, + "table": "posts" + }, + { + "name": "created", + "data_type": "timestamp", + "not_null": true, + "is_array": false, + "table": "posts" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "name", + "data_type": "string", + "not_null": true, + "is_array": false, + "table": "users" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt b/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt index db7128d126..086f885402 100644 --- a/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt @@ -1,5 +1,7 @@ [ { + "name": "GetValue", + "cmd": ":one", "ast": { "Stmt": { "DistinctClause": null, @@ -35,8 +37,8 @@ "Larg": null, "Rarg": null }, - "StmtLocation": 23, - "StmtLen": 8 + "StmtLocation": 0, + "StmtLen": 31 } } ] diff --git a/internal/engine/googlesql/convert.go b/internal/engine/googlesql/convert.go index ae84839a89..acf0af4657 100644 --- a/internal/engine/googlesql/convert.go +++ b/internal/engine/googlesql/convert.go @@ -419,6 +419,7 @@ func (c *cc) convertExpr(node zjast.Node) ast.Node { op = "<<" } return &ast.A_Expr{ + Kind: ast.A_Expr_Kind_OP, Name: &ast.List{Items: []ast.Node{&ast.String{Str: op}}}, Lexpr: c.convertExpr(n.Lhs), Rexpr: c.convertExpr(n.Rhs), @@ -546,6 +547,7 @@ func (c *cc) convertUnaryExpression(n *zjast.UnaryExpression) ast.Node { } } return &ast.A_Expr{ + Kind: ast.A_Expr_Kind_OP, Name: &ast.List{Items: []ast.Node{&ast.String{Str: n.Op}}}, Rexpr: c.convertExpr(n.Operand), Location: n.Pos(), @@ -570,6 +572,7 @@ func (c *cc) convertBinaryExpression(n *zjast.BinaryExpression) ast.Node { } var expr ast.Node = &ast.A_Expr{ + Kind: ast.A_Expr_Kind_OP, Name: &ast.List{Items: []ast.Node{&ast.String{Str: n.Op}}}, Lexpr: c.convertExpr(n.Left), Rexpr: c.convertExpr(n.Right), diff --git a/internal/engine/googlesql/parse.go b/internal/engine/googlesql/parse.go index ea77df7a03..87967c00ce 100644 --- a/internal/engine/googlesql/parse.go +++ b/internal/engine/googlesql/parse.go @@ -29,23 +29,32 @@ func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) { return nil, err } + // zetajones node locations are byte offsets into the input. Following the + // convention used by the other engines, a statement's location starts at the + // end of the previous statement (or the start of the input) so that any + // leading comment — in particular the "-- name:" annotation sqlc relies on — + // is captured as part of the statement. var stmts []ast.Statement + loc := 0 for _, stmt := range stmtNodes { converter := &cc{} out := converter.convert(stmt) if _, ok := out.(*ast.TODO); ok { + // Skip over the unsupported statement (and its trailing semicolon) + // so the next statement's leading comment is measured from here. + loc = stmt.End() + 1 continue } - // zetajones node locations are byte offsets into the input. - loc := stmt.Pos() + end := stmt.End() stmts = append(stmts, ast.Statement{ Raw: &ast.RawStmt{ Stmt: out, StmtLocation: loc, - StmtLen: stmt.End() - loc, + StmtLen: end - loc, }, }) + loc = end + 1 } return stmts, nil diff --git a/internal/engine/googlesql/seed.go b/internal/engine/googlesql/seed.go new file mode 100644 index 0000000000..ae78bef1e2 --- /dev/null +++ b/internal/engine/googlesql/seed.go @@ -0,0 +1,108 @@ +package googlesql + +import ( + "fmt" + + "github.com/sqlc-dev/sqlc/internal/core" +) + +func Dialect() core.Option { + return core.WithSeed(Seed) +} + +type gsqlType struct { + name string + category string +} + +var googlesqlTypes = []gsqlType{ + {"bool", "B"}, + {"int64", "N"}, {"float32", "N"}, {"float64", "N"}, + {"numeric", "N"}, {"bignumeric", "N"}, + {"string", "S"}, {"bytes", "S"}, {"uuid", "S"}, + {"date", "D"}, {"datetime", "D"}, {"time", "D"}, {"timestamp", "D"}, + {"interval", "T"}, + {"json", "U"}, {"geography", "U"}, {"struct", "U"}, {"enum", "U"}, + {"tokenlist", "U"}, + {"array", "A"}, +} + +var comparisonOperators = []string{"=", "<>", "!=", "<", "<=", ">", ">="} + +var arithmeticOperators = []string{"+", "-", "*", "/"} + +var arithmeticTypes = []string{"int64", "float32", "float64", "numeric", "bignumeric"} + +func Seed(cat *core.Catalog) error { + dialectOID, err := cat.CreateDialect("googlesql") + if err != nil { + return err + } + + oids := make(map[string]int64, len(googlesqlTypes)) + for _, t := range googlesqlTypes { + oid, err := cat.CreateTypeSpec(core.TypeSpec{ + Name: t.name, + Typtype: "b", + Category: t.category, + DialectOID: dialectOID, + }) + if err != nil { + return fmt.Errorf("seed googlesql type %q: %w", t.name, err) + } + oids[t.name] = oid + } + + boolOID := oids["bool"] + for _, t := range googlesqlTypes { + if t.name == "array" || t.name == "struct" { + continue + } + for _, op := range comparisonOperators { + if _, err := cat.CreateOperator(core.OperatorSpec{ + Name: op, + DialectOID: dialectOID, + LeftTypeOID: oids[t.name], + RightTypeOID: oids[t.name], + ResultTypeOID: boolOID, + }); err != nil { + return fmt.Errorf("seed googlesql operator %q(%s): %w", op, t.name, err) + } + } + } + + for _, name := range arithmeticTypes { + for _, op := range arithmeticOperators { + if _, err := cat.CreateOperator(core.OperatorSpec{ + Name: op, + DialectOID: dialectOID, + LeftTypeOID: oids[name], + RightTypeOID: oids[name], + ResultTypeOID: oids[name], + }); err != nil { + return fmt.Errorf("seed googlesql operator %q(%s): %w", op, name, err) + } + } + } + + if _, err := cat.CreateProc(core.ProcSpec{ + Name: "count", + DialectOID: dialectOID, + Kind: "a", + ReturnTypeOID: oids["int64"], + }); err != nil { + return fmt.Errorf("seed googlesql function %q: %w", "count", err) + } + + if _, err := cat.CreateOperator(core.OperatorSpec{ + Name: "||", + DialectOID: dialectOID, + LeftTypeOID: oids["string"], + RightTypeOID: oids["string"], + ResultTypeOID: oids["string"], + }); err != nil { + return fmt.Errorf("seed googlesql operator %q: %w", "||", err) + } + + return nil +} diff --git a/internal/sql/rewrite/parameters.go b/internal/sql/rewrite/parameters.go index d1ea1a22cc..77ad5d2573 100644 --- a/internal/sql/rewrite/parameters.go +++ b/internal/sql/rewrite/parameters.go @@ -102,7 +102,16 @@ func NamedParameters(engine config.Engine, raw *ast.RawStmt, numbs map[int]bool, }) var replace string - if engine == config.EngineMySQL || engine == config.EngineSQLite || !dollar { + if engine == config.EngineGoogleSQL { + // GoogleSQL supports named parameters natively (and Spanner + // requires them), so keep the "@name" form rather than + // rewriting to a positional placeholder. + if param.IsSqlcSlice() { + replace = fmt.Sprintf(`/*SLICE:%s*/@%s`, param.Name(), param.Name()) + } else { + replace = fmt.Sprintf("@%s", param.Name()) + } + } else if engine == config.EngineMySQL || engine == config.EngineSQLite || !dollar { if param.IsSqlcSlice() { // This sequence is also replicated in internal/codegen/golang.Field // since it's needed during template generation for replacement @@ -140,7 +149,9 @@ func NamedParameters(engine config.Engine, raw *ast.RawStmt, numbs map[int]bool, // TODO: This code assumes that @foo::bool is on a single line var replace string - if engine == config.EngineMySQL || !dollar { + if engine == config.EngineGoogleSQL { + replace = fmt.Sprintf("@%s", paramName) + } else if engine == config.EngineMySQL || !dollar { replace = "?" } else if engine == config.EngineSQLite { replace = fmt.Sprintf("?%d", argn) @@ -168,7 +179,9 @@ func NamedParameters(engine config.Engine, raw *ast.RawStmt, numbs map[int]bool, // TODO: This code assumes that @foo is on a single line var replace string - if engine == config.EngineMySQL || !dollar { + if engine == config.EngineGoogleSQL { + replace = fmt.Sprintf("@%s", paramName) + } else if engine == config.EngineMySQL || !dollar { replace = "?" } else if engine == config.EngineSQLite { replace = fmt.Sprintf("?%d", argn)