Skip to content

Commit c7de6ed

Browse files
Implemented basic ent driver for YDB (#2)
1 parent 4d347ca commit c7de6ed

File tree

9 files changed

+549
-98
lines changed

9 files changed

+549
-98
lines changed

dialect/dialect.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const (
2020
SQLite = "sqlite3"
2121
Postgres = "postgres"
2222
Gremlin = "gremlin"
23+
YDB = "ydb"
2324
)
2425

2526
// ExecQuerier wraps the 2 database operations.

dialect/ydb/driver.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2019-present Facebook Inc. All rights reserved.
2+
// This source code is licensed under the Apache 2.0 license found
3+
// in the LICENSE file in the root directory of this source tree.
4+
5+
package ydb
6+
7+
import (
8+
"context"
9+
"database/sql"
10+
11+
"entgo.io/ent/dialect"
12+
entSql "entgo.io/ent/dialect/sql"
13+
ydb "github.com/ydb-platform/ydb-go-sdk/v3"
14+
)
15+
16+
// YDBDriver is a [dialect.Driver] implementation for YDB.
17+
type YDBDriver struct {
18+
*entSql.Driver
19+
20+
nativeDriver *ydb.Driver
21+
}
22+
23+
func Open(ctx context.Context, dsn string) (*YDBDriver, error) {
24+
nativeDriver, err := ydb.Open(ctx, dsn)
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
conn, err := ydb.Connector(
30+
nativeDriver,
31+
ydb.WithAutoDeclare(),
32+
ydb.WithTablePathPrefix(nativeDriver.Name()),
33+
)
34+
if err != nil {
35+
panic(err)
36+
}
37+
38+
dbSQLDriver := sql.OpenDB(conn)
39+
40+
return &YDBDriver{
41+
Driver: entSql.OpenDB(dialect.YDB, dbSQLDriver),
42+
nativeDriver: nativeDriver,
43+
}, nil
44+
}
45+
46+
func (y *YDBDriver) NativeDriver() *ydb.Driver {
47+
return y.nativeDriver
48+
}

dialect/ydb/driver_test.go

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
// Copyright 2019-present Facebook Inc. All rights reserved.
2+
// This source code is licensed under the Apache 2.0 license found
3+
// in the LICENSE file in the root directory of this source tree.
4+
5+
package ydb
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"testing"
11+
12+
"entgo.io/ent/dialect"
13+
entSql "entgo.io/ent/dialect/sql"
14+
15+
"github.com/DATA-DOG/go-sqlmock"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
func TestOpenAndClose(t *testing.T) {
20+
// Given
21+
db, mock, err := sqlmock.New()
22+
require.NoError(t, err)
23+
defer db.Close()
24+
25+
driver := &YDBDriver{
26+
Driver: entSql.OpenDB(dialect.YDB, db),
27+
}
28+
29+
// When
30+
mock.ExpectClose()
31+
err = driver.Close()
32+
33+
// Then - verify closed
34+
require.NoError(t, err, "should close connection")
35+
require.NoError(t, mock.ExpectationsWereMet())
36+
}
37+
38+
func TestExecCreateTable(t *testing.T) {
39+
// Given
40+
db, mock, err := sqlmock.New()
41+
require.NoError(t, err)
42+
defer db.Close()
43+
44+
ctx := context.Background()
45+
driver := &YDBDriver{Driver: entSql.OpenDB(dialect.YDB, db)}
46+
47+
// When
48+
mock.ExpectExec("DROP TABLE IF EXISTS test_users").
49+
WillReturnResult(sqlmock.NewResult(0, 0))
50+
51+
mock.ExpectExec("CREATE TABLE test_users").
52+
WillReturnResult(sqlmock.NewResult(0, 0))
53+
54+
_ = driver.Exec(ctx, "DROP TABLE IF EXISTS test_users", []any{}, nil)
55+
err = driver.Exec(ctx, `CREATE TABLE test_users (
56+
id Int64 NOT NULL,
57+
name Utf8,
58+
age Int32,
59+
PRIMARY KEY (id)
60+
)`, []any{}, nil)
61+
require.NoError(t, err, "CREATE TABLE should execute without err")
62+
63+
// Then - verify table created
64+
mock.ExpectQuery("SELECT 1 FROM test_users").
65+
WillReturnRows(sqlmock.NewRows([]string{"1"}))
66+
67+
var rows entSql.Rows
68+
err = driver.Query(ctx, "SELECT 1 FROM test_users", []any{}, &rows)
69+
require.NoError(t, err, "created table should exist")
70+
rows.Close()
71+
72+
require.NoError(t, mock.ExpectationsWereMet())
73+
}
74+
75+
func TestExecInsert(t *testing.T) {
76+
// Given
77+
db, mock, err := sqlmock.New()
78+
require.NoError(t, err)
79+
defer db.Close()
80+
81+
ctx := context.Background()
82+
driver := &YDBDriver{Driver: entSql.OpenDB(dialect.YDB, db)}
83+
84+
// When
85+
mock.ExpectExec("INSERT INTO test_users").
86+
WillReturnResult(sqlmock.NewResult(1, 1))
87+
88+
insertQuery := `INSERT INTO test_users (id, name, age) VALUES (1, 'Alice', 30)`
89+
err = driver.Exec(ctx, insertQuery, []any{}, nil)
90+
require.NoError(t, err, "INSERT data execute without err")
91+
92+
// Then - verify row count
93+
mock.ExpectQuery("SELECT COUNT\\(\\*\\) AS").
94+
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
95+
96+
var rows entSql.Rows
97+
err = driver.Query(ctx, "SELECT COUNT(*) AS `count` FROM test_users", []any{}, &rows)
98+
require.NoError(t, err, "SELECT COUNT(*) should execute without err")
99+
100+
require.True(t, rows.Next(), "Result should have at least 1 row")
101+
var count uint64
102+
err = rows.Scan(&count)
103+
require.NoError(t, err)
104+
require.Equal(t, uint64(1), count, "Table should contain exactly 1 row")
105+
rows.Close()
106+
107+
require.NoError(t, mock.ExpectationsWereMet())
108+
}
109+
110+
func TestExecUpdate(t *testing.T) {
111+
// Given
112+
db, mock, err := sqlmock.New()
113+
require.NoError(t, err)
114+
defer db.Close()
115+
116+
ctx := context.Background()
117+
drv := &YDBDriver{Driver: entSql.OpenDB(dialect.YDB, db)}
118+
119+
mock.ExpectExec("INSERT INTO test_users").
120+
WillReturnResult(sqlmock.NewResult(1, 1))
121+
122+
insertDataQuery := "INSERT INTO test_users (id, name, age) VALUES (1, 'Alice', 30)"
123+
require.NoError(t, drv.Exec(ctx, insertDataQuery, []any{}, nil))
124+
125+
// When
126+
mock.ExpectExec("UPDATE test_users SET age = 31 WHERE id = 1").
127+
WillReturnResult(sqlmock.NewResult(0, 1))
128+
129+
updateQuery := `UPDATE test_users SET age = 31 WHERE id = 1`
130+
err = drv.Exec(ctx, updateQuery, []any{}, nil)
131+
require.NoError(t, err, "should update data")
132+
133+
// Then
134+
mock.ExpectQuery("SELECT \\* FROM test_users").
135+
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"}).
136+
AddRow(1, "Alice", 31))
137+
138+
var rows entSql.Rows
139+
err = drv.Query(ctx, "SELECT * FROM test_users", []any{}, &rows)
140+
require.NoError(t, err)
141+
142+
require.True(t, rows.Next())
143+
var id, age int64
144+
var name string
145+
err = rows.Scan(&id, &name, &age)
146+
require.NoError(t, err)
147+
require.Equal(t, int64(31), age, "Age should've been changed")
148+
rows.Close()
149+
150+
require.NoError(t, mock.ExpectationsWereMet())
151+
}
152+
153+
func TestExecDelete(t *testing.T) {
154+
// Given
155+
db, mock, err := sqlmock.New()
156+
require.NoError(t, err)
157+
defer db.Close()
158+
159+
ctx := context.Background()
160+
drv := &YDBDriver{Driver: entSql.OpenDB(dialect.YDB, db)}
161+
162+
mock.ExpectExec("INSERT INTO test_users").
163+
WillReturnResult(sqlmock.NewResult(1, 1))
164+
165+
insertDataQuery := "INSERT INTO test_users (id, name, age) VALUES (1, 'Alice', 30)"
166+
require.NoError(t, drv.Exec(ctx, insertDataQuery, []any{}, nil))
167+
168+
// When
169+
mock.ExpectExec("DELETE FROM test_users WHERE id = 1").
170+
WillReturnResult(sqlmock.NewResult(0, 1))
171+
172+
deleteQuery := `DELETE FROM test_users WHERE id = 1`
173+
err = drv.Exec(ctx, deleteQuery, []any{}, nil)
174+
require.NoError(t, err, "DELETE request should execute without err")
175+
176+
// Then
177+
mock.ExpectQuery("SELECT COUNT\\(\\*\\)").
178+
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
179+
180+
var rows entSql.Rows
181+
err = drv.Query(ctx, "SELECT COUNT(*) AS `count` FROM test_users", []any{}, &rows)
182+
require.NoError(t, err)
183+
require.True(t, rows.Next())
184+
var count uint64
185+
err = rows.Scan(&count)
186+
require.NoError(t, err)
187+
require.Equal(t, uint64(0), count, "Table should be empty after DELETE")
188+
rows.Close()
189+
190+
require.NoError(t, mock.ExpectationsWereMet())
191+
}
192+
193+
func TestQueryEmptyTable(t *testing.T) {
194+
// Given
195+
db, mock, err := sqlmock.New()
196+
require.NoError(t, err)
197+
defer db.Close()
198+
199+
ctx := context.Background()
200+
drv := &YDBDriver{Driver: entSql.OpenDB(dialect.YDB, db)}
201+
202+
// When
203+
mock.ExpectQuery("SELECT \\* FROM test_users").
204+
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"}))
205+
206+
var rows entSql.Rows
207+
err = drv.Query(ctx, "SELECT * FROM test_users", []any{}, &rows)
208+
209+
// Then
210+
require.NoError(t, err, "SELECT data should execute without err")
211+
212+
counter := 0
213+
for rows.Next() {
214+
counter++
215+
}
216+
require.Equal(t, 0, counter, "Table should be empty")
217+
rows.Close()
218+
219+
require.NoError(t, mock.ExpectationsWereMet())
220+
}
221+
222+
func TestExecMultipleInserts(t *testing.T) {
223+
// Given
224+
db, mock, err := sqlmock.New()
225+
require.NoError(t, err)
226+
defer db.Close()
227+
228+
ctx := context.Background()
229+
drv := &YDBDriver{Driver: entSql.OpenDB(dialect.YDB, db)}
230+
231+
// When
232+
for i := 0; i < 10; i++ {
233+
insertQuery := fmt.Sprintf("INSERT INTO test_users (id, name, age) VALUES (%d, 'User%d', 20)", i, i)
234+
mock.ExpectExec("INSERT INTO test_users").
235+
WillReturnResult(sqlmock.NewResult(int64(i), 1))
236+
237+
err := drv.Exec(ctx, insertQuery, []any{}, nil)
238+
require.NoError(t, err)
239+
}
240+
241+
// Then
242+
mock.ExpectQuery("SELECT COUNT\\(\\*\\)").
243+
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(10))
244+
245+
var rows entSql.Rows
246+
err = drv.Query(ctx, "SELECT COUNT(*) AS `count` FROM test_users", []any{}, &rows)
247+
require.NoError(t, err)
248+
require.True(t, rows.Next())
249+
var count uint64
250+
err = rows.Scan(&count)
251+
require.NoError(t, err)
252+
require.Equal(t, uint64(10), count, "Table should contain exactly 10 rows")
253+
rows.Close()
254+
255+
require.NoError(t, mock.ExpectationsWereMet())
256+
}
257+
258+
func TestQueryInvalidQuery(t *testing.T) {
259+
// Given
260+
db, mock, err := sqlmock.New()
261+
require.NoError(t, err)
262+
defer db.Close()
263+
264+
ctx := context.Background()
265+
drv := &YDBDriver{Driver: entSql.OpenDB(dialect.YDB, db)}
266+
267+
// When
268+
invalidQuery := "SELECT * FROM non_existent_table"
269+
mock.ExpectQuery("SELECT \\* FROM non_existent_table").
270+
WillReturnError(fmt.Errorf("table not found"))
271+
272+
var rows entSql.Rows
273+
err = drv.Query(ctx, invalidQuery, []any{}, &rows)
274+
275+
// Then
276+
require.Error(t, err, "should return error for invalid query")
277+
require.NoError(t, mock.ExpectationsWereMet())
278+
}
279+
280+
func TestContextCancellation(t *testing.T) {
281+
// Given
282+
db, _, err := sqlmock.New()
283+
require.NoError(t, err)
284+
defer db.Close()
285+
286+
ctx := context.Background()
287+
drv := &YDBDriver{Driver: entSql.OpenDB(dialect.YDB, db)}
288+
289+
// When
290+
cancelCtx, cancel := context.WithCancel(ctx)
291+
cancel()
292+
293+
// Then
294+
err = drv.Exec(cancelCtx, "SELECT 1", []any{}, nil)
295+
require.Error(t, err, "should return error when context is cancelled")
296+
require.Contains(t, err.Error(), "context canceled")
297+
}

0 commit comments

Comments
 (0)