Chore: Add initial/experimental xorm spanner driver (#101398)
* make it build and start * run some migrations * add build tags, remove log * remove unused code * revert go.mod changes * move initialisation into dialect file * update workspace * update workspace once again * clean up dependencies * further cleanup * Address some review feedback. * Fix go.sum. --------- Co-authored-by: Peter Štibraný <pstibrany@gmail.com>
This commit is contained in:
co-authored by
Peter Štibraný
parent
4bab25054f
commit
165bca6417
@@ -0,0 +1,284 @@
|
||||
//go:build enterprise || pro
|
||||
|
||||
package xorm
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
_ "github.com/googleapis/go-sql-spanner"
|
||||
"xorm.io/core"
|
||||
)
|
||||
|
||||
func init() {
|
||||
core.RegisterDriver("spanner", &spannerDriver{})
|
||||
core.RegisterDialect("spanner", func() core.Dialect { return &spanner{} })
|
||||
}
|
||||
|
||||
// https://cloud.google.com/spanner/docs/reference/standard-sql/lexical#reserved_keywords
|
||||
var spannerReservedKeywords = map[string]struct{}{
|
||||
"ALL": {},
|
||||
"AND": {},
|
||||
"ANY": {},
|
||||
"ARRAY": {},
|
||||
"AS": {},
|
||||
"ASC": {},
|
||||
"ASSERT_ROWS_MODIFIED": {},
|
||||
"AT": {},
|
||||
"BETWEEN": {},
|
||||
"BY": {},
|
||||
"CASE": {},
|
||||
"CAST": {},
|
||||
"COLLATE": {},
|
||||
"CONTAINS": {},
|
||||
"CREATE": {},
|
||||
"CROSS": {},
|
||||
"CUBE": {},
|
||||
"CURRENT": {},
|
||||
"DEFAULT": {},
|
||||
"DEFINE": {},
|
||||
"DESC": {},
|
||||
"DISTINCT": {},
|
||||
"ELSE": {},
|
||||
"END": {},
|
||||
"ENUM": {},
|
||||
"ESCAPE": {},
|
||||
"EXCEPT": {},
|
||||
"EXCLUDE": {},
|
||||
"EXISTS": {},
|
||||
"EXTRACT": {},
|
||||
"FALSE": {},
|
||||
"FETCH": {},
|
||||
"FOLLOWING": {},
|
||||
"FOR": {},
|
||||
"FROM": {},
|
||||
"FULL": {},
|
||||
"GROUP": {},
|
||||
"GROUPING": {},
|
||||
"GROUPS": {},
|
||||
"HASH": {},
|
||||
"HAVING": {},
|
||||
"IF": {},
|
||||
"IGNORE": {},
|
||||
"IN": {},
|
||||
"INNER": {},
|
||||
"INTERSECT": {},
|
||||
"INTERVAL": {},
|
||||
"INTO": {},
|
||||
"IS": {},
|
||||
"JOIN": {},
|
||||
"LATERAL": {},
|
||||
"LEFT": {},
|
||||
"LIKE": {},
|
||||
"LIMIT": {},
|
||||
"LOOKUP": {},
|
||||
"MERGE": {},
|
||||
"NATURAL": {},
|
||||
"NEW": {},
|
||||
"NO": {},
|
||||
"NOT": {},
|
||||
"NULL": {},
|
||||
"NULLS": {},
|
||||
"OF": {},
|
||||
"ON": {},
|
||||
"OR": {},
|
||||
"ORDER": {},
|
||||
"OUTER": {},
|
||||
"OVER": {},
|
||||
"PARTITION": {},
|
||||
"PRECEDING": {},
|
||||
"PROTO": {},
|
||||
"RANGE": {},
|
||||
"RECURSIVE": {},
|
||||
"RESPECT": {},
|
||||
"RIGHT": {},
|
||||
"ROLLUP": {},
|
||||
"ROWS": {},
|
||||
"SELECT": {},
|
||||
"SET": {},
|
||||
"SOME": {},
|
||||
"STRUCT": {},
|
||||
"TABLESAMPLE": {},
|
||||
"THEN": {},
|
||||
"TO": {},
|
||||
"TREAT": {},
|
||||
"TRUE": {},
|
||||
"UNBOUNDED": {},
|
||||
"UNION": {},
|
||||
"UNNEST": {},
|
||||
"USING": {},
|
||||
"WHEN": {},
|
||||
"WHERE": {},
|
||||
"WINDOW": {},
|
||||
"WITH": {},
|
||||
"WITHIN": {},
|
||||
}
|
||||
|
||||
type spannerDriver struct{}
|
||||
|
||||
func (d *spannerDriver) Parse(_driverName, datasourceName string) (*core.Uri, error) {
|
||||
return &core.Uri{DbType: "spanner", DbName: datasourceName}, nil
|
||||
}
|
||||
|
||||
type spanner struct {
|
||||
core.Base
|
||||
}
|
||||
|
||||
func (s *spanner) Init(db *core.DB, uri *core.Uri, driverName string, datasourceName string) error {
|
||||
return s.Base.Init(db, s, uri, driverName, datasourceName)
|
||||
}
|
||||
func (s *spanner) Filters() []core.Filter { return []core.Filter{&core.IdFilter{}} }
|
||||
func (s *spanner) IsReserved(name string) bool {
|
||||
_, exists := spannerReservedKeywords[name]
|
||||
return exists
|
||||
}
|
||||
func (s *spanner) AndStr() string { return "AND" }
|
||||
func (s *spanner) OrStr() string { return "OR" }
|
||||
func (s *spanner) EqStr() string { return "=" }
|
||||
func (s *spanner) RollBackStr() string { return "ROLL BACK" }
|
||||
func (s *spanner) AutoIncrStr() string { return "" } // Spanner does not support auto-increment
|
||||
func (s *spanner) SupportInsertMany() bool { return false } // Needs manual transaction batching
|
||||
func (s *spanner) SupportEngine() bool { return false } // No support for engine selection
|
||||
func (s *spanner) SupportCharset() bool { return false } // ...or charsets
|
||||
func (s *spanner) SupportDropIfExists() bool { return false } // Drop should be handled differently
|
||||
func (s *spanner) IndexOnTable() bool { return false }
|
||||
func (s *spanner) ShowCreateNull() bool { return false }
|
||||
func (s *spanner) Quote(name string) string { return "`" + name + "`" }
|
||||
func (s *spanner) SqlType(col *core.Column) string {
|
||||
switch col.SQLType.Name {
|
||||
case core.Int, core.BigInt:
|
||||
return "INT64"
|
||||
case core.Varchar, core.Text:
|
||||
return "STRING(MAX)"
|
||||
case core.Bool:
|
||||
return "BOOL"
|
||||
case core.Float, core.Double:
|
||||
return "FLOAT64"
|
||||
case core.Bytea:
|
||||
return "BYTES(MAX)"
|
||||
case core.DateTime, core.TimeStamp:
|
||||
return "TIMESTAMP"
|
||||
default:
|
||||
return "STRING(MAX)" // XXX: more types to add
|
||||
}
|
||||
}
|
||||
|
||||
func (s *spanner) GetColumns(tableName string) ([]string, map[string]*core.Column, error) {
|
||||
query := `SELECT COLUMN_NAME, SPANNER_TYPE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @tableName`
|
||||
rows, err := s.DB().Query(query, map[string]any{"tableName": tableName})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
columns := make(map[string]*core.Column)
|
||||
var colNames []string
|
||||
|
||||
var name, sqlType, isNullable string
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&name, &sqlType, &isNullable); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
col := &core.Column{
|
||||
Name: name,
|
||||
SQLType: core.SQLType{Name: sqlType},
|
||||
Nullable: isNullable == "YES",
|
||||
}
|
||||
columns[name] = col
|
||||
colNames = append(colNames, name)
|
||||
}
|
||||
|
||||
return colNames, columns, nil
|
||||
}
|
||||
|
||||
func (s *spanner) CreateTableSql(table *core.Table, tableName, _, charset string) string {
|
||||
sql := "CREATE TABLE " + s.Quote(tableName) + " ("
|
||||
|
||||
for i, col := range table.Columns() {
|
||||
if i > 0 {
|
||||
sql += ", "
|
||||
}
|
||||
sql += s.Quote(col.Name) + " " + s.SqlType(col)
|
||||
if col.IsPrimaryKey {
|
||||
sql += " PRIMARY KEY"
|
||||
}
|
||||
}
|
||||
|
||||
sql += ") PRIMARY KEY (" + strings.Join(table.PrimaryKeys, ",") + ")"
|
||||
return sql
|
||||
}
|
||||
|
||||
func (s *spanner) CreateIndexSql(tableName string, index *core.Index) string {
|
||||
sql := "CREATE "
|
||||
if index.Type == core.UniqueType {
|
||||
sql += "UNIQUE NULL_FILTERED "
|
||||
}
|
||||
sql += "INDEX " + s.Quote(index.XName(tableName)) + " ON " + s.Quote(tableName) + " (" + strings.Join(index.Cols, ", ") + ")"
|
||||
return sql
|
||||
}
|
||||
|
||||
func (s *spanner) IndexCheckSql(tableName, indexName string) (string, []any) {
|
||||
return `SELECT index_name FROM information_schema.indexes
|
||||
WHERE table_name = ? AND table_schema = "" AND index_name = ?`,
|
||||
[]any{tableName, indexName}
|
||||
}
|
||||
|
||||
func (s *spanner) TableCheckSql(tableName string) (string, []any) {
|
||||
return `SELECT table_name FROM information_schema.tables
|
||||
WHERE table_name = ? AND table_schema = ""`,
|
||||
[]any{tableName}
|
||||
}
|
||||
|
||||
func (s *spanner) GetTables() ([]*core.Table, error) {
|
||||
res, err := s.DB().Query(`
|
||||
SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = ""
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Close()
|
||||
|
||||
tables := []*core.Table{}
|
||||
for res.Next() {
|
||||
var name string
|
||||
if err := res.Scan(&name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := core.NewEmptyTable()
|
||||
t.Name = name
|
||||
tables = append(tables, t)
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func (s *spanner) GetIndexes(tableName string) (map[string]*core.Index, error) {
|
||||
res, err := s.DB().Query(`
|
||||
SELECT index_name, index_type, is_unique FROM information_schema.tables
|
||||
WHERE table_name = ? AND table_schema = ""
|
||||
`, []any{tableName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Close()
|
||||
|
||||
indices := map[string]*core.Index{}
|
||||
for res.Next() {
|
||||
index := struct {
|
||||
Name string `xorm:"index_name"`
|
||||
Type string `xorm:"index_type"`
|
||||
IsUnqiue bool `xorm:"is_unique"`
|
||||
}{}
|
||||
err := res.Scan(&index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case index.Type == "INDEX":
|
||||
indices[index.Name] = core.NewIndex(index.Name, core.IndexType)
|
||||
case index.Type == "PRIMARY_KEY", index.IsUnqiue:
|
||||
indices[index.Name] = core.NewIndex(index.Name, core.UniqueType)
|
||||
}
|
||||
}
|
||||
return indices, nil
|
||||
}
|
||||
+51
-5
@@ -1,10 +1,11 @@
|
||||
module github.com/grafana/grafana/pkg/util/xorm
|
||||
|
||||
go 1.22
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.1
|
||||
toolchain go1.24.0
|
||||
|
||||
require (
|
||||
github.com/googleapis/go-sql-spanner v1.11.1
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/stretchr/testify v1.10.0
|
||||
xorm.io/builder v0.3.6
|
||||
@@ -12,11 +13,56 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.19.0 // indirect
|
||||
cloud.google.com/go v0.118.2 // indirect
|
||||
cloud.google.com/go/auth v0.14.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
cloud.google.com/go/iam v1.3.1 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.4 // indirect
|
||||
cloud.google.com/go/monitoring v1.23.0 // indirect
|
||||
cloud.google.com/go/spanner v1.75.0 // indirect
|
||||
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.3 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.33.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
|
||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
google.golang.org/api v0.220.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect
|
||||
google.golang.org/grpc v1.70.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ package xorm
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -345,6 +346,21 @@ func (session *Session) innerInsert(bean any) (int64, error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// XXX: hack to handle autoincrement in spanner
|
||||
if len(table.AutoIncrement) > 0 && session.engine.dialect.DBType() == "spanner" {
|
||||
var found bool
|
||||
for _, col := range colNames {
|
||||
if col == table.AutoIncrement {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
colNames = append(colNames, table.AutoIncrement)
|
||||
args = append(args, rand.Int63n(9e15))
|
||||
}
|
||||
}
|
||||
|
||||
exprs := session.statement.exprColumns
|
||||
colPlaces := strings.Repeat("?, ", len(colNames))
|
||||
if exprs.Len() <= 0 && len(colPlaces) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user