sqlglot.transforms
1from __future__ import annotations 2 3import typing as t 4 5from sqlglot import expressions as exp 6from sqlglot.errors import UnsupportedError 7from sqlglot.helper import find_new_name, name_sequence, seq_get 8 9 10if t.TYPE_CHECKING: 11 from sqlglot._typing import E 12 from sqlglot.generator import Generator 13 14 15class SqlHandler(t.Protocol): 16 def __call__(self, expression: exp.Expr, *args: t.Any, **kwargs: t.Any) -> str: ... 17 18 19def preprocess( 20 transforms: list[t.Callable[[exp.Expr], exp.Expr]], 21 generator: t.Callable[[Generator, exp.Expr], str] | None = None, 22) -> t.Callable[[Generator, exp.Expr], str]: 23 """ 24 Creates a new transform by chaining a sequence of transformations and converts the resulting 25 expression to SQL, using either the "_sql" method corresponding to the resulting expression, 26 or the appropriate `Generator.TRANSFORMS` function (when applicable -- see below). 27 28 Args: 29 transforms: sequence of transform functions. These will be called in order. 30 31 Returns: 32 Function that can be used as a generator transform. 33 """ 34 35 def _to_sql(self: Generator, expression: exp.Expr) -> str: 36 expression_type = type(expression) 37 38 try: 39 expression = transforms[0](expression) 40 for transform in transforms[1:]: 41 expression = transform(expression) 42 except UnsupportedError as unsupported_error: 43 self.unsupported(str(unsupported_error)) 44 45 if generator: 46 return generator(self, expression) 47 48 _sql_handler: SqlHandler | None = getattr(self, expression.key + "_sql", None) 49 if _sql_handler: 50 return _sql_handler(expression) 51 52 transforms_handler = self.TRANSFORMS.get(type(expression)) 53 if transforms_handler: 54 if expression_type is type(expression): 55 if isinstance(expression, exp.Func): 56 return self.function_fallback_sql(expression) 57 58 # Ensures we don't enter an infinite loop. This can happen when the original expression 59 # has the same type as the final expression and there's no _sql method available for it, 60 # because then it'd re-enter _to_sql. 61 raise ValueError( 62 f"Expr type {expression.__class__.__name__} requires a _sql method in order to be transformed." 63 ) 64 65 return transforms_handler(self, expression) 66 67 raise ValueError(f"Unsupported expression type {expression.__class__.__name__}.") 68 69 return _to_sql 70 71 72def unnest_generate_date_array_using_recursive_cte(expression: exp.Expr) -> exp.Expr: 73 if isinstance(expression, exp.Select): 74 count = 0 75 recursive_ctes: list[exp.Expr] = [] 76 77 for unnest in expression.find_all(exp.Unnest): 78 if ( 79 not isinstance(unnest.parent, (exp.From, exp.Join)) 80 or len(unnest.expressions) != 1 81 or not isinstance(unnest.expressions[0], exp.GenerateDateArray) 82 ): 83 continue 84 85 generate_date_array = unnest.expressions[0] 86 start: exp.Expr | None = generate_date_array.args.get("start") 87 end: exp.Expr | None = generate_date_array.args.get("end") 88 step: exp.Expr | None = generate_date_array.args.get("step") 89 90 if not start or not end or not isinstance(step, exp.Interval): 91 continue 92 93 alias: exp.TableAlias | None = unnest.args.get("alias") 94 column_name: str = ( 95 alias.columns[0] if isinstance(alias, exp.TableAlias) else "date_value" 96 ) 97 98 start = exp.cast(start, "date") 99 date_add = exp.func( 100 "date_add", column_name, exp.Literal.number(step.name), step.args.get("unit") 101 ) 102 cast_date_add = exp.cast(date_add, "date") 103 104 cte_name = "_generated_dates" + (f"_{count}" if count else "") 105 106 base_query = exp.select(start.as_(column_name)) 107 recursive_query = ( 108 exp.select(cast_date_add) 109 .from_(cte_name) 110 .where(cast_date_add <= exp.cast(end, "date")) 111 ) 112 cte_query = base_query.union(recursive_query, distinct=False) 113 114 generate_dates_query = exp.select(column_name).from_(cte_name) 115 unnest.replace(generate_dates_query.subquery(cte_name)) 116 117 recursive_ctes.append( 118 exp.alias_(exp.CTE(this=cte_query), cte_name, table=[column_name]) 119 ) 120 count += 1 121 122 if recursive_ctes: 123 with_expression: exp.With = expression.args.get("with_") or exp.With() 124 with_expression.set("recursive", True) 125 with_expression.set("expressions", [*recursive_ctes, *with_expression.expressions]) 126 expression.set("with_", with_expression) 127 128 return expression 129 130 131def unnest_generate_series(expression: exp.Expr) -> exp.Expr: 132 """Unnests GENERATE_SERIES or SEQUENCE table references.""" 133 this = expression.this 134 if isinstance(expression, exp.Table) and isinstance(this, exp.GenerateSeries): 135 unnest = exp.Unnest(expressions=[this]) 136 if expression.alias: 137 return exp.alias_(unnest, alias="_u", table=[expression.alias], copy=False) 138 139 return unnest 140 141 return expression 142 143 144def eliminate_distinct_on(expression: exp.Expr) -> exp.Expr: 145 """ 146 Convert SELECT DISTINCT ON statements to a subquery with a window function. 147 148 This is useful for dialects that don't support SELECT DISTINCT ON but support window functions. 149 150 Args: 151 expression: the expression that will be transformed. 152 153 Returns: 154 The transformed expression. 155 """ 156 if ( 157 isinstance(expression, exp.Select) 158 and expression.args.get("distinct") 159 and isinstance(expression.args["distinct"].args.get("on"), exp.Tuple) 160 ): 161 row_number_window_alias = find_new_name(expression.named_selects, "_row_number") 162 163 distinct_cols = expression.args["distinct"].pop().args["on"].expressions 164 window = exp.Window(this=exp.RowNumber(), partition_by=distinct_cols) 165 166 order: exp.Order | None = expression.args.get("order") 167 if order: 168 window.set("order", order.pop()) 169 else: 170 window.set("order", exp.Order(expressions=[c.copy() for c in distinct_cols])) 171 172 expression.select(exp.alias_(window, row_number_window_alias), copy=False) 173 174 # We add aliases to the projections so that we can safely reference them in the outer query 175 new_selects: list[exp.Expr] = [] 176 taken_names = {row_number_window_alias} 177 for select in expression.selects[:-1]: 178 if select.is_star: 179 new_selects = [exp.Star()] 180 break 181 182 if not isinstance(select, exp.Alias): 183 alias = find_new_name(taken_names, select.output_name or "_col") 184 quoted: bool | None = ( 185 select.this.args.get("quoted") if isinstance(select, exp.Column) else None 186 ) 187 select = select.replace(exp.alias_(select, alias, quoted=quoted)) 188 189 taken_names.add(select.output_name) 190 new_selects.append(select.args["alias"]) 191 192 return ( 193 exp.select(*new_selects, copy=False) 194 .from_(expression.subquery("_t", copy=False), copy=False) 195 .where(exp.column(row_number_window_alias).eq(1), copy=False) 196 ) 197 198 return expression 199 200 201def eliminate_qualify(expression: exp.Expr) -> exp.Expr: 202 """ 203 Convert SELECT statements that contain the QUALIFY clause into subqueries, filtered equivalently. 204 205 The idea behind this transformation can be seen in Snowflake's documentation for QUALIFY: 206 https://docs.snowflake.com/en/sql-reference/constructs/qualify 207 208 Some dialects don't support window functions in the WHERE clause, so we need to include them as 209 projections in the subquery, in order to refer to them in the outer filter using aliases. Also, 210 if a column is referenced in the QUALIFY clause but is not selected, we need to include it too, 211 otherwise we won't be able to refer to it in the outer query's WHERE clause. Finally, if a 212 newly aliased projection is referenced in the QUALIFY clause, it will be replaced by the 213 corresponding expression to avoid creating invalid column references. 214 """ 215 if isinstance(expression, exp.Select) and expression.args.get("qualify"): 216 parent = expression.parent 217 if isinstance(parent, exp.Insert) and isinstance(parent.parent, exp.MultitableInserts): 218 # A FROM-first multi-table INSERT branch shares its parent's FROM, so it can't be wrapped 219 raise UnsupportedError( 220 "QUALIFY is not supported in a FROM-first multi-table INSERT branch" 221 ) 222 223 taken = set(expression.named_selects) 224 for select in expression.selects: 225 if not select.alias_or_name: 226 alias = find_new_name(taken, "_c") 227 select.replace(exp.alias_(select, alias)) 228 taken.add(alias) 229 230 def _select_alias_or_name(select: exp.Expr) -> str | exp.Column: 231 alias_or_name = select.alias_or_name 232 identifier = select.args.get("alias") or select.this 233 if isinstance(identifier, exp.Identifier): 234 return exp.column(alias_or_name, quoted=identifier.args.get("quoted")) 235 return alias_or_name 236 237 outer_selects = exp.select(*map(_select_alias_or_name, expression.selects)) 238 qualify_filters: exp.Expr = expression.args["qualify"].pop().this 239 expression_by_alias: dict[str, exp.Expr] = { 240 select.alias: select.this 241 for select in expression.selects 242 if isinstance(select, exp.Alias) 243 } 244 245 select_candidates = (exp.Window,) if expression.is_star else (exp.Window, exp.Column) 246 for select_candidate in list(qualify_filters.find_all(*select_candidates)): 247 if isinstance(select_candidate, exp.Window): 248 if expression_by_alias: 249 for column in select_candidate.find_all(exp.Column): 250 expr = expression_by_alias.get(column.name) 251 if expr: 252 column.replace(expr) 253 254 alias = find_new_name(expression.named_selects, "_w") 255 expression.select(exp.alias_(select_candidate, alias), copy=False) 256 column = exp.column(alias) 257 258 if isinstance(select_candidate.parent, exp.Qualify): 259 qualify_filters = column 260 else: 261 select_candidate.replace(column) 262 elif select_candidate.name not in expression.named_selects and not ( 263 select_candidate.find_ancestor(exp.Window) 264 ): 265 # A column that is only read by a window function is computed by the window 266 # itself, so projecting it in the subquery is redundant and can even produce 267 # invalid SQL, e.g. when the query is grouped 268 expression.select(select_candidate.copy(), copy=False) 269 270 return outer_selects.from_(expression.subquery(alias="_t", copy=False), copy=False).where( 271 qualify_filters, copy=False 272 ) 273 274 return expression 275 276 277def remove_precision_parameterized_types(expression: exp.Expr) -> exp.Expr: 278 """ 279 Some dialects only allow the precision for parameterized types to be defined in the DDL and not in 280 other expressions. This transforms removes the precision from parameterized types in expressions. 281 """ 282 for node in expression.find_all(exp.DataType): 283 node.set( 284 "expressions", [e for e in node.expressions if not isinstance(e, exp.DataTypeParam)] 285 ) 286 287 return expression 288 289 290def unqualify_unnest(expression: exp.Expr) -> exp.Expr: 291 """Remove references to unnest table aliases, added by the optimizer's qualify_columns step.""" 292 from sqlglot.optimizer.scope import find_all_in_scope 293 294 if isinstance(expression, exp.Select): 295 unnest_aliases = { 296 unnest.alias 297 for unnest in find_all_in_scope(expression, exp.Unnest) 298 if isinstance(unnest.parent, (exp.From, exp.Join)) 299 } 300 if unnest_aliases: 301 for column in expression.find_all(exp.Column): 302 leftmost_part = column.parts[0] 303 if leftmost_part.arg_key != "this" and leftmost_part.this in unnest_aliases: 304 leftmost_part.pop() 305 306 return expression 307 308 309def unnest_to_explode( 310 expression: exp.Expr, 311 unnest_using_arrays_zip: bool = True, 312) -> exp.Expr: 313 """Convert cross join unnest into lateral view explode.""" 314 315 def _unnest_zip_exprs( 316 u: exp.Unnest, unnest_exprs: list[exp.Expr], has_multi_expr: bool 317 ) -> list[exp.Expr]: 318 if has_multi_expr: 319 if not unnest_using_arrays_zip: 320 raise UnsupportedError("Cannot transpile UNNEST with multiple input arrays") 321 322 # Use INLINE(ARRAYS_ZIP(...)) for multiple expressions 323 zip_exprs: list[exp.Expr] = [exp.Anonymous(this="ARRAYS_ZIP", expressions=unnest_exprs)] 324 u.set("expressions", zip_exprs) 325 return zip_exprs 326 return unnest_exprs 327 328 def _udtf_type(u: exp.Unnest, has_multi_expr: bool) -> type[exp.Func]: 329 if u.args.get("offset"): 330 return exp.Posexplode 331 return exp.Inline if has_multi_expr else exp.Explode 332 333 if isinstance(expression, exp.Select): 334 from_ = expression.args.get("from_") 335 336 if from_ and isinstance(from_.this, exp.Unnest): 337 unnest: exp.Unnest = from_.this 338 alias: exp.TableAlias | None = unnest.args.get("alias") 339 exprs: list[exp.Expr] = unnest.expressions 340 has_multi_expr = len(exprs) > 1 341 this, *_ = _unnest_zip_exprs(unnest, exprs, has_multi_expr) 342 343 columns: list[exp.Identifier] = alias.columns if alias else [] 344 offset: exp.Expr | None = unnest.args.get("offset") 345 if offset: 346 columns.insert( 347 0, offset if isinstance(offset, exp.Identifier) else exp.to_identifier("pos") 348 ) 349 350 unnest.replace( 351 exp.Table( 352 this=_udtf_type(unnest, has_multi_expr)(this=this), 353 alias=exp.TableAlias(this=alias.this, columns=columns) if alias else None, 354 ) 355 ) 356 357 joins: list[exp.Join] = expression.args.get("joins") or [] 358 for join in list(joins): 359 join_expr = join.this 360 361 is_lateral = isinstance(join_expr, exp.Lateral) 362 363 unnest = join_expr.this if is_lateral else join_expr 364 365 if isinstance(unnest, exp.Unnest): 366 if is_lateral: 367 alias = join_expr.args.get("alias") 368 else: 369 alias = unnest.args.get("alias") 370 371 if alias is None: 372 raise UnsupportedError( 373 "CROSS JOIN UNNEST to LATERAL VIEW EXPLODE transformation requires an alias" 374 ) 375 376 exprs = unnest.expressions 377 # The number of unnest.expressions will be changed by _unnest_zip_exprs, we need to record it here 378 has_multi_expr = len(exprs) > 1 379 exprs = _unnest_zip_exprs(unnest, exprs, has_multi_expr) 380 381 joins.remove(join) 382 383 alias_cols: list[exp.Identifier] = alias.columns 384 385 # # Handle UNNEST to LATERAL VIEW EXPLODE: Exception is raised when there are 0 or > 2 aliases 386 # Spark LATERAL VIEW EXPLODE requires single alias for array/struct and two for Map type column unlike unnest in trino/presto which can take an arbitrary amount. 387 # Refs: https://spark.apache.org/docs/latest/sql-ref-syntax-qry-select-lateral-view.html 388 389 if not has_multi_expr and len(alias_cols) not in (1, 2): 390 raise UnsupportedError( 391 "CROSS JOIN UNNEST to LATERAL VIEW EXPLODE transformation requires explicit column aliases" 392 ) 393 394 offset = unnest.args.get("offset") 395 if offset: 396 alias_cols.insert( 397 0, 398 offset if isinstance(offset, exp.Identifier) else exp.to_identifier("pos"), 399 ) 400 401 for e, column in zip(exprs, alias_cols): 402 expression.append( 403 "laterals", 404 exp.Lateral( 405 this=_udtf_type(unnest, has_multi_expr)(this=e), 406 view=True, 407 alias=exp.TableAlias(this=alias.this, columns=alias_cols), 408 ), 409 ) 410 411 return expression 412 413 414def explode_projection_to_unnest( 415 index_offset: int = 0, 416 unnest_map: bool = False, 417) -> t.Callable[[exp.Expr], exp.Expr]: 418 """Convert explode/posexplode projections into unnests.""" 419 420 def _explode_projection_to_unnest(expression: exp.Expr) -> exp.Expr: 421 if isinstance(expression, exp.Select): 422 from sqlglot.optimizer.scope import Scope 423 424 taken_select_names = set(expression.named_selects) 425 taken_source_names = {name for name, _ in Scope(expression).references} 426 427 def new_name(names: set[str], name: str) -> str: 428 name = find_new_name(names, name) 429 names.add(name) 430 return name 431 432 arrays: list[exp.Condition] = [] 433 series_alias = new_name(taken_select_names, "pos") 434 series = exp.alias_( 435 exp.Unnest( 436 expressions=[exp.GenerateSeries(start=exp.Literal.number(index_offset))] 437 ), 438 new_name(taken_source_names, "_u"), 439 table=[series_alias], 440 ) 441 442 # we use list here because expression.selects is mutated inside the loop 443 for select in list(expression.selects): 444 explode = select.find(exp.Explode) 445 446 if explode: 447 if ( 448 unnest_map 449 and type(explode) is exp.Explode 450 and explode.this.is_type(exp.DType.MAP) 451 and (select is explode or isinstance(select, exp.Aliases)) 452 ): 453 map_key_alias: t.Any 454 map_value_alias: t.Any 455 if isinstance(select, exp.Aliases): 456 map_key_alias, map_value_alias = select.aliases 457 else: 458 map_key_alias = new_name(taken_select_names, "key") 459 map_value_alias = new_name(taken_select_names, "value") 460 map_unnest_source = new_name(taken_source_names, "_u") 461 462 map_key_select = select.replace( 463 exp.column(map_key_alias, table=map_unnest_source).as_(map_key_alias) 464 ) 465 466 expressions = expression.expressions 467 expressions.insert( 468 expressions.index(map_key_select) + 1, 469 exp.column(map_value_alias, table=map_unnest_source).as_( 470 map_value_alias 471 ), 472 ) 473 expression.set("expressions", expressions) 474 475 unnest = exp.alias_( 476 exp.Unnest(expressions=[explode.this.copy()]), 477 map_unnest_source, 478 table=[map_key_alias, map_value_alias], 479 ) 480 if expression.args.get("from_"): 481 expression.join(unnest, copy=False, join_type="CROSS") 482 else: 483 expression.from_(unnest, copy=False) 484 485 continue 486 487 pos_alias: t.Any = "" 488 explode_alias: t.Any = "" 489 490 if isinstance(select, exp.Alias): 491 explode_alias = select.args["alias"] 492 alias: exp.Expr = select 493 elif isinstance(select, exp.Aliases): 494 pos_alias = select.aliases[0] 495 explode_alias = select.aliases[1] 496 alias = select.replace(exp.alias_(select.this, "", copy=False)) 497 else: 498 alias = select.replace(exp.alias_(select, "")) 499 explode = alias.find(exp.Explode) 500 assert explode 501 502 is_posexplode = isinstance(explode, exp.Posexplode) 503 explode_arg = explode.this 504 505 if isinstance(explode, exp.ExplodeOuter): 506 bracket = explode_arg[0] 507 bracket.set("safe", True) 508 bracket.set("offset", True) 509 explode_arg = exp.func( 510 "IF", 511 exp.func( 512 "ARRAY_SIZE", exp.func("COALESCE", explode_arg, exp.Array()) 513 ).eq(0), 514 exp.array(bracket, copy=False), 515 explode_arg, 516 ) 517 518 # This ensures that we won't use [POS]EXPLODE's argument as a new selection 519 if isinstance(explode_arg, exp.Column): 520 taken_select_names.add(explode_arg.output_name) 521 522 unnest_source_alias = new_name(taken_source_names, "_u") 523 524 if not explode_alias: 525 explode_alias = new_name(taken_select_names, "col") 526 527 if is_posexplode: 528 pos_alias = new_name(taken_select_names, "pos") 529 530 if not pos_alias: 531 pos_alias = new_name(taken_select_names, "pos") 532 533 alias.set("alias", exp.to_identifier(explode_alias)) 534 535 series_table_alias = series.args["alias"].this 536 column = exp.If( 537 this=exp.column(series_alias, table=series_table_alias).eq( 538 exp.column(pos_alias, table=unnest_source_alias) 539 ), 540 true=exp.column(explode_alias, table=unnest_source_alias), 541 ) 542 543 explode.replace(column) 544 545 if is_posexplode: 546 expressions = expression.expressions 547 expressions.insert( 548 expressions.index(alias) + 1, 549 exp.If( 550 this=exp.column(series_alias, table=series_table_alias).eq( 551 exp.column(pos_alias, table=unnest_source_alias) 552 ), 553 true=exp.column(pos_alias, table=unnest_source_alias), 554 ).as_(pos_alias), 555 ) 556 expression.set("expressions", expressions) 557 558 if not arrays: 559 if expression.args.get("from_"): 560 expression.join(series, copy=False, join_type="CROSS") 561 else: 562 expression.from_(series, copy=False) 563 564 size: exp.Condition = exp.ArraySize(this=explode_arg.copy()) 565 arrays.append(size) 566 567 # trino doesn't support left join unnest with on conditions 568 # if it did, this would be much simpler 569 expression.join( 570 exp.alias_( 571 exp.Unnest( 572 expressions=[explode_arg.copy()], 573 offset=exp.to_identifier(pos_alias), 574 ), 575 unnest_source_alias, 576 table=[explode_alias], 577 ), 578 join_type="CROSS", 579 copy=False, 580 ) 581 582 if index_offset != 1: 583 size = size - 1 584 585 expression.where( 586 exp.column(series_alias, table=series_table_alias) 587 .eq(exp.column(pos_alias, table=unnest_source_alias)) 588 .or_( 589 (exp.column(series_alias, table=series_table_alias) > size).and_( 590 exp.column(pos_alias, table=unnest_source_alias).eq(size) 591 ) 592 ), 593 copy=False, 594 ) 595 596 if arrays: 597 end: exp.Condition = exp.Greatest(this=arrays[0], expressions=arrays[1:]) 598 599 if index_offset != 1: 600 end = end - (1 - index_offset) 601 series.expressions[0].set("end", end) 602 603 return expression 604 605 return _explode_projection_to_unnest 606 607 608def add_within_group_for_percentiles(expression: exp.Expr) -> exp.Expr: 609 """Transforms percentiles by adding a WITHIN GROUP clause to them.""" 610 if ( 611 isinstance(expression, exp.PERCENTILES) 612 and not isinstance(expression.parent, exp.WithinGroup) 613 and expression.expression 614 ): 615 column = expression.this.pop() 616 expression.set("this", expression.expression.pop()) 617 order = exp.Order(expressions=[exp.Ordered(this=column)]) 618 expression = exp.WithinGroup(this=expression, expression=order) 619 620 return expression 621 622 623def remove_within_group_for_percentiles(expression: exp.Expr) -> exp.Expr: 624 """Transforms percentiles by getting rid of their corresponding WITHIN GROUP clause.""" 625 if ( 626 isinstance(expression, exp.WithinGroup) 627 and isinstance(expression.this, exp.PERCENTILES) 628 and isinstance(expression.expression, exp.Order) 629 ): 630 quantile = expression.this.this 631 input_value = t.cast(exp.Ordered, expression.find(exp.Ordered)).this 632 return expression.replace(exp.ApproxQuantile(this=input_value, quantile=quantile)) 633 634 return expression 635 636 637def add_recursive_cte_column_names(expression: exp.Expr) -> exp.Expr: 638 """Uses projection output names in recursive CTE definitions to define the CTEs' columns.""" 639 if isinstance(expression, exp.With) and expression.recursive: 640 next_name = name_sequence("_c_") 641 642 for cte in expression.expressions: 643 if not cte.args["alias"].columns: 644 query = cte.this 645 if isinstance(query, exp.SetOperation): 646 query = query.this 647 648 cte.args["alias"].set( 649 "columns", 650 [exp.to_identifier(s.alias_or_name or next_name()) for s in query.selects], 651 ) 652 653 return expression 654 655 656def epoch_cast_to_ts(expression: exp.Expr) -> exp.Expr: 657 """Replace 'epoch' in casts by the equivalent date literal.""" 658 if ( 659 isinstance(expression, (exp.Cast, exp.TryCast)) 660 and expression.name.lower() == "epoch" 661 and expression.to.this in exp.DataType.TEMPORAL_TYPES 662 ): 663 expression.this.replace(exp.Literal.string("1970-01-01 00:00:00")) 664 665 return expression 666 667 668def eliminate_semi_and_anti_joins(expression: exp.Expr) -> exp.Expr: 669 """Convert SEMI and ANTI joins into equivalent forms that use EXIST instead.""" 670 if isinstance(expression, exp.Select): 671 for join in list[exp.Join](expression.args.get("joins") or []): 672 on: exp.Expr | None = join.args.get("on") 673 if on and join.kind in ("SEMI", "ANTI"): 674 subquery = exp.select("1").from_(join.this).where(on) 675 exists: exp.Exists | exp.Not = exp.Exists(this=subquery) 676 if join.kind == "ANTI": 677 exists = exists.not_(copy=False) 678 679 join.pop() 680 expression.where(exists, copy=False) 681 682 return expression 683 684 685def eliminate_full_outer_join(expression: exp.Expr) -> exp.Expr: 686 """ 687 Converts a query with a FULL OUTER join to a union of identical queries that 688 use LEFT/RIGHT OUTER joins instead. This transformation currently only works 689 for queries that have a single FULL OUTER join. 690 """ 691 if isinstance(expression, exp.Select): 692 full_outer_joins: list[tuple[int, exp.Join]] = [ 693 (index, join) 694 for index, join in enumerate[exp.Join](expression.args.get("joins") or []) 695 if join.side == "FULL" 696 ] 697 698 if len(full_outer_joins) == 1: 699 expression_copy = expression.copy() 700 index, full_outer_join = full_outer_joins[0] 701 702 tables = (expression.args["from_"].alias_or_name, full_outer_join.alias_or_name) 703 join_conditions = full_outer_join.args.get("on") or exp.and_( 704 *[ 705 exp.column(col, tables[0]).eq(exp.column(col, tables[1])) 706 for col in t.cast(list[exp.Identifier], full_outer_join.args.get("using")) 707 ] 708 ) 709 710 full_outer_join.set("side", "left") 711 anti_join_clause = ( 712 exp.select("1").from_(expression.args["from_"]).where(join_conditions) 713 ) 714 expression_copy.args["joins"][index].set("side", "right") 715 expression_copy = expression_copy.where(exp.Exists(this=anti_join_clause).not_()) 716 717 union = exp.union(expression, expression_copy, copy=False, distinct=False) 718 for arg in ("with_", "order", "limit", "offset"): 719 value = expression.args.get(arg) 720 if value: 721 expression.set(arg, None) 722 expression_copy.set(arg, None) 723 union.set(arg, value) 724 return union 725 726 return expression 727 728 729def move_ctes_to_top_level(expression: E) -> E: 730 """ 731 Some dialects (e.g. Hive, T-SQL, Spark prior to version 3) only allow CTEs to be 732 defined at the top-level, so for example queries like: 733 734 SELECT * FROM (WITH t(c) AS (SELECT 1) SELECT * FROM t) AS subq 735 736 are invalid in those dialects. This transformation can be used to ensure all CTEs are 737 moved to the top level so that the final SQL code is valid from a syntax standpoint. 738 739 TODO: handle name clashes whilst moving CTEs (it can get quite tricky & costly). 740 """ 741 top_level_with: exp.With | None = expression.args.get("with_") 742 for inner_with in expression.find_all(exp.With): 743 if inner_with.parent is expression: 744 continue 745 746 if not top_level_with: 747 top_level_with = inner_with.pop() 748 expression.set("with_", top_level_with) 749 else: 750 if inner_with.recursive: 751 top_level_with.set("recursive", True) 752 753 parent_cte = inner_with.find_ancestor(exp.CTE) 754 inner_with.pop() 755 756 if parent_cte: 757 i = top_level_with.expressions.index(parent_cte) 758 top_level_with.expressions[i:i] = inner_with.expressions 759 top_level_with.set("expressions", top_level_with.expressions) 760 else: 761 top_level_with.set( 762 "expressions", top_level_with.expressions + inner_with.expressions 763 ) 764 765 return expression 766 767 768def ensure_bools(expression: exp.Expr) -> exp.Expr: 769 """Converts numeric values used in conditions into explicit boolean expressions.""" 770 from sqlglot.optimizer.canonicalize import ensure_bools 771 772 def _ensure_bool(node: exp.Expr) -> None: 773 if ( 774 node.is_number 775 or ( 776 not isinstance(node, exp.SubqueryPredicate) 777 and node.is_type(exp.DType.UNKNOWN, *exp.DataType.NUMERIC_TYPES) 778 ) 779 or (isinstance(node, exp.Column) and not node.type) 780 ): 781 node.replace(node.neq(0)) 782 783 for node in expression.walk(): 784 ensure_bools(node, _ensure_bool) 785 786 return expression 787 788 789def unqualify_columns(expression: exp.Expr) -> exp.Expr: 790 for column in expression.find_all(exp.Column): 791 # We only wanna pop off the table, db, catalog args 792 for part in column.parts[:-1]: 793 part.pop() 794 795 return expression 796 797 798def unqualify_pivot_fields(expression: exp.Expr) -> exp.Expr: 799 """ 800 Some dialects only accept simple column names in a (UN)PIVOT's FOR clause and IN-list 801 (Oracle raises ORA-01748), even though the aggregate itself may stay qualified. 802 803 Example: 804 >>> from sqlglot import parse_one 805 >>> expr = parse_one("SELECT * FROM tbl PIVOT (SUM(tbl.sales) FOR tbl.quarter IN ('Q1', 'Q2'))") 806 >>> print(unqualify_pivot_fields(expr).sql(dialect="spark")) 807 SELECT * FROM tbl PIVOT(SUM(tbl.sales) FOR quarter IN ('Q1', 'Q2')) 808 """ 809 if isinstance(expression, exp.Pivot): 810 expression.set("fields", [unqualify_columns(field) for field in expression.fields]) 811 812 return expression 813 814 815def remove_unique_constraints(expression: exp.Expr) -> exp.Expr: 816 assert isinstance(expression, exp.Create) 817 for constraint in expression.find_all(exp.UniqueColumnConstraint): 818 parent = constraint.parent 819 (parent if isinstance(parent, (exp.ColumnConstraint, exp.Constraint)) else constraint).pop() 820 821 return expression 822 823 824def ctas_with_tmp_tables_to_create_tmp_view( 825 expression: exp.Expr, 826 tmp_storage_provider: t.Callable[[exp.Expr], exp.Expr] = lambda e: e, 827) -> exp.Expr: 828 assert isinstance(expression, exp.Create) 829 properties: exp.Properties | None = expression.args.get("properties") 830 temporary = any( 831 isinstance(prop, exp.TemporaryProperty) 832 for prop in (properties.expressions if properties is not None else []) 833 ) 834 835 # CTAS with temp tables map to CREATE TEMPORARY VIEW 836 if expression.kind == "TABLE" and temporary: 837 if expression.expression: 838 return exp.Create( 839 kind="TEMPORARY VIEW", 840 this=expression.this, 841 expression=expression.expression, 842 ) 843 return tmp_storage_provider(expression) 844 845 return expression 846 847 848def move_schema_columns_to_partitioned_by(expression: exp.Expr) -> exp.Expr: 849 """ 850 In Hive, the PARTITIONED BY property acts as an extension of a table's schema. When the 851 PARTITIONED BY value is an array of column names, they are transformed into a schema. 852 The corresponding columns are removed from the create statement. 853 """ 854 assert isinstance(expression, exp.Create) 855 schema = expression.this 856 is_partitionable = expression.kind in {"TABLE", "VIEW"} 857 858 if isinstance(schema, exp.Schema) and is_partitionable: 859 prop = expression.find(exp.PartitionedByProperty) 860 if prop and prop.this and not isinstance(prop.this, exp.Schema): 861 columns: set[str] = {v.name.upper() for v in prop.this.expressions} 862 schema_exprs: list[exp.Expr] = schema.expressions 863 partitions = [col for col in schema_exprs if col.name.upper() in columns] 864 schema.set("expressions", [e for e in schema_exprs if e not in partitions]) 865 prop.replace(exp.PartitionedByProperty(this=exp.Schema(expressions=partitions))) 866 expression.set("this", schema) 867 868 return expression 869 870 871def move_partitioned_by_to_schema_columns(expression: exp.Expr) -> exp.Expr: 872 """ 873 Spark 3 supports both "HIVEFORMAT" and "DATASOURCE" formats for CREATE TABLE. 874 875 Currently, SQLGlot uses the DATASOURCE format for Spark 3. 876 """ 877 assert isinstance(expression, exp.Create) 878 prop = expression.find(exp.PartitionedByProperty) 879 if ( 880 prop 881 and prop.this 882 and isinstance(prop.this, exp.Schema) 883 and all(isinstance(e, exp.ColumnDef) and e.kind for e in prop.this.expressions) 884 ): 885 prop_this = exp.Tuple( 886 expressions=[exp.to_identifier(e.this) for e in prop.this.expressions] 887 ) 888 schema: exp.Schema = expression.this 889 for e in prop.this.expressions: 890 schema.append("expressions", e) 891 prop.set("this", prop_this) 892 893 return expression 894 895 896def struct_kv_to_alias(expression: exp.Expr) -> exp.Expr: 897 """Converts struct arguments to aliases, e.g. STRUCT(1 AS y).""" 898 if isinstance(expression, exp.Struct): 899 expression.set( 900 "expressions", 901 [ 902 exp.alias_(e.expression, e.this) if isinstance(e, exp.PropertyEQ) else e 903 for e in expression.expressions 904 ], 905 ) 906 907 return expression 908 909 910def eliminate_join_marks(expression: exp.Expr) -> exp.Expr: 911 """https://docs.oracle.com/cd/B19306_01/server.102/b14200/queries006.htm#sthref3178 912 913 1. You cannot specify the (+) operator in a query block that also contains FROM clause join syntax. 914 915 2. The (+) operator can appear only in the WHERE clause or, in the context of left-correlation (that is, when specifying the TABLE clause) in the FROM clause, and can be applied only to a column of a table or view. 916 917 The (+) operator does not produce an outer join if you specify one table in the outer query and the other table in an inner query. 918 919 You cannot use the (+) operator to outer-join a table to itself, although self joins are valid. 920 921 The (+) operator can be applied only to a column, not to an arbitrary expression. However, an arbitrary expression can contain one or more columns marked with the (+) operator. 922 923 A WHERE condition containing the (+) operator cannot be combined with another condition using the OR logical operator. 924 925 A WHERE condition cannot use the IN comparison condition to compare a column marked with the (+) operator with an expression. 926 927 A WHERE condition cannot compare any column marked with the (+) operator with a subquery. 928 929 -- example with WHERE 930 SELECT d.department_name, sum(e.salary) as total_salary 931 FROM departments d, employees e 932 WHERE e.department_id(+) = d.department_id 933 group by department_name 934 935 -- example of left correlation in select 936 SELECT d.department_name, ( 937 SELECT SUM(e.salary) 938 FROM employees e 939 WHERE e.department_id(+) = d.department_id) AS total_salary 940 FROM departments d; 941 942 -- example of left correlation in from 943 SELECT d.department_name, t.total_salary 944 FROM departments d, ( 945 SELECT SUM(e.salary) AS total_salary 946 FROM employees e 947 WHERE e.department_id(+) = d.department_id 948 ) t 949 """ 950 951 from sqlglot.optimizer.scope import traverse_scope 952 from sqlglot.optimizer.normalize import normalize, normalized 953 from collections import defaultdict 954 955 # we go in reverse to check the main query for left correlation 956 for scope in reversed(traverse_scope(expression)): 957 query = scope.expression 958 959 where: exp.Expr | None = query.args.get("where") 960 joins: list[exp.Join] = query.args.get("joins", []) 961 962 if not where or not any(c.args.get("join_mark") for c in where.find_all(exp.Column)): 963 continue 964 965 # knockout: we do not support left correlation (see point 2) 966 assert not scope.is_correlated_subquery, "Correlated queries are not supported" 967 968 # make sure we have AND of ORs to have clear join terms 969 where = normalize(where.this) 970 assert normalized(where), "Cannot normalize JOIN predicates" 971 # dict of {name: list of join AND conditions} 972 joins_ons: defaultdict[str, list[exp.Expr]] = defaultdict(list) 973 for cond in [where] if not isinstance(where, exp.And) else where.flatten(): 974 join_cols = [col for col in cond.find_all(exp.Column) if col.args.get("join_mark")] 975 976 left_join_table = set(col.table for col in join_cols) 977 if not left_join_table: 978 continue 979 980 assert not (len(left_join_table) > 1), ( 981 "Cannot combine JOIN predicates from different tables" 982 ) 983 984 for col in join_cols: 985 col.set("join_mark", False) 986 987 joins_ons[left_join_table.pop()].append(cond) 988 989 old_joins = {join.alias_or_name: join for join in joins} 990 new_joins: dict[str, exp.Join] = {} 991 query_from = query.args["from_"] 992 993 for table, predicates in joins_ons.items(): 994 join_what = old_joins.get(table, query_from).this.copy() 995 new_joins[join_what.alias_or_name] = exp.Join( 996 this=join_what, on=exp.and_(*predicates), kind="LEFT" 997 ) 998 999 for p in predicates: 1000 while isinstance(p.parent, exp.Paren): 1001 p.parent.replace(p) 1002 1003 parent = p.parent 1004 p.pop() 1005 if isinstance(parent, exp.Binary): 1006 left = parent.args.get("this") 1007 parent.replace(parent.right if left is None else left) 1008 elif isinstance(parent, exp.Where): 1009 parent.pop() 1010 1011 if query_from.alias_or_name in new_joins: 1012 only_old_joins: set[str] = old_joins.keys() - new_joins.keys() 1013 assert len(only_old_joins) >= 1, ( 1014 "Cannot determine which table to use in the new FROM clause" 1015 ) 1016 1017 new_from_name = list[str](only_old_joins)[0] 1018 query.set("from_", exp.From(this=old_joins[new_from_name].this)) 1019 1020 if new_joins: 1021 for n, j in old_joins.items(): # preserve any other joins 1022 if n not in new_joins and n != query.args["from_"].name: 1023 if not j.kind: 1024 j.set("kind", "CROSS") 1025 new_joins[n] = j 1026 query.set("joins", list(new_joins.values())) 1027 1028 return expression 1029 1030 1031def any_to_exists(expression: exp.Expr) -> exp.Expr: 1032 """ 1033 Transform ANY operator to Spark's EXISTS 1034 1035 For example, 1036 - Postgres: SELECT * FROM tbl WHERE 5 > ANY(tbl.col) 1037 - Spark: SELECT * FROM tbl WHERE EXISTS(tbl.col, x -> x < 5) 1038 1039 Both ANY and EXISTS accept queries but currently only array expressions are supported for this 1040 transformation 1041 """ 1042 if isinstance(expression, exp.Select): 1043 for any_expr in expression.find_all(exp.Any): 1044 this: exp.Expr = any_expr.this 1045 if isinstance(this, exp.Query) or isinstance(any_expr.parent, (exp.Like, exp.ILike)): 1046 continue 1047 1048 binop = any_expr.parent 1049 if isinstance(binop, exp.Binary): 1050 lambda_arg = exp.to_identifier("x") 1051 any_expr.replace(lambda_arg) 1052 lambda_expr = exp.Lambda(this=binop.copy(), expressions=[lambda_arg]) 1053 binop.replace(exp.Exists(this=this.unnest(), expression=lambda_expr)) 1054 1055 return expression 1056 1057 1058def eliminate_window_clause(expression: exp.Expr) -> exp.Expr: 1059 """Eliminates the `WINDOW` query clause by inling each named window.""" 1060 windows: list[exp.Expr] | None = expression.args.get("windows") 1061 if isinstance(expression, exp.Select) and windows is not None: 1062 from sqlglot.optimizer.scope import find_all_in_scope 1063 1064 expression.set("windows", None) 1065 1066 window_expression: dict[str, exp.Expr] = {} 1067 1068 def _inline_inherited_window(window: exp.Expr) -> None: 1069 inherited_window = window_expression.get(window.alias.lower()) 1070 if not inherited_window: 1071 return 1072 1073 window.set("alias", None) 1074 for key in ("partition_by", "order", "spec"): 1075 arg: exp.Expr | None = inherited_window.args.get(key) 1076 if arg is not None: 1077 window.set(key, arg.copy()) 1078 1079 for window in windows: 1080 _inline_inherited_window(window) 1081 window_expression[window.name.lower()] = window 1082 1083 for window in find_all_in_scope(expression, exp.Window): 1084 _inline_inherited_window(window) 1085 1086 return expression 1087 1088 1089def inherit_struct_field_names(expression: exp.Expr) -> exp.Expr: 1090 """ 1091 Inherit field names from the first struct in an array. 1092 1093 BigQuery supports implicitly inheriting names from the first STRUCT in an array: 1094 1095 Example: 1096 ARRAY[ 1097 STRUCT('Alice' AS name, 85 AS score), -- defines names 1098 STRUCT('Bob', 92), -- inherits names 1099 STRUCT('Diana', 95) -- inherits names 1100 ] 1101 1102 This transformation makes the field names explicit on all structs by adding 1103 PropertyEQ nodes, in order to facilitate transpilation to other dialects. 1104 1105 Args: 1106 expression: The expression tree to transform 1107 1108 Returns: 1109 The modified expression with field names inherited in all structs 1110 """ 1111 if ( 1112 isinstance(expression, exp.Array) 1113 and expression.args.get("struct_name_inheritance") 1114 and isinstance(first_item := seq_get(expression.expressions, 0), exp.Struct) 1115 and all(isinstance(fld, exp.PropertyEQ) for fld in first_item.expressions) 1116 ): 1117 field_names: list[exp.Identifier] = [fld.this for fld in first_item.expressions] 1118 1119 # Apply field names to subsequent structs that don't have them 1120 for struct in expression.expressions[1:]: 1121 if not isinstance(struct, exp.Struct) or len(struct.expressions) != len(field_names): 1122 continue 1123 1124 # Convert unnamed expressions to PropertyEQ with inherited names 1125 new_expressions: list[exp.PropertyEQ] = [] 1126 for i, expr in enumerate(struct.expressions): 1127 if not isinstance(expr, exp.PropertyEQ): 1128 # Create PropertyEQ: field_name := value, preserving the type from the inner expression 1129 property_eq = exp.PropertyEQ( 1130 this=field_names[i].copy(), 1131 expression=expr, 1132 ) 1133 property_eq.type = expr.type 1134 new_expressions.append(property_eq) 1135 else: 1136 new_expressions.append(expr) 1137 1138 struct.set("expressions", new_expressions) 1139 1140 return expression
16class SqlHandler(t.Protocol): 17 def __call__(self, expression: exp.Expr, *args: t.Any, **kwargs: t.Any) -> str: ...
Base class for protocol classes.
Protocol classes are defined as::
class Proto(Protocol):
def meth(self) -> int:
...
Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing), for example::
class C:
def meth(self) -> int:
return 0
def func(x: Proto) -> int:
return x.meth()
func(C()) # Passes static type check
See PEP 544 for details. Protocol classes decorated with @typing.runtime_checkable act as simple-minded runtime protocols that check only the presence of given attributes, ignoring their type signatures. Protocol classes can be generic, they are defined as::
class GenProto(Protocol[T]):
def meth(self) -> T:
...
1431def _no_init_or_replace_init(self, *args, **kwargs): 1432 cls = type(self) 1433 1434 if cls._is_protocol: 1435 raise TypeError('Protocols cannot be instantiated') 1436 1437 # Already using a custom `__init__`. No need to calculate correct 1438 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1439 if cls.__init__ is not _no_init_or_replace_init: 1440 return 1441 1442 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1443 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1444 # searches for a proper new `__init__` in the MRO. The new `__init__` 1445 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1446 # instantiation of the protocol subclass will thus use the new 1447 # `__init__` and no longer call `_no_init_or_replace_init`. 1448 for base in cls.__mro__: 1449 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1450 if init is not _no_init_or_replace_init: 1451 cls.__init__ = init 1452 break 1453 else: 1454 # should not happen 1455 cls.__init__ = object.__init__ 1456 1457 cls.__init__(self, *args, **kwargs)
20def preprocess( 21 transforms: list[t.Callable[[exp.Expr], exp.Expr]], 22 generator: t.Callable[[Generator, exp.Expr], str] | None = None, 23) -> t.Callable[[Generator, exp.Expr], str]: 24 """ 25 Creates a new transform by chaining a sequence of transformations and converts the resulting 26 expression to SQL, using either the "_sql" method corresponding to the resulting expression, 27 or the appropriate `Generator.TRANSFORMS` function (when applicable -- see below). 28 29 Args: 30 transforms: sequence of transform functions. These will be called in order. 31 32 Returns: 33 Function that can be used as a generator transform. 34 """ 35 36 def _to_sql(self: Generator, expression: exp.Expr) -> str: 37 expression_type = type(expression) 38 39 try: 40 expression = transforms[0](expression) 41 for transform in transforms[1:]: 42 expression = transform(expression) 43 except UnsupportedError as unsupported_error: 44 self.unsupported(str(unsupported_error)) 45 46 if generator: 47 return generator(self, expression) 48 49 _sql_handler: SqlHandler | None = getattr(self, expression.key + "_sql", None) 50 if _sql_handler: 51 return _sql_handler(expression) 52 53 transforms_handler = self.TRANSFORMS.get(type(expression)) 54 if transforms_handler: 55 if expression_type is type(expression): 56 if isinstance(expression, exp.Func): 57 return self.function_fallback_sql(expression) 58 59 # Ensures we don't enter an infinite loop. This can happen when the original expression 60 # has the same type as the final expression and there's no _sql method available for it, 61 # because then it'd re-enter _to_sql. 62 raise ValueError( 63 f"Expr type {expression.__class__.__name__} requires a _sql method in order to be transformed." 64 ) 65 66 return transforms_handler(self, expression) 67 68 raise ValueError(f"Unsupported expression type {expression.__class__.__name__}.") 69 70 return _to_sql
Creates a new transform by chaining a sequence of transformations and converts the resulting
expression to SQL, using either the "_sql" method corresponding to the resulting expression,
or the appropriate Generator.TRANSFORMS function (when applicable -- see below).
Arguments:
- transforms: sequence of transform functions. These will be called in order.
Returns:
Function that can be used as a generator transform.
73def unnest_generate_date_array_using_recursive_cte(expression: exp.Expr) -> exp.Expr: 74 if isinstance(expression, exp.Select): 75 count = 0 76 recursive_ctes: list[exp.Expr] = [] 77 78 for unnest in expression.find_all(exp.Unnest): 79 if ( 80 not isinstance(unnest.parent, (exp.From, exp.Join)) 81 or len(unnest.expressions) != 1 82 or not isinstance(unnest.expressions[0], exp.GenerateDateArray) 83 ): 84 continue 85 86 generate_date_array = unnest.expressions[0] 87 start: exp.Expr | None = generate_date_array.args.get("start") 88 end: exp.Expr | None = generate_date_array.args.get("end") 89 step: exp.Expr | None = generate_date_array.args.get("step") 90 91 if not start or not end or not isinstance(step, exp.Interval): 92 continue 93 94 alias: exp.TableAlias | None = unnest.args.get("alias") 95 column_name: str = ( 96 alias.columns[0] if isinstance(alias, exp.TableAlias) else "date_value" 97 ) 98 99 start = exp.cast(start, "date") 100 date_add = exp.func( 101 "date_add", column_name, exp.Literal.number(step.name), step.args.get("unit") 102 ) 103 cast_date_add = exp.cast(date_add, "date") 104 105 cte_name = "_generated_dates" + (f"_{count}" if count else "") 106 107 base_query = exp.select(start.as_(column_name)) 108 recursive_query = ( 109 exp.select(cast_date_add) 110 .from_(cte_name) 111 .where(cast_date_add <= exp.cast(end, "date")) 112 ) 113 cte_query = base_query.union(recursive_query, distinct=False) 114 115 generate_dates_query = exp.select(column_name).from_(cte_name) 116 unnest.replace(generate_dates_query.subquery(cte_name)) 117 118 recursive_ctes.append( 119 exp.alias_(exp.CTE(this=cte_query), cte_name, table=[column_name]) 120 ) 121 count += 1 122 123 if recursive_ctes: 124 with_expression: exp.With = expression.args.get("with_") or exp.With() 125 with_expression.set("recursive", True) 126 with_expression.set("expressions", [*recursive_ctes, *with_expression.expressions]) 127 expression.set("with_", with_expression) 128 129 return expression
132def unnest_generate_series(expression: exp.Expr) -> exp.Expr: 133 """Unnests GENERATE_SERIES or SEQUENCE table references.""" 134 this = expression.this 135 if isinstance(expression, exp.Table) and isinstance(this, exp.GenerateSeries): 136 unnest = exp.Unnest(expressions=[this]) 137 if expression.alias: 138 return exp.alias_(unnest, alias="_u", table=[expression.alias], copy=False) 139 140 return unnest 141 142 return expression
Unnests GENERATE_SERIES or SEQUENCE table references.
145def eliminate_distinct_on(expression: exp.Expr) -> exp.Expr: 146 """ 147 Convert SELECT DISTINCT ON statements to a subquery with a window function. 148 149 This is useful for dialects that don't support SELECT DISTINCT ON but support window functions. 150 151 Args: 152 expression: the expression that will be transformed. 153 154 Returns: 155 The transformed expression. 156 """ 157 if ( 158 isinstance(expression, exp.Select) 159 and expression.args.get("distinct") 160 and isinstance(expression.args["distinct"].args.get("on"), exp.Tuple) 161 ): 162 row_number_window_alias = find_new_name(expression.named_selects, "_row_number") 163 164 distinct_cols = expression.args["distinct"].pop().args["on"].expressions 165 window = exp.Window(this=exp.RowNumber(), partition_by=distinct_cols) 166 167 order: exp.Order | None = expression.args.get("order") 168 if order: 169 window.set("order", order.pop()) 170 else: 171 window.set("order", exp.Order(expressions=[c.copy() for c in distinct_cols])) 172 173 expression.select(exp.alias_(window, row_number_window_alias), copy=False) 174 175 # We add aliases to the projections so that we can safely reference them in the outer query 176 new_selects: list[exp.Expr] = [] 177 taken_names = {row_number_window_alias} 178 for select in expression.selects[:-1]: 179 if select.is_star: 180 new_selects = [exp.Star()] 181 break 182 183 if not isinstance(select, exp.Alias): 184 alias = find_new_name(taken_names, select.output_name or "_col") 185 quoted: bool | None = ( 186 select.this.args.get("quoted") if isinstance(select, exp.Column) else None 187 ) 188 select = select.replace(exp.alias_(select, alias, quoted=quoted)) 189 190 taken_names.add(select.output_name) 191 new_selects.append(select.args["alias"]) 192 193 return ( 194 exp.select(*new_selects, copy=False) 195 .from_(expression.subquery("_t", copy=False), copy=False) 196 .where(exp.column(row_number_window_alias).eq(1), copy=False) 197 ) 198 199 return expression
Convert SELECT DISTINCT ON statements to a subquery with a window function.
This is useful for dialects that don't support SELECT DISTINCT ON but support window functions.
Arguments:
- expression: the expression that will be transformed.
Returns:
The transformed expression.
202def eliminate_qualify(expression: exp.Expr) -> exp.Expr: 203 """ 204 Convert SELECT statements that contain the QUALIFY clause into subqueries, filtered equivalently. 205 206 The idea behind this transformation can be seen in Snowflake's documentation for QUALIFY: 207 https://docs.snowflake.com/en/sql-reference/constructs/qualify 208 209 Some dialects don't support window functions in the WHERE clause, so we need to include them as 210 projections in the subquery, in order to refer to them in the outer filter using aliases. Also, 211 if a column is referenced in the QUALIFY clause but is not selected, we need to include it too, 212 otherwise we won't be able to refer to it in the outer query's WHERE clause. Finally, if a 213 newly aliased projection is referenced in the QUALIFY clause, it will be replaced by the 214 corresponding expression to avoid creating invalid column references. 215 """ 216 if isinstance(expression, exp.Select) and expression.args.get("qualify"): 217 parent = expression.parent 218 if isinstance(parent, exp.Insert) and isinstance(parent.parent, exp.MultitableInserts): 219 # A FROM-first multi-table INSERT branch shares its parent's FROM, so it can't be wrapped 220 raise UnsupportedError( 221 "QUALIFY is not supported in a FROM-first multi-table INSERT branch" 222 ) 223 224 taken = set(expression.named_selects) 225 for select in expression.selects: 226 if not select.alias_or_name: 227 alias = find_new_name(taken, "_c") 228 select.replace(exp.alias_(select, alias)) 229 taken.add(alias) 230 231 def _select_alias_or_name(select: exp.Expr) -> str | exp.Column: 232 alias_or_name = select.alias_or_name 233 identifier = select.args.get("alias") or select.this 234 if isinstance(identifier, exp.Identifier): 235 return exp.column(alias_or_name, quoted=identifier.args.get("quoted")) 236 return alias_or_name 237 238 outer_selects = exp.select(*map(_select_alias_or_name, expression.selects)) 239 qualify_filters: exp.Expr = expression.args["qualify"].pop().this 240 expression_by_alias: dict[str, exp.Expr] = { 241 select.alias: select.this 242 for select in expression.selects 243 if isinstance(select, exp.Alias) 244 } 245 246 select_candidates = (exp.Window,) if expression.is_star else (exp.Window, exp.Column) 247 for select_candidate in list(qualify_filters.find_all(*select_candidates)): 248 if isinstance(select_candidate, exp.Window): 249 if expression_by_alias: 250 for column in select_candidate.find_all(exp.Column): 251 expr = expression_by_alias.get(column.name) 252 if expr: 253 column.replace(expr) 254 255 alias = find_new_name(expression.named_selects, "_w") 256 expression.select(exp.alias_(select_candidate, alias), copy=False) 257 column = exp.column(alias) 258 259 if isinstance(select_candidate.parent, exp.Qualify): 260 qualify_filters = column 261 else: 262 select_candidate.replace(column) 263 elif select_candidate.name not in expression.named_selects and not ( 264 select_candidate.find_ancestor(exp.Window) 265 ): 266 # A column that is only read by a window function is computed by the window 267 # itself, so projecting it in the subquery is redundant and can even produce 268 # invalid SQL, e.g. when the query is grouped 269 expression.select(select_candidate.copy(), copy=False) 270 271 return outer_selects.from_(expression.subquery(alias="_t", copy=False), copy=False).where( 272 qualify_filters, copy=False 273 ) 274 275 return expression
Convert SELECT statements that contain the QUALIFY clause into subqueries, filtered equivalently.
The idea behind this transformation can be seen in Snowflake's documentation for QUALIFY: https://docs.snowflake.com/en/sql-reference/constructs/qualify
Some dialects don't support window functions in the WHERE clause, so we need to include them as projections in the subquery, in order to refer to them in the outer filter using aliases. Also, if a column is referenced in the QUALIFY clause but is not selected, we need to include it too, otherwise we won't be able to refer to it in the outer query's WHERE clause. Finally, if a newly aliased projection is referenced in the QUALIFY clause, it will be replaced by the corresponding expression to avoid creating invalid column references.
278def remove_precision_parameterized_types(expression: exp.Expr) -> exp.Expr: 279 """ 280 Some dialects only allow the precision for parameterized types to be defined in the DDL and not in 281 other expressions. This transforms removes the precision from parameterized types in expressions. 282 """ 283 for node in expression.find_all(exp.DataType): 284 node.set( 285 "expressions", [e for e in node.expressions if not isinstance(e, exp.DataTypeParam)] 286 ) 287 288 return expression
Some dialects only allow the precision for parameterized types to be defined in the DDL and not in other expressions. This transforms removes the precision from parameterized types in expressions.
291def unqualify_unnest(expression: exp.Expr) -> exp.Expr: 292 """Remove references to unnest table aliases, added by the optimizer's qualify_columns step.""" 293 from sqlglot.optimizer.scope import find_all_in_scope 294 295 if isinstance(expression, exp.Select): 296 unnest_aliases = { 297 unnest.alias 298 for unnest in find_all_in_scope(expression, exp.Unnest) 299 if isinstance(unnest.parent, (exp.From, exp.Join)) 300 } 301 if unnest_aliases: 302 for column in expression.find_all(exp.Column): 303 leftmost_part = column.parts[0] 304 if leftmost_part.arg_key != "this" and leftmost_part.this in unnest_aliases: 305 leftmost_part.pop() 306 307 return expression
Remove references to unnest table aliases, added by the optimizer's qualify_columns step.
310def unnest_to_explode( 311 expression: exp.Expr, 312 unnest_using_arrays_zip: bool = True, 313) -> exp.Expr: 314 """Convert cross join unnest into lateral view explode.""" 315 316 def _unnest_zip_exprs( 317 u: exp.Unnest, unnest_exprs: list[exp.Expr], has_multi_expr: bool 318 ) -> list[exp.Expr]: 319 if has_multi_expr: 320 if not unnest_using_arrays_zip: 321 raise UnsupportedError("Cannot transpile UNNEST with multiple input arrays") 322 323 # Use INLINE(ARRAYS_ZIP(...)) for multiple expressions 324 zip_exprs: list[exp.Expr] = [exp.Anonymous(this="ARRAYS_ZIP", expressions=unnest_exprs)] 325 u.set("expressions", zip_exprs) 326 return zip_exprs 327 return unnest_exprs 328 329 def _udtf_type(u: exp.Unnest, has_multi_expr: bool) -> type[exp.Func]: 330 if u.args.get("offset"): 331 return exp.Posexplode 332 return exp.Inline if has_multi_expr else exp.Explode 333 334 if isinstance(expression, exp.Select): 335 from_ = expression.args.get("from_") 336 337 if from_ and isinstance(from_.this, exp.Unnest): 338 unnest: exp.Unnest = from_.this 339 alias: exp.TableAlias | None = unnest.args.get("alias") 340 exprs: list[exp.Expr] = unnest.expressions 341 has_multi_expr = len(exprs) > 1 342 this, *_ = _unnest_zip_exprs(unnest, exprs, has_multi_expr) 343 344 columns: list[exp.Identifier] = alias.columns if alias else [] 345 offset: exp.Expr | None = unnest.args.get("offset") 346 if offset: 347 columns.insert( 348 0, offset if isinstance(offset, exp.Identifier) else exp.to_identifier("pos") 349 ) 350 351 unnest.replace( 352 exp.Table( 353 this=_udtf_type(unnest, has_multi_expr)(this=this), 354 alias=exp.TableAlias(this=alias.this, columns=columns) if alias else None, 355 ) 356 ) 357 358 joins: list[exp.Join] = expression.args.get("joins") or [] 359 for join in list(joins): 360 join_expr = join.this 361 362 is_lateral = isinstance(join_expr, exp.Lateral) 363 364 unnest = join_expr.this if is_lateral else join_expr 365 366 if isinstance(unnest, exp.Unnest): 367 if is_lateral: 368 alias = join_expr.args.get("alias") 369 else: 370 alias = unnest.args.get("alias") 371 372 if alias is None: 373 raise UnsupportedError( 374 "CROSS JOIN UNNEST to LATERAL VIEW EXPLODE transformation requires an alias" 375 ) 376 377 exprs = unnest.expressions 378 # The number of unnest.expressions will be changed by _unnest_zip_exprs, we need to record it here 379 has_multi_expr = len(exprs) > 1 380 exprs = _unnest_zip_exprs(unnest, exprs, has_multi_expr) 381 382 joins.remove(join) 383 384 alias_cols: list[exp.Identifier] = alias.columns 385 386 # # Handle UNNEST to LATERAL VIEW EXPLODE: Exception is raised when there are 0 or > 2 aliases 387 # Spark LATERAL VIEW EXPLODE requires single alias for array/struct and two for Map type column unlike unnest in trino/presto which can take an arbitrary amount. 388 # Refs: https://spark.apache.org/docs/latest/sql-ref-syntax-qry-select-lateral-view.html 389 390 if not has_multi_expr and len(alias_cols) not in (1, 2): 391 raise UnsupportedError( 392 "CROSS JOIN UNNEST to LATERAL VIEW EXPLODE transformation requires explicit column aliases" 393 ) 394 395 offset = unnest.args.get("offset") 396 if offset: 397 alias_cols.insert( 398 0, 399 offset if isinstance(offset, exp.Identifier) else exp.to_identifier("pos"), 400 ) 401 402 for e, column in zip(exprs, alias_cols): 403 expression.append( 404 "laterals", 405 exp.Lateral( 406 this=_udtf_type(unnest, has_multi_expr)(this=e), 407 view=True, 408 alias=exp.TableAlias(this=alias.this, columns=alias_cols), 409 ), 410 ) 411 412 return expression
Convert cross join unnest into lateral view explode.
415def explode_projection_to_unnest( 416 index_offset: int = 0, 417 unnest_map: bool = False, 418) -> t.Callable[[exp.Expr], exp.Expr]: 419 """Convert explode/posexplode projections into unnests.""" 420 421 def _explode_projection_to_unnest(expression: exp.Expr) -> exp.Expr: 422 if isinstance(expression, exp.Select): 423 from sqlglot.optimizer.scope import Scope 424 425 taken_select_names = set(expression.named_selects) 426 taken_source_names = {name for name, _ in Scope(expression).references} 427 428 def new_name(names: set[str], name: str) -> str: 429 name = find_new_name(names, name) 430 names.add(name) 431 return name 432 433 arrays: list[exp.Condition] = [] 434 series_alias = new_name(taken_select_names, "pos") 435 series = exp.alias_( 436 exp.Unnest( 437 expressions=[exp.GenerateSeries(start=exp.Literal.number(index_offset))] 438 ), 439 new_name(taken_source_names, "_u"), 440 table=[series_alias], 441 ) 442 443 # we use list here because expression.selects is mutated inside the loop 444 for select in list(expression.selects): 445 explode = select.find(exp.Explode) 446 447 if explode: 448 if ( 449 unnest_map 450 and type(explode) is exp.Explode 451 and explode.this.is_type(exp.DType.MAP) 452 and (select is explode or isinstance(select, exp.Aliases)) 453 ): 454 map_key_alias: t.Any 455 map_value_alias: t.Any 456 if isinstance(select, exp.Aliases): 457 map_key_alias, map_value_alias = select.aliases 458 else: 459 map_key_alias = new_name(taken_select_names, "key") 460 map_value_alias = new_name(taken_select_names, "value") 461 map_unnest_source = new_name(taken_source_names, "_u") 462 463 map_key_select = select.replace( 464 exp.column(map_key_alias, table=map_unnest_source).as_(map_key_alias) 465 ) 466 467 expressions = expression.expressions 468 expressions.insert( 469 expressions.index(map_key_select) + 1, 470 exp.column(map_value_alias, table=map_unnest_source).as_( 471 map_value_alias 472 ), 473 ) 474 expression.set("expressions", expressions) 475 476 unnest = exp.alias_( 477 exp.Unnest(expressions=[explode.this.copy()]), 478 map_unnest_source, 479 table=[map_key_alias, map_value_alias], 480 ) 481 if expression.args.get("from_"): 482 expression.join(unnest, copy=False, join_type="CROSS") 483 else: 484 expression.from_(unnest, copy=False) 485 486 continue 487 488 pos_alias: t.Any = "" 489 explode_alias: t.Any = "" 490 491 if isinstance(select, exp.Alias): 492 explode_alias = select.args["alias"] 493 alias: exp.Expr = select 494 elif isinstance(select, exp.Aliases): 495 pos_alias = select.aliases[0] 496 explode_alias = select.aliases[1] 497 alias = select.replace(exp.alias_(select.this, "", copy=False)) 498 else: 499 alias = select.replace(exp.alias_(select, "")) 500 explode = alias.find(exp.Explode) 501 assert explode 502 503 is_posexplode = isinstance(explode, exp.Posexplode) 504 explode_arg = explode.this 505 506 if isinstance(explode, exp.ExplodeOuter): 507 bracket = explode_arg[0] 508 bracket.set("safe", True) 509 bracket.set("offset", True) 510 explode_arg = exp.func( 511 "IF", 512 exp.func( 513 "ARRAY_SIZE", exp.func("COALESCE", explode_arg, exp.Array()) 514 ).eq(0), 515 exp.array(bracket, copy=False), 516 explode_arg, 517 ) 518 519 # This ensures that we won't use [POS]EXPLODE's argument as a new selection 520 if isinstance(explode_arg, exp.Column): 521 taken_select_names.add(explode_arg.output_name) 522 523 unnest_source_alias = new_name(taken_source_names, "_u") 524 525 if not explode_alias: 526 explode_alias = new_name(taken_select_names, "col") 527 528 if is_posexplode: 529 pos_alias = new_name(taken_select_names, "pos") 530 531 if not pos_alias: 532 pos_alias = new_name(taken_select_names, "pos") 533 534 alias.set("alias", exp.to_identifier(explode_alias)) 535 536 series_table_alias = series.args["alias"].this 537 column = exp.If( 538 this=exp.column(series_alias, table=series_table_alias).eq( 539 exp.column(pos_alias, table=unnest_source_alias) 540 ), 541 true=exp.column(explode_alias, table=unnest_source_alias), 542 ) 543 544 explode.replace(column) 545 546 if is_posexplode: 547 expressions = expression.expressions 548 expressions.insert( 549 expressions.index(alias) + 1, 550 exp.If( 551 this=exp.column(series_alias, table=series_table_alias).eq( 552 exp.column(pos_alias, table=unnest_source_alias) 553 ), 554 true=exp.column(pos_alias, table=unnest_source_alias), 555 ).as_(pos_alias), 556 ) 557 expression.set("expressions", expressions) 558 559 if not arrays: 560 if expression.args.get("from_"): 561 expression.join(series, copy=False, join_type="CROSS") 562 else: 563 expression.from_(series, copy=False) 564 565 size: exp.Condition = exp.ArraySize(this=explode_arg.copy()) 566 arrays.append(size) 567 568 # trino doesn't support left join unnest with on conditions 569 # if it did, this would be much simpler 570 expression.join( 571 exp.alias_( 572 exp.Unnest( 573 expressions=[explode_arg.copy()], 574 offset=exp.to_identifier(pos_alias), 575 ), 576 unnest_source_alias, 577 table=[explode_alias], 578 ), 579 join_type="CROSS", 580 copy=False, 581 ) 582 583 if index_offset != 1: 584 size = size - 1 585 586 expression.where( 587 exp.column(series_alias, table=series_table_alias) 588 .eq(exp.column(pos_alias, table=unnest_source_alias)) 589 .or_( 590 (exp.column(series_alias, table=series_table_alias) > size).and_( 591 exp.column(pos_alias, table=unnest_source_alias).eq(size) 592 ) 593 ), 594 copy=False, 595 ) 596 597 if arrays: 598 end: exp.Condition = exp.Greatest(this=arrays[0], expressions=arrays[1:]) 599 600 if index_offset != 1: 601 end = end - (1 - index_offset) 602 series.expressions[0].set("end", end) 603 604 return expression 605 606 return _explode_projection_to_unnest
Convert explode/posexplode projections into unnests.
609def add_within_group_for_percentiles(expression: exp.Expr) -> exp.Expr: 610 """Transforms percentiles by adding a WITHIN GROUP clause to them.""" 611 if ( 612 isinstance(expression, exp.PERCENTILES) 613 and not isinstance(expression.parent, exp.WithinGroup) 614 and expression.expression 615 ): 616 column = expression.this.pop() 617 expression.set("this", expression.expression.pop()) 618 order = exp.Order(expressions=[exp.Ordered(this=column)]) 619 expression = exp.WithinGroup(this=expression, expression=order) 620 621 return expression
Transforms percentiles by adding a WITHIN GROUP clause to them.
624def remove_within_group_for_percentiles(expression: exp.Expr) -> exp.Expr: 625 """Transforms percentiles by getting rid of their corresponding WITHIN GROUP clause.""" 626 if ( 627 isinstance(expression, exp.WithinGroup) 628 and isinstance(expression.this, exp.PERCENTILES) 629 and isinstance(expression.expression, exp.Order) 630 ): 631 quantile = expression.this.this 632 input_value = t.cast(exp.Ordered, expression.find(exp.Ordered)).this 633 return expression.replace(exp.ApproxQuantile(this=input_value, quantile=quantile)) 634 635 return expression
Transforms percentiles by getting rid of their corresponding WITHIN GROUP clause.
638def add_recursive_cte_column_names(expression: exp.Expr) -> exp.Expr: 639 """Uses projection output names in recursive CTE definitions to define the CTEs' columns.""" 640 if isinstance(expression, exp.With) and expression.recursive: 641 next_name = name_sequence("_c_") 642 643 for cte in expression.expressions: 644 if not cte.args["alias"].columns: 645 query = cte.this 646 if isinstance(query, exp.SetOperation): 647 query = query.this 648 649 cte.args["alias"].set( 650 "columns", 651 [exp.to_identifier(s.alias_or_name or next_name()) for s in query.selects], 652 ) 653 654 return expression
Uses projection output names in recursive CTE definitions to define the CTEs' columns.
657def epoch_cast_to_ts(expression: exp.Expr) -> exp.Expr: 658 """Replace 'epoch' in casts by the equivalent date literal.""" 659 if ( 660 isinstance(expression, (exp.Cast, exp.TryCast)) 661 and expression.name.lower() == "epoch" 662 and expression.to.this in exp.DataType.TEMPORAL_TYPES 663 ): 664 expression.this.replace(exp.Literal.string("1970-01-01 00:00:00")) 665 666 return expression
Replace 'epoch' in casts by the equivalent date literal.
669def eliminate_semi_and_anti_joins(expression: exp.Expr) -> exp.Expr: 670 """Convert SEMI and ANTI joins into equivalent forms that use EXIST instead.""" 671 if isinstance(expression, exp.Select): 672 for join in list[exp.Join](expression.args.get("joins") or []): 673 on: exp.Expr | None = join.args.get("on") 674 if on and join.kind in ("SEMI", "ANTI"): 675 subquery = exp.select("1").from_(join.this).where(on) 676 exists: exp.Exists | exp.Not = exp.Exists(this=subquery) 677 if join.kind == "ANTI": 678 exists = exists.not_(copy=False) 679 680 join.pop() 681 expression.where(exists, copy=False) 682 683 return expression
Convert SEMI and ANTI joins into equivalent forms that use EXIST instead.
686def eliminate_full_outer_join(expression: exp.Expr) -> exp.Expr: 687 """ 688 Converts a query with a FULL OUTER join to a union of identical queries that 689 use LEFT/RIGHT OUTER joins instead. This transformation currently only works 690 for queries that have a single FULL OUTER join. 691 """ 692 if isinstance(expression, exp.Select): 693 full_outer_joins: list[tuple[int, exp.Join]] = [ 694 (index, join) 695 for index, join in enumerate[exp.Join](expression.args.get("joins") or []) 696 if join.side == "FULL" 697 ] 698 699 if len(full_outer_joins) == 1: 700 expression_copy = expression.copy() 701 index, full_outer_join = full_outer_joins[0] 702 703 tables = (expression.args["from_"].alias_or_name, full_outer_join.alias_or_name) 704 join_conditions = full_outer_join.args.get("on") or exp.and_( 705 *[ 706 exp.column(col, tables[0]).eq(exp.column(col, tables[1])) 707 for col in t.cast(list[exp.Identifier], full_outer_join.args.get("using")) 708 ] 709 ) 710 711 full_outer_join.set("side", "left") 712 anti_join_clause = ( 713 exp.select("1").from_(expression.args["from_"]).where(join_conditions) 714 ) 715 expression_copy.args["joins"][index].set("side", "right") 716 expression_copy = expression_copy.where(exp.Exists(this=anti_join_clause).not_()) 717 718 union = exp.union(expression, expression_copy, copy=False, distinct=False) 719 for arg in ("with_", "order", "limit", "offset"): 720 value = expression.args.get(arg) 721 if value: 722 expression.set(arg, None) 723 expression_copy.set(arg, None) 724 union.set(arg, value) 725 return union 726 727 return expression
Converts a query with a FULL OUTER join to a union of identical queries that use LEFT/RIGHT OUTER joins instead. This transformation currently only works for queries that have a single FULL OUTER join.
730def move_ctes_to_top_level(expression: E) -> E: 731 """ 732 Some dialects (e.g. Hive, T-SQL, Spark prior to version 3) only allow CTEs to be 733 defined at the top-level, so for example queries like: 734 735 SELECT * FROM (WITH t(c) AS (SELECT 1) SELECT * FROM t) AS subq 736 737 are invalid in those dialects. This transformation can be used to ensure all CTEs are 738 moved to the top level so that the final SQL code is valid from a syntax standpoint. 739 740 TODO: handle name clashes whilst moving CTEs (it can get quite tricky & costly). 741 """ 742 top_level_with: exp.With | None = expression.args.get("with_") 743 for inner_with in expression.find_all(exp.With): 744 if inner_with.parent is expression: 745 continue 746 747 if not top_level_with: 748 top_level_with = inner_with.pop() 749 expression.set("with_", top_level_with) 750 else: 751 if inner_with.recursive: 752 top_level_with.set("recursive", True) 753 754 parent_cte = inner_with.find_ancestor(exp.CTE) 755 inner_with.pop() 756 757 if parent_cte: 758 i = top_level_with.expressions.index(parent_cte) 759 top_level_with.expressions[i:i] = inner_with.expressions 760 top_level_with.set("expressions", top_level_with.expressions) 761 else: 762 top_level_with.set( 763 "expressions", top_level_with.expressions + inner_with.expressions 764 ) 765 766 return expression
Some dialects (e.g. Hive, T-SQL, Spark prior to version 3) only allow CTEs to be defined at the top-level, so for example queries like:
SELECT * FROM (WITH t(c) AS (SELECT 1) SELECT * FROM t) AS subq
are invalid in those dialects. This transformation can be used to ensure all CTEs are moved to the top level so that the final SQL code is valid from a syntax standpoint.
TODO: handle name clashes whilst moving CTEs (it can get quite tricky & costly).
769def ensure_bools(expression: exp.Expr) -> exp.Expr: 770 """Converts numeric values used in conditions into explicit boolean expressions.""" 771 from sqlglot.optimizer.canonicalize import ensure_bools 772 773 def _ensure_bool(node: exp.Expr) -> None: 774 if ( 775 node.is_number 776 or ( 777 not isinstance(node, exp.SubqueryPredicate) 778 and node.is_type(exp.DType.UNKNOWN, *exp.DataType.NUMERIC_TYPES) 779 ) 780 or (isinstance(node, exp.Column) and not node.type) 781 ): 782 node.replace(node.neq(0)) 783 784 for node in expression.walk(): 785 ensure_bools(node, _ensure_bool) 786 787 return expression
Converts numeric values used in conditions into explicit boolean expressions.
799def unqualify_pivot_fields(expression: exp.Expr) -> exp.Expr: 800 """ 801 Some dialects only accept simple column names in a (UN)PIVOT's FOR clause and IN-list 802 (Oracle raises ORA-01748), even though the aggregate itself may stay qualified. 803 804 Example: 805 >>> from sqlglot import parse_one 806 >>> expr = parse_one("SELECT * FROM tbl PIVOT (SUM(tbl.sales) FOR tbl.quarter IN ('Q1', 'Q2'))") 807 >>> print(unqualify_pivot_fields(expr).sql(dialect="spark")) 808 SELECT * FROM tbl PIVOT(SUM(tbl.sales) FOR quarter IN ('Q1', 'Q2')) 809 """ 810 if isinstance(expression, exp.Pivot): 811 expression.set("fields", [unqualify_columns(field) for field in expression.fields]) 812 813 return expression
Some dialects only accept simple column names in a (UN)PIVOT's FOR clause and IN-list (Oracle raises ORA-01748), even though the aggregate itself may stay qualified.
Example:
>>> from sqlglot import parse_one >>> expr = parse_one("SELECT * FROM tbl PIVOT (SUM(tbl.sales) FOR tbl.quarter IN ('Q1', 'Q2'))") >>> print(unqualify_pivot_fields(expr).sql(dialect="spark")) SELECT * FROM tbl PIVOT(SUM(tbl.sales) FOR quarter IN ('Q1', 'Q2'))
816def remove_unique_constraints(expression: exp.Expr) -> exp.Expr: 817 assert isinstance(expression, exp.Create) 818 for constraint in expression.find_all(exp.UniqueColumnConstraint): 819 parent = constraint.parent 820 (parent if isinstance(parent, (exp.ColumnConstraint, exp.Constraint)) else constraint).pop() 821 822 return expression
825def ctas_with_tmp_tables_to_create_tmp_view( 826 expression: exp.Expr, 827 tmp_storage_provider: t.Callable[[exp.Expr], exp.Expr] = lambda e: e, 828) -> exp.Expr: 829 assert isinstance(expression, exp.Create) 830 properties: exp.Properties | None = expression.args.get("properties") 831 temporary = any( 832 isinstance(prop, exp.TemporaryProperty) 833 for prop in (properties.expressions if properties is not None else []) 834 ) 835 836 # CTAS with temp tables map to CREATE TEMPORARY VIEW 837 if expression.kind == "TABLE" and temporary: 838 if expression.expression: 839 return exp.Create( 840 kind="TEMPORARY VIEW", 841 this=expression.this, 842 expression=expression.expression, 843 ) 844 return tmp_storage_provider(expression) 845 846 return expression
849def move_schema_columns_to_partitioned_by(expression: exp.Expr) -> exp.Expr: 850 """ 851 In Hive, the PARTITIONED BY property acts as an extension of a table's schema. When the 852 PARTITIONED BY value is an array of column names, they are transformed into a schema. 853 The corresponding columns are removed from the create statement. 854 """ 855 assert isinstance(expression, exp.Create) 856 schema = expression.this 857 is_partitionable = expression.kind in {"TABLE", "VIEW"} 858 859 if isinstance(schema, exp.Schema) and is_partitionable: 860 prop = expression.find(exp.PartitionedByProperty) 861 if prop and prop.this and not isinstance(prop.this, exp.Schema): 862 columns: set[str] = {v.name.upper() for v in prop.this.expressions} 863 schema_exprs: list[exp.Expr] = schema.expressions 864 partitions = [col for col in schema_exprs if col.name.upper() in columns] 865 schema.set("expressions", [e for e in schema_exprs if e not in partitions]) 866 prop.replace(exp.PartitionedByProperty(this=exp.Schema(expressions=partitions))) 867 expression.set("this", schema) 868 869 return expression
In Hive, the PARTITIONED BY property acts as an extension of a table's schema. When the PARTITIONED BY value is an array of column names, they are transformed into a schema. The corresponding columns are removed from the create statement.
872def move_partitioned_by_to_schema_columns(expression: exp.Expr) -> exp.Expr: 873 """ 874 Spark 3 supports both "HIVEFORMAT" and "DATASOURCE" formats for CREATE TABLE. 875 876 Currently, SQLGlot uses the DATASOURCE format for Spark 3. 877 """ 878 assert isinstance(expression, exp.Create) 879 prop = expression.find(exp.PartitionedByProperty) 880 if ( 881 prop 882 and prop.this 883 and isinstance(prop.this, exp.Schema) 884 and all(isinstance(e, exp.ColumnDef) and e.kind for e in prop.this.expressions) 885 ): 886 prop_this = exp.Tuple( 887 expressions=[exp.to_identifier(e.this) for e in prop.this.expressions] 888 ) 889 schema: exp.Schema = expression.this 890 for e in prop.this.expressions: 891 schema.append("expressions", e) 892 prop.set("this", prop_this) 893 894 return expression
Spark 3 supports both "HIVEFORMAT" and "DATASOURCE" formats for CREATE TABLE.
Currently, SQLGlot uses the DATASOURCE format for Spark 3.
897def struct_kv_to_alias(expression: exp.Expr) -> exp.Expr: 898 """Converts struct arguments to aliases, e.g. STRUCT(1 AS y).""" 899 if isinstance(expression, exp.Struct): 900 expression.set( 901 "expressions", 902 [ 903 exp.alias_(e.expression, e.this) if isinstance(e, exp.PropertyEQ) else e 904 for e in expression.expressions 905 ], 906 ) 907 908 return expression
Converts struct arguments to aliases, e.g. STRUCT(1 AS y).
911def eliminate_join_marks(expression: exp.Expr) -> exp.Expr: 912 """https://docs.oracle.com/cd/B19306_01/server.102/b14200/queries006.htm#sthref3178 913 914 1. You cannot specify the (+) operator in a query block that also contains FROM clause join syntax. 915 916 2. The (+) operator can appear only in the WHERE clause or, in the context of left-correlation (that is, when specifying the TABLE clause) in the FROM clause, and can be applied only to a column of a table or view. 917 918 The (+) operator does not produce an outer join if you specify one table in the outer query and the other table in an inner query. 919 920 You cannot use the (+) operator to outer-join a table to itself, although self joins are valid. 921 922 The (+) operator can be applied only to a column, not to an arbitrary expression. However, an arbitrary expression can contain one or more columns marked with the (+) operator. 923 924 A WHERE condition containing the (+) operator cannot be combined with another condition using the OR logical operator. 925 926 A WHERE condition cannot use the IN comparison condition to compare a column marked with the (+) operator with an expression. 927 928 A WHERE condition cannot compare any column marked with the (+) operator with a subquery. 929 930 -- example with WHERE 931 SELECT d.department_name, sum(e.salary) as total_salary 932 FROM departments d, employees e 933 WHERE e.department_id(+) = d.department_id 934 group by department_name 935 936 -- example of left correlation in select 937 SELECT d.department_name, ( 938 SELECT SUM(e.salary) 939 FROM employees e 940 WHERE e.department_id(+) = d.department_id) AS total_salary 941 FROM departments d; 942 943 -- example of left correlation in from 944 SELECT d.department_name, t.total_salary 945 FROM departments d, ( 946 SELECT SUM(e.salary) AS total_salary 947 FROM employees e 948 WHERE e.department_id(+) = d.department_id 949 ) t 950 """ 951 952 from sqlglot.optimizer.scope import traverse_scope 953 from sqlglot.optimizer.normalize import normalize, normalized 954 from collections import defaultdict 955 956 # we go in reverse to check the main query for left correlation 957 for scope in reversed(traverse_scope(expression)): 958 query = scope.expression 959 960 where: exp.Expr | None = query.args.get("where") 961 joins: list[exp.Join] = query.args.get("joins", []) 962 963 if not where or not any(c.args.get("join_mark") for c in where.find_all(exp.Column)): 964 continue 965 966 # knockout: we do not support left correlation (see point 2) 967 assert not scope.is_correlated_subquery, "Correlated queries are not supported" 968 969 # make sure we have AND of ORs to have clear join terms 970 where = normalize(where.this) 971 assert normalized(where), "Cannot normalize JOIN predicates" 972 # dict of {name: list of join AND conditions} 973 joins_ons: defaultdict[str, list[exp.Expr]] = defaultdict(list) 974 for cond in [where] if not isinstance(where, exp.And) else where.flatten(): 975 join_cols = [col for col in cond.find_all(exp.Column) if col.args.get("join_mark")] 976 977 left_join_table = set(col.table for col in join_cols) 978 if not left_join_table: 979 continue 980 981 assert not (len(left_join_table) > 1), ( 982 "Cannot combine JOIN predicates from different tables" 983 ) 984 985 for col in join_cols: 986 col.set("join_mark", False) 987 988 joins_ons[left_join_table.pop()].append(cond) 989 990 old_joins = {join.alias_or_name: join for join in joins} 991 new_joins: dict[str, exp.Join] = {} 992 query_from = query.args["from_"] 993 994 for table, predicates in joins_ons.items(): 995 join_what = old_joins.get(table, query_from).this.copy() 996 new_joins[join_what.alias_or_name] = exp.Join( 997 this=join_what, on=exp.and_(*predicates), kind="LEFT" 998 ) 999 1000 for p in predicates: 1001 while isinstance(p.parent, exp.Paren): 1002 p.parent.replace(p) 1003 1004 parent = p.parent 1005 p.pop() 1006 if isinstance(parent, exp.Binary): 1007 left = parent.args.get("this") 1008 parent.replace(parent.right if left is None else left) 1009 elif isinstance(parent, exp.Where): 1010 parent.pop() 1011 1012 if query_from.alias_or_name in new_joins: 1013 only_old_joins: set[str] = old_joins.keys() - new_joins.keys() 1014 assert len(only_old_joins) >= 1, ( 1015 "Cannot determine which table to use in the new FROM clause" 1016 ) 1017 1018 new_from_name = list[str](only_old_joins)[0] 1019 query.set("from_", exp.From(this=old_joins[new_from_name].this)) 1020 1021 if new_joins: 1022 for n, j in old_joins.items(): # preserve any other joins 1023 if n not in new_joins and n != query.args["from_"].name: 1024 if not j.kind: 1025 j.set("kind", "CROSS") 1026 new_joins[n] = j 1027 query.set("joins", list(new_joins.values())) 1028 1029 return expression
https://docs.oracle.com/cd/B19306_01/server.102/b14200/queries006.htm#sthref3178
You cannot specify the (+) operator in a query block that also contains FROM clause join syntax.
The (+) operator can appear only in the WHERE clause or, in the context of left-correlation (that is, when specifying the TABLE clause) in the FROM clause, and can be applied only to a column of a table or view.
The (+) operator does not produce an outer join if you specify one table in the outer query and the other table in an inner query.
You cannot use the (+) operator to outer-join a table to itself, although self joins are valid.
The (+) operator can be applied only to a column, not to an arbitrary expression. However, an arbitrary expression can contain one or more columns marked with the (+) operator.
A WHERE condition containing the (+) operator cannot be combined with another condition using the OR logical operator.
A WHERE condition cannot use the IN comparison condition to compare a column marked with the (+) operator with an expression.
A WHERE condition cannot compare any column marked with the (+) operator with a subquery.
-- example with WHERE SELECT d.department_name, sum(e.salary) as total_salary FROM departments d, employees e WHERE e.department_id(+) = d.department_id group by department_name
-- example of left correlation in select SELECT d.department_name, ( SELECT SUM(e.salary) FROM employees e WHERE e.department_id(+) = d.department_id) AS total_salary FROM departments d;
-- example of left correlation in from SELECT d.department_name, t.total_salary FROM departments d, ( SELECT SUM(e.salary) AS total_salary FROM employees e WHERE e.department_id(+) = d.department_id ) t
1032def any_to_exists(expression: exp.Expr) -> exp.Expr: 1033 """ 1034 Transform ANY operator to Spark's EXISTS 1035 1036 For example, 1037 - Postgres: SELECT * FROM tbl WHERE 5 > ANY(tbl.col) 1038 - Spark: SELECT * FROM tbl WHERE EXISTS(tbl.col, x -> x < 5) 1039 1040 Both ANY and EXISTS accept queries but currently only array expressions are supported for this 1041 transformation 1042 """ 1043 if isinstance(expression, exp.Select): 1044 for any_expr in expression.find_all(exp.Any): 1045 this: exp.Expr = any_expr.this 1046 if isinstance(this, exp.Query) or isinstance(any_expr.parent, (exp.Like, exp.ILike)): 1047 continue 1048 1049 binop = any_expr.parent 1050 if isinstance(binop, exp.Binary): 1051 lambda_arg = exp.to_identifier("x") 1052 any_expr.replace(lambda_arg) 1053 lambda_expr = exp.Lambda(this=binop.copy(), expressions=[lambda_arg]) 1054 binop.replace(exp.Exists(this=this.unnest(), expression=lambda_expr)) 1055 1056 return expression
Transform ANY operator to Spark's EXISTS
For example, - Postgres: SELECT * FROM tbl WHERE 5 > ANY(tbl.col) - Spark: SELECT * FROM tbl WHERE EXISTS(tbl.col, x -> x < 5)
Both ANY and EXISTS accept queries but currently only array expressions are supported for this transformation
1059def eliminate_window_clause(expression: exp.Expr) -> exp.Expr: 1060 """Eliminates the `WINDOW` query clause by inling each named window.""" 1061 windows: list[exp.Expr] | None = expression.args.get("windows") 1062 if isinstance(expression, exp.Select) and windows is not None: 1063 from sqlglot.optimizer.scope import find_all_in_scope 1064 1065 expression.set("windows", None) 1066 1067 window_expression: dict[str, exp.Expr] = {} 1068 1069 def _inline_inherited_window(window: exp.Expr) -> None: 1070 inherited_window = window_expression.get(window.alias.lower()) 1071 if not inherited_window: 1072 return 1073 1074 window.set("alias", None) 1075 for key in ("partition_by", "order", "spec"): 1076 arg: exp.Expr | None = inherited_window.args.get(key) 1077 if arg is not None: 1078 window.set(key, arg.copy()) 1079 1080 for window in windows: 1081 _inline_inherited_window(window) 1082 window_expression[window.name.lower()] = window 1083 1084 for window in find_all_in_scope(expression, exp.Window): 1085 _inline_inherited_window(window) 1086 1087 return expression
Eliminates the WINDOW query clause by inling each named window.
1090def inherit_struct_field_names(expression: exp.Expr) -> exp.Expr: 1091 """ 1092 Inherit field names from the first struct in an array. 1093 1094 BigQuery supports implicitly inheriting names from the first STRUCT in an array: 1095 1096 Example: 1097 ARRAY[ 1098 STRUCT('Alice' AS name, 85 AS score), -- defines names 1099 STRUCT('Bob', 92), -- inherits names 1100 STRUCT('Diana', 95) -- inherits names 1101 ] 1102 1103 This transformation makes the field names explicit on all structs by adding 1104 PropertyEQ nodes, in order to facilitate transpilation to other dialects. 1105 1106 Args: 1107 expression: The expression tree to transform 1108 1109 Returns: 1110 The modified expression with field names inherited in all structs 1111 """ 1112 if ( 1113 isinstance(expression, exp.Array) 1114 and expression.args.get("struct_name_inheritance") 1115 and isinstance(first_item := seq_get(expression.expressions, 0), exp.Struct) 1116 and all(isinstance(fld, exp.PropertyEQ) for fld in first_item.expressions) 1117 ): 1118 field_names: list[exp.Identifier] = [fld.this for fld in first_item.expressions] 1119 1120 # Apply field names to subsequent structs that don't have them 1121 for struct in expression.expressions[1:]: 1122 if not isinstance(struct, exp.Struct) or len(struct.expressions) != len(field_names): 1123 continue 1124 1125 # Convert unnamed expressions to PropertyEQ with inherited names 1126 new_expressions: list[exp.PropertyEQ] = [] 1127 for i, expr in enumerate(struct.expressions): 1128 if not isinstance(expr, exp.PropertyEQ): 1129 # Create PropertyEQ: field_name := value, preserving the type from the inner expression 1130 property_eq = exp.PropertyEQ( 1131 this=field_names[i].copy(), 1132 expression=expr, 1133 ) 1134 property_eq.type = expr.type 1135 new_expressions.append(property_eq) 1136 else: 1137 new_expressions.append(expr) 1138 1139 struct.set("expressions", new_expressions) 1140 1141 return expression
Inherit field names from the first struct in an array.
BigQuery supports implicitly inheriting names from the first STRUCT in an array:
Example:
ARRAY[ STRUCT('Alice' AS name, 85 AS score), -- defines names STRUCT('Bob', 92), -- inherits names STRUCT('Diana', 95) -- inherits names ]
This transformation makes the field names explicit on all structs by adding PropertyEQ nodes, in order to facilitate transpilation to other dialects.
Arguments:
- expression: The expression tree to transform
Returns:
The modified expression with field names inherited in all structs