1152 lines
30 KiB
Go
1152 lines
30 KiB
Go
package dialects
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"xorm.io/xorm/convert"
|
|
"xorm.io/xorm/core"
|
|
"xorm.io/xorm/schemas"
|
|
)
|
|
|
|
// from https://github.com/ydb-platform/ydb/blob/main/ydb/library/yql/sql/v1/SQLv1.g.in#L1117
|
|
var (
|
|
ydbReservedWords = map[string]bool{
|
|
"ABORT": true,
|
|
"ACTION": true,
|
|
"ADD": true,
|
|
"AFTER": true,
|
|
"ALL": true,
|
|
"ALTER": true,
|
|
"ANALYZE": true,
|
|
"AND": true,
|
|
"ANSI": true,
|
|
"ANY": true,
|
|
"ARRAY": true,
|
|
"AS": true,
|
|
"ASC": true,
|
|
"ASSUME": true,
|
|
"ASYNC": true,
|
|
"ATTACH": true,
|
|
"AUTOINCREMENT": true,
|
|
"AUTOMAP": true,
|
|
"BEFORE": true,
|
|
"BEGIN": true,
|
|
"BERNOULLI": true,
|
|
"BETWEEN": true,
|
|
"BITCAST": true,
|
|
"BY": true,
|
|
"CALLABLE": true,
|
|
"CASCADE": true,
|
|
"CASE": true,
|
|
"CAST": true,
|
|
"CHANGEFEED": true,
|
|
"CHECK": true,
|
|
"COLLATE": true,
|
|
"COLUMN": true,
|
|
"COLUMNS": true,
|
|
"COMMIT": true,
|
|
"COMPACT": true,
|
|
"CONDITIONAL": true,
|
|
"CONFLICT": true,
|
|
"CONSTRAINT": true,
|
|
"COVER": true,
|
|
"CREATE": true,
|
|
"CROSS": true,
|
|
"CUBE": true,
|
|
"CURRENT": true,
|
|
"CURRENT_TIME": true,
|
|
"CURRENT_DATE": true,
|
|
"CURRENT_TIMESTAMP": true,
|
|
"DATABASE": true,
|
|
"DECIMAL": true,
|
|
"DECLARE": true,
|
|
"DEFAULT": true,
|
|
"DEFERRABLE": true,
|
|
"DEFERRED": true,
|
|
"DEFINE": true,
|
|
"DELETE": true,
|
|
"DESC": true,
|
|
"DETACH": true,
|
|
"DICT": true,
|
|
"DISABLE": true,
|
|
"DISCARD": true,
|
|
"DISTINCT": true,
|
|
"DO": true,
|
|
"DROP": true,
|
|
"EACH": true,
|
|
"ELSE": true,
|
|
"ERROR": true,
|
|
"EMPTY": true,
|
|
"EMPTY_ACTION": true,
|
|
"ENCRYPTED": true,
|
|
"END": true,
|
|
"ENUM": true,
|
|
"ERASE": true,
|
|
"ESCAPE": true,
|
|
"EVALUATE": true,
|
|
"EXCEPT": true,
|
|
"EXCLUDE": true,
|
|
"EXCLUSIVE": true,
|
|
"EXCLUSION": true,
|
|
"EXISTS": true,
|
|
"EXPLAIN": true,
|
|
"EXPORT": true,
|
|
"EXTERNAL": true,
|
|
"FAIL": true,
|
|
"FAMILY": true,
|
|
"FILTER": true,
|
|
"FLATTEN": true,
|
|
"FLOW": true,
|
|
"FOLLOWING": true,
|
|
"FOR": true,
|
|
"FOREIGN": true,
|
|
"FROM": true,
|
|
"FULL": true,
|
|
"FUNCTION": true,
|
|
"GLOB": true,
|
|
"GLOBAL": true,
|
|
"GROUP": true,
|
|
"GROUPING": true,
|
|
"GROUPS": true,
|
|
"HASH": true,
|
|
"HAVING": true,
|
|
"HOP": true,
|
|
"IF": true,
|
|
"IGNORE": true,
|
|
"ILIKE": true,
|
|
"IMMEDIATE": true,
|
|
"IMPORT": true,
|
|
"IN": true,
|
|
"INDEX": true,
|
|
"INDEXED": true,
|
|
"INHERITS": true,
|
|
"INITIALLY": true,
|
|
"INNER": true,
|
|
"INSERT": true,
|
|
"INSTEAD": true,
|
|
"INTERSECT": true,
|
|
"INTO": true,
|
|
"IS": true,
|
|
"ISNULL": true,
|
|
"JOIN": true,
|
|
"JSON_EXISTS": true,
|
|
"JSON_VALUE": true,
|
|
"JSON_QUERY": true,
|
|
"KEY": true,
|
|
"LEFT": true,
|
|
"LIKE": true,
|
|
"LIMIT": true,
|
|
"LIST": true,
|
|
"LOCAL": true,
|
|
"MATCH": true,
|
|
"NATURAL": true,
|
|
"NO": true,
|
|
"NOT": true,
|
|
"NOTNULL": true,
|
|
"NULL": true,
|
|
"NULLS": true,
|
|
"OBJECT": true,
|
|
"OF": true,
|
|
"OFFSET": true,
|
|
"ON": true,
|
|
"ONLY": true,
|
|
"OPTIONAL": true,
|
|
"OR": true,
|
|
"ORDER": true,
|
|
"OTHERS": true,
|
|
"OUTER": true,
|
|
"OVER": true,
|
|
"PARTITION": true,
|
|
"PASSING": true,
|
|
"PASSWORD": true,
|
|
"PLAN": true,
|
|
"PRAGMA": true,
|
|
"PRECEDING": true,
|
|
"PRESORT": true,
|
|
"PRIMARY": true,
|
|
"PROCESS": true,
|
|
"RAISE": true,
|
|
"RANGE": true,
|
|
"REDUCE": true,
|
|
"REFERENCES": true,
|
|
"REGEXP": true,
|
|
"REINDEX": true,
|
|
"RELEASE": true,
|
|
"RENAME": true,
|
|
"REPEATABLE": true,
|
|
"REPLACE": true,
|
|
"RESET": true,
|
|
"RESOURCE": true,
|
|
"RESPECT": true,
|
|
"RESTRICT": true,
|
|
"RESULT": true,
|
|
"RETURN": true,
|
|
"RETURNING": true,
|
|
"REVERT": true,
|
|
"RIGHT": true,
|
|
"RLIKE": true,
|
|
"ROLLBACK": true,
|
|
"ROLLUP": true,
|
|
"ROW": true,
|
|
"ROWS": true,
|
|
"SAMPLE": true,
|
|
"SAVEPOINT": true,
|
|
"SCHEMA": true,
|
|
"SELECT": true,
|
|
"SEMI": true,
|
|
"SET": true,
|
|
"SETS": true,
|
|
"STREAM": true,
|
|
"STRUCT": true,
|
|
"SUBQUERY": true,
|
|
"SYMBOLS": true,
|
|
"SYNC": true,
|
|
"SYSTEM": true,
|
|
"TABLE": true,
|
|
"TABLESAMPLE": true,
|
|
"TABLESTORE": true,
|
|
"TAGGED": true,
|
|
"TEMP": true,
|
|
"TEMPORARY": true,
|
|
"THEN": true,
|
|
"TIES": true,
|
|
"TO": true,
|
|
"TRANSACTION": true,
|
|
"TRIGGER": true,
|
|
"TUPLE": true,
|
|
"UNBOUNDED": true,
|
|
"UNCONDITIONAL": true,
|
|
"UNION": true,
|
|
"UNIQUE": true,
|
|
"UNKNOWN": true,
|
|
"UPDATE": true,
|
|
"UPSERT": true,
|
|
"USE": true,
|
|
"USER": true,
|
|
"USING": true,
|
|
"VACUUM": true,
|
|
"VALUES": true,
|
|
"VARIANT": true,
|
|
"VIEW": true,
|
|
"VIRTUAL": true,
|
|
"WHEN": true,
|
|
"WHERE": true,
|
|
"WINDOW": true,
|
|
"WITH": true,
|
|
"WITHOUT": true,
|
|
"WRAPPER": true,
|
|
"XOR": true,
|
|
"TRUE": true,
|
|
"FALSE": true,
|
|
}
|
|
|
|
ydbQuoter = schemas.Quoter{
|
|
Prefix: '`',
|
|
Suffix: '`',
|
|
IsReserved: schemas.AlwaysReserve,
|
|
}
|
|
)
|
|
|
|
const (
|
|
// numeric types
|
|
yql_Bool = "BOOL"
|
|
|
|
yql_Int8 = "INT8"
|
|
yql_Int16 = "INT16"
|
|
yql_Int32 = "INT32"
|
|
yql_Int64 = "INT64"
|
|
|
|
yql_Uint8 = "UINT8"
|
|
yql_Uint16 = "UINT16"
|
|
yql_Uint32 = "UINT32"
|
|
yql_Uint64 = "UINT64"
|
|
|
|
yql_Float = "FLOAT"
|
|
yql_Double = "DOUBLE"
|
|
yql_Decimal = "DECIMAL"
|
|
|
|
// string types
|
|
yql_String = "STRING"
|
|
yql_Utf8 = "UTF8"
|
|
yql_Json = "JSON"
|
|
yql_JsonDocument = "JSONDOCUMENT"
|
|
yql_Yson = "YSON"
|
|
|
|
// Data and Time
|
|
yql_Date = "DATE"
|
|
yql_DateTime = "DATETIME"
|
|
yql_Timestamp = "TIMESTAMP"
|
|
yql_Interval = "INTERVAL"
|
|
|
|
// Containers
|
|
yql_List = "LIST"
|
|
)
|
|
|
|
func toYQLDataType(t string, defaultLength, defaultLength2 int64) string {
|
|
switch v := t; v {
|
|
case schemas.Bool, schemas.Boolean:
|
|
return yql_Bool
|
|
case schemas.TinyInt:
|
|
return yql_Int8
|
|
case schemas.UnsignedTinyInt:
|
|
return yql_Uint8
|
|
case schemas.SmallInt:
|
|
return yql_Int16
|
|
case schemas.UnsignedSmallInt:
|
|
return yql_Uint16
|
|
case schemas.MediumInt:
|
|
return yql_Int32
|
|
case schemas.UnsignedMediumInt:
|
|
return yql_Uint32
|
|
case schemas.BigInt:
|
|
return yql_Int64
|
|
case schemas.UnsignedBigInt:
|
|
return yql_Uint64
|
|
case schemas.Int, schemas.Integer:
|
|
return yql_Int32
|
|
case schemas.UnsignedInt:
|
|
return yql_Uint32
|
|
case schemas.Float:
|
|
return yql_Float
|
|
case schemas.Double:
|
|
return yql_Double
|
|
case schemas.Blob:
|
|
return yql_String
|
|
case schemas.Json:
|
|
return yql_Json
|
|
case schemas.Array:
|
|
return yql_List
|
|
case schemas.Varchar, schemas.Text:
|
|
return yql_Utf8
|
|
case schemas.TimeStamp, schemas.DateTime:
|
|
return yql_Timestamp
|
|
case schemas.Interval:
|
|
return yql_Interval
|
|
default:
|
|
return yql_String
|
|
}
|
|
}
|
|
|
|
func yqlToSQLType(yqlType string) schemas.SQLType {
|
|
switch yqlType {
|
|
case yql_Bool:
|
|
return schemas.SQLType{Name: schemas.Bool, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_Int8:
|
|
return schemas.SQLType{Name: schemas.TinyInt, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_Uint8:
|
|
return schemas.SQLType{Name: schemas.UnsignedTinyInt, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_Int16:
|
|
return schemas.SQLType{Name: schemas.SmallInt, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_Uint16:
|
|
return schemas.SQLType{Name: schemas.UnsignedSmallInt, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_Int32:
|
|
return schemas.SQLType{Name: schemas.MediumInt, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_Uint32:
|
|
return schemas.SQLType{Name: schemas.UnsignedMediumInt, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_Int64:
|
|
return schemas.SQLType{Name: schemas.BigInt, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_Uint64:
|
|
return schemas.SQLType{Name: schemas.UnsignedBigInt, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_Float:
|
|
return schemas.SQLType{Name: schemas.Float, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_Double:
|
|
return schemas.SQLType{Name: schemas.Double, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_String:
|
|
return schemas.SQLType{Name: schemas.Blob, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_Json:
|
|
return schemas.SQLType{Name: schemas.Json, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_List:
|
|
return schemas.SQLType{Name: schemas.Array, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_Utf8:
|
|
return schemas.SQLType{Name: schemas.Varchar, DefaultLength: 255, DefaultLength2: 0}
|
|
case yql_Timestamp:
|
|
return schemas.SQLType{Name: schemas.TimeStamp, DefaultLength: 0, DefaultLength2: 0}
|
|
case yql_Interval:
|
|
return schemas.SQLType{Name: schemas.Interval, DefaultLength: 0, DefaultLength2: 0}
|
|
default:
|
|
return schemas.SQLType{Name: schemas.Blob, DefaultLength: 0, DefaultLength2: 0}
|
|
}
|
|
}
|
|
|
|
func removeOptional(s string) string {
|
|
if s = strings.ToUpper(s); strings.HasPrefix(s, "OPTIONAL") {
|
|
s = strings.TrimPrefix(s, "OPTIONAL<")
|
|
s = strings.TrimSuffix(s, ">")
|
|
}
|
|
return s
|
|
}
|
|
|
|
type ydb struct {
|
|
Base
|
|
|
|
tableParams map[string]string
|
|
}
|
|
|
|
func (db *ydb) Init(uri *URI) error {
|
|
db.quoter = ydbQuoter
|
|
return db.Base.Init(db, uri)
|
|
}
|
|
|
|
func (db *ydb) getDB(queryer interface{}) *core.DB {
|
|
if internalDB, ok := queryer.(*core.DB); ok {
|
|
return internalDB
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (db *ydb) WithConn(queryer core.Queryer, ctx context.Context, f func(context.Context, *sql.Conn) error) error {
|
|
coreDB := db.getDB(queryer)
|
|
if coreDB == nil {
|
|
return fmt.Errorf("`*core.DB` not found")
|
|
}
|
|
|
|
cc, err := coreDB.Conn(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cc.Close()
|
|
|
|
err = f(ctx, cc)
|
|
|
|
return err
|
|
}
|
|
|
|
func (db *ydb) WithConnRaw(queryer core.Queryer, ctx context.Context, f func(d interface{}) error) (err error) {
|
|
err = db.WithConn(queryer, ctx, func(ctx context.Context, cc *sql.Conn) error {
|
|
err = cc.Raw(f)
|
|
return err
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (db *ydb) SetParams(tableParams map[string]string) {
|
|
db.tableParams = tableParams
|
|
}
|
|
|
|
func (db *ydb) Features() *DialectFeatures {
|
|
return &DialectFeatures{
|
|
AutoincrMode: -1,
|
|
}
|
|
}
|
|
|
|
// unsupported feature
|
|
func (db *ydb) IsSequenceExist(_ context.Context, _ core.Queryer, _ string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
func (db *ydb) AutoIncrStr() string {
|
|
return ""
|
|
}
|
|
|
|
func (db *ydb) IsReserved(name string) bool {
|
|
_, ok := ydbReservedWords[strings.ToUpper(name)]
|
|
return ok
|
|
}
|
|
|
|
func (db *ydb) SetQuotePolicy(quotePolicy QuotePolicy) {
|
|
switch quotePolicy {
|
|
case QuotePolicyNone:
|
|
q := ydbQuoter
|
|
q.IsReserved = schemas.AlwaysNoReserve
|
|
db.quoter = q
|
|
case QuotePolicyReserved:
|
|
q := ydbQuoter
|
|
q.IsReserved = db.IsReserved
|
|
db.quoter = q
|
|
case QuotePolicyAlways:
|
|
fallthrough
|
|
default:
|
|
db.quoter = ydbQuoter
|
|
}
|
|
}
|
|
|
|
func (db *ydb) SQLType(column *schemas.Column) string {
|
|
return toYQLDataType(column.SQLType.Name, column.SQLType.DefaultLength, column.SQLType.DefaultLength2)
|
|
}
|
|
|
|
// https://pkg.go.dev/database/sql#ColumnType.DatabaseTypeName
|
|
func (db *ydb) ColumnTypeKind(t string) int {
|
|
switch t {
|
|
case "BOOL":
|
|
return schemas.BOOL_TYPE
|
|
case "INT8", "INT16", "INT32", "INT64", "UINT8", "UINT16", "UINT32", "UINT64":
|
|
return schemas.NUMERIC_TYPE
|
|
case "UTF8":
|
|
return schemas.TEXT_TYPE
|
|
case "TIMESTAMP":
|
|
return schemas.TIME_TYPE
|
|
default:
|
|
return schemas.UNKNOW_TYPE
|
|
}
|
|
}
|
|
|
|
func (db *ydb) Version(ctx context.Context, queryer core.Queryer) (_ *schemas.Version, err error) {
|
|
var version string
|
|
err = db.WithConnRaw(queryer, ctx, func(dc interface{}) error {
|
|
q, ok := dc.(interface {
|
|
Version(ctx context.Context) (string, error)
|
|
})
|
|
if !ok {
|
|
return fmt.Errorf("driver does not support query metadata")
|
|
}
|
|
version, err = q.Version(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &schemas.Version{
|
|
Edition: version,
|
|
}, nil
|
|
}
|
|
|
|
func (db *ydb) IndexCheckSQL(tableName, indexName string) (string, []interface{}) {
|
|
return "", nil
|
|
}
|
|
|
|
func (db *ydb) IsTableExist(
|
|
queryer core.Queryer,
|
|
ctx context.Context,
|
|
tableName string) (_ bool, err error) {
|
|
var exists bool
|
|
err = db.WithConnRaw(queryer, ctx, func(dc interface{}) error {
|
|
q, ok := dc.(interface {
|
|
IsTableExists(context.Context, string) (bool, error)
|
|
})
|
|
if !ok {
|
|
return fmt.Errorf("driver does not support query metadata")
|
|
}
|
|
exists, err = q.IsTableExists(ctx, tableName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return exists, nil
|
|
}
|
|
|
|
func (db *ydb) AddColumnSQL(tableName string, col *schemas.Column) string {
|
|
quote := db.dialect.Quoter()
|
|
tableName = quote.Quote(tableName)
|
|
columnName := quote.Quote(col.Name)
|
|
dataType := db.SQLType(col)
|
|
|
|
var buf strings.Builder
|
|
buf.WriteString(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s;", tableName, columnName, dataType))
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
// YDB does not support this operation
|
|
func (db *ydb) ModifyColumnSQL(tableName string, column *schemas.Column) string {
|
|
return ""
|
|
}
|
|
|
|
// SYNC by default
|
|
func (db *ydb) CreateIndexSQL(tableName string, index *schemas.Index) string {
|
|
quote := db.dialect.Quoter()
|
|
tableName = quote.Quote(tableName)
|
|
indexName := quote.Quote(index.Name)
|
|
|
|
colsIndex := make([]string, len(index.Cols))
|
|
for i := 0; i < len(index.Cols); i++ {
|
|
colsIndex[i] = quote.Quote(index.Cols[i])
|
|
}
|
|
|
|
indexOn := strings.Join(colsIndex, ",")
|
|
|
|
var buf strings.Builder
|
|
buf.WriteString(fmt.Sprintf("ALTER TABLE %s ADD INDEX %s GLOBAL ON ( %s );", tableName, indexName, indexOn))
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
func (db *ydb) DropIndexSQL(tableName string, index *schemas.Index) string {
|
|
quote := db.dialect.Quoter()
|
|
tableName = quote.Quote(tableName)
|
|
indexName := quote.Quote(index.Name)
|
|
|
|
var buf strings.Builder
|
|
buf.WriteString(fmt.Sprintf("ALTER TABLE %s DROP INDEX %s;", tableName, indexName))
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
func (db *ydb) IsColumnExist(
|
|
queryer core.Queryer,
|
|
ctx context.Context,
|
|
tableName,
|
|
columnName string) (_ bool, err error) {
|
|
var exists bool
|
|
err = db.WithConnRaw(queryer, ctx, func(dc interface{}) error {
|
|
q, ok := dc.(interface {
|
|
IsColumnExists(context.Context, string, string) (bool, error)
|
|
})
|
|
if !ok {
|
|
return fmt.Errorf("driver does not support query metadata")
|
|
}
|
|
exists, err = q.IsColumnExists(ctx, tableName, columnName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return exists, nil
|
|
}
|
|
|
|
func (db *ydb) GetColumns(queryer core.Queryer, ctx context.Context, tableName string) (
|
|
_ []string,
|
|
_ map[string]*schemas.Column,
|
|
err error) {
|
|
colNames := make([]string, 0)
|
|
colMaps := make(map[string]*schemas.Column)
|
|
|
|
err = db.WithConnRaw(queryer, ctx, func(dc interface{}) error {
|
|
q, ok := dc.(interface {
|
|
GetColumns(context.Context, string) ([]string, error)
|
|
GetColumnType(context.Context, string, string) (string, error)
|
|
IsPrimaryKey(context.Context, string, string) (bool, error)
|
|
})
|
|
if !ok {
|
|
return fmt.Errorf("driver does not support query metadata")
|
|
}
|
|
|
|
colNames, err = q.GetColumns(ctx, tableName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, colName := range colNames {
|
|
dataType, err := q.GetColumnType(ctx, tableName, colName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dataType = removeOptional(dataType)
|
|
isPK, err := q.IsPrimaryKey(ctx, tableName, colName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
col := &schemas.Column{
|
|
Name: colName,
|
|
TableName: tableName,
|
|
SQLType: yqlToSQLType(dataType),
|
|
IsPrimaryKey: isPK,
|
|
Nullable: !isPK,
|
|
Indexes: make(map[string]int),
|
|
}
|
|
colMaps[colName] = col
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return colNames, colMaps, nil
|
|
}
|
|
|
|
func (db *ydb) GetTables(queryer core.Queryer, ctx context.Context) (_ []*schemas.Table, err error) {
|
|
tables := make([]*schemas.Table, 0)
|
|
err = db.WithConnRaw(queryer, ctx, func(dc interface{}) error {
|
|
q, ok := dc.(interface {
|
|
GetTables(context.Context, string, bool, bool) ([]string, error)
|
|
})
|
|
if !ok {
|
|
return fmt.Errorf("driver does not support query metadata")
|
|
}
|
|
tableNames, err := q.GetTables(ctx, ".", true, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, tableName := range tableNames {
|
|
table := schemas.NewEmptyTable()
|
|
table.Name = tableName
|
|
tables = append(tables, table)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tables, nil
|
|
}
|
|
|
|
func (db *ydb) GetIndexes(
|
|
queryer core.Queryer,
|
|
ctx context.Context,
|
|
tableName string) (_ map[string]*schemas.Index, err error) {
|
|
indexes := make(map[string]*schemas.Index, 0)
|
|
err = db.WithConnRaw(queryer, ctx, func(dc interface{}) error {
|
|
q, ok := dc.(interface {
|
|
GetIndexes(context.Context, string) ([]string, error)
|
|
GetIndexColumns(context.Context, string, string) ([]string, error)
|
|
})
|
|
if !ok {
|
|
return fmt.Errorf("driver does not support query metadata")
|
|
}
|
|
indexNames, err := q.GetIndexes(ctx, tableName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, indexName := range indexNames {
|
|
cols, err := q.GetIndexColumns(ctx, tableName, indexName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
indexes[indexName] = &schemas.Index{
|
|
Name: indexName,
|
|
Type: schemas.IndexType,
|
|
Cols: cols,
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return indexes, nil
|
|
}
|
|
|
|
func (db *ydb) CreateTableSQL(
|
|
ctx context.Context,
|
|
_ core.Queryer,
|
|
table *schemas.Table,
|
|
tableName string) (string, bool, error) {
|
|
quote := db.dialect.Quoter()
|
|
tableName = quote.Quote(tableName)
|
|
|
|
var buf strings.Builder
|
|
buf.WriteString(fmt.Sprintf("CREATE TABLE %s ( ", tableName))
|
|
|
|
// build primary key
|
|
if len(table.PrimaryKeys) == 0 {
|
|
return "", false, errors.New("table must have at least one primary key")
|
|
}
|
|
pk := make([]string, len(table.PrimaryKeys))
|
|
pkMap := make(map[string]bool)
|
|
for i := 0; i < len(table.PrimaryKeys); i++ {
|
|
pk[i] = quote.Quote(table.PrimaryKeys[i])
|
|
pkMap[pk[i]] = true
|
|
}
|
|
primaryKey := fmt.Sprintf("PRIMARY KEY ( %s )", strings.Join(pk, ", "))
|
|
|
|
// build column
|
|
columnsList := []string{}
|
|
for _, c := range table.Columns() {
|
|
columnName := quote.Quote(c.Name)
|
|
dataType := db.SQLType(c)
|
|
|
|
if _, isPk := pkMap[columnName]; isPk {
|
|
columnsList = append(columnsList, fmt.Sprintf("%s %s NOT NULL", columnName, dataType))
|
|
} else {
|
|
columnsList = append(columnsList, fmt.Sprintf("%s %s", columnName, dataType))
|
|
}
|
|
}
|
|
joinColumns := strings.Join(columnsList, ", ")
|
|
|
|
// build index
|
|
indexList := []string{}
|
|
for indexName, index := range table.Indexes {
|
|
name := quote.Quote(indexName)
|
|
onCols := make([]string, len(index.Cols))
|
|
for i := 0; i < len(index.Cols); i++ {
|
|
onCols[i] = quote.Quote(index.Cols[i])
|
|
}
|
|
indexList = append(indexList,
|
|
fmt.Sprintf(
|
|
"INDEX %s GLOBAL ON ( %s )",
|
|
name, strings.Join(onCols, ", ")))
|
|
}
|
|
joinIndexes := strings.Join(indexList, ", ")
|
|
|
|
if joinIndexes != "" {
|
|
buf.WriteString(strings.Join([]string{joinColumns, joinIndexes, primaryKey}, ", "))
|
|
} else {
|
|
buf.WriteString(strings.Join([]string{joinColumns, primaryKey}, ", "))
|
|
}
|
|
|
|
buf.WriteString(" ) ")
|
|
|
|
if db.tableParams != nil && len(db.tableParams) > 0 {
|
|
params := make([]string, 0)
|
|
for param, value := range db.tableParams {
|
|
if param == "" || value == "" {
|
|
continue
|
|
}
|
|
params = append(params, fmt.Sprintf("%s = %s", param, value))
|
|
}
|
|
if len(params) > 0 {
|
|
buf.WriteString(fmt.Sprintf("WITH ( %s ) ", strings.Join(params, ", ")))
|
|
}
|
|
}
|
|
|
|
buf.WriteString("; ")
|
|
|
|
return buf.String(), true, nil
|
|
}
|
|
|
|
func (db *ydb) DropTableSQL(tableName string) (string, bool) {
|
|
quote := db.dialect.Quoter()
|
|
tableName = quote.Quote(tableName)
|
|
|
|
var buf strings.Builder
|
|
buf.WriteString(fmt.Sprintf("DROP TABLE %s;", tableName))
|
|
|
|
return buf.String(), false
|
|
}
|
|
|
|
// https://github.com/ydb-platform/ydb-go-sdk/blob/master/SQL.md#specifying-query-parameters-
|
|
func (db *ydb) Filters() []Filter {
|
|
return []Filter{&ydbSeqFilter{
|
|
Prefix: "$",
|
|
Start: 1,
|
|
}}
|
|
}
|
|
|
|
const (
|
|
ydb_grpc_Canceled uint32 = 1
|
|
ydb_grpc_Unknown uint32 = 2
|
|
ydb_grpc_InvalidArgument uint32 = 3
|
|
ydb_grpc_DeadlineExceeded uint32 = 4
|
|
ydb_grpc_NotFound uint32 = 5
|
|
ydb_grpc_AlreadyExists uint32 = 6
|
|
ydb_grpc_PermissionDenied uint32 = 7
|
|
ydb_grpc_ResourceExhausted uint32 = 8
|
|
ydb_grpc_FailedPrecondition uint32 = 9
|
|
ydb_grpc_Aborted uint32 = 10
|
|
ydb_grpc_OutOfRange uint32 = 11
|
|
ydb_grpc_Unimplemented uint32 = 12
|
|
ydb_grpc_Internal uint32 = 13
|
|
ydb_grpc_Unavailable uint32 = 14
|
|
ydb_grpc_DataLoss uint32 = 15
|
|
ydb_grpc_Unauthenticated uint32 = 16
|
|
)
|
|
|
|
const (
|
|
ydb_STATUS_CODE_UNSPECIFIED int32 = 0
|
|
ydb_SUCCESS int32 = 400000
|
|
ydb_BAD_REQUEST int32 = 400010
|
|
ydb_UNAUTHORIZED int32 = 400020
|
|
ydb_INTERNAL_ERROR int32 = 400030
|
|
ydb_ABORTED int32 = 400040
|
|
ydb_UNAVAILABLE int32 = 400050
|
|
ydb_OVERLOADED int32 = 400060
|
|
ydb_SCHEME_ERROR int32 = 400070
|
|
ydb_GENERIC_ERROR int32 = 400080
|
|
ydb_TIMEOUT int32 = 400090
|
|
ydb_BAD_SESSION int32 = 400100
|
|
ydb_PRECONDITION_FAILED int32 = 400120
|
|
ydb_ALREADY_EXISTS int32 = 400130
|
|
ydb_NOT_FOUND int32 = 400140
|
|
ydb_SESSION_EXPIRED int32 = 400150
|
|
ydb_CANCELLED int32 = 400160
|
|
ydb_UNDETERMINED int32 = 400170
|
|
ydb_UNSUPPORTED int32 = 400180
|
|
ydb_SESSION_BUSY int32 = 400190
|
|
)
|
|
|
|
// https://github.com/ydb-platform/ydb-go-sdk/blob/ca13feb3ca560ac7385e79d4365ffe0cd8c23e21/errors.go#L27
|
|
func (db *ydb) IsRetryable(err error) bool {
|
|
var target interface {
|
|
error
|
|
Code() int32
|
|
Name() string
|
|
}
|
|
if errors.Is(err, fmt.Errorf("unknown error")) ||
|
|
errors.Is(err, context.DeadlineExceeded) ||
|
|
errors.Is(err, context.Canceled) {
|
|
return false
|
|
}
|
|
if !errors.As(err, &target) {
|
|
return false
|
|
}
|
|
|
|
switch target.Code() {
|
|
case
|
|
int32(ydb_grpc_Unknown),
|
|
int32(ydb_grpc_InvalidArgument),
|
|
int32(ydb_grpc_DeadlineExceeded),
|
|
int32(ydb_grpc_NotFound),
|
|
int32(ydb_grpc_AlreadyExists),
|
|
int32(ydb_grpc_PermissionDenied),
|
|
int32(ydb_grpc_FailedPrecondition),
|
|
int32(ydb_grpc_OutOfRange),
|
|
int32(ydb_grpc_Unimplemented),
|
|
int32(ydb_grpc_DataLoss),
|
|
int32(ydb_grpc_Unauthenticated):
|
|
return false
|
|
case
|
|
int32(ydb_grpc_Canceled),
|
|
int32(ydb_grpc_ResourceExhausted),
|
|
int32(ydb_grpc_Aborted),
|
|
int32(ydb_grpc_Internal),
|
|
int32(ydb_grpc_Unavailable):
|
|
return true
|
|
case
|
|
ydb_STATUS_CODE_UNSPECIFIED,
|
|
ydb_BAD_REQUEST,
|
|
ydb_UNAUTHORIZED,
|
|
ydb_INTERNAL_ERROR,
|
|
ydb_SCHEME_ERROR,
|
|
ydb_GENERIC_ERROR,
|
|
ydb_TIMEOUT,
|
|
ydb_PRECONDITION_FAILED,
|
|
ydb_ALREADY_EXISTS,
|
|
ydb_NOT_FOUND,
|
|
ydb_SESSION_EXPIRED,
|
|
ydb_CANCELLED,
|
|
ydb_UNSUPPORTED:
|
|
return false
|
|
case
|
|
ydb_ABORTED,
|
|
ydb_UNAVAILABLE,
|
|
ydb_OVERLOADED,
|
|
ydb_BAD_SESSION,
|
|
ydb_UNDETERMINED,
|
|
ydb_SESSION_BUSY:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
type ydbDriver struct {
|
|
baseDriver
|
|
}
|
|
|
|
func (ydbDrv *ydbDriver) Features() *DriverFeatures {
|
|
return &DriverFeatures{
|
|
SupportReturnInsertedID: false,
|
|
}
|
|
}
|
|
|
|
// DSN format: https://github.com/ydb-platform/ydb-go-sdk/blob/a804c31be0d3c44dfd7b21ed49d863619217b11d/connection.go#L339
|
|
func (ydbDrv *ydbDriver) Parse(driverName, dataSourceName string) (*URI, error) {
|
|
info := &URI{DBType: schemas.YDB}
|
|
|
|
uri, err := url.Parse(dataSourceName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed on parse data source %v", dataSourceName)
|
|
}
|
|
|
|
const (
|
|
secure = "grpcs"
|
|
insecure = "grpc"
|
|
)
|
|
|
|
if uri.Scheme != secure && uri.Scheme != insecure {
|
|
return nil, fmt.Errorf("unsupported scheme %v", uri.Scheme)
|
|
}
|
|
|
|
info.Host = uri.Host
|
|
if spl := strings.Split(uri.Host, ":"); len(spl) > 1 {
|
|
info.Host = spl[0]
|
|
info.Port = spl[1]
|
|
}
|
|
|
|
info.DBName = uri.Path
|
|
if info.DBName == "" {
|
|
return nil, errors.New("database path can not be empty")
|
|
}
|
|
|
|
if uri.User != nil {
|
|
info.Passwd, _ = uri.User.Password()
|
|
info.User = uri.User.Username()
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// https://pkg.go.dev/database/sql#ColumnType.DatabaseTypeName
|
|
func (ydbDrv *ydbDriver) GenScanResult(columnType string) (interface{}, error) {
|
|
switch columnType = removeOptional(columnType); columnType {
|
|
case yql_Bool:
|
|
var ret sql.NullBool
|
|
return &ret, nil
|
|
case yql_Int16:
|
|
var ret sql.NullInt16
|
|
return &ret, nil
|
|
case yql_Int32:
|
|
var ret sql.NullInt32
|
|
return &ret, nil
|
|
case yql_Int64:
|
|
var ret sql.NullInt64
|
|
return &ret, nil
|
|
case yql_Uint8:
|
|
var ret sql.NullByte
|
|
return &ret, nil
|
|
case yql_Uint32:
|
|
var ret convert.NullUint32
|
|
return &ret, nil
|
|
case yql_Uint64:
|
|
var ret convert.NullUint64
|
|
return &ret, nil
|
|
case yql_Double:
|
|
var ret sql.NullFloat64
|
|
return &ret, nil
|
|
case yql_Utf8:
|
|
var ret sql.NullString
|
|
return &ret, nil
|
|
case yql_Timestamp:
|
|
var ret sql.NullTime
|
|
return &ret, nil
|
|
case yql_Interval:
|
|
var ret convert.NullDuration
|
|
return &ret, nil
|
|
default:
|
|
var ret sql.RawBytes
|
|
return &ret, nil
|
|
}
|
|
}
|
|
|
|
func (ydbDrv *ydbDriver) Scan(ctx *ScanContext, rows *core.Rows, types []*sql.ColumnType, v ...interface{}) error {
|
|
if err := rows.Scan(v...); err != nil {
|
|
return err
|
|
}
|
|
|
|
if ctx.DBLocation == nil {
|
|
return nil
|
|
}
|
|
|
|
for i := range v {
|
|
// !datbeohbbh! YDB saves time in UTC. When returned value is time type, then value will be represented in local time.
|
|
// So value in time type must be converted to DBLocation.
|
|
switch des := v[i].(type) {
|
|
case *time.Time:
|
|
*des = (*des).In(ctx.DBLocation)
|
|
case *sql.NullTime:
|
|
if des.Valid {
|
|
(*des).Time = (*des).Time.In(ctx.DBLocation)
|
|
}
|
|
case *interface{}:
|
|
switch t := (*des).(type) {
|
|
case time.Time:
|
|
*des = t.In(ctx.DBLocation)
|
|
case sql.NullTime:
|
|
if t.Valid {
|
|
*des = t.Time.In(ctx.DBLocation)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// !datbeohbbh! this is a 'helper' function for YDB to bypass the custom type.
|
|
// Example:
|
|
// --
|
|
// type CustomInt int64
|
|
// engine.Where("ID > ?", CustomInt(10)).Get(...)
|
|
// --
|
|
// ydb-go-sdk does not know about `CustomInt` type and will cause error.
|
|
func (ydbDrv *ydbDriver) Cast(paramStr ...interface{}) {
|
|
for i := range paramStr {
|
|
if paramStr[i] == nil {
|
|
continue
|
|
}
|
|
|
|
var (
|
|
val = reflect.ValueOf(paramStr[i])
|
|
res interface{}
|
|
)
|
|
|
|
fieldType := val.Type()
|
|
k := fieldType.Kind()
|
|
if k == reflect.Ptr {
|
|
if val.IsNil() || !val.IsValid() {
|
|
paramStr[i] = val.Interface()
|
|
continue
|
|
} else {
|
|
val = val.Elem()
|
|
fieldType = val.Type()
|
|
k = fieldType.Kind()
|
|
}
|
|
}
|
|
|
|
switch k {
|
|
case reflect.Bool:
|
|
res = val.Bool()
|
|
case reflect.String:
|
|
res = val.String()
|
|
case reflect.Struct:
|
|
if fieldType.ConvertibleTo(schemas.TimeType) {
|
|
res = val.Convert(schemas.TimeType).Interface().(time.Time)
|
|
} else if fieldType.ConvertibleTo(schemas.IntervalType) {
|
|
res = val.Convert(schemas.IntervalType).Interface().(time.Duration)
|
|
} else if fieldType.ConvertibleTo(schemas.NullBoolType) {
|
|
res = val.Convert(schemas.NullBoolType).Interface().(sql.NullBool)
|
|
} else if fieldType.ConvertibleTo(schemas.NullFloat64Type) {
|
|
res = val.Convert(schemas.NullFloat64Type).Interface().(sql.NullFloat64)
|
|
} else if fieldType.ConvertibleTo(schemas.NullInt16Type) {
|
|
res = val.Convert(schemas.NullInt16Type).Interface().(sql.NullInt16)
|
|
} else if fieldType.ConvertibleTo(schemas.NullInt32Type) {
|
|
res = val.Convert(schemas.NullInt32Type).Interface().(sql.NullInt32)
|
|
} else if fieldType.ConvertibleTo(schemas.NullInt64Type) {
|
|
res = val.Convert(schemas.NullInt64Type).Interface().(sql.NullInt64)
|
|
} else if fieldType.ConvertibleTo(schemas.NullStringType) {
|
|
res = val.Convert(schemas.NullStringType).Interface().(sql.NullString)
|
|
} else if fieldType.ConvertibleTo(schemas.NullTimeType) {
|
|
res = val.Convert(schemas.NullTimeType).Interface().(sql.NullTime)
|
|
} else {
|
|
res = val.Interface()
|
|
}
|
|
case reflect.Array, reflect.Slice, reflect.Map:
|
|
res = val.Interface()
|
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
|
val := val.Uint()
|
|
switch k {
|
|
case reflect.Uint8:
|
|
res = uint8(val)
|
|
case reflect.Uint16:
|
|
res = uint16(val)
|
|
case reflect.Uint32:
|
|
res = uint32(val)
|
|
case reflect.Uint64:
|
|
res = uint64(val)
|
|
default:
|
|
res = val
|
|
}
|
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
|
val := val.Int()
|
|
switch k {
|
|
case reflect.Int8:
|
|
res = int8(val)
|
|
case reflect.Int16:
|
|
res = int16(val)
|
|
case reflect.Int32:
|
|
res = int32(val)
|
|
case reflect.Int64:
|
|
res = int64(val)
|
|
default:
|
|
res = val
|
|
}
|
|
default:
|
|
if val.Interface() == nil {
|
|
res = (*[]byte)(nil)
|
|
} else {
|
|
res = val.Interface()
|
|
}
|
|
}
|
|
paramStr[i] = res
|
|
}
|
|
}
|