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/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
+ }