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
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Experimental
|
|
3
|
+
Work in progress, breaking changes are possible.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import collections
|
|
7
|
+
import collections.abc
|
|
8
|
+
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Type, Union
|
|
9
|
+
|
|
10
|
+
import sqlalchemy as sa
|
|
11
|
+
import ydb
|
|
12
|
+
from sqlalchemy.engine import reflection
|
|
13
|
+
from sqlalchemy.engine.default import DefaultExecutionContext, StrCompileDialect
|
|
14
|
+
from sqlalchemy.exc import CompileError, NoSuchTableError
|
|
15
|
+
from sqlalchemy.sql import functions, literal_column
|
|
16
|
+
from sqlalchemy.sql.compiler import (
|
|
17
|
+
DDLCompiler,
|
|
18
|
+
IdentifierPreparer,
|
|
19
|
+
StrSQLCompiler,
|
|
20
|
+
StrSQLTypeCompiler,
|
|
21
|
+
selectable,
|
|
22
|
+
)
|
|
23
|
+
from sqlalchemy.sql.elements import ClauseList
|
|
24
|
+
from sqlalchemy.util.compat import inspect_getfullargspec
|
|
25
|
+
|
|
26
|
+
import ydb_sqlalchemy.dbapi as dbapi
|
|
27
|
+
from ydb_sqlalchemy.dbapi.constants import YDB_KEYWORDS
|
|
28
|
+
from ydb_sqlalchemy.sqlalchemy.dml import Upsert
|
|
29
|
+
|
|
30
|
+
from . import types
|
|
31
|
+
|
|
32
|
+
STR_QUOTE_MAP = {
|
|
33
|
+
"'": "\\'",
|
|
34
|
+
"\\": "\\\\",
|
|
35
|
+
"\0": "\\0",
|
|
36
|
+
"\b": "\\b",
|
|
37
|
+
"\f": "\\f",
|
|
38
|
+
"\r": "\\r",
|
|
39
|
+
"\n": "\\n",
|
|
40
|
+
"\t": "\\t",
|
|
41
|
+
"%": "%%",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
COMPOUND_KEYWORDS = {
|
|
45
|
+
selectable.CompoundSelect.UNION: "UNION ALL",
|
|
46
|
+
selectable.CompoundSelect.UNION_ALL: "UNION ALL",
|
|
47
|
+
selectable.CompoundSelect.EXCEPT: "EXCEPT",
|
|
48
|
+
selectable.CompoundSelect.EXCEPT_ALL: "EXCEPT ALL",
|
|
49
|
+
selectable.CompoundSelect.INTERSECT: "INTERSECT",
|
|
50
|
+
selectable.CompoundSelect.INTERSECT_ALL: "INTERSECT ALL",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class YqlIdentifierPreparer(IdentifierPreparer):
|
|
55
|
+
reserved_words = IdentifierPreparer.reserved_words
|
|
56
|
+
reserved_words.update(YDB_KEYWORDS)
|
|
57
|
+
|
|
58
|
+
def __init__(self, dialect):
|
|
59
|
+
super(YqlIdentifierPreparer, self).__init__(
|
|
60
|
+
dialect,
|
|
61
|
+
initial_quote="`",
|
|
62
|
+
final_quote="`",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class YqlTypeCompiler(StrSQLTypeCompiler):
|
|
67
|
+
def visit_JSON(self, type_: Union[sa.JSON, types.YqlJSON], **kw):
|
|
68
|
+
return "JSON"
|
|
69
|
+
|
|
70
|
+
def visit_CHAR(self, type_: sa.CHAR, **kw):
|
|
71
|
+
return "UTF8"
|
|
72
|
+
|
|
73
|
+
def visit_VARCHAR(self, type_: sa.VARCHAR, **kw):
|
|
74
|
+
return "UTF8"
|
|
75
|
+
|
|
76
|
+
def visit_unicode(self, type_: sa.Unicode, **kw):
|
|
77
|
+
return "UTF8"
|
|
78
|
+
|
|
79
|
+
def visit_uuid(self, type_: sa.Uuid, **kw):
|
|
80
|
+
return "UTF8"
|
|
81
|
+
|
|
82
|
+
def visit_NVARCHAR(self, type_: sa.NVARCHAR, **kw):
|
|
83
|
+
return "UTF8"
|
|
84
|
+
|
|
85
|
+
def visit_TEXT(self, type_: sa.TEXT, **kw):
|
|
86
|
+
return "UTF8"
|
|
87
|
+
|
|
88
|
+
def visit_FLOAT(self, type_: sa.FLOAT, **kw):
|
|
89
|
+
return "FLOAT"
|
|
90
|
+
|
|
91
|
+
def visit_BOOLEAN(self, type_: sa.BOOLEAN, **kw):
|
|
92
|
+
return "BOOL"
|
|
93
|
+
|
|
94
|
+
def visit_uint64(self, type_: types.UInt64, **kw):
|
|
95
|
+
return "UInt64"
|
|
96
|
+
|
|
97
|
+
def visit_uint32(self, type_: types.UInt32, **kw):
|
|
98
|
+
return "UInt32"
|
|
99
|
+
|
|
100
|
+
def visit_uint16(self, type_: types.UInt16, **kw):
|
|
101
|
+
return "UInt16"
|
|
102
|
+
|
|
103
|
+
def visit_uint8(self, type_: types.UInt8, **kw):
|
|
104
|
+
return "UInt8"
|
|
105
|
+
|
|
106
|
+
def visit_int64(self, type_: types.Int64, **kw):
|
|
107
|
+
return "Int64"
|
|
108
|
+
|
|
109
|
+
def visit_int32(self, type_: types.Int32, **kw):
|
|
110
|
+
return "Int32"
|
|
111
|
+
|
|
112
|
+
def visit_int16(self, type_: types.Int16, **kw):
|
|
113
|
+
return "Int16"
|
|
114
|
+
|
|
115
|
+
def visit_int8(self, type_: types.Int8, **kw):
|
|
116
|
+
return "Int8"
|
|
117
|
+
|
|
118
|
+
def visit_INTEGER(self, type_: sa.INTEGER, **kw):
|
|
119
|
+
return "Int64"
|
|
120
|
+
|
|
121
|
+
def visit_NUMERIC(self, type_: sa.Numeric, **kw):
|
|
122
|
+
"""Only Decimal(22,9) is supported for table columns"""
|
|
123
|
+
return f"Decimal({type_.precision}, {type_.scale})"
|
|
124
|
+
|
|
125
|
+
def visit_BINARY(self, type_: sa.BINARY, **kw):
|
|
126
|
+
return "String"
|
|
127
|
+
|
|
128
|
+
def visit_BLOB(self, type_: sa.BLOB, **kw):
|
|
129
|
+
return "String"
|
|
130
|
+
|
|
131
|
+
def visit_DATETIME(self, type_: sa.TIMESTAMP, **kw):
|
|
132
|
+
return "Timestamp"
|
|
133
|
+
|
|
134
|
+
def visit_list_type(self, type_: types.ListType, **kw):
|
|
135
|
+
inner = self.process(type_.item_type, **kw)
|
|
136
|
+
return f"List<{inner}>"
|
|
137
|
+
|
|
138
|
+
def visit_ARRAY(self, type_: sa.ARRAY, **kw):
|
|
139
|
+
inner = self.process(type_.item_type, **kw)
|
|
140
|
+
return f"List<{inner}>"
|
|
141
|
+
|
|
142
|
+
def visit_struct_type(self, type_: types.StructType, **kw):
|
|
143
|
+
text = "Struct<"
|
|
144
|
+
for field, field_type in type_.fields_types:
|
|
145
|
+
text += f"{field}:{self.process(field_type, **kw)}"
|
|
146
|
+
return text + ">"
|
|
147
|
+
|
|
148
|
+
def get_ydb_type(
|
|
149
|
+
self, type_: sa.types.TypeEngine, is_optional: bool
|
|
150
|
+
) -> Union[ydb.PrimitiveType, ydb.AbstractTypeBuilder]:
|
|
151
|
+
if isinstance(type_, sa.TypeDecorator):
|
|
152
|
+
type_ = type_.impl
|
|
153
|
+
|
|
154
|
+
if isinstance(type_, (sa.Text, sa.String, sa.Uuid)):
|
|
155
|
+
ydb_type = ydb.PrimitiveType.Utf8
|
|
156
|
+
|
|
157
|
+
# Integers
|
|
158
|
+
elif isinstance(type_, types.UInt64):
|
|
159
|
+
ydb_type = ydb.PrimitiveType.Uint64
|
|
160
|
+
elif isinstance(type_, types.UInt32):
|
|
161
|
+
ydb_type = ydb.PrimitiveType.Uint32
|
|
162
|
+
elif isinstance(type_, types.UInt16):
|
|
163
|
+
ydb_type = ydb.PrimitiveType.Uint16
|
|
164
|
+
elif isinstance(type_, types.UInt8):
|
|
165
|
+
ydb_type = ydb.PrimitiveType.Uint8
|
|
166
|
+
elif isinstance(type_, types.Int64):
|
|
167
|
+
ydb_type = ydb.PrimitiveType.Int64
|
|
168
|
+
elif isinstance(type_, types.Int32):
|
|
169
|
+
ydb_type = ydb.PrimitiveType.Int32
|
|
170
|
+
elif isinstance(type_, types.Int16):
|
|
171
|
+
ydb_type = ydb.PrimitiveType.Int16
|
|
172
|
+
elif isinstance(type_, types.Int8):
|
|
173
|
+
ydb_type = ydb.PrimitiveType.Int8
|
|
174
|
+
elif isinstance(type_, sa.Integer):
|
|
175
|
+
ydb_type = ydb.PrimitiveType.Int64
|
|
176
|
+
# Integers
|
|
177
|
+
|
|
178
|
+
# Json
|
|
179
|
+
elif isinstance(type_, sa.JSON):
|
|
180
|
+
ydb_type = ydb.PrimitiveType.Json
|
|
181
|
+
elif isinstance(type_, sa.JSON.JSONStrIndexType):
|
|
182
|
+
ydb_type = ydb.PrimitiveType.Utf8
|
|
183
|
+
elif isinstance(type_, sa.JSON.JSONIntIndexType):
|
|
184
|
+
ydb_type = ydb.PrimitiveType.Int64
|
|
185
|
+
elif isinstance(type_, sa.JSON.JSONPathType):
|
|
186
|
+
ydb_type = ydb.PrimitiveType.Utf8
|
|
187
|
+
elif isinstance(type_, types.YqlJSON):
|
|
188
|
+
ydb_type = ydb.PrimitiveType.Json
|
|
189
|
+
elif isinstance(type_, types.YqlJSON.YqlJSONPathType):
|
|
190
|
+
ydb_type = ydb.PrimitiveType.Utf8
|
|
191
|
+
# Json
|
|
192
|
+
|
|
193
|
+
elif isinstance(type_, sa.DateTime):
|
|
194
|
+
ydb_type = ydb.PrimitiveType.Timestamp
|
|
195
|
+
elif isinstance(type_, sa.Date):
|
|
196
|
+
ydb_type = ydb.PrimitiveType.Date
|
|
197
|
+
elif isinstance(type_, sa.BINARY):
|
|
198
|
+
ydb_type = ydb.PrimitiveType.String
|
|
199
|
+
elif isinstance(type_, sa.Float):
|
|
200
|
+
ydb_type = ydb.PrimitiveType.Float
|
|
201
|
+
elif isinstance(type_, sa.Double):
|
|
202
|
+
ydb_type = ydb.PrimitiveType.Double
|
|
203
|
+
elif isinstance(type_, sa.Boolean):
|
|
204
|
+
ydb_type = ydb.PrimitiveType.Bool
|
|
205
|
+
elif isinstance(type_, sa.Numeric):
|
|
206
|
+
ydb_type = ydb.DecimalType(type_.precision, type_.scale)
|
|
207
|
+
elif isinstance(type_, (types.ListType, sa.ARRAY)):
|
|
208
|
+
ydb_type = ydb.ListType(self.get_ydb_type(type_.item_type, is_optional=False))
|
|
209
|
+
elif isinstance(type_, types.StructType):
|
|
210
|
+
ydb_type = ydb.StructType()
|
|
211
|
+
for field, field_type in type_.fields_types.items():
|
|
212
|
+
ydb_type.add_member(field, self.get_ydb_type(field_type(), is_optional=False))
|
|
213
|
+
else:
|
|
214
|
+
raise dbapi.NotSupportedError(f"{type_} bind variables not supported")
|
|
215
|
+
|
|
216
|
+
if is_optional:
|
|
217
|
+
return ydb.OptionalType(ydb_type)
|
|
218
|
+
|
|
219
|
+
return ydb_type
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class ParametrizedFunction(functions.Function):
|
|
223
|
+
__visit_name__ = "parametrized_function"
|
|
224
|
+
|
|
225
|
+
def __init__(self, name, params, *args, **kwargs):
|
|
226
|
+
super(ParametrizedFunction, self).__init__(name, *args, **kwargs)
|
|
227
|
+
self._func_name = name
|
|
228
|
+
self._func_params = params
|
|
229
|
+
self.params_expr = ClauseList(operator=functions.operators.comma_op, group_contents=True, *params).self_group()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class YqlCompiler(StrSQLCompiler):
|
|
233
|
+
compound_keywords = COMPOUND_KEYWORDS
|
|
234
|
+
|
|
235
|
+
def render_bind_cast(self, type_, dbapi_type, sqltext):
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
def group_by_clause(self, select, **kw):
|
|
239
|
+
# Hack to ensure it is possible to define labels in groupby.
|
|
240
|
+
kw.update(within_columns_clause=True)
|
|
241
|
+
return super(YqlCompiler, self).group_by_clause(select, **kw)
|
|
242
|
+
|
|
243
|
+
def limit_clause(self, select, **kw):
|
|
244
|
+
text = ""
|
|
245
|
+
if select._limit_clause is not None:
|
|
246
|
+
limit_clause = self._maybe_cast(
|
|
247
|
+
select._limit_clause, types.UInt64, skip_types=(types.UInt64, types.UInt32, types.UInt16, types.UInt8)
|
|
248
|
+
)
|
|
249
|
+
text += "\n LIMIT " + self.process(limit_clause, **kw)
|
|
250
|
+
if select._offset_clause is not None:
|
|
251
|
+
offset_clause = self._maybe_cast(
|
|
252
|
+
select._offset_clause, types.UInt64, skip_types=(types.UInt64, types.UInt32, types.UInt16, types.UInt8)
|
|
253
|
+
)
|
|
254
|
+
if select._limit_clause is None:
|
|
255
|
+
text += "\n LIMIT NULL"
|
|
256
|
+
text += " OFFSET " + self.process(offset_clause, **kw)
|
|
257
|
+
return text
|
|
258
|
+
|
|
259
|
+
def _maybe_cast(
|
|
260
|
+
self,
|
|
261
|
+
element: Any,
|
|
262
|
+
cast_to: Type[sa.types.TypeEngine],
|
|
263
|
+
skip_types: Optional[Tuple[Type[sa.types.TypeEngine], ...]] = None,
|
|
264
|
+
) -> Any:
|
|
265
|
+
if not skip_types:
|
|
266
|
+
skip_types = (cast_to,)
|
|
267
|
+
if cast_to not in skip_types:
|
|
268
|
+
skip_types = (*skip_types, cast_to)
|
|
269
|
+
if not hasattr(element, "type") or not isinstance(element.type, skip_types):
|
|
270
|
+
return sa.Cast(element, cast_to)
|
|
271
|
+
return element
|
|
272
|
+
|
|
273
|
+
def render_literal_value(self, value, type_):
|
|
274
|
+
if isinstance(value, str):
|
|
275
|
+
value = "".join(STR_QUOTE_MAP.get(x, x) for x in value)
|
|
276
|
+
return f"'{value}'"
|
|
277
|
+
return super().render_literal_value(value, type_)
|
|
278
|
+
|
|
279
|
+
def visit_lambda(self, lambda_, **kw):
|
|
280
|
+
func = lambda_.func
|
|
281
|
+
spec = inspect_getfullargspec(func)
|
|
282
|
+
|
|
283
|
+
if spec.varargs:
|
|
284
|
+
raise CompileError("Lambdas with *args are not supported")
|
|
285
|
+
if spec.varkw:
|
|
286
|
+
raise CompileError("Lambdas with **kwargs are not supported")
|
|
287
|
+
|
|
288
|
+
args = [literal_column("$" + arg) for arg in spec.args]
|
|
289
|
+
text = f'({", ".join("$" + arg for arg in spec.args)}) -> ' f"{{ RETURN {self.process(func(*args), **kw)} ;}}"
|
|
290
|
+
|
|
291
|
+
return text
|
|
292
|
+
|
|
293
|
+
def visit_parametrized_function(self, func, **kwargs):
|
|
294
|
+
name = func.name
|
|
295
|
+
name_parts = []
|
|
296
|
+
for name in name.split("::"):
|
|
297
|
+
fname = (
|
|
298
|
+
self.preparer.quote(name)
|
|
299
|
+
if self.preparer._requires_quotes_illegal_chars(name) or isinstance(name, sa.sql.elements.quoted_name)
|
|
300
|
+
else name
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
name_parts.append(fname)
|
|
304
|
+
|
|
305
|
+
name = "::".join(name_parts)
|
|
306
|
+
params = func.params_expr._compiler_dispatch(self, **kwargs)
|
|
307
|
+
args = self.function_argspec(func, **kwargs)
|
|
308
|
+
return "%(name)s%(params)s%(args)s" % dict(name=name, params=params, args=args)
|
|
309
|
+
|
|
310
|
+
def visit_function(self, func, add_to_result_map=None, **kwargs):
|
|
311
|
+
# Copypaste of `sa.sql.compiler.SQLCompiler.visit_function` with
|
|
312
|
+
# `::` as namespace separator instead of `.`
|
|
313
|
+
if add_to_result_map:
|
|
314
|
+
add_to_result_map(func.name, func.name, (), func.type)
|
|
315
|
+
|
|
316
|
+
disp = getattr(self, f"visit_{func.name.lower()}_func", None)
|
|
317
|
+
if disp:
|
|
318
|
+
return disp(func, **kwargs)
|
|
319
|
+
|
|
320
|
+
name = sa.sql.compiler.FUNCTIONS.get(func.__class__)
|
|
321
|
+
if name:
|
|
322
|
+
if func._has_args:
|
|
323
|
+
name += "%(expr)s"
|
|
324
|
+
else:
|
|
325
|
+
name = func.name
|
|
326
|
+
name = (
|
|
327
|
+
self.preparer.quote(name)
|
|
328
|
+
if self.preparer._requires_quotes_illegal_chars(name) or isinstance(name, sa.sql.elements.quoted_name)
|
|
329
|
+
else name
|
|
330
|
+
)
|
|
331
|
+
name += "%(expr)s"
|
|
332
|
+
|
|
333
|
+
return "::".join(
|
|
334
|
+
[
|
|
335
|
+
(
|
|
336
|
+
self.preparer.quote(tok)
|
|
337
|
+
if self.preparer._requires_quotes_illegal_chars(tok)
|
|
338
|
+
or isinstance(name, sa.sql.elements.quoted_name)
|
|
339
|
+
else tok
|
|
340
|
+
)
|
|
341
|
+
for tok in func.packagenames
|
|
342
|
+
]
|
|
343
|
+
+ [name]
|
|
344
|
+
) % {"expr": self.function_argspec(func, **kwargs)}
|
|
345
|
+
|
|
346
|
+
def _yson_convert_to(self, statement: str, target_type: sa.types.TypeEngine) -> str:
|
|
347
|
+
type_name = target_type.compile(self.dialect)
|
|
348
|
+
if isinstance(target_type, sa.Numeric) and not isinstance(target_type, (sa.Float, sa.Double)):
|
|
349
|
+
# Since Decimal is stored in JSON either as String or as Float
|
|
350
|
+
string_value = f"Yson::ConvertTo({statement}, Optional<String>, Yson::Options(true AS AutoConvert))"
|
|
351
|
+
return f"CAST({string_value} AS Optional<{type_name}>)"
|
|
352
|
+
return f"Yson::ConvertTo({statement}, Optional<{type_name}>)"
|
|
353
|
+
|
|
354
|
+
def visit_json_getitem_op_binary(self, binary: sa.BinaryExpression, operator, **kw) -> str:
|
|
355
|
+
json_field = self.process(binary.left, **kw)
|
|
356
|
+
index = self.process(binary.right, **kw)
|
|
357
|
+
return self._yson_convert_to(f"{json_field}[{index}]", binary.type)
|
|
358
|
+
|
|
359
|
+
def visit_json_path_getitem_op_binary(self, binary: sa.BinaryExpression, operator, **kw) -> str:
|
|
360
|
+
json_field = self.process(binary.left, **kw)
|
|
361
|
+
path = self.process(binary.right, **kw)
|
|
362
|
+
return self._yson_convert_to(f"Yson::YPath({json_field}, {path})", binary.type)
|
|
363
|
+
|
|
364
|
+
def visit_regexp_match_op_binary(self, binary, operator, **kw):
|
|
365
|
+
return self._generate_generic_binary(binary, " REGEXP ", **kw)
|
|
366
|
+
|
|
367
|
+
def visit_not_regexp_match_op_binary(self, binary, operator, **kw):
|
|
368
|
+
return self._generate_generic_binary(binary, " NOT REGEXP ", **kw)
|
|
369
|
+
|
|
370
|
+
def _is_bound_to_nullable_column(self, bind_name: str) -> bool:
|
|
371
|
+
if bind_name in self.column_keys and hasattr(self.compile_state, "dml_table"):
|
|
372
|
+
if bind_name in self.compile_state.dml_table.c:
|
|
373
|
+
column = self.compile_state.dml_table.c[bind_name]
|
|
374
|
+
return column.nullable and not column.primary_key
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
def _guess_bound_variable_type_by_parameters(
|
|
378
|
+
self, bind: sa.BindParameter, post_compile_bind_values: list
|
|
379
|
+
) -> Optional[sa.types.TypeEngine]:
|
|
380
|
+
bind_type = bind.type
|
|
381
|
+
if bind.expanding or (isinstance(bind.type, sa.types.NullType) and post_compile_bind_values):
|
|
382
|
+
not_null_values = [v for v in post_compile_bind_values if v is not None]
|
|
383
|
+
if not_null_values:
|
|
384
|
+
bind_type = sa.BindParameter("", not_null_values[0]).type
|
|
385
|
+
|
|
386
|
+
if isinstance(bind_type, sa.types.NullType):
|
|
387
|
+
return None
|
|
388
|
+
|
|
389
|
+
return bind_type
|
|
390
|
+
|
|
391
|
+
def _get_expanding_bind_names(self, bind_name: str, parameters_values: Mapping[str, List[Any]]) -> List[Any]:
|
|
392
|
+
expanding_bind_names = []
|
|
393
|
+
for parameter_name in parameters_values:
|
|
394
|
+
parameter_bind_name = "_".join(parameter_name.split("_")[:-1])
|
|
395
|
+
if parameter_bind_name == bind_name:
|
|
396
|
+
expanding_bind_names.append(parameter_name)
|
|
397
|
+
return expanding_bind_names
|
|
398
|
+
|
|
399
|
+
def get_bind_types(
|
|
400
|
+
self, post_compile_parameters: Optional[Union[Sequence[Mapping[str, Any]], Mapping[str, Any]]]
|
|
401
|
+
) -> Dict[str, Union[ydb.PrimitiveType, ydb.AbstractTypeBuilder]]:
|
|
402
|
+
"""
|
|
403
|
+
This method extracts information about bound variables from the table definition and parameters.
|
|
404
|
+
"""
|
|
405
|
+
if isinstance(post_compile_parameters, collections.abc.Mapping):
|
|
406
|
+
post_compile_parameters = [post_compile_parameters]
|
|
407
|
+
|
|
408
|
+
parameters_values = collections.defaultdict(list)
|
|
409
|
+
for parameters_entry in post_compile_parameters:
|
|
410
|
+
for parameter_name, parameter_value in parameters_entry.items():
|
|
411
|
+
parameters_values[parameter_name].append(parameter_value)
|
|
412
|
+
|
|
413
|
+
parameter_types = {}
|
|
414
|
+
for bind_name in self.bind_names.values():
|
|
415
|
+
bind = self.binds[bind_name]
|
|
416
|
+
|
|
417
|
+
if bind.literal_execute:
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
if not bind.expanding:
|
|
421
|
+
post_compile_bind_names = [bind_name]
|
|
422
|
+
post_compile_bind_values = parameters_values[bind_name]
|
|
423
|
+
else:
|
|
424
|
+
post_compile_bind_names = self._get_expanding_bind_names(bind_name, parameters_values)
|
|
425
|
+
post_compile_bind_values = []
|
|
426
|
+
for parameter_name, parameter_values in parameters_values.items():
|
|
427
|
+
if parameter_name in post_compile_bind_names:
|
|
428
|
+
post_compile_bind_values.extend(parameter_values)
|
|
429
|
+
|
|
430
|
+
is_optional = self._is_bound_to_nullable_column(bind_name)
|
|
431
|
+
if not post_compile_bind_values or None in post_compile_bind_values:
|
|
432
|
+
is_optional = True
|
|
433
|
+
|
|
434
|
+
bind_type = self._guess_bound_variable_type_by_parameters(bind, post_compile_bind_values)
|
|
435
|
+
|
|
436
|
+
if bind_type:
|
|
437
|
+
for post_compile_bind_name in post_compile_bind_names:
|
|
438
|
+
parameter_types[post_compile_bind_name] = YqlTypeCompiler(self.dialect).get_ydb_type(
|
|
439
|
+
bind_type, is_optional
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
return parameter_types
|
|
443
|
+
|
|
444
|
+
def visit_upsert(self, insert_stmt, visited_bindparam=None, **kw):
|
|
445
|
+
return self.visit_insert(insert_stmt, visited_bindparam, **kw).replace("INSERT", "UPSERT", 1)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class YqlDDLCompiler(DDLCompiler):
|
|
449
|
+
def post_create_table(self, table: sa.Table) -> str:
|
|
450
|
+
ydb_opts = table.dialect_options["ydb"]
|
|
451
|
+
with_clause_list = self._render_table_partitioning_settings(ydb_opts)
|
|
452
|
+
if with_clause_list:
|
|
453
|
+
with_clause_text = ",\n".join(with_clause_list)
|
|
454
|
+
return f"\nWITH (\n\t{with_clause_text}\n)"
|
|
455
|
+
return ""
|
|
456
|
+
|
|
457
|
+
def _render_table_partitioning_settings(self, ydb_opts: Dict[str, Any]) -> List[str]:
|
|
458
|
+
table_partitioning_settings = []
|
|
459
|
+
if ydb_opts["auto_partitioning_by_size"] is not None:
|
|
460
|
+
auto_partitioning_by_size = "ENABLED" if ydb_opts["auto_partitioning_by_size"] else "DISABLED"
|
|
461
|
+
table_partitioning_settings.append(f"AUTO_PARTITIONING_BY_SIZE = {auto_partitioning_by_size}")
|
|
462
|
+
if ydb_opts["auto_partitioning_by_load"] is not None:
|
|
463
|
+
auto_partitioning_by_load = "ENABLED" if ydb_opts["auto_partitioning_by_load"] else "DISABLED"
|
|
464
|
+
table_partitioning_settings.append(f"AUTO_PARTITIONING_BY_LOAD = {auto_partitioning_by_load}")
|
|
465
|
+
if ydb_opts["auto_partitioning_partition_size_mb"] is not None:
|
|
466
|
+
table_partitioning_settings.append(
|
|
467
|
+
f"AUTO_PARTITIONING_PARTITION_SIZE_MB = {ydb_opts['auto_partitioning_partition_size_mb']}"
|
|
468
|
+
)
|
|
469
|
+
if ydb_opts["auto_partitioning_min_partitions_count"] is not None:
|
|
470
|
+
table_partitioning_settings.append(
|
|
471
|
+
f"AUTO_PARTITIONING_MIN_PARTITIONS_COUNT = {ydb_opts['auto_partitioning_min_partitions_count']}"
|
|
472
|
+
)
|
|
473
|
+
if ydb_opts["auto_partitioning_max_partitions_count"] is not None:
|
|
474
|
+
table_partitioning_settings.append(
|
|
475
|
+
f"AUTO_PARTITIONING_MAX_PARTITIONS_COUNT = {ydb_opts['auto_partitioning_max_partitions_count']}"
|
|
476
|
+
)
|
|
477
|
+
if ydb_opts["uniform_partitions"] is not None:
|
|
478
|
+
table_partitioning_settings.append(f"UNIFORM_PARTITIONS = {ydb_opts['uniform_partitions']}")
|
|
479
|
+
if ydb_opts["partition_at_keys"] is not None:
|
|
480
|
+
table_partitioning_settings.append(f"PARTITION_AT_KEYS = {ydb_opts['partition_at_keys']}")
|
|
481
|
+
return table_partitioning_settings
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def upsert(table):
|
|
485
|
+
return Upsert(table)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
COLUMN_TYPES = {
|
|
489
|
+
ydb.PrimitiveType.Int8: sa.INTEGER,
|
|
490
|
+
ydb.PrimitiveType.Int16: sa.INTEGER,
|
|
491
|
+
ydb.PrimitiveType.Int32: sa.INTEGER,
|
|
492
|
+
ydb.PrimitiveType.Int64: sa.INTEGER,
|
|
493
|
+
ydb.PrimitiveType.Uint8: sa.INTEGER,
|
|
494
|
+
ydb.PrimitiveType.Uint16: sa.INTEGER,
|
|
495
|
+
ydb.PrimitiveType.Uint32: types.UInt32,
|
|
496
|
+
ydb.PrimitiveType.Uint64: types.UInt64,
|
|
497
|
+
ydb.PrimitiveType.Float: sa.FLOAT,
|
|
498
|
+
ydb.PrimitiveType.Double: sa.FLOAT,
|
|
499
|
+
ydb.PrimitiveType.String: sa.BINARY,
|
|
500
|
+
ydb.PrimitiveType.Utf8: sa.TEXT,
|
|
501
|
+
ydb.PrimitiveType.Json: sa.JSON,
|
|
502
|
+
ydb.PrimitiveType.JsonDocument: sa.JSON,
|
|
503
|
+
ydb.DecimalType: sa.DECIMAL,
|
|
504
|
+
ydb.PrimitiveType.Yson: sa.TEXT,
|
|
505
|
+
ydb.PrimitiveType.Date: sa.DATE,
|
|
506
|
+
ydb.PrimitiveType.Datetime: sa.DATETIME,
|
|
507
|
+
ydb.PrimitiveType.Timestamp: sa.DATETIME,
|
|
508
|
+
ydb.PrimitiveType.Interval: sa.INTEGER,
|
|
509
|
+
ydb.PrimitiveType.Bool: sa.BOOLEAN,
|
|
510
|
+
ydb.PrimitiveType.DyNumber: sa.TEXT,
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _get_column_info(t):
|
|
515
|
+
nullable = False
|
|
516
|
+
if isinstance(t, ydb.OptionalType):
|
|
517
|
+
nullable = True
|
|
518
|
+
t = t.item
|
|
519
|
+
|
|
520
|
+
if isinstance(t, ydb.DecimalType):
|
|
521
|
+
return sa.DECIMAL(precision=t.precision, scale=t.scale), nullable
|
|
522
|
+
|
|
523
|
+
return COLUMN_TYPES[t], nullable
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class YqlDialect(StrCompileDialect):
|
|
527
|
+
name = "yql"
|
|
528
|
+
driver = "ydb"
|
|
529
|
+
|
|
530
|
+
supports_alter = False
|
|
531
|
+
max_identifier_length = 63
|
|
532
|
+
supports_sane_rowcount = False
|
|
533
|
+
supports_statement_cache = False
|
|
534
|
+
|
|
535
|
+
supports_native_enum = False
|
|
536
|
+
supports_native_boolean = True
|
|
537
|
+
supports_native_decimal = True
|
|
538
|
+
supports_smallserial = False
|
|
539
|
+
supports_schemas = False
|
|
540
|
+
supports_constraint_comments = False
|
|
541
|
+
supports_json_type = True
|
|
542
|
+
|
|
543
|
+
insert_returning = False
|
|
544
|
+
update_returning = False
|
|
545
|
+
delete_returning = False
|
|
546
|
+
|
|
547
|
+
supports_sequences = False
|
|
548
|
+
sequences_optional = False
|
|
549
|
+
preexecute_autoincrement_sequences = True
|
|
550
|
+
postfetch_lastrowid = False
|
|
551
|
+
|
|
552
|
+
supports_default_values = False
|
|
553
|
+
supports_empty_insert = False
|
|
554
|
+
supports_multivalues_insert = True
|
|
555
|
+
default_paramstyle = "qmark"
|
|
556
|
+
|
|
557
|
+
isolation_level = None
|
|
558
|
+
|
|
559
|
+
preparer = YqlIdentifierPreparer
|
|
560
|
+
statement_compiler = YqlCompiler
|
|
561
|
+
ddl_compiler = YqlDDLCompiler
|
|
562
|
+
type_compiler = YqlTypeCompiler
|
|
563
|
+
colspecs = {
|
|
564
|
+
sa.types.JSON: types.YqlJSON,
|
|
565
|
+
sa.types.JSON.JSONPathType: types.YqlJSON.YqlJSONPathType,
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
construct_arguments = [
|
|
569
|
+
(
|
|
570
|
+
sa.schema.Table,
|
|
571
|
+
{
|
|
572
|
+
"auto_partitioning_by_size": None,
|
|
573
|
+
"auto_partitioning_by_load": None,
|
|
574
|
+
"auto_partitioning_partition_size_mb": None,
|
|
575
|
+
"auto_partitioning_min_partitions_count": None,
|
|
576
|
+
"auto_partitioning_max_partitions_count": None,
|
|
577
|
+
"uniform_partitions": None,
|
|
578
|
+
"partition_at_keys": None,
|
|
579
|
+
},
|
|
580
|
+
),
|
|
581
|
+
]
|
|
582
|
+
|
|
583
|
+
@classmethod
|
|
584
|
+
def import_dbapi(cls: Any):
|
|
585
|
+
return dbapi.YdbDBApi()
|
|
586
|
+
|
|
587
|
+
def __init__(self, json_serializer=None, json_deserializer=None, **kwargs):
|
|
588
|
+
super().__init__(**kwargs)
|
|
589
|
+
|
|
590
|
+
self._json_deserializer = json_deserializer
|
|
591
|
+
self._json_serializer = json_serializer
|
|
592
|
+
|
|
593
|
+
def _describe_table(self, connection, table_name, schema=None):
|
|
594
|
+
if schema is not None:
|
|
595
|
+
raise dbapi.NotSupportedError("unsupported on non empty schema")
|
|
596
|
+
|
|
597
|
+
qt = table_name if isinstance(table_name, str) else table_name.name
|
|
598
|
+
raw_conn = connection.connection
|
|
599
|
+
try:
|
|
600
|
+
return raw_conn.describe(qt)
|
|
601
|
+
except dbapi.DatabaseError as e:
|
|
602
|
+
raise NoSuchTableError(qt) from e
|
|
603
|
+
|
|
604
|
+
@reflection.cache
|
|
605
|
+
def get_columns(self, connection, table_name, schema=None, **kw):
|
|
606
|
+
table = self._describe_table(connection, table_name, schema)
|
|
607
|
+
as_compatible = []
|
|
608
|
+
for column in table.columns:
|
|
609
|
+
col_type, nullable = _get_column_info(column.type)
|
|
610
|
+
as_compatible.append(
|
|
611
|
+
{
|
|
612
|
+
"name": column.name,
|
|
613
|
+
"type": col_type,
|
|
614
|
+
"nullable": nullable,
|
|
615
|
+
"default": None,
|
|
616
|
+
}
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
return as_compatible
|
|
620
|
+
|
|
621
|
+
@reflection.cache
|
|
622
|
+
def get_table_names(self, connection, schema=None, **kw) -> List[str]:
|
|
623
|
+
if schema:
|
|
624
|
+
raise dbapi.NotSupportedError("unsupported on non empty schema")
|
|
625
|
+
|
|
626
|
+
raw_conn = connection.connection
|
|
627
|
+
return raw_conn.get_table_names()
|
|
628
|
+
|
|
629
|
+
@reflection.cache
|
|
630
|
+
def has_table(self, connection, table_name, schema=None, **kwargs):
|
|
631
|
+
try:
|
|
632
|
+
self._describe_table(connection, table_name, schema)
|
|
633
|
+
return True
|
|
634
|
+
except NoSuchTableError:
|
|
635
|
+
return False
|
|
636
|
+
|
|
637
|
+
@reflection.cache
|
|
638
|
+
def get_pk_constraint(self, connection, table_name, schema=None, **kwargs):
|
|
639
|
+
table = self._describe_table(connection, table_name, schema)
|
|
640
|
+
return {"constrained_columns": table.primary_key, "name": None}
|
|
641
|
+
|
|
642
|
+
@reflection.cache
|
|
643
|
+
def get_foreign_keys(self, connection, table_name, schema=None, **kwargs):
|
|
644
|
+
# foreign keys unsupported
|
|
645
|
+
return []
|
|
646
|
+
|
|
647
|
+
@reflection.cache
|
|
648
|
+
def get_indexes(self, connection, table_name, schema=None, **kwargs):
|
|
649
|
+
# TODO: implement me
|
|
650
|
+
return []
|
|
651
|
+
|
|
652
|
+
def set_isolation_level(self, dbapi_connection: dbapi.Connection, level: str) -> None:
|
|
653
|
+
dbapi_connection.set_isolation_level(level)
|
|
654
|
+
|
|
655
|
+
def get_default_isolation_level(self, dbapi_conn: dbapi.Connection) -> str:
|
|
656
|
+
return dbapi.IsolationLevel.AUTOCOMMIT
|
|
657
|
+
|
|
658
|
+
def get_isolation_level(self, dbapi_connection: dbapi.Connection) -> str:
|
|
659
|
+
return dbapi_connection.get_isolation_level()
|
|
660
|
+
|
|
661
|
+
def connect(self, *cargs, **cparams):
|
|
662
|
+
return self.loaded_dbapi.connect(*cargs, **cparams)
|
|
663
|
+
|
|
664
|
+
def do_begin(self, dbapi_connection: dbapi.Connection) -> None:
|
|
665
|
+
dbapi_connection.begin()
|
|
666
|
+
|
|
667
|
+
def do_rollback(self, dbapi_connection: dbapi.Connection) -> None:
|
|
668
|
+
dbapi_connection.rollback()
|
|
669
|
+
|
|
670
|
+
def do_commit(self, dbapi_connection: dbapi.Connection) -> None:
|
|
671
|
+
dbapi_connection.commit()
|
|
672
|
+
|
|
673
|
+
def _format_variables(
|
|
674
|
+
self,
|
|
675
|
+
statement: str,
|
|
676
|
+
parameters: Optional[Union[Sequence[Mapping[str, Any]], Mapping[str, Any]]],
|
|
677
|
+
execute_many: bool,
|
|
678
|
+
) -> Tuple[str, Optional[Union[Sequence[Mapping[str, Any]], Mapping[str, Any]]]]:
|
|
679
|
+
formatted_statement = statement
|
|
680
|
+
formatted_parameters = None
|
|
681
|
+
|
|
682
|
+
if parameters:
|
|
683
|
+
if execute_many:
|
|
684
|
+
parameters_sequence: Sequence[Mapping[str, Any]] = parameters
|
|
685
|
+
variable_names = set()
|
|
686
|
+
formatted_parameters = []
|
|
687
|
+
for i in range(len(parameters_sequence)):
|
|
688
|
+
variable_names.update(set(parameters_sequence[i].keys()))
|
|
689
|
+
formatted_parameters.append({f"${k}": v for k, v in parameters_sequence[i].items()})
|
|
690
|
+
else:
|
|
691
|
+
variable_names = set(parameters.keys())
|
|
692
|
+
formatted_parameters = {f"${k}": v for k, v in parameters.items()}
|
|
693
|
+
|
|
694
|
+
formatted_variable_names = {variable_name: f"${variable_name}" for variable_name in variable_names}
|
|
695
|
+
formatted_statement = formatted_statement % formatted_variable_names
|
|
696
|
+
|
|
697
|
+
formatted_statement = formatted_statement.replace("%%", "%")
|
|
698
|
+
return formatted_statement, formatted_parameters
|
|
699
|
+
|
|
700
|
+
def _make_ydb_operation(
|
|
701
|
+
self,
|
|
702
|
+
statement: str,
|
|
703
|
+
context: Optional[DefaultExecutionContext] = None,
|
|
704
|
+
parameters: Optional[Union[Sequence[Mapping[str, Any]], Mapping[str, Any]]] = None,
|
|
705
|
+
execute_many: bool = False,
|
|
706
|
+
) -> Tuple[dbapi.YdbQuery, Optional[Union[Sequence[Mapping[str, Any]], Mapping[str, Any]]]]:
|
|
707
|
+
is_ddl = context.isddl if context is not None else False
|
|
708
|
+
|
|
709
|
+
if not is_ddl and parameters:
|
|
710
|
+
parameters_types = context.compiled.get_bind_types(parameters)
|
|
711
|
+
parameters_types = {f"${k}": v for k, v in parameters_types.items()}
|
|
712
|
+
statement, parameters = self._format_variables(statement, parameters, execute_many)
|
|
713
|
+
return dbapi.YdbQuery(yql_text=statement, parameters_types=parameters_types, is_ddl=is_ddl), parameters
|
|
714
|
+
|
|
715
|
+
statement, parameters = self._format_variables(statement, parameters, execute_many)
|
|
716
|
+
return dbapi.YdbQuery(yql_text=statement, is_ddl=is_ddl), parameters
|
|
717
|
+
|
|
718
|
+
def do_ping(self, dbapi_connection: dbapi.Connection) -> bool:
|
|
719
|
+
cursor = dbapi_connection.cursor()
|
|
720
|
+
statement, _ = self._make_ydb_operation(self._dialect_specific_select_one)
|
|
721
|
+
try:
|
|
722
|
+
cursor.execute(statement)
|
|
723
|
+
finally:
|
|
724
|
+
cursor.close()
|
|
725
|
+
return True
|
|
726
|
+
|
|
727
|
+
def do_executemany(
|
|
728
|
+
self,
|
|
729
|
+
cursor: dbapi.Cursor,
|
|
730
|
+
statement: str,
|
|
731
|
+
parameters: Optional[Sequence[Mapping[str, Any]]],
|
|
732
|
+
context: Optional[DefaultExecutionContext] = None,
|
|
733
|
+
) -> None:
|
|
734
|
+
operation, parameters = self._make_ydb_operation(statement, context, parameters, execute_many=True)
|
|
735
|
+
cursor.executemany(operation, parameters)
|
|
736
|
+
|
|
737
|
+
def do_execute(
|
|
738
|
+
self,
|
|
739
|
+
cursor: dbapi.Cursor,
|
|
740
|
+
statement: str,
|
|
741
|
+
parameters: Optional[Mapping[str, Any]] = None,
|
|
742
|
+
context: Optional[DefaultExecutionContext] = None,
|
|
743
|
+
) -> None:
|
|
744
|
+
operation, parameters = self._make_ydb_operation(statement, context, parameters, execute_many=False)
|
|
745
|
+
cursor.execute(operation, parameters)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
class AsyncYqlDialect(YqlDialect):
|
|
749
|
+
driver = "ydb_async"
|
|
750
|
+
is_async = True
|
|
751
|
+
supports_statement_cache = False
|
|
752
|
+
|
|
753
|
+
def connect(self, *cargs, **cparams):
|
|
754
|
+
return self.loaded_dbapi.async_connect(*cargs, **cparams)
|