ydb-sqlalchemy 0.0.1b4__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
test/test_core.py ADDED
@@ -0,0 +1,734 @@
1
+ import asyncio
2
+ from datetime import date, datetime
3
+ from decimal import Decimal
4
+ from typing import NamedTuple
5
+
6
+ import pytest
7
+ import sqlalchemy as sa
8
+ import ydb
9
+ from sqlalchemy import Column, Integer, String, Table, Unicode
10
+ from sqlalchemy.testing.fixtures import TablesTest, TestBase, config
11
+ from ydb._grpc.v4.protos import ydb_common_pb2
12
+
13
+ from ydb_sqlalchemy import IsolationLevel, dbapi
14
+ from ydb_sqlalchemy import sqlalchemy as ydb_sa
15
+ from ydb_sqlalchemy.sqlalchemy import types
16
+
17
+
18
+ def clear_sql(stm):
19
+ return stm.replace("\n", " ").replace(" ", " ").strip()
20
+
21
+
22
+ class TestText(TestBase):
23
+ __backend__ = True
24
+
25
+ def test_sa_text(self, connection):
26
+ rs = connection.execute(sa.text("SELECT 1 AS value"))
27
+ assert rs.fetchone() == (1,)
28
+
29
+ rs = connection.execute(
30
+ sa.text(
31
+ """
32
+ DECLARE :data AS List<Struct<x:Int64, y:Int64>>;
33
+ SELECT x, y FROM AS_TABLE(:data)
34
+ """
35
+ ),
36
+ [{"data": [{"x": 2, "y": 1}, {"x": 3, "y": 2}]}],
37
+ )
38
+ assert set(rs.fetchall()) == {(2, 1), (3, 2)}
39
+
40
+
41
+ class TestCrud(TablesTest):
42
+ __backend__ = True
43
+
44
+ @classmethod
45
+ def define_tables(cls, metadata):
46
+ Table(
47
+ "test",
48
+ metadata,
49
+ Column("id", Integer, primary_key=True),
50
+ Column("text", Unicode),
51
+ )
52
+
53
+ def test_sa_crud(self, connection):
54
+ select_stm = sa.select(self.tables.test)
55
+ assert clear_sql(str(select_stm)) == "SELECT test.id, test.text FROM test"
56
+ assert connection.execute(select_stm).fetchall() == []
57
+
58
+ insert_stm = sa.insert(self.tables.test).values(id=2, text="foo")
59
+ assert clear_sql(str(insert_stm)) == "INSERT INTO test (id, text) VALUES (:id, :text)"
60
+ assert connection.execute(insert_stm)
61
+
62
+ insert_many = sa.insert(self.tables.test).values([(3, "a"), (4, "b"), (5, "c")])
63
+ assert connection.execute(insert_many)
64
+
65
+ rs = connection.execute(select_stm.order_by(self.tables.test.c.id))
66
+ row = rs.fetchone()
67
+ assert row.id == 2
68
+ assert row.text == "foo"
69
+
70
+ update_stm = sa.update(self.tables.test).where(self.tables.test.c.id == 2).values(text="bar")
71
+ assert clear_sql(str(update_stm)) == "UPDATE test SET text=:text WHERE test.id = :id_1"
72
+ assert connection.execute(update_stm)
73
+
74
+ select_where_stm = sa.select(self.tables.test.c.text).filter(self.tables.test.c.id == 2)
75
+ assert clear_sql(str(select_where_stm)) == "SELECT test.text FROM test WHERE test.id = :id_1"
76
+ assert connection.execute(select_where_stm).fetchall() == [("bar",)]
77
+
78
+ delete_stm = sa.delete(self.tables.test).where(self.tables.test.c.id == 2)
79
+ assert connection.execute(delete_stm)
80
+ assert connection.execute(select_stm.order_by(self.tables.test.c.id)).fetchall() == [
81
+ (3, "a"),
82
+ (4, "b"),
83
+ (5, "c"),
84
+ ]
85
+
86
+
87
+ class TestSimpleSelect(TablesTest):
88
+ __backend__ = True
89
+
90
+ @classmethod
91
+ def define_tables(cls, metadata):
92
+ Table(
93
+ "test",
94
+ metadata,
95
+ Column("id", Integer, primary_key=True, nullable=False),
96
+ Column("value", Unicode),
97
+ Column("num", sa.Numeric(22, 9)),
98
+ )
99
+
100
+ @classmethod
101
+ def insert_data(cls, connection):
102
+ data = [
103
+ {"id": 1, "value": "some text", "num": Decimal("3.141592653")},
104
+ {"id": 2, "value": "test text", "num": Decimal("3.14159265")},
105
+ {"id": 3, "value": "test test", "num": Decimal("3.1415926")},
106
+ {"id": 4, "value": "text text", "num": Decimal("3.141592")},
107
+ {"id": 5, "value": "some some", "num": Decimal("3.14159")},
108
+ {"id": 6, "value": "some test", "num": Decimal("3.1415")},
109
+ {"id": 7, "value": "text text", "num": Decimal("3.141")},
110
+ ]
111
+ connection.execute(cls.tables.test.insert(), data)
112
+
113
+ def test_sa_select_simple(self, connection):
114
+ tb = self.tables.test
115
+
116
+ # simple WHERE
117
+ row = connection.execute(sa.select(tb.c.value).where(tb.c.id == 7)).fetchone()
118
+ assert row.value == "text text"
119
+
120
+ # simple filter
121
+ row = connection.execute(tb.select().filter(tb.c.id == 7)).fetchone()
122
+ assert row == (7, "text text", Decimal("3.141"))
123
+
124
+ # OR operator
125
+ rows = connection.execute(tb.select().where((tb.c.id == 1) | (tb.c.value == "test test"))).fetchall()
126
+ assert set(rows) == {
127
+ (1, "some text", Decimal("3.141592653")),
128
+ (3, "test test", Decimal("3.1415926")),
129
+ }
130
+
131
+ # AND operator, LIKE operator
132
+ cur = connection.execute(tb.select().where(tb.c.value.like("some %") & (tb.c.num > Decimal("3.141592"))))
133
+ assert cur.fetchall() == [(1, "some text", Decimal("3.141592653"))]
134
+
135
+ # + operator, CAST
136
+ rows = connection.execute(tb.select().where(tb.c.id + sa.cast(tb.c.num, sa.INTEGER) > 9)).fetchall()
137
+ assert rows == [(7, "text text", Decimal("3.141"))]
138
+
139
+ # REGEXP matching
140
+ stm = tb.select().where(tb.c.value.regexp_match(r"s\w{3}\ss\w{3}"))
141
+ rows = connection.execute(stm).fetchall()
142
+ assert rows == [(5, "some some", Decimal("3.14159"))]
143
+
144
+ stm = sa.select(tb.c.id).where(~tb.c.value.regexp_match(r"s\w{3}\ss\w{3}"))
145
+ rows = connection.execute(stm).fetchall()
146
+ assert set(rows) == {(1,), (2,), (3,), (4,), (6,), (7,)}
147
+
148
+ # LIMIT/OFFSET
149
+ # rows = connection.execute(tb.select().order_by(tb.c.id).limit(2)).fetchall()
150
+ # assert rows == [
151
+ # (1, "some text", Decimal("3.141592653")),
152
+ # (2, "test text", Decimal("3.14159265")),
153
+ # ]
154
+
155
+ # ORDER BY ASC
156
+ rows = connection.execute(sa.select(tb.c.id).order_by(tb.c.id)).fetchall()
157
+ assert rows == [(1,), (2,), (3,), (4,), (5,), (6,), (7,)]
158
+
159
+ # ORDER BY DESC
160
+ rows = connection.execute(sa.select(tb.c.id).order_by(tb.c.id.desc())).fetchall()
161
+ assert rows == [(7,), (6,), (5,), (4,), (3,), (2,), (1,)]
162
+
163
+ # BETWEEN operator
164
+ rows = connection.execute(sa.select(tb.c.id).filter(tb.c.id.between(3, 5))).fetchall()
165
+ assert set(rows) == {(3,), (4,), (5,)}
166
+
167
+ # IN operator
168
+ rows = connection.execute(sa.select(tb.c.id).filter(tb.c.id.in_([1, 3, 5, 7]))).fetchall()
169
+ assert set(rows) == {(1,), (3,), (5,), (7,)}
170
+
171
+ # aggregates: MIN, MAX, COUNT, AVG, SUM
172
+ assert connection.execute(sa.func.min(tb.c.id)).first() == (1,)
173
+ assert connection.execute(sa.func.max(tb.c.id)).first() == (7,)
174
+ assert connection.execute(sa.func.count(tb.c.id)).first() == (7,)
175
+ assert connection.execute(sa.func.sum(tb.c.id)).first() == (28,)
176
+ assert connection.execute(sa.func.avg(tb.c.id)).first() == (4,)
177
+ assert connection.execute(sa.func.sum(tb.c.num)).first() == (Decimal("21.990459903"),)
178
+ assert connection.execute(sa.func.avg(tb.c.num)).first() == (Decimal("3.141494272"),)
179
+
180
+
181
+ class TestTypes(TablesTest):
182
+ __backend__ = True
183
+
184
+ @classmethod
185
+ def define_tables(cls, metadata):
186
+ Table(
187
+ "test_types",
188
+ metadata,
189
+ Column("id", Integer, primary_key=True),
190
+ # Column("bin", sa.BINARY),
191
+ Column("str", sa.String),
192
+ Column("num", sa.Float),
193
+ Column("bl", sa.Boolean),
194
+ Column("ts", sa.TIMESTAMP),
195
+ Column("date", sa.Date),
196
+ # Column("interval", sa.Interval),
197
+ )
198
+
199
+ def test_select_types(self, connection):
200
+ tb = self.tables.test_types
201
+
202
+ now, today = datetime.now(), date.today()
203
+
204
+ stm = tb.insert().values(
205
+ id=1,
206
+ # bin=b"abc",
207
+ str="Hello World!",
208
+ num=3.5,
209
+ bl=True,
210
+ ts=now,
211
+ date=today,
212
+ # interval=timedelta(minutes=45),
213
+ )
214
+ connection.execute(stm)
215
+
216
+ row = connection.execute(sa.select(tb)).fetchone()
217
+ assert row == (1, "Hello World!", 3.5, True, now, today)
218
+
219
+ def test_integer_types(self, connection):
220
+ stmt = sa.Select(
221
+ sa.func.FormatType(sa.func.TypeOf(sa.bindparam("p_uint8", 8, types.UInt8))),
222
+ sa.func.FormatType(sa.func.TypeOf(sa.bindparam("p_uint16", 16, types.UInt16))),
223
+ sa.func.FormatType(sa.func.TypeOf(sa.bindparam("p_uint32", 32, types.UInt32))),
224
+ sa.func.FormatType(sa.func.TypeOf(sa.bindparam("p_uint64", 64, types.UInt64))),
225
+ sa.func.FormatType(sa.func.TypeOf(sa.bindparam("p_int8", -8, types.Int8))),
226
+ sa.func.FormatType(sa.func.TypeOf(sa.bindparam("p_int16", -16, types.Int16))),
227
+ sa.func.FormatType(sa.func.TypeOf(sa.bindparam("p_int32", -32, types.Int32))),
228
+ sa.func.FormatType(sa.func.TypeOf(sa.bindparam("p_int64", -64, types.Int64))),
229
+ )
230
+
231
+ result = connection.execute(stmt).fetchone()
232
+ assert result == (b"Uint8", b"Uint16", b"Uint32", b"Uint64", b"Int8", b"Int16", b"Int32", b"Int64")
233
+
234
+
235
+ class TestWithClause(TablesTest):
236
+ __backend__ = True
237
+ run_create_tables = "each"
238
+
239
+ @staticmethod
240
+ def _create_table_and_get_desc(connection, metadata, **kwargs):
241
+ table = Table(
242
+ "clause_with_test",
243
+ metadata,
244
+ Column("id", types.UInt32, primary_key=True),
245
+ **kwargs,
246
+ )
247
+ table.create(connection)
248
+
249
+ return connection.connection.driver_connection.describe(table.name)
250
+
251
+ @pytest.mark.parametrize(
252
+ "auto_partitioning_by_size,res",
253
+ [
254
+ (None, ydb_common_pb2.FeatureFlag.Status.ENABLED),
255
+ (True, ydb_common_pb2.FeatureFlag.Status.ENABLED),
256
+ (False, ydb_common_pb2.FeatureFlag.Status.DISABLED),
257
+ ],
258
+ )
259
+ def test_auto_partitioning_by_size(self, connection, auto_partitioning_by_size, res, metadata):
260
+ desc = self._create_table_and_get_desc(
261
+ connection, metadata, ydb_auto_partitioning_by_size=auto_partitioning_by_size
262
+ )
263
+ assert desc.partitioning_settings.partitioning_by_size == res
264
+
265
+ @pytest.mark.parametrize(
266
+ "auto_partitioning_by_load,res",
267
+ [
268
+ (None, ydb_common_pb2.FeatureFlag.Status.DISABLED),
269
+ (True, ydb_common_pb2.FeatureFlag.Status.ENABLED),
270
+ (False, ydb_common_pb2.FeatureFlag.Status.DISABLED),
271
+ ],
272
+ )
273
+ def test_auto_partitioning_by_load(self, connection, auto_partitioning_by_load, res, metadata):
274
+ desc = self._create_table_and_get_desc(
275
+ connection,
276
+ metadata,
277
+ ydb_auto_partitioning_by_load=auto_partitioning_by_load,
278
+ )
279
+ assert desc.partitioning_settings.partitioning_by_load == res
280
+
281
+ @pytest.mark.parametrize(
282
+ "auto_partitioning_partition_size_mb,res",
283
+ [
284
+ (None, 2048),
285
+ (2000, 2000),
286
+ ],
287
+ )
288
+ def test_auto_partitioning_partition_size_mb(self, connection, auto_partitioning_partition_size_mb, res, metadata):
289
+ desc = self._create_table_and_get_desc(
290
+ connection,
291
+ metadata,
292
+ ydb_auto_partitioning_partition_size_mb=auto_partitioning_partition_size_mb,
293
+ )
294
+ assert desc.partitioning_settings.partition_size_mb == res
295
+
296
+ @pytest.mark.parametrize(
297
+ "auto_partitioning_min_partitions_count,res",
298
+ [
299
+ (None, 1),
300
+ (10, 10),
301
+ ],
302
+ )
303
+ def test_auto_partitioning_min_partitions_count(
304
+ self,
305
+ connection,
306
+ auto_partitioning_min_partitions_count,
307
+ res,
308
+ metadata,
309
+ ):
310
+ desc = self._create_table_and_get_desc(
311
+ connection,
312
+ metadata,
313
+ ydb_auto_partitioning_min_partitions_count=auto_partitioning_min_partitions_count,
314
+ )
315
+ assert desc.partitioning_settings.min_partitions_count == res
316
+
317
+ @pytest.mark.parametrize(
318
+ "auto_partitioning_max_partitions_count,res",
319
+ [
320
+ (None, 0),
321
+ (10, 10),
322
+ ],
323
+ )
324
+ def test_auto_partitioning_max_partitions_count(
325
+ self,
326
+ connection,
327
+ auto_partitioning_max_partitions_count,
328
+ res,
329
+ metadata,
330
+ ):
331
+ desc = self._create_table_and_get_desc(
332
+ connection,
333
+ metadata,
334
+ ydb_auto_partitioning_max_partitions_count=auto_partitioning_max_partitions_count,
335
+ )
336
+ assert desc.partitioning_settings.max_partitions_count == res
337
+
338
+ @pytest.mark.parametrize(
339
+ "uniform_partitions,res",
340
+ [
341
+ (None, 1),
342
+ (10, 10),
343
+ ],
344
+ )
345
+ def test_uniform_partitions(
346
+ self,
347
+ connection,
348
+ uniform_partitions,
349
+ res,
350
+ metadata,
351
+ ):
352
+ desc = self._create_table_and_get_desc(
353
+ connection,
354
+ metadata,
355
+ ydb_uniform_partitions=uniform_partitions,
356
+ )
357
+ # it not only do the initiation partition but also set up the minimum partition count
358
+ assert desc.partitioning_settings.min_partitions_count == res
359
+
360
+ @pytest.mark.parametrize(
361
+ "partition_at_keys,res",
362
+ [
363
+ (None, 1),
364
+ ((100, 1000), 3),
365
+ ],
366
+ )
367
+ def test_partition_at_keys(
368
+ self,
369
+ connection,
370
+ partition_at_keys,
371
+ res,
372
+ metadata,
373
+ ):
374
+ desc = self._create_table_and_get_desc(
375
+ connection,
376
+ metadata,
377
+ ydb_partition_at_keys=partition_at_keys,
378
+ )
379
+ assert desc.partitioning_settings.min_partitions_count == res
380
+
381
+ def test_several_keys(self, connection, metadata):
382
+ desc = self._create_table_and_get_desc(
383
+ connection,
384
+ metadata,
385
+ ydb_auto_partitioning_by_size=True,
386
+ ydb_auto_partitioning_by_load=True,
387
+ ydb_auto_partitioning_min_partitions_count=3,
388
+ ydb_auto_partitioning_max_partitions_count=5,
389
+ )
390
+ assert desc.partitioning_settings.partitioning_by_size == 1
391
+ assert desc.partitioning_settings.partitioning_by_load == 1
392
+ assert desc.partitioning_settings.min_partitions_count == 3
393
+ assert desc.partitioning_settings.max_partitions_count == 5
394
+
395
+
396
+ class TestTransaction(TablesTest):
397
+ __backend__ = True
398
+
399
+ @classmethod
400
+ def define_tables(cls, metadata: sa.MetaData):
401
+ Table(
402
+ "test",
403
+ metadata,
404
+ Column("id", Integer, primary_key=True),
405
+ )
406
+
407
+ def test_rollback(self, connection_no_trans: sa.Connection, connection: sa.Connection):
408
+ table = self.tables.test
409
+
410
+ connection_no_trans.execution_options(isolation_level=IsolationLevel.SERIALIZABLE)
411
+ with connection_no_trans.begin():
412
+ stm1 = table.insert().values(id=1)
413
+ connection_no_trans.execute(stm1)
414
+ stm2 = table.insert().values(id=2)
415
+ connection_no_trans.execute(stm2)
416
+ connection_no_trans.rollback()
417
+
418
+ cursor = connection.execute(sa.select(table))
419
+ result = cursor.fetchall()
420
+ assert result == []
421
+
422
+ def test_commit(self, connection_no_trans: sa.Connection, connection: sa.Connection):
423
+ table = self.tables.test
424
+
425
+ connection_no_trans.execution_options(isolation_level=IsolationLevel.SERIALIZABLE)
426
+ with connection_no_trans.begin():
427
+ stm1 = table.insert().values(id=3)
428
+ connection_no_trans.execute(stm1)
429
+ stm2 = table.insert().values(id=4)
430
+ connection_no_trans.execute(stm2)
431
+
432
+ cursor = connection.execute(sa.select(table))
433
+ result = cursor.fetchall()
434
+ assert set(result) == {(3,), (4,)}
435
+
436
+ @pytest.mark.parametrize("isolation_level", (IsolationLevel.SERIALIZABLE, IsolationLevel.SNAPSHOT_READONLY))
437
+ def test_interactive_transaction(
438
+ self, connection_no_trans: sa.Connection, connection: sa.Connection, isolation_level
439
+ ):
440
+ table = self.tables.test
441
+ dbapi_connection: dbapi.Connection = connection_no_trans.connection.dbapi_connection
442
+
443
+ stm1 = table.insert().values([{"id": 5}, {"id": 6}])
444
+ connection.execute(stm1)
445
+
446
+ connection_no_trans.execution_options(isolation_level=isolation_level)
447
+ with connection_no_trans.begin():
448
+ tx_id = dbapi_connection.tx_context.tx_id
449
+ assert tx_id is not None
450
+ cursor1 = connection_no_trans.execute(sa.select(table))
451
+ cursor2 = connection_no_trans.execute(sa.select(table))
452
+ assert dbapi_connection.tx_context.tx_id == tx_id
453
+
454
+ assert set(cursor1.fetchall()) == {(5,), (6,)}
455
+ assert set(cursor2.fetchall()) == {(5,), (6,)}
456
+
457
+ @pytest.mark.parametrize(
458
+ "isolation_level",
459
+ (
460
+ IsolationLevel.ONLINE_READONLY,
461
+ IsolationLevel.ONLINE_READONLY_INCONSISTENT,
462
+ IsolationLevel.STALE_READONLY,
463
+ IsolationLevel.AUTOCOMMIT,
464
+ ),
465
+ )
466
+ def test_not_interactive_transaction(
467
+ self, connection_no_trans: sa.Connection, connection: sa.Connection, isolation_level
468
+ ):
469
+ table = self.tables.test
470
+ dbapi_connection: dbapi.Connection = connection_no_trans.connection.dbapi_connection
471
+
472
+ stm1 = table.insert().values([{"id": 7}, {"id": 8}])
473
+ connection.execute(stm1)
474
+
475
+ connection_no_trans.execution_options(isolation_level=isolation_level)
476
+ with connection_no_trans.begin():
477
+ assert dbapi_connection.tx_context is None
478
+ cursor1 = connection_no_trans.execute(sa.select(table))
479
+ cursor2 = connection_no_trans.execute(sa.select(table))
480
+ assert dbapi_connection.tx_context is None
481
+
482
+ assert set(cursor1.fetchall()) == {(7,), (8,)}
483
+ assert set(cursor2.fetchall()) == {(7,), (8,)}
484
+
485
+
486
+ class TestTransactionIsolationLevel(TestBase):
487
+ __backend__ = True
488
+
489
+ class IsolationSettings(NamedTuple):
490
+ ydb_mode: ydb.AbstractTransactionModeBuilder
491
+ interactive: bool
492
+
493
+ YDB_ISOLATION_SETTINGS_MAP = {
494
+ IsolationLevel.AUTOCOMMIT: IsolationSettings(ydb.SerializableReadWrite().name, False),
495
+ IsolationLevel.SERIALIZABLE: IsolationSettings(ydb.SerializableReadWrite().name, True),
496
+ IsolationLevel.ONLINE_READONLY: IsolationSettings(ydb.OnlineReadOnly().name, False),
497
+ IsolationLevel.ONLINE_READONLY_INCONSISTENT: IsolationSettings(
498
+ ydb.OnlineReadOnly().with_allow_inconsistent_reads().name, False
499
+ ),
500
+ IsolationLevel.STALE_READONLY: IsolationSettings(ydb.StaleReadOnly().name, False),
501
+ IsolationLevel.SNAPSHOT_READONLY: IsolationSettings(ydb.SnapshotReadOnly().name, True),
502
+ }
503
+
504
+ def test_connection_set(self, connection_no_trans: sa.Connection):
505
+ dbapi_connection: dbapi.Connection = connection_no_trans.connection.dbapi_connection
506
+
507
+ for sa_isolation_level, ydb_isolation_settings in self.YDB_ISOLATION_SETTINGS_MAP.items():
508
+ connection_no_trans.execution_options(isolation_level=sa_isolation_level)
509
+ with connection_no_trans.begin():
510
+ assert dbapi_connection.tx_mode.name == ydb_isolation_settings[0]
511
+ assert dbapi_connection.interactive_transaction is ydb_isolation_settings[1]
512
+ if dbapi_connection.interactive_transaction:
513
+ assert dbapi_connection.tx_context is not None
514
+ assert dbapi_connection.tx_context.tx_id is not None
515
+ else:
516
+ assert dbapi_connection.tx_context is None
517
+
518
+
519
+ class TestEngine(TestBase):
520
+ __backend__ = True
521
+ __only_on__ = "yql+ydb"
522
+
523
+ @pytest.fixture(scope="class")
524
+ def ydb_driver(self):
525
+ url = config.db_url
526
+ driver = ydb.Driver(endpoint=f"grpc://{url.host}:{url.port}", database=url.database)
527
+ try:
528
+ driver.wait(timeout=5, fail_fast=True)
529
+ yield driver
530
+ finally:
531
+ driver.stop()
532
+
533
+ driver.stop()
534
+
535
+ @pytest.fixture(scope="class")
536
+ def ydb_pool(self, ydb_driver):
537
+ session_pool = ydb.SessionPool(ydb_driver, size=5, workers_threads_count=1)
538
+
539
+ try:
540
+ yield session_pool
541
+ finally:
542
+ session_pool.stop()
543
+
544
+ def test_sa_queue_pool_with_ydb_shared_session_pool(self, ydb_driver, ydb_pool):
545
+ engine1 = sa.create_engine(config.db_url, poolclass=sa.QueuePool, connect_args={"ydb_session_pool": ydb_pool})
546
+ engine2 = sa.create_engine(config.db_url, poolclass=sa.QueuePool, connect_args={"ydb_session_pool": ydb_pool})
547
+
548
+ with engine1.connect() as conn1, engine2.connect() as conn2:
549
+ dbapi_conn1: dbapi.Connection = conn1.connection.dbapi_connection
550
+ dbapi_conn2: dbapi.Connection = conn2.connection.dbapi_connection
551
+
552
+ assert dbapi_conn1.session_pool is dbapi_conn2.session_pool
553
+ assert dbapi_conn1.driver is dbapi_conn2.driver
554
+
555
+ engine1.dispose()
556
+ engine2.dispose()
557
+ assert not ydb_driver._stopped
558
+
559
+ def test_sa_null_pool_with_ydb_shared_session_pool(self, ydb_driver, ydb_pool):
560
+ engine1 = sa.create_engine(config.db_url, poolclass=sa.NullPool, connect_args={"ydb_session_pool": ydb_pool})
561
+ engine2 = sa.create_engine(config.db_url, poolclass=sa.NullPool, connect_args={"ydb_session_pool": ydb_pool})
562
+
563
+ with engine1.connect() as conn1, engine2.connect() as conn2:
564
+ dbapi_conn1: dbapi.Connection = conn1.connection.dbapi_connection
565
+ dbapi_conn2: dbapi.Connection = conn2.connection.dbapi_connection
566
+
567
+ assert dbapi_conn1.session_pool is dbapi_conn2.session_pool
568
+ assert dbapi_conn1.driver is dbapi_conn2.driver
569
+
570
+ engine1.dispose()
571
+ engine2.dispose()
572
+ assert not ydb_driver._stopped
573
+
574
+
575
+ class TestAsyncEngine(TestEngine):
576
+ __only_on__ = "yql+ydb_async"
577
+
578
+ @pytest.fixture(scope="class")
579
+ def ydb_driver(self):
580
+ loop = asyncio.get_event_loop()
581
+ url = config.db_url
582
+ driver = ydb.aio.Driver(endpoint=f"grpc://{url.host}:{url.port}", database=url.database)
583
+ try:
584
+ loop.run_until_complete(driver.wait(timeout=5, fail_fast=True))
585
+ yield driver
586
+ finally:
587
+ loop.run_until_complete(driver.stop())
588
+
589
+ @pytest.fixture(scope="class")
590
+ def ydb_pool(self, ydb_driver):
591
+ session_pool = ydb.aio.SessionPool(ydb_driver, size=5)
592
+
593
+ try:
594
+ yield session_pool
595
+ finally:
596
+ loop = asyncio.get_event_loop()
597
+ loop.run_until_complete(session_pool.stop())
598
+
599
+
600
+ class TestUpsert(TablesTest):
601
+ __backend__ = True
602
+
603
+ @classmethod
604
+ def define_tables(cls, metadata):
605
+ Table(
606
+ "test_upsert",
607
+ metadata,
608
+ Column("id", Integer, primary_key=True),
609
+ Column("val", Integer),
610
+ )
611
+
612
+ def test_string(self, connection):
613
+ tb = self.tables.test_upsert
614
+ stm = ydb_sa.upsert(tb).values(id=0, val=5)
615
+
616
+ assert str(stm) == "UPSERT INTO test_upsert (id, val) VALUES (?, ?)"
617
+
618
+ def test_upsert_new_id(self, connection):
619
+ tb = self.tables.test_upsert
620
+ stm = ydb_sa.upsert(tb).values(id=0, val=1)
621
+ connection.execute(stm)
622
+ row = connection.execute(sa.select(tb)).fetchall()
623
+ assert row == [(0, 1)]
624
+
625
+ stm = ydb_sa.upsert(tb).values(id=1, val=2)
626
+ connection.execute(stm)
627
+ row = connection.execute(sa.select(tb)).fetchall()
628
+ assert row == [(0, 1), (1, 2)]
629
+
630
+ def test_upsert_existing_id(self, connection):
631
+ tb = self.tables.test_upsert
632
+ stm = ydb_sa.upsert(tb).values(id=0, val=5)
633
+ connection.execute(stm)
634
+ row = connection.execute(sa.select(tb)).fetchall()
635
+
636
+ assert row == [(0, 5)]
637
+
638
+ stm = ydb_sa.upsert(tb).values(id=0, val=6)
639
+ connection.execute(stm)
640
+ row = connection.execute(sa.select(tb)).fetchall()
641
+
642
+ assert row == [(0, 6)]
643
+
644
+ def test_upsert_several_diff_id(self, connection):
645
+ tb = self.tables.test_upsert
646
+ stm = ydb_sa.upsert(tb).values(
647
+ [
648
+ {"id": 0, "val": 4},
649
+ {"id": 1, "val": 5},
650
+ {"id": 2, "val": 6},
651
+ ]
652
+ )
653
+ connection.execute(stm)
654
+ row = connection.execute(sa.select(tb)).fetchall()
655
+
656
+ assert row == [(0, 4), (1, 5), (2, 6)]
657
+
658
+ def test_upsert_several_same_id(self, connection):
659
+ tb = self.tables.test_upsert
660
+ stm = ydb_sa.upsert(tb).values(
661
+ [
662
+ {"id": 0, "val": 4},
663
+ {"id": 0, "val": 5},
664
+ {"id": 0, "val": 6},
665
+ ]
666
+ )
667
+ connection.execute(stm)
668
+ row = connection.execute(sa.select(tb)).fetchall()
669
+
670
+ assert row == [(0, 6)]
671
+
672
+ def test_upsert_from_select(self, connection, metadata):
673
+ table_to_select_from = Table(
674
+ "table_to_select_from",
675
+ metadata,
676
+ Column("id", Integer, primary_key=True),
677
+ Column("val", Integer),
678
+ )
679
+ table_to_select_from.create(connection)
680
+ stm = sa.insert(table_to_select_from).values(
681
+ [
682
+ {"id": 100, "val": 0},
683
+ {"id": 110, "val": 1},
684
+ {"id": 120, "val": 2},
685
+ {"id": 130, "val": 3},
686
+ ]
687
+ )
688
+ connection.execute(stm)
689
+
690
+ tb = self.tables.test_upsert
691
+ select_stm = sa.select(table_to_select_from.c.id, table_to_select_from.c.val).where(
692
+ table_to_select_from.c.id > 115,
693
+ )
694
+ upsert_stm = ydb_sa.upsert(tb).from_select(["id", "val"], select_stm)
695
+ connection.execute(upsert_stm)
696
+ row = connection.execute(sa.select(tb)).fetchall()
697
+
698
+ assert row == [(120, 2), (130, 3)]
699
+
700
+
701
+ class TestUpsertDoesNotReplaceInsert(TablesTest):
702
+ __backend__ = True
703
+
704
+ @classmethod
705
+ def define_tables(cls, metadata):
706
+ Table(
707
+ "test_upsert_does_not_replace_insert",
708
+ metadata,
709
+ Column("id", Integer, primary_key=True),
710
+ Column("VALUE_TO_INSERT", String),
711
+ )
712
+
713
+ def test_string(self, connection):
714
+ tb = self.tables.test_upsert_does_not_replace_insert
715
+
716
+ stm = ydb_sa.upsert(tb).values(id=0, VALUE_TO_INSERT="5")
717
+
718
+ assert str(stm) == "UPSERT INTO test_upsert_does_not_replace_insert (id, `VALUE_TO_INSERT`) VALUES (?, ?)"
719
+
720
+ def test_insert_in_name(self, connection):
721
+ tb = self.tables.test_upsert_does_not_replace_insert
722
+ stm = ydb_sa.upsert(tb).values(id=1, VALUE_TO_INSERT="5")
723
+ connection.execute(stm)
724
+ row = connection.execute(sa.select(tb).where(tb.c.id == 1)).fetchone()
725
+
726
+ assert row == (1, "5")
727
+
728
+ def test_insert_in_name_and_field(self, connection):
729
+ tb = self.tables.test_upsert_does_not_replace_insert
730
+ stm = ydb_sa.upsert(tb).values(id=2, VALUE_TO_INSERT="INSERT is my favourite operation")
731
+ connection.execute(stm)
732
+ row = connection.execute(sa.select(tb).where(tb.c.id == 2)).fetchone()
733
+
734
+ assert row == (2, "INSERT is my favourite operation")