diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d317d0bb2..8985029261 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -274,6 +274,18 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + ydb: + image: ydbplatform/local-ydb:latest + ports: + - 2135:2135 + - 2136:2136 + - 8765:8765 + env: + YDB_LOCAL_SURVIVE_RESTART: true + YDB_USE_IN_MEMORY_PDISKS: true + YDB_TABLE_ENABLE_PREPARED_DDL: true + YDB_ENABLE_COLUMN_TABLES: true + options: '-h localhost' steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 @@ -476,6 +488,18 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + ydb: + image: ydbplatform/local-ydb:latest + ports: + - 2135:2135 + - 2136:2136 + - 8765:8765 + env: + YDB_LOCAL_SURVIVE_RESTART: true + YDB_USE_IN_MEMORY_PDISKS: true + YDB_TABLE_ENABLE_PREPARED_DDL: true + YDB_ENABLE_COLUMN_TABLES: true + options: '-h localhost' steps: - uses: actions/checkout@v4 with: diff --git a/dialect/dialect.go b/dialect/dialect.go index 3378463480..79bceeb405 100644 --- a/dialect/dialect.go +++ b/dialect/dialect.go @@ -20,6 +20,7 @@ const ( SQLite = "sqlite3" Postgres = "postgres" Gremlin = "gremlin" + YDB = "ydb" ) // ExecQuerier wraps the 2 database operations. diff --git a/dialect/sql/ydb.go b/dialect/sql/ydb.go new file mode 100644 index 0000000000..fbf58b84f9 --- /dev/null +++ b/dialect/sql/ydb.go @@ -0,0 +1,201 @@ +// Copyright 2024-present Facebook Inc. All rights reserved. +// This source code is licensed under the Apache 2.0 license found +// in the LICENSE file in the root directory of this source tree. + +package sql + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "entgo.io/ent/dialect" +) + +// YDB implements the dialect.Dialect interface for YDB. +type YDB struct { + *Driver +} + +// init registers the YDB driver with the SQL dialect. +func init() { + Register(dialect.YDB, &YDB{}) +} + +// Placeholder returns the placeholder for the i'th argument in the query. +func (YDB) Placeholder() string { + return "$" +} + +// Array returns the placeholder for array values in the query. +func (YDB) Array() string { + return "?" +} + +// Drivers returns the list of supported drivers by YDB. +func (YDB) Drivers() []string { + return []string{"ydb"} +} + +// QueryBuilder returns the query builder for YDB. +func (d *YDB) QueryBuilder(b *Builder) *Builder { + b.Quote = func(s string) string { + return fmt.Sprintf("`%s`", s) + } + return b +} + +// Schema returns the schema name of the database. +func (YDB) Schema() string { + return "ydb" +} + +// Version returns the version of the database. +func (d *YDB) Version(ctx context.Context) (*sql.DB, string, error) { + if db, ok := d.DB().(*sql.DB); ok { + var version string + if err := db.QueryRowContext(ctx, "SELECT version()").Scan(&version); err != nil { + return db, "", fmt.Errorf("ydb: failed getting version: %w", err) + } + return db, version, nil + } + return nil, "", fmt.Errorf("ydb: failed getting version: not a *sql.DB") +} + +// YDBType represents a YDB data type. +type YDBType string + +const ( + // YDBTypeInt8 represents YDB INT8 type. + YDBTypeInt8 YDBType = "Int8" + // YDBTypeInt16 represents YDB INT16 type. + YDBTypeInt16 YDBType = "Int16" + // YDBTypeInt32 represents YDB INT32 type. + YDBTypeInt32 YDBType = "Int32" + // YDBTypeInt64 represents YDB INT64 type. + YDBTypeInt64 YDBType = "Int64" + // YDBTypeUint8 represents YDB UINT8 type. + YDBTypeUint8 YDBType = "Uint8" + // YDBTypeUint16 represents YDB UINT16 type. + YDBTypeUint16 YDBType = "Uint16" + // YDBTypeUint32 represents YDB UINT32 type. + YDBTypeUint32 YDBType = "Uint32" + // YDBTypeUint64 represents YDB UINT64 type. + YDBTypeUint64 YDBType = "Uint64" + // YDBTypeFloat represents YDB FLOAT type. + YDBTypeFloat YDBType = "Float" + // YDBTypeDouble represents YDB DOUBLE type. + YDBTypeDouble YDBType = "Double" + // YDBTypeString represents YDB STRING type. + YDBTypeString YDBType = "String" + // YDBTypeBytes represents YDB BYTES type. + YDBTypeBytes YDBType = "Bytes" + // YDBTypeTimestamp represents YDB TIMESTAMP type. + YDBTypeTimestamp YDBType = "Timestamp" + // YDBTypeDate represents YDB DATE type. + YDBTypeDate YDBType = "Date" + // YDBTypeDateTime represents YDB DATETIME type. + YDBTypeDateTime YDBType = "Datetime" + // YDBTypeInterval represents YDB INTERVAL type. + YDBTypeInterval YDBType = "Interval" + // YDBTypeBool represents YDB BOOL type. + YDBTypeBool YDBType = "Bool" + // YDBTypeJSON represents YDB JSON type. + YDBTypeJSON YDBType = "Json" +) + +// ConvertType converts Go type to YDB type. +func (YDB) ConvertType(t interface{}) (YDBType, error) { + switch t.(type) { + case int8: + return YDBTypeInt8, nil + case int16: + return YDBTypeInt16, nil + case int32: + return YDBTypeInt32, nil + case int64: + return YDBTypeInt64, nil + case uint8: + return YDBTypeUint8, nil + case uint16: + return YDBTypeUint16, nil + case uint32: + return YDBTypeUint32, nil + case uint64: + return YDBTypeUint64, nil + case float32: + return YDBTypeFloat, nil + case float64: + return YDBTypeDouble, nil + case string: + return YDBTypeString, nil + case []byte: + return YDBTypeBytes, nil + case bool: + return YDBTypeBool, nil + default: + return "", fmt.Errorf("unsupported type: %T", t) + } +} + +// FormatError formats YDB error messages. +func (d *YDB) FormatError(err error) error { + if err == nil { + return nil + } + msg := err.Error() + if strings.Contains(msg, "not found") { + return &NotFoundError{err} + } + return &Error{err} +} + +// YDBDriver wraps the standard SQL driver to add YDB-specific functionality. +type YDBDriver struct { + *Driver +} + +// Open opens a new YDB connection. +func Open(driverName, dataSourceName string) (*YDBDriver, error) { + db, err := sql.Open(driverName, dataSourceName) + if err != nil { + return nil, err + } + drv := &Driver{DB: db, Dialect: &YDB{}} + return &YDBDriver{drv}, nil +} + +// ExecContext executes a query that doesn't return rows. +// For example, in SQL, INSERT or UPDATE. +func (d *YDBDriver) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { + // YDB-specific query modifications if needed + query = d.modifyQuery(query) + return d.Driver.ExecContext(ctx, query, args...) +} + +// QueryContext executes a query that returns rows, typically a SELECT. +func (d *YDBDriver) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { + // YDB-specific query modifications if needed + query = d.modifyQuery(query) + return d.Driver.QueryContext(ctx, query, args...) +} + +// modifyQuery modifies the SQL query to be compatible with YDB syntax. +func (d *YDBDriver) modifyQuery(query string) string { + // Replace standard SQL syntax with YDB-specific syntax where needed + // For example, replace LIMIT with TOP, adjust JOIN syntax, etc. + query = strings.Replace(query, "LIMIT", "TOP", -1) + return query +} + +// BeginTx starts a new transaction with the given options. +func (d *YDBDriver) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) { + if opts == nil { + opts = &sql.TxOptions{ + Isolation: sql.LevelSerializable, + ReadOnly: false, + } + } + return d.Driver.BeginTx(ctx, opts) +} diff --git a/dialect/sql/ydb_test.go b/dialect/sql/ydb_test.go new file mode 100644 index 0000000000..284d29ddce --- /dev/null +++ b/dialect/sql/ydb_test.go @@ -0,0 +1,92 @@ +package sql + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestYDB_Placeholder(t *testing.T) { + d := YDB{} + assert.Equal(t, "$", d.Placeholder()) +} + +func TestYDB_Array(t *testing.T) { + d := YDB{} + assert.Equal(t, "?", d.Array()) +} + +func TestYDB_Drivers(t *testing.T) { + d := YDB{} + drivers := d.Drivers() + assert.Equal(t, []string{"ydb"}, drivers) +} + +func TestYDB_Schema(t *testing.T) { + d := YDB{} + assert.Equal(t, "ydb", d.Schema()) +} + +func TestYDB_ConvertType(t *testing.T) { + d := YDB{} + tests := []struct { + name string + input interface{} + expected YDBType + wantErr bool + }{ + {"int8", int8(1), YDBTypeInt8, false}, + {"int16", int16(1), YDBTypeInt16, false}, + {"int32", int32(1), YDBTypeInt32, false}, + {"int64", int64(1), YDBTypeInt64, false}, + {"uint8", uint8(1), YDBTypeUint8, false}, + {"uint16", uint16(1), YDBTypeUint16, false}, + {"uint32", uint32(1), YDBTypeUint32, false}, + {"uint64", uint64(1), YDBTypeUint64, false}, + {"float32", float32(1), YDBTypeFloat, false}, + {"float64", float64(1), YDBTypeDouble, false}, + {"string", "test", YDBTypeString, false}, + {"[]byte", []byte("test"), YDBTypeBytes, false}, + {"bool", true, YDBTypeBool, false}, + {"unsupported", struct{}{}, "", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := d.ConvertType(tt.input) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.expected, got) + }) + } +} + +func TestYDBDriver_ModifyQuery(t *testing.T) { + d := &YDBDriver{} + tests := []struct { + name string + input string + expected string + }{ + { + name: "replace LIMIT", + input: "SELECT * FROM users LIMIT 10", + expected: "SELECT * FROM users TOP 10", + }, + { + name: "no modification needed", + input: "SELECT * FROM users", + expected: "SELECT * FROM users", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := d.modifyQuery(tt.input) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/go.mod b/go.mod index 226f757ded..f77b76a7da 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83 github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/go-openapi/inflect v0.19.0 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.0 github.com/jessevdk/go-flags v1.5.0 github.com/json-iterator/go v1.1.12 @@ -41,6 +41,6 @@ require ( github.com/zclconf/go-cty-yaml v1.1.0 // indirect golang.org/x/mod v0.23.0 // indirect golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/text v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2ef4248f96..ea1b22ad6f 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc= @@ -139,8 +139,8 @@ golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=