Edit on GitHub

sqlglot.transforms

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

def unnest_generate_date_array_using_recursive_cte( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
 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
def unnest_generate_series( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
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.

def eliminate_distinct_on( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
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.

def eliminate_qualify( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
202def eliminate_qualify(expression: exp.Expr) -> exp.Expr:
203    """
204    Convert SELECT statements that contain the QUALIFY clause into subqueries, filtered equivalently.
205
206    The idea behind this transformation can be seen in Snowflake's documentation for QUALIFY:
207    https://docs.snowflake.com/en/sql-reference/constructs/qualify
208
209    Some dialects don't support window functions in the WHERE clause, so we need to include them as
210    projections in the subquery, in order to refer to them in the outer filter using aliases. Also,
211    if a column is referenced in the QUALIFY clause but is not selected, we need to include it too,
212    otherwise we won't be able to refer to it in the outer query's WHERE clause. Finally, if a
213    newly aliased projection is referenced in the QUALIFY clause, it will be replaced by the
214    corresponding expression to avoid creating invalid column references.
215    """
216    if isinstance(expression, exp.Select) and expression.args.get("qualify"):
217        parent = expression.parent
218        if isinstance(parent, exp.Insert) and isinstance(parent.parent, exp.MultitableInserts):
219            # A FROM-first multi-table INSERT branch shares its parent's FROM, so it can't be wrapped
220            raise UnsupportedError(
221                "QUALIFY is not supported in a FROM-first multi-table INSERT branch"
222            )
223
224        taken = set(expression.named_selects)
225        for select in expression.selects:
226            if not select.alias_or_name:
227                alias = find_new_name(taken, "_c")
228                select.replace(exp.alias_(select, alias))
229                taken.add(alias)
230
231        def _select_alias_or_name(select: exp.Expr) -> str | exp.Column:
232            alias_or_name = select.alias_or_name
233            identifier = select.args.get("alias") or select.this
234            if isinstance(identifier, exp.Identifier):
235                return exp.column(alias_or_name, quoted=identifier.args.get("quoted"))
236            return alias_or_name
237
238        outer_selects = exp.select(*map(_select_alias_or_name, expression.selects))
239        qualify_filters: exp.Expr = expression.args["qualify"].pop().this
240        expression_by_alias: dict[str, exp.Expr] = {
241            select.alias: select.this
242            for select in expression.selects
243            if isinstance(select, exp.Alias)
244        }
245
246        select_candidates = (exp.Window,) if expression.is_star else (exp.Window, exp.Column)
247        for select_candidate in list(qualify_filters.find_all(*select_candidates)):
248            if isinstance(select_candidate, exp.Window):
249                if expression_by_alias:
250                    for column in select_candidate.find_all(exp.Column):
251                        expr = expression_by_alias.get(column.name)
252                        if expr:
253                            column.replace(expr)
254
255                alias = find_new_name(expression.named_selects, "_w")
256                expression.select(exp.alias_(select_candidate, alias), copy=False)
257                column = exp.column(alias)
258
259                if isinstance(select_candidate.parent, exp.Qualify):
260                    qualify_filters = column
261                else:
262                    select_candidate.replace(column)
263            elif select_candidate.name not in expression.named_selects and not (
264                select_candidate.find_ancestor(exp.Window)
265            ):
266                # A column that is only read by a window function is computed by the window
267                # itself, so projecting it in the subquery is redundant and can even produce
268                # invalid SQL, e.g. when the query is grouped
269                expression.select(select_candidate.copy(), copy=False)
270
271        return outer_selects.from_(expression.subquery(alias="_t", copy=False), copy=False).where(
272            qualify_filters, copy=False
273        )
274
275    return expression

Convert SELECT statements that contain the QUALIFY clause into subqueries, filtered equivalently.

The idea behind this transformation can be seen in Snowflake's documentation for QUALIFY: https://docs.snowflake.com/en/sql-reference/constructs/qualify

Some dialects don't support window functions in the WHERE clause, so we need to include them as projections in the subquery, in order to refer to them in the outer filter using aliases. Also, if a column is referenced in the QUALIFY clause but is not selected, we need to include it too, otherwise we won't be able to refer to it in the outer query's WHERE clause. Finally, if a newly aliased projection is referenced in the QUALIFY clause, it will be replaced by the corresponding expression to avoid creating invalid column references.

def remove_precision_parameterized_types( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
278def remove_precision_parameterized_types(expression: exp.Expr) -> exp.Expr:
279    """
280    Some dialects only allow the precision for parameterized types to be defined in the DDL and not in
281    other expressions. This transforms removes the precision from parameterized types in expressions.
282    """
283    for node in expression.find_all(exp.DataType):
284        node.set(
285            "expressions", [e for e in node.expressions if not isinstance(e, exp.DataTypeParam)]
286        )
287
288    return expression

Some dialects only allow the precision for parameterized types to be defined in the DDL and not in other expressions. This transforms removes the precision from parameterized types in expressions.

def unqualify_unnest( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
291def unqualify_unnest(expression: exp.Expr) -> exp.Expr:
292    """Remove references to unnest table aliases, added by the optimizer's qualify_columns step."""
293    from sqlglot.optimizer.scope import find_all_in_scope
294
295    if isinstance(expression, exp.Select):
296        unnest_aliases = {
297            unnest.alias
298            for unnest in find_all_in_scope(expression, exp.Unnest)
299            if isinstance(unnest.parent, (exp.From, exp.Join))
300        }
301        if unnest_aliases:
302            for column in expression.find_all(exp.Column):
303                leftmost_part = column.parts[0]
304                if leftmost_part.arg_key != "this" and leftmost_part.this in unnest_aliases:
305                    leftmost_part.pop()
306
307    return expression

Remove references to unnest table aliases, added by the optimizer's qualify_columns step.

def unnest_to_explode( expression: sqlglot.expressions.core.Expr, unnest_using_arrays_zip: bool = True) -> sqlglot.expressions.core.Expr:
310def unnest_to_explode(
311    expression: exp.Expr,
312    unnest_using_arrays_zip: bool = True,
313) -> exp.Expr:
314    """Convert cross join unnest into lateral view explode."""
315
316    def _unnest_zip_exprs(
317        u: exp.Unnest, unnest_exprs: list[exp.Expr], has_multi_expr: bool
318    ) -> list[exp.Expr]:
319        if has_multi_expr:
320            if not unnest_using_arrays_zip:
321                raise UnsupportedError("Cannot transpile UNNEST with multiple input arrays")
322
323            # Use INLINE(ARRAYS_ZIP(...)) for multiple expressions
324            zip_exprs: list[exp.Expr] = [exp.Anonymous(this="ARRAYS_ZIP", expressions=unnest_exprs)]
325            u.set("expressions", zip_exprs)
326            return zip_exprs
327        return unnest_exprs
328
329    def _udtf_type(u: exp.Unnest, has_multi_expr: bool) -> type[exp.Func]:
330        if u.args.get("offset"):
331            return exp.Posexplode
332        return exp.Inline if has_multi_expr else exp.Explode
333
334    if isinstance(expression, exp.Select):
335        from_ = expression.args.get("from_")
336
337        if from_ and isinstance(from_.this, exp.Unnest):
338            unnest: exp.Unnest = from_.this
339            alias: exp.TableAlias | None = unnest.args.get("alias")
340            exprs: list[exp.Expr] = unnest.expressions
341            has_multi_expr = len(exprs) > 1
342            this, *_ = _unnest_zip_exprs(unnest, exprs, has_multi_expr)
343
344            columns: list[exp.Identifier] = alias.columns if alias else []
345            offset: exp.Expr | None = unnest.args.get("offset")
346            if offset:
347                columns.insert(
348                    0, offset if isinstance(offset, exp.Identifier) else exp.to_identifier("pos")
349                )
350
351            unnest.replace(
352                exp.Table(
353                    this=_udtf_type(unnest, has_multi_expr)(this=this),
354                    alias=exp.TableAlias(this=alias.this, columns=columns) if alias else None,
355                )
356            )
357
358        joins: list[exp.Join] = expression.args.get("joins") or []
359        for join in list(joins):
360            join_expr = join.this
361
362            is_lateral = isinstance(join_expr, exp.Lateral)
363
364            unnest = join_expr.this if is_lateral else join_expr
365
366            if isinstance(unnest, exp.Unnest):
367                if is_lateral:
368                    alias = join_expr.args.get("alias")
369                else:
370                    alias = unnest.args.get("alias")
371
372                if alias is None:
373                    raise UnsupportedError(
374                        "CROSS JOIN UNNEST to LATERAL VIEW EXPLODE transformation requires an alias"
375                    )
376
377                exprs = unnest.expressions
378                # The number of unnest.expressions will be changed by _unnest_zip_exprs, we need to record it here
379                has_multi_expr = len(exprs) > 1
380                exprs = _unnest_zip_exprs(unnest, exprs, has_multi_expr)
381
382                joins.remove(join)
383
384                alias_cols: list[exp.Identifier] = alias.columns
385
386                # # Handle UNNEST to LATERAL VIEW EXPLODE: Exception is raised when there are 0 or > 2 aliases
387                # Spark LATERAL VIEW EXPLODE requires single alias for array/struct and two for Map type column unlike unnest in trino/presto which can take an arbitrary amount.
388                # Refs: https://spark.apache.org/docs/latest/sql-ref-syntax-qry-select-lateral-view.html
389
390                if not has_multi_expr and len(alias_cols) not in (1, 2):
391                    raise UnsupportedError(
392                        "CROSS JOIN UNNEST to LATERAL VIEW EXPLODE transformation requires explicit column aliases"
393                    )
394
395                offset = unnest.args.get("offset")
396                if offset:
397                    alias_cols.insert(
398                        0,
399                        offset if isinstance(offset, exp.Identifier) else exp.to_identifier("pos"),
400                    )
401
402                for e, column in zip(exprs, alias_cols):
403                    expression.append(
404                        "laterals",
405                        exp.Lateral(
406                            this=_udtf_type(unnest, has_multi_expr)(this=e),
407                            view=True,
408                            alias=exp.TableAlias(this=alias.this, columns=alias_cols),
409                        ),
410                    )
411
412    return expression

Convert cross join unnest into lateral view explode.

def explode_projection_to_unnest( index_offset: int = 0, unnest_map: bool = False) -> Callable[[sqlglot.expressions.core.Expr], sqlglot.expressions.core.Expr]:
415def explode_projection_to_unnest(
416    index_offset: int = 0,
417    unnest_map: bool = False,
418) -> t.Callable[[exp.Expr], exp.Expr]:
419    """Convert explode/posexplode projections into unnests."""
420
421    def _explode_projection_to_unnest(expression: exp.Expr) -> exp.Expr:
422        if isinstance(expression, exp.Select):
423            from sqlglot.optimizer.scope import Scope
424
425            taken_select_names = set(expression.named_selects)
426            taken_source_names = {name for name, _ in Scope(expression).references}
427
428            def new_name(names: set[str], name: str) -> str:
429                name = find_new_name(names, name)
430                names.add(name)
431                return name
432
433            arrays: list[exp.Condition] = []
434            series_alias = new_name(taken_select_names, "pos")
435            series = exp.alias_(
436                exp.Unnest(
437                    expressions=[exp.GenerateSeries(start=exp.Literal.number(index_offset))]
438                ),
439                new_name(taken_source_names, "_u"),
440                table=[series_alias],
441            )
442
443            # we use list here because expression.selects is mutated inside the loop
444            for select in list(expression.selects):
445                explode = select.find(exp.Explode)
446
447                if explode:
448                    if (
449                        unnest_map
450                        and type(explode) is exp.Explode
451                        and explode.this.is_type(exp.DType.MAP)
452                        and (select is explode or isinstance(select, exp.Aliases))
453                    ):
454                        map_key_alias: t.Any
455                        map_value_alias: t.Any
456                        if isinstance(select, exp.Aliases):
457                            map_key_alias, map_value_alias = select.aliases
458                        else:
459                            map_key_alias = new_name(taken_select_names, "key")
460                            map_value_alias = new_name(taken_select_names, "value")
461                        map_unnest_source = new_name(taken_source_names, "_u")
462
463                        map_key_select = select.replace(
464                            exp.column(map_key_alias, table=map_unnest_source).as_(map_key_alias)
465                        )
466
467                        expressions = expression.expressions
468                        expressions.insert(
469                            expressions.index(map_key_select) + 1,
470                            exp.column(map_value_alias, table=map_unnest_source).as_(
471                                map_value_alias
472                            ),
473                        )
474                        expression.set("expressions", expressions)
475
476                        unnest = exp.alias_(
477                            exp.Unnest(expressions=[explode.this.copy()]),
478                            map_unnest_source,
479                            table=[map_key_alias, map_value_alias],
480                        )
481                        if expression.args.get("from_"):
482                            expression.join(unnest, copy=False, join_type="CROSS")
483                        else:
484                            expression.from_(unnest, copy=False)
485
486                        continue
487
488                    pos_alias: t.Any = ""
489                    explode_alias: t.Any = ""
490
491                    if isinstance(select, exp.Alias):
492                        explode_alias = select.args["alias"]
493                        alias: exp.Expr = select
494                    elif isinstance(select, exp.Aliases):
495                        pos_alias = select.aliases[0]
496                        explode_alias = select.aliases[1]
497                        alias = select.replace(exp.alias_(select.this, "", copy=False))
498                    else:
499                        alias = select.replace(exp.alias_(select, ""))
500                        explode = alias.find(exp.Explode)
501                        assert explode
502
503                    is_posexplode = isinstance(explode, exp.Posexplode)
504                    explode_arg = explode.this
505
506                    if isinstance(explode, exp.ExplodeOuter):
507                        bracket = explode_arg[0]
508                        bracket.set("safe", True)
509                        bracket.set("offset", True)
510                        explode_arg = exp.func(
511                            "IF",
512                            exp.func(
513                                "ARRAY_SIZE", exp.func("COALESCE", explode_arg, exp.Array())
514                            ).eq(0),
515                            exp.array(bracket, copy=False),
516                            explode_arg,
517                        )
518
519                    # This ensures that we won't use [POS]EXPLODE's argument as a new selection
520                    if isinstance(explode_arg, exp.Column):
521                        taken_select_names.add(explode_arg.output_name)
522
523                    unnest_source_alias = new_name(taken_source_names, "_u")
524
525                    if not explode_alias:
526                        explode_alias = new_name(taken_select_names, "col")
527
528                        if is_posexplode:
529                            pos_alias = new_name(taken_select_names, "pos")
530
531                    if not pos_alias:
532                        pos_alias = new_name(taken_select_names, "pos")
533
534                    alias.set("alias", exp.to_identifier(explode_alias))
535
536                    series_table_alias = series.args["alias"].this
537                    column = exp.If(
538                        this=exp.column(series_alias, table=series_table_alias).eq(
539                            exp.column(pos_alias, table=unnest_source_alias)
540                        ),
541                        true=exp.column(explode_alias, table=unnest_source_alias),
542                    )
543
544                    explode.replace(column)
545
546                    if is_posexplode:
547                        expressions = expression.expressions
548                        expressions.insert(
549                            expressions.index(alias) + 1,
550                            exp.If(
551                                this=exp.column(series_alias, table=series_table_alias).eq(
552                                    exp.column(pos_alias, table=unnest_source_alias)
553                                ),
554                                true=exp.column(pos_alias, table=unnest_source_alias),
555                            ).as_(pos_alias),
556                        )
557                        expression.set("expressions", expressions)
558
559                    if not arrays:
560                        if expression.args.get("from_"):
561                            expression.join(series, copy=False, join_type="CROSS")
562                        else:
563                            expression.from_(series, copy=False)
564
565                    size: exp.Condition = exp.ArraySize(this=explode_arg.copy())
566                    arrays.append(size)
567
568                    # trino doesn't support left join unnest with on conditions
569                    # if it did, this would be much simpler
570                    expression.join(
571                        exp.alias_(
572                            exp.Unnest(
573                                expressions=[explode_arg.copy()],
574                                offset=exp.to_identifier(pos_alias),
575                            ),
576                            unnest_source_alias,
577                            table=[explode_alias],
578                        ),
579                        join_type="CROSS",
580                        copy=False,
581                    )
582
583                    if index_offset != 1:
584                        size = size - 1
585
586                    expression.where(
587                        exp.column(series_alias, table=series_table_alias)
588                        .eq(exp.column(pos_alias, table=unnest_source_alias))
589                        .or_(
590                            (exp.column(series_alias, table=series_table_alias) > size).and_(
591                                exp.column(pos_alias, table=unnest_source_alias).eq(size)
592                            )
593                        ),
594                        copy=False,
595                    )
596
597            if arrays:
598                end: exp.Condition = exp.Greatest(this=arrays[0], expressions=arrays[1:])
599
600                if index_offset != 1:
601                    end = end - (1 - index_offset)
602                series.expressions[0].set("end", end)
603
604        return expression
605
606    return _explode_projection_to_unnest

Convert explode/posexplode projections into unnests.

def add_within_group_for_percentiles( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
609def add_within_group_for_percentiles(expression: exp.Expr) -> exp.Expr:
610    """Transforms percentiles by adding a WITHIN GROUP clause to them."""
611    if (
612        isinstance(expression, exp.PERCENTILES)
613        and not isinstance(expression.parent, exp.WithinGroup)
614        and expression.expression
615    ):
616        column = expression.this.pop()
617        expression.set("this", expression.expression.pop())
618        order = exp.Order(expressions=[exp.Ordered(this=column)])
619        expression = exp.WithinGroup(this=expression, expression=order)
620
621    return expression

Transforms percentiles by adding a WITHIN GROUP clause to them.

def remove_within_group_for_percentiles( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
624def remove_within_group_for_percentiles(expression: exp.Expr) -> exp.Expr:
625    """Transforms percentiles by getting rid of their corresponding WITHIN GROUP clause."""
626    if (
627        isinstance(expression, exp.WithinGroup)
628        and isinstance(expression.this, exp.PERCENTILES)
629        and isinstance(expression.expression, exp.Order)
630    ):
631        quantile = expression.this.this
632        input_value = t.cast(exp.Ordered, expression.find(exp.Ordered)).this
633        return expression.replace(exp.ApproxQuantile(this=input_value, quantile=quantile))
634
635    return expression

Transforms percentiles by getting rid of their corresponding WITHIN GROUP clause.

def add_recursive_cte_column_names( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
638def add_recursive_cte_column_names(expression: exp.Expr) -> exp.Expr:
639    """Uses projection output names in recursive CTE definitions to define the CTEs' columns."""
640    if isinstance(expression, exp.With) and expression.recursive:
641        next_name = name_sequence("_c_")
642
643        for cte in expression.expressions:
644            if not cte.args["alias"].columns:
645                query = cte.this
646                if isinstance(query, exp.SetOperation):
647                    query = query.this
648
649                cte.args["alias"].set(
650                    "columns",
651                    [exp.to_identifier(s.alias_or_name or next_name()) for s in query.selects],
652                )
653
654    return expression

Uses projection output names in recursive CTE definitions to define the CTEs' columns.

def epoch_cast_to_ts( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
657def epoch_cast_to_ts(expression: exp.Expr) -> exp.Expr:
658    """Replace 'epoch' in casts by the equivalent date literal."""
659    if (
660        isinstance(expression, (exp.Cast, exp.TryCast))
661        and expression.name.lower() == "epoch"
662        and expression.to.this in exp.DataType.TEMPORAL_TYPES
663    ):
664        expression.this.replace(exp.Literal.string("1970-01-01 00:00:00"))
665
666    return expression

Replace 'epoch' in casts by the equivalent date literal.

def eliminate_semi_and_anti_joins( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
669def eliminate_semi_and_anti_joins(expression: exp.Expr) -> exp.Expr:
670    """Convert SEMI and ANTI joins into equivalent forms that use EXIST instead."""
671    if isinstance(expression, exp.Select):
672        for join in list[exp.Join](expression.args.get("joins") or []):
673            on: exp.Expr | None = join.args.get("on")
674            if on and join.kind in ("SEMI", "ANTI"):
675                subquery = exp.select("1").from_(join.this).where(on)
676                exists: exp.Exists | exp.Not = exp.Exists(this=subquery)
677                if join.kind == "ANTI":
678                    exists = exists.not_(copy=False)
679
680                join.pop()
681                expression.where(exists, copy=False)
682
683    return expression

Convert SEMI and ANTI joins into equivalent forms that use EXIST instead.

def eliminate_full_outer_join( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
686def eliminate_full_outer_join(expression: exp.Expr) -> exp.Expr:
687    """
688    Converts a query with a FULL OUTER join to a union of identical queries that
689    use LEFT/RIGHT OUTER joins instead. This transformation currently only works
690    for queries that have a single FULL OUTER join.
691    """
692    if isinstance(expression, exp.Select):
693        full_outer_joins: list[tuple[int, exp.Join]] = [
694            (index, join)
695            for index, join in enumerate[exp.Join](expression.args.get("joins") or [])
696            if join.side == "FULL"
697        ]
698
699        if len(full_outer_joins) == 1:
700            expression_copy = expression.copy()
701            index, full_outer_join = full_outer_joins[0]
702
703            tables = (expression.args["from_"].alias_or_name, full_outer_join.alias_or_name)
704            join_conditions = full_outer_join.args.get("on") or exp.and_(
705                *[
706                    exp.column(col, tables[0]).eq(exp.column(col, tables[1]))
707                    for col in t.cast(list[exp.Identifier], full_outer_join.args.get("using"))
708                ]
709            )
710
711            full_outer_join.set("side", "left")
712            anti_join_clause = (
713                exp.select("1").from_(expression.args["from_"]).where(join_conditions)
714            )
715            expression_copy.args["joins"][index].set("side", "right")
716            expression_copy = expression_copy.where(exp.Exists(this=anti_join_clause).not_())
717
718            union = exp.union(expression, expression_copy, copy=False, distinct=False)
719            for arg in ("with_", "order", "limit", "offset"):
720                value = expression.args.get(arg)
721                if value:
722                    expression.set(arg, None)
723                    expression_copy.set(arg, None)
724                    union.set(arg, value)
725            return union
726
727    return expression

Converts a query with a FULL OUTER join to a union of identical queries that use LEFT/RIGHT OUTER joins instead. This transformation currently only works for queries that have a single FULL OUTER join.

def move_ctes_to_top_level(expression: ~E) -> ~E:
730def move_ctes_to_top_level(expression: E) -> E:
731    """
732    Some dialects (e.g. Hive, T-SQL, Spark prior to version 3) only allow CTEs to be
733    defined at the top-level, so for example queries like:
734
735        SELECT * FROM (WITH t(c) AS (SELECT 1) SELECT * FROM t) AS subq
736
737    are invalid in those dialects. This transformation can be used to ensure all CTEs are
738    moved to the top level so that the final SQL code is valid from a syntax standpoint.
739
740    TODO: handle name clashes whilst moving CTEs (it can get quite tricky & costly).
741    """
742    top_level_with: exp.With | None = expression.args.get("with_")
743    for inner_with in expression.find_all(exp.With):
744        if inner_with.parent is expression:
745            continue
746
747        if not top_level_with:
748            top_level_with = inner_with.pop()
749            expression.set("with_", top_level_with)
750        else:
751            if inner_with.recursive:
752                top_level_with.set("recursive", True)
753
754            parent_cte = inner_with.find_ancestor(exp.CTE)
755            inner_with.pop()
756
757            if parent_cte:
758                i = top_level_with.expressions.index(parent_cte)
759                top_level_with.expressions[i:i] = inner_with.expressions
760                top_level_with.set("expressions", top_level_with.expressions)
761            else:
762                top_level_with.set(
763                    "expressions", top_level_with.expressions + inner_with.expressions
764                )
765
766    return expression

Some dialects (e.g. Hive, T-SQL, Spark prior to version 3) only allow CTEs to be defined at the top-level, so for example queries like:

SELECT * FROM (WITH t(c) AS (SELECT 1) SELECT * FROM t) AS subq

are invalid in those dialects. This transformation can be used to ensure all CTEs are moved to the top level so that the final SQL code is valid from a syntax standpoint.

TODO: handle name clashes whilst moving CTEs (it can get quite tricky & costly).

def ensure_bools( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
769def ensure_bools(expression: exp.Expr) -> exp.Expr:
770    """Converts numeric values used in conditions into explicit boolean expressions."""
771    from sqlglot.optimizer.canonicalize import ensure_bools
772
773    def _ensure_bool(node: exp.Expr) -> None:
774        if (
775            node.is_number
776            or (
777                not isinstance(node, exp.SubqueryPredicate)
778                and node.is_type(exp.DType.UNKNOWN, *exp.DataType.NUMERIC_TYPES)
779            )
780            or (isinstance(node, exp.Column) and not node.type)
781        ):
782            node.replace(node.neq(0))
783
784    for node in expression.walk():
785        ensure_bools(node, _ensure_bool)
786
787    return expression

Converts numeric values used in conditions into explicit boolean expressions.

def unqualify_columns( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
790def unqualify_columns(expression: exp.Expr) -> exp.Expr:
791    for column in expression.find_all(exp.Column):
792        # We only wanna pop off the table, db, catalog args
793        for part in column.parts[:-1]:
794            part.pop()
795
796    return expression
def unqualify_pivot_fields( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
799def unqualify_pivot_fields(expression: exp.Expr) -> exp.Expr:
800    """
801    Some dialects only accept simple column names in a (UN)PIVOT's FOR clause and IN-list
802    (Oracle raises ORA-01748), even though the aggregate itself may stay qualified.
803
804    Example:
805        >>> from sqlglot import parse_one
806        >>> expr = parse_one("SELECT * FROM tbl PIVOT (SUM(tbl.sales) FOR tbl.quarter IN ('Q1', 'Q2'))")
807        >>> print(unqualify_pivot_fields(expr).sql(dialect="spark"))
808        SELECT * FROM tbl PIVOT(SUM(tbl.sales) FOR quarter IN ('Q1', 'Q2'))
809    """
810    if isinstance(expression, exp.Pivot):
811        expression.set("fields", [unqualify_columns(field) for field in expression.fields])
812
813    return expression

Some dialects only accept simple column names in a (UN)PIVOT's FOR clause and IN-list (Oracle raises ORA-01748), even though the aggregate itself may stay qualified.

Example:
>>> from sqlglot import parse_one
>>> expr = parse_one("SELECT * FROM tbl PIVOT (SUM(tbl.sales) FOR tbl.quarter IN ('Q1', 'Q2'))")
>>> print(unqualify_pivot_fields(expr).sql(dialect="spark"))
SELECT * FROM tbl PIVOT(SUM(tbl.sales) FOR quarter IN ('Q1', 'Q2'))
def remove_unique_constraints( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
816def remove_unique_constraints(expression: exp.Expr) -> exp.Expr:
817    assert isinstance(expression, exp.Create)
818    for constraint in expression.find_all(exp.UniqueColumnConstraint):
819        parent = constraint.parent
820        (parent if isinstance(parent, (exp.ColumnConstraint, exp.Constraint)) else constraint).pop()
821
822    return expression
def ctas_with_tmp_tables_to_create_tmp_view( expression: sqlglot.expressions.core.Expr, tmp_storage_provider: Callable[[sqlglot.expressions.core.Expr], sqlglot.expressions.core.Expr] = <function <lambda>>) -> sqlglot.expressions.core.Expr:
825def ctas_with_tmp_tables_to_create_tmp_view(
826    expression: exp.Expr,
827    tmp_storage_provider: t.Callable[[exp.Expr], exp.Expr] = lambda e: e,
828) -> exp.Expr:
829    assert isinstance(expression, exp.Create)
830    properties: exp.Properties | None = expression.args.get("properties")
831    temporary = any(
832        isinstance(prop, exp.TemporaryProperty)
833        for prop in (properties.expressions if properties is not None else [])
834    )
835
836    # CTAS with temp tables map to CREATE TEMPORARY VIEW
837    if expression.kind == "TABLE" and temporary:
838        if expression.expression:
839            return exp.Create(
840                kind="TEMPORARY VIEW",
841                this=expression.this,
842                expression=expression.expression,
843            )
844        return tmp_storage_provider(expression)
845
846    return expression
def move_schema_columns_to_partitioned_by( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
849def move_schema_columns_to_partitioned_by(expression: exp.Expr) -> exp.Expr:
850    """
851    In Hive, the PARTITIONED BY property acts as an extension of a table's schema. When the
852    PARTITIONED BY value is an array of column names, they are transformed into a schema.
853    The corresponding columns are removed from the create statement.
854    """
855    assert isinstance(expression, exp.Create)
856    schema = expression.this
857    is_partitionable = expression.kind in {"TABLE", "VIEW"}
858
859    if isinstance(schema, exp.Schema) and is_partitionable:
860        prop = expression.find(exp.PartitionedByProperty)
861        if prop and prop.this and not isinstance(prop.this, exp.Schema):
862            columns: set[str] = {v.name.upper() for v in prop.this.expressions}
863            schema_exprs: list[exp.Expr] = schema.expressions
864            partitions = [col for col in schema_exprs if col.name.upper() in columns]
865            schema.set("expressions", [e for e in schema_exprs if e not in partitions])
866            prop.replace(exp.PartitionedByProperty(this=exp.Schema(expressions=partitions)))
867            expression.set("this", schema)
868
869    return expression

In Hive, the PARTITIONED BY property acts as an extension of a table's schema. When the PARTITIONED BY value is an array of column names, they are transformed into a schema. The corresponding columns are removed from the create statement.

def move_partitioned_by_to_schema_columns( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
872def move_partitioned_by_to_schema_columns(expression: exp.Expr) -> exp.Expr:
873    """
874    Spark 3 supports both "HIVEFORMAT" and "DATASOURCE" formats for CREATE TABLE.
875
876    Currently, SQLGlot uses the DATASOURCE format for Spark 3.
877    """
878    assert isinstance(expression, exp.Create)
879    prop = expression.find(exp.PartitionedByProperty)
880    if (
881        prop
882        and prop.this
883        and isinstance(prop.this, exp.Schema)
884        and all(isinstance(e, exp.ColumnDef) and e.kind for e in prop.this.expressions)
885    ):
886        prop_this = exp.Tuple(
887            expressions=[exp.to_identifier(e.this) for e in prop.this.expressions]
888        )
889        schema: exp.Schema = expression.this
890        for e in prop.this.expressions:
891            schema.append("expressions", e)
892        prop.set("this", prop_this)
893
894    return expression

Spark 3 supports both "HIVEFORMAT" and "DATASOURCE" formats for CREATE TABLE.

Currently, SQLGlot uses the DATASOURCE format for Spark 3.

def struct_kv_to_alias( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
897def struct_kv_to_alias(expression: exp.Expr) -> exp.Expr:
898    """Converts struct arguments to aliases, e.g. STRUCT(1 AS y)."""
899    if isinstance(expression, exp.Struct):
900        expression.set(
901            "expressions",
902            [
903                exp.alias_(e.expression, e.this) if isinstance(e, exp.PropertyEQ) else e
904                for e in expression.expressions
905            ],
906        )
907
908    return expression

Converts struct arguments to aliases, e.g. STRUCT(1 AS y).

def eliminate_join_marks( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
 911def eliminate_join_marks(expression: exp.Expr) -> exp.Expr:
 912    """https://docs.oracle.com/cd/B19306_01/server.102/b14200/queries006.htm#sthref3178
 913
 914    1. You cannot specify the (+) operator in a query block that also contains FROM clause join syntax.
 915
 916    2. The (+) operator can appear only in the WHERE clause or, in the context of left-correlation (that is, when specifying the TABLE clause) in the FROM clause, and can be applied only to a column of a table or view.
 917
 918    The (+) operator does not produce an outer join if you specify one table in the outer query and the other table in an inner query.
 919
 920    You cannot use the (+) operator to outer-join a table to itself, although self joins are valid.
 921
 922    The (+) operator can be applied only to a column, not to an arbitrary expression. However, an arbitrary expression can contain one or more columns marked with the (+) operator.
 923
 924    A WHERE condition containing the (+) operator cannot be combined with another condition using the OR logical operator.
 925
 926    A WHERE condition cannot use the IN comparison condition to compare a column marked with the (+) operator with an expression.
 927
 928    A WHERE condition cannot compare any column marked with the (+) operator with a subquery.
 929
 930    -- example with WHERE
 931    SELECT d.department_name, sum(e.salary) as total_salary
 932    FROM departments d, employees e
 933    WHERE e.department_id(+) = d.department_id
 934    group by department_name
 935
 936    -- example of left correlation in select
 937    SELECT d.department_name, (
 938        SELECT SUM(e.salary)
 939            FROM employees e
 940            WHERE e.department_id(+) = d.department_id) AS total_salary
 941    FROM departments d;
 942
 943    -- example of left correlation in from
 944    SELECT d.department_name, t.total_salary
 945    FROM departments d, (
 946            SELECT SUM(e.salary) AS total_salary
 947            FROM employees e
 948            WHERE e.department_id(+) = d.department_id
 949        ) t
 950    """
 951
 952    from sqlglot.optimizer.scope import traverse_scope
 953    from sqlglot.optimizer.normalize import normalize, normalized
 954    from collections import defaultdict
 955
 956    # we go in reverse to check the main query for left correlation
 957    for scope in reversed(traverse_scope(expression)):
 958        query = scope.expression
 959
 960        where: exp.Expr | None = query.args.get("where")
 961        joins: list[exp.Join] = query.args.get("joins", [])
 962
 963        if not where or not any(c.args.get("join_mark") for c in where.find_all(exp.Column)):
 964            continue
 965
 966        # knockout: we do not support left correlation (see point 2)
 967        assert not scope.is_correlated_subquery, "Correlated queries are not supported"
 968
 969        # make sure we have AND of ORs to have clear join terms
 970        where = normalize(where.this)
 971        assert normalized(where), "Cannot normalize JOIN predicates"
 972        # dict of {name: list of join AND conditions}
 973        joins_ons: defaultdict[str, list[exp.Expr]] = defaultdict(list)
 974        for cond in [where] if not isinstance(where, exp.And) else where.flatten():
 975            join_cols = [col for col in cond.find_all(exp.Column) if col.args.get("join_mark")]
 976
 977            left_join_table = set(col.table for col in join_cols)
 978            if not left_join_table:
 979                continue
 980
 981            assert not (len(left_join_table) > 1), (
 982                "Cannot combine JOIN predicates from different tables"
 983            )
 984
 985            for col in join_cols:
 986                col.set("join_mark", False)
 987
 988            joins_ons[left_join_table.pop()].append(cond)
 989
 990        old_joins = {join.alias_or_name: join for join in joins}
 991        new_joins: dict[str, exp.Join] = {}
 992        query_from = query.args["from_"]
 993
 994        for table, predicates in joins_ons.items():
 995            join_what = old_joins.get(table, query_from).this.copy()
 996            new_joins[join_what.alias_or_name] = exp.Join(
 997                this=join_what, on=exp.and_(*predicates), kind="LEFT"
 998            )
 999
1000            for p in predicates:
1001                while isinstance(p.parent, exp.Paren):
1002                    p.parent.replace(p)
1003
1004                parent = p.parent
1005                p.pop()
1006                if isinstance(parent, exp.Binary):
1007                    left = parent.args.get("this")
1008                    parent.replace(parent.right if left is None else left)
1009                elif isinstance(parent, exp.Where):
1010                    parent.pop()
1011
1012        if query_from.alias_or_name in new_joins:
1013            only_old_joins: set[str] = old_joins.keys() - new_joins.keys()
1014            assert len(only_old_joins) >= 1, (
1015                "Cannot determine which table to use in the new FROM clause"
1016            )
1017
1018            new_from_name = list[str](only_old_joins)[0]
1019            query.set("from_", exp.From(this=old_joins[new_from_name].this))
1020
1021        if new_joins:
1022            for n, j in old_joins.items():  # preserve any other joins
1023                if n not in new_joins and n != query.args["from_"].name:
1024                    if not j.kind:
1025                        j.set("kind", "CROSS")
1026                    new_joins[n] = j
1027            query.set("joins", list(new_joins.values()))
1028
1029    return expression

https://docs.oracle.com/cd/B19306_01/server.102/b14200/queries006.htm#sthref3178

  1. You cannot specify the (+) operator in a query block that also contains FROM clause join syntax.

  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.

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

def any_to_exists( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
1032def any_to_exists(expression: exp.Expr) -> exp.Expr:
1033    """
1034    Transform ANY operator to Spark's EXISTS
1035
1036    For example,
1037        - Postgres: SELECT * FROM tbl WHERE 5 > ANY(tbl.col)
1038        - Spark: SELECT * FROM tbl WHERE EXISTS(tbl.col, x -> x < 5)
1039
1040    Both ANY and EXISTS accept queries but currently only array expressions are supported for this
1041    transformation
1042    """
1043    if isinstance(expression, exp.Select):
1044        for any_expr in expression.find_all(exp.Any):
1045            this: exp.Expr = any_expr.this
1046            if isinstance(this, exp.Query) or isinstance(any_expr.parent, (exp.Like, exp.ILike)):
1047                continue
1048
1049            binop = any_expr.parent
1050            if isinstance(binop, exp.Binary):
1051                lambda_arg = exp.to_identifier("x")
1052                any_expr.replace(lambda_arg)
1053                lambda_expr = exp.Lambda(this=binop.copy(), expressions=[lambda_arg])
1054                binop.replace(exp.Exists(this=this.unnest(), expression=lambda_expr))
1055
1056    return expression

Transform ANY operator to Spark's EXISTS

For example, - Postgres: SELECT * FROM tbl WHERE 5 > ANY(tbl.col) - Spark: SELECT * FROM tbl WHERE EXISTS(tbl.col, x -> x < 5)

Both ANY and EXISTS accept queries but currently only array expressions are supported for this transformation

def eliminate_window_clause( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
1059def eliminate_window_clause(expression: exp.Expr) -> exp.Expr:
1060    """Eliminates the `WINDOW` query clause by inling each named window."""
1061    windows: list[exp.Expr] | None = expression.args.get("windows")
1062    if isinstance(expression, exp.Select) and windows is not None:
1063        from sqlglot.optimizer.scope import find_all_in_scope
1064
1065        expression.set("windows", None)
1066
1067        window_expression: dict[str, exp.Expr] = {}
1068
1069        def _inline_inherited_window(window: exp.Expr) -> None:
1070            inherited_window = window_expression.get(window.alias.lower())
1071            if not inherited_window:
1072                return
1073
1074            window.set("alias", None)
1075            for key in ("partition_by", "order", "spec"):
1076                arg: exp.Expr | None = inherited_window.args.get(key)
1077                if arg is not None:
1078                    window.set(key, arg.copy())
1079
1080        for window in windows:
1081            _inline_inherited_window(window)
1082            window_expression[window.name.lower()] = window
1083
1084        for window in find_all_in_scope(expression, exp.Window):
1085            _inline_inherited_window(window)
1086
1087    return expression

Eliminates the WINDOW query clause by inling each named window.

def inherit_struct_field_names( expression: sqlglot.expressions.core.Expr) -> sqlglot.expressions.core.Expr:
1090def inherit_struct_field_names(expression: exp.Expr) -> exp.Expr:
1091    """
1092    Inherit field names from the first struct in an array.
1093
1094    BigQuery supports implicitly inheriting names from the first STRUCT in an array:
1095
1096    Example:
1097        ARRAY[
1098          STRUCT('Alice' AS name, 85 AS score),  -- defines names
1099          STRUCT('Bob', 92),                     -- inherits names
1100          STRUCT('Diana', 95)                    -- inherits names
1101        ]
1102
1103    This transformation makes the field names explicit on all structs by adding
1104    PropertyEQ nodes, in order to facilitate transpilation to other dialects.
1105
1106    Args:
1107        expression: The expression tree to transform
1108
1109    Returns:
1110        The modified expression with field names inherited in all structs
1111    """
1112    if (
1113        isinstance(expression, exp.Array)
1114        and expression.args.get("struct_name_inheritance")
1115        and isinstance(first_item := seq_get(expression.expressions, 0), exp.Struct)
1116        and all(isinstance(fld, exp.PropertyEQ) for fld in first_item.expressions)
1117    ):
1118        field_names: list[exp.Identifier] = [fld.this for fld in first_item.expressions]
1119
1120        # Apply field names to subsequent structs that don't have them
1121        for struct in expression.expressions[1:]:
1122            if not isinstance(struct, exp.Struct) or len(struct.expressions) != len(field_names):
1123                continue
1124
1125            # Convert unnamed expressions to PropertyEQ with inherited names
1126            new_expressions: list[exp.PropertyEQ] = []
1127            for i, expr in enumerate(struct.expressions):
1128                if not isinstance(expr, exp.PropertyEQ):
1129                    # Create PropertyEQ: field_name := value, preserving the type from the inner expression
1130                    property_eq = exp.PropertyEQ(
1131                        this=field_names[i].copy(),
1132                        expression=expr,
1133                    )
1134                    property_eq.type = expr.type
1135                    new_expressions.append(property_eq)
1136                else:
1137                    new_expressions.append(expr)
1138
1139            struct.set("expressions", new_expressions)
1140
1141    return expression

Inherit field names from the first struct in an array.

BigQuery supports implicitly inheriting names from the first STRUCT in an array:

Example:

ARRAY[ STRUCT('Alice' AS name, 85 AS score), -- defines names STRUCT('Bob', 92), -- inherits names STRUCT('Diana', 95) -- inherits names ]

This transformation makes the field names explicit on all structs by adding PropertyEQ nodes, in order to facilitate transpilation to other dialects.

Arguments:
  • expression: The expression tree to transform
Returns:

The modified expression with field names inherited in all structs