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.
@@ -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)