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 taken = set(expression.named_selects) 217 for select in expression.selects: 218 if not select.alias_or_name: 219 alias = find_new_name(taken, "_c") 220 select.replace(exp.alias_(select, alias)) 221 taken.add(alias) 222 223 def _select_alias_or_name(select: exp.Expr) -> str | exp.Column: 224 alias_or_name = select.alias_or_name 225 identifier = select.args.get("alias") or select.this 226 if isinstance(identifier, exp.Identifier): 227 return exp.column(alias_or_name, quoted=identifier.args.get("quoted")) 228 return alias_or_name 229 230 outer_selects = exp.select(*map(_select_alias_or_name, expression.selects)) 231 qualify_filters: exp.Expr = expression.args["qualify"].pop().this 232 expression_by_alias: dict[str, exp.Expr] = { 233 select.alias: select.this 234 for select in expression.selects 235 if isinstance(select, exp.Alias) 236 } 237 238 select_candidates = (exp.Window,) if expression.is_star else (exp.Window, exp.Column) 239 for select_candidate in list(qualify_filters.find_all(*select_candidates)): 240 if isinstance(select_candidate, exp.Window): 241 if expression_by_alias: 242 for column in select_candidate.find_all(exp.Column): 243 expr = expression_by_alias.get(column.name) 244 if expr: 245 column.replace(expr) 246 247 alias = find_new_name(expression.named_selects, "_w") 248 expression.select(exp.alias_(select_candidate, alias), copy=False) 249 column = exp.column(alias) 250 251 if isinstance(select_candidate.parent, exp.Qualify): 252 qualify_filters = column 253 else: 254 select_candidate.replace(column) 255 elif select_candidate.name not in expression.named_selects: 256 expression.select(select_candidate.copy(), copy=False) 257 258 return outer_selects.from_(expression.subquery(alias="_t", copy=False), copy=False).where( 259 qualify_filters, copy=False 260 ) 261 262 return expression 263 264 265def remove_precision_parameterized_types(expression: exp.Expr) -> exp.Expr: 266 """ 267 Some dialects only allow the precision for parameterized types to be defined in the DDL and not in 268 other expressions. This transforms removes the precision from parameterized types in expressions. 269 """ 270 for node in expression.find_all(exp.DataType): 271 node.set( 272 "expressions", [e for e in node.expressions if not isinstance(e, exp.DataTypeParam)] 273 ) 274 275 return expression 276 277 278def unqualify_unnest(expression: exp.Expr) -> exp.Expr: 279 """Remove references to unnest table aliases, added by the optimizer's qualify_columns step.""" 280 from sqlglot.optimizer.scope import find_all_in_scope 281 282 if isinstance(expression, exp.Select): 283 unnest_aliases = { 284 unnest.alias 285 for unnest in find_all_in_scope(expression, exp.Unnest) 286 if isinstance(unnest.parent, (exp.From, exp.Join)) 287 } 288 if unnest_aliases: 289 for column in expression.find_all(exp.Column): 290 leftmost_part = column.parts[0] 291 if leftmost_part.arg_key != "this" and leftmost_part.this in unnest_aliases: 292 leftmost_part.pop() 293 294 return expression 295 296 297def unnest_to_explode( 298 expression: exp.Expr, 299 unnest_using_arrays_zip: bool = True, 300) -> exp.Expr: 301 """Convert cross join unnest into lateral view explode.""" 302 303 def _unnest_zip_exprs( 304 u: exp.Unnest, unnest_exprs: list[exp.Expr], has_multi_expr: bool 305 ) -> list[exp.Expr]: 306 if has_multi_expr: 307 if not unnest_using_arrays_zip: 308 raise UnsupportedError("Cannot transpile UNNEST with multiple input arrays") 309 310 # Use INLINE(ARRAYS_ZIP(...)) for multiple expressions 311 zip_exprs: list[exp.Expr] = [exp.Anonymous(this="ARRAYS_ZIP", expressions=unnest_exprs)] 312 u.set("expressions", zip_exprs) 313 return zip_exprs 314 return unnest_exprs 315 316 def _udtf_type(u: exp.Unnest, has_multi_expr: bool) -> type[exp.Func]: 317 if u.args.get("offset"): 318 return exp.Posexplode 319 return exp.Inline if has_multi_expr else exp.Explode 320 321 if isinstance(expression, exp.Select): 322 from_ = expression.args.get("from_") 323 324 if from_ and isinstance(from_.this, exp.Unnest): 325 unnest: exp.Unnest = from_.this 326 alias: exp.TableAlias | None = unnest.args.get("alias") 327 exprs: list[exp.Expr] = unnest.expressions 328 has_multi_expr = len(exprs) > 1 329 this, *_ = _unnest_zip_exprs(unnest, exprs, has_multi_expr) 330 331 columns: list[exp.Identifier] = alias.columns if alias else [] 332 offset: exp.Expr | None = unnest.args.get("offset") 333 if offset: 334 columns.insert( 335 0, offset if isinstance(offset, exp.Identifier) else exp.to_identifier("pos") 336 ) 337 338 unnest.replace( 339 exp.Table( 340 this=_udtf_type(unnest, has_multi_expr)(this=this), 341 alias=exp.TableAlias(this=alias.this, columns=columns) if alias else None, 342 ) 343 ) 344 345 joins: list[exp.Join] = expression.args.get("joins") or [] 346 for join in list(joins): 347 join_expr = join.this 348 349 is_lateral = isinstance(join_expr, exp.Lateral) 350 351 unnest = join_expr.this if is_lateral else join_expr 352 353 if isinstance(unnest, exp.Unnest): 354 if is_lateral: 355 alias = join_expr.args.get("alias") 356 else: 357 alias = unnest.args.get("alias") 358 359 if alias is None: 360 raise UnsupportedError( 361 "CROSS JOIN UNNEST to LATERAL VIEW EXPLODE transformation requires an alias" 362 ) 363 364 exprs = unnest.expressions 365 # The number of unnest.expressions will be changed by _unnest_zip_exprs, we need to record it here 366 has_multi_expr = len(exprs) > 1 367 exprs = _unnest_zip_exprs(unnest, exprs, has_multi_expr) 368 369 joins.remove(join) 370 371 alias_cols: list[exp.Identifier] = alias.columns 372 373 # # Handle UNNEST to LATERAL VIEW EXPLODE: Exception is raised when there are 0 or > 2 aliases 374 # 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. 375 # Refs: https://spark.apache.org/docs/latest/sql-ref-syntax-qry-select-lateral-view.html 376 377 if not has_multi_expr and len(alias_cols) not in (1, 2): 378 raise UnsupportedError( 379 "CROSS JOIN UNNEST to LATERAL VIEW EXPLODE transformation requires explicit column aliases" 380 ) 381 382 offset = unnest.args.get("offset") 383 if offset: 384 alias_cols.insert( 385 0, 386 offset if isinstance(offset, exp.Identifier) else exp.to_identifier("pos"), 387 ) 388 389 for e, column in zip(exprs, alias_cols): 390 expression.append( 391 "laterals", 392 exp.Lateral( 393 this=_udtf_type(unnest, has_multi_expr)(this=e), 394 view=True, 395 alias=exp.TableAlias(this=alias.this, columns=alias_cols), 396 ), 397 ) 398 399 return expression 400 401 402def explode_projection_to_unnest( 403 index_offset: int = 0, 404) -> t.Callable[[exp.Expr], exp.Expr]: 405 """Convert explode/posexplode projections into unnests.""" 406 407 def _explode_projection_to_unnest(expression: exp.Expr) -> exp.Expr: 408 if isinstance(expression, exp.Select): 409 from sqlglot.optimizer.scope import Scope 410 411 taken_select_names = set(expression.named_selects) 412 taken_source_names = {name for name, _ in Scope(expression).references} 413 414 def new_name(names: set[str], name: str) -> str: 415 name = find_new_name(names, name) 416 names.add(name) 417 return name 418 419 arrays: list[exp.Condition] = [] 420 series_alias = new_name(taken_select_names, "pos") 421 series = exp.alias_( 422 exp.Unnest( 423 expressions=[exp.GenerateSeries(start=exp.Literal.number(index_offset))] 424 ), 425 new_name(taken_source_names, "_u"), 426 table=[series_alias], 427 ) 428 429 # we use list here because expression.selects is mutated inside the loop 430 for select in list(expression.selects): 431 explode = select.find(exp.Explode) 432 433 if explode: 434 pos_alias: t.Any = "" 435 explode_alias: t.Any = "" 436 437 if isinstance(select, exp.Alias): 438 explode_alias = select.args["alias"] 439 alias: exp.Expr = select 440 elif isinstance(select, exp.Aliases): 441 pos_alias = select.aliases[0] 442 explode_alias = select.aliases[1] 443 alias = select.replace(exp.alias_(select.this, "", copy=False)) 444 else: 445 alias = select.replace(exp.alias_(select, "")) 446 explode = alias.find(exp.Explode) 447 assert explode 448 449 is_posexplode = isinstance(explode, exp.Posexplode) 450 explode_arg = explode.this 451 452 if isinstance(explode, exp.ExplodeOuter): 453 bracket = explode_arg[0] 454 bracket.set("safe", True) 455 bracket.set("offset", True) 456 explode_arg = exp.func( 457 "IF", 458 exp.func( 459 "ARRAY_SIZE", exp.func("COALESCE", explode_arg, exp.Array()) 460 ).eq(0), 461 exp.array(bracket, copy=False), 462 explode_arg, 463 ) 464 465 # This ensures that we won't use [POS]EXPLODE's argument as a new selection 466 if isinstance(explode_arg, exp.Column): 467 taken_select_names.add(explode_arg.output_name) 468 469 unnest_source_alias = new_name(taken_source_names, "_u") 470 471 if not explode_alias: 472 explode_alias = new_name(taken_select_names, "col") 473 474 if is_posexplode: 475 pos_alias = new_name(taken_select_names, "pos") 476 477 if not pos_alias: 478 pos_alias = new_name(taken_select_names, "pos") 479 480 alias.set("alias", exp.to_identifier(explode_alias)) 481 482 series_table_alias = series.args["alias"].this 483 column = exp.If( 484 this=exp.column(series_alias, table=series_table_alias).eq( 485 exp.column(pos_alias, table=unnest_source_alias) 486 ), 487 true=exp.column(explode_alias, table=unnest_source_alias), 488 ) 489 490 explode.replace(column) 491 492 if is_posexplode: 493 expressions = expression.expressions 494 expressions.insert( 495 expressions.index(alias) + 1, 496 exp.If( 497 this=exp.column(series_alias, table=series_table_alias).eq( 498 exp.column(pos_alias, table=unnest_source_alias) 499 ), 500 true=exp.column(pos_alias, table=unnest_source_alias), 501 ).as_(pos_alias), 502 ) 503 expression.set("expressions", expressions) 504 505 if not arrays: 506 if expression.args.get("from_"): 507 expression.join(series, copy=False, join_type="CROSS") 508 else: 509 expression.from_(series, copy=False) 510 511 size: exp.Condition = exp.ArraySize(this=explode_arg.copy()) 512 arrays.append(size) 513 514 # trino doesn't support left join unnest with on conditions 515 # if it did, this would be much simpler 516 expression.join( 517 exp.alias_( 518 exp.Unnest( 519 expressions=[explode_arg.copy()], 520 offset=exp.to_identifier(pos_alias), 521 ), 522 unnest_source_alias, 523 table=[explode_alias], 524 ), 525 join_type="CROSS", 526 copy=False, 527 ) 528 529 if index_offset != 1: 530 size = size - 1 531 532 expression.where( 533 exp.column(series_alias, table=series_table_alias) 534 .eq(exp.column(pos_alias, table=unnest_source_alias)) 535 .or_( 536 (exp.column(series_alias, table=series_table_alias) > size).and_( 537 exp.column(pos_alias, table=unnest_source_alias).eq(size) 538 ) 539 ), 540 copy=False, 541 ) 542 543 if arrays: 544 end: exp.Condition = exp.Greatest(this=arrays[0], expressions=arrays[1:]) 545 546 if index_offset != 1: 547 end = end - (1 - index_offset) 548 series.expressions[0].set("end", end) 549 550 return expression 551 552 return _explode_projection_to_unnest 553 554 555def add_within_group_for_percentiles(expression: exp.Expr) -> exp.Expr: 556 """Transforms percentiles by adding a WITHIN GROUP clause to them.""" 557 if ( 558 isinstance(expression, exp.PERCENTILES) 559 and not isinstance(expression.parent, exp.WithinGroup) 560 and expression.expression 561 ): 562 column = expression.this.pop() 563 expression.set("this", expression.expression.pop()) 564 order = exp.Order(expressions=[exp.Ordered(this=column)]) 565 expression = exp.WithinGroup(this=expression, expression=order) 566 567 return expression 568 569 570def remove_within_group_for_percentiles(expression: exp.Expr) -> exp.Expr: 571 """Transforms percentiles by getting rid of their corresponding WITHIN GROUP clause.""" 572 if ( 573 isinstance(expression, exp.WithinGroup) 574 and isinstance(expression.this, exp.PERCENTILES) 575 and isinstance(expression.expression, exp.Order) 576 ): 577 quantile = expression.this.this 578 input_value = t.cast(exp.Ordered, expression.find(exp.Ordered)).this 579 return expression.replace(exp.ApproxQuantile(this=input_value, quantile=quantile)) 580 581 return expression 582 583 584def add_recursive_cte_column_names(expression: exp.Expr) -> exp.Expr: 585 """Uses projection output names in recursive CTE definitions to define the CTEs' columns.""" 586 if isinstance(expression, exp.With) and expression.recursive: 587 next_name = name_sequence("_c_") 588 589 for cte in expression.expressions: 590 if not cte.args["alias"].columns: 591 query = cte.this 592 if isinstance(query, exp.SetOperation): 593 query = query.this 594 595 cte.args["alias"].set( 596 "columns", 597 [exp.to_identifier(s.alias_or_name or next_name()) for s in query.selects], 598 ) 599 600 return expression 601 602 603def epoch_cast_to_ts(expression: exp.Expr) -> exp.Expr: 604 """Replace 'epoch' in casts by the equivalent date literal.""" 605 if ( 606 isinstance(expression, (exp.Cast, exp.TryCast)) 607 and expression.name.lower() == "epoch" 608 and expression.to.this in exp.DataType.TEMPORAL_TYPES 609 ): 610 expression.this.replace(exp.Literal.string("1970-01-01 00:00:00")) 611 612 return expression 613 614 615def eliminate_semi_and_anti_joins(expression: exp.Expr) -> exp.Expr: 616 """Convert SEMI and ANTI joins into equivalent forms that use EXIST instead.""" 617 if isinstance(expression, exp.Select): 618 for join in list[exp.Join](expression.args.get("joins") or []): 619 on: exp.Expr | None = join.args.get("on") 620 if on and join.kind in ("SEMI", "ANTI"): 621 subquery = exp.select("1").from_(join.this).where(on) 622 exists: exp.Exists | exp.Not = exp.Exists(this=subquery) 623 if join.kind == "ANTI": 624 exists = exists.not_(copy=False) 625 626 join.pop() 627 expression.where(exists, copy=False) 628 629 return expression 630 631 632def eliminate_full_outer_join(expression: exp.Expr) -> exp.Expr: 633 """ 634 Converts a query with a FULL OUTER join to a union of identical queries that 635 use LEFT/RIGHT OUTER joins instead. This transformation currently only works 636 for queries that have a single FULL OUTER join. 637 """ 638 if isinstance(expression, exp.Select): 639 full_outer_joins: list[tuple[int, exp.Join]] = [ 640 (index, join) 641 for index, join in enumerate[exp.Join](expression.args.get("joins") or []) 642 if join.side == "FULL" 643 ] 644 645 if len(full_outer_joins) == 1: 646 expression_copy = expression.copy() 647 expression.set("limit", None) 648 index, full_outer_join = full_outer_joins[0] 649 650 tables = (expression.args["from_"].alias_or_name, full_outer_join.alias_or_name) 651 join_conditions = full_outer_join.args.get("on") or exp.and_( 652 *[ 653 exp.column(col, tables[0]).eq(exp.column(col, tables[1])) 654 for col in t.cast(list[exp.Identifier], full_outer_join.args.get("using")) 655 ] 656 ) 657 658 full_outer_join.set("side", "left") 659 anti_join_clause = ( 660 exp.select("1").from_(expression.args["from_"]).where(join_conditions) 661 ) 662 expression_copy.args["joins"][index].set("side", "right") 663 expression_copy = expression_copy.where(exp.Exists(this=anti_join_clause).not_()) 664 expression_copy.set("with_", None) # remove CTEs from RIGHT side 665 expression.set("order", None) # remove order by from LEFT side 666 667 return exp.union(expression, expression_copy, copy=False, distinct=False) 668 669 return expression 670 671 672def move_ctes_to_top_level(expression: E) -> E: 673 """ 674 Some dialects (e.g. Hive, T-SQL, Spark prior to version 3) only allow CTEs to be 675 defined at the top-level, so for example queries like: 676 677 SELECT * FROM (WITH t(c) AS (SELECT 1) SELECT * FROM t) AS subq 678 679 are invalid in those dialects. This transformation can be used to ensure all CTEs are 680 moved to the top level so that the final SQL code is valid from a syntax standpoint. 681 682 TODO: handle name clashes whilst moving CTEs (it can get quite tricky & costly). 683 """ 684 top_level_with: exp.With | None = expression.args.get("with_") 685 for inner_with in expression.find_all(exp.With): 686 if inner_with.parent is expression: 687 continue 688 689 if not top_level_with: 690 top_level_with = inner_with.pop() 691 expression.set("with_", top_level_with) 692 else: 693 if inner_with.recursive: 694 top_level_with.set("recursive", True) 695 696 parent_cte = inner_with.find_ancestor(exp.CTE) 697 inner_with.pop() 698 699 if parent_cte: 700 i = top_level_with.expressions.index(parent_cte) 701 top_level_with.expressions[i:i] = inner_with.expressions 702 top_level_with.set("expressions", top_level_with.expressions) 703 else: 704 top_level_with.set( 705 "expressions", top_level_with.expressions + inner_with.expressions 706 ) 707 708 return expression 709 710 711def ensure_bools(expression: exp.Expr) -> exp.Expr: 712 """Converts numeric values used in conditions into explicit boolean expressions.""" 713 from sqlglot.optimizer.canonicalize import ensure_bools 714 715 def _ensure_bool(node: exp.Expr) -> None: 716 if ( 717 node.is_number 718 or ( 719 not isinstance(node, exp.SubqueryPredicate) 720 and node.is_type(exp.DType.UNKNOWN, *exp.DataType.NUMERIC_TYPES) 721 ) 722 or (isinstance(node, exp.Column) and not node.type) 723 ): 724 node.replace(node.neq(0)) 725 726 for node in expression.walk(): 727 ensure_bools(node, _ensure_bool) 728 729 return expression 730 731 732def unqualify_columns(expression: exp.Expr) -> exp.Expr: 733 for column in expression.find_all(exp.Column): 734 # We only wanna pop off the table, db, catalog args 735 for part in column.parts[:-1]: 736 part.pop() 737 738 return expression 739 740 741def unqualify_pivot_fields(expression: exp.Expr) -> exp.Expr: 742 """ 743 Some dialects only accept simple column names in a (UN)PIVOT's FOR clause and IN-list 744 (Oracle raises ORA-01748), even though the aggregate itself may stay qualified. 745 746 Example: 747 >>> from sqlglot import parse_one 748 >>> expr = parse_one("SELECT * FROM tbl PIVOT (SUM(tbl.sales) FOR tbl.quarter IN ('Q1', 'Q2'))") 749 >>> print(unqualify_pivot_fields(expr).sql(dialect="spark")) 750 SELECT * FROM tbl PIVOT(SUM(tbl.sales) FOR quarter IN ('Q1', 'Q2')) 751 """ 752 if isinstance(expression, exp.Pivot): 753 expression.set("fields", [unqualify_columns(field) for field in expression.fields]) 754 755 return expression 756 757 758def remove_unique_constraints(expression: exp.Expr) -> exp.Expr: 759 assert isinstance(expression, exp.Create) 760 for constraint in expression.find_all(exp.UniqueColumnConstraint): 761 if constraint.parent: 762 constraint.parent.pop() 763 764 return expression 765 766 767def ctas_with_tmp_tables_to_create_tmp_view( 768 expression: exp.Expr, 769 tmp_storage_provider: t.Callable[[exp.Expr], exp.Expr] = lambda e: e, 770) -> exp.Expr: 771 assert isinstance(expression, exp.Create) 772 properties: exp.Properties | None = expression.args.get("properties") 773 temporary = any( 774 isinstance(prop, exp.TemporaryProperty) 775 for prop in (properties.expressions if properties is not None else []) 776 ) 777 778 # CTAS with temp tables map to CREATE TEMPORARY VIEW 779 if expression.kind == "TABLE" and temporary: 780 if expression.expression: 781 return exp.Create( 782 kind="TEMPORARY VIEW", 783 this=expression.this, 784 expression=expression.expression, 785 ) 786 return tmp_storage_provider(expression) 787 788 return expression 789 790 791def move_schema_columns_to_partitioned_by(expression: exp.Expr) -> exp.Expr: 792 """ 793 In Hive, the PARTITIONED BY property acts as an extension of a table's schema. When the 794 PARTITIONED BY value is an array of column names, they are transformed into a schema. 795 The corresponding columns are removed from the create statement. 796 """ 797 assert isinstance(expression, exp.Create) 798 schema = expression.this 799 is_partitionable = expression.kind in {"TABLE", "VIEW"} 800 801 if isinstance(schema, exp.Schema) and is_partitionable: 802 prop = expression.find(exp.PartitionedByProperty) 803 if prop and prop.this and not isinstance(prop.this, exp.Schema): 804 columns: set[str] = {v.name.upper() for v in prop.this.expressions} 805 schema_exprs: list[exp.Expr] = schema.expressions 806 partitions = [col for col in schema_exprs if col.name.upper() in columns] 807 schema.set("expressions", [e for e in schema_exprs if e not in partitions]) 808 prop.replace(exp.PartitionedByProperty(this=exp.Schema(expressions=partitions))) 809 expression.set("this", schema) 810 811 return expression 812 813 814def move_partitioned_by_to_schema_columns(expression: exp.Expr) -> exp.Expr: 815 """ 816 Spark 3 supports both "HIVEFORMAT" and "DATASOURCE" formats for CREATE TABLE. 817 818 Currently, SQLGlot uses the DATASOURCE format for Spark 3. 819 """ 820 assert isinstance(expression, exp.Create) 821 prop = expression.find(exp.PartitionedByProperty) 822 if ( 823 prop 824 and prop.this 825 and isinstance(prop.this, exp.Schema) 826 and all(isinstance(e, exp.ColumnDef) and e.kind for e in prop.this.expressions) 827 ): 828 prop_this = exp.Tuple( 829 expressions=[exp.to_identifier(e.this) for e in prop.this.expressions] 830 ) 831 schema: exp.Schema = expression.this 832 for e in prop.this.expressions: 833 schema.append("expressions", e) 834 prop.set("this", prop_this) 835 836 return expression 837 838 839def struct_kv_to_alias(expression: exp.Expr) -> exp.Expr: 840 """Converts struct arguments to aliases, e.g. STRUCT(1 AS y).""" 841 if isinstance(expression, exp.Struct): 842 expression.set( 843 "expressions", 844 [ 845 exp.alias_(e.expression, e.this) if isinstance(e, exp.PropertyEQ) else e 846 for e in expression.expressions 847 ], 848 ) 849 850 return expression 851 852 853def eliminate_join_marks(expression: exp.Expr) -> exp.Expr: 854 """https://docs.oracle.com/cd/B19306_01/server.102/b14200/queries006.htm#sthref3178 855 856 1. You cannot specify the (+) operator in a query block that also contains FROM clause join syntax. 857 858 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. 859 860 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. 861 862 You cannot use the (+) operator to outer-join a table to itself, although self joins are valid. 863 864 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. 865 866 A WHERE condition containing the (+) operator cannot be combined with another condition using the OR logical operator. 867 868 A WHERE condition cannot use the IN comparison condition to compare a column marked with the (+) operator with an expression. 869 870 A WHERE condition cannot compare any column marked with the (+) operator with a subquery. 871 872 -- example with WHERE 873 SELECT d.department_name, sum(e.salary) as total_salary 874 FROM departments d, employees e 875 WHERE e.department_id(+) = d.department_id 876 group by department_name 877 878 -- example of left correlation in select 879 SELECT d.department_name, ( 880 SELECT SUM(e.salary) 881 FROM employees e 882 WHERE e.department_id(+) = d.department_id) AS total_salary 883 FROM departments d; 884 885 -- example of left correlation in from 886 SELECT d.department_name, t.total_salary 887 FROM departments d, ( 888 SELECT SUM(e.salary) AS total_salary 889 FROM employees e 890 WHERE e.department_id(+) = d.department_id 891 ) t 892 """ 893 894 from sqlglot.optimizer.scope import traverse_scope 895 from sqlglot.optimizer.normalize import normalize, normalized 896 from collections import defaultdict 897 898 # we go in reverse to check the main query for left correlation 899 for scope in reversed(traverse_scope(expression)): 900 query = scope.expression 901 902 where: exp.Expr | None = query.args.get("where") 903 joins: list[exp.Join] = query.args.get("joins", []) 904 905 if not where or not any(c.args.get("join_mark") for c in where.find_all(exp.Column)): 906 continue 907 908 # knockout: we do not support left correlation (see point 2) 909 assert not scope.is_correlated_subquery, "Correlated queries are not supported" 910 911 # make sure we have AND of ORs to have clear join terms 912 where = normalize(where.this) 913 assert normalized(where), "Cannot normalize JOIN predicates" 914 # dict of {name: list of join AND conditions} 915 joins_ons: defaultdict[str, list[exp.Expr]] = defaultdict(list) 916 for cond in [where] if not isinstance(where, exp.And) else where.flatten(): 917 join_cols = [col for col in cond.find_all(exp.Column) if col.args.get("join_mark")] 918 919 left_join_table = set(col.table for col in join_cols) 920 if not left_join_table: 921 continue 922 923 assert not (len(left_join_table) > 1), ( 924 "Cannot combine JOIN predicates from different tables" 925 ) 926 927 for col in join_cols: 928 col.set("join_mark", False) 929 930 joins_ons[left_join_table.pop()].append(cond) 931 932 old_joins = {join.alias_or_name: join for join in joins} 933 new_joins: dict[str, exp.Join] = {} 934 query_from = query.args["from_"] 935 936 for table, predicates in joins_ons.items(): 937 join_what = old_joins.get(table, query_from).this.copy() 938 new_joins[join_what.alias_or_name] = exp.Join( 939 this=join_what, on=exp.and_(*predicates), kind="LEFT" 940 ) 941 942 for p in predicates: 943 while isinstance(p.parent, exp.Paren): 944 p.parent.replace(p) 945 946 parent = p.parent 947 p.pop() 948 if isinstance(parent, exp.Binary): 949 left = parent.args.get("this") 950 parent.replace(parent.right if left is None else left) 951 elif isinstance(parent, exp.Where): 952 parent.pop() 953 954 if query_from.alias_or_name in new_joins: 955 only_old_joins: set[str] = old_joins.keys() - new_joins.keys() 956 assert len(only_old_joins) >= 1, ( 957 "Cannot determine which table to use in the new FROM clause" 958 ) 959 960 new_from_name = list[str](only_old_joins)[0] 961 query.set("from_", exp.From(this=old_joins[new_from_name].this)) 962 963 if new_joins: 964 for n, j in old_joins.items(): # preserve any other joins 965 if n not in new_joins and n != query.args["from_"].name: 966 if not j.kind: 967 j.set("kind", "CROSS") 968 new_joins[n] = j 969 query.set("joins", list(new_joins.values())) 970 971 return expression 972 973 974def any_to_exists(expression: exp.Expr) -> exp.Expr: 975 """ 976 Transform ANY operator to Spark's EXISTS 977 978 For example, 979 - Postgres: SELECT * FROM tbl WHERE 5 > ANY(tbl.col) 980 - Spark: SELECT * FROM tbl WHERE EXISTS(tbl.col, x -> x < 5) 981 982 Both ANY and EXISTS accept queries but currently only array expressions are supported for this 983 transformation 984 """ 985 if isinstance(expression, exp.Select): 986 for any_expr in expression.find_all(exp.Any): 987 this: exp.Expr = any_expr.this 988 if isinstance(this, exp.Query) or isinstance(any_expr.parent, (exp.Like, exp.ILike)): 989 continue 990 991 binop = any_expr.parent 992 if isinstance(binop, exp.Binary): 993 lambda_arg = exp.to_identifier("x") 994 any_expr.replace(lambda_arg) 995 lambda_expr = exp.Lambda(this=binop.copy(), expressions=[lambda_arg]) 996 binop.replace(exp.Exists(this=this.unnest(), expression=lambda_expr)) 997 998 return expression 999 1000 1001def eliminate_window_clause(expression: exp.Expr) -> exp.Expr: 1002 """Eliminates the `WINDOW` query clause by inling each named window.""" 1003 windows: list[exp.Expr] | None = expression.args.get("windows") 1004 if isinstance(expression, exp.Select) and windows is not None: 1005 from sqlglot.optimizer.scope import find_all_in_scope 1006 1007 expression.set("windows", None) 1008 1009 window_expression: dict[str, exp.Expr] = {} 1010 1011 def _inline_inherited_window(window: exp.Expr) -> None: 1012 inherited_window = window_expression.get(window.alias.lower()) 1013 if not inherited_window: 1014 return 1015 1016 window.set("alias", None) 1017 for key in ("partition_by", "order", "spec"): 1018 arg: exp.Expr | None = inherited_window.args.get(key) 1019 if arg is not None: 1020 window.set(key, arg.copy()) 1021 1022 for window in windows: 1023 _inline_inherited_window(window) 1024 window_expression[window.name.lower()] = window 1025 1026 for window in find_all_in_scope(expression, exp.Window): 1027 _inline_inherited_window(window) 1028 1029 return expression 1030 1031 1032def inherit_struct_field_names(expression: exp.Expr) -> exp.Expr: 1033 """ 1034 Inherit field names from the first struct in an array. 1035 1036 BigQuery supports implicitly inheriting names from the first STRUCT in an array: 1037 1038 Example: 1039 ARRAY[ 1040 STRUCT('Alice' AS name, 85 AS score), -- defines names 1041 STRUCT('Bob', 92), -- inherits names 1042 STRUCT('Diana', 95) -- inherits names 1043 ] 1044 1045 This transformation makes the field names explicit on all structs by adding 1046 PropertyEQ nodes, in order to facilitate transpilation to other dialects. 1047 1048 Args: 1049 expression: The expression tree to transform 1050 1051 Returns: 1052 The modified expression with field names inherited in all structs 1053 """ 1054 if ( 1055 isinstance(expression, exp.Array) 1056 and expression.args.get("struct_name_inheritance") 1057 and isinstance(first_item := seq_get(expression.expressions, 0), exp.Struct) 1058 and all(isinstance(fld, exp.PropertyEQ) for fld in first_item.expressions) 1059 ): 1060 field_names: list[exp.Identifier] = [fld.this for fld in first_item.expressions] 1061 1062 # Apply field names to subsequent structs that don't have them 1063 for struct in expression.expressions[1:]: 1064 if not isinstance(struct, exp.Struct) or len(struct.expressions) != len(field_names): 1065 continue 1066 1067 # Convert unnamed expressions to PropertyEQ with inherited names 1068 new_expressions: list[exp.PropertyEQ] = [] 1069 for i, expr in enumerate(struct.expressions): 1070 if not isinstance(expr, exp.PropertyEQ): 1071 # Create PropertyEQ: field_name := value, preserving the type from the inner expression 1072 property_eq = exp.PropertyEQ( 1073 this=field_names[i].copy(), 1074 expression=expr, 1075 ) 1076 property_eq.type = expr.type 1077 new_expressions.append(property_eq) 1078 else: 1079 new_expressions.append(expr) 1080 1081 struct.set("expressions", new_expressions) 1082 1083 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 taken = set(expression.named_selects) 218 for select in expression.selects: 219 if not select.alias_or_name: 220 alias = find_new_name(taken, "_c") 221 select.replace(exp.alias_(select, alias)) 222 taken.add(alias) 223 224 def _select_alias_or_name(select: exp.Expr) -> str | exp.Column: 225 alias_or_name = select.alias_or_name 226 identifier = select.args.get("alias") or select.this 227 if isinstance(identifier, exp.Identifier): 228 return exp.column(alias_or_name, quoted=identifier.args.get("quoted")) 229 return alias_or_name 230 231 outer_selects = exp.select(*map(_select_alias_or_name, expression.selects)) 232 qualify_filters: exp.Expr = expression.args["qualify"].pop().this 233 expression_by_alias: dict[str, exp.Expr] = { 234 select.alias: select.this 235 for select in expression.selects 236 if isinstance(select, exp.Alias) 237 } 238 239 select_candidates = (exp.Window,) if expression.is_star else (exp.Window, exp.Column) 240 for select_candidate in list(qualify_filters.find_all(*select_candidates)): 241 if isinstance(select_candidate, exp.Window): 242 if expression_by_alias: 243 for column in select_candidate.find_all(exp.Column): 244 expr = expression_by_alias.get(column.name) 245 if expr: 246 column.replace(expr) 247 248 alias = find_new_name(expression.named_selects, "_w") 249 expression.select(exp.alias_(select_candidate, alias), copy=False) 250 column = exp.column(alias) 251 252 if isinstance(select_candidate.parent, exp.Qualify): 253 qualify_filters = column 254 else: 255 select_candidate.replace(column) 256 elif select_candidate.name not in expression.named_selects: 257 expression.select(select_candidate.copy(), copy=False) 258 259 return outer_selects.from_(expression.subquery(alias="_t", copy=False), copy=False).where( 260 qualify_filters, copy=False 261 ) 262 263 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.
266def remove_precision_parameterized_types(expression: exp.Expr) -> exp.Expr: 267 """ 268 Some dialects only allow the precision for parameterized types to be defined in the DDL and not in 269 other expressions. This transforms removes the precision from parameterized types in expressions. 270 """ 271 for node in expression.find_all(exp.DataType): 272 node.set( 273 "expressions", [e for e in node.expressions if not isinstance(e, exp.DataTypeParam)] 274 ) 275 276 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.
279def unqualify_unnest(expression: exp.Expr) -> exp.Expr: 280 """Remove references to unnest table aliases, added by the optimizer's qualify_columns step.""" 281 from sqlglot.optimizer.scope import find_all_in_scope 282 283 if isinstance(expression, exp.Select): 284 unnest_aliases = { 285 unnest.alias 286 for unnest in find_all_in_scope(expression, exp.Unnest) 287 if isinstance(unnest.parent, (exp.From, exp.Join)) 288 } 289 if unnest_aliases: 290 for column in expression.find_all(exp.Column): 291 leftmost_part = column.parts[0] 292 if leftmost_part.arg_key != "this" and leftmost_part.this in unnest_aliases: 293 leftmost_part.pop() 294 295 return expression
Remove references to unnest table aliases, added by the optimizer's qualify_columns step.
298def unnest_to_explode( 299 expression: exp.Expr, 300 unnest_using_arrays_zip: bool = True, 301) -> exp.Expr: 302 """Convert cross join unnest into lateral view explode.""" 303 304 def _unnest_zip_exprs( 305 u: exp.Unnest, unnest_exprs: list[exp.Expr], has_multi_expr: bool 306 ) -> list[exp.Expr]: 307 if has_multi_expr: 308 if not unnest_using_arrays_zip: 309 raise UnsupportedError("Cannot transpile UNNEST with multiple input arrays") 310 311 # Use INLINE(ARRAYS_ZIP(...)) for multiple expressions 312 zip_exprs: list[exp.Expr] = [exp.Anonymous(this="ARRAYS_ZIP", expressions=unnest_exprs)] 313 u.set("expressions", zip_exprs) 314 return zip_exprs 315 return unnest_exprs 316 317 def _udtf_type(u: exp.Unnest, has_multi_expr: bool) -> type[exp.Func]: 318 if u.args.get("offset"): 319 return exp.Posexplode 320 return exp.Inline if has_multi_expr else exp.Explode 321 322 if isinstance(expression, exp.Select): 323 from_ = expression.args.get("from_") 324 325 if from_ and isinstance(from_.this, exp.Unnest): 326 unnest: exp.Unnest = from_.this 327 alias: exp.TableAlias | None = unnest.args.get("alias") 328 exprs: list[exp.Expr] = unnest.expressions 329 has_multi_expr = len(exprs) > 1 330 this, *_ = _unnest_zip_exprs(unnest, exprs, has_multi_expr) 331 332 columns: list[exp.Identifier] = alias.columns if alias else [] 333 offset: exp.Expr | None = unnest.args.get("offset") 334 if offset: 335 columns.insert( 336 0, offset if isinstance(offset, exp.Identifier) else exp.to_identifier("pos") 337 ) 338 339 unnest.replace( 340 exp.Table( 341 this=_udtf_type(unnest, has_multi_expr)(this=this), 342 alias=exp.TableAlias(this=alias.this, columns=columns) if alias else None, 343 ) 344 ) 345 346 joins: list[exp.Join] = expression.args.get("joins") or [] 347 for join in list(joins): 348 join_expr = join.this 349 350 is_lateral = isinstance(join_expr, exp.Lateral) 351 352 unnest = join_expr.this if is_lateral else join_expr 353 354 if isinstance(unnest, exp.Unnest): 355 if is_lateral: 356 alias = join_expr.args.get("alias") 357 else: 358 alias = unnest.args.get("alias") 359 360 if alias is None: 361 raise UnsupportedError( 362 "CROSS JOIN UNNEST to LATERAL VIEW EXPLODE transformation requires an alias" 363 ) 364 365 exprs = unnest.expressions 366 # The number of unnest.expressions will be changed by _unnest_zip_exprs, we need to record it here 367 has_multi_expr = len(exprs) > 1 368 exprs = _unnest_zip_exprs(unnest, exprs, has_multi_expr) 369 370 joins.remove(join) 371 372 alias_cols: list[exp.Identifier] = alias.columns 373 374 # # Handle UNNEST to LATERAL VIEW EXPLODE: Exception is raised when there are 0 or > 2 aliases 375 # 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. 376 # Refs: https://spark.apache.org/docs/latest/sql-ref-syntax-qry-select-lateral-view.html 377 378 if not has_multi_expr and len(alias_cols) not in (1, 2): 379 raise UnsupportedError( 380 "CROSS JOIN UNNEST to LATERAL VIEW EXPLODE transformation requires explicit column aliases" 381 ) 382 383 offset = unnest.args.get("offset") 384 if offset: 385 alias_cols.insert( 386 0, 387 offset if isinstance(offset, exp.Identifier) else exp.to_identifier("pos"), 388 ) 389 390 for e, column in zip(exprs, alias_cols): 391 expression.append( 392 "laterals", 393 exp.Lateral( 394 this=_udtf_type(unnest, has_multi_expr)(this=e), 395 view=True, 396 alias=exp.TableAlias(this=alias.this, columns=alias_cols), 397 ), 398 ) 399 400 return expression
Convert cross join unnest into lateral view explode.
403def explode_projection_to_unnest( 404 index_offset: int = 0, 405) -> t.Callable[[exp.Expr], exp.Expr]: 406 """Convert explode/posexplode projections into unnests.""" 407 408 def _explode_projection_to_unnest(expression: exp.Expr) -> exp.Expr: 409 if isinstance(expression, exp.Select): 410 from sqlglot.optimizer.scope import Scope 411 412 taken_select_names = set(expression.named_selects) 413 taken_source_names = {name for name, _ in Scope(expression).references} 414 415 def new_name(names: set[str], name: str) -> str: 416 name = find_new_name(names, name) 417 names.add(name) 418 return name 419 420 arrays: list[exp.Condition] = [] 421 series_alias = new_name(taken_select_names, "pos") 422 series = exp.alias_( 423 exp.Unnest( 424 expressions=[exp.GenerateSeries(start=exp.Literal.number(index_offset))] 425 ), 426 new_name(taken_source_names, "_u"), 427 table=[series_alias], 428 ) 429 430 # we use list here because expression.selects is mutated inside the loop 431 for select in list(expression.selects): 432 explode = select.find(exp.Explode) 433 434 if explode: 435 pos_alias: t.Any = "" 436 explode_alias: t.Any = "" 437 438 if isinstance(select, exp.Alias): 439 explode_alias = select.args["alias"] 440 alias: exp.Expr = select 441 elif isinstance(select, exp.Aliases): 442 pos_alias = select.aliases[0] 443 explode_alias = select.aliases[1] 444 alias = select.replace(exp.alias_(select.this, "", copy=False)) 445 else: 446 alias = select.replace(exp.alias_(select, "")) 447 explode = alias.find(exp.Explode) 448 assert explode 449 450 is_posexplode = isinstance(explode, exp.Posexplode) 451 explode_arg = explode.this 452 453 if isinstance(explode, exp.ExplodeOuter): 454 bracket = explode_arg[0] 455 bracket.set("safe", True) 456 bracket.set("offset", True) 457 explode_arg = exp.func( 458 "IF", 459 exp.func( 460 "ARRAY_SIZE", exp.func("COALESCE", explode_arg, exp.Array()) 461 ).eq(0), 462 exp.array(bracket, copy=False), 463 explode_arg, 464 ) 465 466 # This ensures that we won't use [POS]EXPLODE's argument as a new selection 467 if isinstance(explode_arg, exp.Column): 468 taken_select_names.add(explode_arg.output_name) 469 470 unnest_source_alias = new_name(taken_source_names, "_u") 471 472 if not explode_alias: 473 explode_alias = new_name(taken_select_names, "col") 474 475 if is_posexplode: 476 pos_alias = new_name(taken_select_names, "pos") 477 478 if not pos_alias: 479 pos_alias = new_name(taken_select_names, "pos") 480 481 alias.set("alias", exp.to_identifier(explode_alias)) 482 483 series_table_alias = series.args["alias"].this 484 column = exp.If( 485 this=exp.column(series_alias, table=series_table_alias).eq( 486 exp.column(pos_alias, table=unnest_source_alias) 487 ), 488 true=exp.column(explode_alias, table=unnest_source_alias), 489 ) 490 491 explode.replace(column) 492 493 if is_posexplode: 494 expressions = expression.expressions 495 expressions.insert( 496 expressions.index(alias) + 1, 497 exp.If( 498 this=exp.column(series_alias, table=series_table_alias).eq( 499 exp.column(pos_alias, table=unnest_source_alias) 500 ), 501 true=exp.column(pos_alias, table=unnest_source_alias), 502 ).as_(pos_alias), 503 ) 504 expression.set("expressions", expressions) 505 506 if not arrays: 507 if expression.args.get("from_"): 508 expression.join(series, copy=False, join_type="CROSS") 509 else: 510 expression.from_(series, copy=False) 511 512 size: exp.Condition = exp.ArraySize(this=explode_arg.copy()) 513 arrays.append(size) 514 515 # trino doesn't support left join unnest with on conditions 516 # if it did, this would be much simpler 517 expression.join( 518 exp.alias_( 519 exp.Unnest( 520 expressions=[explode_arg.copy()], 521 offset=exp.to_identifier(pos_alias), 522 ), 523 unnest_source_alias, 524 table=[explode_alias], 525 ), 526 join_type="CROSS", 527 copy=False, 528 ) 529 530 if index_offset != 1: 531 size = size - 1 532 533 expression.where( 534 exp.column(series_alias, table=series_table_alias) 535 .eq(exp.column(pos_alias, table=unnest_source_alias)) 536 .or_( 537 (exp.column(series_alias, table=series_table_alias) > size).and_( 538 exp.column(pos_alias, table=unnest_source_alias).eq(size) 539 ) 540 ), 541 copy=False, 542 ) 543 544 if arrays: 545 end: exp.Condition = exp.Greatest(this=arrays[0], expressions=arrays[1:]) 546 547 if index_offset != 1: 548 end = end - (1 - index_offset) 549 series.expressions[0].set("end", end) 550 551 return expression 552 553 return _explode_projection_to_unnest
Convert explode/posexplode projections into unnests.
556def add_within_group_for_percentiles(expression: exp.Expr) -> exp.Expr: 557 """Transforms percentiles by adding a WITHIN GROUP clause to them.""" 558 if ( 559 isinstance(expression, exp.PERCENTILES) 560 and not isinstance(expression.parent, exp.WithinGroup) 561 and expression.expression 562 ): 563 column = expression.this.pop() 564 expression.set("this", expression.expression.pop()) 565 order = exp.Order(expressions=[exp.Ordered(this=column)]) 566 expression = exp.WithinGroup(this=expression, expression=order) 567 568 return expression
Transforms percentiles by adding a WITHIN GROUP clause to them.
571def remove_within_group_for_percentiles(expression: exp.Expr) -> exp.Expr: 572 """Transforms percentiles by getting rid of their corresponding WITHIN GROUP clause.""" 573 if ( 574 isinstance(expression, exp.WithinGroup) 575 and isinstance(expression.this, exp.PERCENTILES) 576 and isinstance(expression.expression, exp.Order) 577 ): 578 quantile = expression.this.this 579 input_value = t.cast(exp.Ordered, expression.find(exp.Ordered)).this 580 return expression.replace(exp.ApproxQuantile(this=input_value, quantile=quantile)) 581 582 return expression
Transforms percentiles by getting rid of their corresponding WITHIN GROUP clause.
585def add_recursive_cte_column_names(expression: exp.Expr) -> exp.Expr: 586 """Uses projection output names in recursive CTE definitions to define the CTEs' columns.""" 587 if isinstance(expression, exp.With) and expression.recursive: 588 next_name = name_sequence("_c_") 589 590 for cte in expression.expressions: 591 if not cte.args["alias"].columns: 592 query = cte.this 593 if isinstance(query, exp.SetOperation): 594 query = query.this 595 596 cte.args["alias"].set( 597 "columns", 598 [exp.to_identifier(s.alias_or_name or next_name()) for s in query.selects], 599 ) 600 601 return expression
Uses projection output names in recursive CTE definitions to define the CTEs' columns.
604def epoch_cast_to_ts(expression: exp.Expr) -> exp.Expr: 605 """Replace 'epoch' in casts by the equivalent date literal.""" 606 if ( 607 isinstance(expression, (exp.Cast, exp.TryCast)) 608 and expression.name.lower() == "epoch" 609 and expression.to.this in exp.DataType.TEMPORAL_TYPES 610 ): 611 expression.this.replace(exp.Literal.string("1970-01-01 00:00:00")) 612 613 return expression
Replace 'epoch' in casts by the equivalent date literal.
616def eliminate_semi_and_anti_joins(expression: exp.Expr) -> exp.Expr: 617 """Convert SEMI and ANTI joins into equivalent forms that use EXIST instead.""" 618 if isinstance(expression, exp.Select): 619 for join in list[exp.Join](expression.args.get("joins") or []): 620 on: exp.Expr | None = join.args.get("on") 621 if on and join.kind in ("SEMI", "ANTI"): 622 subquery = exp.select("1").from_(join.this).where(on) 623 exists: exp.Exists | exp.Not = exp.Exists(this=subquery) 624 if join.kind == "ANTI": 625 exists = exists.not_(copy=False) 626 627 join.pop() 628 expression.where(exists, copy=False) 629 630 return expression
Convert SEMI and ANTI joins into equivalent forms that use EXIST instead.
633def eliminate_full_outer_join(expression: exp.Expr) -> exp.Expr: 634 """ 635 Converts a query with a FULL OUTER join to a union of identical queries that 636 use LEFT/RIGHT OUTER joins instead. This transformation currently only works 637 for queries that have a single FULL OUTER join. 638 """ 639 if isinstance(expression, exp.Select): 640 full_outer_joins: list[tuple[int, exp.Join]] = [ 641 (index, join) 642 for index, join in enumerate[exp.Join](expression.args.get("joins") or []) 643 if join.side == "FULL" 644 ] 645 646 if len(full_outer_joins) == 1: 647 expression_copy = expression.copy() 648 expression.set("limit", None) 649 index, full_outer_join = full_outer_joins[0] 650 651 tables = (expression.args["from_"].alias_or_name, full_outer_join.alias_or_name) 652 join_conditions = full_outer_join.args.get("on") or exp.and_( 653 *[ 654 exp.column(col, tables[0]).eq(exp.column(col, tables[1])) 655 for col in t.cast(list[exp.Identifier], full_outer_join.args.get("using")) 656 ] 657 ) 658 659 full_outer_join.set("side", "left") 660 anti_join_clause = ( 661 exp.select("1").from_(expression.args["from_"]).where(join_conditions) 662 ) 663 expression_copy.args["joins"][index].set("side", "right") 664 expression_copy = expression_copy.where(exp.Exists(this=anti_join_clause).not_()) 665 expression_copy.set("with_", None) # remove CTEs from RIGHT side 666 expression.set("order", None) # remove order by from LEFT side 667 668 return exp.union(expression, expression_copy, copy=False, distinct=False) 669 670 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.
673def move_ctes_to_top_level(expression: E) -> E: 674 """ 675 Some dialects (e.g. Hive, T-SQL, Spark prior to version 3) only allow CTEs to be 676 defined at the top-level, so for example queries like: 677 678 SELECT * FROM (WITH t(c) AS (SELECT 1) SELECT * FROM t) AS subq 679 680 are invalid in those dialects. This transformation can be used to ensure all CTEs are 681 moved to the top level so that the final SQL code is valid from a syntax standpoint. 682 683 TODO: handle name clashes whilst moving CTEs (it can get quite tricky & costly). 684 """ 685 top_level_with: exp.With | None = expression.args.get("with_") 686 for inner_with in expression.find_all(exp.With): 687 if inner_with.parent is expression: 688 continue 689 690 if not top_level_with: 691 top_level_with = inner_with.pop() 692 expression.set("with_", top_level_with) 693 else: 694 if inner_with.recursive: 695 top_level_with.set("recursive", True) 696 697 parent_cte = inner_with.find_ancestor(exp.CTE) 698 inner_with.pop() 699 700 if parent_cte: 701 i = top_level_with.expressions.index(parent_cte) 702 top_level_with.expressions[i:i] = inner_with.expressions 703 top_level_with.set("expressions", top_level_with.expressions) 704 else: 705 top_level_with.set( 706 "expressions", top_level_with.expressions + inner_with.expressions 707 ) 708 709 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).
712def ensure_bools(expression: exp.Expr) -> exp.Expr: 713 """Converts numeric values used in conditions into explicit boolean expressions.""" 714 from sqlglot.optimizer.canonicalize import ensure_bools 715 716 def _ensure_bool(node: exp.Expr) -> None: 717 if ( 718 node.is_number 719 or ( 720 not isinstance(node, exp.SubqueryPredicate) 721 and node.is_type(exp.DType.UNKNOWN, *exp.DataType.NUMERIC_TYPES) 722 ) 723 or (isinstance(node, exp.Column) and not node.type) 724 ): 725 node.replace(node.neq(0)) 726 727 for node in expression.walk(): 728 ensure_bools(node, _ensure_bool) 729 730 return expression
Converts numeric values used in conditions into explicit boolean expressions.
742def unqualify_pivot_fields(expression: exp.Expr) -> exp.Expr: 743 """ 744 Some dialects only accept simple column names in a (UN)PIVOT's FOR clause and IN-list 745 (Oracle raises ORA-01748), even though the aggregate itself may stay qualified. 746 747 Example: 748 >>> from sqlglot import parse_one 749 >>> expr = parse_one("SELECT * FROM tbl PIVOT (SUM(tbl.sales) FOR tbl.quarter IN ('Q1', 'Q2'))") 750 >>> print(unqualify_pivot_fields(expr).sql(dialect="spark")) 751 SELECT * FROM tbl PIVOT(SUM(tbl.sales) FOR quarter IN ('Q1', 'Q2')) 752 """ 753 if isinstance(expression, exp.Pivot): 754 expression.set("fields", [unqualify_columns(field) for field in expression.fields]) 755 756 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'))
768def ctas_with_tmp_tables_to_create_tmp_view( 769 expression: exp.Expr, 770 tmp_storage_provider: t.Callable[[exp.Expr], exp.Expr] = lambda e: e, 771) -> exp.Expr: 772 assert isinstance(expression, exp.Create) 773 properties: exp.Properties | None = expression.args.get("properties") 774 temporary = any( 775 isinstance(prop, exp.TemporaryProperty) 776 for prop in (properties.expressions if properties is not None else []) 777 ) 778 779 # CTAS with temp tables map to CREATE TEMPORARY VIEW 780 if expression.kind == "TABLE" and temporary: 781 if expression.expression: 782 return exp.Create( 783 kind="TEMPORARY VIEW", 784 this=expression.this, 785 expression=expression.expression, 786 ) 787 return tmp_storage_provider(expression) 788 789 return expression
792def move_schema_columns_to_partitioned_by(expression: exp.Expr) -> exp.Expr: 793 """ 794 In Hive, the PARTITIONED BY property acts as an extension of a table's schema. When the 795 PARTITIONED BY value is an array of column names, they are transformed into a schema. 796 The corresponding columns are removed from the create statement. 797 """ 798 assert isinstance(expression, exp.Create) 799 schema = expression.this 800 is_partitionable = expression.kind in {"TABLE", "VIEW"} 801 802 if isinstance(schema, exp.Schema) and is_partitionable: 803 prop = expression.find(exp.PartitionedByProperty) 804 if prop and prop.this and not isinstance(prop.this, exp.Schema): 805 columns: set[str] = {v.name.upper() for v in prop.this.expressions} 806 schema_exprs: list[exp.Expr] = schema.expressions 807 partitions = [col for col in schema_exprs if col.name.upper() in columns] 808 schema.set("expressions", [e for e in schema_exprs if e not in partitions]) 809 prop.replace(exp.PartitionedByProperty(this=exp.Schema(expressions=partitions))) 810 expression.set("this", schema) 811 812 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.
815def move_partitioned_by_to_schema_columns(expression: exp.Expr) -> exp.Expr: 816 """ 817 Spark 3 supports both "HIVEFORMAT" and "DATASOURCE" formats for CREATE TABLE. 818 819 Currently, SQLGlot uses the DATASOURCE format for Spark 3. 820 """ 821 assert isinstance(expression, exp.Create) 822 prop = expression.find(exp.PartitionedByProperty) 823 if ( 824 prop 825 and prop.this 826 and isinstance(prop.this, exp.Schema) 827 and all(isinstance(e, exp.ColumnDef) and e.kind for e in prop.this.expressions) 828 ): 829 prop_this = exp.Tuple( 830 expressions=[exp.to_identifier(e.this) for e in prop.this.expressions] 831 ) 832 schema: exp.Schema = expression.this 833 for e in prop.this.expressions: 834 schema.append("expressions", e) 835 prop.set("this", prop_this) 836 837 return expression
Spark 3 supports both "HIVEFORMAT" and "DATASOURCE" formats for CREATE TABLE.
Currently, SQLGlot uses the DATASOURCE format for Spark 3.
840def struct_kv_to_alias(expression: exp.Expr) -> exp.Expr: 841 """Converts struct arguments to aliases, e.g. STRUCT(1 AS y).""" 842 if isinstance(expression, exp.Struct): 843 expression.set( 844 "expressions", 845 [ 846 exp.alias_(e.expression, e.this) if isinstance(e, exp.PropertyEQ) else e 847 for e in expression.expressions 848 ], 849 ) 850 851 return expression
Converts struct arguments to aliases, e.g. STRUCT(1 AS y).
854def eliminate_join_marks(expression: exp.Expr) -> exp.Expr: 855 """https://docs.oracle.com/cd/B19306_01/server.102/b14200/queries006.htm#sthref3178 856 857 1. You cannot specify the (+) operator in a query block that also contains FROM clause join syntax. 858 859 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. 860 861 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. 862 863 You cannot use the (+) operator to outer-join a table to itself, although self joins are valid. 864 865 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. 866 867 A WHERE condition containing the (+) operator cannot be combined with another condition using the OR logical operator. 868 869 A WHERE condition cannot use the IN comparison condition to compare a column marked with the (+) operator with an expression. 870 871 A WHERE condition cannot compare any column marked with the (+) operator with a subquery. 872 873 -- example with WHERE 874 SELECT d.department_name, sum(e.salary) as total_salary 875 FROM departments d, employees e 876 WHERE e.department_id(+) = d.department_id 877 group by department_name 878 879 -- example of left correlation in select 880 SELECT d.department_name, ( 881 SELECT SUM(e.salary) 882 FROM employees e 883 WHERE e.department_id(+) = d.department_id) AS total_salary 884 FROM departments d; 885 886 -- example of left correlation in from 887 SELECT d.department_name, t.total_salary 888 FROM departments d, ( 889 SELECT SUM(e.salary) AS total_salary 890 FROM employees e 891 WHERE e.department_id(+) = d.department_id 892 ) t 893 """ 894 895 from sqlglot.optimizer.scope import traverse_scope 896 from sqlglot.optimizer.normalize import normalize, normalized 897 from collections import defaultdict 898 899 # we go in reverse to check the main query for left correlation 900 for scope in reversed(traverse_scope(expression)): 901 query = scope.expression 902 903 where: exp.Expr | None = query.args.get("where") 904 joins: list[exp.Join] = query.args.get("joins", []) 905 906 if not where or not any(c.args.get("join_mark") for c in where.find_all(exp.Column)): 907 continue 908 909 # knockout: we do not support left correlation (see point 2) 910 assert not scope.is_correlated_subquery, "Correlated queries are not supported" 911 912 # make sure we have AND of ORs to have clear join terms 913 where = normalize(where.this) 914 assert normalized(where), "Cannot normalize JOIN predicates" 915 # dict of {name: list of join AND conditions} 916 joins_ons: defaultdict[str, list[exp.Expr]] = defaultdict(list) 917 for cond in [where] if not isinstance(where, exp.And) else where.flatten(): 918 join_cols = [col for col in cond.find_all(exp.Column) if col.args.get("join_mark")] 919 920 left_join_table = set(col.table for col in join_cols) 921 if not left_join_table: 922 continue 923 924 assert not (len(left_join_table) > 1), ( 925 "Cannot combine JOIN predicates from different tables" 926 ) 927 928 for col in join_cols: 929 col.set("join_mark", False) 930 931 joins_ons[left_join_table.pop()].append(cond) 932 933 old_joins = {join.alias_or_name: join for join in joins} 934 new_joins: dict[str, exp.Join] = {} 935 query_from = query.args["from_"] 936 937 for table, predicates in joins_ons.items(): 938 join_what = old_joins.get(table, query_from).this.copy() 939 new_joins[join_what.alias_or_name] = exp.Join( 940 this=join_what, on=exp.and_(*predicates), kind="LEFT" 941 ) 942 943 for p in predicates: 944 while isinstance(p.parent, exp.Paren): 945 p.parent.replace(p) 946 947 parent = p.parent 948 p.pop() 949 if isinstance(parent, exp.Binary): 950 left = parent.args.get("this") 951 parent.replace(parent.right if left is None else left) 952 elif isinstance(parent, exp.Where): 953 parent.pop() 954 955 if query_from.alias_or_name in new_joins: 956 only_old_joins: set[str] = old_joins.keys() - new_joins.keys() 957 assert len(only_old_joins) >= 1, ( 958 "Cannot determine which table to use in the new FROM clause" 959 ) 960 961 new_from_name = list[str](only_old_joins)[0] 962 query.set("from_", exp.From(this=old_joins[new_from_name].this)) 963 964 if new_joins: 965 for n, j in old_joins.items(): # preserve any other joins 966 if n not in new_joins and n != query.args["from_"].name: 967 if not j.kind: 968 j.set("kind", "CROSS") 969 new_joins[n] = j 970 query.set("joins", list(new_joins.values())) 971 972 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
975def any_to_exists(expression: exp.Expr) -> exp.Expr: 976 """ 977 Transform ANY operator to Spark's EXISTS 978 979 For example, 980 - Postgres: SELECT * FROM tbl WHERE 5 > ANY(tbl.col) 981 - Spark: SELECT * FROM tbl WHERE EXISTS(tbl.col, x -> x < 5) 982 983 Both ANY and EXISTS accept queries but currently only array expressions are supported for this 984 transformation 985 """ 986 if isinstance(expression, exp.Select): 987 for any_expr in expression.find_all(exp.Any): 988 this: exp.Expr = any_expr.this 989 if isinstance(this, exp.Query) or isinstance(any_expr.parent, (exp.Like, exp.ILike)): 990 continue 991 992 binop = any_expr.parent 993 if isinstance(binop, exp.Binary): 994 lambda_arg = exp.to_identifier("x") 995 any_expr.replace(lambda_arg) 996 lambda_expr = exp.Lambda(this=binop.copy(), expressions=[lambda_arg]) 997 binop.replace(exp.Exists(this=this.unnest(), expression=lambda_expr)) 998 999 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
1002def eliminate_window_clause(expression: exp.Expr) -> exp.Expr: 1003 """Eliminates the `WINDOW` query clause by inling each named window.""" 1004 windows: list[exp.Expr] | None = expression.args.get("windows") 1005 if isinstance(expression, exp.Select) and windows is not None: 1006 from sqlglot.optimizer.scope import find_all_in_scope 1007 1008 expression.set("windows", None) 1009 1010 window_expression: dict[str, exp.Expr] = {} 1011 1012 def _inline_inherited_window(window: exp.Expr) -> None: 1013 inherited_window = window_expression.get(window.alias.lower()) 1014 if not inherited_window: 1015 return 1016 1017 window.set("alias", None) 1018 for key in ("partition_by", "order", "spec"): 1019 arg: exp.Expr | None = inherited_window.args.get(key) 1020 if arg is not None: 1021 window.set(key, arg.copy()) 1022 1023 for window in windows: 1024 _inline_inherited_window(window) 1025 window_expression[window.name.lower()] = window 1026 1027 for window in find_all_in_scope(expression, exp.Window): 1028 _inline_inherited_window(window) 1029 1030 return expression
Eliminates the WINDOW query clause by inling each named window.
1033def inherit_struct_field_names(expression: exp.Expr) -> exp.Expr: 1034 """ 1035 Inherit field names from the first struct in an array. 1036 1037 BigQuery supports implicitly inheriting names from the first STRUCT in an array: 1038 1039 Example: 1040 ARRAY[ 1041 STRUCT('Alice' AS name, 85 AS score), -- defines names 1042 STRUCT('Bob', 92), -- inherits names 1043 STRUCT('Diana', 95) -- inherits names 1044 ] 1045 1046 This transformation makes the field names explicit on all structs by adding 1047 PropertyEQ nodes, in order to facilitate transpilation to other dialects. 1048 1049 Args: 1050 expression: The expression tree to transform 1051 1052 Returns: 1053 The modified expression with field names inherited in all structs 1054 """ 1055 if ( 1056 isinstance(expression, exp.Array) 1057 and expression.args.get("struct_name_inheritance") 1058 and isinstance(first_item := seq_get(expression.expressions, 0), exp.Struct) 1059 and all(isinstance(fld, exp.PropertyEQ) for fld in first_item.expressions) 1060 ): 1061 field_names: list[exp.Identifier] = [fld.this for fld in first_item.expressions] 1062 1063 # Apply field names to subsequent structs that don't have them 1064 for struct in expression.expressions[1:]: 1065 if not isinstance(struct, exp.Struct) or len(struct.expressions) != len(field_names): 1066 continue 1067 1068 # Convert unnamed expressions to PropertyEQ with inherited names 1069 new_expressions: list[exp.PropertyEQ] = [] 1070 for i, expr in enumerate(struct.expressions): 1071 if not isinstance(expr, exp.PropertyEQ): 1072 # Create PropertyEQ: field_name := value, preserving the type from the inner expression 1073 property_eq = exp.PropertyEQ( 1074 this=field_names[i].copy(), 1075 expression=expr, 1076 ) 1077 property_eq.type = expr.type 1078 new_expressions.append(property_eq) 1079 else: 1080 new_expressions.append(expr) 1081 1082 struct.set("expressions", new_expressions) 1083 1084 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