2025-03-04 00:02:10 +08:00
//go:build enterprise || pro
package xorm
import (
2025-03-06 23:11:20 +08:00
"database/sql"
"fmt"
"strconv"
2025-03-04 00:02:10 +08:00
"strings"
2025-04-03 22:26:09 +08:00
spannerclient "cloud.google.com/go/spanner"
2025-03-04 00:02:10 +08:00
_ "github.com/googleapis/go-sql-spanner"
2025-03-20 23:50:50 +08:00
spannerdriver "github.com/googleapis/go-sql-spanner"
2025-03-24 19:16:12 +08:00
"google.golang.org/api/option"
"google.golang.org/grpc"
2025-04-03 22:26:09 +08:00
"google.golang.org/grpc/codes"
2025-03-24 19:16:12 +08:00
"google.golang.org/grpc/credentials/insecure"
2025-03-04 00:02:10 +08:00
"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
}
2025-03-06 23:11:20 +08:00
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 {
// Spanner does not support auto-increment, but supports unique generated IDs (not sequential!).
return "GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE)"
}
2025-03-04 00:02:10 +08:00
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 {
2025-03-06 23:11:20 +08:00
case core . Int , core . SmallInt , core . BigInt :
2025-03-04 00:02:10 +08:00
return "INT64"
2025-03-06 23:11:20 +08:00
case core . Varchar , core . Text , core . MediumText , core . LongText , core . Char , core . NVarchar , core . NChar , core . NText :
l := col . Length
if l == 0 {
l = col . SQLType . DefaultLength
}
if l > 0 {
return fmt . Sprintf ( "STRING(%d)" , l )
}
2025-03-04 00:02:10 +08:00
return "STRING(MAX)"
2025-03-26 17:58:06 +08:00
case core . Jsonb :
return "STRING(MAX)"
2025-03-06 23:11:20 +08:00
case core . Bool , core . TinyInt :
2025-03-04 00:02:10 +08:00
return "BOOL"
case core . Float , core . Double :
return "FLOAT64"
2025-03-06 23:11:20 +08:00
case core . Bytea , core . Blob , core . MediumBlob , core . LongBlob :
l := col . Length
if l == 0 {
l = col . SQLType . DefaultLength
}
if l > 0 {
return fmt . Sprintf ( "BYTES(%d)" , l )
}
2025-03-04 00:02:10 +08:00
return "BYTES(MAX)"
case core . DateTime , core . TimeStamp :
return "TIMESTAMP"
default :
2025-03-06 23:11:20 +08:00
panic ( "unknown column type: " + col . SQLType . Name )
//default:
// return "STRING(MAX)" // XXX: more types to add
2025-03-04 00:02:10 +08:00
}
}
func ( s * spanner ) GetColumns ( tableName string ) ( [ ] string , map [ string ] * core . Column , error ) {
2025-03-06 23:11:20 +08:00
query := ` SELECT COLUMN_NAME , SPANNER_TYPE , IS_NULLABLE , IS_IDENTITY , IDENTITY_GENERATION , IDENTITY_KIND , COLUMN_DEFAULT
FROM INFORMATION_SCHEMA . COLUMNS WHERE TABLE_NAME = ? AND TABLE_SCHEMA = "" ORDER BY ORDINAL_POSITION `
rows , err := s . DB ( ) . Query ( query , tableName )
2025-03-04 00:02:10 +08:00
if err != nil {
return nil , nil , err
}
defer rows . Close ( )
columns := make ( map [ string ] * core . Column )
var colNames [ ] string
var name , sqlType , isNullable string
2025-03-06 23:11:20 +08:00
var isIdentity , identityGeneration , identityKind , columnDefault sql . NullString
2025-03-04 00:02:10 +08:00
for rows . Next ( ) {
2025-03-06 23:11:20 +08:00
if err := rows . Scan ( & name , & sqlType , & isNullable , & isIdentity , & identityGeneration , & identityKind , & columnDefault ) ; err != nil {
2025-03-04 00:02:10 +08:00
return nil , nil , err
}
2025-03-06 23:11:20 +08:00
var length int
switch {
case sqlType == "INT64" :
sqlType = core . Int
case sqlType == "FLOAT32" || sqlType == "FLOAT64" :
sqlType = core . Float
case sqlType == "BOOL" :
sqlType = core . Bool
case sqlType == "BYTES(MAX)" :
sqlType = core . Blob
case sqlType == "STRING(MAX)" :
sqlType = core . NVarchar
case sqlType == "TIMESTAMP" :
sqlType = core . DateTime
case strings . HasPrefix ( sqlType , "BYTES(" ) :
// 6 == len(`BYTES(`), we also remove ")" from the end.
if l , err := strconv . Atoi ( sqlType [ 6 : len ( sqlType ) - 1 ] ) ; err == nil {
length = l
}
sqlType = core . Blob
case strings . HasPrefix ( sqlType , "STRING(" ) :
// 7 == len(`STRING(`), we also remove ")" from the end.
if l , err := strconv . Atoi ( sqlType [ 7 : len ( sqlType ) - 1 ] ) ; err == nil {
length = l
}
sqlType = core . Varchar
default :
panic ( "unknown column type: " + sqlType )
}
autoincrement := isIdentity . Valid && isIdentity . String == "YES" &&
identityGeneration . Valid && identityGeneration . String == "BY DEFAULT" &&
identityKind . Valid && identityKind . String == "BIT_REVERSED_POSITIVE_SEQUENCE"
defValue := ""
defEmpty := true
if columnDefault . Valid {
defValue = columnDefault . String
defEmpty = false
}
2025-03-04 00:02:10 +08:00
col := & core . Column {
2025-03-06 23:11:20 +08:00
Name : name ,
SQLType : core . SQLType { Name : sqlType } ,
Length : length ,
Nullable : isNullable == "YES" ,
IsAutoIncrement : autoincrement ,
Indexes : map [ string ] int { } ,
Default : defValue ,
DefaultIsEmpty : defEmpty ,
2025-03-04 00:02:10 +08:00
}
columns [ name ] = col
colNames = append ( colNames , name )
}
2025-03-06 23:11:20 +08:00
return colNames , columns , rows . Err ( )
2025-03-04 00:02:10 +08:00
}
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 += ", "
}
2025-03-06 23:11:20 +08:00
2025-03-04 00:02:10 +08:00
sql += s . Quote ( col . Name ) + " " + s . SqlType ( col )
2025-03-06 23:11:20 +08:00
if ! col . Nullable {
sql += " NOT NULL"
}
if col . Default != "" {
sql += " DEFAULT (" + col . Default + ")"
}
if col . IsAutoIncrement {
sql += " GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE)"
2025-03-04 00:02:10 +08:00
}
}
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 ) {
2025-03-06 23:11:20 +08:00
res , err := s . DB ( ) . Query ( ` SELECT table_name FROM information_schema.tables WHERE table_schema = "" ` )
2025-03-04 00:02:10 +08:00
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 )
}
2025-03-06 23:11:20 +08:00
return tables , res . Err ( )
2025-03-04 00:02:10 +08:00
}
func ( s * spanner ) GetIndexes ( tableName string ) ( map [ string ] * core . Index , error ) {
2025-03-06 23:11:20 +08:00
res , err := s . DB ( ) . Query ( ` SELECT ix . INDEX_NAME , ix . INDEX_TYPE , ix . IS_UNIQUE , c . COLUMN_NAME
FROM INFORMATION_SCHEMA . INDEXES ix
JOIN INFORMATION_SCHEMA . INDEX_COLUMNS c ON ( ix . TABLE_NAME = c . TABLE_NAME AND ix . INDEX_NAME = c . INDEX_NAME )
WHERE ix . TABLE_SCHEMA = "" AND ix . TABLE_NAME = ?
ORDER BY ix . INDEX_NAME , c . ORDINAL_POSITION ` , tableName )
2025-03-04 00:02:10 +08:00
if err != nil {
return nil , err
}
defer res . Close ( )
2025-03-06 23:11:20 +08:00
indexes := map [ string ] * core . Index { }
var ixName , ixType , colName string
var isUnique bool
2025-03-04 00:02:10 +08:00
for res . Next ( ) {
2025-03-06 23:11:20 +08:00
err := res . Scan ( & ixName , & ixType , & isUnique , & colName )
2025-03-04 00:02:10 +08:00
if err != nil {
return nil , err
}
2025-03-06 23:11:20 +08:00
isRegular := false
if strings . HasPrefix ( ixName , "IDX_" + tableName ) || strings . HasPrefix ( ixName , "UQE_" + tableName ) {
ixName = ixName [ 5 + len ( tableName ) : ]
isRegular = true
}
var index * core . Index
var ok bool
if index , ok = indexes [ ixName ] ; ! ok {
t := core . IndexType // ixType == "INDEX" && !isUnique
if ixType == "PRIMARY KEY" || isUnique {
t = core . UniqueType
}
index = & core . Index { }
index . IsRegular = isRegular
index . Type = t
index . Name = ixName
indexes [ ixName ] = index
2025-03-04 00:02:10 +08:00
}
2025-03-06 23:11:20 +08:00
index . AddColumn ( colName )
2025-03-04 00:02:10 +08:00
}
2025-03-06 23:11:20 +08:00
return indexes , res . Err ( )
2025-03-04 00:02:10 +08:00
}
2025-03-20 23:50:50 +08:00
func ( s * spanner ) CreateSequenceGenerator ( db * sql . DB ) ( SequenceGenerator , error ) {
dsn := s . DataSourceName ( )
connectorConfig , err := spannerdriver . ExtractConnectorConfig ( dsn )
if err != nil {
return nil , err
}
2025-03-24 19:16:12 +08:00
if UsePlainText ( connectorConfig ) {
2025-03-20 23:50:50 +08:00
// Plain-text means we're either using spannertest or Spanner emulator.
// Switch to fake in-memory sequence number generator in that case.
//
// Using database-based sequence generator doesn't work with emulator, as emulator
// only supports single transaction. If there is already another transaction started
// generating new ID via database-based sequence generator would always fail.
return newInMemSequenceGenerator ( ) , nil
}
return newSequenceGenerator ( db ) , nil
}
2025-03-24 19:16:12 +08:00
func UsePlainText ( connectorConfig spannerdriver . ConnectorConfig ) bool {
if strval , ok := connectorConfig . Params [ "useplaintext" ] ; ok {
if val , err := strconv . ParseBool ( strval ) ; err == nil {
return val
}
}
return false
}
// SpannerConnectorConfigToClientOptions is adapted from https://github.com/googleapis/go-sql-spanner/blob/main/driver.go#L341-L477, from version 1.11.1.
func SpannerConnectorConfigToClientOptions ( connectorConfig spannerdriver . ConnectorConfig ) [ ] option . ClientOption {
var opts [ ] option . ClientOption
if connectorConfig . Host != "" {
opts = append ( opts , option . WithEndpoint ( connectorConfig . Host ) )
}
if strval , ok := connectorConfig . Params [ "credentials" ] ; ok {
opts = append ( opts , option . WithCredentialsFile ( strval ) )
}
if strval , ok := connectorConfig . Params [ "credentialsjson" ] ; ok {
opts = append ( opts , option . WithCredentialsJSON ( [ ] byte ( strval ) ) )
}
if UsePlainText ( connectorConfig ) {
opts = append ( opts ,
option . WithGRPCDialOption ( grpc . WithTransportCredentials ( insecure . NewCredentials ( ) ) ) ,
option . WithoutAuthentication ( ) )
}
return opts
}
2025-04-03 22:26:09 +08:00
func ( s * spanner ) RetryOnError ( err error ) bool {
return err != nil && spannerclient . ErrCode ( spannerclient . ToSpannerError ( err ) ) == codes . Aborted
}