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/__init__.py +0 -0
- test/conftest.py +11 -0
- test/test_core.py +734 -0
- test/test_inspect.py +34 -0
- test/test_suite.py +553 -0
- test_dbapi/__init__.py +0 -0
- test_dbapi/test_dbapi.py +196 -0
- ydb_sqlalchemy/__init__.py +3 -0
- ydb_sqlalchemy/_version.py +1 -0
- ydb_sqlalchemy/dbapi/__init__.py +43 -0
- ydb_sqlalchemy/dbapi/connection.py +180 -0
- ydb_sqlalchemy/dbapi/constants.py +218 -0
- ydb_sqlalchemy/dbapi/cursor.py +304 -0
- ydb_sqlalchemy/dbapi/errors.py +103 -0
- ydb_sqlalchemy/sqlalchemy/__init__.py +754 -0
- ydb_sqlalchemy/sqlalchemy/dml.py +12 -0
- ydb_sqlalchemy/sqlalchemy/json.py +34 -0
- ydb_sqlalchemy/sqlalchemy/requirements.py +105 -0
- ydb_sqlalchemy/sqlalchemy/test_sqlalchemy.py +27 -0
- ydb_sqlalchemy/sqlalchemy/types.py +67 -0
- ydb_sqlalchemy-0.0.1b4.dist-info/LICENSE +201 -0
- ydb_sqlalchemy-0.0.1b4.dist-info/METADATA +89 -0
- ydb_sqlalchemy-0.0.1b4.dist-info/RECORD +26 -0
- ydb_sqlalchemy-0.0.1b4.dist-info/WHEEL +6 -0
- ydb_sqlalchemy-0.0.1b4.dist-info/entry_points.txt +6 -0
- ydb_sqlalchemy-0.0.1b4.dist-info/top_level.txt +3 -0
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")
|