ydb-sqlglot-plugin 0.1.0__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.
- ydb_sqlglot/__init__.py +7 -0
- ydb_sqlglot/version.py +1 -0
- ydb_sqlglot/ydb.py +1815 -0
- ydb_sqlglot_plugin-0.1.0.dist-info/METADATA +92 -0
- ydb_sqlglot_plugin-0.1.0.dist-info/RECORD +9 -0
- ydb_sqlglot_plugin-0.1.0.dist-info/WHEEL +5 -0
- ydb_sqlglot_plugin-0.1.0.dist-info/entry_points.txt +2 -0
- ydb_sqlglot_plugin-0.1.0.dist-info/licenses/LICENSE +201 -0
- ydb_sqlglot_plugin-0.1.0.dist-info/top_level.txt +1 -0
ydb_sqlglot/ydb.py
ADDED
|
@@ -0,0 +1,1815 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp, tokens, generator, transforms, TokenType, parser, Generator, Expression
|
|
4
|
+
from sqlglot.dialects.dialect import Dialect, unit_to_var
|
|
5
|
+
from sqlglot.dialects.dialect import NormalizationStrategy, concat_to_dpipe_sql
|
|
6
|
+
from sqlglot.helper import name_sequence, seq_get, flatten
|
|
7
|
+
from sqlglot.optimizer.simplify import simplify
|
|
8
|
+
from sqlglot.transforms import move_ctes_to_top_level
|
|
9
|
+
from sqlglot.optimizer.scope import find_in_scope, ScopeType, traverse_scope
|
|
10
|
+
from sqlglot.transforms import eliminate_join_marks
|
|
11
|
+
|
|
12
|
+
JOIN_ATTRS = ("on", "side", "kind", "using", "method")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def rename_func_not_normalize(name: str) -> t.Callable[[Generator, exp.Expression], str]:
|
|
16
|
+
return lambda self, expression: self.func(
|
|
17
|
+
name, *flatten(expression.args.values()), normalize=False
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def table_names_to_lower_case(expression: exp.Expression) -> exp.Expression:
|
|
22
|
+
for table in expression.find_all(exp.Table):
|
|
23
|
+
if isinstance(table.this, exp.Identifier):
|
|
24
|
+
ident = table.this
|
|
25
|
+
table.set("this", ident.this.lower())
|
|
26
|
+
return expression
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def make_db_name_lower(expression: exp.Expression) -> exp.Expression:
|
|
30
|
+
"""
|
|
31
|
+
Converts all database names to uppercase
|
|
32
|
+
Args:
|
|
33
|
+
expression: The SQL expression to modify
|
|
34
|
+
Returns:
|
|
35
|
+
Modified expression with uppercase database names
|
|
36
|
+
"""
|
|
37
|
+
for table in expression.find_all(exp.Table):
|
|
38
|
+
if table.db:
|
|
39
|
+
table.set("db", table.db.lower())
|
|
40
|
+
|
|
41
|
+
return expression
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def make_db_name_lower(expression: exp.Expression) -> exp.Expression:
|
|
45
|
+
"""
|
|
46
|
+
Converts all database names to uppercase
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
expression: The SQL expression to modify
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Modified expression with uppercase database names
|
|
53
|
+
"""
|
|
54
|
+
for table in expression.find_all(exp.Table):
|
|
55
|
+
if table.db:
|
|
56
|
+
table.set("db", table.db.lower())
|
|
57
|
+
|
|
58
|
+
return expression
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def apply_alias_to_select_from_table(expression: exp.Expression) -> Expression:
|
|
62
|
+
"""
|
|
63
|
+
Applies aliases to columns in SELECT statements that reference tables
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
expression: The SQL expression to modify
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Modified expression with aliases applied to columns
|
|
70
|
+
"""
|
|
71
|
+
for column in expression.find_all(exp.Column):
|
|
72
|
+
if not isinstance(column.this, exp.Star):
|
|
73
|
+
if hasattr(column, "table") and column.table and len(column.table) > 0:
|
|
74
|
+
if isinstance(column.parent, exp.Select):
|
|
75
|
+
column.replace(exp.alias_(column, column.alias_or_name))
|
|
76
|
+
return expression
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _replace(expression, condition):
|
|
80
|
+
"""
|
|
81
|
+
Helper function to replace an expression with a condition
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
expression: The expression to replace
|
|
85
|
+
condition: The condition to replace with
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The replaced expression
|
|
89
|
+
"""
|
|
90
|
+
return expression.replace(exp.condition(condition))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _other_operand(expression):
|
|
94
|
+
"""
|
|
95
|
+
Returns the other operand of a binary operation involving a subquery
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
expression: The expression containing a binary operation
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The operand that is not a subquery, or None
|
|
102
|
+
"""
|
|
103
|
+
if isinstance(expression, exp.In):
|
|
104
|
+
return expression.this
|
|
105
|
+
|
|
106
|
+
if isinstance(expression, (exp.Any, exp.All)):
|
|
107
|
+
return _other_operand(expression.parent)
|
|
108
|
+
|
|
109
|
+
if isinstance(expression, exp.Binary):
|
|
110
|
+
return (
|
|
111
|
+
expression.right
|
|
112
|
+
if isinstance(expression.left, (exp.Subquery, exp.Any, exp.Exists, exp.All))
|
|
113
|
+
else expression.left
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class YDB(Dialect):
|
|
120
|
+
"""
|
|
121
|
+
YDB SQL dialect implementation for sqlglot.
|
|
122
|
+
Implements the specific syntax and features of YDB database.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
DATE_FORMAT = "'%Y-%m-%d'"
|
|
126
|
+
TIME_FORMAT = "'%Y-%m-%d %H:%M:%S'"
|
|
127
|
+
|
|
128
|
+
TIME_MAPPING = {
|
|
129
|
+
"%Y": "%Y",
|
|
130
|
+
"%m": "%m",
|
|
131
|
+
"%d": "%d",
|
|
132
|
+
"%H": "%H",
|
|
133
|
+
"%M": "%M",
|
|
134
|
+
"%S": "%S",
|
|
135
|
+
}
|
|
136
|
+
NORMALIZE_FUNCTIONS = False
|
|
137
|
+
|
|
138
|
+
class Tokenizer(tokens.Tokenizer):
|
|
139
|
+
"""
|
|
140
|
+
Tokenizer implementation for YDB SQL dialect.
|
|
141
|
+
Defines how the SQL text is broken into tokens.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
SINGLE_TOKENS = {
|
|
145
|
+
**tokens.Tokenizer.SINGLE_TOKENS,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
SUPPORTS_VALUES_DEFAULT = False
|
|
149
|
+
QUOTES = ["'", '"']
|
|
150
|
+
COMMENTS = ["--", ("/*", "*/")]
|
|
151
|
+
IDENTIFIERS = ["`"]
|
|
152
|
+
|
|
153
|
+
class Parser(parser.Parser):
|
|
154
|
+
def _parse_struct_types(self, type_required=True) -> t.Optional[exp.Expression]:
|
|
155
|
+
if not self._curr:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
key = self._parse_id_var()
|
|
159
|
+
if not key:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
if not self._match(TokenType.COLON):
|
|
163
|
+
self.raise_error("Expected colon after struct key")
|
|
164
|
+
|
|
165
|
+
value = self._parse_conjunction()
|
|
166
|
+
if not value:
|
|
167
|
+
self.raise_error("Expected value after colon")
|
|
168
|
+
|
|
169
|
+
return exp.EQ(this=key, expression=value)
|
|
170
|
+
|
|
171
|
+
def _parse_primary(self) -> t.Optional[exp.Expression]:
|
|
172
|
+
if self._match(TokenType.L_PAREN):
|
|
173
|
+
comments = self._prev_comments
|
|
174
|
+
query = self._parse_select()
|
|
175
|
+
|
|
176
|
+
if query:
|
|
177
|
+
expressions = [query]
|
|
178
|
+
else:
|
|
179
|
+
expressions = self._parse_expressions()
|
|
180
|
+
|
|
181
|
+
lambda_expr = self._parse_lambda_body(expressions)
|
|
182
|
+
if lambda_expr:
|
|
183
|
+
return lambda_expr
|
|
184
|
+
|
|
185
|
+
this = self._parse_query_modifiers(seq_get(expressions, 0))
|
|
186
|
+
|
|
187
|
+
if not this and self._match(TokenType.R_PAREN, advance=False):
|
|
188
|
+
this = self.expression(exp.Tuple)
|
|
189
|
+
elif isinstance(this, exp.UNWRAPPED_QUERIES):
|
|
190
|
+
this = self._parse_subquery(this=this, parse_alias=False)
|
|
191
|
+
elif isinstance(this, exp.Subquery):
|
|
192
|
+
this = self._parse_subquery(
|
|
193
|
+
this=self._parse_set_operations(this), parse_alias=False
|
|
194
|
+
)
|
|
195
|
+
elif len(expressions) > 1 or self._prev.token_type == TokenType.COMMA:
|
|
196
|
+
this = self.expression(exp.Tuple, expressions=expressions)
|
|
197
|
+
else:
|
|
198
|
+
this = self.expression(exp.Paren, this=this)
|
|
199
|
+
|
|
200
|
+
if this:
|
|
201
|
+
this.add_comments(comments)
|
|
202
|
+
|
|
203
|
+
self._match_r_paren(expression=this)
|
|
204
|
+
return this
|
|
205
|
+
return super()._parse_primary()
|
|
206
|
+
|
|
207
|
+
def _parse_lambda_body(self, params):
|
|
208
|
+
if (
|
|
209
|
+
self._curr.token_type != TokenType.R_PAREN
|
|
210
|
+
or self._next.token_type != TokenType.ARROW
|
|
211
|
+
):
|
|
212
|
+
return None
|
|
213
|
+
self._advance()
|
|
214
|
+
self._advance()
|
|
215
|
+
self._match(TokenType.L_PAREN)
|
|
216
|
+
|
|
217
|
+
if not (self._curr.text == "RETURN"):
|
|
218
|
+
self.raise_error("Expected lambda body expression after '->'")
|
|
219
|
+
self._advance()
|
|
220
|
+
body = self._parse_conjunction()
|
|
221
|
+
if not body:
|
|
222
|
+
self.raise_error("Expected lambda body expression after '->'")
|
|
223
|
+
|
|
224
|
+
self._match(TokenType.R_BRACE)
|
|
225
|
+
return exp.Lambda(this=body, expressions=params)
|
|
226
|
+
|
|
227
|
+
class Generator(generator.Generator):
|
|
228
|
+
"""
|
|
229
|
+
SQL Generator for YDB dialect.
|
|
230
|
+
Responsible for translating SQL AST back to SQL text with YDB-specific syntax.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
SUPPORTS_VALUES_DEFAULT = False
|
|
234
|
+
NORMALIZATION_STRATEGY = NormalizationStrategy.CASE_SENSITIVE
|
|
235
|
+
JOIN_HINTS = False
|
|
236
|
+
TABLE_HINTS = False
|
|
237
|
+
QUERY_HINTS = False
|
|
238
|
+
NVL2_SUPPORTED = False
|
|
239
|
+
JSON_PATH_BRACKETED_KEY_SUPPORTED = False
|
|
240
|
+
SUPPORTS_CREATE_TABLE_LIKE = False
|
|
241
|
+
SUPPORTS_TABLE_ALIAS_COLUMNS = False
|
|
242
|
+
SUPPORTS_TO_NUMBER = False
|
|
243
|
+
EXCEPT_INTERSECT_SUPPORT_ALL_CLAUSE = False
|
|
244
|
+
SUPPORTS_MEDIAN = False
|
|
245
|
+
JSON_KEY_VALUE_PAIR_SEP = ","
|
|
246
|
+
VARCHAR_REQUIRES_SIZE = False
|
|
247
|
+
CAN_IMPLEMENT_ARRAY_ANY = True
|
|
248
|
+
STRUCT_DELIMITER = ("<|", "|>")
|
|
249
|
+
NULL_ORDERING_SUPPORTED: t.Optional[bool] = True
|
|
250
|
+
NULL_ORDERING = None
|
|
251
|
+
MATCHED_BY_SOURCE = False
|
|
252
|
+
|
|
253
|
+
def __init__(self, **kwargs):
|
|
254
|
+
"""
|
|
255
|
+
Initialize the YDB SQL Generator with optional configuration.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
**kwargs: Additional keyword arguments to pass to the parent Generator.
|
|
259
|
+
"""
|
|
260
|
+
super().__init__(**kwargs)
|
|
261
|
+
self.expression_to_alias = {}
|
|
262
|
+
self.ydb_variables = {}
|
|
263
|
+
|
|
264
|
+
def create_sql(self, expression: exp.Create, pretty=True) -> str:
|
|
265
|
+
"""
|
|
266
|
+
Generate SQL for CREATE expressions with special handling for CREATE VIEW.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
expression: The CREATE expression to generate SQL for
|
|
270
|
+
pretty: Whether to format the SQL with indentation
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Generated SQL string
|
|
274
|
+
"""
|
|
275
|
+
if expression.kind == "VIEW" and expression.this and expression.this.this:
|
|
276
|
+
ident = expression.this.this
|
|
277
|
+
ident_sql = self.sql(ident)
|
|
278
|
+
sql = self.sql(expression.expression)
|
|
279
|
+
|
|
280
|
+
return f"CREATE VIEW {ident_sql} WITH (security_invoker = TRUE) AS {sql}"
|
|
281
|
+
elif expression.kind == "FUNCTION":
|
|
282
|
+
# CREATE -> FUNCTION -> TABLE
|
|
283
|
+
func_name = self.sql(expression.this.this.alias_or_name)
|
|
284
|
+
|
|
285
|
+
params = []
|
|
286
|
+
for param in expression.this.expressions:
|
|
287
|
+
if isinstance(param, exp.ColumnDef):
|
|
288
|
+
param_name = self.sql(param.this)
|
|
289
|
+
params.append(f"${param_name}")
|
|
290
|
+
else:
|
|
291
|
+
params.append(self.sql(param))
|
|
292
|
+
|
|
293
|
+
params_str = ", ".join(params)
|
|
294
|
+
|
|
295
|
+
body = f" RETURN {self.sql(expression.expression)}"
|
|
296
|
+
return f"${func_name} = ({params_str}) -> {{ {body} }};"
|
|
297
|
+
else:
|
|
298
|
+
return super().create_sql(expression)
|
|
299
|
+
|
|
300
|
+
def table_sql(self, expression: exp.Table, copy=True) -> str:
|
|
301
|
+
"""
|
|
302
|
+
Generate SQL for TABLE expressions with proper quoting and database prefix.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
expression: The TABLE expression
|
|
306
|
+
copy: Whether to copy the expression before processing
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Generated SQL string for the table reference
|
|
310
|
+
"""
|
|
311
|
+
prefix = f"{expression.db}/" if expression.db else ""
|
|
312
|
+
sql = f"`{prefix}{expression.name}`"
|
|
313
|
+
|
|
314
|
+
if expression.alias:
|
|
315
|
+
sql += f" AS {expression.alias}"
|
|
316
|
+
|
|
317
|
+
return sql
|
|
318
|
+
|
|
319
|
+
def is_sql(self, expression: exp.Is) -> str:
|
|
320
|
+
"""
|
|
321
|
+
Generate SQL for IS expressions with special handling for IS NOT NULL.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
expression: The IS expression
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Generated SQL string
|
|
328
|
+
"""
|
|
329
|
+
is_sql = super().is_sql(expression)
|
|
330
|
+
|
|
331
|
+
if isinstance(expression.parent, exp.Not):
|
|
332
|
+
# value IS NOT NULL -> NOT (value IS NULL)
|
|
333
|
+
is_sql = self.wrap(is_sql)
|
|
334
|
+
|
|
335
|
+
return is_sql
|
|
336
|
+
|
|
337
|
+
def anonymous_sql(self, expression: exp.Anonymous) -> str:
|
|
338
|
+
"""
|
|
339
|
+
Generate SQL for Anonymous functions, with special handling for YQL lambda variables.
|
|
340
|
+
Variables starting with $ should not be normalized.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
expression: The Anonymous expression
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Generated SQL string
|
|
347
|
+
"""
|
|
348
|
+
# We don't normalize qualified functions such as a.b.foo(), because they can be case-sensitive
|
|
349
|
+
parent = expression.parent
|
|
350
|
+
is_qualified = isinstance(parent, exp.Dot) and expression is parent.expression
|
|
351
|
+
|
|
352
|
+
func_name = self.sql(expression, "this")
|
|
353
|
+
# Don't normalize YQL lambda variables (starting with $) or qualified functions
|
|
354
|
+
normalize = not (is_qualified or func_name.startswith("$"))
|
|
355
|
+
return self.func(func_name, *expression.expressions, normalize=normalize)
|
|
356
|
+
|
|
357
|
+
# YDB doesn't allow comparison of nullable and non-nullable types.
|
|
358
|
+
# Wrapping it in a lambda can help circumvent this limitation.
|
|
359
|
+
# def _wrap_non_optional(self, expr: exp.Expression) -> exp.Expression:
|
|
360
|
+
# """
|
|
361
|
+
# Helper to wrap non-Optional types using the YQL lambda function.
|
|
362
|
+
# Uses the $wrap_non_optional_in_comparisons lambda function.
|
|
363
|
+
#
|
|
364
|
+
# Args:
|
|
365
|
+
# expr: The expression to potentially wrap
|
|
366
|
+
#
|
|
367
|
+
# Returns:
|
|
368
|
+
# Expression wrapped using the lambda function
|
|
369
|
+
# """
|
|
370
|
+
# # Use the lambda function: $wrap_non_optional_in_comparisons(expr)
|
|
371
|
+
# return exp.Anonymous(this="$wrap_non_optional_in_comparisons", expressions=[expr])
|
|
372
|
+
#
|
|
373
|
+
# def eq_sql(self, expression: exp.EQ) -> str:
|
|
374
|
+
# """
|
|
375
|
+
# Generate SQL for EQ (equals) with Just() for non-Optional types.
|
|
376
|
+
# Wraps non-Optional values with Just() to make them Optional.
|
|
377
|
+
#
|
|
378
|
+
# Args:
|
|
379
|
+
# expression: The EQ expression
|
|
380
|
+
#
|
|
381
|
+
# Returns:
|
|
382
|
+
# Generated SQL string with Just() wrapping for non-Optional types
|
|
383
|
+
# """
|
|
384
|
+
# left = self._wrap_non_optional(expression.this)
|
|
385
|
+
# right = self._wrap_non_optional(expression.expression)
|
|
386
|
+
# return self.binary(exp.EQ(this=left, expression=right), "=")
|
|
387
|
+
#
|
|
388
|
+
# def neq_sql(self, expression: exp.NEQ) -> str:
|
|
389
|
+
# """
|
|
390
|
+
# Generate SQL for NEQ (not equals) with Just() for non-Optional types.
|
|
391
|
+
# Wraps non-Optional values with Just() to make them Optional.
|
|
392
|
+
#
|
|
393
|
+
# Args:
|
|
394
|
+
# expression: The NEQ expression
|
|
395
|
+
#
|
|
396
|
+
# Returns:
|
|
397
|
+
# Generated SQL string with Just() wrapping for non-Optional types
|
|
398
|
+
# """
|
|
399
|
+
# left = self._wrap_non_optional(expression.this)
|
|
400
|
+
# right = self._wrap_non_optional(expression.expression)
|
|
401
|
+
# return self.binary(exp.NEQ(this=left, expression=right), "<>")
|
|
402
|
+
#
|
|
403
|
+
# def gt_sql(self, expression: exp.GT) -> str:
|
|
404
|
+
# """
|
|
405
|
+
# Generate SQL for GT (greater than) with Just() for non-Optional types.
|
|
406
|
+
# Wraps non-Optional values with Just() to make them Optional.
|
|
407
|
+
#
|
|
408
|
+
# Args:
|
|
409
|
+
# expression: The GT expression
|
|
410
|
+
#
|
|
411
|
+
# Returns:
|
|
412
|
+
# Generated SQL string with Just() wrapping for non-Optional types
|
|
413
|
+
# """
|
|
414
|
+
# left = self._wrap_non_optional(expression.this)
|
|
415
|
+
# right = self._wrap_non_optional(expression.expression)
|
|
416
|
+
# return self.binary(exp.GT(this=left, expression=right), ">")
|
|
417
|
+
#
|
|
418
|
+
# def gte_sql(self, expression: exp.GTE) -> str:
|
|
419
|
+
# """
|
|
420
|
+
# Generate SQL for GTE (greater than or equal) with Just() for non-Optional types.
|
|
421
|
+
# Wraps non-Optional values with Just() to make them Optional.
|
|
422
|
+
#
|
|
423
|
+
# Args:
|
|
424
|
+
# expression: The GTE expression
|
|
425
|
+
#
|
|
426
|
+
# Returns:
|
|
427
|
+
# Generated SQL string with Just() wrapping for non-Optional types
|
|
428
|
+
# """
|
|
429
|
+
# left = self._wrap_non_optional(expression.this)
|
|
430
|
+
# right = self._wrap_non_optional(expression.expression)
|
|
431
|
+
# return self.binary(exp.GTE(this=left, expression=right), ">=")
|
|
432
|
+
#
|
|
433
|
+
# def lt_sql(self, expression: exp.LT) -> str:
|
|
434
|
+
# """
|
|
435
|
+
# Generate SQL for LT (less than) with Just() for non-Optional types.
|
|
436
|
+
# Wraps non-Optional values with Just() to make them Optional.
|
|
437
|
+
#
|
|
438
|
+
# Args:
|
|
439
|
+
# expression: The LT expression
|
|
440
|
+
#
|
|
441
|
+
# Returns:
|
|
442
|
+
# Generated SQL string with Just() wrapping for non-Optional types
|
|
443
|
+
# """
|
|
444
|
+
# left = self._wrap_non_optional(expression.this)
|
|
445
|
+
# right = self._wrap_non_optional(expression.expression)
|
|
446
|
+
# return self.binary(exp.LT(this=left, expression=right), "<")
|
|
447
|
+
#
|
|
448
|
+
# def lte_sql(self, expression: exp.LTE) -> str:
|
|
449
|
+
# """
|
|
450
|
+
# Generate SQL for LTE (less than or equal) with Just() for non-Optional types.
|
|
451
|
+
# Wraps non-Optional values with Just() to make them Optional.
|
|
452
|
+
#
|
|
453
|
+
# Args:
|
|
454
|
+
# expression: The LTE expression
|
|
455
|
+
#
|
|
456
|
+
# Returns:
|
|
457
|
+
# Generated SQL string with Just() wrapping for non-Optional types
|
|
458
|
+
# """
|
|
459
|
+
# left = self._wrap_non_optional(expression.this)
|
|
460
|
+
# right = self._wrap_non_optional(expression.expression)
|
|
461
|
+
# return self.binary(exp.LTE(this=left, expression=right), "<=")
|
|
462
|
+
|
|
463
|
+
def datatype_sql(self, expression: exp.DataType) -> str:
|
|
464
|
+
"""
|
|
465
|
+
Generate SQL for data type expressions with YDB-specific type mapping.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
expression: The data type expression
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Generated SQL string for the data type
|
|
472
|
+
"""
|
|
473
|
+
if (
|
|
474
|
+
expression.is_type(exp.DataType.Type.NVARCHAR)
|
|
475
|
+
or expression.is_type(exp.DataType.Type.VARCHAR)
|
|
476
|
+
or expression.is_type(exp.DataType.Type.CHAR)
|
|
477
|
+
):
|
|
478
|
+
expression = exp.DataType.build("text")
|
|
479
|
+
elif expression.is_type(exp.DataType.Type.DECIMAL):
|
|
480
|
+
size_expressions = list(expression.find_all(exp.DataTypeParam))
|
|
481
|
+
|
|
482
|
+
column_def = expression.parent
|
|
483
|
+
is_pk = False
|
|
484
|
+
if isinstance(column_def, exp.ColumnDef):
|
|
485
|
+
for constraint in column_def.constraints:
|
|
486
|
+
if isinstance(constraint.kind, exp.PrimaryKeyColumnConstraint):
|
|
487
|
+
expression = exp.DataType.build("int64")
|
|
488
|
+
is_pk = True
|
|
489
|
+
|
|
490
|
+
if is_pk:
|
|
491
|
+
pass
|
|
492
|
+
elif not size_expressions:
|
|
493
|
+
expression = exp.DataType.build("int64")
|
|
494
|
+
else:
|
|
495
|
+
if len(size_expressions) == 1 or (
|
|
496
|
+
len(size_expressions) == 2 and int(size_expressions[1].name) == 0
|
|
497
|
+
):
|
|
498
|
+
if isinstance(size_expressions[0].this, exp.Star):
|
|
499
|
+
expression = exp.DataType.build("decimal(38, 0)")
|
|
500
|
+
else:
|
|
501
|
+
mantis = int(size_expressions[0].name)
|
|
502
|
+
expression = exp.DataType.build(f"decimal({mantis}, 0)")
|
|
503
|
+
else:
|
|
504
|
+
precision = int(size_expressions[0].name)
|
|
505
|
+
scale = int(size_expressions[1].name)
|
|
506
|
+
expression = exp.DataType.build(f"decimal({precision}, {scale})")
|
|
507
|
+
elif expression.is_type(exp.DataType.Type.TIMESTAMP):
|
|
508
|
+
expression = exp.DataType.build("Timestamp")
|
|
509
|
+
elif expression.this in exp.DataType.TEMPORAL_TYPES:
|
|
510
|
+
expression = exp.DataType.build(expression.this)
|
|
511
|
+
elif expression.is_type("float"):
|
|
512
|
+
size_expression = expression.find(exp.DataTypeParam)
|
|
513
|
+
if size_expression:
|
|
514
|
+
size = int(size_expression.name)
|
|
515
|
+
expression = (
|
|
516
|
+
exp.DataType.build("float") if size <= 32 else exp.DataType.build("double")
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
return super().datatype_sql(expression)
|
|
520
|
+
|
|
521
|
+
def primarykeycolumnconstraint_sql(self, expression: exp.PrimaryKeyColumnConstraint) -> str:
|
|
522
|
+
"""
|
|
523
|
+
Generate SQL for PRIMARY KEY column constraints.
|
|
524
|
+
In YDB, these are handled differently at the table level.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
expression: The PRIMARY KEY column constraint
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
Empty string as YDB handles primary keys differently
|
|
531
|
+
"""
|
|
532
|
+
return ""
|
|
533
|
+
|
|
534
|
+
def _cte_to_lambda(self, expression: exp.Expression) -> str:
|
|
535
|
+
"""
|
|
536
|
+
Convert Common Table Expressions (CTEs) to YDB-style lambdas.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
expression: The SQL expression containing CTEs
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
YDB-specific SQL with lambdas instead of CTEs
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
all_ctes = list(expression.find_all(exp.CTE))
|
|
546
|
+
|
|
547
|
+
if not all_ctes:
|
|
548
|
+
output = self.sql(expression)
|
|
549
|
+
else:
|
|
550
|
+
aliases = []
|
|
551
|
+
|
|
552
|
+
def _table_to_var(node):
|
|
553
|
+
if (isinstance(node, exp.Table)) and node.name in aliases:
|
|
554
|
+
return exp.Var(this=f"${node.name} AS {node.alias_or_name}")
|
|
555
|
+
return node
|
|
556
|
+
|
|
557
|
+
for cte in all_ctes:
|
|
558
|
+
alias = cte.alias
|
|
559
|
+
aliases.append(alias)
|
|
560
|
+
|
|
561
|
+
expression.transform(_table_to_var, copy=False)
|
|
562
|
+
|
|
563
|
+
for cte in all_ctes:
|
|
564
|
+
cte.pop()
|
|
565
|
+
|
|
566
|
+
all_with = list(expression.find_all(exp.With))
|
|
567
|
+
for w in all_with:
|
|
568
|
+
w.pop()
|
|
569
|
+
|
|
570
|
+
output = ""
|
|
571
|
+
|
|
572
|
+
for cte in all_ctes:
|
|
573
|
+
cte_sql = self.sql(cte.this)
|
|
574
|
+
output += f"${cte.alias_or_name} = ({cte_sql});\n\n"
|
|
575
|
+
|
|
576
|
+
body_sql = self.sql(expression)
|
|
577
|
+
|
|
578
|
+
output += body_sql
|
|
579
|
+
|
|
580
|
+
ydb_vars_sql = ""
|
|
581
|
+
for var_name, subquery in self.ydb_variables.items():
|
|
582
|
+
subquery_sql = self.sql(subquery)
|
|
583
|
+
ydb_vars_sql += f"${var_name} = ({subquery_sql});\n"
|
|
584
|
+
self.ydb_variables = {}
|
|
585
|
+
output = ydb_vars_sql + output
|
|
586
|
+
return output
|
|
587
|
+
|
|
588
|
+
def _generate_create_table(self, expression: exp.Expression) -> str:
|
|
589
|
+
"""
|
|
590
|
+
Generate CREATE TABLE SQL with YDB-specific syntax.
|
|
591
|
+
Handles primary keys, constraints, and partitioning.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
expression: The CREATE TABLE expression
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
SQL string for creating a table in YDB
|
|
598
|
+
"""
|
|
599
|
+
# Clean up index parts from table
|
|
600
|
+
for ex in list(expression.this.expressions):
|
|
601
|
+
if isinstance(ex, exp.Identifier):
|
|
602
|
+
ex.pop()
|
|
603
|
+
|
|
604
|
+
def enforce_not_null(col):
|
|
605
|
+
"""Add NOT NULL constraint if not present"""
|
|
606
|
+
for constraint in col.constraints:
|
|
607
|
+
if isinstance(constraint.kind, exp.NotNullColumnConstraint):
|
|
608
|
+
break
|
|
609
|
+
else:
|
|
610
|
+
col.append(
|
|
611
|
+
"constraints", exp.ColumnConstraint(kind=exp.NotNullColumnConstraint())
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
def enforce_pk(col):
|
|
615
|
+
"""Add PRIMARY KEY constraint if not present"""
|
|
616
|
+
for constraint in col.constraints:
|
|
617
|
+
if isinstance(constraint.kind, exp.PrimaryKeyColumnConstraint):
|
|
618
|
+
break
|
|
619
|
+
else:
|
|
620
|
+
col.append(
|
|
621
|
+
"constraints", exp.ColumnConstraint(kind=exp.PrimaryKeyColumnConstraint())
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
pks = list(expression.find_all(exp.PrimaryKey))
|
|
625
|
+
if len(pks) > 0:
|
|
626
|
+
for pk in pks:
|
|
627
|
+
for pk_ex in pk.expressions:
|
|
628
|
+
pk_cols = [
|
|
629
|
+
col
|
|
630
|
+
for col in expression.this.find_all(exp.ColumnDef)
|
|
631
|
+
if col.alias_or_name.lower() == pk_ex.alias_or_name.lower()
|
|
632
|
+
]
|
|
633
|
+
if len(pk_cols) > 0:
|
|
634
|
+
col = pk_cols[0]
|
|
635
|
+
enforce_not_null(col)
|
|
636
|
+
enforce_pk(col)
|
|
637
|
+
pk.pop()
|
|
638
|
+
|
|
639
|
+
def is_pk(col):
|
|
640
|
+
"""Check if a column has a PRIMARY KEY constraint"""
|
|
641
|
+
for constraint in col.constraints:
|
|
642
|
+
if isinstance(constraint, exp.ColumnConstraint):
|
|
643
|
+
if isinstance(constraint.kind, exp.PrimaryKeyColumnConstraint):
|
|
644
|
+
return True
|
|
645
|
+
return False
|
|
646
|
+
|
|
647
|
+
for col in expression.find_all(exp.ColumnDef):
|
|
648
|
+
if is_pk(col):
|
|
649
|
+
break
|
|
650
|
+
else:
|
|
651
|
+
col = list(expression.find_all(exp.ColumnDef))[0]
|
|
652
|
+
enforce_pk(col)
|
|
653
|
+
|
|
654
|
+
for col in expression.this.find_all(exp.ColumnDef):
|
|
655
|
+
if is_pk(col):
|
|
656
|
+
enforce_not_null(col)
|
|
657
|
+
|
|
658
|
+
for constraint in list(expression.this.find_all(exp.Constraint)):
|
|
659
|
+
constraint.pop()
|
|
660
|
+
|
|
661
|
+
sql = super().generate(expression)
|
|
662
|
+
|
|
663
|
+
pk_s = []
|
|
664
|
+
for col in expression.find_all(exp.ColumnDef):
|
|
665
|
+
if is_pk(col):
|
|
666
|
+
pk_s.append(col.alias_or_name)
|
|
667
|
+
|
|
668
|
+
if not pk_s:
|
|
669
|
+
raise ValueError("No primary key columns found")
|
|
670
|
+
ind = sql.rfind(")")
|
|
671
|
+
col_names = ",".join([f"`{pk}`" for pk in pk_s])
|
|
672
|
+
sql = sql[:ind] + f", PRIMARY KEY({col_names}))\nPARTITION BY HASH ({col_names});"
|
|
673
|
+
return sql
|
|
674
|
+
|
|
675
|
+
def generate(self, expression: exp.Expression, copy: bool = True) -> str:
|
|
676
|
+
"""
|
|
677
|
+
Generate SQL for any expression with YDB-specific handling.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
expression: The SQL expression to generate
|
|
681
|
+
copy: Whether to copy the expression before processing
|
|
682
|
+
|
|
683
|
+
Returns:
|
|
684
|
+
Generated SQL string
|
|
685
|
+
"""
|
|
686
|
+
|
|
687
|
+
self.unnest_subqueries(expression)
|
|
688
|
+
expression = eliminate_join_marks(expression)
|
|
689
|
+
expression = expression.copy() if copy else expression
|
|
690
|
+
|
|
691
|
+
# Without pragmas, some queries may not work - for example, implicit cross joins are disabled by default.
|
|
692
|
+
# pragma_statements = []
|
|
693
|
+
#
|
|
694
|
+
# if isinstance(expression, (exp.Select, exp.Insert, exp.Update, exp.Delete, exp.Create)):
|
|
695
|
+
# pragma_statements = ['PRAGMA AnsiImplicitCrossJoin;',
|
|
696
|
+
# 'PRAGMA AnsiInForEmptyOrNullableItemsCollections;']
|
|
697
|
+
|
|
698
|
+
if not isinstance(expression, exp.Create) or (
|
|
699
|
+
isinstance(expression, exp.Create)
|
|
700
|
+
and expression.kind
|
|
701
|
+
and expression.kind.lower() != "table"
|
|
702
|
+
):
|
|
703
|
+
sql = self._cte_to_lambda(expression)
|
|
704
|
+
else:
|
|
705
|
+
sql = self._generate_create_table(expression)
|
|
706
|
+
|
|
707
|
+
# can be uncommented to support comparisons of optional types with non-optional
|
|
708
|
+
# wrap_lambda = '$wrap_non_optional_in_comparisons = ($column) -> {RETURN IF(FormatType(TypeOf($column)) LIKE "Optional<%", $column, Just($column))};\n\n'
|
|
709
|
+
# return "\n".join(pragma_statements) + "\n" + wrap_lambda + sql
|
|
710
|
+
return sql
|
|
711
|
+
|
|
712
|
+
def unnest_subqueries(self, expression):
|
|
713
|
+
"""
|
|
714
|
+
Rewrite sqlglot AST to convert some predicates with subqueries into joins.
|
|
715
|
+
|
|
716
|
+
Convert scalar subqueries into cross joins.
|
|
717
|
+
Convert correlated or vectorized subqueries into a group by so it is not a many to many left join.
|
|
718
|
+
|
|
719
|
+
Example:
|
|
720
|
+
>>> import sqlglot
|
|
721
|
+
>>> expression = sqlglot.parse_one("SELECT * FROM x AS x WHERE (SELECT y.a AS a FROM y AS y WHERE x.a = y.a) = 1 ")
|
|
722
|
+
>>> unnest_subqueries(expression).sql()
|
|
723
|
+
'SELECT * FROM x AS x LEFT JOIN (SELECT y.a AS a FROM y AS y WHERE TRUE GROUP BY y.a) AS _u_0 ON x.a = _u_0.a WHERE _u_0.a = 1'
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
expression (sqlglot.Expression): expression to unnest
|
|
727
|
+
Returns:
|
|
728
|
+
sqlglot.Expression: unnested expression
|
|
729
|
+
"""
|
|
730
|
+
next_alias_name = name_sequence("_u_")
|
|
731
|
+
|
|
732
|
+
for scope in traverse_scope(expression):
|
|
733
|
+
select = scope.expression
|
|
734
|
+
parent = select.parent_select
|
|
735
|
+
if not parent:
|
|
736
|
+
continue
|
|
737
|
+
if scope.external_columns:
|
|
738
|
+
self.decorrelate(select, parent, scope.external_columns, next_alias_name)
|
|
739
|
+
if scope.scope_type == ScopeType.SUBQUERY:
|
|
740
|
+
self.unnest(select, parent, next_alias_name)
|
|
741
|
+
|
|
742
|
+
return expression
|
|
743
|
+
|
|
744
|
+
@staticmethod
|
|
745
|
+
def remove_star_when_other_columns(expression: exp.Expression) -> exp.Expression:
|
|
746
|
+
"""
|
|
747
|
+
Remove * from SELECT list when there are other columns present.
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
expression: The SQL expression to modify
|
|
751
|
+
|
|
752
|
+
Returns:
|
|
753
|
+
Modified expression without redundant *
|
|
754
|
+
"""
|
|
755
|
+
for select_expr in expression.find_all(exp.Select):
|
|
756
|
+
expressions = select_expr.expressions
|
|
757
|
+
|
|
758
|
+
# Check if there's a * and at least one other column
|
|
759
|
+
has_star = any(
|
|
760
|
+
isinstance(expr, exp.Star)
|
|
761
|
+
or (isinstance(expr, exp.Column) and isinstance(expr.this, exp.Star))
|
|
762
|
+
for expr in expressions
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
has_other_columns = any(
|
|
766
|
+
not (
|
|
767
|
+
isinstance(expr, exp.Star)
|
|
768
|
+
or (isinstance(expr, exp.Column) and isinstance(expr.this, exp.Star))
|
|
769
|
+
)
|
|
770
|
+
for expr in expressions
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
if has_star and has_other_columns:
|
|
774
|
+
# Remove all * expressions
|
|
775
|
+
new_expressions = [
|
|
776
|
+
expr
|
|
777
|
+
for expr in expressions
|
|
778
|
+
if not (
|
|
779
|
+
isinstance(expr, exp.Star)
|
|
780
|
+
or (isinstance(expr, exp.Column) and isinstance(expr.this, exp.Star))
|
|
781
|
+
)
|
|
782
|
+
]
|
|
783
|
+
select_expr.set("expressions", new_expressions)
|
|
784
|
+
|
|
785
|
+
return expression
|
|
786
|
+
|
|
787
|
+
def unnest(self, select, parent_select, next_alias_name):
|
|
788
|
+
"""
|
|
789
|
+
Unnests a subquery by transforming it into a join
|
|
790
|
+
"""
|
|
791
|
+
if len(select.selects) > 1:
|
|
792
|
+
return
|
|
793
|
+
self.ensure_select_aliases(select)
|
|
794
|
+
|
|
795
|
+
predicate = select.find_ancestor(exp.Condition)
|
|
796
|
+
if (
|
|
797
|
+
not predicate
|
|
798
|
+
or parent_select is not predicate.parent_select
|
|
799
|
+
or not parent_select.args.get("from_")
|
|
800
|
+
):
|
|
801
|
+
return
|
|
802
|
+
|
|
803
|
+
if any(
|
|
804
|
+
isinstance(expr, exp.Star)
|
|
805
|
+
or (isinstance(expr, exp.Column) and isinstance(expr.this, exp.Star))
|
|
806
|
+
for expr in select.selects
|
|
807
|
+
):
|
|
808
|
+
return
|
|
809
|
+
|
|
810
|
+
if isinstance(select, exp.SetOperation):
|
|
811
|
+
select = exp.select(*select.selects).from_(select.subquery(next_alias_name()))
|
|
812
|
+
|
|
813
|
+
alias = next_alias_name()
|
|
814
|
+
clause = predicate.find_ancestor(exp.Having, exp.Where, exp.Join)
|
|
815
|
+
|
|
816
|
+
# This subquery returns a scalar and can just be converted to a cross join
|
|
817
|
+
if not isinstance(predicate, (exp.In, exp.Any)):
|
|
818
|
+
first_select = select.selects[0]
|
|
819
|
+
column_alias = first_select.alias_or_name
|
|
820
|
+
|
|
821
|
+
if (
|
|
822
|
+
not column_alias
|
|
823
|
+
or column_alias == ""
|
|
824
|
+
or (column_alias == "*" and isinstance(first_select, exp.AggFunc))
|
|
825
|
+
):
|
|
826
|
+
if isinstance(first_select, exp.Alias):
|
|
827
|
+
expr = first_select.this
|
|
828
|
+
else:
|
|
829
|
+
expr = first_select
|
|
830
|
+
|
|
831
|
+
# Generate a meaningful alias based on the expression type
|
|
832
|
+
if isinstance(expr, exp.AggFunc):
|
|
833
|
+
func_name = expr.sql_name().lower() if hasattr(expr, "sql_name") else "agg"
|
|
834
|
+
column_alias = f"_{func_name}"
|
|
835
|
+
else:
|
|
836
|
+
column_alias = "_col"
|
|
837
|
+
|
|
838
|
+
# Add alias to the select if it doesn't have one
|
|
839
|
+
if not isinstance(first_select, exp.Alias):
|
|
840
|
+
new_selects = [exp.alias_(first_select.copy(), column_alias)]
|
|
841
|
+
if len(select.selects) > 1:
|
|
842
|
+
new_selects.extend(select.selects[1:])
|
|
843
|
+
select.set("expressions", new_selects)
|
|
844
|
+
# Update first_select to point to the newly aliased expression
|
|
845
|
+
first_select = select.selects[0]
|
|
846
|
+
elif not first_select.alias or first_select.alias_or_name == "*":
|
|
847
|
+
first_select.set("alias", exp.to_identifier(column_alias))
|
|
848
|
+
|
|
849
|
+
# Re-read the alias after setting it to ensure we have the correct value
|
|
850
|
+
column_alias = first_select.alias_or_name
|
|
851
|
+
|
|
852
|
+
column = exp.column(column_alias, alias)
|
|
853
|
+
|
|
854
|
+
clause_parent_select = clause.parent_select if clause else None
|
|
855
|
+
|
|
856
|
+
if (isinstance(clause, exp.Having) and clause_parent_select is parent_select) or (
|
|
857
|
+
(not clause or clause_parent_select is not parent_select)
|
|
858
|
+
and (
|
|
859
|
+
parent_select.args.get("group")
|
|
860
|
+
or any(
|
|
861
|
+
find_in_scope(select, exp.AggFunc) for select in parent_select.selects
|
|
862
|
+
)
|
|
863
|
+
)
|
|
864
|
+
):
|
|
865
|
+
column = exp.Max(this=column)
|
|
866
|
+
elif not isinstance(select.parent, exp.Subquery) and not isinstance(
|
|
867
|
+
select.parent, exp.Exists
|
|
868
|
+
):
|
|
869
|
+
return
|
|
870
|
+
|
|
871
|
+
_replace(select.parent, column)
|
|
872
|
+
parent_select.join(select, join_type="CROSS", join_alias=alias, copy=False)
|
|
873
|
+
return
|
|
874
|
+
|
|
875
|
+
if select.find(exp.Limit, exp.Offset):
|
|
876
|
+
return
|
|
877
|
+
|
|
878
|
+
if isinstance(predicate, exp.Any):
|
|
879
|
+
predicate = predicate.find_ancestor(exp.EQ)
|
|
880
|
+
|
|
881
|
+
if not predicate or parent_select is not predicate.parent_select:
|
|
882
|
+
return
|
|
883
|
+
|
|
884
|
+
column = _other_operand(predicate)
|
|
885
|
+
self.ensure_select_aliases(select)
|
|
886
|
+
value = select.selects[0]
|
|
887
|
+
join_key = exp.column(value.alias, alias)
|
|
888
|
+
join_key_not_null = join_key.is_(exp.null()).not_()
|
|
889
|
+
|
|
890
|
+
if isinstance(clause, exp.Join):
|
|
891
|
+
_replace(predicate, exp.true())
|
|
892
|
+
parent_select.where(join_key_not_null, copy=False)
|
|
893
|
+
else:
|
|
894
|
+
_replace(predicate, join_key_not_null)
|
|
895
|
+
|
|
896
|
+
group = select.args.get("group")
|
|
897
|
+
|
|
898
|
+
if group:
|
|
899
|
+
# Remove table qualifiers from GROUP BY expressions
|
|
900
|
+
group_expressions = []
|
|
901
|
+
for expr in group.expressions:
|
|
902
|
+
if isinstance(expr, exp.Column) and expr.table:
|
|
903
|
+
# Remove table qualifier
|
|
904
|
+
unqualified_expr = exp.Column(this=expr.this)
|
|
905
|
+
group_expressions.append(unqualified_expr)
|
|
906
|
+
else:
|
|
907
|
+
group_expressions.append(expr)
|
|
908
|
+
|
|
909
|
+
# Check if value.this (without qualifier) matches any group expression
|
|
910
|
+
value_this_unqualified = value.this
|
|
911
|
+
if isinstance(value_this_unqualified, exp.Column) and value_this_unqualified.table:
|
|
912
|
+
value_this_unqualified = exp.Column(this=value_this_unqualified.this)
|
|
913
|
+
|
|
914
|
+
if {value_this_unqualified} != set(group_expressions):
|
|
915
|
+
select = (
|
|
916
|
+
exp.select(exp.alias_(exp.column(value.alias, "_q"), value.alias))
|
|
917
|
+
.from_(select.subquery("_q", copy=False), copy=False)
|
|
918
|
+
.group_by(exp.column(value.alias, "_q"), copy=False)
|
|
919
|
+
)
|
|
920
|
+
else:
|
|
921
|
+
# Update group with unqualified expressions
|
|
922
|
+
new_group = exp.Group(expressions=group_expressions)
|
|
923
|
+
select.set("group", new_group)
|
|
924
|
+
elif not find_in_scope(value.this, exp.AggFunc):
|
|
925
|
+
# Remove table qualifier from value.this if it's a column for GROUP BY
|
|
926
|
+
group_by_expr = value.this
|
|
927
|
+
if isinstance(group_by_expr, exp.Column) and group_by_expr.table:
|
|
928
|
+
group_by_expr = exp.Column(this=group_by_expr.this)
|
|
929
|
+
select = select.group_by(group_by_expr, copy=False)
|
|
930
|
+
|
|
931
|
+
parent_select.join(
|
|
932
|
+
select,
|
|
933
|
+
on=column.eq(join_key),
|
|
934
|
+
join_type="LEFT",
|
|
935
|
+
join_alias=alias,
|
|
936
|
+
copy=False,
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
@staticmethod
|
|
940
|
+
def ensure_select_aliases(select, default_prefix="_col"):
|
|
941
|
+
"""
|
|
942
|
+
Ensure all select expressions have a non-empty, unique alias.
|
|
943
|
+
Use the original column name as alias if possible.
|
|
944
|
+
"""
|
|
945
|
+
for i, expr in enumerate(select.selects):
|
|
946
|
+
if isinstance(expr, exp.Alias):
|
|
947
|
+
alias_name = expr.alias_or_name
|
|
948
|
+
if not alias_name or alias_name == "*":
|
|
949
|
+
base_name = (
|
|
950
|
+
expr.this.alias_or_name
|
|
951
|
+
if hasattr(expr.this, "alias_or_name")
|
|
952
|
+
else f"{default_prefix}{i}"
|
|
953
|
+
)
|
|
954
|
+
expr.set("alias", exp.to_identifier(base_name))
|
|
955
|
+
elif isinstance(expr, exp.Column):
|
|
956
|
+
base_name = expr.alias_or_name or f"{default_prefix}{i}"
|
|
957
|
+
select.selects[i] = exp.alias_(expr, base_name)
|
|
958
|
+
else:
|
|
959
|
+
select.selects[i] = exp.alias_(expr, f"{default_prefix}{i}")
|
|
960
|
+
|
|
961
|
+
def decorrelate(self, select, parent_select, external_columns, next_alias_name):
|
|
962
|
+
"""
|
|
963
|
+
Decorrelates a subquery by transforming it into a join
|
|
964
|
+
"""
|
|
965
|
+
where = select.args.get("where")
|
|
966
|
+
if not where or where.find(exp.Or) or select.find(exp.Limit, exp.Offset):
|
|
967
|
+
return
|
|
968
|
+
|
|
969
|
+
table_alias = next_alias_name()
|
|
970
|
+
keys = []
|
|
971
|
+
|
|
972
|
+
# for all external columns in the where statement, find the relevant predicate
|
|
973
|
+
# keys to convert it into a join
|
|
974
|
+
for column in external_columns:
|
|
975
|
+
predicate = column.find_ancestor(exp.Predicate)
|
|
976
|
+
|
|
977
|
+
if isinstance(predicate, exp.Binary):
|
|
978
|
+
key = (
|
|
979
|
+
predicate.right
|
|
980
|
+
if any(node is column for node in predicate.left.walk())
|
|
981
|
+
else predicate.left
|
|
982
|
+
)
|
|
983
|
+
elif isinstance(predicate, exp.Between):
|
|
984
|
+
key = predicate.this
|
|
985
|
+
else:
|
|
986
|
+
return
|
|
987
|
+
|
|
988
|
+
keys.append((key, column, predicate))
|
|
989
|
+
|
|
990
|
+
is_subquery_projection = any(
|
|
991
|
+
node is select.parent
|
|
992
|
+
for node in map(lambda s: s.unalias(), parent_select.selects)
|
|
993
|
+
if isinstance(node, exp.Subquery)
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
value = select.selects[0]
|
|
997
|
+
key_aliases = {}
|
|
998
|
+
group_by = []
|
|
999
|
+
|
|
1000
|
+
external_tables = [
|
|
1001
|
+
col.table
|
|
1002
|
+
for col in external_columns
|
|
1003
|
+
if isinstance(col, exp.Column) and hasattr(col, "table") and col.table
|
|
1004
|
+
]
|
|
1005
|
+
|
|
1006
|
+
external_column_set = set()
|
|
1007
|
+
for col in external_columns:
|
|
1008
|
+
if isinstance(col, exp.Column):
|
|
1009
|
+
if col.table:
|
|
1010
|
+
external_column_set.add(
|
|
1011
|
+
(
|
|
1012
|
+
col.table,
|
|
1013
|
+
col.this.name if hasattr(col.this, "name") else col.alias_or_name,
|
|
1014
|
+
)
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
def is_external_column(col):
|
|
1018
|
+
if not isinstance(col, exp.Column):
|
|
1019
|
+
return False
|
|
1020
|
+
col_table = col.table if col.table else None
|
|
1021
|
+
col_name = col.this.name if hasattr(col.this, "name") else col.alias_or_name
|
|
1022
|
+
return (col_table, col_name) in external_column_set or (
|
|
1023
|
+
None,
|
|
1024
|
+
col_name,
|
|
1025
|
+
) in external_column_set
|
|
1026
|
+
|
|
1027
|
+
keys = [
|
|
1028
|
+
(key, column, predicate)
|
|
1029
|
+
for key, column, predicate in keys
|
|
1030
|
+
if isinstance(key, exp.Column)
|
|
1031
|
+
and (
|
|
1032
|
+
not key.table # No table qualifier = from subquery
|
|
1033
|
+
or (
|
|
1034
|
+
key.table and key.table not in external_tables
|
|
1035
|
+
) # Has qualifier but not external
|
|
1036
|
+
)
|
|
1037
|
+
and is_external_column(column)
|
|
1038
|
+
] # Verify column is actually external
|
|
1039
|
+
|
|
1040
|
+
parent_predicate = select.find_ancestor(exp.Predicate)
|
|
1041
|
+
is_exists = isinstance(parent_predicate, exp.Exists)
|
|
1042
|
+
|
|
1043
|
+
if is_exists and not keys:
|
|
1044
|
+
return
|
|
1045
|
+
|
|
1046
|
+
if is_exists:
|
|
1047
|
+
select.set("expressions", [])
|
|
1048
|
+
|
|
1049
|
+
for key, _, predicate in keys:
|
|
1050
|
+
if is_exists:
|
|
1051
|
+
if key not in key_aliases:
|
|
1052
|
+
alias_name = next_alias_name()
|
|
1053
|
+
key_aliases[key] = alias_name
|
|
1054
|
+
|
|
1055
|
+
key_copy = key.copy()
|
|
1056
|
+
if isinstance(key_copy, exp.Column) and key_copy.table:
|
|
1057
|
+
key_copy.set("table", None)
|
|
1058
|
+
|
|
1059
|
+
select.select(exp.alias_(key_copy, alias_name, quoted=False), copy=False)
|
|
1060
|
+
|
|
1061
|
+
if isinstance(predicate, exp.EQ) and key not in group_by:
|
|
1062
|
+
group_by.append(key)
|
|
1063
|
+
else:
|
|
1064
|
+
if value and key == value.this:
|
|
1065
|
+
alias = value.alias if value.alias != "" else next_alias_name()
|
|
1066
|
+
key_aliases[key] = alias
|
|
1067
|
+
group_by.append(key)
|
|
1068
|
+
else:
|
|
1069
|
+
key_aliases[key] = next_alias_name()
|
|
1070
|
+
if isinstance(predicate, exp.EQ) and key not in group_by:
|
|
1071
|
+
group_by.append(key)
|
|
1072
|
+
|
|
1073
|
+
if is_exists:
|
|
1074
|
+
value_alias = "_exists_flag"
|
|
1075
|
+
select.select(
|
|
1076
|
+
exp.alias_(exp.Literal.number(1), value_alias, quoted=False), copy=False
|
|
1077
|
+
)
|
|
1078
|
+
alias = exp.column(value_alias, table_alias)
|
|
1079
|
+
elif value:
|
|
1080
|
+
agg_func = exp.Max if is_subquery_projection else exp.ArrayAgg
|
|
1081
|
+
|
|
1082
|
+
# exists queries should not have any selects as it only checks if there are any rows
|
|
1083
|
+
# all selects will be added by the optimizer and only used for join keys
|
|
1084
|
+
for key, alias_val in key_aliases.items():
|
|
1085
|
+
if key in group_by:
|
|
1086
|
+
# add all keys to the projections of the subquery
|
|
1087
|
+
# so that we can use it as a join keyjoin_sql
|
|
1088
|
+
select.select(exp.alias_(key.copy(), alias_val, quoted=False), copy=False)
|
|
1089
|
+
else:
|
|
1090
|
+
select.select(
|
|
1091
|
+
exp.alias_(agg_func(this=key.copy()), alias_val, quoted=False),
|
|
1092
|
+
copy=False,
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
if not value.alias_or_name or value.alias_or_name == "*":
|
|
1096
|
+
# Generate a meaningful alias based on the expression type
|
|
1097
|
+
if isinstance(value.this, exp.Count):
|
|
1098
|
+
value_alias = "_count"
|
|
1099
|
+
elif isinstance(value.this, exp.AggFunc):
|
|
1100
|
+
func_name = (
|
|
1101
|
+
value.this.sql_name().lower()
|
|
1102
|
+
if hasattr(value.this, "sql_name")
|
|
1103
|
+
else "agg"
|
|
1104
|
+
)
|
|
1105
|
+
value_alias = f"_{func_name}"
|
|
1106
|
+
else:
|
|
1107
|
+
value_alias = next_alias_name()
|
|
1108
|
+
|
|
1109
|
+
if isinstance(value, exp.Alias):
|
|
1110
|
+
value.set("alias", value_alias)
|
|
1111
|
+
else:
|
|
1112
|
+
value = exp.alias_(value, value_alias)
|
|
1113
|
+
select.selects[0] = value
|
|
1114
|
+
else:
|
|
1115
|
+
value_alias = value.alias_or_name
|
|
1116
|
+
alias = exp.column(value_alias, table_alias)
|
|
1117
|
+
else:
|
|
1118
|
+
return
|
|
1119
|
+
|
|
1120
|
+
self.remove_star_when_other_columns(select)
|
|
1121
|
+
other = _other_operand(parent_predicate)
|
|
1122
|
+
op_type = type(parent_predicate.parent) if parent_predicate else None
|
|
1123
|
+
|
|
1124
|
+
if is_exists:
|
|
1125
|
+
if key_aliases:
|
|
1126
|
+
first_key_alias = list(key_aliases.values())[0]
|
|
1127
|
+
alias = exp.column(first_key_alias, table_alias)
|
|
1128
|
+
parent_predicate.replace(exp.condition(f"NOT {self.sql(alias)} IS NULL"))
|
|
1129
|
+
else:
|
|
1130
|
+
if select.selects:
|
|
1131
|
+
first_select = select.selects[0]
|
|
1132
|
+
alias_name = first_select.alias_or_name or "_exists"
|
|
1133
|
+
alias = exp.column(alias_name, table_alias)
|
|
1134
|
+
parent_predicate.replace(exp.condition(f"NOT {self.sql(alias)} IS NULL"))
|
|
1135
|
+
elif isinstance(parent_predicate, exp.All):
|
|
1136
|
+
if not issubclass(op_type, exp.Binary):
|
|
1137
|
+
raise ValueError("op_type must be a subclass of Binary")
|
|
1138
|
+
assert issubclass(op_type, exp.Binary)
|
|
1139
|
+
predicate = op_type(this=other, expression=exp.column("_x"))
|
|
1140
|
+
_replace(parent_predicate.parent, f"ARRAY_ALL({alias}, _x -> {predicate})")
|
|
1141
|
+
elif isinstance(parent_predicate, exp.Any):
|
|
1142
|
+
if not issubclass(op_type, exp.Binary):
|
|
1143
|
+
raise ValueError("op_type must be a subclass of Binary")
|
|
1144
|
+
if value.this in group_by:
|
|
1145
|
+
predicate = op_type(this=other, expression=alias)
|
|
1146
|
+
_replace(parent_predicate.parent, predicate)
|
|
1147
|
+
else:
|
|
1148
|
+
predicate = op_type(this=other, expression=exp.column("_x"))
|
|
1149
|
+
_replace(parent_predicate, f"ARRAY_ANY({alias}, _x -> {predicate})")
|
|
1150
|
+
elif isinstance(parent_predicate, exp.In):
|
|
1151
|
+
if value.this in group_by:
|
|
1152
|
+
_replace(parent_predicate, f"{other} = {alias}")
|
|
1153
|
+
else:
|
|
1154
|
+
_replace(
|
|
1155
|
+
parent_predicate,
|
|
1156
|
+
f"ARRAY_ANY({alias}, _x -> _x = {parent_predicate.this})",
|
|
1157
|
+
)
|
|
1158
|
+
else:
|
|
1159
|
+
if is_subquery_projection and select.parent.alias:
|
|
1160
|
+
alias = exp.alias_(alias, select.parent.alias)
|
|
1161
|
+
|
|
1162
|
+
# COUNT always returns 0 on empty datasets, so we need take that into consideration here
|
|
1163
|
+
# by transforming all counts into 0 and using that as the coalesced value
|
|
1164
|
+
# However, don't add COALESCE if value.this is a Star (from COUNT(*)) -
|
|
1165
|
+
# scalar subqueries are handled by unnest which creates proper aliases
|
|
1166
|
+
if value.find(exp.Count) and not isinstance(value.this, exp.Star):
|
|
1167
|
+
|
|
1168
|
+
def remove_aggs(node):
|
|
1169
|
+
if isinstance(node, exp.Count):
|
|
1170
|
+
return exp.Literal.number(0)
|
|
1171
|
+
elif isinstance(node, exp.AggFunc):
|
|
1172
|
+
return exp.null()
|
|
1173
|
+
return node
|
|
1174
|
+
|
|
1175
|
+
transformed = value.this.transform(remove_aggs)
|
|
1176
|
+
# Only add COALESCE if the transformed expression is not a Star
|
|
1177
|
+
if not isinstance(transformed, exp.Star):
|
|
1178
|
+
alias = exp.Coalesce(this=alias, expressions=[transformed])
|
|
1179
|
+
|
|
1180
|
+
select.parent.replace(alias)
|
|
1181
|
+
|
|
1182
|
+
on_predicates = []
|
|
1183
|
+
|
|
1184
|
+
for key, column, predicate in keys:
|
|
1185
|
+
if isinstance(predicate, exp.EQ):
|
|
1186
|
+
predicate.replace(exp.true())
|
|
1187
|
+
|
|
1188
|
+
# Create the ON condition: external_column = subquery_alias.column_alias
|
|
1189
|
+
if key in key_aliases:
|
|
1190
|
+
# Use the alias we created for the key in the SELECT list
|
|
1191
|
+
nested_col = exp.column(key_aliases[key], table_alias)
|
|
1192
|
+
|
|
1193
|
+
external_col_copy = column.copy()
|
|
1194
|
+
|
|
1195
|
+
on_predicates.append(exp.EQ(this=external_col_copy, expression=nested_col))
|
|
1196
|
+
else:
|
|
1197
|
+
if key in key_aliases:
|
|
1198
|
+
nested_col = exp.column(key_aliases[key], table_alias)
|
|
1199
|
+
|
|
1200
|
+
key.replace(nested_col)
|
|
1201
|
+
|
|
1202
|
+
if group_by:
|
|
1203
|
+
new_group_by = []
|
|
1204
|
+
for gb_expr in group_by:
|
|
1205
|
+
if isinstance(gb_expr, exp.Column) and gb_expr.table:
|
|
1206
|
+
unqualified_expr = exp.Column(this=gb_expr.this)
|
|
1207
|
+
new_group_by.append(unqualified_expr)
|
|
1208
|
+
else:
|
|
1209
|
+
new_group_by.append(gb_expr)
|
|
1210
|
+
group_by = new_group_by
|
|
1211
|
+
|
|
1212
|
+
if on_predicates:
|
|
1213
|
+
if len(on_predicates) == 1:
|
|
1214
|
+
on_clause = on_predicates[0]
|
|
1215
|
+
else:
|
|
1216
|
+
on_clause = on_predicates[0]
|
|
1217
|
+
for pred in on_predicates[1:]:
|
|
1218
|
+
on_clause = exp.and_(on_clause, pred)
|
|
1219
|
+
|
|
1220
|
+
parent_select.join(
|
|
1221
|
+
select.group_by(*group_by, copy=False) if group_by else select,
|
|
1222
|
+
on=on_clause,
|
|
1223
|
+
join_type="LEFT",
|
|
1224
|
+
join_alias=table_alias,
|
|
1225
|
+
copy=False,
|
|
1226
|
+
)
|
|
1227
|
+
else:
|
|
1228
|
+
parent_select.join(
|
|
1229
|
+
select.group_by(*group_by, copy=False) if group_by else select,
|
|
1230
|
+
join_type="CROSS",
|
|
1231
|
+
join_alias=table_alias,
|
|
1232
|
+
copy=False,
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
STRING_TYPE_MAPPING = {
|
|
1236
|
+
exp.DataType.Type.BLOB: "String",
|
|
1237
|
+
exp.DataType.Type.CHAR: "String",
|
|
1238
|
+
exp.DataType.Type.LONGBLOB: "String",
|
|
1239
|
+
exp.DataType.Type.LONGTEXT: "String",
|
|
1240
|
+
exp.DataType.Type.MEDIUMBLOB: "String",
|
|
1241
|
+
exp.DataType.Type.MEDIUMTEXT: "String",
|
|
1242
|
+
exp.DataType.Type.TINYBLOB: "String",
|
|
1243
|
+
exp.DataType.Type.TINYTEXT: "String",
|
|
1244
|
+
exp.DataType.Type.TEXT: "Utf8",
|
|
1245
|
+
exp.DataType.Type.VARBINARY: "String",
|
|
1246
|
+
exp.DataType.Type.VARCHAR: "Utf8",
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
def _date_trunc_sql(self, expression: exp.DateTrunc) -> str:
|
|
1250
|
+
"""
|
|
1251
|
+
Generate SQL for DATE_TRUNC function with YDB-specific implementation.
|
|
1252
|
+
|
|
1253
|
+
Args:
|
|
1254
|
+
expression: The DATE_TRUNC expression
|
|
1255
|
+
|
|
1256
|
+
Returns:
|
|
1257
|
+
YDB-specific SQL for truncating dates
|
|
1258
|
+
"""
|
|
1259
|
+
expr = self.sql(expression, "this")
|
|
1260
|
+
unit = expression.text("unit").upper()
|
|
1261
|
+
|
|
1262
|
+
if unit == "WEEK":
|
|
1263
|
+
return f"DateTime::MakeDate(DateTime::StartOfWeek({expr}))"
|
|
1264
|
+
elif unit == "MONTH":
|
|
1265
|
+
return f"DateTime::MakeDate(DateTime::StartOfMonth({expr}))"
|
|
1266
|
+
elif unit == "QUARTER":
|
|
1267
|
+
return f"DateTime::MakeDate(DateTime::StartOfQuarter({expr}))"
|
|
1268
|
+
elif unit == "YEAR":
|
|
1269
|
+
return f"DateTime::MakeDate(DateTime::StartOfYear({expr}))"
|
|
1270
|
+
else:
|
|
1271
|
+
if unit != "DAY":
|
|
1272
|
+
self.unsupported(f"Unexpected interval unit: {unit}")
|
|
1273
|
+
return self.func("DATE", expr)
|
|
1274
|
+
|
|
1275
|
+
def _current_timestamp_sql(self, expression: exp.CurrentTimestamp) -> str:
|
|
1276
|
+
"""
|
|
1277
|
+
Generate SQL for CURRENT_TIMESTAMP function with YDB-specific implementation.
|
|
1278
|
+
|
|
1279
|
+
Args:
|
|
1280
|
+
expression: The CURRENT_TIMESTAMP expression
|
|
1281
|
+
|
|
1282
|
+
Returns:
|
|
1283
|
+
YDB-specific SQL for current timestamp
|
|
1284
|
+
"""
|
|
1285
|
+
return 'AddTimezone(CurrentUtcTimestamp(), "Europe/Moscow")'
|
|
1286
|
+
|
|
1287
|
+
def _str_to_date(self, expression: exp.StrToDate) -> str:
|
|
1288
|
+
"""
|
|
1289
|
+
Generate SQL for STR_TO_DATE function with YDB-specific implementation.
|
|
1290
|
+
|
|
1291
|
+
Args:
|
|
1292
|
+
expression: The STR_TO_DATE expression
|
|
1293
|
+
|
|
1294
|
+
Returns:
|
|
1295
|
+
YDB-specific SQL for converting strings to dates
|
|
1296
|
+
"""
|
|
1297
|
+
str_value = expression.this.name
|
|
1298
|
+
# formatted_time = self.format_time(expression, self.dialect.INVERSE_FORMAT_MAPPING,
|
|
1299
|
+
# self.dialect.INVERSE_FORMAT_TRIE)
|
|
1300
|
+
formatted_time = self.format_time(expression)
|
|
1301
|
+
return f'DateTime::MakeTimestamp(DateTime::Parse({formatted_time})("{str_value}"))'
|
|
1302
|
+
|
|
1303
|
+
def _extract(self, expression: exp.Extract) -> str:
|
|
1304
|
+
"""
|
|
1305
|
+
Generate SQL for EXTRACT function with YDB-specific implementation.
|
|
1306
|
+
|
|
1307
|
+
Args:
|
|
1308
|
+
expression: The EXTRACT expression
|
|
1309
|
+
|
|
1310
|
+
Returns:
|
|
1311
|
+
YDB-specific SQL for extracting date parts
|
|
1312
|
+
"""
|
|
1313
|
+
unit = expression.name.upper()
|
|
1314
|
+
expr = self.sql(expression.expression)
|
|
1315
|
+
|
|
1316
|
+
if unit == "WEEK":
|
|
1317
|
+
return f"DateTime::GetWeekOfYear({expr})"
|
|
1318
|
+
elif unit == "MONTH":
|
|
1319
|
+
return f"DateTime::GetMonth({expr})"
|
|
1320
|
+
elif unit == "YEAR":
|
|
1321
|
+
return f"DateTime::GetYear({expr})"
|
|
1322
|
+
else:
|
|
1323
|
+
if unit != "DAY":
|
|
1324
|
+
self.unsupported(f"Unexpected interval unit: {unit}")
|
|
1325
|
+
return self.func("DATE", expr)
|
|
1326
|
+
|
|
1327
|
+
def _lambda(self, expression: exp.Lambda, arrow_sep: str = "->") -> str:
|
|
1328
|
+
"""
|
|
1329
|
+
Generate SQL for Lambda expressions with YDB-specific syntax.
|
|
1330
|
+
|
|
1331
|
+
Args:
|
|
1332
|
+
expression: The Lambda expression
|
|
1333
|
+
arrow_sep: The separator to use between parameters and body
|
|
1334
|
+
|
|
1335
|
+
Returns:
|
|
1336
|
+
YDB-specific SQL for lambda functions
|
|
1337
|
+
"""
|
|
1338
|
+
for ident in expression.find_all(exp.Identifier):
|
|
1339
|
+
new_ident = exp.to_identifier("$" + ident.alias_or_name)
|
|
1340
|
+
new_ident.set("quoted", False)
|
|
1341
|
+
ident.replace(new_ident)
|
|
1342
|
+
|
|
1343
|
+
args = self.expressions(expression, flat=True)
|
|
1344
|
+
args = f"({args})" if len(args.split(",")) > 1 else args
|
|
1345
|
+
return f"({args}) {arrow_sep} {{RETURN {self.sql(expression, 'this')}}}"
|
|
1346
|
+
|
|
1347
|
+
def _is_simple_expression(self, expr: exp.Expression) -> bool:
|
|
1348
|
+
"""
|
|
1349
|
+
Check if an expression is simple enough to be used directly in CASE/IF.
|
|
1350
|
+
Simple expressions are literals, columns, identifiers, and basic operations.
|
|
1351
|
+
|
|
1352
|
+
Args:
|
|
1353
|
+
expr: The expression to check
|
|
1354
|
+
|
|
1355
|
+
Returns:
|
|
1356
|
+
True if the expression is simple, False otherwise
|
|
1357
|
+
"""
|
|
1358
|
+
if isinstance(expr, (exp.Literal, exp.Null)):
|
|
1359
|
+
return True
|
|
1360
|
+
|
|
1361
|
+
if isinstance(expr, exp.Column):
|
|
1362
|
+
col_name = (
|
|
1363
|
+
expr.this.name
|
|
1364
|
+
if hasattr(expr.this, "name")
|
|
1365
|
+
else (expr.alias_or_name if hasattr(expr, "alias_or_name") else None)
|
|
1366
|
+
)
|
|
1367
|
+
if not col_name or col_name == "*" or col_name == "":
|
|
1368
|
+
return False
|
|
1369
|
+
return True
|
|
1370
|
+
|
|
1371
|
+
if isinstance(expr, (exp.Star, exp.Identifier)):
|
|
1372
|
+
return True
|
|
1373
|
+
|
|
1374
|
+
if isinstance(expr, exp.Binary):
|
|
1375
|
+
return self._is_simple_expression(expr.this) and self._is_simple_expression(
|
|
1376
|
+
expr.expression
|
|
1377
|
+
)
|
|
1378
|
+
if isinstance(expr, exp.Paren):
|
|
1379
|
+
return self._is_simple_expression(expr.this)
|
|
1380
|
+
if isinstance(expr, (exp.Subquery, exp.Case, exp.If, exp.Func, exp.AggFunc)):
|
|
1381
|
+
return False
|
|
1382
|
+
return not any(
|
|
1383
|
+
isinstance(node, (exp.Subquery, exp.Case, exp.If, exp.Func, exp.AggFunc))
|
|
1384
|
+
for node in expr.walk()
|
|
1385
|
+
if node is not expr
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
def _references_unnest_alias(self, expr: exp.Expression) -> bool:
|
|
1389
|
+
"""
|
|
1390
|
+
Check if an expression references table aliases from unnesting (like _u_0, _u_1).
|
|
1391
|
+
These aliases are only available in the main query, not in standalone SELECT statements.
|
|
1392
|
+
|
|
1393
|
+
Args:
|
|
1394
|
+
expr: The expression to check
|
|
1395
|
+
|
|
1396
|
+
Returns:
|
|
1397
|
+
True if the expression references an unnest alias, False otherwise
|
|
1398
|
+
"""
|
|
1399
|
+
for node in expr.walk():
|
|
1400
|
+
if isinstance(node, exp.Column) and hasattr(node, "table") and node.table:
|
|
1401
|
+
table_name = (
|
|
1402
|
+
node.table
|
|
1403
|
+
if isinstance(node.table, str)
|
|
1404
|
+
else (node.table.name if hasattr(node.table, "name") else str(node.table))
|
|
1405
|
+
)
|
|
1406
|
+
if table_name and table_name.startswith("_u_"):
|
|
1407
|
+
return True
|
|
1408
|
+
return False
|
|
1409
|
+
|
|
1410
|
+
def _if(self, expression: exp.If) -> str:
|
|
1411
|
+
# Extract complex expressions to variables
|
|
1412
|
+
condition = expression.this
|
|
1413
|
+
true_expr = expression.args.get("true")
|
|
1414
|
+
false_expr = expression.args.get("false")
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
condition = condition.copy()
|
|
1418
|
+
true_expr = true_expr.copy()
|
|
1419
|
+
false_expr = false_expr.copy()
|
|
1420
|
+
|
|
1421
|
+
this = self.sql(condition)
|
|
1422
|
+
true = self.sql(true_expr) if true_expr else ""
|
|
1423
|
+
false = self.sql(false_expr) if false_expr else ""
|
|
1424
|
+
return f"IF({this}, {true}, {false})"
|
|
1425
|
+
|
|
1426
|
+
def _null_if(self, expression: exp.Nullif) -> str:
|
|
1427
|
+
lhs = expression.this
|
|
1428
|
+
rhs = expression.expression
|
|
1429
|
+
|
|
1430
|
+
cond = exp.EQ(this=lhs, expression=rhs)
|
|
1431
|
+
return self.sql(exp.If(this=cond, true=exp.Null(), false=lhs))
|
|
1432
|
+
|
|
1433
|
+
E = t.TypeVar("E", bound=Expression)
|
|
1434
|
+
|
|
1435
|
+
def _simplify_unless_literal(self, expression: E) -> E:
|
|
1436
|
+
if not isinstance(expression, exp.Literal):
|
|
1437
|
+
expression = simplify(expression, dialect=self.dialect)
|
|
1438
|
+
return expression
|
|
1439
|
+
|
|
1440
|
+
# we move the WHERE expression from ON, using literals
|
|
1441
|
+
def join_sql(self, expression: exp.Join) -> str:
|
|
1442
|
+
on_condition = expression.args.get("on")
|
|
1443
|
+
join_kind = expression.kind or ""
|
|
1444
|
+
|
|
1445
|
+
# If LEFT/RIGHT/FULL JOIN has no ON clause, convert to CROSS JOIN
|
|
1446
|
+
# YDB requires LEFT JOINs to have an ON clause
|
|
1447
|
+
if not on_condition and any(
|
|
1448
|
+
kind in join_kind.upper() for kind in ["LEFT", "RIGHT", "FULL", "OUTER", ""]
|
|
1449
|
+
):
|
|
1450
|
+
expression.set("kind", None)
|
|
1451
|
+
expression.set("on", None)
|
|
1452
|
+
return super().join_sql(expression)
|
|
1453
|
+
|
|
1454
|
+
if on_condition:
|
|
1455
|
+
# Extract all non-equality conditions (including those with literals)
|
|
1456
|
+
# YDB only allows equality predicates in JOIN ON
|
|
1457
|
+
literal_conditions: list[Expression] = []
|
|
1458
|
+
non_equality_conditions: list[Expression] = []
|
|
1459
|
+
equality_conditions: list[Expression] = []
|
|
1460
|
+
|
|
1461
|
+
if isinstance(on_condition, exp.And):
|
|
1462
|
+
conditions = list(on_condition.flatten())
|
|
1463
|
+
else:
|
|
1464
|
+
conditions = [on_condition]
|
|
1465
|
+
|
|
1466
|
+
for cond in conditions:
|
|
1467
|
+
# Check if it's an equality predicate
|
|
1468
|
+
if isinstance(cond, exp.EQ):
|
|
1469
|
+
# Check if it's a true equi-join (columns from different tables)
|
|
1470
|
+
left = cond.this
|
|
1471
|
+
right = cond.expression
|
|
1472
|
+
if (
|
|
1473
|
+
isinstance(left, exp.Column)
|
|
1474
|
+
and isinstance(right, exp.Column)
|
|
1475
|
+
and hasattr(left, "table")
|
|
1476
|
+
and hasattr(right, "table")
|
|
1477
|
+
and left.table
|
|
1478
|
+
and right.table
|
|
1479
|
+
and left.table != right.table
|
|
1480
|
+
):
|
|
1481
|
+
equality_conditions.append(cond)
|
|
1482
|
+
else:
|
|
1483
|
+
if self._contains_literals(cond):
|
|
1484
|
+
literal_conditions.append(cond)
|
|
1485
|
+
else:
|
|
1486
|
+
non_equality_conditions.append(cond)
|
|
1487
|
+
else:
|
|
1488
|
+
if self._contains_literals(cond):
|
|
1489
|
+
literal_conditions.append(cond)
|
|
1490
|
+
else:
|
|
1491
|
+
non_equality_conditions.append(cond)
|
|
1492
|
+
|
|
1493
|
+
conditions_to_move = literal_conditions + non_equality_conditions
|
|
1494
|
+
|
|
1495
|
+
if equality_conditions:
|
|
1496
|
+
if len(equality_conditions) == 1:
|
|
1497
|
+
on_condition = equality_conditions[0]
|
|
1498
|
+
else:
|
|
1499
|
+
on_condition = equality_conditions[0]
|
|
1500
|
+
for cond in equality_conditions[1:]:
|
|
1501
|
+
on_condition = exp.and_(on_condition, cond)
|
|
1502
|
+
expression.set("on", on_condition)
|
|
1503
|
+
else:
|
|
1504
|
+
# No valid equality conditions
|
|
1505
|
+
# For LEFT/RIGHT/FULL JOINs, YDB requires ON clause, so convert to CROSS JOIN
|
|
1506
|
+
join_kind = expression.side or ""
|
|
1507
|
+
if any(
|
|
1508
|
+
kind in join_kind.upper() for kind in ["LEFT", "RIGHT", "FULL", "OUTER"]
|
|
1509
|
+
):
|
|
1510
|
+
# Convert to CROSS JOIN by removing kind and ON
|
|
1511
|
+
expression.set("kind", None)
|
|
1512
|
+
expression.set("on", None)
|
|
1513
|
+
expression.set("side", "CROSS")
|
|
1514
|
+
else:
|
|
1515
|
+
expression.set("on", None)
|
|
1516
|
+
|
|
1517
|
+
if conditions_to_move:
|
|
1518
|
+
select_stmt = expression.find_ancestor(exp.Select)
|
|
1519
|
+
if select_stmt:
|
|
1520
|
+
combined_condition = conditions_to_move[0]
|
|
1521
|
+
for cond in conditions_to_move[1:]:
|
|
1522
|
+
combined_condition = exp.and_(combined_condition, cond)
|
|
1523
|
+
|
|
1524
|
+
existing_where = select_stmt.args.get("where")
|
|
1525
|
+
if existing_where:
|
|
1526
|
+
new_where = exp.and_(existing_where.this, combined_condition)
|
|
1527
|
+
select_stmt.set("where", exp.Where(this=new_where))
|
|
1528
|
+
else:
|
|
1529
|
+
select_stmt.set("where", exp.Where(this=combined_condition))
|
|
1530
|
+
|
|
1531
|
+
join_sql = super().join_sql(expression)
|
|
1532
|
+
return join_sql
|
|
1533
|
+
|
|
1534
|
+
return super().join_sql(expression)
|
|
1535
|
+
|
|
1536
|
+
def select_sql(self, expression: exp.Select) -> str:
|
|
1537
|
+
# Store the original-to-alias mapping for GROUP BY/ORDER BY reference
|
|
1538
|
+
self.expression_to_alias = {}
|
|
1539
|
+
|
|
1540
|
+
# Build mapping of original expressions to their aliases
|
|
1541
|
+
# After that, in WHERE and ORDER BY use aliases
|
|
1542
|
+
for select_expr in expression.expressions:
|
|
1543
|
+
if isinstance(select_expr, exp.Alias):
|
|
1544
|
+
expr_sql = self.sql(select_expr.this).strip()
|
|
1545
|
+
self.expression_to_alias[expr_sql] = select_expr.alias_or_name
|
|
1546
|
+
else:
|
|
1547
|
+
expr_sql = self.sql(select_expr).strip()
|
|
1548
|
+
if isinstance(select_expr, (exp.Column, exp.Identifier)):
|
|
1549
|
+
self.expression_to_alias[expr_sql] = select_expr.alias_or_name
|
|
1550
|
+
# in .sql() calls ww generated ydb_variables, drop it not to produce unused vars
|
|
1551
|
+
self.ydb_variables = {}
|
|
1552
|
+
return super().select_sql(expression)
|
|
1553
|
+
|
|
1554
|
+
def _contains_literals(self, condition: exp.Expression) -> bool:
|
|
1555
|
+
return condition.find(exp.Literal) is not None
|
|
1556
|
+
|
|
1557
|
+
def where_sql(self, expression: exp.Where) -> str:
|
|
1558
|
+
original_where = super().where_sql(expression) if expression else ""
|
|
1559
|
+
return original_where
|
|
1560
|
+
|
|
1561
|
+
def _date_add(self, expression: exp.Expression) -> str:
|
|
1562
|
+
this = expression.this
|
|
1563
|
+
unit = unit_to_var(expression.expression)
|
|
1564
|
+
op = (
|
|
1565
|
+
"+"
|
|
1566
|
+
if isinstance(
|
|
1567
|
+
expression, (exp.DateAdd, exp.TimeAdd, exp.DatetimeAdd, exp.TsOrDsAdd)
|
|
1568
|
+
)
|
|
1569
|
+
else "-"
|
|
1570
|
+
)
|
|
1571
|
+
|
|
1572
|
+
expr = expression.expression
|
|
1573
|
+
|
|
1574
|
+
source = None
|
|
1575
|
+
if isinstance(this, exp.Literal):
|
|
1576
|
+
if " " in this.name:
|
|
1577
|
+
source = f"DateTime::MakeDateTime(DateTime::ParseIso8601({self.sql(this).replace(' ', 'T')}))"
|
|
1578
|
+
else:
|
|
1579
|
+
source = f"CAST({self.sql(this)} AS DATE)"
|
|
1580
|
+
else:
|
|
1581
|
+
source = self.sql(this)
|
|
1582
|
+
if not unit:
|
|
1583
|
+
return ""
|
|
1584
|
+
if unit.name in ["MONTH", "YEARS"]:
|
|
1585
|
+
to_type = (
|
|
1586
|
+
"DateTime"
|
|
1587
|
+
if isinstance(expression, (exp.DatetimeAdd, exp.DatetimeSub))
|
|
1588
|
+
else "Date"
|
|
1589
|
+
)
|
|
1590
|
+
if unit.name == "YEARS":
|
|
1591
|
+
return f"DateTime::Make{to_type}(DateTime::ShiftYears({source}, {op if op == '-' else ''}{expr.name}))"
|
|
1592
|
+
if unit.name == "MONTH":
|
|
1593
|
+
return f"DateTime::Make{to_type}(DateTime::ShiftMonths({source}, {op if op == '-' else ''}{expr.name}))"
|
|
1594
|
+
return ""
|
|
1595
|
+
else:
|
|
1596
|
+
if unit.name == "DAY":
|
|
1597
|
+
interval_expr = f"DateTime::IntervalFromDays({expr.name})"
|
|
1598
|
+
elif unit.name == "HOUR":
|
|
1599
|
+
interval_expr = f"DateTime::IntervalFromHours({expr.name})"
|
|
1600
|
+
elif unit.name == "MINUTE":
|
|
1601
|
+
interval_expr = f"DateTime::IntervalFromMinutes({expr.name})"
|
|
1602
|
+
elif unit.name == "SECOND":
|
|
1603
|
+
interval_expr = f"DateTime::IntervalFromSeconds({expr.name})"
|
|
1604
|
+
else:
|
|
1605
|
+
raise ValueError(f"Unsupported interval type: {unit.name}")
|
|
1606
|
+
|
|
1607
|
+
return f"{source} {op} {interval_expr}"
|
|
1608
|
+
|
|
1609
|
+
def _arrayany(self, expression: exp.ArrayAny) -> str:
|
|
1610
|
+
"""
|
|
1611
|
+
Generate SQL for ARRAY_ANY function with YDB-specific implementation.
|
|
1612
|
+
|
|
1613
|
+
Args:
|
|
1614
|
+
expression: The ARRAY_ANY expression
|
|
1615
|
+
|
|
1616
|
+
Returns:
|
|
1617
|
+
YDB-specific SQL for array existence checks
|
|
1618
|
+
"""
|
|
1619
|
+
param = expression.expression.expressions[0]
|
|
1620
|
+
column_references = {}
|
|
1621
|
+
|
|
1622
|
+
for ident in expression.expression.this.find_all(exp.Column):
|
|
1623
|
+
if len(ident.parts) < 2:
|
|
1624
|
+
continue
|
|
1625
|
+
|
|
1626
|
+
table_reference = ident.parts[0]
|
|
1627
|
+
column_reference = ident.parts[1]
|
|
1628
|
+
column_references[
|
|
1629
|
+
f"{table_reference.alias_or_name}.{column_reference.alias_or_name}"
|
|
1630
|
+
] = (table_reference, column_reference)
|
|
1631
|
+
|
|
1632
|
+
if len(column_references) > 0:
|
|
1633
|
+
table_aliases = {}
|
|
1634
|
+
next_alias = name_sequence("p_")
|
|
1635
|
+
for column_reference in column_references:
|
|
1636
|
+
table_aliases[column_reference] = next_alias()
|
|
1637
|
+
|
|
1638
|
+
params_l = [
|
|
1639
|
+
f"${param}" for param in [param.alias_or_name] + list(table_aliases.values())
|
|
1640
|
+
]
|
|
1641
|
+
params = f"({', '.join(params_l)})"
|
|
1642
|
+
|
|
1643
|
+
for ident in list(expression.expression.this.find_all(exp.Column)):
|
|
1644
|
+
if len(ident.parts) < 2:
|
|
1645
|
+
continue
|
|
1646
|
+
|
|
1647
|
+
table_reference = ident.parts[0]
|
|
1648
|
+
column_reference = ident.parts[1]
|
|
1649
|
+
full_column_reference = (
|
|
1650
|
+
f"{table_reference.alias_or_name}.{column_reference.alias_or_name}"
|
|
1651
|
+
)
|
|
1652
|
+
table_alias = table_aliases[full_column_reference]
|
|
1653
|
+
table_reference.pop()
|
|
1654
|
+
column_reference.replace(exp.to_identifier(table_alias))
|
|
1655
|
+
|
|
1656
|
+
lambda_sql = self.sql(expression.expression)
|
|
1657
|
+
table_aliases_sql = (
|
|
1658
|
+
f"({', '.join([expression.this.alias_or_name] + list(table_aliases.keys()))})"
|
|
1659
|
+
)
|
|
1660
|
+
|
|
1661
|
+
return f"ListHasItems({params}->(ListFilter(${param.alias_or_name}, {lambda_sql})){table_aliases_sql})"
|
|
1662
|
+
else:
|
|
1663
|
+
return f"ListHasItems(ListFilter({self.sql(expression.expression)}))"
|
|
1664
|
+
|
|
1665
|
+
def _set_sql(self, expression: exp.Set) -> str:
|
|
1666
|
+
eq = expression.find(exp.EQ)
|
|
1667
|
+
if not eq:
|
|
1668
|
+
return ""
|
|
1669
|
+
var_name = exp.Identifier(this="$" + eq.this.name)
|
|
1670
|
+
|
|
1671
|
+
new_eq = exp.EQ(this=var_name, expression=eq.expression)
|
|
1672
|
+
|
|
1673
|
+
return self.binary(new_eq, "=")
|
|
1674
|
+
|
|
1675
|
+
def _group_by(self, expression: exp.Group) -> str:
|
|
1676
|
+
"""Generate GROUP BY using alias references."""
|
|
1677
|
+
select_stmt = expression.find_ancestor(exp.Select)
|
|
1678
|
+
|
|
1679
|
+
if not select_stmt:
|
|
1680
|
+
group_by_items = ", ".join(self.sql(e) for e in expression.expressions)
|
|
1681
|
+
return f" GROUP BY {group_by_items}" if group_by_items else " GROUP BY"
|
|
1682
|
+
|
|
1683
|
+
transformed = []
|
|
1684
|
+
for gb_expr in expression.expressions:
|
|
1685
|
+
gb_sql = self.sql(gb_expr).strip()
|
|
1686
|
+
|
|
1687
|
+
# Check if we have a stored mapping for this expression
|
|
1688
|
+
if hasattr(self, "expression_to_alias") and gb_sql in self.expression_to_alias:
|
|
1689
|
+
alias_name = self.expression_to_alias[gb_sql]
|
|
1690
|
+
alias_expr = exp.alias_(gb_expr, alias_name)
|
|
1691
|
+
transformed.append(alias_expr)
|
|
1692
|
+
else:
|
|
1693
|
+
if isinstance(gb_expr, (exp.Column, exp.Identifier)):
|
|
1694
|
+
# Use the column name as the alias
|
|
1695
|
+
column_name = gb_expr.alias_or_name
|
|
1696
|
+
alias_expr = exp.alias_(gb_expr, column_name)
|
|
1697
|
+
transformed.append(alias_expr)
|
|
1698
|
+
else:
|
|
1699
|
+
transformed.append(gb_expr)
|
|
1700
|
+
|
|
1701
|
+
group_by_items = ", ".join(f"{self.sql(e)}" for e in transformed) if transformed else ""
|
|
1702
|
+
|
|
1703
|
+
# Handle ROLLUP, CUBE, and GROUPING SETS
|
|
1704
|
+
rollup = self.expressions(expression, key="rollup")
|
|
1705
|
+
cube = self.expressions(expression, key="cube")
|
|
1706
|
+
grouping_sets = self.expressions(expression, key="grouping_sets")
|
|
1707
|
+
|
|
1708
|
+
# Build the GROUP BY clause
|
|
1709
|
+
if group_by_items:
|
|
1710
|
+
result = f" GROUP BY ({group_by_items})"
|
|
1711
|
+
else:
|
|
1712
|
+
result = " GROUP BY"
|
|
1713
|
+
|
|
1714
|
+
# Add ROLLUP, CUBE, or GROUPING SETS
|
|
1715
|
+
if rollup:
|
|
1716
|
+
result += f" {rollup}"
|
|
1717
|
+
elif cube:
|
|
1718
|
+
result += f" {cube}"
|
|
1719
|
+
elif grouping_sets:
|
|
1720
|
+
result += f" {grouping_sets}"
|
|
1721
|
+
|
|
1722
|
+
return result
|
|
1723
|
+
|
|
1724
|
+
def _order_sql(self, expression: exp.Order) -> str:
|
|
1725
|
+
"""Generate ORDER BY using alias references."""
|
|
1726
|
+
select_stmt = expression.find_ancestor(exp.Select)
|
|
1727
|
+
|
|
1728
|
+
if not select_stmt:
|
|
1729
|
+
return super().order_sql(expression)
|
|
1730
|
+
|
|
1731
|
+
orders = []
|
|
1732
|
+
for order_expr in expression.expressions:
|
|
1733
|
+
if isinstance(order_expr, exp.Ordered):
|
|
1734
|
+
expr = order_expr.this
|
|
1735
|
+
expr_sql = self.sql(expr).strip()
|
|
1736
|
+
|
|
1737
|
+
if (
|
|
1738
|
+
hasattr(self, "expression_to_alias")
|
|
1739
|
+
and expr_sql in self.expression_to_alias
|
|
1740
|
+
):
|
|
1741
|
+
alias_name = self.expression_to_alias[expr_sql]
|
|
1742
|
+
alias_expr = exp.to_identifier(alias_name)
|
|
1743
|
+
ordered = exp.Ordered(this=alias_expr, desc=order_expr.args.get("desc"))
|
|
1744
|
+
orders.append(ordered)
|
|
1745
|
+
else:
|
|
1746
|
+
orders.append(order_expr)
|
|
1747
|
+
else:
|
|
1748
|
+
expr_sql = self.sql(order_expr).strip()
|
|
1749
|
+
if (
|
|
1750
|
+
hasattr(self, "expression_to_alias")
|
|
1751
|
+
and expr_sql in self.expression_to_alias
|
|
1752
|
+
):
|
|
1753
|
+
alias_name = self.expression_to_alias[expr_sql]
|
|
1754
|
+
alias_expr = exp.to_identifier(alias_name)
|
|
1755
|
+
orders.append(alias_expr)
|
|
1756
|
+
else:
|
|
1757
|
+
orders.append(order_expr)
|
|
1758
|
+
if not orders:
|
|
1759
|
+
return ""
|
|
1760
|
+
|
|
1761
|
+
order_sql = ", ".join(self.sql(e) for e in orders)
|
|
1762
|
+
return f" ORDER BY {order_sql}"
|
|
1763
|
+
|
|
1764
|
+
TYPE_MAPPING = {
|
|
1765
|
+
**generator.Generator.TYPE_MAPPING,
|
|
1766
|
+
**STRING_TYPE_MAPPING,
|
|
1767
|
+
exp.DataType.Type.TINYINT: "INT8",
|
|
1768
|
+
exp.DataType.Type.SMALLINT: "INT16",
|
|
1769
|
+
exp.DataType.Type.INT: "INT32",
|
|
1770
|
+
exp.DataType.Type.BIGINT: "INT64",
|
|
1771
|
+
exp.DataType.Type.DECIMAL: "Decimal",
|
|
1772
|
+
exp.DataType.Type.FLOAT: "Float",
|
|
1773
|
+
exp.DataType.Type.DOUBLE: "Double",
|
|
1774
|
+
exp.DataType.Type.BOOLEAN: "Uint8",
|
|
1775
|
+
exp.DataType.Type.TIMESTAMP: "Timestamp",
|
|
1776
|
+
exp.DataType.Type.BIT: "Uint8",
|
|
1777
|
+
exp.DataType.Type.VARCHAR: "String",
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
TRANSFORMS = {
|
|
1781
|
+
**generator.Generator.TRANSFORMS,
|
|
1782
|
+
exp.Create: create_sql,
|
|
1783
|
+
exp.DefaultColumnConstraint: lambda self, e: "",
|
|
1784
|
+
exp.DateTrunc: _date_trunc_sql,
|
|
1785
|
+
exp.Select: transforms.preprocess(
|
|
1786
|
+
[apply_alias_to_select_from_table, move_ctes_to_top_level]
|
|
1787
|
+
),
|
|
1788
|
+
exp.CurrentTimestamp: _current_timestamp_sql,
|
|
1789
|
+
exp.StrToDate: _str_to_date,
|
|
1790
|
+
exp.Extract: _extract,
|
|
1791
|
+
exp.ArraySize: rename_func_not_normalize("ListLength"),
|
|
1792
|
+
exp.ArrayFilter: rename_func_not_normalize("ListFilter"),
|
|
1793
|
+
exp.Lambda: _lambda,
|
|
1794
|
+
exp.ArrayAny: _arrayany,
|
|
1795
|
+
exp.ArrayAgg: rename_func_not_normalize("AGGREGATE_LIST"),
|
|
1796
|
+
exp.Concat: concat_to_dpipe_sql,
|
|
1797
|
+
exp.If: _if,
|
|
1798
|
+
exp.Nullif: _null_if,
|
|
1799
|
+
exp.DateAdd: _date_add,
|
|
1800
|
+
exp.DateSub: _date_add,
|
|
1801
|
+
exp.JSONBContains: rename_func_not_normalize("Yson::Contains"),
|
|
1802
|
+
exp.ForeignKey: lambda self, e: self.unsupported("constraint not supported"),
|
|
1803
|
+
exp.StringToArray: rename_func_not_normalize("String::SplitToList"),
|
|
1804
|
+
exp.Array: rename_func_not_normalize("AsList"),
|
|
1805
|
+
exp.ArrayToString: rename_func_not_normalize("String::JoinFromList"),
|
|
1806
|
+
exp.Upper: rename_func_not_normalize("String::Upper"),
|
|
1807
|
+
exp.Lower: rename_func_not_normalize("String::Lower"),
|
|
1808
|
+
exp.StrPosition: rename_func_not_normalize("Find"),
|
|
1809
|
+
exp.Length: rename_func_not_normalize("String::Length"),
|
|
1810
|
+
exp.Unnest: rename_func_not_normalize("FLATTEN BY"),
|
|
1811
|
+
exp.Round: rename_func_not_normalize("Math::Round"),
|
|
1812
|
+
exp.Set: _set_sql,
|
|
1813
|
+
exp.Group: _group_by,
|
|
1814
|
+
exp.Order: _order_sql,
|
|
1815
|
+
}
|