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