ydb-sqlalchemy 0.1.13__py2.py3-none-any.whl → 0.1.15__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 +135 -0
- test/test_suite.py +0 -1
- ydb_sqlalchemy/_version.py +1 -1
- ydb_sqlalchemy/sqlalchemy/__init__.py +30 -0
- ydb_sqlalchemy/sqlalchemy/compiler/base.py +24 -6
- ydb_sqlalchemy/sqlalchemy/dbapi_adapter.py +6 -0
- ydb_sqlalchemy/sqlalchemy/test_sqlalchemy.py +108 -0
- ydb_sqlalchemy/sqlalchemy/types.py +53 -2
- {ydb_sqlalchemy-0.1.13.dist-info → ydb_sqlalchemy-0.1.15.dist-info}/METADATA +2 -2
- {ydb_sqlalchemy-0.1.13.dist-info → ydb_sqlalchemy-0.1.15.dist-info}/RECORD +14 -14
- {ydb_sqlalchemy-0.1.13.dist-info → ydb_sqlalchemy-0.1.15.dist-info}/LICENSE +0 -0
- {ydb_sqlalchemy-0.1.13.dist-info → ydb_sqlalchemy-0.1.15.dist-info}/WHEEL +0 -0
- {ydb_sqlalchemy-0.1.13.dist-info → ydb_sqlalchemy-0.1.15.dist-info}/entry_points.txt +0 -0
- {ydb_sqlalchemy-0.1.13.dist-info → ydb_sqlalchemy-0.1.15.dist-info}/top_level.txt +0 -0
test/test_core.py
CHANGED
|
@@ -228,6 +228,15 @@ class TestTypes(TablesTest):
|
|
|
228
228
|
Column("float", sa.Float),
|
|
229
229
|
Column("bool", sa.Boolean),
|
|
230
230
|
)
|
|
231
|
+
Table(
|
|
232
|
+
"test_all_binary_types",
|
|
233
|
+
metadata,
|
|
234
|
+
Column("id", sa.Integer, primary_key=True),
|
|
235
|
+
Column("bin", sa.BINARY),
|
|
236
|
+
Column("large_bin", sa.LargeBinary),
|
|
237
|
+
Column("blob", sa.BLOB),
|
|
238
|
+
Column("custom_bin", types.Binary),
|
|
239
|
+
)
|
|
231
240
|
Table(
|
|
232
241
|
"test_datetime_types",
|
|
233
242
|
metadata,
|
|
@@ -255,6 +264,21 @@ class TestTypes(TablesTest):
|
|
|
255
264
|
row = connection.execute(sa.select(table)).fetchone()
|
|
256
265
|
assert row == (42, "Hello World!", 3.5, True)
|
|
257
266
|
|
|
267
|
+
def test_all_binary_types(self, connection):
|
|
268
|
+
table = self.tables.test_all_binary_types
|
|
269
|
+
data = {
|
|
270
|
+
"id": 1,
|
|
271
|
+
"bin": b"binary",
|
|
272
|
+
"large_bin": b"large_binary",
|
|
273
|
+
"blob": b"blob",
|
|
274
|
+
"custom_bin": b"custom_binary",
|
|
275
|
+
}
|
|
276
|
+
statement = sa.insert(table).values(**data)
|
|
277
|
+
connection.execute(statement)
|
|
278
|
+
|
|
279
|
+
row = connection.execute(sa.select(table)).fetchone()
|
|
280
|
+
assert row == (1, b"binary", b"large_binary", b"blob", b"custom_binary")
|
|
281
|
+
|
|
258
282
|
def test_integer_types(self, connection):
|
|
259
283
|
stmt = sa.select(
|
|
260
284
|
sa.func.FormatType(sa.func.TypeOf(sa.bindparam("p_uint8", 8, types.UInt8))),
|
|
@@ -1112,3 +1136,114 @@ class TestTablePathPrefix(TablesTest):
|
|
|
1112
1136
|
metadata.reflect(reflection_engine)
|
|
1113
1137
|
|
|
1114
1138
|
assert "nested_dir/table" in metadata.tables
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
class TestAsTable(TablesTest):
|
|
1142
|
+
__backend__ = True
|
|
1143
|
+
|
|
1144
|
+
@classmethod
|
|
1145
|
+
def define_tables(cls, metadata):
|
|
1146
|
+
Table(
|
|
1147
|
+
"test_as_table",
|
|
1148
|
+
metadata,
|
|
1149
|
+
Column("id", Integer, primary_key=True),
|
|
1150
|
+
Column("val_int", Integer, nullable=True),
|
|
1151
|
+
Column("val_str", String, nullable=True),
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
def test_upsert_as_table(self, connection):
|
|
1155
|
+
table = self.tables.test_as_table
|
|
1156
|
+
|
|
1157
|
+
input_data = [
|
|
1158
|
+
{"id": 1, "val_int": 10, "val_str": "a"},
|
|
1159
|
+
{"id": 2, "val_int": None, "val_str": "b"},
|
|
1160
|
+
{"id": 3, "val_int": 30, "val_str": None},
|
|
1161
|
+
]
|
|
1162
|
+
|
|
1163
|
+
struct_type = types.StructType(
|
|
1164
|
+
{
|
|
1165
|
+
"id": Integer,
|
|
1166
|
+
"val_int": types.Optional(Integer),
|
|
1167
|
+
"val_str": types.Optional(String),
|
|
1168
|
+
}
|
|
1169
|
+
)
|
|
1170
|
+
list_type = types.ListType(struct_type)
|
|
1171
|
+
|
|
1172
|
+
bind_param = sa.bindparam("data", type_=list_type)
|
|
1173
|
+
|
|
1174
|
+
upsert_stm = ydb_sa.upsert(table).from_select(
|
|
1175
|
+
["id", "val_int", "val_str"],
|
|
1176
|
+
sa.select(
|
|
1177
|
+
sa.column("id", type_=Integer), sa.column("val_int", type_=Integer), sa.column("val_str", type_=String)
|
|
1178
|
+
).select_from(sa.func.AS_TABLE(bind_param)),
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
connection.execute(upsert_stm, {"data": input_data})
|
|
1182
|
+
|
|
1183
|
+
rows = connection.execute(sa.select(table).order_by(table.c.id)).fetchall()
|
|
1184
|
+
assert rows == [
|
|
1185
|
+
(1, 10, "a"),
|
|
1186
|
+
(2, None, "b"),
|
|
1187
|
+
(3, 30, None),
|
|
1188
|
+
]
|
|
1189
|
+
|
|
1190
|
+
def test_insert_as_table(self, connection):
|
|
1191
|
+
table = self.tables.test_as_table
|
|
1192
|
+
|
|
1193
|
+
input_data = [
|
|
1194
|
+
{"id": 4, "val_int": 40, "val_str": "d"},
|
|
1195
|
+
{"id": 5, "val_int": None, "val_str": "e"},
|
|
1196
|
+
]
|
|
1197
|
+
|
|
1198
|
+
struct_type = types.StructType(
|
|
1199
|
+
{
|
|
1200
|
+
"id": Integer,
|
|
1201
|
+
"val_int": types.Optional(Integer),
|
|
1202
|
+
"val_str": types.Optional(String),
|
|
1203
|
+
}
|
|
1204
|
+
)
|
|
1205
|
+
list_type = types.ListType(struct_type)
|
|
1206
|
+
|
|
1207
|
+
bind_param = sa.bindparam("data", type_=list_type)
|
|
1208
|
+
|
|
1209
|
+
insert_stm = sa.insert(table).from_select(
|
|
1210
|
+
["id", "val_int", "val_str"],
|
|
1211
|
+
sa.select(
|
|
1212
|
+
sa.column("id", type_=Integer), sa.column("val_int", type_=Integer), sa.column("val_str", type_=String)
|
|
1213
|
+
).select_from(sa.func.AS_TABLE(bind_param)),
|
|
1214
|
+
)
|
|
1215
|
+
|
|
1216
|
+
connection.execute(insert_stm, {"data": input_data})
|
|
1217
|
+
|
|
1218
|
+
rows = connection.execute(sa.select(table).where(table.c.id >= 4).order_by(table.c.id)).fetchall()
|
|
1219
|
+
assert rows == [
|
|
1220
|
+
(4, 40, "d"),
|
|
1221
|
+
(5, None, "e"),
|
|
1222
|
+
]
|
|
1223
|
+
|
|
1224
|
+
def test_upsert_from_table_reflection(self, connection):
|
|
1225
|
+
table = self.tables.test_as_table
|
|
1226
|
+
|
|
1227
|
+
input_data = [
|
|
1228
|
+
{"id": 1, "val_int": 10, "val_str": "a"},
|
|
1229
|
+
{"id": 2, "val_int": None, "val_str": "b"},
|
|
1230
|
+
]
|
|
1231
|
+
|
|
1232
|
+
struct_type = types.StructType.from_table(table)
|
|
1233
|
+
list_type = types.ListType(struct_type)
|
|
1234
|
+
|
|
1235
|
+
bind_param = sa.bindparam("data", type_=list_type)
|
|
1236
|
+
|
|
1237
|
+
cols = [sa.column(c.name, type_=c.type) for c in table.columns]
|
|
1238
|
+
upsert_stm = ydb_sa.upsert(table).from_select(
|
|
1239
|
+
[c.name for c in table.columns],
|
|
1240
|
+
sa.select(*cols).select_from(sa.func.AS_TABLE(bind_param)),
|
|
1241
|
+
)
|
|
1242
|
+
|
|
1243
|
+
connection.execute(upsert_stm, {"data": input_data})
|
|
1244
|
+
|
|
1245
|
+
rows = connection.execute(sa.select(table).order_by(table.c.id)).fetchall()
|
|
1246
|
+
assert rows == [
|
|
1247
|
+
(1, 10, "a"),
|
|
1248
|
+
(2, None, "b"),
|
|
1249
|
+
]
|
test/test_suite.py
CHANGED
ydb_sqlalchemy/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
VERSION = "0.1.
|
|
1
|
+
VERSION = "0.1.15"
|
|
@@ -99,6 +99,19 @@ class YdbRequestSettingsCharacteristic(characteristics.ConnectionCharacteristic)
|
|
|
99
99
|
return dialect.get_ydb_request_settings(dbapi_connection)
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
class YdbRetrySettingsCharacteristic(characteristics.ConnectionCharacteristic):
|
|
103
|
+
def reset_characteristic(self, dialect: "YqlDialect", dbapi_connection: ydb_dbapi.Connection) -> None:
|
|
104
|
+
dialect.reset_ydb_retry_settings(dbapi_connection)
|
|
105
|
+
|
|
106
|
+
def set_characteristic(
|
|
107
|
+
self, dialect: "YqlDialect", dbapi_connection: ydb_dbapi.Connection, value: ydb.RetrySettings
|
|
108
|
+
) -> None:
|
|
109
|
+
dialect.set_ydb_retry_settings(dbapi_connection, value)
|
|
110
|
+
|
|
111
|
+
def get_characteristic(self, dialect: "YqlDialect", dbapi_connection: ydb_dbapi.Connection) -> ydb.RetrySettings:
|
|
112
|
+
return dialect.get_ydb_retry_settings(dbapi_connection)
|
|
113
|
+
|
|
114
|
+
|
|
102
115
|
class YqlDialect(StrCompileDialect):
|
|
103
116
|
name = "yql"
|
|
104
117
|
driver = "ydb"
|
|
@@ -144,12 +157,16 @@ class YqlDialect(StrCompileDialect):
|
|
|
144
157
|
sa.types.DATETIME: types.YqlDateTime,
|
|
145
158
|
sa.types.TIMESTAMP: types.YqlTimestamp,
|
|
146
159
|
sa.types.DECIMAL: types.Decimal,
|
|
160
|
+
sa.types.BINARY: types.Binary,
|
|
161
|
+
sa.types.LargeBinary: types.Binary,
|
|
162
|
+
sa.types.BLOB: types.Binary,
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
connection_characteristics = util.immutabledict(
|
|
150
166
|
{
|
|
151
167
|
"isolation_level": characteristics.IsolationLevelCharacteristic(),
|
|
152
168
|
"ydb_request_settings": YdbRequestSettingsCharacteristic(),
|
|
169
|
+
"ydb_retry_settings": YdbRetrySettingsCharacteristic(),
|
|
153
170
|
}
|
|
154
171
|
)
|
|
155
172
|
|
|
@@ -312,6 +329,19 @@ class YqlDialect(StrCompileDialect):
|
|
|
312
329
|
def get_ydb_request_settings(self, dbapi_connection: ydb_dbapi.Connection) -> ydb.BaseRequestSettings:
|
|
313
330
|
return dbapi_connection.get_ydb_request_settings()
|
|
314
331
|
|
|
332
|
+
def set_ydb_retry_settings(
|
|
333
|
+
self,
|
|
334
|
+
dbapi_connection: ydb_dbapi.Connection,
|
|
335
|
+
value: ydb.RetrySettings,
|
|
336
|
+
) -> None:
|
|
337
|
+
dbapi_connection.set_ydb_retry_settings(value)
|
|
338
|
+
|
|
339
|
+
def reset_ydb_retry_settings(self, dbapi_connection: ydb_dbapi.Connection):
|
|
340
|
+
self.set_ydb_retry_settings(dbapi_connection, ydb.RetrySettings())
|
|
341
|
+
|
|
342
|
+
def get_ydb_retry_settings(self, dbapi_connection: ydb_dbapi.Connection) -> ydb.RetrySettings:
|
|
343
|
+
return dbapi_connection.get_ydb_retry_settings()
|
|
344
|
+
|
|
315
345
|
def create_connect_args(self, url):
|
|
316
346
|
args, kwargs = super().create_connect_args(url)
|
|
317
347
|
# YDB database name should start with '/'
|
|
@@ -12,6 +12,7 @@ from sqlalchemy.sql.compiler import (
|
|
|
12
12
|
StrSQLTypeCompiler,
|
|
13
13
|
selectable,
|
|
14
14
|
)
|
|
15
|
+
from sqlalchemy.sql.type_api import to_instance
|
|
15
16
|
from typing import (
|
|
16
17
|
Any,
|
|
17
18
|
Dict,
|
|
@@ -24,6 +25,12 @@ from typing import (
|
|
|
24
25
|
Union,
|
|
25
26
|
)
|
|
26
27
|
|
|
28
|
+
try:
|
|
29
|
+
from sqlalchemy.types import _Binary as _BinaryType
|
|
30
|
+
except ImportError:
|
|
31
|
+
# For older sqlalchemy versions
|
|
32
|
+
from sqlalchemy.sql.sqltypes import _Binary as _BinaryType
|
|
33
|
+
|
|
27
34
|
|
|
28
35
|
from .. import types
|
|
29
36
|
|
|
@@ -152,11 +159,17 @@ class BaseYqlTypeCompiler(StrSQLTypeCompiler):
|
|
|
152
159
|
inner = self.process(type_.item_type, **kw)
|
|
153
160
|
return f"List<{inner}>"
|
|
154
161
|
|
|
162
|
+
def visit_optional(self, type_: types.Optional, **kw):
|
|
163
|
+
el = to_instance(type_.element_type)
|
|
164
|
+
inner = self.process(el, **kw)
|
|
165
|
+
return f"Optional<{inner}>"
|
|
166
|
+
|
|
155
167
|
def visit_struct_type(self, type_: types.StructType, **kw):
|
|
156
|
-
|
|
157
|
-
for field, field_type in type_.fields_types:
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
rendered_types = []
|
|
169
|
+
for field, field_type in type_.fields_types.items():
|
|
170
|
+
type_str = self.process(field_type, **kw)
|
|
171
|
+
rendered_types.append(f"{field}:{type_str}")
|
|
172
|
+
return f"Struct<{','.join(rendered_types)}>"
|
|
160
173
|
|
|
161
174
|
def get_ydb_type(
|
|
162
175
|
self, type_: sa.types.TypeEngine, is_optional: bool
|
|
@@ -167,6 +180,10 @@ class BaseYqlTypeCompiler(StrSQLTypeCompiler):
|
|
|
167
180
|
if isinstance(type_, (sa.Text, sa.String)):
|
|
168
181
|
ydb_type = ydb.PrimitiveType.Utf8
|
|
169
182
|
|
|
183
|
+
elif isinstance(type_, types.Optional):
|
|
184
|
+
inner = to_instance(type_.element_type)
|
|
185
|
+
return self.get_ydb_type(inner, is_optional=True)
|
|
186
|
+
|
|
170
187
|
# Integers
|
|
171
188
|
elif isinstance(type_, types.UInt64):
|
|
172
189
|
ydb_type = ydb.PrimitiveType.Uint64
|
|
@@ -216,7 +233,7 @@ class BaseYqlTypeCompiler(StrSQLTypeCompiler):
|
|
|
216
233
|
ydb_type = ydb.PrimitiveType.Timestamp
|
|
217
234
|
elif isinstance(type_, sa.Date):
|
|
218
235
|
ydb_type = ydb.PrimitiveType.Date
|
|
219
|
-
elif isinstance(type_,
|
|
236
|
+
elif isinstance(type_, _BinaryType):
|
|
220
237
|
ydb_type = ydb.PrimitiveType.String
|
|
221
238
|
elif isinstance(type_, sa.Float):
|
|
222
239
|
ydb_type = ydb.PrimitiveType.Float
|
|
@@ -235,7 +252,8 @@ class BaseYqlTypeCompiler(StrSQLTypeCompiler):
|
|
|
235
252
|
elif isinstance(type_, types.StructType):
|
|
236
253
|
ydb_type = ydb.StructType()
|
|
237
254
|
for field, field_type in type_.fields_types.items():
|
|
238
|
-
|
|
255
|
+
inner_type = to_instance(field_type)
|
|
256
|
+
ydb_type.add_member(field, self.get_ydb_type(inner_type, is_optional=False))
|
|
239
257
|
else:
|
|
240
258
|
raise NotSupportedError(f"{type_} bind variables not supported")
|
|
241
259
|
|
|
@@ -56,6 +56,12 @@ class AdaptedAsyncConnection(AdaptedConnection):
|
|
|
56
56
|
def get_ydb_request_settings(self) -> ydb.BaseRequestSettings:
|
|
57
57
|
return self._connection.get_ydb_request_settings()
|
|
58
58
|
|
|
59
|
+
def set_ydb_retry_settings(self, value: ydb.RetrySettings) -> None:
|
|
60
|
+
self._connection.set_ydb_retry_settings(value)
|
|
61
|
+
|
|
62
|
+
def get_ydb_retry_settings(self) -> ydb.RetrySettings:
|
|
63
|
+
return self._connection.get_ydb_retry_settings()
|
|
64
|
+
|
|
59
65
|
def describe(self, table_path: str):
|
|
60
66
|
return await_only(self._connection.describe(table_path))
|
|
61
67
|
|
|
@@ -35,3 +35,111 @@ def test_ydb_types():
|
|
|
35
35
|
compiled = query.compile(dialect=dialect, compile_kwargs={"literal_binds": True})
|
|
36
36
|
|
|
37
37
|
assert str(compiled) == "Date('1996-11-19')"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_binary_type():
|
|
41
|
+
dialect = YqlDialect()
|
|
42
|
+
expr = sa.literal(b"some bytes")
|
|
43
|
+
compiled = expr.compile(dialect=dialect, compile_kwargs={"literal_binds": True})
|
|
44
|
+
assert str(compiled) == "'some bytes'"
|
|
45
|
+
|
|
46
|
+
expr_binary = sa.cast(expr, sa.BINARY)
|
|
47
|
+
compiled_binary = expr_binary.compile(dialect=dialect, compile_kwargs={"literal_binds": True})
|
|
48
|
+
assert str(compiled_binary) == "CAST('some bytes' AS String)"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_all_binary_types():
|
|
52
|
+
dialect = YqlDialect()
|
|
53
|
+
expr = sa.literal(b"some bytes")
|
|
54
|
+
|
|
55
|
+
binary_types = [
|
|
56
|
+
sa.BINARY,
|
|
57
|
+
sa.LargeBinary,
|
|
58
|
+
sa.BLOB,
|
|
59
|
+
types.Binary,
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
for type_ in binary_types:
|
|
63
|
+
expr_binary = sa.cast(expr, type_)
|
|
64
|
+
compiled_binary = expr_binary.compile(dialect=dialect, compile_kwargs={"literal_binds": True})
|
|
65
|
+
assert str(compiled_binary) == "CAST('some bytes' AS String)"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_struct_type_generation():
|
|
69
|
+
dialect = YqlDialect()
|
|
70
|
+
type_compiler = dialect.type_compiler
|
|
71
|
+
|
|
72
|
+
# Test default (non-optional)
|
|
73
|
+
struct_type = types.StructType(
|
|
74
|
+
{
|
|
75
|
+
"id": sa.Integer,
|
|
76
|
+
"val_int": sa.Integer,
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
ydb_type = type_compiler.get_ydb_type(struct_type, is_optional=False)
|
|
80
|
+
# Keys are sorted
|
|
81
|
+
assert str(ydb_type) == "Struct<id:Int64,val_int:Int64>"
|
|
82
|
+
|
|
83
|
+
# Test optional
|
|
84
|
+
struct_type_opt = types.StructType(
|
|
85
|
+
{
|
|
86
|
+
"id": sa.Integer,
|
|
87
|
+
"val_int": types.Optional(sa.Integer),
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
ydb_type_opt = type_compiler.get_ydb_type(struct_type_opt, is_optional=False)
|
|
91
|
+
assert str(ydb_type_opt) == "Struct<id:Int64,val_int:Int64?>"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_types_compilation():
|
|
95
|
+
dialect = YqlDialect()
|
|
96
|
+
|
|
97
|
+
def compile_type(type_):
|
|
98
|
+
return dialect.type_compiler.process(type_)
|
|
99
|
+
|
|
100
|
+
assert compile_type(types.UInt64()) == "UInt64"
|
|
101
|
+
assert compile_type(types.UInt32()) == "UInt32"
|
|
102
|
+
assert compile_type(types.UInt16()) == "UInt16"
|
|
103
|
+
assert compile_type(types.UInt8()) == "UInt8"
|
|
104
|
+
|
|
105
|
+
assert compile_type(types.Int64()) == "Int64"
|
|
106
|
+
assert compile_type(types.Int32()) == "Int32"
|
|
107
|
+
assert compile_type(types.Int16()) == "Int32"
|
|
108
|
+
assert compile_type(types.Int8()) == "Int8"
|
|
109
|
+
|
|
110
|
+
assert compile_type(types.ListType(types.Int64())) == "List<Int64>"
|
|
111
|
+
|
|
112
|
+
struct = types.StructType({"a": types.Int32(), "b": types.ListType(types.Int32())})
|
|
113
|
+
# Ordered by key: a, b
|
|
114
|
+
assert compile_type(struct) == "Struct<a:Int32,b:List<Int32>>"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_optional_type_compilation():
|
|
118
|
+
dialect = YqlDialect()
|
|
119
|
+
type_compiler = dialect.type_compiler
|
|
120
|
+
|
|
121
|
+
def compile_type(type_):
|
|
122
|
+
return type_compiler.process(type_)
|
|
123
|
+
|
|
124
|
+
# Test Optional(Integer)
|
|
125
|
+
opt_int = types.Optional(sa.Integer)
|
|
126
|
+
assert compile_type(opt_int) == "Optional<Int64>"
|
|
127
|
+
|
|
128
|
+
# Test Optional(String)
|
|
129
|
+
opt_str = types.Optional(sa.String)
|
|
130
|
+
assert compile_type(opt_str) == "Optional<UTF8>"
|
|
131
|
+
|
|
132
|
+
# Test Nested Optional
|
|
133
|
+
opt_opt_int = types.Optional(types.Optional(sa.Integer))
|
|
134
|
+
assert compile_type(opt_opt_int) == "Optional<Optional<Int64>>"
|
|
135
|
+
|
|
136
|
+
# Test get_ydb_type
|
|
137
|
+
ydb_type = type_compiler.get_ydb_type(opt_int, is_optional=False)
|
|
138
|
+
import ydb
|
|
139
|
+
|
|
140
|
+
assert isinstance(ydb_type, ydb.OptionalType)
|
|
141
|
+
# Int64 corresponds to PrimitiveType.Int64
|
|
142
|
+
# Note: ydb.PrimitiveType.Int64 is an enum member, but ydb_type.item is also an instance/enum?
|
|
143
|
+
# get_ydb_type returns ydb.PrimitiveType.Int64 (enum) wrapped in OptionalType.
|
|
144
|
+
# OptionalType.item is the inner type.
|
|
145
|
+
assert ydb_type.item == ydb.PrimitiveType.Int64
|
|
@@ -8,7 +8,7 @@ if sa_version.startswith("2."):
|
|
|
8
8
|
else:
|
|
9
9
|
from sqlalchemy.sql.expression import ColumnElement
|
|
10
10
|
|
|
11
|
-
from sqlalchemy import ARRAY, exc, types
|
|
11
|
+
from sqlalchemy import ARRAY, exc, Table, types
|
|
12
12
|
from sqlalchemy.sql import type_api
|
|
13
13
|
|
|
14
14
|
from .datetime_types import YqlDate, YqlDateTime, YqlTimestamp, YqlDate32, YqlTimestamp64, YqlDateTime64 # noqa: F401
|
|
@@ -116,12 +116,56 @@ class HashableDict(dict):
|
|
|
116
116
|
return hash(tuple(self.items()))
|
|
117
117
|
|
|
118
118
|
|
|
119
|
+
class Optional(types.TypeEngine):
|
|
120
|
+
"""
|
|
121
|
+
Wrapper for YDB Optional type.
|
|
122
|
+
|
|
123
|
+
Used primarily within StructType to denote nullable fields.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
__visit_name__ = "optional"
|
|
127
|
+
|
|
128
|
+
def __init__(self, element_type: Union[Type[types.TypeEngine], types.TypeEngine]):
|
|
129
|
+
self.element_type = element_type
|
|
130
|
+
|
|
131
|
+
|
|
119
132
|
class StructType(types.TypeEngine[Mapping[str, Any]]):
|
|
133
|
+
"""
|
|
134
|
+
YDB Struct type.
|
|
135
|
+
|
|
136
|
+
Represents a structured data type with named fields, mapped to a Python dictionary.
|
|
137
|
+
"""
|
|
138
|
+
|
|
120
139
|
__visit_name__ = "struct_type"
|
|
121
140
|
|
|
122
|
-
def __init__(
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
fields_types: Mapping[
|
|
144
|
+
str,
|
|
145
|
+
Union[Type[types.TypeEngine], types.TypeEngine, Optional],
|
|
146
|
+
],
|
|
147
|
+
):
|
|
123
148
|
self.fields_types = HashableDict(dict(sorted(fields_types.items())))
|
|
124
149
|
|
|
150
|
+
@classmethod
|
|
151
|
+
def from_table(cls, table: Table) -> "StructType":
|
|
152
|
+
"""
|
|
153
|
+
Create a StructType definition from a SQLAlchemy Table.
|
|
154
|
+
|
|
155
|
+
Automatically wraps nullable columns in Optional.
|
|
156
|
+
|
|
157
|
+
:param table: SQLAlchemy Table object
|
|
158
|
+
:return: StructType instance
|
|
159
|
+
"""
|
|
160
|
+
fields = {}
|
|
161
|
+
for col in table.columns:
|
|
162
|
+
t = col.type
|
|
163
|
+
if col.nullable:
|
|
164
|
+
fields[col.name] = Optional(t)
|
|
165
|
+
else:
|
|
166
|
+
fields[col.name] = t
|
|
167
|
+
return cls(fields)
|
|
168
|
+
|
|
125
169
|
@property
|
|
126
170
|
def python_type(self):
|
|
127
171
|
return dict
|
|
@@ -139,3 +183,10 @@ class Lambda(ColumnElement):
|
|
|
139
183
|
|
|
140
184
|
self.type = type_api.NULLTYPE
|
|
141
185
|
self.func = func
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class Binary(types.LargeBinary):
|
|
189
|
+
__visit_name__ = "BINARY"
|
|
190
|
+
|
|
191
|
+
def bind_processor(self, dialect):
|
|
192
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ydb-sqlalchemy
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.15
|
|
4
4
|
Summary: YDB Dialect for SQLAlchemy
|
|
5
5
|
Home-page: http://github.com/ydb-platform/ydb-sqlalchemy
|
|
6
6
|
Author: Yandex LLC
|
|
@@ -14,7 +14,7 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: sqlalchemy <3.0.0,>=1.4.0
|
|
16
16
|
Requires-Dist: ydb >=3.21.6
|
|
17
|
-
Requires-Dist: ydb-dbapi >=0.1.
|
|
17
|
+
Requires-Dist: ydb-dbapi >=0.1.16
|
|
18
18
|
Provides-Extra: yc
|
|
19
19
|
Requires-Dist: yandexcloud ; extra == 'yc'
|
|
20
20
|
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
test/conftest.py,sha256=rhWa0EQB9EwO_wAwxPdK17Qi582DdbBE8p5Gv4180Ds,570
|
|
3
|
-
test/test_core.py,sha256=
|
|
3
|
+
test/test_core.py,sha256=5ijngbM6b_8avees4CqCv0FgzAza2RRPDwgT2saU99k,47254
|
|
4
4
|
test/test_inspect.py,sha256=c4kc3jc48MCOfllO-ciiYf1vO-HOfuv0xVoXYT1Jxro,1106
|
|
5
5
|
test/test_orm.py,sha256=jQVVld50zbUwxwgW9ySIWGaNDEOLzHKXjTkdpsG9TpA,1825
|
|
6
|
-
test/test_suite.py,sha256=
|
|
6
|
+
test/test_suite.py,sha256=JYBGZjaRbg_ZiAqTHeCfL7DLnB6N6xkXN82gnooCyd8,31063
|
|
7
7
|
ydb_sqlalchemy/__init__.py,sha256=hX7Gy-KOiHk7B5-0wj3ZmLjk4YDJnSMHIAqxVGn_PJY,181
|
|
8
|
-
ydb_sqlalchemy/_version.py,sha256=
|
|
9
|
-
ydb_sqlalchemy/sqlalchemy/__init__.py,sha256=
|
|
8
|
+
ydb_sqlalchemy/_version.py,sha256=f1tdrTNNxSCrK-kaNyZL5_MXChoOPzxvT7JLn3_KW6k,19
|
|
9
|
+
ydb_sqlalchemy/sqlalchemy/__init__.py,sha256=Z5aprCPByfUmH2DvybA0o7W0lQ8YDCR1XSyyfGmBdoE,17911
|
|
10
10
|
ydb_sqlalchemy/sqlalchemy/datetime_types.py,sha256=wrI9kpsI_f7Jhbm7Fu0o_S1QoGCLIe6A9jfUwb41aMM,1929
|
|
11
|
-
ydb_sqlalchemy/sqlalchemy/dbapi_adapter.py,sha256=
|
|
11
|
+
ydb_sqlalchemy/sqlalchemy/dbapi_adapter.py,sha256=7FDjganh9QStIkoXYPFfcRRhd07YCX63_8OmMnge1FI,3542
|
|
12
12
|
ydb_sqlalchemy/sqlalchemy/dml.py,sha256=k_m6PLOAY7dVzG1gsyo2bB3Lp-o3rhzN0oSX_nfkbFU,310
|
|
13
13
|
ydb_sqlalchemy/sqlalchemy/json.py,sha256=b4ydjlQjBhlhqGP_Sy2uZVKmt__D-9M7-YLGQMdYGME,1043
|
|
14
14
|
ydb_sqlalchemy/sqlalchemy/requirements.py,sha256=zm6fcLormtk3KHnbtrBvxfkbG9ZyzNan38HrRB6vC3c,2505
|
|
15
|
-
ydb_sqlalchemy/sqlalchemy/test_sqlalchemy.py,sha256=
|
|
16
|
-
ydb_sqlalchemy/sqlalchemy/types.py,sha256=
|
|
15
|
+
ydb_sqlalchemy/sqlalchemy/test_sqlalchemy.py,sha256=4wyRHmE8YQaMElQPHX6ToEj7A9F8Mvv909aaz_0wRnA,4535
|
|
16
|
+
ydb_sqlalchemy/sqlalchemy/types.py,sha256=_PxK76x6BJZv-7gVdXL-XJtaOFjiYZY4f-_WBRWH8x0,4996
|
|
17
17
|
ydb_sqlalchemy/sqlalchemy/compiler/__init__.py,sha256=QqA6r-_bw1R97nQZy5ZSJN724znXg88l4mi5PpqAOxI,492
|
|
18
|
-
ydb_sqlalchemy/sqlalchemy/compiler/base.py,sha256=
|
|
18
|
+
ydb_sqlalchemy/sqlalchemy/compiler/base.py,sha256=zKOf4SrcAjMKf1LjRvQA7IzMRic0EIznHuZTHySvc_k,20143
|
|
19
19
|
ydb_sqlalchemy/sqlalchemy/compiler/sa14.py,sha256=LanxAnwOiMnsnrY05B0jpmvGn5NXuOKMcxi_6N3obVM,1186
|
|
20
20
|
ydb_sqlalchemy/sqlalchemy/compiler/sa20.py,sha256=rvVhe-pq5bOyuW4KMMMAD7JIWMzy355eijymBvuPwKw,3421
|
|
21
|
-
ydb_sqlalchemy-0.1.
|
|
22
|
-
ydb_sqlalchemy-0.1.
|
|
23
|
-
ydb_sqlalchemy-0.1.
|
|
24
|
-
ydb_sqlalchemy-0.1.
|
|
25
|
-
ydb_sqlalchemy-0.1.
|
|
26
|
-
ydb_sqlalchemy-0.1.
|
|
21
|
+
ydb_sqlalchemy-0.1.15.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
22
|
+
ydb_sqlalchemy-0.1.15.dist-info/METADATA,sha256=iOoVXzaJ3tJ7ct7jssAXLSUwptb9C4e7Yx3dPp8VzdA,5395
|
|
23
|
+
ydb_sqlalchemy-0.1.15.dist-info/WHEEL,sha256=Ll72iyqtt6Rbxp-Q7FSafYA1LeRv98X15xcZWRsFEmY,109
|
|
24
|
+
ydb_sqlalchemy-0.1.15.dist-info/entry_points.txt,sha256=iJxbKYuliWNBmL0iIiw8MxvOXrSEz5xe5fuEBqMRwCE,267
|
|
25
|
+
ydb_sqlalchemy-0.1.15.dist-info/top_level.txt,sha256=iS69Y1GTAcTok0u0oQdxP-Q5iVgUGI71XBsaEUrWhMg,20
|
|
26
|
+
ydb_sqlalchemy-0.1.15.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|