Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions dialect/dialect.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
SQLite = "sqlite3"
Postgres = "postgres"
Gremlin = "gremlin"
YDB = "ydb"
)

// ExecQuerier wraps the 2 database operations.
Expand Down
201 changes: 201 additions & 0 deletions dialect/sql/ydb.go
Original file line number Diff line number Diff line change
@@ -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{})

Check failure on line 23 in dialect/sql/ydb.go

View workflow job for this annotation

GitHub Actions / lint

undefined: Register
}

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

Check failure on line 43 in dialect/sql/ydb.go

View workflow job for this annotation

GitHub Actions / lint

cannot assign to b.Quote (neither addressable nor a map index expression)
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 {

Check failure on line 56 in dialect/sql/ydb.go

View workflow job for this annotation

GitHub Actions / lint

invalid operation: d.DB() (value of type *"database/sql".DB) is not an interface
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}

Check failure on line 149 in dialect/sql/ydb.go

View workflow job for this annotation

GitHub Actions / lint

undefined: NotFoundError
}
return &Error{err}

Check failure on line 151 in dialect/sql/ydb.go

View workflow job for this annotation

GitHub Actions / lint

undefined: Error
}

// 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) {

Check failure on line 160 in dialect/sql/ydb.go

View workflow job for this annotation

GitHub Actions / lint

Open redeclared in this block
db, err := sql.Open(driverName, dataSourceName)
if err != nil {
return nil, err
}
drv := &Driver{DB: db, Dialect: &YDB{}}

Check failure on line 165 in dialect/sql/ydb.go

View workflow job for this annotation

GitHub Actions / lint

unknown field Dialect in struct literal of type Driver, but does have dialect

Check failure on line 165 in dialect/sql/ydb.go

View workflow job for this annotation

GitHub Actions / lint

unknown field DB in struct literal of type Driver
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)
}
92 changes: 92 additions & 0 deletions dialect/sql/ydb_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
Loading