diff --git a/internal/compiler/parse_core.go b/internal/compiler/parse_core.go index 543ca5f3cc..2e1dfc7042 100644 --- a/internal/compiler/parse_core.go +++ b/internal/compiler/parse_core.go @@ -201,5 +201,13 @@ func coreParamColumn(p core.Parameter, params *named.ParamSet) *Column { col.IsSqlcSlice = param.IsSqlcSlice() col.IsNamedParam = true } + // sqlc.slice() already says the argument is a list of values, so a + // slice the analyzer typed as an array, as it does "= ANY(sqlc.slice(x))", + // is a slice of the array's elements. + if col.IsSqlcSlice && col.TypeExpr.IsArray() { + col.TypeExpr = col.TypeExpr.Element() + col.IsArray = false + describeType(col, col.TypeExpr) + } return col } diff --git a/internal/core/analyzer/expr.go b/internal/core/analyzer/expr.go index 4f0dd8ec8b..62a69b273d 100644 --- a/internal/core/analyzer/expr.go +++ b/internal/core/analyzer/expr.go @@ -79,6 +79,9 @@ func (a *analyzer) typeExpr(n ast.Node) (exprType, error) { case *ast.A_ArrayExpr: return a.typeArrayExpr(e) + case *ast.A_Indirection: + return a.typeIndirection(e) + case *ast.SubLink: return a.typeSubLink(e) @@ -366,7 +369,17 @@ func (a *analyzer) typeQuantifiedExpr(e *ast.A_Expr) (exprType, error) { if err != nil { return exprType{}, err } - if err := a.typeOperands(e.Rexpr, leftT); err != nil { + // "x = ANY(SELECT ...)" compares x against the subquery's column, but + // "x = ANY(array)" against the elements of an array, so a placeholder + // there is an array of x's type. + right := leftT + if _, sub := e.Rexpr.(*ast.SubLink); !sub { + right = a.arrayOf(leftT) + right.sourceClassOID = leftT.sourceClassOID + right.sourceAttributeOID = leftT.sourceAttributeOID + right.sourceTableAlias = leftT.sourceTableAlias + } + if err := a.typeOperands(e.Rexpr, right); err != nil { return exprType{}, err } return a.boolType(false) @@ -524,6 +537,51 @@ func (a *analyzer) typeArrayExpr(e *ast.A_ArrayExpr) (exprType, error) { return a.lookupType(core.Array(element.WithNullable(false))), nil } +// typeIndirection types an array subscript, arr[i], as the array's element, +// and a slice, arr[i:j], as the array itself. An element may be NULL, and a +// subscript past the end of the array yields NULL in the dialects that do not +// raise an error for it. The subscripts are integers. +func (a *analyzer) typeIndirection(e *ast.A_Indirection) (exprType, error) { + t, err := a.typeExpr(e.Arg) + if err != nil { + return exprType{}, err + } + intT := exprType{} + if oid, err := a.cat.ConstTypeOID(core.ConstInteger); err == nil { + intT.typeOID = oid + } + for _, item := range listItems(e.Indirection) { + idx, ok := item.(*ast.A_Indices) + if !ok { + return exprType{}, fmt.Errorf("typeExpr: unsupported indirection %T", item) + } + for _, bound := range []ast.Node{idx.Lidx, idx.Uidx} { + if err := a.typeOperands(bound, intT); err != nil { + return exprType{}, err + } + } + if idx.IsSlice { + continue + } + element := a.exprOf(t).Element() + if element == nil { + return exprType{nullable: true}, nil + } + t = a.lookupType(element) + t.nullable = true + } + return t, nil +} + +// arrayOf is the type of an array of t's type. +func (a *analyzer) arrayOf(t exprType) exprType { + element := a.exprOf(t) + if element == nil { + return exprType{} + } + return a.lookupType(core.Array(element.WithNullable(false))) +} + // lookupType is the type an expression refers to: its row when the catalog // holds one, its family's row when it holds only that, and the expression // alone when it holds neither. @@ -619,7 +677,7 @@ func (a *analyzer) typeSubLink(e *ast.SubLink) (exprType, error) { return exprType{}, err } switch e.SubLinkType { - case ast.EXPR_SUBLINK, ast.ARRAY_SUBLINK: + case ast.EXPR_SUBLINK: if len(cols) == 0 { return exprType{nullable: true}, nil } @@ -627,6 +685,13 @@ func (a *analyzer) typeSubLink(e *ast.SubLink) (exprType, error) { t := columnExprType(cols[0]) t.nullable = true return t, nil + case ast.ARRAY_SUBLINK: + // ARRAY(SELECT ...) collects the column into an array, which is + // empty rather than NULL when no row matches. + if len(cols) == 0 { + return exprType{}, nil + } + return a.arrayOf(columnExprType(cols[0])), nil default: return a.boolType(false) } @@ -831,12 +896,20 @@ func (a *analyzer) typeFuncCall(f *ast.FuncCall) (exprType, error) { } p := a.pickOverload(overloads, argOIDs) // An argument that is a bare placeholder takes the parameter's type, - // unless the parameter is polymorphic and says nothing. + // unless the parameter is polymorphic, when it takes what the other + // arguments say: the element of the array passed for an anyarray, or + // an array of the value passed for an anyelement, as + // array_includes(anyarray, anyelement) relates them. for i, arg := range args { if i >= len(p.ArgTypes) { break } if a.isPolymorphicOID(p.ArgTypes[i]) { + if t, ok := a.polymorphicArg(p, i, argTypes); ok { + if err := a.typeOperands(arg, t); err != nil { + return exprType{}, err + } + } continue } if err := a.typeOperands(arg, exprType{typeOID: p.ArgTypes[i]}); err != nil { @@ -916,11 +989,61 @@ func (a *analyzer) returnType(p core.ProcOverload, argTypes []exprType) exprType return exprType{} } if isPolymorphic(name) && (argTypes[0].typeOID != 0 || argTypes[0].expr != nil) { - return exprType{typeOID: argTypes[0].typeOID, expr: argTypes[0].expr} + arg := exprType{typeOID: argTypes[0].typeOID, expr: argTypes[0].expr} + // An element taken from an array argument, unnest(anyarray) + // returning anyelement, is the array's element; an array built from + // an element argument is an array of it. + paramName := "" + if len(p.ArgTypes) > 0 { + paramName, _ = a.cat.TypeName(p.ArgTypes[0]) + } + switch { + case isPolymorphicArray(paramName) && !isPolymorphicArray(name): + if element := a.exprOf(arg).Element(); element != nil { + return a.lookupType(element) + } + case !isPolymorphicArray(paramName) && isPolymorphicArray(name) && isPolymorphic(paramName): + return a.arrayOf(arg) + } + return arg } return exprType{typeOID: p.ReturnTypeOID} } +// polymorphicArg is the type the other arguments of a call give its i-th, +// polymorphic, parameter: the element of an array passed for an anyarray +// when the parameter is an element, or an array of an element passed for +// an anyelement when the parameter is an array. +func (a *analyzer) polymorphicArg(p core.ProcOverload, i int, argTypes []exprType) (exprType, bool) { + name, err := a.cat.TypeName(p.ArgTypes[i]) + if err != nil || !isPolymorphic(name) { + return exprType{}, false + } + for j, other := range p.ArgTypes { + if j == i || j >= len(argTypes) || (argTypes[j].typeOID == 0 && argTypes[j].expr == nil) { + continue + } + otherName, err := a.cat.TypeName(other) + if err != nil || !isPolymorphic(otherName) { + continue + } + switch { + case isPolymorphicArray(otherName) && !isPolymorphicArray(name): + if element := a.exprOf(argTypes[j]).Element(); element != nil { + return a.lookupType(element), true + } + case !isPolymorphicArray(otherName) && isPolymorphicArray(name): + return a.arrayOf(argTypes[j]), true + } + } + return exprType{}, false +} + +// isPolymorphicArray reports whether a polymorphic type stands for an array. +func isPolymorphicArray(typeName string) bool { + return typeName == "anyarray" || typeName == "anycompatiblearray" +} + // argIndex reads a seed's "$n" pseudo-type as the zero-based index of the // argument whose type it stands for. func argIndex(typeName string) (int, bool) { diff --git a/internal/core/analyzer/scope.go b/internal/core/analyzer/scope.go index 020d3d1f12..218a8e0b11 100644 --- a/internal/core/analyzer/scope.go +++ b/internal/core/analyzer/scope.go @@ -100,19 +100,28 @@ func (a *analyzer) bindRangeFunction(rf *ast.RangeFunction) (scopeRel, error) { } // The arguments are typed against the scope built so far, which is what - // gives a placeholder passed to the function its type. - if _, err := a.typeFuncCall(call); err != nil { + // gives a placeholder passed to the function its type, and what + // resolves a polymorphic result: unnest of an array of strings yields + // strings. + t, err := a.typeFuncCall(call) + if err != nil { return scopeRel{}, err } - p := overloads[0] - rel := scopeRel{ - alias: name, - cols: []core.ClassColumn{{Name: name, TypeOID: p.ReturnTypeOID, NotNull: !p.ReturnNullable}}, - } + // The relation's alias names its single column too, as "unnest(tags) + // AS tag" does. + colName := name + rel := scopeRel{alias: name} if rf.Alias != nil && rf.Alias.Aliasname != nil && *rf.Alias.Aliasname != "" { rel.alias = *rf.Alias.Aliasname + colName = rel.alias + } + col := core.ClassColumn{Name: colName, TypeOID: t.typeOID, Type: t.expr, NotNull: !t.nullable} + if t.typeOID == 0 && t.expr == nil { + p := overloads[0] + col.TypeOID, col.NotNull = p.ReturnTypeOID, !p.ReturnNullable } + rel.cols = []core.ClassColumn{col} return rel, nil } diff --git a/internal/core/seed/seed.go b/internal/core/seed/seed.go index e14f17aac8..064afedae8 100644 --- a/internal/core/seed/seed.go +++ b/internal/core/seed/seed.go @@ -320,12 +320,24 @@ func apply(cat *core.Catalog, fsys fs.FS, settings Settings) error { if err := b.rules(); err != nil { return err } - if err := b.categoryOperators(); err != nil { + // An operator the dialect lists replaces the one its rules would + // generate for the same operands, as GoogleSQL's INT64 / INT64 is a + // FLOAT64, so the list is read before the rules apply. + var operators []Operator + if err := stream(fsys, OperatorsFile, func(op Operator) error { + operators = append(operators, op) + return nil + }); err != nil { return err } - if err := stream(fsys, OperatorsFile, b.addOperator); err != nil { + if err := b.categoryOperators(operators); err != nil { return err } + for i, op := range operators { + if err := b.addOperator(op); err != nil { + return fmt.Errorf("seed: %s: record %d: %w", OperatorsFile, i+1, err) + } + } if err := stream(fsys, CastsFile, b.addCast); err != nil { return err } @@ -713,8 +725,12 @@ func (b *builder) rules() error { return nil } -func (b *builder) categoryOperators() error { +func (b *builder) categoryOperators(listed []Operator) error { s := b.settings + overridden := map[[3]string]bool{} + for _, op := range listed { + overridden[[3]string{op.Name, strings.ToLower(op.Left), strings.ToLower(op.Right)}] = true + } boolOID, ok := b.oids[strings.ToLower(s.Bool)] if len(s.Comparison) > 0 && !ok { return fmt.Errorf("seed %s: comparison operators need a bool type", s.Dialect) @@ -729,6 +745,9 @@ func (b *builder) categoryOperators() error { } if strings.Contains(s.ArithmeticCategories, t.category) { for _, op := range s.Arithmetic { + if overridden[[3]string{op, strings.ToLower(t.name), strings.ToLower(t.name)}] { + continue + } if err := b.operator(op, t.name, t.name, t.name, b.oids[t.name]); err != nil { return err } diff --git a/internal/endtoend/testdata/analyze_arrays/googlesql/exec.json b/internal/endtoend/testdata/analyze_arrays/googlesql/exec.json new file mode 100644 index 0000000000..a53ddddc6d --- /dev/null +++ b/internal/endtoend/testdata/analyze_arrays/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_arrays/googlesql/query.sql b/internal/endtoend/testdata/analyze_arrays/googlesql/query.sql new file mode 100644 index 0000000000..5d4666d91f --- /dev/null +++ b/internal/endtoend/testdata/analyze_arrays/googlesql/query.sql @@ -0,0 +1,8 @@ +-- name: AlbumIDs :one +SELECT singer_id, ARRAY(SELECT album_id FROM albums WHERE albums.singer_id = singers.singer_id) AS album_ids +FROM singers WHERE singer_id = @singer_id; + +-- name: Literals :one +SELECT [1, 2, 3] AS ints, ARRAY['a'] AS strs, DATE '2024-01-02' AS d, + TIMESTAMP '2024-01-02 03:04:05+00' AS ts, NUMERIC '1.5' AS n, JSON '{"a": 1}' AS j, + b'abc' AS raw, INTERVAL 3 DAY AS iv; diff --git a/internal/endtoend/testdata/analyze_arrays/googlesql/schema.sql b/internal/endtoend/testdata/analyze_arrays/googlesql/schema.sql new file mode 100644 index 0000000000..667e087fb6 --- /dev/null +++ b/internal/endtoend/testdata/analyze_arrays/googlesql/schema.sql @@ -0,0 +1,13 @@ +CREATE TABLE singers ( + singer_id INT64 NOT NULL, + name STRING(MAX) NOT NULL, + tags ARRAY, + scores ARRAY NOT NULL, +) PRIMARY KEY (singer_id); + +CREATE TABLE albums ( + singer_id INT64 NOT NULL, + album_id INT64 NOT NULL, + title STRING(MAX), +) PRIMARY KEY (singer_id, album_id), + INTERLEAVE IN PARENT singers ON DELETE CASCADE; diff --git a/internal/endtoend/testdata/analyze_arrays/googlesql/stdout.json b/internal/endtoend/testdata/analyze_arrays/googlesql/stdout.json new file mode 100644 index 0000000000..920fc64da6 --- /dev/null +++ b/internal/endtoend/testdata/analyze_arrays/googlesql/stdout.json @@ -0,0 +1,109 @@ +[ + { + "name": "AlbumIDs", + "cmd": ":one", + "columns": [ + { + "name": "singer_id", + "type": { + "name": "int64" + }, + "table": "singers" + }, + { + "name": "album_ids", + "type": { + "name": "array", + "args": [ + { + "type": { + "name": "int64" + } + } + ] + } + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "singer_id", + "type": { + "name": "int64" + }, + "table": "singers" + } + } + ] + }, + { + "name": "Literals", + "cmd": ":one", + "columns": [ + { + "name": "ints", + "type": { + "name": "array", + "args": [ + { + "type": { + "name": "int64" + } + } + ] + } + }, + { + "name": "strs", + "type": { + "name": "array", + "args": [ + { + "type": { + "name": "string" + } + } + ] + } + }, + { + "name": "d", + "type": { + "name": "date" + } + }, + { + "name": "ts", + "type": { + "name": "timestamp" + } + }, + { + "name": "n", + "type": { + "name": "numeric" + } + }, + { + "name": "j", + "type": { + "name": "json" + } + }, + { + "name": "raw", + "type": { + "name": "bytes" + } + }, + { + "name": "iv", + "type": { + "name": "interval" + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_functions/googlesql/exec.json b/internal/endtoend/testdata/analyze_functions/googlesql/exec.json new file mode 100644 index 0000000000..a53ddddc6d --- /dev/null +++ b/internal/endtoend/testdata/analyze_functions/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_functions/googlesql/query.sql b/internal/endtoend/testdata/analyze_functions/googlesql/query.sql new file mode 100644 index 0000000000..d441415585 --- /dev/null +++ b/internal/endtoend/testdata/analyze_functions/googlesql/query.sql @@ -0,0 +1,25 @@ +-- name: Strings :many +SELECT LENGTH(name) AS len, UPPER(name) AS upper_name, CONCAT(name, '!') AS shout, + SPLIT(name, ',') AS parts, STARTS_WITH(name, 'a') AS starts, TO_HEX(SHA256(name)) AS digest, + FARM_FINGERPRINT(name) AS fingerprint, REPLACE(name, 'a', 'b') AS replaced, TO_BASE64(payload) AS b64 +FROM events; + +-- name: Numbers :many +SELECT ABS(id) AS abs_id, id / 2 AS half, amount * id AS scaled, ratio + id AS shifted, + ROUND(ratio, 2) AS rounded, MOD(id, 7) AS bucket, DIV(id, 2) AS halved, COSINE_DISTANCE(embedding, embedding) AS distance +FROM events; + +-- name: Times :many +SELECT DATE_ADD(happened_on, INTERVAL 1 DAY) AS next_day, DATE_DIFF(happened_on, happened_on, DAY) AS days, + TIMESTAMP_TRUNC(happened_at, HOUR) AS hour, EXTRACT(YEAR FROM happened_at) AS year, + EXTRACT(DATE FROM happened_at) AS day, UNIX_SECONDS(happened_at) AS epoch, + FORMAT_TIMESTAMP('%Y', happened_at) AS formatted +FROM events; + +-- name: Collections :many +SELECT ARRAY_LENGTH(scores) AS n, ARRAY_REVERSE(scores) AS reversed, GENERATE_ARRAY(1, id) AS series, + TO_JSON(id) AS as_json, JSON_TYPE(details) AS json_kind +FROM events; + +-- name: Counts :one +SELECT COUNT(*) AS total, COUNT(name) AS named, COUNTIF(ratio > 0.5) AS high FROM events; diff --git a/internal/endtoend/testdata/analyze_functions/googlesql/schema.sql b/internal/endtoend/testdata/analyze_functions/googlesql/schema.sql new file mode 100644 index 0000000000..ea03d7fcef --- /dev/null +++ b/internal/endtoend/testdata/analyze_functions/googlesql/schema.sql @@ -0,0 +1,12 @@ +CREATE TABLE events ( + id INT64 NOT NULL, + name STRING(MAX) NOT NULL, + payload BYTES(MAX) NOT NULL, + amount NUMERIC NOT NULL, + ratio FLOAT64 NOT NULL, + happened_on DATE NOT NULL, + happened_at TIMESTAMP NOT NULL, + details JSON NOT NULL, + scores ARRAY NOT NULL, + embedding ARRAY NOT NULL, +) PRIMARY KEY (id); diff --git a/internal/endtoend/testdata/analyze_functions/googlesql/stdout.json b/internal/endtoend/testdata/analyze_functions/googlesql/stdout.json new file mode 100644 index 0000000000..5e44b966d9 --- /dev/null +++ b/internal/endtoend/testdata/analyze_functions/googlesql/stdout.json @@ -0,0 +1,250 @@ +[ + { + "name": "Strings", + "cmd": ":many", + "columns": [ + { + "name": "len", + "type": { + "name": "int64" + } + }, + { + "name": "upper_name", + "type": { + "name": "string" + } + }, + { + "name": "shout", + "type": { + "name": "string" + } + }, + { + "name": "parts", + "type": { + "name": "array", + "args": [ + { + "type": { + "name": "string" + } + } + ] + } + }, + { + "name": "starts", + "type": { + "name": "bool" + } + }, + { + "name": "digest", + "type": { + "name": "string" + } + }, + { + "name": "fingerprint", + "type": { + "name": "int64" + } + }, + { + "name": "replaced", + "type": { + "name": "string" + } + }, + { + "name": "b64", + "type": { + "name": "string" + } + } + ], + "params": [] + }, + { + "name": "Numbers", + "cmd": ":many", + "columns": [ + { + "name": "abs_id", + "type": { + "name": "int64" + } + }, + { + "name": "half", + "type": { + "name": "float64" + } + }, + { + "name": "scaled", + "type": { + "name": "numeric" + } + }, + { + "name": "shifted", + "type": { + "name": "float64" + } + }, + { + "name": "rounded", + "type": { + "name": "float64" + } + }, + { + "name": "bucket", + "type": { + "name": "int64" + } + }, + { + "name": "halved", + "type": { + "name": "int64" + } + }, + { + "name": "distance", + "type": { + "name": "float64" + } + } + ], + "params": [] + }, + { + "name": "Times", + "cmd": ":many", + "columns": [ + { + "name": "next_day", + "type": { + "name": "date" + } + }, + { + "name": "days", + "type": { + "name": "int64" + } + }, + { + "name": "hour", + "type": { + "name": "timestamp" + } + }, + { + "name": "year", + "type": { + "name": "int64" + } + }, + { + "name": "day", + "type": { + "name": "date" + } + }, + { + "name": "epoch", + "type": { + "name": "int64" + } + }, + { + "name": "formatted", + "type": { + "name": "string" + } + } + ], + "params": [] + }, + { + "name": "Collections", + "cmd": ":many", + "columns": [ + { + "name": "n", + "type": { + "name": "int64" + } + }, + { + "name": "reversed", + "type": { + "name": "array", + "args": [ + { + "type": { + "name": "int64" + } + } + ] + } + }, + { + "name": "series", + "type": { + "name": "array", + "args": [ + { + "type": { + "name": "int64" + } + } + ] + } + }, + { + "name": "as_json", + "type": { + "name": "json" + } + }, + { + "name": "json_kind", + "type": { + "name": "string" + } + } + ], + "params": [] + }, + { + "name": "Counts", + "cmd": ":one", + "columns": [ + { + "name": "total", + "type": { + "name": "int64" + } + }, + { + "name": "named", + "type": { + "name": "int64" + } + }, + { + "name": "high", + "type": { + "name": "int64" + } + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_types/bigquery/stdout.json b/internal/endtoend/testdata/analyze_types/bigquery/stdout.json index 8119ad12a2..34ebaf0b24 100644 --- a/internal/endtoend/testdata/analyze_types/bigquery/stdout.json +++ b/internal/endtoend/testdata/analyze_types/bigquery/stdout.json @@ -291,7 +291,17 @@ } }, { - "name": "f" + "name": "f", + "type": { + "name": "array", + "args": [ + { + "type": { + "name": "int64" + } + } + ] + } } ], "params": [ diff --git a/internal/endtoend/testdata/array_unnest/googlesql/exec.json b/internal/endtoend/testdata/array_unnest/googlesql/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/array_unnest/googlesql/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/array_unnest/googlesql/go/db.go b/internal/endtoend/testdata/array_unnest/googlesql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/array_unnest/googlesql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/array_unnest/googlesql/go/models.go b/internal/endtoend/testdata/array_unnest/googlesql/go/models.go new file mode 100644 index 0000000000..62304209ba --- /dev/null +++ b/internal/endtoend/testdata/array_unnest/googlesql/go/models.go @@ -0,0 +1,24 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" + + "cloud.google.com/go/spanner" +) + +type Album struct { + SingerID int64 + AlbumID int64 + Title sql.NullString +} + +type Singer struct { + SingerID int64 + Name string + Tags []spanner.NullString + Scores []spanner.NullInt64 +} diff --git a/internal/endtoend/testdata/array_unnest/googlesql/go/query.sql.go b/internal/endtoend/testdata/array_unnest/googlesql/go/query.sql.go new file mode 100644 index 0000000000..3443ac6d1a --- /dev/null +++ b/internal/endtoend/testdata/array_unnest/googlesql/go/query.sql.go @@ -0,0 +1,144 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" + + "cloud.google.com/go/spanner" +) + +const albumTitles = `-- name: AlbumTitles :one +SELECT singer_id, ARRAY(SELECT title FROM albums WHERE albums.singer_id = singers.singer_id) AS titles +FROM singers WHERE singer_id = @singer_id +` + +type AlbumTitlesRow struct { + SingerID int64 + Titles []spanner.NullString +} + +func (q *Queries) AlbumTitles(ctx context.Context, singerID int64) (AlbumTitlesRow, error) { + row := q.db.QueryRowContext(ctx, albumTitles, sql.Named("singer_id", singerID)) + var i AlbumTitlesRow + err := row.Scan(&i.SingerID, &i.Titles) + return i, err +} + +const firstTag = `-- name: FirstTag :one +SELECT tags[OFFSET(0)] AS first_tag, tags[SAFE_ORDINAL(@n)] AS nth_tag, scores[SAFE_OFFSET(0)] AS first_score +FROM singers WHERE singer_id = @singer_id +` + +type FirstTagParams struct { + N int64 + SingerID int64 +} + +type FirstTagRow struct { + FirstTag sql.NullString + NthTag sql.NullString + FirstScore sql.NullInt64 +} + +func (q *Queries) FirstTag(ctx context.Context, arg FirstTagParams) (FirstTagRow, error) { + row := q.db.QueryRowContext(ctx, firstTag, sql.Named("n", arg.N), sql.Named("singer_id", arg.SingerID)) + var i FirstTagRow + err := row.Scan(&i.FirstTag, &i.NthTag, &i.FirstScore) + return i, err +} + +const singerTags = `-- name: SingerTags :many +SELECT singer_id, tag FROM singers, UNNEST(tags) AS tag +` + +type SingerTagsRow struct { + SingerID int64 + Tag sql.NullString +} + +func (q *Queries) SingerTags(ctx context.Context) ([]SingerTagsRow, error) { + rows, err := q.db.QueryContext(ctx, singerTags) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SingerTagsRow + for rows.Next() { + var i SingerTagsRow + if err := rows.Scan(&i.SingerID, &i.Tag); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const singersByIDs = `-- name: SingersByIDs :many +SELECT singer_id, name FROM singers WHERE singer_id IN UNNEST(@ids) +` + +type SingersByIDsRow struct { + SingerID int64 + Name string +} + +func (q *Queries) SingersByIDs(ctx context.Context, ids []spanner.NullInt64) ([]SingersByIDsRow, error) { + rows, err := q.db.QueryContext(ctx, singersByIDs, sql.Named("ids", ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SingersByIDsRow + for rows.Next() { + var i SingersByIDsRow + if err := rows.Scan(&i.SingerID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const singersNotIn = `-- name: SingersNotIn :many +SELECT singer_id FROM singers WHERE singer_id NOT IN UNNEST(@ids) +` + +func (q *Queries) SingersNotIn(ctx context.Context, ids []spanner.NullInt64) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, singersNotIn, sql.Named("ids", ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var singer_id int64 + if err := rows.Scan(&singer_id); err != nil { + return nil, err + } + items = append(items, singer_id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/array_unnest/googlesql/query.sql b/internal/endtoend/testdata/array_unnest/googlesql/query.sql new file mode 100644 index 0000000000..caf34a04a2 --- /dev/null +++ b/internal/endtoend/testdata/array_unnest/googlesql/query.sql @@ -0,0 +1,16 @@ +-- name: SingersByIDs :many +SELECT singer_id, name FROM singers WHERE singer_id IN UNNEST(@ids); + +-- name: SingersNotIn :many +SELECT singer_id FROM singers WHERE singer_id NOT IN UNNEST(@ids); + +-- name: SingerTags :many +SELECT singer_id, tag FROM singers, UNNEST(tags) AS tag; + +-- name: AlbumTitles :one +SELECT singer_id, ARRAY(SELECT title FROM albums WHERE albums.singer_id = singers.singer_id) AS titles +FROM singers WHERE singer_id = @singer_id; + +-- name: FirstTag :one +SELECT tags[OFFSET(0)] AS first_tag, tags[SAFE_ORDINAL(@n)] AS nth_tag, scores[SAFE_OFFSET(0)] AS first_score +FROM singers WHERE singer_id = @singer_id; diff --git a/internal/endtoend/testdata/array_unnest/googlesql/schema.sql b/internal/endtoend/testdata/array_unnest/googlesql/schema.sql new file mode 100644 index 0000000000..667e087fb6 --- /dev/null +++ b/internal/endtoend/testdata/array_unnest/googlesql/schema.sql @@ -0,0 +1,13 @@ +CREATE TABLE singers ( + singer_id INT64 NOT NULL, + name STRING(MAX) NOT NULL, + tags ARRAY, + scores ARRAY NOT NULL, +) PRIMARY KEY (singer_id); + +CREATE TABLE albums ( + singer_id INT64 NOT NULL, + album_id INT64 NOT NULL, + title STRING(MAX), +) PRIMARY KEY (singer_id, album_id), + INTERLEAVE IN PARENT singers ON DELETE CASCADE; diff --git a/internal/endtoend/testdata/array_unnest/googlesql/sqlc.json b/internal/endtoend/testdata/array_unnest/googlesql/sqlc.json new file mode 100644 index 0000000000..40d6c76211 --- /dev/null +++ b/internal/endtoend/testdata/array_unnest/googlesql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "googlesql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/create_view/googlesql/exec.json b/internal/endtoend/testdata/create_view/googlesql/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/create_view/googlesql/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/create_view/googlesql/go/db.go b/internal/endtoend/testdata/create_view/googlesql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/create_view/googlesql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/create_view/googlesql/go/models.go b/internal/endtoend/testdata/create_view/googlesql/go/models.go new file mode 100644 index 0000000000..522a07f37d --- /dev/null +++ b/internal/endtoend/testdata/create_view/googlesql/go/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Venue struct { + ID int64 + Name string + City sql.NullString +} diff --git a/internal/endtoend/testdata/create_view/googlesql/go/query.sql.go b/internal/endtoend/testdata/create_view/googlesql/go/query.sql.go new file mode 100644 index 0000000000..da7ee04904 --- /dev/null +++ b/internal/endtoend/testdata/create_view/googlesql/go/query.sql.go @@ -0,0 +1,44 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const listVenueNames = `-- name: ListVenueNames :many +SELECT id, name, city FROM venue_names +` + +type ListVenueNamesRow struct { + ID int64 + Name string + City sql.NullString +} + +func (q *Queries) ListVenueNames(ctx context.Context) ([]ListVenueNamesRow, error) { + rows, err := q.db.QueryContext(ctx, listVenueNames) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListVenueNamesRow + for rows.Next() { + var i ListVenueNamesRow + if err := rows.Scan(&i.ID, &i.Name, &i.City); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/create_view/googlesql/query.sql b/internal/endtoend/testdata/create_view/googlesql/query.sql new file mode 100644 index 0000000000..c82210095d --- /dev/null +++ b/internal/endtoend/testdata/create_view/googlesql/query.sql @@ -0,0 +1,2 @@ +-- name: ListVenueNames :many +SELECT * FROM venue_names; diff --git a/internal/endtoend/testdata/create_view/googlesql/schema.sql b/internal/endtoend/testdata/create_view/googlesql/schema.sql new file mode 100644 index 0000000000..6919db659e --- /dev/null +++ b/internal/endtoend/testdata/create_view/googlesql/schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE venues ( + id INT64 NOT NULL, + name STRING(MAX) NOT NULL, + city STRING(MAX), +) PRIMARY KEY (id); +CREATE VIEW venue_names SQL SECURITY INVOKER AS +SELECT venues.id, venues.name FROM venues; +CREATE OR REPLACE VIEW venue_names SQL SECURITY INVOKER AS +SELECT venues.id, venues.name, venues.city FROM venues; +CREATE VIEW dropped SQL SECURITY INVOKER AS SELECT venues.id FROM venues; +DROP VIEW dropped; diff --git a/internal/endtoend/testdata/create_view/googlesql/sqlc.json b/internal/endtoend/testdata/create_view/googlesql/sqlc.json new file mode 100644 index 0000000000..40d6c76211 --- /dev/null +++ b/internal/endtoend/testdata/create_view/googlesql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "googlesql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/exec.json b/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/go/db.go b/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/go/models.go b/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/go/models.go new file mode 100644 index 0000000000..36f498a442 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Venue struct { + ID int64 + Name sql.NullString + Location sql.NullString + Size int64 +} diff --git a/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/go/query.sql.go b/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/go/query.sql.go new file mode 100644 index 0000000000..a197546f8a --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/go/query.sql.go @@ -0,0 +1,42 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" +) + +const placeholder = `-- name: Placeholder :many +SELECT id, name, location, size FROM venues +` + +func (q *Queries) Placeholder(ctx context.Context) ([]Venue, error) { + rows, err := q.db.QueryContext(ctx, placeholder) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Venue + for rows.Next() { + var i Venue + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Location, + &i.Size, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/query.sql b/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/query.sql new file mode 100644 index 0000000000..3564511e10 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/query.sql @@ -0,0 +1,2 @@ +-- name: Placeholder :many +SELECT * FROM venues; diff --git a/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/schema.sql b/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/schema.sql new file mode 100644 index 0000000000..d4a74b1bf5 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE venues ( + id INT64 NOT NULL, + name STRING(MAX), +) PRIMARY KEY (id); +ALTER TABLE venues ADD COLUMN location STRING(MAX); +ALTER TABLE venues ADD COLUMN IF NOT EXISTS size INT64 NOT NULL DEFAULT (0); diff --git a/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/sqlc.json b/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/sqlc.json new file mode 100644 index 0000000000..40d6c76211 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_add_column/googlesql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "googlesql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/exec.json b/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/go/db.go b/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/go/models.go b/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/go/models.go new file mode 100644 index 0000000000..8e18cbc391 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Venue struct { + ID int64 + Name string + Capacity int64 + Notes sql.NullString +} diff --git a/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/go/query.sql.go b/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/go/query.sql.go new file mode 100644 index 0000000000..92690255c1 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/go/query.sql.go @@ -0,0 +1,42 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" +) + +const placeholder = `-- name: Placeholder :many +SELECT id, name, capacity, notes FROM venues +` + +func (q *Queries) Placeholder(ctx context.Context) ([]Venue, error) { + rows, err := q.db.QueryContext(ctx, placeholder) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Venue + for rows.Next() { + var i Venue + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Capacity, + &i.Notes, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/query.sql b/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/query.sql new file mode 100644 index 0000000000..3564511e10 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/query.sql @@ -0,0 +1,2 @@ +-- name: Placeholder :many +SELECT * FROM venues; diff --git a/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/schema.sql b/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/schema.sql new file mode 100644 index 0000000000..3571469d9e --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE venues ( + id INT64 NOT NULL, + name STRING(10) NOT NULL, + capacity INT64, + notes STRING(MAX) NOT NULL, +) PRIMARY KEY (id); +ALTER TABLE venues ALTER COLUMN name STRING(MAX) NOT NULL; +ALTER TABLE venues ALTER COLUMN capacity INT64 NOT NULL DEFAULT (0); +ALTER TABLE venues ALTER COLUMN notes STRING(MAX); diff --git a/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/sqlc.json b/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/sqlc.json new file mode 100644 index 0000000000..40d6c76211 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_alter_type/googlesql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "googlesql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/exec.json b/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/go/db.go b/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/go/models.go b/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/go/models.go new file mode 100644 index 0000000000..c67630d1c3 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/go/models.go @@ -0,0 +1,14 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Venue struct { + ID int64 + Name sql.NullString +} diff --git a/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/go/query.sql.go b/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/go/query.sql.go new file mode 100644 index 0000000000..d0eefdd98e --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/go/query.sql.go @@ -0,0 +1,37 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" +) + +const placeholder = `-- name: Placeholder :many +SELECT id, name FROM venues +` + +func (q *Queries) Placeholder(ctx context.Context) ([]Venue, error) { + rows, err := q.db.QueryContext(ctx, placeholder) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Venue + for rows.Next() { + var i Venue + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/query.sql b/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/query.sql new file mode 100644 index 0000000000..3564511e10 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/query.sql @@ -0,0 +1,2 @@ +-- name: Placeholder :many +SELECT * FROM venues; diff --git a/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/schema.sql b/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/schema.sql new file mode 100644 index 0000000000..6cd6d76fff --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE venues ( + id INT64 NOT NULL, + name STRING(MAX), + slug STRING(MAX), +) PRIMARY KEY (id); +ALTER TABLE venues DROP COLUMN slug; diff --git a/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/sqlc.json b/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/sqlc.json new file mode 100644 index 0000000000..40d6c76211 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_drop_column/googlesql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "googlesql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/exec.json b/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/go/db.go b/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/go/models.go b/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/go/models.go new file mode 100644 index 0000000000..b6eccd45e1 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/go/models.go @@ -0,0 +1,13 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +type Arena struct { + ID int64 +} + +type Venue struct { + ID int64 +} diff --git a/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/go/query.sql.go b/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/go/query.sql.go new file mode 100644 index 0000000000..ef6ecead1f --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/go/query.sql.go @@ -0,0 +1,64 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" +) + +const listArenas = `-- name: ListArenas :many +SELECT id FROM arena +` + +func (q *Queries) ListArenas(ctx context.Context) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, listArenas) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listVenues = `-- name: ListVenues :many +SELECT id FROM venue +` + +func (q *Queries) ListVenues(ctx context.Context) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, listVenues) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/query.sql b/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/query.sql new file mode 100644 index 0000000000..274ee7b250 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/query.sql @@ -0,0 +1,5 @@ +-- name: ListVenues :many +SELECT * FROM venue; + +-- name: ListArenas :many +SELECT * FROM arena; diff --git a/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/schema.sql b/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/schema.sql new file mode 100644 index 0000000000..d6da5181dd --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE venues ( + id INT64 NOT NULL, +) PRIMARY KEY (id); +CREATE TABLE arenas ( + id INT64 NOT NULL, +) PRIMARY KEY (id); +ALTER TABLE venues RENAME TO venue; +RENAME TABLE arenas TO stadiums, stadiums TO arena; diff --git a/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/sqlc.json b/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/sqlc.json new file mode 100644 index 0000000000..40d6c76211 --- /dev/null +++ b/internal/endtoend/testdata/ddl_alter_table_rename/googlesql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "googlesql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/ddl_spanner/googlesql/exec.json b/internal/endtoend/testdata/ddl_spanner/googlesql/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/ddl_spanner/googlesql/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/ddl_spanner/googlesql/go/db.go b/internal/endtoend/testdata/ddl_spanner/googlesql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/ddl_spanner/googlesql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/ddl_spanner/googlesql/go/models.go b/internal/endtoend/testdata/ddl_spanner/googlesql/go/models.go new file mode 100644 index 0000000000..973faeb46e --- /dev/null +++ b/internal/endtoend/testdata/ddl_spanner/googlesql/go/models.go @@ -0,0 +1,35 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" + "time" + + "cloud.google.com/go/spanner" +) + +type Album struct { + SingerID int64 + AlbumID int64 + Title sql.NullString + CreatedAt sql.NullTime +} + +type Singer struct { + SingerID int64 + FirstName sql.NullString + LastName string + FullName string + Active bool + UpdatedAt time.Time +} + +type Ticket struct { + TicketID int64 + SingerID int64 + SeatID sql.NullInt64 + Embedding []spanner.NullFloat32 +} diff --git a/internal/endtoend/testdata/ddl_spanner/googlesql/go/query.sql.go b/internal/endtoend/testdata/ddl_spanner/googlesql/go/query.sql.go new file mode 100644 index 0000000000..34ff922f61 --- /dev/null +++ b/internal/endtoend/testdata/ddl_spanner/googlesql/go/query.sql.go @@ -0,0 +1,109 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const listAlbums = `-- name: ListAlbums :many +SELECT singer_id, album_id, title, created_at FROM albums WHERE singer_id = @singer_id +` + +func (q *Queries) ListAlbums(ctx context.Context, singerID int64) ([]Album, error) { + rows, err := q.db.QueryContext(ctx, listAlbums, sql.Named("singer_id", singerID)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Album + for rows.Next() { + var i Album + if err := rows.Scan( + &i.SingerID, + &i.AlbumID, + &i.Title, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listSingers = `-- name: ListSingers :many +SELECT singer_id, first_name, last_name, full_name, active, updated_at FROM singers +` + +func (q *Queries) ListSingers(ctx context.Context) ([]Singer, error) { + rows, err := q.db.QueryContext(ctx, listSingers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Singer + for rows.Next() { + var i Singer + if err := rows.Scan( + &i.SingerID, + &i.FirstName, + &i.LastName, + &i.FullName, + &i.Active, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listTickets = `-- name: ListTickets :many +SELECT ticket_id, singer_id, seat_id, embedding FROM tickets +` + +func (q *Queries) ListTickets(ctx context.Context) ([]Ticket, error) { + rows, err := q.db.QueryContext(ctx, listTickets) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Ticket + for rows.Next() { + var i Ticket + if err := rows.Scan( + &i.TicketID, + &i.SingerID, + &i.SeatID, + &i.Embedding, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/ddl_spanner/googlesql/query.sql b/internal/endtoend/testdata/ddl_spanner/googlesql/query.sql new file mode 100644 index 0000000000..c42d016638 --- /dev/null +++ b/internal/endtoend/testdata/ddl_spanner/googlesql/query.sql @@ -0,0 +1,8 @@ +-- name: ListSingers :many +SELECT * FROM singers; + +-- name: ListAlbums :many +SELECT * FROM albums WHERE singer_id = @singer_id; + +-- name: ListTickets :many +SELECT * FROM tickets; diff --git a/internal/endtoend/testdata/ddl_spanner/googlesql/schema.sql b/internal/endtoend/testdata/ddl_spanner/googlesql/schema.sql new file mode 100644 index 0000000000..fb0203267e --- /dev/null +++ b/internal/endtoend/testdata/ddl_spanner/googlesql/schema.sql @@ -0,0 +1,42 @@ +CREATE SEQUENCE ticket_ids OPTIONS (sequence_kind = 'bit_reversed_positive'); + +CREATE TABLE singers ( + singer_id INT64 NOT NULL, + first_name STRING(1024), + last_name STRING(1024) NOT NULL, + full_name STRING(2048) NOT NULL AS (ARRAY_TO_STRING([first_name, last_name], ' ')) STORED, + active BOOL NOT NULL DEFAULT (true), + updated_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp = true), + name_tokens TOKENLIST AS (TOKENIZE_FULLTEXT(last_name)) HIDDEN, +) PRIMARY KEY (singer_id); + +CREATE TABLE albums ( + singer_id INT64 NOT NULL, + album_id INT64 NOT NULL, + title STRING(MAX), + created_at TIMESTAMP, + CONSTRAINT title_not_empty CHECK (title != ''), +) PRIMARY KEY (singer_id, album_id), + INTERLEAVE IN PARENT singers ON DELETE CASCADE, + ROW DELETION POLICY (OLDER_THAN(created_at, INTERVAL 30 DAY)); + +CREATE TABLE tickets ( + ticket_id INT64 NOT NULL DEFAULT (GET_NEXT_SEQUENCE_VALUE(SEQUENCE ticket_ids)), + singer_id INT64 NOT NULL, + seat_id INT64 GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), + embedding ARRAY(vector_length=>3), + CONSTRAINT fk_singer FOREIGN KEY (singer_id) REFERENCES singers (singer_id), +) PRIMARY KEY (ticket_id); + +CREATE INDEX albums_by_title ON albums(title); +CREATE UNIQUE NULL_FILTERED INDEX singers_by_last_name ON singers(last_name) STORING (first_name); +CREATE SEARCH INDEX singers_by_name_tokens ON singers(name_tokens); +CREATE VECTOR INDEX tickets_by_embedding ON tickets(embedding) WHERE embedding IS NOT NULL OPTIONS (distance_type = 'COSINE'); + +CREATE CHANGE STREAM everything FOR ALL; +CREATE CHANGE STREAM singer_changes FOR singers(first_name, last_name) OPTIONS (retention_period = '7d'); + +CREATE ROLE reader; +GRANT SELECT ON TABLE singers, albums TO ROLE reader; + +ALTER TABLE albums REPLACE ROW DELETION POLICY (OLDER_THAN(created_at, INTERVAL 60 DAY)); diff --git a/internal/endtoend/testdata/ddl_spanner/googlesql/sqlc.json b/internal/endtoend/testdata/ddl_spanner/googlesql/sqlc.json new file mode 100644 index 0000000000..40d6c76211 --- /dev/null +++ b/internal/endtoend/testdata/ddl_spanner/googlesql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "googlesql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/func_nullability/googlesql/exec.json b/internal/endtoend/testdata/func_nullability/googlesql/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/func_nullability/googlesql/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/func_nullability/googlesql/go/db.go b/internal/endtoend/testdata/func_nullability/googlesql/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/func_nullability/googlesql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/func_nullability/googlesql/go/models.go b/internal/endtoend/testdata/func_nullability/googlesql/go/models.go new file mode 100644 index 0000000000..2880a343dc --- /dev/null +++ b/internal/endtoend/testdata/func_nullability/googlesql/go/models.go @@ -0,0 +1,24 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" + "math/big" + "time" + + "cloud.google.com/go/spanner" +) + +type Event struct { + ID int64 + Name string + Nickname sql.NullString + Amount big.Rat + Ratio sql.NullFloat64 + HappenedAt time.Time + Details spanner.NullJSON + Scores []spanner.NullInt64 +} diff --git a/internal/endtoend/testdata/func_nullability/googlesql/go/query.sql.go b/internal/endtoend/testdata/func_nullability/googlesql/go/query.sql.go new file mode 100644 index 0000000000..62cd454cf2 --- /dev/null +++ b/internal/endtoend/testdata/func_nullability/googlesql/go/query.sql.go @@ -0,0 +1,205 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" + "time" + + "cloud.google.com/go/civil" + "cloud.google.com/go/spanner" +) + +const aggregates = `-- name: Aggregates :one +SELECT COUNT(*) AS total, SUM(amount) AS total_amount, AVG(id) AS mean_id, MAX(name) AS last_name, + ARRAY_AGG(id) AS ids, STRING_AGG(name, ',') AS names +FROM events +` + +type AggregatesRow struct { + Total int64 + TotalAmount spanner.NullNumeric + MeanID sql.NullFloat64 + LastName sql.NullString + Ids []spanner.NullInt64 + Names sql.NullString +} + +func (q *Queries) Aggregates(ctx context.Context) (AggregatesRow, error) { + row := q.db.QueryRowContext(ctx, aggregates) + var i AggregatesRow + err := row.Scan( + &i.Total, + &i.TotalAmount, + &i.MeanID, + &i.LastName, + &i.Ids, + &i.Names, + ) + return i, err +} + +const conditionals = `-- name: Conditionals :many +SELECT COALESCE(nickname, name) AS display_name, IFNULL(ratio, 0) AS ratio_or_zero, + NULLIF(name, '') AS non_empty, IF(id > 10, name, nickname) AS chosen, GREATEST(id, 1) AS at_least_one +FROM events +` + +type ConditionalsRow struct { + DisplayName string + RatioOrZero float64 + NonEmpty sql.NullString + Chosen sql.NullString + AtLeastOne int64 +} + +func (q *Queries) Conditionals(ctx context.Context) ([]ConditionalsRow, error) { + rows, err := q.db.QueryContext(ctx, conditionals) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ConditionalsRow + for rows.Next() { + var i ConditionalsRow + if err := rows.Scan( + &i.DisplayName, + &i.RatioOrZero, + &i.NonEmpty, + &i.Chosen, + &i.AtLeastOne, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const mayBeNull = `-- name: MayBeNull :many +SELECT JSON_VALUE(details, '$.kind') AS kind, REGEXP_EXTRACT(name, 'a+') AS run, + SAFE_DIVIDE(id, 0) AS ratio, SAFE.SUBSTR(name, 1, 3) AS prefix, ARRAY_FIRST(scores) AS first_score +FROM events +` + +type MayBeNullRow struct { + Kind sql.NullString + Run sql.NullString + Ratio sql.NullFloat64 + Prefix sql.NullString + FirstScore sql.NullInt64 +} + +func (q *Queries) MayBeNull(ctx context.Context) ([]MayBeNullRow, error) { + rows, err := q.db.QueryContext(ctx, mayBeNull) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MayBeNullRow + for rows.Next() { + var i MayBeNullRow + if err := rows.Scan( + &i.Kind, + &i.Run, + &i.Ratio, + &i.Prefix, + &i.FirstScore, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const paramsFromFunctions = `-- name: ParamsFromFunctions :many +SELECT id FROM events +WHERE STARTS_WITH(name, @prefix) AND TIMESTAMP_TRUNC(happened_at, DAY) = @day + AND EXTRACT(DATE FROM happened_at) = @on AND ARRAY_INCLUDES(scores, @score) +` + +type ParamsFromFunctionsParams struct { + Prefix string + Day time.Time + On civil.Date + Score int64 +} + +func (q *Queries) ParamsFromFunctions(ctx context.Context, arg ParamsFromFunctionsParams) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, paramsFromFunctions, + sql.Named("prefix", arg.Prefix), + sql.Named("day", arg.Day), + sql.Named("on", arg.On), + sql.Named("score", arg.Score), + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const propagates = `-- name: Propagates :many +SELECT UPPER(name) AS upper_name, UPPER(nickname) AS upper_nickname, LENGTH(nickname) AS nickname_length +FROM events +` + +type PropagatesRow struct { + UpperName string + UpperNickname sql.NullString + NicknameLength sql.NullInt64 +} + +func (q *Queries) Propagates(ctx context.Context) ([]PropagatesRow, error) { + rows, err := q.db.QueryContext(ctx, propagates) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PropagatesRow + for rows.Next() { + var i PropagatesRow + if err := rows.Scan(&i.UpperName, &i.UpperNickname, &i.NicknameLength); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/func_nullability/googlesql/query.sql b/internal/endtoend/testdata/func_nullability/googlesql/query.sql new file mode 100644 index 0000000000..517af79cc8 --- /dev/null +++ b/internal/endtoend/testdata/func_nullability/googlesql/query.sql @@ -0,0 +1,23 @@ +-- name: Aggregates :one +SELECT COUNT(*) AS total, SUM(amount) AS total_amount, AVG(id) AS mean_id, MAX(name) AS last_name, + ARRAY_AGG(id) AS ids, STRING_AGG(name, ',') AS names +FROM events; + +-- name: Propagates :many +SELECT UPPER(name) AS upper_name, UPPER(nickname) AS upper_nickname, LENGTH(nickname) AS nickname_length +FROM events; + +-- name: MayBeNull :many +SELECT JSON_VALUE(details, '$.kind') AS kind, REGEXP_EXTRACT(name, 'a+') AS run, + SAFE_DIVIDE(id, 0) AS ratio, SAFE.SUBSTR(name, 1, 3) AS prefix, ARRAY_FIRST(scores) AS first_score +FROM events; + +-- name: Conditionals :many +SELECT COALESCE(nickname, name) AS display_name, IFNULL(ratio, 0) AS ratio_or_zero, + NULLIF(name, '') AS non_empty, IF(id > 10, name, nickname) AS chosen, GREATEST(id, 1) AS at_least_one +FROM events; + +-- name: ParamsFromFunctions :many +SELECT id FROM events +WHERE STARTS_WITH(name, @prefix) AND TIMESTAMP_TRUNC(happened_at, DAY) = @day + AND EXTRACT(DATE FROM happened_at) = @on AND ARRAY_INCLUDES(scores, @score); diff --git a/internal/endtoend/testdata/func_nullability/googlesql/schema.sql b/internal/endtoend/testdata/func_nullability/googlesql/schema.sql new file mode 100644 index 0000000000..cce2a7cf8e --- /dev/null +++ b/internal/endtoend/testdata/func_nullability/googlesql/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE events ( + id INT64 NOT NULL, + name STRING(MAX) NOT NULL, + nickname STRING(MAX), + amount NUMERIC NOT NULL, + ratio FLOAT64, + happened_at TIMESTAMP NOT NULL, + details JSON NOT NULL, + scores ARRAY NOT NULL, +) PRIMARY KEY (id); diff --git a/internal/endtoend/testdata/func_nullability/googlesql/sqlc.json b/internal/endtoend/testdata/func_nullability/googlesql/sqlc.json new file mode 100644 index 0000000000..40d6c76211 --- /dev/null +++ b/internal/endtoend/testdata/func_nullability/googlesql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "googlesql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/engine/googlesql/convert.go b/internal/engine/googlesql/convert.go index abce8d5bdf..aa694299b6 100644 --- a/internal/engine/googlesql/convert.go +++ b/internal/engine/googlesql/convert.go @@ -28,6 +28,12 @@ func (c *cc) convert(node zjast.Node) ast.Node { return c.convertDeleteStatement(n) case *zjast.CreateTableStatement: return c.convertCreateTableStatement(n) + case *zjast.CreateViewStatement: + return c.convertCreateViewStatement(n) + case *zjast.AlterStatement: + return c.convertAlterStatement(n) + case *zjast.RenameStatement: + return c.convertRenameStatement(n) case *zjast.DropStatement: return c.convertDropStatement(n) case *zjast.TruncateStatement: @@ -473,10 +479,45 @@ func (c *cc) convertExpr(node zjast.Node) ast.Node { Arg: c.convertExpr(n.Array), Indirection: &ast.List{ Items: []ast.Node{ - &ast.A_Indices{Uidx: c.convertExpr(n.Position)}, + &ast.A_Indices{Uidx: c.convertExpr(arrayPosition(n.Position))}, }, }, } + case *zjast.ArrayConstructor: + return c.convertArrayConstructor(n) + case *zjast.ExtractExpression: + return c.convertExtractExpression(n) + case *zjast.DateOrTimeLiteral: + kind := strings.ToLower(strings.TrimPrefix(n.TypeKind, "TYPE_")) + return c.typedLiteral(n.Value, kind, n.Pos()) + case *zjast.NumericLiteral: + return c.typedLiteral(n.Value, "numeric", n.Pos()) + case *zjast.BigNumericLiteral: + return c.typedLiteral(n.Value, "bignumeric", n.Pos()) + case *zjast.JSONLiteral: + return c.typedLiteral(n.Value, "json", n.Pos()) + case *zjast.BytesLiteral: + var sb strings.Builder + for _, comp := range n.Components { + if comp != nil { + sb.WriteString(unquote(comp.Image)) + } + } + return &ast.TypeCast{ + Arg: &ast.A_Const{Val: &ast.String{Str: sb.String()}, Location: n.Pos()}, + TypeName: spelledTypeName("bytes"), + Location: n.Pos(), + } + case *zjast.IntervalExpr: + unit := "" + if n.DatePart != nil { + unit = strings.ToLower(n.DatePart.Name) + } + return &ast.IntervalExpr{ + Value: c.convertExpr(n.Value), + Unit: unit, + Location: n.Pos(), + } case *zjast.NamedArgument: return c.convertExpr(n.Value) case *zjast.ExpressionWithAlias: @@ -613,6 +654,25 @@ func (c *cc) convertBoolExpr(op ast.BoolExprType, operands []zjast.Node, loc int } func (c *cc) convertInExpression(n *zjast.InExpression) ast.Node { + // "x IN UNNEST(array)" tests membership of an array, which is what + // "x = ANY(array)" says. + if n.UnnestExpr != nil && len(n.UnnestExpr.Expressions) == 1 && n.UnnestExpr.Expressions[0] != nil { + var expr ast.Node = &ast.A_Expr{ + Kind: ast.A_Expr_Kind_OP_ANY, + Name: &ast.List{Items: []ast.Node{&ast.String{Str: "="}}}, + Lexpr: c.convertExpr(n.Lhs), + Rexpr: c.convertExpr(n.UnnestExpr.Expressions[0].Expr), + Location: n.Pos(), + } + if n.IsNot { + expr = &ast.BoolExpr{ + Boolop: ast.BoolExprTypeNot, + Args: &ast.List{Items: []ast.Node{expr}}, + Location: n.Pos(), + } + } + return expr + } in := &ast.In{ Expr: c.convertExpr(n.Lhs), Not: n.IsNot, @@ -690,6 +750,89 @@ func (c *cc) newFuncCall(parts []string, loc int) *ast.FuncCall { } } +// convertConditional converts the conditional functions whose result is one +// of their arguments to the nodes that say so, which is what types the +// result after the arguments and makes it nullable only when it can be: +// COALESCE and IFNULL, NULLIF, IF, GREATEST and LEAST. It returns nil for +// anything else. +func (c *cc) convertConditional(name string, args []zjast.Node, loc int) ast.Node { + convert := func() *ast.List { + list := &ast.List{} + for _, arg := range args { + list.Items = append(list.Items, c.convertExpr(arg)) + } + return list + } + switch { + case (name == "coalesce" && len(args) > 0) || (name == "ifnull" && len(args) == 2): + return &ast.CoalesceExpr{Args: convert(), Location: loc} + case name == "nullif" && len(args) == 2: + return &ast.A_Expr{ + Kind: ast.A_Expr_Kind_NULLIF, + Name: &ast.List{Items: []ast.Node{&ast.String{Str: "="}}}, + Lexpr: c.convertExpr(args[0]), + Rexpr: c.convertExpr(args[1]), + Location: loc, + } + case name == "if" && len(args) == 3: + return c.buildCaseExpr(nil, args, loc) + case (name == "greatest" || name == "least") && len(args) > 0: + // The ops number as PostgreSQL's do: IS_GREATEST, then IS_LEAST. + op := ast.MinMaxOp(0) + if name == "least" { + op = 1 + } + return &ast.MinMaxExpr{Op: op, Args: convert(), Location: loc} + } + return nil +} + +// typedLiteral converts a literal spelled with its type, DATE '2024-01-01' +// or JSON '{}', to its string cast to that type. +func (c *cc) typedLiteral(value *zjast.StringLiteral, typ string, loc int) ast.Node { + if value == nil || typ == "" { + return todo(value) + } + return &ast.TypeCast{ + Arg: &ast.A_Const{Val: &ast.String{Str: stringLiteralValue(value)}, Location: value.Pos()}, + TypeName: spelledTypeName(typ), + Location: loc, + } +} + +// convertArrayConstructor converts [a, b] and ARRAY[a, b]. An explicit +// element type is a cast of the array to it, which is what gives an empty +// array its type. +func (c *cc) convertArrayConstructor(n *zjast.ArrayConstructor) ast.Node { + arr := &ast.A_ArrayExpr{Elements: &ast.List{}, Location: n.Pos()} + for _, elem := range n.Elements { + arr.Elements.Items = append(arr.Elements.Items, c.convertExpr(elem)) + } + if n.Type == nil { + return arr + } + return &ast.TypeCast{ + Arg: arr, + TypeName: spelledTypeName(typeName(n.Type)), + Location: n.Pos(), + } +} + +// arrayPosition unwraps the OFFSET, ORDINAL, SAFE_OFFSET or SAFE_ORDINAL +// that says how an array subscript counts to the position it wraps. They all +// address one element, so they type alike. +func arrayPosition(node zjast.Node) zjast.Node { + call, ok := node.(*zjast.FunctionCall) + if !ok || call.IsChained || len(call.Args) != 1 { + return node + } + switch strings.ToUpper(strings.Join(pathParts(call.Function), ".")) { + case "OFFSET", "ORDINAL", "SAFE_OFFSET", "SAFE_ORDINAL": + return call.Args[0] + } + return node +} + func (c *cc) convertFunctionCall(n *zjast.FunctionCall) ast.Node { // Chained calls ("expr.method(...)") have no sqlc AST equivalent. if n.IsChained { @@ -699,13 +842,38 @@ func (c *cc) convertFunctionCall(n *zjast.FunctionCall) ast.Node { if len(parts) == 0 { return todo(n) } + // SAFE.f(...) is f(...), except that it returns NULL where f raises an + // error, so its result is f's made nullable: a CASE with no ELSE. + if len(parts) == 2 && strings.EqualFold(parts[0], "SAFE") { + call := *n + call.Function = &zjast.PathExpression{Names: n.Function.Names[1:]} + return &ast.CaseExpr{ + Args: &ast.List{Items: []ast.Node{&ast.CaseWhen{ + Expr: &ast.A_Const{Val: &ast.Boolean{Boolval: true}, Location: n.Pos()}, + Result: c.convertFunctionCall(&call), + }}}, + Location: n.Pos(), + } + } + if len(parts) == 1 && !n.Distinct && n.OrderBy == nil { + if expr := c.convertConditional(strings.ToLower(parts[0]), n.Args, n.Pos()); expr != nil { + return expr + } + } fc := c.newFuncCall(parts, n.Pos()) fc.AggDistinct = n.Distinct - for _, arg := range n.Args { + partArg, hasPart := datePartArg[fc.Func.Name] + for i, arg := range n.Args { if _, ok := arg.(*zjast.Star); ok { fc.AggStar = true continue } + if hasPart && i == partArg { + if part, ok := datePart(arg); ok { + fc.Args.Items = append(fc.Args.Items, &ast.A_Const{Val: &ast.String{Str: part}, Location: arg.Pos()}) + continue + } + } fc.Args.Items = append(fc.Args.Items, c.convertExpr(arg)) } if n.OrderBy != nil { @@ -714,6 +882,56 @@ func (c *cc) convertFunctionCall(n *zjast.FunctionCall) ast.Node { return fc } +// datePartArg is the position of the argument naming a date part, DAY or +// WEEK(MONDAY), in the functions that take one. The part is a keyword, not +// a column, so it is converted to a string. +var datePartArg = map[string]int{ + "date_diff": 2, + "date_trunc": 1, + "timestamp_diff": 2, + "timestamp_trunc": 1, + "last_day": 1, +} + +// datePart spells a date part argument: DAY, or WEEK(MONDAY). +func datePart(node zjast.Node) (string, bool) { + switch n := node.(type) { + case *zjast.PathExpression: + if parts := pathParts(n); len(parts) == 1 { + return strings.ToUpper(parts[0]), true + } + case *zjast.FunctionCall: + parts := pathParts(n.Function) + if len(parts) == 1 && len(n.Args) == 1 { + if arg, ok := datePart(n.Args[0]); ok { + return strings.ToUpper(parts[0]) + "(" + arg + ")", true + } + } + } + return "", false +} + +// convertExtractExpression converts EXTRACT(part FROM expr). Every part is +// an INT64 but DATE, which is the date of a timestamp, so EXTRACT is the +// extract function the dialect seeds as INT64, and EXTRACT(DATE FROM ts) +// is date(ts). +func (c *cc) convertExtractExpression(n *zjast.ExtractExpression) ast.Node { + part, ok := datePart(n.LhsExpr) + if !ok || n.RhsExpr == nil { + return todo(n) + } + name := "extract" + if part == "DATE" { + name = "date" + } + fc := c.newFuncCall([]string{name}, n.Pos()) + fc.Args.Items = append(fc.Args.Items, c.convertExpr(n.RhsExpr)) + if n.TimeZone != nil { + fc.Args.Items = append(fc.Args.Items, c.convertExpr(n.TimeZone)) + } + return fc +} + func (c *cc) convertAnalyticFunctionCall(n *zjast.AnalyticFunctionCall) ast.Node { base := c.convertExpr(n.Expr) fc, ok := base.(*ast.FuncCall) @@ -932,12 +1150,17 @@ func (c *cc) convertColumnDefinition(n *zjast.ColumnDefinition) *ast.ColumnDef { col.IsNotNull = true case *zjast.PrimaryKeyColumnAttribute: col.PrimaryKey = true + case *zjast.HiddenColumnAttribute: + // Spanner leaves a HIDDEN column out of SELECT *. + col.IsHidden = true } } } if simple, ok := n.Schema.(*zjast.SimpleColumnSchema); ok { - if simple.DefaultExpression != nil { + // A generated column's "AS (expression) STORED" and an identity + // column's IDENTITY(...) arrive here too; neither is a default. + if _, generated := simple.DefaultExpression.(*zjast.GeneratedColumnInfo); !generated && simple.DefaultExpression != nil { col.RawDefault = c.convertExpr(simple.DefaultExpression) } } @@ -945,10 +1168,174 @@ func (c *cc) convertColumnDefinition(n *zjast.ColumnDefinition) *ast.ColumnDef { return col } +// convertCreateViewStatement converts CREATE [OR REPLACE] VIEW. Spanner's +// SQL SECURITY clause says nothing about the view's columns. +func (c *cc) convertCreateViewStatement(n *zjast.CreateViewStatement) ast.Node { + if n.ViewKind != "" || n.Query == nil || n.Name == nil { + return todo(n) + } + stmt := &ast.ViewStmt{ + View: parseRangeVar(n.Name), + Query: c.convertQuery(n.Query), + Replace: n.IsOrReplace, + } + if n.Columns != nil { + stmt.Aliases = &ast.List{} + for _, col := range n.Columns.Columns { + if col == nil || col.Name == nil { + continue + } + stmt.Aliases.Items = append(stmt.Aliases.Items, NewIdentifier(col.Name.Name)) + } + } + return stmt +} + +// convertAlterStatement converts ALTER TABLE. Its column actions become one +// AlterTableStmt; RENAME COLUMN and RENAME TO, which the sqlc AST spells as +// statements of their own, follow it in a list. Actions that change nothing +// about a column's name, type or nullability, such as constraints, options, +// defaults and row deletion policies, are dropped. +func (c *cc) convertAlterStatement(n *zjast.AlterStatement) ast.Node { + if n.NodeName != "AlterTableStatement" || n.Path == nil || n.Actions == nil { + return todo(n) + } + table := parseTableName(n.Path) + alter := &ast.AlterTableStmt{ + Table: table, + Cmds: &ast.List{}, + MissingOk: n.IsIfExists, + } + stmts := []ast.Node{alter} + for _, action := range n.Actions.Actions { + switch a := action.(type) { + case *zjast.AddColumnAction: + if a.Column == nil { + continue + } + def := c.convertColumnDefinition(a.Column) + alter.Cmds.Items = append(alter.Cmds.Items, &ast.AlterTableCmd{ + Name: &def.Colname, + Subtype: ast.AT_AddColumn, + Def: def, + MissingOk: a.IsIfNotExists, + }) + case *zjast.DropColumnAction: + if a.Name == nil { + continue + } + name := identifier(a.Name.Name) + alter.Cmds.Items = append(alter.Cmds.Items, &ast.AlterTableCmd{ + Name: &name, + Subtype: ast.AT_DropColumn, + MissingOk: a.IsIfExists, + }) + case *zjast.SpannerAlterColumnAction: + // Spanner's "ALTER COLUMN c [NOT NULL] ..." restates the + // column's whole definition, nullability included. + if a.Column == nil { + continue + } + def := c.convertColumnDefinition(a.Column) + alter.Cmds.Items = append(alter.Cmds.Items, &ast.AlterTableCmd{ + Name: &def.Colname, + Subtype: ast.AT_AlterColumnType, + Def: def, + }) + if !def.IsNotNull { + alter.Cmds.Items = append(alter.Cmds.Items, &ast.AlterTableCmd{ + Name: &def.Colname, + Subtype: ast.AT_DropNotNull, + }) + } + case *zjast.AlterColumnTypeAction: + if a.Column == nil { + continue + } + name := identifier(a.Column.Name) + alter.Cmds.Items = append(alter.Cmds.Items, &ast.AlterTableCmd{ + Name: &name, + Subtype: ast.AT_AlterColumnType, + MissingOk: a.IsIfExists, + Def: &ast.ColumnDef{ + Colname: name, + TypeName: spelledTypeName(columnSchemaTypeName(a.Schema)), + }, + }) + case *zjast.AlterColumnDropNotNullAction: + if a.Column == nil { + continue + } + name := identifier(a.Column.Name) + alter.Cmds.Items = append(alter.Cmds.Items, &ast.AlterTableCmd{ + Name: &name, + Subtype: ast.AT_DropNotNull, + MissingOk: a.IsIfExists, + }) + case *zjast.RenameColumnAction: + if a.Name == nil || a.NewName == nil { + continue + } + newName := identifier(a.NewName.Name) + stmts = append(stmts, &ast.RenameColumnStmt{ + Table: table, + Col: &ast.ColumnRef{Name: identifier(a.Name.Name)}, + NewName: &newName, + MissingOk: n.IsIfExists, + }) + case *zjast.RenameToClause: + parts := pathParts(a.NewName) + if len(parts) == 0 { + continue + } + newName := identifier(parts[len(parts)-1]) + stmts = append(stmts, &ast.RenameTableStmt{ + Table: table, + NewName: &newName, + MissingOk: n.IsIfExists, + }) + } + } + if len(stmts) == 1 { + return alter + } + return &ast.List{Items: stmts} +} + +// convertRenameStatement converts Spanner's "RENAME TABLE a TO b [, c TO +// d ...]", which renames each table in turn. +func (c *cc) convertRenameStatement(n *zjast.RenameStatement) ast.Node { + if n.Identifier == nil || !strings.EqualFold(n.Identifier.Name, "TABLE") { + return todo(n) + } + list := &ast.List{} + add := func(oldName, newName *zjast.PathExpression) { + parts := pathParts(newName) + if oldName == nil || len(parts) == 0 { + return + } + name := identifier(parts[len(parts)-1]) + list.Items = append(list.Items, &ast.RenameTableStmt{ + Table: parseTableName(oldName), + NewName: &name, + }) + } + add(n.OldName, n.NewName) + for _, more := range n.MoreRenames { + add(more.OldName, more.NewName) + } + if len(list.Items) == 0 { + return todo(n) + } + return list +} + 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 { + // DROP TABLE and DROP VIEW map onto the sqlc AST, which drops both + // kinds of relation alike; other object kinds (indexes, sequences, + // schemas, ...) fall through to TODO. + kind := strings.ToUpper(n.ObjectKind) + if (kind == "TABLE" || kind == "VIEW") && n.Path != nil { return &ast.DropTableStmt{ IfExists: n.IsIfExists, Tables: []*ast.TableName{parseTableName(n.Path)}, diff --git a/internal/engine/googlesql/dialect/dialect.json b/internal/engine/googlesql/dialect/dialect.json index 728546d80b..ef0e61426a 100644 --- a/internal/engine/googlesql/dialect/dialect.json +++ b/internal/engine/googlesql/dialect/dialect.json @@ -11,5 +11,6 @@ "comparison_categories": "BNSDTU", "arithmetic": ["+", "-", "*", "/"], "arithmetic_categories": "N", - "idents": ["max"] + "idents": ["max"], + "propagate_nullable": true } diff --git a/internal/engine/googlesql/dialect/functions.jsonl b/internal/engine/googlesql/dialect/functions.jsonl index 42bf28990e..e01d9a0fba 100644 --- a/internal/engine/googlesql/dialect/functions.jsonl +++ b/internal/engine/googlesql/dialect/functions.jsonl @@ -1 +1,291 @@ -{"name": "count", "kind": "a", "returns": "int64"} +{"name":"abs","args":[{"type":"int64"}],"returns":"int64"} +{"name":"abs","args":[{"type":"float64"}],"returns":"float64"} +{"name":"abs","args":[{"type":"float32"}],"returns":"float32"} +{"name":"abs","args":[{"type":"numeric"}],"returns":"numeric"} +{"name":"acos","args":[{"type":"float64"}],"returns":"float64"} +{"name":"any_value","kind":"a","args":[{"type":"anyelement"}],"returns":"$1","nullable":true} +{"name":"array_agg","kind":"a","args":[{"type":"anyelement"}],"returns":"array($1)","nullable":true} +{"name":"array_concat","args":[{"type":"anyarray"},{"type":"anyarray"}],"returns":"$1"} +{"name":"array_concat_agg","kind":"a","args":[{"type":"anyarray"}],"returns":"$1","nullable":true} +{"name":"array_first","args":[{"type":"anyarray"}],"returns":"anyelement","nullable":true} +{"name":"array_includes","args":[{"type":"anyarray"},{"type":"anyelement"}],"returns":"bool"} +{"name":"array_includes_all","args":[{"type":"anyarray"},{"type":"anyarray"}],"returns":"bool"} +{"name":"array_includes_any","args":[{"type":"anyarray"},{"type":"anyarray"}],"returns":"bool"} +{"name":"array_is_distinct","args":[{"type":"anyarray"}],"returns":"bool"} +{"name":"array_last","args":[{"type":"anyarray"}],"returns":"anyelement","nullable":true} +{"name":"array_length","args":[{"type":"anyarray"}],"returns":"int64"} +{"name":"array_max","args":[{"type":"anyarray"}],"returns":"anyelement","nullable":true} +{"name":"array_min","args":[{"type":"anyarray"}],"returns":"anyelement","nullable":true} +{"name":"array_reverse","args":[{"type":"anyarray"}],"returns":"$1"} +{"name":"array_slice","args":[{"type":"anyarray"},{"type":"int64"},{"type":"int64"}],"returns":"$1"} +{"name":"array_to_string","args":[{"type":"array(string)"},{"type":"string"}],"returns":"string"} +{"name":"array_to_string","args":[{"type":"array(bytes)"},{"type":"bytes"}],"returns":"bytes"} +{"name":"array_to_string","args":[{"type":"array(string)"},{"type":"string"},{"type":"string"}],"returns":"string"} +{"name":"asin","args":[{"type":"float64"}],"returns":"float64"} +{"name":"atan","args":[{"type":"float64"}],"returns":"float64"} +{"name":"atan2","args":[{"type":"float64"},{"type":"float64"}],"returns":"float64"} +{"name":"avg","kind":"a","args":[{"type":"int64"}],"returns":"float64","nullable":true} +{"name":"avg","kind":"a","args":[{"type":"float64"}],"returns":"float64","nullable":true} +{"name":"avg","kind":"a","args":[{"type":"float32"}],"returns":"float64","nullable":true} +{"name":"avg","kind":"a","args":[{"type":"numeric"}],"returns":"numeric","nullable":true} +{"name":"avg","kind":"a","args":[{"type":"interval"}],"returns":"interval","nullable":true} +{"name":"bit_and","kind":"a","args":[{"type":"int64"}],"returns":"int64","nullable":true} +{"name":"bit_count","args":[{"type":"int64"}],"returns":"int64"} +{"name":"bit_count","args":[{"type":"bytes"}],"returns":"int64"} +{"name":"bit_or","kind":"a","args":[{"type":"int64"}],"returns":"int64","nullable":true} +{"name":"bit_reverse","args":[{"type":"int64"},{"type":"bool"}],"returns":"int64"} +{"name":"bit_xor","kind":"a","args":[{"type":"int64"}],"returns":"int64","nullable":true} +{"name":"bool","args":[{"type":"json"}],"returns":"bool"} +{"name":"byte_length","args":[{"type":"string"}],"returns":"int64"} +{"name":"byte_length","args":[{"type":"bytes"}],"returns":"int64"} +{"name":"ceil","args":[{"type":"float64"}],"returns":"float64"} +{"name":"ceil","args":[{"type":"numeric"}],"returns":"numeric"} +{"name":"ceiling","args":[{"type":"float64"}],"returns":"float64"} +{"name":"ceiling","args":[{"type":"numeric"}],"returns":"numeric"} +{"name":"char_length","args":[{"type":"string"}],"returns":"int64"} +{"name":"character_length","args":[{"type":"string"}],"returns":"int64"} +{"name":"code_points_to_bytes","args":[{"type":"array(int64)"}],"returns":"bytes"} +{"name":"code_points_to_string","args":[{"type":"array(int64)"}],"returns":"string"} +{"name":"concat","args":[{"type":"string"},{"type":"string"}],"returns":"string"} +{"name":"concat","args":[{"type":"bytes"},{"type":"bytes"}],"returns":"bytes"} +{"name":"cos","args":[{"type":"float64"}],"returns":"float64"} +{"name":"cosine_distance","args":[{"type":"array(float64)"},{"type":"array(float64)"}],"returns":"float64"} +{"name":"cosine_distance","args":[{"type":"array(float32)"},{"type":"array(float32)"}],"returns":"float64"} +{"name":"count","kind":"a","args":[{"type":"anyelement"}],"returns":"int64","never_null":true} +{"name":"countif","kind":"a","args":[{"type":"bool"}],"returns":"int64","never_null":true} +{"name":"current_date","returns":"date"} +{"name":"current_timestamp","returns":"timestamp"} +{"name":"date","args":[{"type":"timestamp"}],"returns":"date"} +{"name":"date","args":[{"type":"timestamp"},{"type":"string"}],"returns":"date"} +{"name":"date","args":[{"type":"int64"},{"type":"int64"},{"type":"int64"}],"returns":"date"} +{"name":"date_add","args":[{"type":"date"},{"type":"interval"}],"returns":"date"} +{"name":"date_diff","args":[{"type":"date"},{"type":"date"},{"type":"string"}],"returns":"int64"} +{"name":"date_from_unix_date","args":[{"type":"int64"}],"returns":"date"} +{"name":"date_sub","args":[{"type":"date"},{"type":"interval"}],"returns":"date"} +{"name":"date_trunc","args":[{"type":"date"},{"type":"string"}],"returns":"date"} +{"name":"div","args":[{"type":"int64"},{"type":"int64"}],"returns":"int64"} +{"name":"div","args":[{"type":"numeric"},{"type":"numeric"}],"returns":"numeric"} +{"name":"dot_product","args":[{"type":"array(float64)"},{"type":"array(float64)"}],"returns":"float64"} +{"name":"dot_product","args":[{"type":"array(float32)"},{"type":"array(float32)"}],"returns":"float64"} +{"name":"dot_product","args":[{"type":"array(int64)"},{"type":"array(int64)"}],"returns":"float64"} +{"name":"ends_with","args":[{"type":"string"},{"type":"string"}],"returns":"bool"} +{"name":"ends_with","args":[{"type":"bytes"},{"type":"bytes"}],"returns":"bool"} +{"name":"euclidean_distance","args":[{"type":"array(float64)"},{"type":"array(float64)"}],"returns":"float64"} +{"name":"euclidean_distance","args":[{"type":"array(float32)"},{"type":"array(float32)"}],"returns":"float64"} +{"name":"exp","args":[{"type":"float64"}],"returns":"float64"} +{"name":"exp","args":[{"type":"numeric"}],"returns":"numeric"} +{"name":"extract","args":[{"type":"timestamp"}],"returns":"int64"} +{"name":"extract","args":[{"type":"date"}],"returns":"int64"} +{"name":"extract","args":[{"type":"timestamp"},{"type":"string"}],"returns":"int64"} +{"name":"farm_fingerprint","args":[{"type":"string"}],"returns":"int64"} +{"name":"farm_fingerprint","args":[{"type":"bytes"}],"returns":"int64"} +{"name":"float32","args":[{"type":"json"}],"returns":"float32"} +{"name":"float64","args":[{"type":"json"}],"returns":"float64"} +{"name":"floor","args":[{"type":"float64"}],"returns":"float64"} +{"name":"floor","args":[{"type":"numeric"}],"returns":"numeric"} +{"name":"format","args":[{"type":"string"}],"returns":"string"} +{"name":"format_date","args":[{"type":"string"},{"type":"date"}],"returns":"string"} +{"name":"format_timestamp","args":[{"type":"string"},{"type":"timestamp"}],"returns":"string"} +{"name":"format_timestamp","args":[{"type":"string"},{"type":"timestamp"},{"type":"string"}],"returns":"string"} +{"name":"from_base32","args":[{"type":"string"}],"returns":"bytes"} +{"name":"from_base64","args":[{"type":"string"}],"returns":"bytes"} +{"name":"from_hex","args":[{"type":"string"}],"returns":"bytes"} +{"name":"generate_array","args":[{"type":"int64"},{"type":"int64"}],"returns":"array(int64)"} +{"name":"generate_array","args":[{"type":"int64"},{"type":"int64"},{"type":"int64"}],"returns":"array(int64)"} +{"name":"generate_array","args":[{"type":"float64"},{"type":"float64"},{"type":"float64"}],"returns":"array(float64)"} +{"name":"generate_date_array","args":[{"type":"date"},{"type":"date"}],"returns":"array(date)"} +{"name":"generate_date_array","args":[{"type":"date"},{"type":"date"},{"type":"interval"}],"returns":"array(date)"} +{"name":"generate_uuid","returns":"string","never_null":true} +{"name":"get_next_sequence_value","args":[{"type":"anyelement"}],"returns":"int64","never_null":true} +{"name":"ieee_divide","args":[{"type":"float64"},{"type":"float64"}],"returns":"float64"} +{"name":"int64","args":[{"type":"json"}],"returns":"int64"} +{"name":"is_inf","args":[{"type":"float64"}],"returns":"bool"} +{"name":"is_nan","args":[{"type":"float64"}],"returns":"bool"} +{"name":"json_array","args":[{"type":"anyelement"}],"returns":"json"} +{"name":"json_array_append","args":[{"type":"json"},{"type":"string"},{"type":"anyelement"}],"returns":"json"} +{"name":"json_array_insert","args":[{"type":"json"},{"type":"string"},{"type":"anyelement"}],"returns":"json"} +{"name":"json_contains","args":[{"type":"json"},{"type":"json"}],"returns":"bool"} +{"name":"json_keys","args":[{"type":"json"}],"returns":"array(string)"} +{"name":"json_object","args":[{"type":"string"},{"type":"anyelement"}],"returns":"json"} +{"name":"json_query","args":[{"type":"json"},{"type":"string"}],"returns":"json","nullable":true} +{"name":"json_query","args":[{"type":"string"},{"type":"string"}],"returns":"string","nullable":true} +{"name":"json_query_array","args":[{"type":"json"}],"returns":"array(json)","nullable":true} +{"name":"json_query_array","args":[{"type":"json"},{"type":"string"}],"returns":"array(json)","nullable":true} +{"name":"json_remove","args":[{"type":"json"},{"type":"string"}],"returns":"json"} +{"name":"json_set","args":[{"type":"json"},{"type":"string"},{"type":"anyelement"}],"returns":"json"} +{"name":"json_strip_nulls","args":[{"type":"json"}],"returns":"json"} +{"name":"json_type","args":[{"type":"json"}],"returns":"string"} +{"name":"json_value","args":[{"type":"json"},{"type":"string"}],"returns":"string","nullable":true} +{"name":"json_value","args":[{"type":"string"},{"type":"string"}],"returns":"string","nullable":true} +{"name":"json_value_array","args":[{"type":"json"}],"returns":"array(string)","nullable":true} +{"name":"json_value_array","args":[{"type":"json"},{"type":"string"}],"returns":"array(string)","nullable":true} +{"name":"justify_days","args":[{"type":"interval"}],"returns":"interval"} +{"name":"justify_hours","args":[{"type":"interval"}],"returns":"interval"} +{"name":"justify_interval","args":[{"type":"interval"}],"returns":"interval"} +{"name":"lax_bool","args":[{"type":"json"}],"returns":"bool","nullable":true} +{"name":"lax_float64","args":[{"type":"json"}],"returns":"float64","nullable":true} +{"name":"lax_int64","args":[{"type":"json"}],"returns":"int64","nullable":true} +{"name":"lax_string","args":[{"type":"json"}],"returns":"string","nullable":true} +{"name":"length","args":[{"type":"string"}],"returns":"int64"} +{"name":"length","args":[{"type":"bytes"}],"returns":"int64"} +{"name":"ln","args":[{"type":"float64"}],"returns":"float64"} +{"name":"ln","args":[{"type":"numeric"}],"returns":"numeric"} +{"name":"log","args":[{"type":"float64"}],"returns":"float64"} +{"name":"log","args":[{"type":"numeric"}],"returns":"numeric"} +{"name":"log","args":[{"type":"float64"},{"type":"float64"}],"returns":"float64"} +{"name":"log10","args":[{"type":"float64"}],"returns":"float64"} +{"name":"log10","args":[{"type":"numeric"}],"returns":"numeric"} +{"name":"logical_and","kind":"a","args":[{"type":"bool"}],"returns":"bool","nullable":true} +{"name":"logical_or","kind":"a","args":[{"type":"bool"}],"returns":"bool","nullable":true} +{"name":"lower","args":[{"type":"string"}],"returns":"string"} +{"name":"lower","args":[{"type":"bytes"}],"returns":"bytes"} +{"name":"lpad","args":[{"type":"string"},{"type":"int64"}],"returns":"string"} +{"name":"lpad","args":[{"type":"string"},{"type":"int64"},{"type":"string"}],"returns":"string"} +{"name":"lpad","args":[{"type":"bytes"},{"type":"int64"},{"type":"bytes"}],"returns":"bytes"} +{"name":"ltrim","args":[{"type":"string"}],"returns":"string"} +{"name":"ltrim","args":[{"type":"string"},{"type":"string"}],"returns":"string"} +{"name":"ltrim","args":[{"type":"bytes"},{"type":"bytes"}],"returns":"bytes"} +{"name":"make_interval","args":[{"type":"int64"},{"type":"int64"},{"type":"int64"}],"returns":"interval"} +{"name":"max","kind":"a","args":[{"type":"anyelement"}],"returns":"$1","nullable":true} +{"name":"md5","args":[{"type":"string"}],"returns":"bytes"} +{"name":"md5","args":[{"type":"bytes"}],"returns":"bytes"} +{"name":"min","kind":"a","args":[{"type":"anyelement"}],"returns":"$1","nullable":true} +{"name":"mod","args":[{"type":"int64"},{"type":"int64"}],"returns":"int64"} +{"name":"mod","args":[{"type":"numeric"},{"type":"numeric"}],"returns":"numeric"} +{"name":"new_uuid","returns":"uuid","never_null":true} +{"name":"normalize","args":[{"type":"string"}],"returns":"string"} +{"name":"normalize_and_casefold","args":[{"type":"string"}],"returns":"string"} +{"name":"parse_date","args":[{"type":"string"},{"type":"string"}],"returns":"date"} +{"name":"parse_json","args":[{"type":"string"}],"returns":"json"} +{"name":"parse_timestamp","args":[{"type":"string"},{"type":"string"}],"returns":"timestamp"} +{"name":"parse_timestamp","args":[{"type":"string"},{"type":"string"},{"type":"string"}],"returns":"timestamp"} +{"name":"pending_commit_timestamp","returns":"timestamp","never_null":true} +{"name":"pow","args":[{"type":"float64"},{"type":"float64"}],"returns":"float64"} +{"name":"pow","args":[{"type":"numeric"},{"type":"numeric"}],"returns":"numeric"} +{"name":"power","args":[{"type":"float64"},{"type":"float64"}],"returns":"float64"} +{"name":"power","args":[{"type":"numeric"},{"type":"numeric"}],"returns":"numeric"} +{"name":"regexp_contains","args":[{"type":"string"},{"type":"string"}],"returns":"bool"} +{"name":"regexp_contains","args":[{"type":"bytes"},{"type":"bytes"}],"returns":"bool"} +{"name":"regexp_extract","args":[{"type":"string"},{"type":"string"}],"returns":"string","nullable":true} +{"name":"regexp_extract","args":[{"type":"bytes"},{"type":"bytes"}],"returns":"bytes","nullable":true} +{"name":"regexp_extract_all","args":[{"type":"string"},{"type":"string"}],"returns":"array(string)"} +{"name":"regexp_extract_all","args":[{"type":"bytes"},{"type":"bytes"}],"returns":"array(bytes)"} +{"name":"regexp_replace","args":[{"type":"string"},{"type":"string"},{"type":"string"}],"returns":"string"} +{"name":"regexp_replace","args":[{"type":"bytes"},{"type":"bytes"},{"type":"bytes"}],"returns":"bytes"} +{"name":"repeat","args":[{"type":"string"},{"type":"int64"}],"returns":"string"} +{"name":"repeat","args":[{"type":"bytes"},{"type":"int64"}],"returns":"bytes"} +{"name":"replace","args":[{"type":"string"},{"type":"string"},{"type":"string"}],"returns":"string"} +{"name":"replace","args":[{"type":"bytes"},{"type":"bytes"},{"type":"bytes"}],"returns":"bytes"} +{"name":"reverse","args":[{"type":"string"}],"returns":"string"} +{"name":"reverse","args":[{"type":"bytes"}],"returns":"bytes"} +{"name":"round","args":[{"type":"float64"}],"returns":"float64"} +{"name":"round","args":[{"type":"numeric"}],"returns":"numeric"} +{"name":"round","args":[{"type":"float64"},{"type":"int64"}],"returns":"float64"} +{"name":"round","args":[{"type":"numeric"},{"type":"int64"}],"returns":"numeric"} +{"name":"rpad","args":[{"type":"string"},{"type":"int64"}],"returns":"string"} +{"name":"rpad","args":[{"type":"string"},{"type":"int64"},{"type":"string"}],"returns":"string"} +{"name":"rpad","args":[{"type":"bytes"},{"type":"int64"},{"type":"bytes"}],"returns":"bytes"} +{"name":"rtrim","args":[{"type":"string"}],"returns":"string"} +{"name":"rtrim","args":[{"type":"string"},{"type":"string"}],"returns":"string"} +{"name":"rtrim","args":[{"type":"bytes"},{"type":"bytes"}],"returns":"bytes"} +{"name":"safe_add","args":[{"type":"int64"},{"type":"int64"}],"returns":"int64","nullable":true} +{"name":"safe_add","args":[{"type":"float64"},{"type":"float64"}],"returns":"float64","nullable":true} +{"name":"safe_add","args":[{"type":"numeric"},{"type":"numeric"}],"returns":"numeric","nullable":true} +{"name":"safe_convert_bytes_to_string","args":[{"type":"bytes"}],"returns":"string"} +{"name":"safe_divide","args":[{"type":"int64"},{"type":"int64"}],"returns":"float64","nullable":true} +{"name":"safe_divide","args":[{"type":"float64"},{"type":"float64"}],"returns":"float64","nullable":true} +{"name":"safe_divide","args":[{"type":"numeric"},{"type":"numeric"}],"returns":"numeric","nullable":true} +{"name":"safe_multiply","args":[{"type":"int64"},{"type":"int64"}],"returns":"int64","nullable":true} +{"name":"safe_multiply","args":[{"type":"float64"},{"type":"float64"}],"returns":"float64","nullable":true} +{"name":"safe_multiply","args":[{"type":"numeric"},{"type":"numeric"}],"returns":"numeric","nullable":true} +{"name":"safe_negate","args":[{"type":"int64"}],"returns":"int64","nullable":true} +{"name":"safe_negate","args":[{"type":"float64"}],"returns":"float64","nullable":true} +{"name":"safe_negate","args":[{"type":"numeric"}],"returns":"numeric","nullable":true} +{"name":"safe_subtract","args":[{"type":"int64"},{"type":"int64"}],"returns":"int64","nullable":true} +{"name":"safe_subtract","args":[{"type":"float64"},{"type":"float64"}],"returns":"float64","nullable":true} +{"name":"safe_subtract","args":[{"type":"numeric"},{"type":"numeric"}],"returns":"numeric","nullable":true} +{"name":"safe_to_json","args":[{"type":"anyelement"}],"returns":"json","never_null":true} +{"name":"score","args":[{"type":"tokenlist"},{"type":"string"}],"returns":"float64"} +{"name":"score_ngrams","args":[{"type":"tokenlist"},{"type":"string"}],"returns":"float64"} +{"name":"search","args":[{"type":"tokenlist"},{"type":"string"}],"returns":"bool"} +{"name":"search_ngrams","args":[{"type":"tokenlist"},{"type":"string"}],"returns":"bool"} +{"name":"search_substring","args":[{"type":"tokenlist"},{"type":"string"}],"returns":"bool"} +{"name":"sha1","args":[{"type":"string"}],"returns":"bytes"} +{"name":"sha1","args":[{"type":"bytes"}],"returns":"bytes"} +{"name":"sha256","args":[{"type":"string"}],"returns":"bytes"} +{"name":"sha256","args":[{"type":"bytes"}],"returns":"bytes"} +{"name":"sha512","args":[{"type":"string"}],"returns":"bytes"} +{"name":"sha512","args":[{"type":"bytes"}],"returns":"bytes"} +{"name":"sign","args":[{"type":"int64"}],"returns":"int64"} +{"name":"sign","args":[{"type":"float64"}],"returns":"float64"} +{"name":"sign","args":[{"type":"numeric"}],"returns":"numeric"} +{"name":"sin","args":[{"type":"float64"}],"returns":"float64"} +{"name":"snippet","args":[{"type":"string"},{"type":"string"}],"returns":"json"} +{"name":"soundex","args":[{"type":"string"}],"returns":"string"} +{"name":"split","args":[{"type":"string"}],"returns":"array(string)"} +{"name":"split","args":[{"type":"string"},{"type":"string"}],"returns":"array(string)"} +{"name":"split","args":[{"type":"bytes"},{"type":"bytes"}],"returns":"array(bytes)"} +{"name":"split_substr","args":[{"type":"string"},{"type":"string"},{"type":"int64"}],"returns":"string"} +{"name":"split_substr","args":[{"type":"string"},{"type":"string"},{"type":"int64"},{"type":"int64"}],"returns":"string"} +{"name":"sqrt","args":[{"type":"float64"}],"returns":"float64"} +{"name":"sqrt","args":[{"type":"numeric"}],"returns":"numeric"} +{"name":"starts_with","args":[{"type":"string"},{"type":"string"}],"returns":"bool"} +{"name":"starts_with","args":[{"type":"bytes"},{"type":"bytes"}],"returns":"bool"} +{"name":"stddev","kind":"a","args":[{"type":"float64"}],"returns":"float64","nullable":true} +{"name":"stddev_samp","kind":"a","args":[{"type":"float64"}],"returns":"float64","nullable":true} +{"name":"string","args":[{"type":"timestamp"}],"returns":"string"} +{"name":"string","args":[{"type":"json"}],"returns":"string"} +{"name":"string","args":[{"type":"timestamp"},{"type":"string"}],"returns":"string"} +{"name":"string_agg","kind":"a","args":[{"type":"string"}],"returns":"string","nullable":true} +{"name":"string_agg","kind":"a","args":[{"type":"bytes"}],"returns":"bytes","nullable":true} +{"name":"string_agg","kind":"a","args":[{"type":"string"},{"type":"string"}],"returns":"string","nullable":true} +{"name":"string_agg","kind":"a","args":[{"type":"bytes"},{"type":"bytes"}],"returns":"bytes","nullable":true} +{"name":"strpos","args":[{"type":"string"},{"type":"string"}],"returns":"int64"} +{"name":"strpos","args":[{"type":"bytes"},{"type":"bytes"}],"returns":"int64"} +{"name":"substr","args":[{"type":"string"},{"type":"int64"}],"returns":"string"} +{"name":"substr","args":[{"type":"bytes"},{"type":"int64"}],"returns":"bytes"} +{"name":"substr","args":[{"type":"string"},{"type":"int64"},{"type":"int64"}],"returns":"string"} +{"name":"substr","args":[{"type":"bytes"},{"type":"int64"},{"type":"int64"}],"returns":"bytes"} +{"name":"substring","args":[{"type":"string"},{"type":"int64"}],"returns":"string"} +{"name":"substring","args":[{"type":"string"},{"type":"int64"},{"type":"int64"}],"returns":"string"} +{"name":"sum","kind":"a","args":[{"type":"int64"}],"returns":"int64","nullable":true} +{"name":"sum","kind":"a","args":[{"type":"float64"}],"returns":"float64","nullable":true} +{"name":"sum","kind":"a","args":[{"type":"float32"}],"returns":"float64","nullable":true} +{"name":"sum","kind":"a","args":[{"type":"numeric"}],"returns":"numeric","nullable":true} +{"name":"sum","kind":"a","args":[{"type":"interval"}],"returns":"interval","nullable":true} +{"name":"tan","args":[{"type":"float64"}],"returns":"float64"} +{"name":"timestamp","args":[{"type":"string"}],"returns":"timestamp"} +{"name":"timestamp","args":[{"type":"date"}],"returns":"timestamp"} +{"name":"timestamp","args":[{"type":"string"},{"type":"string"}],"returns":"timestamp"} +{"name":"timestamp","args":[{"type":"date"},{"type":"string"}],"returns":"timestamp"} +{"name":"timestamp_add","args":[{"type":"timestamp"},{"type":"interval"}],"returns":"timestamp"} +{"name":"timestamp_diff","args":[{"type":"timestamp"},{"type":"timestamp"},{"type":"string"}],"returns":"int64"} +{"name":"timestamp_micros","args":[{"type":"int64"}],"returns":"timestamp"} +{"name":"timestamp_millis","args":[{"type":"int64"}],"returns":"timestamp"} +{"name":"timestamp_seconds","args":[{"type":"int64"}],"returns":"timestamp"} +{"name":"timestamp_sub","args":[{"type":"timestamp"},{"type":"interval"}],"returns":"timestamp"} +{"name":"timestamp_trunc","args":[{"type":"timestamp"},{"type":"string"}],"returns":"timestamp"} +{"name":"timestamp_trunc","args":[{"type":"timestamp"},{"type":"string"},{"type":"string"}],"returns":"timestamp"} +{"name":"to_base32","args":[{"type":"bytes"}],"returns":"string"} +{"name":"to_base64","args":[{"type":"bytes"}],"returns":"string"} +{"name":"to_code_points","args":[{"type":"string"}],"returns":"array(int64)"} +{"name":"to_code_points","args":[{"type":"bytes"}],"returns":"array(int64)"} +{"name":"to_hex","args":[{"type":"bytes"}],"returns":"string"} +{"name":"to_json","args":[{"type":"anyelement"}],"returns":"json","never_null":true} +{"name":"to_json_string","args":[{"type":"json"}],"returns":"string","never_null":true} +{"name":"trim","args":[{"type":"string"}],"returns":"string"} +{"name":"trim","args":[{"type":"string"},{"type":"string"}],"returns":"string"} +{"name":"trim","args":[{"type":"bytes"},{"type":"bytes"}],"returns":"bytes"} +{"name":"trunc","args":[{"type":"float64"}],"returns":"float64"} +{"name":"trunc","args":[{"type":"numeric"}],"returns":"numeric"} +{"name":"trunc","args":[{"type":"float64"},{"type":"int64"}],"returns":"float64"} +{"name":"trunc","args":[{"type":"numeric"},{"type":"int64"}],"returns":"numeric"} +{"name":"unix_date","args":[{"type":"date"}],"returns":"int64"} +{"name":"unix_micros","args":[{"type":"timestamp"}],"returns":"int64"} +{"name":"unix_millis","args":[{"type":"timestamp"}],"returns":"int64"} +{"name":"unix_seconds","args":[{"type":"timestamp"}],"returns":"int64"} +{"name":"unnest","args":[{"type":"anyarray"}],"returns":"anyelement","nullable":true} +{"name":"upper","args":[{"type":"string"}],"returns":"string"} +{"name":"upper","args":[{"type":"bytes"}],"returns":"bytes"} +{"name":"var_samp","kind":"a","args":[{"type":"float64"}],"returns":"float64","nullable":true} +{"name":"variance","kind":"a","args":[{"type":"float64"}],"returns":"float64","nullable":true} diff --git a/internal/engine/googlesql/dialect/operators.jsonl b/internal/engine/googlesql/dialect/operators.jsonl index 5967f9f055..7a414883bc 100644 --- a/internal/engine/googlesql/dialect/operators.jsonl +++ b/internal/engine/googlesql/dialect/operators.jsonl @@ -1 +1,83 @@ -{"name": "||", "left": "string", "right": "string", "result": "string"} +{"name":"+","left":"int64","right":"int64","result":"int64"} +{"name":"+","left":"int64","right":"float64","result":"float64"} +{"name":"+","left":"int64","right":"float32","result":"float64"} +{"name":"+","left":"int64","right":"numeric","result":"numeric"} +{"name":"+","left":"float64","right":"int64","result":"float64"} +{"name":"+","left":"float64","right":"float64","result":"float64"} +{"name":"+","left":"float64","right":"float32","result":"float64"} +{"name":"+","left":"float64","right":"numeric","result":"float64"} +{"name":"+","left":"float32","right":"int64","result":"float64"} +{"name":"+","left":"float32","right":"float64","result":"float64"} +{"name":"+","left":"float32","right":"float32","result":"float64"} +{"name":"+","left":"float32","right":"numeric","result":"float64"} +{"name":"+","left":"numeric","right":"int64","result":"numeric"} +{"name":"+","left":"numeric","right":"float64","result":"float64"} +{"name":"+","left":"numeric","right":"float32","result":"float64"} +{"name":"+","left":"numeric","right":"numeric","result":"numeric"} +{"name":"-","left":"int64","right":"int64","result":"int64"} +{"name":"-","left":"int64","right":"float64","result":"float64"} +{"name":"-","left":"int64","right":"float32","result":"float64"} +{"name":"-","left":"int64","right":"numeric","result":"numeric"} +{"name":"-","left":"float64","right":"int64","result":"float64"} +{"name":"-","left":"float64","right":"float64","result":"float64"} +{"name":"-","left":"float64","right":"float32","result":"float64"} +{"name":"-","left":"float64","right":"numeric","result":"float64"} +{"name":"-","left":"float32","right":"int64","result":"float64"} +{"name":"-","left":"float32","right":"float64","result":"float64"} +{"name":"-","left":"float32","right":"float32","result":"float64"} +{"name":"-","left":"float32","right":"numeric","result":"float64"} +{"name":"-","left":"numeric","right":"int64","result":"numeric"} +{"name":"-","left":"numeric","right":"float64","result":"float64"} +{"name":"-","left":"numeric","right":"float32","result":"float64"} +{"name":"-","left":"numeric","right":"numeric","result":"numeric"} +{"name":"*","left":"int64","right":"int64","result":"int64"} +{"name":"*","left":"int64","right":"float64","result":"float64"} +{"name":"*","left":"int64","right":"float32","result":"float64"} +{"name":"*","left":"int64","right":"numeric","result":"numeric"} +{"name":"*","left":"float64","right":"int64","result":"float64"} +{"name":"*","left":"float64","right":"float64","result":"float64"} +{"name":"*","left":"float64","right":"float32","result":"float64"} +{"name":"*","left":"float64","right":"numeric","result":"float64"} +{"name":"*","left":"float32","right":"int64","result":"float64"} +{"name":"*","left":"float32","right":"float64","result":"float64"} +{"name":"*","left":"float32","right":"float32","result":"float64"} +{"name":"*","left":"float32","right":"numeric","result":"float64"} +{"name":"*","left":"numeric","right":"int64","result":"numeric"} +{"name":"*","left":"numeric","right":"float64","result":"float64"} +{"name":"*","left":"numeric","right":"float32","result":"float64"} +{"name":"*","left":"numeric","right":"numeric","result":"numeric"} +{"name":"/","left":"int64","right":"int64","result":"float64"} +{"name":"/","left":"int64","right":"float64","result":"float64"} +{"name":"/","left":"int64","right":"float32","result":"float64"} +{"name":"/","left":"int64","right":"numeric","result":"numeric"} +{"name":"/","left":"float64","right":"int64","result":"float64"} +{"name":"/","left":"float64","right":"float64","result":"float64"} +{"name":"/","left":"float64","right":"float32","result":"float64"} +{"name":"/","left":"float64","right":"numeric","result":"float64"} +{"name":"/","left":"float32","right":"int64","result":"float64"} +{"name":"/","left":"float32","right":"float64","result":"float64"} +{"name":"/","left":"float32","right":"float32","result":"float64"} +{"name":"/","left":"float32","right":"numeric","result":"float64"} +{"name":"/","left":"numeric","right":"int64","result":"numeric"} +{"name":"/","left":"numeric","right":"float64","result":"float64"} +{"name":"/","left":"numeric","right":"float32","result":"float64"} +{"name":"/","left":"numeric","right":"numeric","result":"numeric"} +{"name":"||","left":"string","right":"string","result":"string"} +{"name":"||","left":"bytes","right":"bytes","result":"bytes"} +{"name":"+","left":"timestamp","right":"interval","result":"timestamp"} +{"name":"+","left":"interval","right":"interval","result":"interval"} +{"name":"-","left":"timestamp","right":"interval","result":"timestamp"} +{"name":"-","left":"interval","right":"interval","result":"interval"} +{"name":"+","left":"interval","right":"timestamp","result":"timestamp"} +{"name":"-","left":"date","right":"date","result":"interval"} +{"name":"-","left":"timestamp","right":"timestamp","result":"interval"} +{"name":"*","left":"interval","right":"int64","result":"interval"} +{"name":"/","left":"interval","right":"int64","result":"interval"} +{"name":"\u0026","left":"int64","right":"int64","result":"int64"} +{"name":"|","left":"int64","right":"int64","result":"int64"} +{"name":"^","left":"int64","right":"int64","result":"int64"} +{"name":"\u003c\u003c","left":"int64","right":"int64","result":"int64"} +{"name":"\u003e\u003e","left":"int64","right":"int64","result":"int64"} +{"name":"\u0026","left":"bytes","right":"bytes","result":"bytes"} +{"name":"|","left":"bytes","right":"bytes","result":"bytes"} +{"name":"^","left":"bytes","right":"bytes","result":"bytes"} diff --git a/internal/engine/googlesql/dialect/types.jsonl b/internal/engine/googlesql/dialect/types.jsonl index 8c6dac4005..10f671d9a7 100644 --- a/internal/engine/googlesql/dialect/types.jsonl +++ b/internal/engine/googlesql/dialect/types.jsonl @@ -1,23 +1,23 @@ -{"name": "bool", "category": "B"} -{"name": "int64", "category": "N"} -{"name": "float32", "category": "N"} -{"name": "float64", "category": "N"} -{"name": "numeric", "category": "N"} -{"name": "bignumeric", "category": "N"} -{"name": "string", "category": "S"} -{"name": "bytes", "category": "S"} -{"name": "uuid", "category": "S"} -{"name": "date", "category": "D"} -{"name": "datetime", "category": "D"} -{"name": "time", "category": "D"} -{"name": "timestamp", "category": "D"} -{"name": "interval", "category": "T"} -{"name": "json", "category": "U"} -{"name": "geography", "category": "U"} -{"name": "struct", "category": "U"} -{"name": "enum", "category": "U"} -{"name": "proto", "category": "U"} -{"name": "tokenlist", "category": "U"} -{"name": "array", "category": "A"} -{"name": "range", "category": "U"} -{"name": "map", "category": "U"} +{"name":"bool","category":"B"} +{"name":"int64","category":"N"} +{"name":"float32","category":"N"} +{"name":"float64","category":"N"} +{"name":"numeric","category":"N"} +{"name":"bignumeric","category":"N"} +{"name":"string","category":"S"} +{"name":"bytes","category":"S"} +{"name":"uuid","category":"S"} +{"name":"date","category":"D"} +{"name":"datetime","category":"D"} +{"name":"time","category":"D"} +{"name":"timestamp","category":"D"} +{"name":"interval","category":"T"} +{"name":"json","category":"U"} +{"name":"geography","category":"U"} +{"name":"struct","category":"U"} +{"name":"enum","category":"U"} +{"name":"proto","category":"U"} +{"name":"tokenlist","category":"U"} +{"name":"array","category":"A"} +{"name":"range","category":"U"} +{"name":"map","category":"U"} diff --git a/internal/goldeneye/README.md b/internal/goldeneye/README.md index e4e8374fff..ed2c67d040 100644 --- a/internal/goldeneye/README.md +++ b/internal/goldeneye/README.md @@ -110,9 +110,15 @@ the hand-written files alone, and the checks do not look at them. the gRPC endpoint such as `localhost:15000`, reached without TLS or credentials as Omni is, and writes into `internal/engine/googlesql/dialect`, since sqlc's engine is named after the language Spanner speaks. Spanner - keeps no catalog of its types, functions or operators, so `types.jsonl`, - `functions.jsonl` and `operators.jsonl` are hand-written. What it does - describe is its information schema: `relations.jsonl` is every view of + keeps no catalog of its types, functions or operators, so they are read + by asking: `spanner/library.jsonl` lists the function signatures to try, + and each is compiled in PLAN mode against a probe database, the ones + Spanner accepts written to `functions.jsonl` with the type it reports; + `operators.jsonl` is every binary operator Spanner accepts over the + operands tried, and `types.jsonl` the types the dialect lists, each one + Spanner has checked by a cast. Whether a result can be NULL is what + Spanner does not say, so `library.jsonl` says it. Spanner also + describes its information schema: `relations.jsonl` is every view of `INFORMATION_SCHEMA` and `SPANNER_SYS`, read from `INFORMATION_SCHEMA` itself in a database created for the purpose in the instance Omni's single server provides, `projects/default/instances/default`. Names are diff --git a/internal/goldeneye/dialect/dialect.go b/internal/goldeneye/dialect/dialect.go index 6b053a120a..59a530cd47 100644 --- a/internal/goldeneye/dialect/dialect.go +++ b/internal/goldeneye/dialect/dialect.go @@ -69,6 +69,9 @@ type Function struct { Args []Arg `json:"args,omitempty"` Returns string `json:"returns"` Nullable bool `json:"nullable,omitempty"` + // NeverNull marks a result that is never NULL even when an argument + // is, in a dialect that propagates nullability. + NeverNull bool `json:"never_null,omitempty"` } // Arg is one of a function's parameters. Mode is 'i'n, 'o'ut, 'b'oth, diff --git a/internal/goldeneye/spanner/library.go b/internal/goldeneye/spanner/library.go new file mode 100644 index 0000000000..6aaadc5d4a --- /dev/null +++ b/internal/goldeneye/spanner/library.go @@ -0,0 +1,428 @@ +package spanner + +import ( + "bufio" + "bytes" + "context" + _ "embed" + "encoding/json" + "fmt" + "sort" + "strconv" + "strings" + + "cloud.google.com/go/spanner/apiv1/spannerpb" + + "github.com/sqlc-dev/sqlc/internal/goldeneye/dialect" +) + +// Spanner keeps no catalog of its types, functions or operators, so the +// library is read by asking: each candidate is compiled in PLAN mode, which +// runs nothing, against a probe database, and what Spanner accepts is kept +// with the type it reports. + +// libraryDatabase is the probe database the library is read from. +const libraryDatabase = "goldeneye_library" + +// librarySchema is the probe database: a table with a NOT NULL column of +// each type a candidate's argument can be, the search indexes SCORE and +// SEARCH need, the sequence GET_NEXT_SEQUENCE_VALUE draws from, and a +// commit timestamp column for PENDING_COMMIT_TIMESTAMP. +var librarySchema = []string{ + `CREATE TABLE probe ( + id INT64 NOT NULL, + i INT64 NOT NULL, + s STRING(MAX) NOT NULL, + f FLOAT64 NOT NULL, + f32 FLOAT32 NOT NULL, + n NUMERIC NOT NULL, + b BOOL NOT NULL, + byt BYTES(MAX) NOT NULL, + d DATE NOT NULL, + ts TIMESTAMP NOT NULL, + j JSON NOT NULL, + ai ARRAY NOT NULL, + ast ARRAY NOT NULL, + af ARRAY NOT NULL, + af32 ARRAY NOT NULL, + aj ARRAY NOT NULL, + ab ARRAY NOT NULL, + tl TOKENLIST AS (TOKENIZE_FULLTEXT(s)) HIDDEN, + tl_substring TOKENLIST AS (TOKENIZE_SUBSTRING(s)) HIDDEN, + tl_ngrams TOKENLIST AS (TOKENIZE_NGRAMS(s)) HIDDEN, +) PRIMARY KEY (id)`, + `CREATE SEARCH INDEX probe_search ON probe(tl, tl_substring, tl_ngrams)`, + `CREATE SEQUENCE probe_sequence OPTIONS (sequence_kind = 'bit_reversed_positive')`, + `CREATE TABLE probe_commits ( + id INT64 NOT NULL, + committed_at TIMESTAMP OPTIONS (allow_commit_timestamp = true), +) PRIMARY KEY (id)`, +} + +// probeValues is the expression a candidate's argument of each type is +// called with: the probe table's column of that type, so that the call is +// made the way a query makes it, on a value rather than a constant. +var probeValues = map[string]string{ + "int64": "i", + "string": "s", + "float64": "f", + "float32": "f32", + "numeric": "n", + "bool": "b", + "bytes": "byt", + "date": "d", + "timestamp": "ts", + "json": "j", + "interval": "INTERVAL 1 DAY", + "tokenlist": "tl", + "array(int64)": "ai", + "array(string)": "ast", + "array(float64)": "af", + "array(float32)": "af32", + "array(json)": "aj", + "array(bytes)": "ab", +} + +// libraryTypes are the types the dialect lists, in the order types.jsonl +// does, with the category sqlc groups each in and the spelling Spanner is +// asked to cast to. A type Spanner does not have, BigQuery's BIGNUMERIC or +// DATETIME, is GoogleSQL's all the same, and the engine is GoogleSQL's, +// shared with BigQuery; those have no probe and are listed as they are. +var libraryTypes = []struct { + dialect.Type + probe string +}{ + {dialect.Type{Name: "bool", Category: "B"}, "BOOL"}, + {dialect.Type{Name: "int64", Category: "N"}, "INT64"}, + {dialect.Type{Name: "float32", Category: "N"}, "FLOAT32"}, + {dialect.Type{Name: "float64", Category: "N"}, "FLOAT64"}, + {dialect.Type{Name: "numeric", Category: "N"}, "NUMERIC"}, + {dialect.Type{Name: "bignumeric", Category: "N"}, ""}, + {dialect.Type{Name: "string", Category: "S"}, "STRING"}, + {dialect.Type{Name: "bytes", Category: "S"}, "BYTES"}, + {dialect.Type{Name: "uuid", Category: "S"}, "UUID"}, + {dialect.Type{Name: "date", Category: "D"}, "DATE"}, + {dialect.Type{Name: "datetime", Category: "D"}, ""}, + {dialect.Type{Name: "time", Category: "D"}, ""}, + {dialect.Type{Name: "timestamp", Category: "D"}, "TIMESTAMP"}, + {dialect.Type{Name: "interval", Category: "T"}, "INTERVAL"}, + {dialect.Type{Name: "json", Category: "U"}, "JSON"}, + {dialect.Type{Name: "geography", Category: "U"}, ""}, + {dialect.Type{Name: "struct", Category: "U"}, "STRUCT"}, + {dialect.Type{Name: "enum", Category: "U"}, ""}, + {dialect.Type{Name: "proto", Category: "U"}, ""}, + {dialect.Type{Name: "tokenlist", Category: "U"}, ""}, + {dialect.Type{Name: "array", Category: "A"}, "ARRAY"}, + {dialect.Type{Name: "range", Category: "U"}, ""}, + {dialect.Type{Name: "map", Category: "U"}, ""}, +} + +//go:embed library.jsonl +var libraryCandidates []byte + +// candidate is one line of library.jsonl. +type candidate struct { + Name string `json:"name"` + Kind string `json:"kind,omitempty"` + Args []string `json:"args,omitempty"` + Returns string `json:"returns,omitempty"` + Nullable bool `json:"nullable,omitempty"` + NeverNull bool `json:"never_null,omitempty"` + // Expr is the expression to compile in place of name(args), and Query + // the whole statement, with $1, $2... standing for the arguments. + Expr string `json:"expr,omitempty"` + Query string `json:"query,omitempty"` +} + +// polymorphic reports whether a candidate takes a T or an AT. +func (c candidate) polymorphic() bool { + for _, a := range c.Args { + if a == "T" || a == "AT" { + return true + } + } + return false +} + +// sql is the statement that compiles a call of the candidate with the +// arguments given, as types. +func (c candidate) sql(args []string) (string, error) { + vals := make([]string, len(args)) + for i, a := range args { + if typ, lit, ok := strings.Cut(a, "="); ok { + _ = typ + vals[i] = lit + continue + } + v, ok := probeValues[a] + if !ok { + if c.Expr == "" && c.Query == "" { + return "", fmt.Errorf("%s: no probe value for %s", c.Name, a) + } + v = "NULL" + } + vals[i] = v + } + fill := func(tmpl string) string { + for i := len(vals); i >= 1; i-- { + tmpl = strings.ReplaceAll(tmpl, "$"+strconv.Itoa(i), vals[i-1]) + } + return tmpl + } + if c.Query != "" { + return fill(c.Query), nil + } + call := c.Name + "(" + strings.Join(vals, ", ") + ")" + if c.Expr != "" { + call = fill(c.Expr) + } + return "SELECT " + call + " AS r FROM probe", nil +} + +// seedType is the type an argument is written as in functions.jsonl. +func seedType(arg string) string { + switch arg { + case "T": + return "anyelement" + case "AT": + return "anyarray" + } + typ, _, _ := strings.Cut(arg, "=") + return typ +} + +// readLibrary reads types.jsonl, functions.jsonl and operators.jsonl from +// the probe database. +func readLibrary(ctx context.Context, s *server, session string) (dialect.Files, error) { + compile := func(sql string) (*spannerpb.Type, error) { + req := &spannerpb.ExecuteSqlRequest{Session: session, Sql: sql, QueryMode: spannerpb.ExecuteSqlRequest_PLAN} + if isDML(sql) { + req.Transaction = &spannerpb.TransactionSelector{Selector: &spannerpb.TransactionSelector_Begin{ + Begin: &spannerpb.TransactionOptions{Mode: &spannerpb.TransactionOptions_ReadWrite_{ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}}}, + }} + req.Seqno = 1 + } + rs, err := s.data.ExecuteSql(ctx, req) + if err != nil { + return nil, err + } + if rs.Metadata != nil && rs.Metadata.Transaction != nil && len(rs.Metadata.Transaction.Id) > 0 { + s.data.Rollback(context.WithoutCancel(ctx), &spannerpb.RollbackRequest{Session: session, TransactionId: rs.Metadata.Transaction.Id}) + } + if rs.Metadata == nil || rs.Metadata.RowType == nil || len(rs.Metadata.RowType.Fields) == 0 { + return nil, nil + } + return rs.Metadata.RowType.Fields[0].Type, nil + } + + types, err := readLibraryTypes(compile) + if err != nil { + return nil, err + } + funcs, err := readLibraryFunctions(compile) + if err != nil { + return nil, err + } + operators, err := readLibraryOperators(compile) + if err != nil { + return nil, err + } + files := dialect.Files{} + for name, records := range map[string]any{ + dialect.TypesFile: types, + dialect.FunctionsFile: funcs, + dialect.OperatorsFile: operators, + } { + var blob []byte + var err error + switch r := records.(type) { + case []dialect.Type: + blob, err = dialect.JSONL(r) + case []dialect.Function: + blob, err = dialect.JSONL(r) + case []dialect.Operator: + blob, err = dialect.JSONL(r) + } + if err != nil { + return nil, err + } + files[name] = blob + } + return files, nil +} + +type compiler func(sql string) (*spannerpb.Type, error) + +// readLibraryTypes checks that Spanner has each type the dialect lists with +// a probe, failing when one is gone, since sqlc would then accept a schema +// Spanner rejects. +func readLibraryTypes(compile compiler) ([]dialect.Type, error) { + types := make([]dialect.Type, 0, len(libraryTypes)) + for _, t := range libraryTypes { + if t.probe != "" { + // IS NULL, since Spanner returns no STRUCT as a column. + if _, err := compile("SELECT CAST(NULL AS " + t.probe + ") IS NULL AS r"); err != nil { + return nil, fmt.Errorf("type %s: %w", t.Name, err) + } + } + types = append(types, t.Type) + } + return types, nil +} + +// readLibraryFunctions compiles every candidate in library.jsonl and lists +// the ones Spanner accepts, sorted by name and then by arity, the order the +// candidates give otherwise kept. +func readLibraryFunctions(compile compiler) ([]dialect.Function, error) { + var funcs []dialect.Function + scanner := bufio.NewScanner(bytes.NewReader(libraryCandidates)) + for line := 1; scanner.Scan(); line++ { + text := strings.TrimSpace(scanner.Text()) + if text == "" || strings.HasPrefix(text, "#") { + continue + } + var c candidate + if err := json.Unmarshal([]byte(text), &c); err != nil { + return nil, fmt.Errorf("library.jsonl:%d: %w", line, err) + } + returns, ok, err := probeCandidate(compile, c) + if err != nil { + return nil, fmt.Errorf("library.jsonl:%d: %w", line, err) + } + if !ok { + continue + } + fn := dialect.Function{Name: c.Name, Kind: c.Kind, Returns: returns, Nullable: c.Nullable, NeverNull: c.NeverNull} + for _, a := range c.Args { + fn.Args = append(fn.Args, dialect.Arg{Type: seedType(a)}) + } + funcs = append(funcs, fn) + } + if err := scanner.Err(); err != nil { + return nil, err + } + sort.SliceStable(funcs, func(i, j int) bool { + if funcs[i].Name != funcs[j].Name { + return funcs[i].Name < funcs[j].Name + } + return len(funcs[i].Args) < len(funcs[j].Args) + }) + return funcs, nil +} + +// probeCandidate compiles a candidate and returns the type it is seeded +// with, or false when Spanner rejects it. A candidate that is not there is +// left out rather than failing the run: a release of Spanner that drops a +// function shows in the diff of functions.jsonl. +func probeCandidate(compile compiler, c candidate) (string, bool, error) { + if !c.polymorphic() { + sql, err := c.sql(c.Args) + if err != nil { + return "", false, err + } + t, err := compile(sql) + if err != nil { + return "", false, nil + } + got := typeSpelling(t) + if c.Query != "" && c.Returns != "" { + // The statement's result, if it has one, is not the call's. + return c.Returns, true, nil + } + if c.Returns != "" && c.Returns != got { + return "", false, fmt.Errorf("%s returns %s, not %s", c.Name, got, c.Returns) + } + return got, true, nil + } + for _, elem := range []string{"int64", "string"} { + args := make([]string, len(c.Args)) + for i, a := range c.Args { + switch a { + case "T": + args[i] = elem + case "AT": + args[i] = "array(" + elem + ")" + default: + args[i] = a + } + } + sql, err := c.sql(args) + if err != nil { + return "", false, err + } + t, err := compile(sql) + if err != nil { + return "", false, nil + } + want := c.Returns + for i := len(args); i >= 1; i-- { + want = strings.ReplaceAll(want, "$"+strconv.Itoa(i), args[i-1]) + } + switch want { + case "anyelement": + want = elem + case "anyarray": + want = "array(" + elem + ")" + } + if got := typeSpelling(t); got != want { + return "", false, fmt.Errorf("%s(%s) returns %s, not %s", c.Name, strings.Join(args, ", "), got, want) + } + } + return c.Returns, true, nil +} + +// typeSpelling spells a type the way functions.jsonl does. +func typeSpelling(t *spannerpb.Type) string { + if t == nil { + return "" + } + if t.Code == spannerpb.TypeCode_ARRAY { + return "array(" + typeSpelling(t.ArrayElementType) + ")" + } + return strings.ToLower(t.Code.String()) +} + +// libraryOperandTypes are the operands each operator is tried with. +var ( + numericOperands = []string{"int64", "float64", "float32", "numeric"} + operatorPairs = func() [][3]string { + var pairs [][3]string + for _, op := range []string{"+", "-", "*", "/"} { + for _, l := range numericOperands { + for _, r := range numericOperands { + pairs = append(pairs, [3]string{op, l, r}) + } + } + } + for _, t := range []string{"string", "bytes"} { + pairs = append(pairs, [3]string{"||", t, t}) + } + for _, op := range []string{"+", "-"} { + pairs = append(pairs, [3]string{op, "timestamp", "interval"}, [3]string{op, "interval", "interval"}) + } + return append(pairs, + [3]string{"+", "interval", "timestamp"}, + [3]string{"-", "date", "date"}, [3]string{"-", "timestamp", "timestamp"}, + [3]string{"*", "interval", "int64"}, [3]string{"/", "interval", "int64"}, + [3]string{"&", "int64", "int64"}, [3]string{"|", "int64", "int64"}, [3]string{"^", "int64", "int64"}, + [3]string{"<<", "int64", "int64"}, [3]string{">>", "int64", "int64"}, + [3]string{"&", "bytes", "bytes"}, [3]string{"|", "bytes", "bytes"}, [3]string{"^", "bytes", "bytes"}, + ) + }() +) + +// readLibraryOperators compiles each binary operator over the operands it +// is tried with and lists the ones Spanner accepts with their result. The +// comparisons are left to dialect.json's rules, which make every one of +// them a BOOL. +func readLibraryOperators(compile compiler) ([]dialect.Operator, error) { + var ops []dialect.Operator + for _, p := range operatorPairs { + t, err := compile("SELECT " + probeValues[p[1]] + " " + p[0] + " " + probeValues[p[2]] + " AS r FROM probe") + if err != nil { + continue + } + ops = append(ops, dialect.Operator{Name: p[0], Left: p[1], Right: p[2], Result: typeSpelling(t)}) + } + return ops, nil +} diff --git a/internal/goldeneye/spanner/library.jsonl b/internal/goldeneye/spanner/library.jsonl new file mode 100644 index 0000000000..a0a9c286b9 --- /dev/null +++ b/internal/goldeneye/spanner/library.jsonl @@ -0,0 +1,322 @@ +# Candidate signatures of Spanner's GoogleSQL functions. Spanner keeps no +# catalog of its functions, so this file lists the calls to try; goldeneye +# compiles each one in PLAN mode against the probe table in library.go, +# keeps the ones Spanner accepts and writes the result type Spanner reports. +# +# An argument is a type, a type=literal for an argument Spanner wants as a +# literal, T for a polymorphic value or AT for an array of one. A +# polymorphic signature is tried with INT64 and STRING, and its returns +# ($1 for the first argument's type, array($1), anyelement for an AT's +# element) must match what Spanner reports for both. nullable and +# never_null say what Spanner cannot: whether the result can be NULL when +# the arguments are not, or is never NULL when they are. +# Aggregates: NULL over no rows, except the counts. +{"name": "count", "kind": "a", "args": ["T"], "returns": "int64", "never_null": true} +{"name": "countif", "kind": "a", "args": ["bool"], "never_null": true} +{"name": "any_value", "kind": "a", "args": ["T"], "returns": "$1", "nullable": true} +{"name": "array_agg", "kind": "a", "args": ["T"], "returns": "array($1)", "nullable": true} +{"name": "array_concat_agg", "kind": "a", "args": ["AT"], "returns": "$1", "nullable": true} +{"name": "avg", "kind": "a", "args": ["int64"], "nullable": true} +{"name": "avg", "kind": "a", "args": ["float64"], "nullable": true} +{"name": "avg", "kind": "a", "args": ["float32"], "nullable": true} +{"name": "avg", "kind": "a", "args": ["numeric"], "nullable": true} +{"name": "avg", "kind": "a", "args": ["interval"], "nullable": true} +{"name": "bit_and", "kind": "a", "args": ["int64"], "nullable": true} +{"name": "bit_or", "kind": "a", "args": ["int64"], "nullable": true} +{"name": "bit_xor", "kind": "a", "args": ["int64"], "nullable": true} +{"name": "logical_and", "kind": "a", "args": ["bool"], "nullable": true} +{"name": "logical_or", "kind": "a", "args": ["bool"], "nullable": true} +{"name": "max", "kind": "a", "args": ["T"], "returns": "$1", "nullable": true} +{"name": "min", "kind": "a", "args": ["T"], "returns": "$1", "nullable": true} +{"name": "string_agg", "kind": "a", "args": ["string"], "nullable": true} +{"name": "string_agg", "kind": "a", "args": ["string", "string=','"], "nullable": true} +{"name": "string_agg", "kind": "a", "args": ["bytes"], "nullable": true} +{"name": "string_agg", "kind": "a", "args": ["bytes", "bytes=b','"], "nullable": true} +{"name": "sum", "kind": "a", "args": ["int64"], "nullable": true} +{"name": "sum", "kind": "a", "args": ["float64"], "nullable": true} +{"name": "sum", "kind": "a", "args": ["float32"], "nullable": true} +{"name": "sum", "kind": "a", "args": ["numeric"], "nullable": true} +{"name": "sum", "kind": "a", "args": ["interval"], "nullable": true} +{"name": "stddev", "kind": "a", "args": ["float64"], "nullable": true} +{"name": "stddev_samp", "kind": "a", "args": ["float64"], "nullable": true} +{"name": "variance", "kind": "a", "args": ["float64"], "nullable": true} +{"name": "var_samp", "kind": "a", "args": ["float64"], "nullable": true} +# Window functions. +# Math. +{"name": "abs", "args": ["int64"]} +{"name": "abs", "args": ["float64"]} +{"name": "abs", "args": ["float32"]} +{"name": "abs", "args": ["numeric"]} +{"name": "sign", "args": ["int64"]} +{"name": "sign", "args": ["float64"]} +{"name": "sign", "args": ["numeric"]} +{"name": "mod", "args": ["int64", "int64"]} +{"name": "mod", "args": ["numeric", "numeric"]} +{"name": "div", "args": ["int64", "int64"]} +{"name": "div", "args": ["numeric", "numeric"]} +{"name": "round", "args": ["float64"]} +{"name": "round", "args": ["numeric"]} +{"name": "round", "args": ["float64", "int64"]} +{"name": "round", "args": ["numeric", "int64"]} +{"name": "trunc", "args": ["float64"]} +{"name": "trunc", "args": ["numeric"]} +{"name": "trunc", "args": ["float64", "int64"]} +{"name": "trunc", "args": ["numeric", "int64"]} +{"name": "ceil", "args": ["float64"]} +{"name": "ceil", "args": ["numeric"]} +{"name": "ceiling", "args": ["float64"]} +{"name": "ceiling", "args": ["numeric"]} +{"name": "floor", "args": ["float64"]} +{"name": "floor", "args": ["numeric"]} +{"name": "sqrt", "args": ["float64"]} +{"name": "sqrt", "args": ["numeric"]} +{"name": "pow", "args": ["float64", "float64"]} +{"name": "pow", "args": ["numeric", "numeric"]} +{"name": "power", "args": ["float64", "float64"]} +{"name": "power", "args": ["numeric", "numeric"]} +{"name": "exp", "args": ["float64"]} +{"name": "exp", "args": ["numeric"]} +{"name": "ln", "args": ["float64"]} +{"name": "ln", "args": ["numeric"]} +{"name": "log", "args": ["float64"]} +{"name": "log", "args": ["numeric"]} +{"name": "log", "args": ["float64", "float64"]} +{"name": "log10", "args": ["float64"]} +{"name": "log10", "args": ["numeric"]} +{"name": "ieee_divide", "args": ["float64", "float64"]} +{"name": "safe_divide", "args": ["int64", "int64"], "nullable": true} +{"name": "safe_divide", "args": ["float64", "float64"], "nullable": true} +{"name": "safe_divide", "args": ["numeric", "numeric"], "nullable": true} +{"name": "safe_add", "args": ["int64", "int64"], "nullable": true} +{"name": "safe_add", "args": ["float64", "float64"], "nullable": true} +{"name": "safe_add", "args": ["numeric", "numeric"], "nullable": true} +{"name": "safe_subtract", "args": ["int64", "int64"], "nullable": true} +{"name": "safe_subtract", "args": ["float64", "float64"], "nullable": true} +{"name": "safe_subtract", "args": ["numeric", "numeric"], "nullable": true} +{"name": "safe_multiply", "args": ["int64", "int64"], "nullable": true} +{"name": "safe_multiply", "args": ["float64", "float64"], "nullable": true} +{"name": "safe_multiply", "args": ["numeric", "numeric"], "nullable": true} +{"name": "safe_negate", "args": ["int64"], "nullable": true} +{"name": "safe_negate", "args": ["float64"], "nullable": true} +{"name": "safe_negate", "args": ["numeric"], "nullable": true} +{"name": "is_inf", "args": ["float64"]} +{"name": "is_nan", "args": ["float64"]} +{"name": "sin", "args": ["float64"]} +{"name": "cos", "args": ["float64"]} +{"name": "tan", "args": ["float64"]} +{"name": "asin", "args": ["float64"]} +{"name": "acos", "args": ["float64"]} +{"name": "atan", "args": ["float64"]} +{"name": "atan2", "args": ["float64", "float64"]} +{"name": "bit_count", "args": ["int64"]} +{"name": "bit_count", "args": ["bytes"]} +{"name": "bit_reverse", "args": ["int64", "bool"]} +# Strings. +{"name": "length", "args": ["string"]} +{"name": "length", "args": ["bytes"]} +{"name": "byte_length", "args": ["string"]} +{"name": "byte_length", "args": ["bytes"]} +{"name": "char_length", "args": ["string"]} +{"name": "character_length", "args": ["string"]} +{"name": "concat", "args": ["string", "string"]} +{"name": "concat", "args": ["bytes", "bytes"]} +{"name": "lower", "args": ["string"]} +{"name": "lower", "args": ["bytes"]} +{"name": "upper", "args": ["string"]} +{"name": "upper", "args": ["bytes"]} +{"name": "trim", "args": ["string"]} +{"name": "trim", "args": ["string", "string"]} +{"name": "trim", "args": ["bytes", "bytes"]} +{"name": "ltrim", "args": ["string"]} +{"name": "ltrim", "args": ["string", "string"]} +{"name": "rtrim", "args": ["string"]} +{"name": "rtrim", "args": ["string", "string"]} +{"name": "lpad", "args": ["string", "int64"]} +{"name": "lpad", "args": ["string", "int64", "string"]} +{"name": "lpad", "args": ["bytes", "int64", "bytes"]} +{"name": "rpad", "args": ["string", "int64"]} +{"name": "rpad", "args": ["string", "int64", "string"]} +{"name": "rpad", "args": ["bytes", "int64", "bytes"]} +{"name": "substr", "args": ["string", "int64"]} +{"name": "substr", "args": ["string", "int64", "int64"]} +{"name": "substr", "args": ["bytes", "int64"]} +{"name": "substr", "args": ["bytes", "int64", "int64"]} +{"name": "substring", "args": ["string", "int64"]} +{"name": "substring", "args": ["string", "int64", "int64"]} +{"name": "strpos", "args": ["string", "string"]} +{"name": "strpos", "args": ["bytes", "bytes"]} +{"name": "starts_with", "args": ["string", "string"]} +{"name": "starts_with", "args": ["bytes", "bytes"]} +{"name": "ends_with", "args": ["string", "string"]} +{"name": "ends_with", "args": ["bytes", "bytes"]} +{"name": "replace", "args": ["string", "string", "string"]} +{"name": "replace", "args": ["bytes", "bytes", "bytes"]} +{"name": "repeat", "args": ["string", "int64"]} +{"name": "repeat", "args": ["bytes", "int64"]} +{"name": "reverse", "args": ["string"]} +{"name": "reverse", "args": ["bytes"]} +{"name": "split", "args": ["string"]} +{"name": "split", "args": ["string", "string"]} +{"name": "split", "args": ["bytes", "bytes"]} +{"name": "split_substr", "args": ["string", "string", "int64"]} +{"name": "split_substr", "args": ["string", "string", "int64", "int64"]} +{"name": "regexp_contains", "args": ["string", "string='a'"]} +{"name": "regexp_contains", "args": ["bytes", "bytes=b'a'"]} +{"name": "regexp_extract", "args": ["string", "string='a'"], "nullable": true} +{"name": "regexp_extract", "args": ["bytes", "bytes=b'a'"], "nullable": true} +{"name": "regexp_extract_all", "args": ["string", "string='a'"]} +{"name": "regexp_extract_all", "args": ["bytes", "bytes=b'a'"]} +{"name": "regexp_replace", "args": ["string", "string='a'", "string"]} +{"name": "regexp_replace", "args": ["bytes", "bytes=b'a'", "bytes"]} +{"name": "format", "args": ["string"]} +{"name": "to_base64", "args": ["bytes"]} +{"name": "from_base64", "args": ["string"]} +{"name": "to_base32", "args": ["bytes"]} +{"name": "from_base32", "args": ["string"]} +{"name": "to_hex", "args": ["bytes"]} +{"name": "from_hex", "args": ["string"]} +{"name": "to_code_points", "args": ["string"]} +{"name": "to_code_points", "args": ["bytes"]} +{"name": "code_points_to_string", "args": ["array(int64)"]} +{"name": "code_points_to_bytes", "args": ["array(int64)"]} +{"name": "safe_convert_bytes_to_string", "args": ["bytes"]} +{"name": "soundex", "args": ["string"]} +{"name": "normalize", "args": ["string"]} +{"name": "normalize_and_casefold", "args": ["string"]} +{"name": "ltrim", "args": ["bytes", "bytes"]} +{"name": "rtrim", "args": ["bytes", "bytes"]} +# Dates and timestamps; date parts are passed as their keyword. +{"name": "current_date"} +{"name": "current_timestamp"} +{"name": "date", "args": ["int64", "int64", "int64"]} +{"name": "date", "args": ["timestamp"]} +{"name": "date", "args": ["timestamp", "string"]} +{"name": "date_add", "args": ["date", "interval"]} +{"name": "date_sub", "args": ["date", "interval"]} +{"name": "date_diff", "args": ["date", "date", "string=DAY"]} +{"name": "date_trunc", "args": ["date", "string=MONTH"]} +{"name": "date_from_unix_date", "args": ["int64"]} +{"name": "unix_date", "args": ["date"]} +{"name": "format_date", "args": ["string", "date"]} +{"name": "parse_date", "args": ["string", "string"]} +{"name": "timestamp", "args": ["string"]} +{"name": "timestamp", "args": ["date"]} +{"name": "timestamp", "args": ["string", "string"]} +{"name": "timestamp", "args": ["date", "string"]} +{"name": "timestamp_add", "args": ["timestamp", "interval"]} +{"name": "timestamp_sub", "args": ["timestamp", "interval"]} +{"name": "timestamp_diff", "args": ["timestamp", "timestamp", "string=SECOND"]} +{"name": "timestamp_trunc", "args": ["timestamp", "string=DAY"]} +{"name": "timestamp_trunc", "args": ["timestamp", "string=DAY", "string"]} +{"name": "timestamp_seconds", "args": ["int64"]} +{"name": "timestamp_millis", "args": ["int64"]} +{"name": "timestamp_micros", "args": ["int64"]} +{"name": "unix_seconds", "args": ["timestamp"]} +{"name": "unix_millis", "args": ["timestamp"]} +{"name": "unix_micros", "args": ["timestamp"]} +{"name": "format_timestamp", "args": ["string", "timestamp"]} +{"name": "format_timestamp", "args": ["string", "timestamp", "string"]} +{"name": "parse_timestamp", "args": ["string", "string"]} +{"name": "parse_timestamp", "args": ["string", "string", "string"]} +{"name": "string", "args": ["timestamp"]} +{"name": "string", "args": ["timestamp", "string"]} +{"name": "string", "args": ["json"]} +{"name": "make_interval", "args": ["int64", "int64", "int64"]} +{"name": "justify_days", "args": ["interval"]} +{"name": "justify_hours", "args": ["interval"]} +{"name": "justify_interval", "args": ["interval"]} +# JSON. +{"name": "json_value", "args": ["json", "string='$.a'"], "nullable": true} +{"name": "json_value", "args": ["string", "string='$.a'"], "nullable": true} +{"name": "json_query", "args": ["json", "string='$.a'"], "nullable": true} +{"name": "json_query", "args": ["string", "string='$.a'"], "nullable": true} +{"name": "json_query_array", "args": ["json", "string='$.a'"], "nullable": true} +{"name": "json_query_array", "args": ["json"], "nullable": true} +{"name": "json_value_array", "args": ["json", "string='$.a'"], "nullable": true} +{"name": "json_value_array", "args": ["json"], "nullable": true} +{"name": "json_type", "args": ["json"]} +{"name": "json_keys", "args": ["json"]} +{"name": "json_strip_nulls", "args": ["json"]} +{"name": "parse_json", "args": ["string"]} +{"name": "to_json", "args": ["T"], "returns": "json", "never_null": true} +{"name": "to_json_string", "args": ["json"], "never_null": true} +{"name": "safe_to_json", "args": ["T"], "returns": "json", "never_null": true} +{"name": "bool", "args": ["json"]} +{"name": "int64", "args": ["json"]} +{"name": "float64", "args": ["json"]} +{"name": "float32", "args": ["json"]} +{"name": "lax_bool", "args": ["json"], "nullable": true} +{"name": "lax_int64", "args": ["json"], "nullable": true} +{"name": "lax_float64", "args": ["json"], "nullable": true} +{"name": "lax_string", "args": ["json"], "nullable": true} +{"name": "json_array", "args": ["T"], "returns": "json"} +{"name": "json_object", "args": ["string", "T"], "returns": "json"} +{"name": "json_set", "args": ["json", "string='$.a'", "T"], "returns": "json"} +{"name": "json_remove", "args": ["json", "string='$.a'"]} +{"name": "json_array_append", "args": ["json", "string='$'", "T"], "returns": "json"} +{"name": "json_array_insert", "args": ["json", "string='$[0]'", "T"], "returns": "json"} +{"name": "json_contains", "args": ["json", "json"]} +# Arrays. +{"name": "array_length", "args": ["AT"], "returns": "int64"} +{"name": "array_to_string", "args": ["array(string)", "string"]} +{"name": "array_to_string", "args": ["array(string)", "string", "string"]} +{"name": "array_to_string", "args": ["array(bytes)", "bytes"]} +{"name": "array_reverse", "args": ["AT"], "returns": "$1"} +{"name": "array_concat", "args": ["AT", "AT"], "returns": "$1"} +{"name": "array_first", "args": ["AT"], "returns": "anyelement", "nullable": true} +{"name": "array_last", "args": ["AT"], "returns": "anyelement", "nullable": true} +{"name": "array_min", "args": ["AT"], "returns": "anyelement", "nullable": true} +{"name": "array_max", "args": ["AT"], "returns": "anyelement", "nullable": true} +{"name": "array_slice", "args": ["AT", "int64", "int64"], "returns": "$1"} +{"name": "array_includes", "args": ["AT", "T"], "returns": "bool"} +{"name": "array_includes_any", "args": ["AT", "AT"], "returns": "bool"} +{"name": "array_includes_all", "args": ["AT", "AT"], "returns": "bool"} +{"name": "array_is_distinct", "args": ["AT"], "returns": "bool"} +{"name": "generate_array", "args": ["int64", "int64"]} +{"name": "generate_array", "args": ["int64", "int64", "int64"]} +{"name": "generate_array", "args": ["float64", "float64", "float64"]} +{"name": "generate_date_array", "args": ["date", "date"]} +{"name": "generate_date_array", "args": ["date", "date", "interval"]} +# Hashing, ids and sequences. +{"name": "farm_fingerprint", "args": ["string"]} +{"name": "farm_fingerprint", "args": ["bytes"]} +{"name": "md5", "args": ["string"]} +{"name": "md5", "args": ["bytes"]} +{"name": "sha1", "args": ["string"]} +{"name": "sha1", "args": ["bytes"]} +{"name": "sha256", "args": ["string"]} +{"name": "sha256", "args": ["bytes"]} +{"name": "sha512", "args": ["string"]} +{"name": "sha512", "args": ["bytes"]} +{"name": "generate_uuid", "never_null": true} +{"name": "new_uuid", "never_null": true} +# Search and vectors. +{"name": "search", "args": ["tokenlist", "string='a'"]} +{"name": "search_substring", "args": ["tokenlist", "string='a'"]} +{"name": "search_ngrams", "args": ["tokenlist", "string='a'"]} +{"name": "score", "args": ["tokenlist", "string='a'"]} +{"name": "score_ngrams", "args": ["tokenlist", "string='a'"]} +{"name": "snippet", "args": ["string", "string='a'"]} +{"name": "cosine_distance", "args": ["array(float64)", "array(float64)"]} +{"name": "cosine_distance", "args": ["array(float32)", "array(float32)"]} +{"name": "euclidean_distance", "args": ["array(float64)", "array(float64)"]} +{"name": "euclidean_distance", "args": ["array(float32)", "array(float32)"]} +{"name": "dot_product", "args": ["array(float64)", "array(float64)"]} +{"name": "dot_product", "args": ["array(float32)", "array(float32)"]} +{"name": "dot_product", "args": ["array(int64)", "array(int64)"]} +# Calls a select list cannot spell as name(args): the expression or the +# whole statement to compile instead, with $1, $2... for the arguments. A +# statement whose result is not the call names what the call returns. +{"name": "extract", "args": ["timestamp"], "expr": "EXTRACT(YEAR FROM $1)"} +{"name": "extract", "args": ["timestamp", "string"], "expr": "EXTRACT(YEAR FROM $1 AT TIME ZONE $2)"} +{"name": "extract", "args": ["date"], "expr": "EXTRACT(YEAR FROM $1)"} +{"name": "get_next_sequence_value", "args": ["anyelement"], "expr": "GET_NEXT_SEQUENCE_VALUE(SEQUENCE probe_sequence)", "never_null": true} +{"name": "pending_commit_timestamp", "query": "INSERT INTO probe_commits (id, committed_at) VALUES (1, PENDING_COMMIT_TIMESTAMP())", "returns": "timestamp", "never_null": true} +{"name": "search", "args": ["tokenlist", "string='a'"], "query": "SELECT id FROM probe WHERE SEARCH($1, $2)", "returns": "bool"} +{"name": "search_substring", "args": ["tokenlist", "string='a'"], "query": "SELECT id FROM probe WHERE SEARCH_SUBSTRING(tl_substring, $2)", "returns": "bool"} +{"name": "search_ngrams", "args": ["tokenlist", "string='a'"], "query": "SELECT id FROM probe WHERE SEARCH_NGRAMS(tl_ngrams, $2)", "returns": "bool"} +{"name": "score", "args": ["tokenlist", "string='a'"], "query": "SELECT SCORE($1, $2) FROM probe WHERE SEARCH($1, $2)"} +{"name": "score_ngrams", "args": ["tokenlist", "string='a'"], "query": "SELECT SCORE_NGRAMS(tl_ngrams, $2) FROM probe WHERE SEARCH_NGRAMS(tl_ngrams, $2)"} +# unnest stands for UNNEST(array) in FROM and IN, which sqlc reads as a +# call; its element can be NULL. +{"name": "unnest", "args": ["AT"], "returns": "anyelement", "expr": "(SELECT e FROM UNNEST($1) AS e LIMIT 1)", "nullable": true} diff --git a/internal/goldeneye/spanner/spanner.go b/internal/goldeneye/spanner/spanner.go index 90d82a13fe..61a8858fd0 100644 --- a/internal/goldeneye/spanner/spanner.go +++ b/internal/goldeneye/spanner/spanner.go @@ -6,11 +6,13 @@ // googlesql directory, since sqlc's engine is named after the language // Spanner speaks rather than the database. // -// Spanner keeps no catalog of its types, functions or operators, so -// types.jsonl, functions.jsonl and operators.jsonl are written by hand and -// are not this package's business. What it does describe is its -// information schema: relations.jsonl is every view of INFORMATION_SCHEMA -// and SPANNER_SYS, read from INFORMATION_SCHEMA itself. +// relations.jsonl is every view of INFORMATION_SCHEMA and SPANNER_SYS, +// read from INFORMATION_SCHEMA itself. Spanner keeps no catalog of its +// types, functions or operators, so types.jsonl, functions.jsonl and +// operators.jsonl are read by asking: library.jsonl lists the function +// signatures to try, and each is compiled in PLAN mode against a probe +// database, the ones Spanner accepts kept with the type it reports; the +// operators and types are tried the same way (see library.go). // // The package also verifies the GoogleSQL analyze cases against the same // server: each case's schema becomes a database of its own, its fixture is @@ -193,7 +195,8 @@ func (s *server) query(ctx context.Context, session, sql string) ([][]*structpb. // Generate reads the dialect from the server: relations.jsonl, the views of // INFORMATION_SCHEMA and SPANNER_SYS, read from a database of their own, -// since every database has the same ones. +// since every database has the same ones, and the types, functions and +// operators, read from a probe database. func Generate(ctx context.Context, endpoint string) (dialect.Files, error) { s, err := open(ctx, endpoint) if err != nil { @@ -220,5 +223,23 @@ func Generate(ctx context.Context, endpoint string) (dialect.Files, error) { if err != nil { return nil, err } - return dialect.Files{dialect.RelationsFile: blob}, nil + + lib := Instance + "/databases/" + libraryDatabase + if err := s.dropDatabase(ctx, lib); err != nil { + return nil, err + } + if _, err := s.createDatabase(ctx, libraryDatabase, librarySchema); err != nil { + return nil, err + } + defer s.dropDatabase(context.WithoutCancel(ctx), lib) + libSession, err := s.session(ctx, lib) + if err != nil { + return nil, err + } + files, err := readLibrary(ctx, s, libSession) + if err != nil { + return nil, err + } + files[dialect.RelationsFile] = blob + return files, nil }