snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 02:17:34 +05:00
commit 0f4df23290
7340 changed files with 238743 additions and 0 deletions

View File

@@ -0,0 +1,673 @@
package spanner
import (
"context"
"fmt"
"hash/crc32"
"os"
"regexp"
"strconv"
"strings"
"cloud.google.com/go/spanner/apiv1/spannerpb"
"github.com/sqlc-dev/sqlc/internal/goldeneye/analysis"
"github.com/sqlc-dev/sqlc/internal/goldeneye/endtoend"
)
// The analyze cases are checked against a live server, which compiles
// each query in PLAN mode: nothing runs, and the server reports the name
// and type of each result column, the type of each parameter the query
// leaves undeclared, and the query plan. The metadata types a value the
// way the wire does — STRING, ARRAY<INT64> — without the length a
// declaration gives a STRING(10) or whether the column can be NULL, so a
// result column the plan reads from a table column is described from the
// case's INFORMATION_SCHEMA instead, and a parameter the plan compares
// with or assigns to a table column is described as that column.
// placeholder is one parameter of a query as sqlc numbers them: each
// @name or sqlc.arg name once, at its first appearance, which is how
// Spanner numbers its named parameters too.
type placeholder struct {
Number int
Name string
}
var identRe = regexp.MustCompile(`^@([A-Za-z_][A-Za-z0-9_]*)`)
// bind rewrites the query so that every parameter is a Spanner @name, and
// lists the parameters in sqlc's order. The query's own @name references
// are kept, since that is how sqlc's GoogleSQL queries name their
// parameters; a sqlc.arg becomes one, and each ? in turn becomes one
// named after its position. A @@system_variable is not a parameter.
func bind(query string) (string, []placeholder) {
positional := 0
sql := endtoend.Rewrite(query, func(name, _ string) string {
if name == "" {
positional++
name = "p" + strconv.Itoa(positional)
}
return "@" + name
})
var phs []placeholder
numbers := map[string]int{}
i := 0
for i < len(sql) {
c := sql[i]
switch {
case c == '\'' || c == '"' || c == '`':
i = skipQuoted(sql, i)
case strings.HasPrefix(sql[i:], "--") || strings.HasPrefix(sql[i:], "#"):
end := strings.IndexByte(sql[i:], '\n')
if end < 0 {
i = len(sql)
} else {
i += end
}
case strings.HasPrefix(sql[i:], "/*"):
end := strings.Index(sql[i:], "*/")
if end < 0 {
i = len(sql)
} else {
i += end + 2
}
case c == '@' && i+1 < len(sql) && sql[i+1] == '@':
i += 2
for i < len(sql) && (isWordByte(sql[i])) {
i++
}
case c == '@' && identRe.MatchString(sql[i:]):
m := identRe.FindStringSubmatch(sql[i:])
if _, ok := numbers[m[1]]; !ok {
numbers[m[1]] = len(phs) + 1
phs = append(phs, placeholder{Number: len(phs) + 1, Name: m[1]})
}
i += len(m[0])
default:
i++
}
}
return sql, phs
}
func isWordByte(c byte) bool {
return c == '_' || c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
}
// skipQuoted returns the index just past the quoted token starting at i,
// honouring backslash escapes.
func skipQuoted(s string, i int) int {
q := s[i]
j := i + 1
for j < len(s) {
switch {
case s[j] == '\\' && j+1 < len(s):
j += 2
case s[j] == q:
return j + 1
default:
j++
}
}
return len(s)
}
// splitStatements splits a script on the semicolons outside strings and
// comments, which is how a schema's DDL is handed to CREATE DATABASE and a
// fixture's DML to a transaction.
func splitStatements(src string) []string {
var stmts []string
flush := func(s string) {
if s = strings.TrimSpace(s); s != "" {
stmts = append(stmts, s)
}
}
start, i := 0, 0
for i < len(src) {
c := src[i]
switch {
case c == '\'' || c == '"' || c == '`':
i = skipQuoted(src, i)
case strings.HasPrefix(src[i:], "--") || strings.HasPrefix(src[i:], "#"):
end := strings.IndexByte(src[i:], '\n')
if end < 0 {
i = len(src)
} else {
i += end
}
case strings.HasPrefix(src[i:], "/*"):
end := strings.Index(src[i:], "*/")
if end < 0 {
i = len(src)
} else {
i += end + 2
}
case c == ';':
flush(src[start:i])
start = i + 1
i++
default:
i++
}
}
flush(src[start:])
return stmts
}
// column is what INFORMATION_SCHEMA says about a column of the case's
// database: its type as the schema declared it, and whether it can be
// NULL.
type column struct {
name string
typ *analysis.TypeExpr
nullable bool
}
// analyzer holds the session a case is compiled in and the catalog of the
// case's database, keyed by table name in the database's own schema.
type analyzer struct {
s *server
session string
catalog map[string][]column
keys map[string][]string // the primary key columns of each table
}
var dbNameRe = regexp.MustCompile(`[^a-z0-9_]+`)
// Analyze creates a database of the case's own from its schema, writes its
// fixture there, compiles its queries and returns what Spanner reports in
// the JSON shape sqlc analyze prints.
func Analyze(ctx context.Context, endpoint string, c endtoend.Case) ([]byte, error) {
s, err := open(ctx, endpoint)
if err != nil {
return nil, err
}
defer s.Close()
schema, err := os.ReadFile(c.Schema)
if err != nil {
return nil, err
}
var fixture []byte
if c.Fixture != "" {
if fixture, err = os.ReadFile(c.Fixture); err != nil {
return nil, err
}
}
queries, err := c.Queries()
if err != nil {
return nil, err
}
// A database id is lower-case letters, digits and underscores, at
// most 30 characters: the case's name, cut to fit, and a hash of the
// whole of it, so that two cases with the same head stay apart.
hash := fmt.Sprintf("%08x", crc32.ChecksumIEEE([]byte(c.Name)))
name := "goldeneye_" + dbNameRe.ReplaceAllString(strings.ToLower(c.Name), "_")
if max := 30 - 1 - len(hash); len(name) > max {
name = name[:max]
}
name = strings.TrimRight(name, "_") + "_" + hash
db := Instance + "/databases/" + name
if err := s.dropDatabase(ctx, db); err != nil {
return nil, err
}
if _, err := s.createDatabase(ctx, name, splitStatements(string(schema))); err != nil {
return nil, fmt.Errorf("loading %s: %w", c.Schema, err)
}
defer s.dropDatabase(context.WithoutCancel(ctx), db)
session, err := s.session(ctx, db)
if err != nil {
return nil, err
}
if stmts := splitStatements(string(fixture)); len(stmts) > 0 {
if err := s.write(ctx, session, stmts); err != nil {
return nil, fmt.Errorf("loading %s: %w", c.Fixture, err)
}
}
a := &analyzer{s: s, session: session}
if err := a.readCatalog(ctx); err != nil {
return nil, err
}
out := make([]analysis.Query, 0, len(queries))
for _, q := range queries {
aq, err := a.analyzeQuery(ctx, q)
if err != nil {
return nil, fmt.Errorf("%s: %w", q.Name, err)
}
out = append(out, aq)
}
return analysis.Encode(out)
}
// Check compares what Spanner reports for a case with the output the
// case committed, returning a diff when they differ.
func Check(ctx context.Context, endpoint string, c endtoend.Case) (string, error) {
got, err := Analyze(ctx, endpoint, c)
if err != nil {
return "", err
}
return c.Compare(got)
}
// write runs DML statements in a read-write transaction and commits it.
func (s *server) write(ctx context.Context, session string, stmts []string) error {
tx, err := s.data.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{
Session: session,
Options: &spannerpb.TransactionOptions{Mode: &spannerpb.TransactionOptions_ReadWrite_{ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}}},
})
if err != nil {
return err
}
req := &spannerpb.ExecuteBatchDmlRequest{
Session: session,
Transaction: &spannerpb.TransactionSelector{Selector: &spannerpb.TransactionSelector_Id{Id: tx.Id}},
Seqno: 1,
}
for _, stmt := range stmts {
req.Statements = append(req.Statements, &spannerpb.ExecuteBatchDmlRequest_Statement{Sql: stmt})
}
resp, err := s.data.ExecuteBatchDml(ctx, req)
if err == nil && resp.Status != nil && resp.Status.Code != 0 {
err = fmt.Errorf("%s", resp.Status.Message)
}
if err != nil {
s.data.Rollback(context.WithoutCancel(ctx), &spannerpb.RollbackRequest{Session: session, TransactionId: tx.Id})
return err
}
_, err = s.data.Commit(ctx, &spannerpb.CommitRequest{
Session: session,
Transaction: &spannerpb.CommitRequest_TransactionId{TransactionId: tx.Id},
})
return err
}
const catalogQuery = `
SELECT TABLE_NAME, COLUMN_NAME, SPANNER_TYPE, IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = ''
ORDER BY TABLE_NAME, ORDINAL_POSITION`
const keysQuery = `
SELECT TABLE_NAME, COLUMN_NAME
FROM INFORMATION_SCHEMA.INDEX_COLUMNS
WHERE TABLE_SCHEMA = '' AND INDEX_TYPE = 'PRIMARY_KEY'
ORDER BY TABLE_NAME, ORDINAL_POSITION`
// readCatalog reads the tables the schema created, and their keys.
func (a *analyzer) readCatalog(ctx context.Context) error {
rows, err := a.s.query(ctx, a.session, catalogQuery)
if err != nil {
return err
}
a.catalog = map[string][]column{}
for _, row := range rows {
table := row[0].GetStringValue()
a.catalog[table] = append(a.catalog[table], column{
name: row[1].GetStringValue(),
typ: parseType(row[2].GetStringValue()),
nullable: row[3].GetStringValue() == "YES",
})
}
rows, err = a.s.query(ctx, a.session, keysQuery)
if err != nil {
return err
}
a.keys = map[string][]string{}
for _, row := range rows {
table := row[0].GetStringValue()
a.keys[table] = append(a.keys[table], row[1].GetStringValue())
}
return nil
}
// tableName returns the catalog's spelling of a table's name, which
// Spanner matches in any case.
func (a *analyzer) tableName(table string) string {
for t := range a.catalog {
if strings.EqualFold(t, table) {
return t
}
}
return table
}
// lookup finds a column of a table of the case's database. Spanner
// matches table and column names in any case.
func (a *analyzer) lookup(table, name string) (column, bool) {
for t, cols := range a.catalog {
if !strings.EqualFold(t, table) {
continue
}
for _, col := range cols {
if strings.EqualFold(col.name, name) {
return col, true
}
}
}
return column{}, false
}
// parseType reads a type spelled the way SPANNER_TYPE and a declaration
// spell it — STRING(10), ARRAY<STRING(MAX)>, STRUCT<a INT64, b STRING> —
// into an expression, in lower case, with MAX an identifier.
func parseType(s string) *analysis.TypeExpr {
s = strings.TrimSpace(s)
lower := strings.ToLower(s)
if element, ok := strings.CutPrefix(lower, "array<"); ok && strings.HasSuffix(element, ">") {
return &analysis.TypeExpr{Name: "array", Args: []analysis.TypeArg{{Type: parseType(s[6 : len(s)-1])}}}
}
for _, kind := range []string{"proto", "enum"} {
if message, ok := strings.CutPrefix(lower, kind+"<"); ok && strings.HasSuffix(message, ">") {
m := s[len(kind)+1 : len(s)-1]
return &analysis.TypeExpr{Name: kind, Args: []analysis.TypeArg{{String: &m}}}
}
}
if fields, ok := strings.CutPrefix(lower, "struct<"); ok && strings.HasSuffix(fields, ">") {
t := &analysis.TypeExpr{Name: "struct"}
for _, f := range splitTop(s[7:len(s)-1], ',') {
f = strings.TrimSpace(f)
label, typ := "", f
if i := strings.IndexAny(f, " \t"); i > 0 && !strings.ContainsAny(f[:i], "<(") {
label, typ = f[:i], strings.TrimSpace(f[i+1:])
}
t.Args = append(t.Args, analysis.TypeArg{Label: label, Type: parseType(typ)})
}
return t
}
name, args := lower, ""
if open := strings.IndexByte(lower, '('); open >= 0 && strings.HasSuffix(lower, ")") {
name, args = strings.TrimSpace(lower[:open]), lower[open+1:len(lower)-1]
}
t := &analysis.TypeExpr{Name: name}
if args == "" {
return t
}
for _, arg := range strings.Split(args, ",") {
arg = strings.TrimSpace(arg)
if n, err := strconv.ParseInt(arg, 10, 64); err == nil {
t.Args = append(t.Args, analysis.TypeArg{Int: &n})
} else {
ident := arg
t.Args = append(t.Args, analysis.TypeArg{Ident: &ident})
}
}
return t
}
// splitTop splits on a separator outside angle brackets and parentheses.
func splitTop(s string, sep byte) []string {
var out []string
depth, start := 0, 0
for i := 0; i < len(s); i++ {
switch s[i] {
case '<', '(':
depth++
case '>', ')':
depth--
case sep:
if depth == 0 {
out = append(out, s[start:i])
start = i + 1
}
}
}
return append(out, s[start:])
}
// typeOf reads a type the way the wire spells it, which names the family
// and, for an array or a struct, what it holds, and nothing of a length.
func typeOf(t *spannerpb.Type) *analysis.TypeExpr {
if t == nil {
return nil
}
switch t.Code {
case spannerpb.TypeCode_ARRAY:
return &analysis.TypeExpr{Name: "array", Args: []analysis.TypeArg{{Type: typeOf(t.ArrayElementType)}}}
case spannerpb.TypeCode_STRUCT:
out := &analysis.TypeExpr{Name: "struct"}
if t.StructType != nil {
for _, f := range t.StructType.Fields {
out.Args = append(out.Args, analysis.TypeArg{Label: f.Name, Type: typeOf(f.Type)})
}
}
return out
case spannerpb.TypeCode_PROTO, spannerpb.TypeCode_ENUM:
// A proto or enum is named by its message, the way a declaration
// spells PROTO<p.M>.
fqn := t.ProtoTypeFqn
return &analysis.TypeExpr{Name: strings.ToLower(t.Code.String()), Args: []analysis.TypeArg{{String: &fqn}}}
}
return &analysis.TypeExpr{Name: strings.ToLower(t.Code.String())}
}
// withNullable copies a type with its nullability set.
func withNullable(t *analysis.TypeExpr, nullable bool) *analysis.TypeExpr {
if t == nil {
return nil
}
out := *t
out.Nullable = nullable
return &out
}
// isDML reports whether a statement writes, and so has to be compiled in a
// read-write transaction, which is begun for it and never committed. The
// statement's first word decides, past any comment it opens with.
func isDML(sql string) bool {
i := 0
for i < len(sql) {
switch {
case sql[i] == ' ' || sql[i] == '\t' || sql[i] == '\n' || sql[i] == '\r':
i++
case strings.HasPrefix(sql[i:], "--") || strings.HasPrefix(sql[i:], "#"):
end := strings.IndexByte(sql[i:], '\n')
if end < 0 {
return false
}
i += end
case strings.HasPrefix(sql[i:], "/*"):
end := strings.Index(sql[i:], "*/")
if end < 0 {
return false
}
i += end + 2
default:
head := strings.ToLower(sql[i:])
for _, kw := range []string{"insert", "update", "delete"} {
if strings.HasPrefix(head, kw) {
return true
}
}
return false
}
}
return false
}
// compile compiles a statement in PLAN mode and returns what the server
// reports about it.
func (a *analyzer) compile(ctx context.Context, sql string) (*spannerpb.ResultSet, error) {
req := &spannerpb.ExecuteSqlRequest{
Session: a.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 := a.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 {
a.s.data.Rollback(context.WithoutCancel(ctx), &spannerpb.RollbackRequest{Session: a.session, TransactionId: rs.Metadata.Transaction.Id})
}
return rs, nil
}
// analyzeQuery compiles one query.
func (a *analyzer) analyzeQuery(ctx context.Context, q endtoend.Query) (analysis.Query, error) {
sql, phs := bind(q.SQL)
aq := analysis.Query{
Name: q.Name,
Cmd: q.Cmd,
Columns: []analysis.Column{},
Params: []analysis.Param{},
}
rs, err := a.compile(ctx, sql)
if err != nil {
return analysis.Query{}, err
}
p := newPlan(rs.Stats.GetQueryPlan())
partners := p.partners()
describe := func(o origin) (analysis.Column, bool) {
if o.param != "" {
o = partners[o.param]
}
if o.table == "" {
return analysis.Column{}, false
}
col, ok := a.lookup(o.table, o.column)
if !ok {
return analysis.Column{}, false
}
return analysis.Column{Name: col.name, Type: withNullable(col.typ, col.nullable), Table: a.tableName(o.table)}, true
}
var fields []*spannerpb.StructType_Field
if rs.Metadata != nil && rs.Metadata.RowType != nil {
fields = rs.Metadata.RowType.Fields
}
outputs := p.outputs()
table, operation := p.mutation()
if table != "" {
// A DML plan lists the columns it returns, then the values it
// writes: each of those is a parameter standing for the column
// it is written to.
written := a.written(sql, table, operation)
writes := outputs[min(len(fields), len(outputs)):]
for i, w := range written {
if i >= len(writes) {
break
}
if o := p.resolve(writes[i], map[int32]bool{}); o.param != "" {
if _, ok := partners[o.param]; !ok {
partners[o.param] = origin{table: table, column: w}
}
}
}
outputs = outputs[:min(len(fields), len(outputs))]
}
for i, f := range fields {
ac := analysis.Column{Name: f.Name, Type: typeOf(f.Type)}
var out *spannerpb.PlanNode
if i < len(outputs) {
out = outputs[i]
}
if col, ok := describe(p.resolve(out, map[int32]bool{})); ok {
ac.Type, ac.Table = col.Type, col.Table
} else if table != "" && (out == nil || out.DisplayName != "Function") {
// A THEN RETURN column the plan reads as a constant, such
// as the default a column not inserted takes, is the table's
// column of that name.
if col, ok := a.lookup(table, f.Name); ok {
ac.Type = withNullable(col.typ, col.nullable)
ac.Table = a.tableName(table)
}
}
aq.Columns = append(aq.Columns, ac)
}
declared := map[string]*spannerpb.Type{}
if rs.Metadata != nil && rs.Metadata.UndeclaredParameters != nil {
for _, f := range rs.Metadata.UndeclaredParameters.Fields {
declared[f.Name] = f.Type
}
}
for _, ph := range phs {
ac, ok := describe(origin{param: ph.Name})
if !ok {
ac = analysis.Column{Type: typeOf(declared[ph.Name])}
}
aq.Params = append(aq.Params, analysis.Param{Number: ph.Number, Column: ac})
}
return aq, nil
}
var (
insertRe = regexp.MustCompile("(?is)^insert\\s+(?:or\\s+\\w+\\s+)?into\\s+[\\w.`]+\\s*(?:\\(([^)]*)\\))?")
setRe = regexp.MustCompile("(?is)^update\\s+[\\w.`]+(?:\\s+(?:as\\s+)?\\w+)?\\s+set\\s+")
)
// written lists the columns a DML statement writes, in the order the plan
// lists their values: for an INSERT the columns it inserts, which are the
// statement's column list or every column of the table, and for an
// UPDATE or DELETE the table's key columns, then the columns an UPDATE
// sets.
func (a *analyzer) written(sql, table, operation string) []string {
switch operation {
case "INSERT":
m := insertRe.FindStringSubmatch(sql)
if m == nil {
return nil
}
if strings.TrimSpace(m[1]) == "" {
var cols []string
for _, col := range a.catalog[a.tableName(table)] {
cols = append(cols, col.name)
}
return cols
}
var cols []string
for _, c := range strings.Split(m[1], ",") {
cols = append(cols, strings.Trim(strings.TrimSpace(c), "`"))
}
return cols
case "UPDATE":
cols := append([]string(nil), a.keys[a.tableName(table)]...)
if m := setRe.FindStringIndex(sql); m != nil {
for _, assignment := range setList(sql[m[1]:]) {
target, _, _ := strings.Cut(assignment, "=")
target = strings.TrimSpace(target)
if i := strings.LastIndexByte(target, '.'); i >= 0 {
target = target[i+1:]
}
cols = append(cols, strings.Trim(target, "`"))
}
}
return cols
case "DELETE":
return a.keys[a.tableName(table)]
}
return nil
}
// setList splits an UPDATE's SET list into its assignments: the text up
// to the WHERE outside strings and parentheses, split on the commas
// outside them.
func setList(s string) []string {
var out []string
depth, start := 0, 0
for i := 0; i < len(s); i++ {
c := s[i]
switch {
case c == '\'' || c == '"' || c == '`':
i = skipQuoted(s, i) - 1
case c == '(':
depth++
case c == ')':
depth--
case c == ',' && depth == 0:
out = append(out, s[start:i])
start = i + 1
case depth == 0 && (i == 0 || !isWordByte(s[i-1])) && len(s)-i >= 5 && strings.EqualFold(s[i:i+5], "where") && (len(s) == i+5 || !isWordByte(s[i+5])):
return append(out, s[start:i])
}
}
return append(out, s[start:])
}

View File

@@ -0,0 +1,220 @@
package spanner
import (
"regexp"
"strings"
"cloud.google.com/go/spanner/apiv1/spannerpb"
)
// The query plan is the one thing that says where a result column comes
// from and what a parameter stands for. It is a list of nodes: relational
// ones, such as a Scan of a table or the Serialize Result that produces the
// rows, and scalar ones, such as a Reference to a column or a variable, a
// Parameter, a Function. A relational node defines variables through its
// child links: a Scan of a table defines one per column it reads, named
// after the column, and any node may define one as another scalar, which
// a Reference names with a $. The children of Serialize Result after the
// relation it serializes are the result columns, in order; a DML plan
// lists its THEN RETURN columns first and the values it writes after
// them: for an INSERT the columns it inserts, in the statement's order,
// and for an UPDATE or DELETE the key columns of the table, then the
// columns an UPDATE sets. A comparison is a Function whose description
// reads ($col = @param).
// origin is what a scalar of the plan resolves to: a table column, a
// parameter, or nothing.
type origin struct {
table, column string
param string
}
// plan is a parsed query plan.
type plan struct {
nodes []*spannerpb.PlanNode
// vars maps a variable to the node that defines it and the scalar it
// is defined as.
vars map[string]definition
}
type definition struct {
owner *spannerpb.PlanNode
child *spannerpb.PlanNode
}
func newPlan(qp *spannerpb.QueryPlan) *plan {
p := &plan{vars: map[string]definition{}}
if qp == nil {
return p
}
p.nodes = qp.PlanNodes
for _, n := range p.nodes {
for _, l := range n.ChildLinks {
if l.Variable != "" {
p.vars[l.Variable] = definition{owner: n, child: p.node(l.ChildIndex)}
}
}
}
return p
}
func (p *plan) node(index int32) *spannerpb.PlanNode {
for _, n := range p.nodes {
if n.Index == index {
return n
}
}
return nil
}
func (p *plan) root() *spannerpb.PlanNode {
return p.node(0)
}
func meta(n *spannerpb.PlanNode, key string) string {
if n == nil || n.Metadata == nil {
return ""
}
if v, ok := n.Metadata.Fields[key]; ok {
return v.GetStringValue()
}
return ""
}
func description(n *spannerpb.PlanNode) string {
if n == nil || n.ShortRepresentation == nil {
return ""
}
return n.ShortRepresentation.Description
}
// serializeResult finds the node that produces the rows, the first
// Serialize Result reached from the root.
func (p *plan) serializeResult() *spannerpb.PlanNode {
var found *spannerpb.PlanNode
seen := map[int32]bool{}
var walk func(n *spannerpb.PlanNode)
walk = func(n *spannerpb.PlanNode) {
if n == nil || found != nil || seen[n.Index] {
return
}
seen[n.Index] = true
if n.DisplayName == "Serialize Result" {
found = n
return
}
for _, l := range n.ChildLinks {
walk(p.node(l.ChildIndex))
}
}
walk(p.root())
return found
}
// outputs lists the scalars Serialize Result produces, in order: every
// child after the relation it serializes.
func (p *plan) outputs() []*spannerpb.PlanNode {
sr := p.serializeResult()
if sr == nil {
return nil
}
var out []*spannerpb.PlanNode
for _, l := range sr.ChildLinks {
if c := p.node(l.ChildIndex); c != nil && c.Kind == spannerpb.PlanNode_SCALAR {
out = append(out, c)
}
}
return out
}
// mutation names the table a DML plan writes, or "" for a query.
func (p *plan) mutation() (table, operation string) {
for _, n := range p.nodes {
if n.DisplayName == "Apply Mutations" {
return meta(n, "table"), meta(n, "operation_type")
}
}
return "", ""
}
// resolve follows a scalar to what it stands for.
func (p *plan) resolve(n *spannerpb.PlanNode, seen map[int32]bool) origin {
if n == nil || seen[n.Index] {
return origin{}
}
seen[n.Index] = true
switch n.DisplayName {
case "Parameter":
return origin{param: meta(n, "name")}
case "Reference":
desc := description(n)
if name, ok := strings.CutPrefix(desc, "$"); ok {
return p.resolveVar(name, seen)
}
// A bare name is a column of the scan it is read by, or an input
// of a union, which names it in the link's type.
for _, m := range p.nodes {
for _, l := range m.ChildLinks {
if l.ChildIndex != n.Index {
continue
}
switch {
case m.DisplayName == "Scan" && meta(m, "scan_type") == "TableScan":
return origin{table: meta(m, "scan_target"), column: desc}
case m.DisplayName == "Scan":
// A batch scan reads the batch variable of that name.
target := strings.TrimPrefix(meta(m, "scan_target"), "$")
return p.resolveVar(target+".Batch."+desc, seen)
}
}
}
for _, m := range p.nodes {
if m.DisplayName != "Union Input" {
continue
}
for _, l := range m.ChildLinks {
if l.Type == desc {
return p.resolve(p.node(l.ChildIndex), seen)
}
}
}
}
return origin{}
}
// resolveVar follows a variable to what it is defined as.
func (p *plan) resolveVar(name string, seen map[int32]bool) origin {
d, ok := p.vars[name]
if !ok {
return origin{}
}
return p.resolve(d.child, seen)
}
var compareRe = regexp.MustCompile(`^\((.+) (=|!=|<>|<|<=|>|>=) (.+)\)$`)
// partners finds the table column each parameter is compared with, keyed
// by parameter name: the other operand of a comparison Function the
// parameter is an operand of. The first found wins.
func (p *plan) partners() map[string]origin {
out := map[string]origin{}
for _, n := range p.nodes {
if n.DisplayName != "Function" || len(n.ChildLinks) != 2 || !compareRe.MatchString(description(n)) {
continue
}
sides := []*spannerpb.PlanNode{p.node(n.ChildLinks[0].ChildIndex), p.node(n.ChildLinks[1].ChildIndex)}
for i, side := range sides {
o := p.resolve(side, map[int32]bool{})
if o.param == "" {
continue
}
other := p.resolve(sides[1-i], map[int32]bool{})
if other.table != "" {
if _, ok := out[o.param]; !ok {
out[o.param] = other
}
}
}
}
return out
}

View File

@@ -0,0 +1,96 @@
package spanner
import (
"context"
"fmt"
"strings"
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
)
// relationQuery lists the relations of every schema but the database's
// own, unnamed one — INFORMATION_SCHEMA and SPANNER_SYS — with their
// columns in declared order, so that the output is stable. A column's
// type is spelled the way SPANNER_TYPE spells a declaration: STRING(MAX),
// ARRAY<INT64>.
const relationQuery = `
SELECT t.TABLE_SCHEMA, t.TABLE_NAME, t.TABLE_TYPE,
c.COLUMN_NAME, c.SPANNER_TYPE, c.IS_NULLABLE
FROM INFORMATION_SCHEMA.TABLES AS t
JOIN INFORMATION_SCHEMA.COLUMNS AS c
ON c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME
WHERE t.TABLE_SCHEMA != ''
ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME, c.ORDINAL_POSITION`
// readRelations reads the views of the system schemas. Their names are
// kept as the catalog spells them, in upper case, which is how a query
// names them; a column's type is spelled in lower case, the way the
// dialect's types.jsonl spells its types, with an ARRAY<T> written as T
// with the array flag, since that is how a seed spells an array.
func readRelations(ctx context.Context, s *server, session string) ([]dialect.Relation, error) {
rows, err := s.query(ctx, session, relationQuery)
if err != nil {
return nil, err
}
var relations []dialect.Relation
var cur *dialect.Relation
for _, row := range rows {
if len(row) != 6 {
return nil, fmt.Errorf("expected 6 columns, got %d", len(row))
}
schema, name, kind := row[0].GetStringValue(), row[1].GetStringValue(), row[2].GetStringValue()
if cur == nil || cur.Schema != schema || cur.Name != name {
rel := dialect.Relation{Schema: schema, Name: name}
if kind == "VIEW" {
rel.Kind = "v"
}
relations = append(relations, rel)
cur = &relations[len(relations)-1]
}
col := dialect.Column{
Name: row[3].GetStringValue(),
NotNull: row[5].GetStringValue() == "NO",
}
col.Type, col.Array = typeName(row[4].GetStringValue())
cur.Columns = append(cur.Columns, col)
}
return relations, nil
}
// typeName spells a SPANNER_TYPE the way a seed spells a column's type:
// in lower case, with an ARRAY<T> as its element and the array flag, a
// STRUCT<a T, b U> as struct(a: t, b: u) and a PROTO<p.M> or ENUM<p.E> as
// proto('p.M') or enum('p.E'), since a seed spells a type's arguments in
// parentheses.
func typeName(spannerType string) (string, bool) {
t := strings.TrimSpace(spannerType)
if element, ok := strings.CutPrefix(t, "ARRAY<"); ok && strings.HasSuffix(element, ">") {
name, _ := typeName(strings.TrimSuffix(element, ">"))
return name, true
}
if fields, ok := strings.CutPrefix(t, "STRUCT<"); ok && strings.HasSuffix(fields, ">") {
var args []string
for _, f := range splitTop(strings.TrimSuffix(fields, ">"), ',') {
f = strings.TrimSpace(f)
label, typ := "", f
if i := strings.IndexAny(f, " \t"); i > 0 && !strings.ContainsAny(f[:i], "<(") {
label, typ = f[:i], strings.TrimSpace(f[i+1:])
}
name, array := typeName(typ)
if array {
name += "[]"
}
if label != "" {
name = label + ": " + name
}
args = append(args, name)
}
return "struct(" + strings.Join(args, ", ") + ")", false
}
for _, kind := range []string{"PROTO<", "ENUM<"} {
if message, ok := strings.CutPrefix(t, kind); ok && strings.HasSuffix(message, ">") {
return strings.ToLower(strings.TrimSuffix(kind, "<")) + "('" + strings.TrimSuffix(message, ">") + "')", false
}
}
return strings.ToLower(t), false
}

View File

@@ -0,0 +1,224 @@
// Package spanner generates the GoogleSQL dialect seed under
// internal/engine/googlesql/dialect from a live Spanner server, and
// verifies the GoogleSQL analyze cases under internal/endtoend/testdata
// against the same server. The server is Spanner Omni, the downloadable
// Spanner, run from its container image; the package writes into the
// 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.
//
// 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
// written there, and each query is compiled in PLAN mode, which runs
// nothing and reports the type of each result column and of each
// parameter the query leaves undeclared, and the query plan, which says
// which table column each result column and parameter stands for. The
// answer is printed in the JSON shape sqlc analyze prints and compared
// with the case's committed stdout.json byte for byte.
//
// The server is named by SPANNER_SERVER_URI, the gRPC endpoint of a
// Spanner Omni server such as localhost:15000, which is reached without
// TLS or credentials, as Omni is. Databases are created in the instance
// Omni's single server provides, projects/default/instances/default.
package spanner
import (
"context"
"errors"
"fmt"
"os"
"strings"
database "cloud.google.com/go/spanner/admin/database/apiv1"
"cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
instance "cloud.google.com/go/spanner/admin/instance/apiv1"
"cloud.google.com/go/spanner/admin/instance/apiv1/instancepb"
spannerapi "cloud.google.com/go/spanner/apiv1"
"cloud.google.com/go/spanner/apiv1/spannerpb"
"google.golang.org/api/option"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
)
// Engine is the name goldeneye knows the database by.
const Engine = "spanner"
// Dir is the name of the engine directory the dialect lives under, and
// Cases the name of the analyze case directories under
// internal/endtoend/testdata: both are named after the language, GoogleSQL,
// which is how sqlc knows the dialect.
const (
Dir = "googlesql"
Cases = "googlesql"
)
// Instance is the instance a Spanner Omni single server provides, which
// every database is created in.
const Instance = "projects/default/instances/default"
// Locate returns the server to generate from, named by SPANNER_SERVER_URI.
func Locate() (string, error) {
endpoint := os.Getenv("SPANNER_SERVER_URI")
if endpoint == "" {
return "", errors.New("SPANNER_SERVER_URI is not set")
}
if strings.Contains(endpoint, "://") {
return "", fmt.Errorf("SPANNER_SERVER_URI: %q should be a host:port, the gRPC endpoint of a Spanner Omni server", endpoint)
}
return endpoint, nil
}
// server holds the clients a run talks to the server through.
type server struct {
endpoint string
instances *instance.InstanceAdminClient
databases *database.DatabaseAdminClient
data *spannerapi.Client
}
// open connects to the server. Omni serves plaintext gRPC and asks for no
// credentials.
func open(ctx context.Context, endpoint string) (*server, error) {
opts := []option.ClientOption{
option.WithEndpoint(endpoint),
option.WithoutAuthentication(),
option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
}
s := &server{endpoint: endpoint}
var err error
if s.instances, err = instance.NewInstanceAdminClient(ctx, opts...); err != nil {
return nil, err
}
if s.databases, err = database.NewDatabaseAdminClient(ctx, opts...); err != nil {
s.Close()
return nil, err
}
if s.data, err = spannerapi.NewClient(ctx, opts...); err != nil {
s.Close()
return nil, err
}
return s, nil
}
func (s *server) Close() {
if s.instances != nil {
s.instances.Close()
}
if s.databases != nil {
s.databases.Close()
}
if s.data != nil {
s.data.Close()
}
}
// Version describes the server: Spanner Omni reports no release of its
// own through the API, so the instance the databases are created in is
// described instead.
func Version(ctx context.Context, endpoint string) (string, error) {
s, err := open(ctx, endpoint)
if err != nil {
return "", err
}
defer s.Close()
inst, err := s.instances.GetInstance(ctx, &instancepb.GetInstanceRequest{Name: Instance})
if err != nil {
return "", fmt.Errorf("%s: %w", Instance, err)
}
return fmt.Sprintf("Spanner Omni at %s, instance %s (%s)", endpoint, inst.Name, inst.State), nil
}
// createDatabase creates a database in the instance, running the DDL
// statements in it, and returns its full name.
func (s *server) createDatabase(ctx context.Context, name string, ddl []string) (string, error) {
op, err := s.databases.CreateDatabase(ctx, &databasepb.CreateDatabaseRequest{
Parent: Instance,
CreateStatement: "CREATE DATABASE `" + name + "`",
ExtraStatements: ddl,
})
if err != nil {
return "", err
}
db, err := op.Wait(ctx)
if err != nil {
return "", err
}
return db.Name, nil
}
// dropDatabase drops a database by its full name. A database that does
// not exist is not an error, so that a run can clear the way for itself.
func (s *server) dropDatabase(ctx context.Context, db string) error {
err := s.databases.DropDatabase(ctx, &databasepb.DropDatabaseRequest{Database: db})
if status.Code(err) == codes.NotFound && strings.Contains(err.Error(), "Database") {
return nil
}
return err
}
// session opens a session on a database, multiplexed as Omni requires.
func (s *server) session(ctx context.Context, db string) (string, error) {
sess, err := s.data.CreateSession(ctx, &spannerpb.CreateSessionRequest{
Database: db,
Session: &spannerpb.Session{Multiplexed: true},
})
if err != nil {
return "", err
}
return sess.Name, nil
}
// query runs a statement and returns its rows as lists of values.
func (s *server) query(ctx context.Context, session, sql string) ([][]*structpb.Value, error) {
rs, err := s.data.ExecuteSql(ctx, &spannerpb.ExecuteSqlRequest{Session: session, Sql: sql})
if err != nil {
return nil, err
}
var rows [][]*structpb.Value
for _, row := range rs.Rows {
rows = append(rows, row.Values)
}
return rows, nil
}
// 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.
func Generate(ctx context.Context, endpoint string) (dialect.Files, error) {
s, err := open(ctx, endpoint)
if err != nil {
return nil, err
}
defer s.Close()
db := Instance + "/databases/goldeneye_dialect"
if err := s.dropDatabase(ctx, db); err != nil {
return nil, err
}
if _, err := s.createDatabase(ctx, "goldeneye_dialect", nil); err != nil {
return nil, err
}
defer s.dropDatabase(context.WithoutCancel(ctx), db)
session, err := s.session(ctx, db)
if err != nil {
return nil, err
}
relations, err := readRelations(ctx, s, session)
if err != nil {
return nil, err
}
blob, err := dialect.JSONL(relations)
if err != nil {
return nil, err
}
return dialect.Files{dialect.RelationsFile: blob}, nil
}

View File

@@ -0,0 +1,66 @@
package spanner
import (
"context"
"testing"
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
"github.com/sqlc-dev/sqlc/internal/goldeneye/endtoend"
)
// TestDialect verifies the committed GoogleSQL dialect against what a
// Spanner server reports. It skips unless SPANNER_SERVER_URI names one.
func TestDialect(t *testing.T) {
endpoint, err := Locate()
if err != nil {
t.Skip(err)
}
ctx := context.Background()
version, err := Version(ctx, endpoint)
if err != nil {
t.Fatal(err)
}
files, err := Generate(ctx, endpoint)
if err != nil {
t.Fatal(err)
}
dir, err := dialect.Dir(Dir)
if err != nil {
t.Fatal(err)
}
report, err := dialect.Check(dir, files)
if err != nil {
t.Fatal(err)
}
if report != "" {
t.Errorf("%s does not match what %s reports:\n%s", dir, version, report)
}
}
// TestAnalyzeCases verifies every GoogleSQL analyze case under
// internal/endtoend/testdata against what a Spanner server reports. It
// skips unless SPANNER_SERVER_URI names one.
func TestAnalyzeCases(t *testing.T) {
endpoint, err := Locate()
if err != nil {
t.Skip(err)
}
cases, err := endtoend.Cases(Cases)
if err != nil {
t.Fatal(err)
}
if len(cases) == 0 {
t.Fatal("no googlesql analyze cases found")
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
diff, err := Check(context.Background(), endpoint, c)
if err != nil {
t.Fatal(err)
}
if diff != "" {
t.Errorf("%s does not match what Spanner reports (-committed +spanner):\n%s", c.Output, diff)
}
})
}
}