Files
upstream-mirror 0f4df23290
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
snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
2026-09-16 02:17:34 +05:00

225 lines
7.5 KiB
Go

// 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
}