sqlglot.planner API documentation

Edit on GitHub

# sqlglot.planner

View Source

      1from __future__ import annotations
      2
      3import math
      4import typing as t
      5
      6from sqlglot import alias, exp
      7from sqlglot.helper import name_sequence
      8from sqlglot.optimizer.eliminate_joins import join_condition
      9
     10
     11class Plan:
     12    def __init__(self, expression: exp.Expression) -> None:
     13        self.expression = expression.copy()
     14        self.root = Step.from_expression(self.expression)
     15        self._dag: t.Dict[Step, t.Set[Step]] = {}
     16
     17    @property
     18    def dag(self) -> t.Dict[Step, t.Set[Step]]:
     19        if not self._dag:
     20            dag: t.Dict[Step, t.Set[Step]] = {}
     21            nodes = {self.root}
     22
     23            while nodes:
     24                node = nodes.pop()
     25                dag[node] = set()
     26
     27                for dep in node.dependencies:
     28                    dag[node].add(dep)
     29                    nodes.add(dep)
     30
     31            self._dag = dag
     32
     33        return self._dag
     34
     35    @property
     36    def leaves(self) -> t.Iterator[Step]:
     37        return (node for node, deps in self.dag.items() if not deps)
     38
     39    def __repr__(self) -> str:
     40        return f"Plan\n----\n{repr(self.root)}"
     41
     42
     43class Step:
     44    @classmethod
     45    def from_expression(
     46        cls, expression: exp.Expression, ctes: t.Optional[t.Dict[str, Step]] = None
     47    ) -> Step:
     48        """
     49        Builds a DAG of Steps from a SQL expression so that it's easier to execute in an engine.
     50        Note: the expression's tables and subqueries must be aliased for this method to work. For
     51        example, given the following expression:
     52
     53        SELECT
     54          x.a,
     55          SUM(x.b)
     56        FROM x AS x
     57        JOIN y AS y
     58          ON x.a = y.a
     59        GROUP BY x.a
     60
     61        the following DAG is produced (the expression IDs might differ per execution):
     62
     63        - Aggregate: x (4347984624)
     64            Context:
     65              Aggregations:
     66                - SUM(x.b)
     67              Group:
     68                - x.a
     69            Projections:
     70              - x.a
     71              - "x".""
     72            Dependencies:
     73            - Join: x (4347985296)
     74              Context:
     75                y:
     76                On: x.a = y.a
     77              Projections:
     78              Dependencies:
     79              - Scan: x (4347983136)
     80                Context:
     81                  Source: x AS x
     82                Projections:
     83              - Scan: y (4343416624)
     84                Context:
     85                  Source: y AS y
     86                Projections:
     87
     88        Args:
     89            expression: the expression to build the DAG from.
     90            ctes: a dictionary that maps CTEs to their corresponding Step DAG by name.
     91
     92        Returns:
     93            A Step DAG corresponding to `expression`.
     94        """
     95        ctes = ctes or {}
     96        expression = expression.unnest()
     97        with_ = expression.args.get("with")
     98
     99        # CTEs break the mold of scope and introduce themselves to all in the context.
    100        if with_:
    101            ctes = ctes.copy()
    102            for cte in with_.expressions:
    103                step = Step.from_expression(cte.this, ctes)
    104                step.name = cte.alias
    105                ctes[step.name] = step  # type: ignore
    106
    107        from_ = expression.args.get("from")
    108
    109        if isinstance(expression, exp.Select) and from_:
    110            step = Scan.from_expression(from_.this, ctes)
    111        elif isinstance(expression, exp.Union):
    112            step = SetOperation.from_expression(expression, ctes)
    113        else:
    114            step = Scan()
    115
    116        joins = expression.args.get("joins")
    117
    118        if joins:
    119            join = Join.from_joins(joins, ctes)
    120            join.name = step.name
    121            join.add_dependency(step)
    122            step = join
    123
    124        projections = []  # final selects in this chain of steps representing a select
    125        operands = {}  # intermediate computations of agg funcs eg x + 1 in SUM(x + 1)
    126        aggregations = set()
    127        next_operand_name = name_sequence("_a_")
    128
    129        def extract_agg_operands(expression):
    130            agg_funcs = tuple(expression.find_all(exp.AggFunc))
    131            if agg_funcs:
    132                aggregations.add(expression)
    133
    134            for agg in agg_funcs:
    135                for operand in agg.unnest_operands():
    136                    if isinstance(operand, exp.Column):
    137                        continue
    138                    if operand not in operands:
    139                        operands[operand] = next_operand_name()
    140
    141                    operand.replace(exp.column(operands[operand], quoted=True))
    142
    143            return bool(agg_funcs)
    144
    145        def set_ops_and_aggs(step):
    146            step.operands = tuple(alias(operand, alias_) for operand, alias_ in operands.items())
    147            step.aggregations = list(aggregations)
    148
    149        for e in expression.expressions:
    150            if e.find(exp.AggFunc):
    151                projections.append(exp.column(e.alias_or_name, step.name, quoted=True))
    152                extract_agg_operands(e)
    153            else:
    154                projections.append(e)
    155
    156        where = expression.args.get("where")
    157
    158        if where:
    159            step.condition = where.this
    160
    161        group = expression.args.get("group")
    162
    163        if group or aggregations:
    164            aggregate = Aggregate()
    165            aggregate.source = step.name
    166            aggregate.name = step.name
    167
    168            having = expression.args.get("having")
    169
    170            if having:
    171                if extract_agg_operands(exp.alias_(having.this, "_h", quoted=True)):
    172                    aggregate.condition = exp.column("_h", step.name, quoted=True)
    173                else:
    174                    aggregate.condition = having.this
    175
    176            set_ops_and_aggs(aggregate)
    177
    178            # give aggregates names and replace projections with references to them
    179            aggregate.group = {
    180                f"_g{i}": e for i, e in enumerate(group.expressions if group else [])
    181            }
    182
    183            intermediate: t.Dict[str | exp.Expression, str] = {}
    184            for k, v in aggregate.group.items():
    185                intermediate[v] = k
    186                if isinstance(v, exp.Column):
    187                    intermediate[v.name] = k
    188
    189            for projection in projections:
    190                for node, *_ in projection.walk():
    191                    name = intermediate.get(node)
    192                    if name:
    193                        node.replace(exp.column(name, step.name))
    194
    195            if aggregate.condition:
    196                for node, *_ in aggregate.condition.walk():
    197                    name = intermediate.get(node) or intermediate.get(node.name)
    198                    if name:
    199                        node.replace(exp.column(name, step.name))
    200
    201            aggregate.add_dependency(step)
    202            step = aggregate
    203
    204        order = expression.args.get("order")
    205
    206        if order:
    207            if isinstance(step, Aggregate):
    208                for i, ordered in enumerate(order.expressions):
    209                    if extract_agg_operands(exp.alias_(ordered.this, f"_o_{i}", quoted=True)):
    210                        ordered.this.replace(exp.column(f"_o_{i}", step.name, quoted=True))
    211
    212                set_ops_and_aggs(aggregate)
    213
    214            sort = Sort()
    215            sort.name = step.name
    216            sort.key = order.expressions
    217            sort.add_dependency(step)
    218            step = sort
    219
    220        step.projections = projections
    221
    222        if isinstance(expression, exp.Select) and expression.args.get("distinct"):
    223            distinct = Aggregate()
    224            distinct.source = step.name
    225            distinct.name = step.name
    226            distinct.group = {
    227                e.alias_or_name: exp.column(col=e.alias_or_name, table=step.name)
    228                for e in projections or expression.expressions
    229            }
    230            distinct.add_dependency(step)
    231            step = distinct
    232
    233        limit = expression.args.get("limit")
    234
    235        if limit:
    236            step.limit = int(limit.text("expression"))
    237
    238        return step
    239
    240    def __init__(self) -> None:
    241        self.name: t.Optional[str] = None
    242        self.dependencies: t.Set[Step] = set()
    243        self.dependents: t.Set[Step] = set()
    244        self.projections: t.Sequence[exp.Expression] = []
    245        self.limit: float = math.inf
    246        self.condition: t.Optional[exp.Expression] = None
    247
    248    def add_dependency(self, dependency: Step) -> None:
    249        self.dependencies.add(dependency)
    250        dependency.dependents.add(self)
    251
    252    def __repr__(self) -> str:
    253        return self.to_s()
    254
    255    def to_s(self, level: int = 0) -> str:
    256        indent = "  " * level
    257        nested = f"{indent}    "
    258
    259        context = self._to_s(f"{nested}  ")
    260
    261        if context:
    262            context = [f"{nested}Context:"] + context
    263
    264        lines = [
    265            f"{indent}- {self.id}",
    266            *context,
    267            f"{nested}Projections:",
    268        ]
    269
    270        for expression in self.projections:
    271            lines.append(f"{nested}  - {expression.sql()}")
    272
    273        if self.condition:
    274            lines.append(f"{nested}Condition: {self.condition.sql()}")
    275
    276        if self.limit is not math.inf:
    277            lines.append(f"{nested}Limit: {self.limit}")
    278
    279        if self.dependencies:
    280            lines.append(f"{nested}Dependencies:")
    281            for dependency in self.dependencies:
    282                lines.append("  " + dependency.to_s(level + 1))
    283
    284        return "\n".join(lines)
    285
    286    @property
    287    def type_name(self) -> str:
    288        return self.__class__.__name__
    289
    290    @property
    291    def id(self) -> str:
    292        name = self.name
    293        name = f" {name}" if name else ""
    294        return f"{self.type_name}:{name} ({id(self)})"
    295
    296    def _to_s(self, _indent: str) -> t.List[str]:
    297        return []
    298
    299
    300class Scan(Step):
    301    @classmethod
    302    def from_expression(
    303        cls, expression: exp.Expression, ctes: t.Optional[t.Dict[str, Step]] = None
    304    ) -> Step:
    305        table = expression
    306        alias_ = expression.alias_or_name
    307
    308        if isinstance(expression, exp.Subquery):
    309            table = expression.this
    310            step = Step.from_expression(table, ctes)
    311            step.name = alias_
    312            return step
    313
    314        step = Scan()
    315        step.name = alias_
    316        step.source = expression
    317        if ctes and table.name in ctes:
    318            step.add_dependency(ctes[table.name])
    319
    320        return step
    321
    322    def __init__(self) -> None:
    323        super().__init__()
    324        self.source: t.Optional[exp.Expression] = None
    325
    326    def _to_s(self, indent: str) -> t.List[str]:
    327        return [f"{indent}Source: {self.source.sql() if self.source else '-static-'}"]  # type: ignore
    328
    329
    330class Join(Step):
    331    @classmethod
    332    def from_joins(
    333        cls, joins: t.Iterable[exp.Join], ctes: t.Optional[t.Dict[str, Step]] = None
    334    ) -> Step:
    335        step = Join()
    336
    337        for join in joins:
    338            source_key, join_key, condition = join_condition(join)
    339            step.joins[join.alias_or_name] = {
    340                "side": join.side,  # type: ignore
    341                "join_key": join_key,
    342                "source_key": source_key,
    343                "condition": condition,
    344            }
    345
    346            step.add_dependency(Scan.from_expression(join.this, ctes))
    347
    348        return step
    349
    350    def __init__(self) -> None:
    351        super().__init__()
    352        self.joins: t.Dict[str, t.Dict[str, t.List[str] | exp.Expression]] = {}
    353
    354    def _to_s(self, indent: str) -> t.List[str]:
    355        lines = []
    356        for name, join in self.joins.items():
    357            lines.append(f"{indent}{name}: {join['side'] or 'INNER'}")
    358            join_key = ", ".join(str(key) for key in t.cast(list, join.get("join_key") or []))
    359            if join_key:
    360                lines.append(f"{indent}Key: {join_key}")
    361            if join.get("condition"):
    362                lines.append(f"{indent}On: {join['condition'].sql()}")  # type: ignore
    363        return lines
    364
    365
    366class Aggregate(Step):
    367    def __init__(self) -> None:
    368        super().__init__()
    369        self.aggregations: t.List[exp.Expression] = []
    370        self.operands: t.Tuple[exp.Expression, ...] = ()
    371        self.group: t.Dict[str, exp.Expression] = {}
    372        self.source: t.Optional[str] = None
    373
    374    def _to_s(self, indent: str) -> t.List[str]:
    375        lines = [f"{indent}Aggregations:"]
    376
    377        for expression in self.aggregations:
    378            lines.append(f"{indent}  - {expression.sql()}")
    379
    380        if self.group:
    381            lines.append(f"{indent}Group:")
    382            for expression in self.group.values():
    383                lines.append(f"{indent}  - {expression.sql()}")
    384        if self.condition:
    385            lines.append(f"{indent}Having:")
    386            lines.append(f"{indent}  - {self.condition.sql()}")
    387        if self.operands:
    388            lines.append(f"{indent}Operands:")
    389            for expression in self.operands:
    390                lines.append(f"{indent}  - {expression.sql()}")
    391
    392        return lines
    393
    394
    395class Sort(Step):
    396    def __init__(self) -> None:
    397        super().__init__()
    398        self.key = None
    399
    400    def _to_s(self, indent: str) -> t.List[str]:
    401        lines = [f"{indent}Key:"]
    402
    403        for expression in self.key:  # type: ignore
    404            lines.append(f"{indent}  - {expression.sql()}")
    405
    406        return lines
    407
    408
    409class SetOperation(Step):
    410    def __init__(
    411        self,
    412        op: t.Type[exp.Expression],
    413        left: str | None,
    414        right: str | None,
    415        distinct: bool = False,
    416    ) -> None:
    417        super().__init__()
    418        self.op = op
    419        self.left = left
    420        self.right = right
    421        self.distinct = distinct
    422
    423    @classmethod
    424    def from_expression(
    425        cls, expression: exp.Expression, ctes: t.Optional[t.Dict[str, Step]] = None
    426    ) -> Step:
    427        assert isinstance(expression, exp.Union)
    428
    429        left = Step.from_expression(expression.left, ctes)
    430        # SELECT 1 UNION SELECT 2  <-- these subqueries don't have names
    431        left.name = left.name or "left"
    432        right = Step.from_expression(expression.right, ctes)
    433        right.name = right.name or "right"
    434        step = cls(
    435            op=expression.__class__,
    436            left=left.name,
    437            right=right.name,
    438            distinct=bool(expression.args.get("distinct")),
    439        )
    440
    441        step.add_dependency(left)
    442        step.add_dependency(right)
    443
    444        limit = expression.args.get("limit")
    445
    446        if limit:
    447            step.limit = int(limit.text("expression"))
    448
    449        return step
    450
    451    def _to_s(self, indent: str) -> t.List[str]:
    452        lines = []
    453        if self.distinct:
    454            lines.append(f"{indent}Distinct: {self.distinct}")
    455        return lines
    456
    457    @property
    458    def type_name(self) -> str:
    459        return self.op.__name__

class Plan: View Source

    12class Plan:
    13    def __init__(self, expression: exp.Expression) -> None:
    14        self.expression = expression.copy()
    15        self.root = Step.from_expression(self.expression)
    16        self._dag: t.Dict[Step, t.Set[Step]] = {}
    17
    18    @property
    19    def dag(self) -> t.Dict[Step, t.Set[Step]]:
    20        if not self._dag:
    21            dag: t.Dict[Step, t.Set[Step]] = {}
    22            nodes = {self.root}
    23
    24            while nodes:
    25                node = nodes.pop()
    26                dag[node] = set()
    27
    28                for dep in node.dependencies:
    29                    dag[node].add(dep)
    30                    nodes.add(dep)
    31
    32            self._dag = dag
    33
    34        return self._dag
    35
    36    @property
    37    def leaves(self) -> t.Iterator[Step]:
    38        return (node for node, deps in self.dag.items() if not deps)
    39
    40    def __repr__(self) -> str:
    41        return f"Plan\n----\n{repr(self.root)}"

Plan(expression: sqlglot.expressions.Expression) View Source

    13    def __init__(self, expression: exp.Expression) -> None:
    14        self.expression = expression.copy()
    15        self.root = Step.from_expression(self.expression)
    16        self._dag: t.Dict[Step, t.Set[Step]] = {}

expression
root
dag: Dict[Step, Set[Step]]
leaves: Iterator[Step]
class Step: View Source

     44class Step:
     45    @classmethod
     46    def from_expression(
     47        cls, expression: exp.Expression, ctes: t.Optional[t.Dict[str, Step]] = None
     48    ) -> Step:
     49        """
     50        Builds a DAG of Steps from a SQL expression so that it's easier to execute in an engine.
     51        Note: the expression's tables and subqueries must be aliased for this method to work. For
     52        example, given the following expression:
     53
     54        SELECT
     55          x.a,
     56          SUM(x.b)
     57        FROM x AS x
     58        JOIN y AS y
     59          ON x.a = y.a
     60        GROUP BY x.a
     61
     62        the following DAG is produced (the expression IDs might differ per execution):
     63
     64        - Aggregate: x (4347984624)
     65            Context:
     66              Aggregations:
     67                - SUM(x.b)
     68              Group:
     69                - x.a
     70            Projections:
     71              - x.a
     72              - "x".""
     73            Dependencies:
     74            - Join: x (4347985296)
     75              Context:
     76                y:
     77                On: x.a = y.a
     78              Projections:
     79              Dependencies:
     80              - Scan: x (4347983136)
     81                Context:
     82                  Source: x AS x
     83                Projections:
     84              - Scan: y (4343416624)
     85                Context:
     86                  Source: y AS y
     87                Projections:
     88
     89        Args:
     90            expression: the expression to build the DAG from.
     91            ctes: a dictionary that maps CTEs to their corresponding Step DAG by name.
     92
     93        Returns:
     94            A Step DAG corresponding to `expression`.
     95        """
     96        ctes = ctes or {}
     97        expression = expression.unnest()
     98        with_ = expression.args.get("with")
     99
    100        # CTEs break the mold of scope and introduce themselves to all in the context.
    101        if with_:
    102            ctes = ctes.copy()
    103            for cte in with_.expressions:
    104                step = Step.from_expression(cte.this, ctes)
    105                step.name = cte.alias
    106                ctes[step.name] = step  # type: ignore
    107
    108        from_ = expression.args.get("from")
    109
    110        if isinstance(expression, exp.Select) and from_:
    111            step = Scan.from_expression(from_.this, ctes)
    112        elif isinstance(expression, exp.Union):
    113            step = SetOperation.from_expression(expression, ctes)
    114        else:
    115            step = Scan()
    116
    117        joins = expression.args.get("joins")
    118
    119        if joins:
    120            join = Join.from_joins(joins, ctes)
    121            join.name = step.name
    122            join.add_dependency(step)
    123            step = join
    124
    125        projections = []  # final selects in this chain of steps representing a select
    126        operands = {}  # intermediate computations of agg funcs eg x + 1 in SUM(x + 1)
    127        aggregations = set()
    128        next_operand_name = name_sequence("_a_")
    129
    130        def extract_agg_operands(expression):
    131            agg_funcs = tuple(expression.find_all(exp.AggFunc))
    132            if agg_funcs:
    133                aggregations.add(expression)
    134
    135            for agg in agg_funcs:
    136                for operand in agg.unnest_operands():
    137                    if isinstance(operand, exp.Column):
    138                        continue
    139                    if operand not in operands:
    140                        operands[operand] = next_operand_name()
    141
    142                    operand.replace(exp.column(operands[operand], quoted=True))
    143
    144            return bool(agg_funcs)
    145
    146        def set_ops_and_aggs(step):
    147            step.operands = tuple(alias(operand, alias_) for operand, alias_ in operands.items())
    148            step.aggregations = list(aggregations)
    149
    150        for e in expression.expressions:
    151            if e.find(exp.AggFunc):
    152                projections.append(exp.column(e.alias_or_name, step.name, quoted=True))
    153                extract_agg_operands(e)
    154            else:
    155                projections.append(e)
    156
    157        where = expression.args.get("where")
    158
    159        if where:
    160            step.condition = where.this
    161
    162        group = expression.args.get("group")
    163
    164        if group or aggregations:
    165            aggregate = Aggregate()
    166            aggregate.source = step.name
    167            aggregate.name = step.name
    168
    169            having = expression.args.get("having")
    170
    171            if having:
    172                if extract_agg_operands(exp.alias_(having.this, "_h", quoted=True)):
    173                    aggregate.condition = exp.column("_h", step.name, quoted=True)
    174                else:
    175                    aggregate.condition = having.this
    176
    177            set_ops_and_aggs(aggregate)
    178
    179            # give aggregates names and replace projections with references to them
    180            aggregate.group = {
    181                f"_g{i}": e for i, e in enumerate(group.expressions if group else [])
    182            }
    183
    184            intermediate: t.Dict[str | exp.Expression, str] = {}
    185            for k, v in aggregate.group.items():
    186                intermediate[v] = k
    187                if isinstance(v, exp.Column):
    188                    intermediate[v.name] = k
    189
    190            for projection in projections:
    191                for node, *_ in projection.walk():
    192                    name = intermediate.get(node)
    193                    if name:
    194                        node.replace(exp.column(name, step.name))
    195
    196            if aggregate.condition:
    197                for node, *_ in aggregate.condition.walk():
    198                    name = intermediate.get(node) or intermediate.get(node.name)
    199                    if name:
    200                        node.replace(exp.column(name, step.name))
    201
    202            aggregate.add_dependency(step)
    203            step = aggregate
    204
    205        order = expression.args.get("order")
    206
    207        if order:
    208            if isinstance(step, Aggregate):
    209                for i, ordered in enumerate(order.expressions):
    210                    if extract_agg_operands(exp.alias_(ordered.this, f"_o_{i}", quoted=True)):
    211                        ordered.this.replace(exp.column(f"_o_{i}", step.name, quoted=True))
    212
    213                set_ops_and_aggs(aggregate)
    214
    215            sort = Sort()
    216            sort.name = step.name
    217            sort.key = order.expressions
    218            sort.add_dependency(step)
    219            step = sort
    220
    221        step.projections = projections
    222
    223        if isinstance(expression, exp.Select) and expression.args.get("distinct"):
    224            distinct = Aggregate()
    225            distinct.source = step.name
    226            distinct.name = step.name
    227            distinct.group = {
    228                e.alias_or_name: exp.column(col=e.alias_or_name, table=step.name)
    229                for e in projections or expression.expressions
    230            }
    231            distinct.add_dependency(step)
    232            step = distinct
    233
    234        limit = expression.args.get("limit")
    235
    236        if limit:
    237            step.limit = int(limit.text("expression"))
    238
    239        return step
    240
    241    def __init__(self) -> None:
    242        self.name: t.Optional[str] = None
    243        self.dependencies: t.Set[Step] = set()
    244        self.dependents: t.Set[Step] = set()
    245        self.projections: t.Sequence[exp.Expression] = []
    246        self.limit: float = math.inf
    247        self.condition: t.Optional[exp.Expression] = None
    248
    249    def add_dependency(self, dependency: Step) -> None:
    250        self.dependencies.add(dependency)
    251        dependency.dependents.add(self)
    252
    253    def __repr__(self) -> str:
    254        return self.to_s()
    255
    256    def to_s(self, level: int = 0) -> str:
    257        indent = "  " * level
    258        nested = f"{indent}    "
    259
    260        context = self._to_s(f"{nested}  ")
    261
    262        if context:
    263            context = [f"{nested}Context:"] + context
    264
    265        lines = [
    266            f"{indent}- {self.id}",
    267            *context,
    268            f"{nested}Projections:",
    269        ]
    270
    271        for expression in self.projections:
    272            lines.append(f"{nested}  - {expression.sql()}")
    273
    274        if self.condition:
    275            lines.append(f"{nested}Condition: {self.condition.sql()}")
    276
    277        if self.limit is not math.inf:
    278            lines.append(f"{nested}Limit: {self.limit}")
    279
    280        if self.dependencies:
    281            lines.append(f"{nested}Dependencies:")
    282            for dependency in self.dependencies:
    283                lines.append("  " + dependency.to_s(level + 1))
    284
    285        return "\n".join(lines)
    286
    287    @property
    288    def type_name(self) -> str:
    289        return self.__class__.__name__
    290
    291    @property
    292    def id(self) -> str:
    293        name = self.name
    294        name = f" {name}" if name else ""
    295        return f"{self.type_name}:{name} ({id(self)})"
    296
    297    def _to_s(self, _indent: str) -> t.List[str]:
    298        return []

@classmethod
def from_expression( cls, expression: sqlglot.expressions.Expression, ctes: Optional[Dict[str, Step]] = None) -> Step: View Source

     45    @classmethod
     46    def from_expression(
     47        cls, expression: exp.Expression, ctes: t.Optional[t.Dict[str, Step]] = None
     48    ) -> Step:
     49        """
     50        Builds a DAG of Steps from a SQL expression so that it's easier to execute in an engine.
     51        Note: the expression's tables and subqueries must be aliased for this method to work. For
     52        example, given the following expression:
     53
     54        SELECT
     55          x.a,
     56          SUM(x.b)
     57        FROM x AS x
     58        JOIN y AS y
     59          ON x.a = y.a
     60        GROUP BY x.a
     61
     62        the following DAG is produced (the expression IDs might differ per execution):
     63
     64        - Aggregate: x (4347984624)
     65            Context:
     66              Aggregations:
     67                - SUM(x.b)
     68              Group:
     69                - x.a
     70            Projections:
     71              - x.a
     72              - "x".""
     73            Dependencies:
     74            - Join: x (4347985296)
     75              Context:
     76                y:
     77                On: x.a = y.a
     78              Projections:
     79              Dependencies:
     80              - Scan: x (4347983136)
     81                Context:
     82                  Source: x AS x
     83                Projections:
     84              - Scan: y (4343416624)
     85                Context:
     86                  Source: y AS y
     87                Projections:
     88
     89        Args:
     90            expression: the expression to build the DAG from.
     91            ctes: a dictionary that maps CTEs to their corresponding Step DAG by name.
     92
     93        Returns:
     94            A Step DAG corresponding to `expression`.
     95        """
     96        ctes = ctes or {}
     97        expression = expression.unnest()
     98        with_ = expression.args.get("with")
     99
    100        # CTEs break the mold of scope and introduce themselves to all in the context.
    101        if with_:
    102            ctes = ctes.copy()
    103            for cte in with_.expressions:
    104                step = Step.from_expression(cte.this, ctes)
    105                step.name = cte.alias
    106                ctes[step.name] = step  # type: ignore
    107
    108        from_ = expression.args.get("from")
    109
    110        if isinstance(expression, exp.Select) and from_:
    111            step = Scan.from_expression(from_.this, ctes)
    112        elif isinstance(expression, exp.Union):
    113            step = SetOperation.from_expression(expression, ctes)
    114        else:
    115            step = Scan()
    116
    117        joins = expression.args.get("joins")
    118
    119        if joins:
    120            join = Join.from_joins(joins, ctes)
    121            join.name = step.name
    122            join.add_dependency(step)
    123            step = join
    124
    125        projections = []  # final selects in this chain of steps representing a select
    126        operands = {}  # intermediate computations of agg funcs eg x + 1 in SUM(x + 1)
    127        aggregations = set()
    128        next_operand_name = name_sequence("_a_")
    129
    130        def extract_agg_operands(expression):
    131            agg_funcs = tuple(expression.find_all(exp.AggFunc))
    132            if agg_funcs:
    133                aggregations.add(expression)
    134
    135            for agg in agg_funcs:
    136                for operand in agg.unnest_operands():
    137                    if isinstance(operand, exp.Column):
    138                        continue
    139                    if operand not in operands:
    140                        operands[operand] = next_operand_name()
    141
    142                    operand.replace(exp.column(operands[operand], quoted=True))
    143
    144            return bool(agg_funcs)
    145
    146        def set_ops_and_aggs(step):
    147            step.operands = tuple(alias(operand, alias_) for operand, alias_ in operands.items())
    148            step.aggregations = list(aggregations)
    149
    150        for e in expression.expressions:
    151            if e.find(exp.AggFunc):
    152                projections.append(exp.column(e.alias_or_name, step.name, quoted=True))
    153                extract_agg_operands(e)
    154            else:
    155                projections.append(e)
    156
    157        where = expression.args.get("where")
    158
    159        if where:
    160            step.condition = where.this
    161
    162        group = expression.args.get("group")
    163
    164        if group or aggregations:
    165            aggregate = Aggregate()
    166            aggregate.source = step.name
    167            aggregate.name = step.name
    168
    169            having = expression.args.get("having")
    170
    171            if having:
    172                if extract_agg_operands(exp.alias_(having.this, "_h", quoted=True)):
    173                    aggregate.condition = exp.column("_h", step.name, quoted=True)
    174                else:
    175                    aggregate.condition = having.this
    176
    177            set_ops_and_aggs(aggregate)
    178
    179            # give aggregates names and replace projections with references to them
    180            aggregate.group = {
    181                f"_g{i}": e for i, e in enumerate(group.expressions if group else [])
    182            }
    183
    184            intermediate: t.Dict[str | exp.Expression, str] = {}
    185            for k, v in aggregate.group.items():
    186                intermediate[v] = k
    187                if isinstance(v, exp.Column):
    188                    intermediate[v.name] = k
    189
    190            for projection in projections:
    191                for node, *_ in projection.walk():
    192                    name = intermediate.get(node)
    193                    if name:
    194                        node.replace(exp.column(name, step.name))
    195
    196            if aggregate.condition:
    197                for node, *_ in aggregate.condition.walk():
    198                    name = intermediate.get(node) or intermediate.get(node.name)
    199                    if name:
    200                        node.replace(exp.column(name, step.name))
    201
    202            aggregate.add_dependency(step)
    203            step = aggregate
    204
    205        order = expression.args.get("order")
    206
    207        if order:
    208            if isinstance(step, Aggregate):
    209                for i, ordered in enumerate(order.expressions):
    210                    if extract_agg_operands(exp.alias_(ordered.this, f"_o_{i}", quoted=True)):
    211                        ordered.this.replace(exp.column(f"_o_{i}", step.name, quoted=True))
    212
    213                set_ops_and_aggs(aggregate)
    214
    215            sort = Sort()
    216            sort.name = step.name
    217            sort.key = order.expressions
    218            sort.add_dependency(step)
    219            step = sort
    220
    221        step.projections = projections
    222
    223        if isinstance(expression, exp.Select) and expression.args.get("distinct"):
    224            distinct = Aggregate()
    225            distinct.source = step.name
    226            distinct.name = step.name
    227            distinct.group = {
    228                e.alias_or_name: exp.column(col=e.alias_or_name, table=step.name)
    229                for e in projections or expression.expressions
    230            }
    231            distinct.add_dependency(step)
    232            step = distinct
    233
    234        limit = expression.args.get("limit")
    235
    236        if limit:
    237            step.limit = int(limit.text("expression"))
    238
    239        return step

Builds a DAG of Steps from a SQL expression so that it's easier to execute in an engine. Note: the expression's tables and subqueries must be aliased for this method to work. For example, given the following expression:

SELECT x.a, SUM(x.b) FROM x AS x JOIN y AS y ON x.a = y.a GROUP BY x.a

the following DAG is produced (the expression IDs might differ per execution):

- Aggregate: x (4347984624) Context: Aggregations: - SUM(x.b) Group: - x.a Projections:
  - x.a
  - "x"."" Dependencies:
    - Join: x (4347985296) Context: y: On: x.a = y.a Projections: Dependencies:
  - Scan: x (4347983136) Context: Source: x AS x Projections:
  - Scan: y (4343416624) Context: Source: y AS y Projections:

###### Arguments:

- expression: the expression to build the DAG from.
- ctes: a dictionary that maps CTEs to their corresponding Step DAG by name.

###### Returns:

> A Step DAG corresponding to `expression`.

name: Optional[str]
dependencies: Set[Step]
dependents: Set[Step]
projections: Sequence[sqlglot.expressions.Expression]
limit: float
condition: Optional[sqlglot.expressions.Expression]
def add_dependency(self, dependency: Step) -> None: View Source

    249    def add_dependency(self, dependency: Step) -> None:
    250        self.dependencies.add(dependency)
    251        dependency.dependents.add(self)

def to_s(self, level: int = 0) -> str: View Source

    256    def to_s(self, level: int = 0) -> str:
    257        indent = "  " * level
    258        nested = f"{indent}    "
    259
    260        context = self._to_s(f"{nested}  ")
    261
    262        if context:
    263            context = [f"{nested}Context:"] + context
    264
    265        lines = [
    266            f"{indent}- {self.id}",
    267            *context,
    268            f"{nested}Projections:",
    269        ]
    270
    271        for expression in self.projections:
    272            lines.append(f"{nested}  - {expression.sql()}")
    273
    274        if self.condition:
    275            lines.append(f"{nested}Condition: {self.condition.sql()}")
    276
    277        if self.limit is not math.inf:
    278            lines.append(f"{nested}Limit: {self.limit}")
    279
    280        if self.dependencies:
    281            lines.append(f"{nested}Dependencies:")
    282            for dependency in self.dependencies:
    283                lines.append("  " + dependency.to_s(level + 1))
    284
    285        return "\n".join(lines)

type_name: str
id: str
class Scan(Step): View Source

    301class Scan(Step):
    302    @classmethod
    303    def from_expression(
    304        cls, expression: exp.Expression, ctes: t.Optional[t.Dict[str, Step]] = None
    305    ) -> Step:
    306        table = expression
    307        alias_ = expression.alias_or_name
    308
    309        if isinstance(expression, exp.Subquery):
    310            table = expression.this
    311            step = Step.from_expression(table, ctes)
    312            step.name = alias_
    313            return step
    314
    315        step = Scan()
    316        step.name = alias_
    317        step.source = expression
    318        if ctes and table.name in ctes:
    319            step.add_dependency(ctes[table.name])
    320
    321        return step
    322
    323    def __init__(self) -> None:
    324        super().__init__()
    325        self.source: t.Optional[exp.Expression] = None
    326
    327    def _to_s(self, indent: str) -> t.List[str]:
    328        return [f"{indent}Source: {self.source.sql() if self.source else '-static-'}"]  # type: ignore

@classmethod
def from_expression( cls, expression: sqlglot.expressions.Expression, ctes: Optional[Dict[str, Step]] = None) -> Step: View Source

    302    @classmethod
    303    def from_expression(
    304        cls, expression: exp.Expression, ctes: t.Optional[t.Dict[str, Step]] = None
    305    ) -> Step:
    306        table = expression
    307        alias_ = expression.alias_or_name
    308
    309        if isinstance(expression, exp.Subquery):
    310            table = expression.this
    311            step = Step.from_expression(table, ctes)
    312            step.name = alias_
    313            return step
    314
    315        step = Scan()
    316        step.name = alias_
    317        step.source = expression
    318        if ctes and table.name in ctes:
    319            step.add_dependency(ctes[table.name])
    320
    321        return step

Builds a DAG of Steps from a SQL expression so that it's easier to execute in an engine. Note: the expression's tables and subqueries must be aliased for this method to work. For example, given the following expression:

SELECT x.a, SUM(x.b) FROM x AS x JOIN y AS y ON x.a = y.a GROUP BY x.a

the following DAG is produced (the expression IDs might differ per execution):

- Aggregate: x (4347984624) Context: Aggregations: - SUM(x.b) Group: - x.a Projections:
  - x.a
  - "x"."" Dependencies:
    - Join: x (4347985296) Context: y: On: x.a = y.a Projections: Dependencies:
  - Scan: x (4347983136) Context: Source: x AS x Projections:
  - Scan: y (4343416624) Context: Source: y AS y Projections:

###### Arguments:

- expression: the expression to build the DAG from.
- ctes: a dictionary that maps CTEs to their corresponding Step DAG by name.

###### Returns:

> A Step DAG corresponding to `expression`.

source: Optional[sqlglot.expressions.Expression]

##### Inherited Members

Step
  name
  dependencies
  dependents
  projections
  limit
  condition
  add_dependency
  to_s
  type_name
  id

class Join(Step): View Source

    331class Join(Step):
    332    @classmethod
    333    def from_joins(
    334        cls, joins: t.Iterable[exp.Join], ctes: t.Optional[t.Dict[str, Step]] = None
    335    ) -> Step:
    336        step = Join()
    337
    338        for join in joins:
    339            source_key, join_key, condition = join_condition(join)
    340            step.joins[join.alias_or_name] = {
    341                "side": join.side,  # type: ignore
    342                "join_key": join_key,
    343                "source_key": source_key,
    344                "condition": condition,
    345            }
    346
    347            step.add_dependency(Scan.from_expression(join.this, ctes))
    348
    349        return step
    350
    351    def __init__(self) -> None:
    352        super().__init__()
    353        self.joins: t.Dict[str, t.Dict[str, t.List[str] | exp.Expression]] = {}
    354
    355    def _to_s(self, indent: str) -> t.List[str]:
    356        lines = []
    357        for name, join in self.joins.items():
    358            lines.append(f"{indent}{name}: {join['side'] or 'INNER'}")
    359            join_key = ", ".join(str(key) for key in t.cast(list, join.get("join_key") or []))
    360            if join_key:
    361                lines.append(f"{indent}Key: {join_key}")
    362            if join.get("condition"):
    363                lines.append(f"{indent}On: {join['condition'].sql()}")  # type: ignore
    364        return lines

@classmethod
def from_joins( cls, joins: Iterable[sqlglot.expressions.Join], ctes: Optional[Dict[str, Step]] = None) -> Step: View Source

    332    @classmethod
    333    def from_joins(
    334        cls, joins: t.Iterable[exp.Join], ctes: t.Optional[t.Dict[str, Step]] = None
    335    ) -> Step:
    336        step = Join()
    337
    338        for join in joins:
    339            source_key, join_key, condition = join_condition(join)
    340            step.joins[join.alias_or_name] = {
    341                "side": join.side,  # type: ignore
    342                "join_key": join_key,
    343                "source_key": source_key,
    344                "condition": condition,
    345            }
    346
    347            step.add_dependency(Scan.from_expression(join.this, ctes))
    348
    349        return step

joins: Dict[str, Dict[str, Union[List[str], sqlglot.expressions.Expression]]]

##### Inherited Members

Step
  from_expression
  name
  dependencies
  dependents
  projections
  limit
  condition
  add_dependency
  to_s
  type_name
  id

class Aggregate(Step): View Source

    367class Aggregate(Step):
    368    def __init__(self) -> None:
    369        super().__init__()
    370        self.aggregations: t.List[exp.Expression] = []
    371        self.operands: t.Tuple[exp.Expression, ...] = ()
    372        self.group: t.Dict[str, exp.Expression] = {}
    373        self.source: t.Optional[str] = None
    374
    375    def _to_s(self, indent: str) -> t.List[str]:
    376        lines = [f"{indent}Aggregations:"]
    377
    378        for expression in self.aggregations:
    379            lines.append(f"{indent}  - {expression.sql()}")
    380
    381        if self.group:
    382            lines.append(f"{indent}Group:")
    383            for expression in self.group.values():
    384                lines.append(f"{indent}  - {expression.sql()}")
    385        if self.condition:
    386            lines.append(f"{indent}Having:")
    387            lines.append(f"{indent}  - {self.condition.sql()}")
    388        if self.operands:
    389            lines.append(f"{indent}Operands:")
    390            for expression in self.operands:
    391                lines.append(f"{indent}  - {expression.sql()}")
    392
    393        return lines

aggregations: List[sqlglot.expressions.Expression]
operands: Tuple[sqlglot.expressions.Expression, ...]
group: Dict[str, sqlglot.expressions.Expression]
source: Optional[str]

##### Inherited Members

Step
  from_expression
  name
  dependencies
  dependents
  projections
  limit
  condition
  add_dependency
  to_s
  type_name
  id

class Sort(Step): View Source

    396class Sort(Step):
    397    def __init__(self) -> None:
    398        super().__init__()
    399        self.key = None
    400
    401    def _to_s(self, indent: str) -> t.List[str]:
    402        lines = [f"{indent}Key:"]
    403
    404        for expression in self.key:  # type: ignore
    405            lines.append(f"{indent}  - {expression.sql()}")
    406
    407        return lines

key

##### Inherited Members

Step
  from_expression
  name
  dependencies
  dependents
  projections
  limit
  condition
  add_dependency
  to_s
  type_name
  id

class SetOperation(Step): View Source

    410class SetOperation(Step):
    411    def __init__(
    412        self,
    413        op: t.Type[exp.Expression],
    414        left: str | None,
    415        right: str | None,
    416        distinct: bool = False,
    417    ) -> None:
    418        super().__init__()
    419        self.op = op
    420        self.left = left
    421        self.right = right
    422        self.distinct = distinct
    423
    424    @classmethod
    425    def from_expression(
    426        cls, expression: exp.Expression, ctes: t.Optional[t.Dict[str, Step]] = None
    427    ) -> Step:
    428        assert isinstance(expression, exp.Union)
    429
    430        left = Step.from_expression(expression.left, ctes)
    431        # SELECT 1 UNION SELECT 2  <-- these subqueries don't have names
    432        left.name = left.name or "left"
    433        right = Step.from_expression(expression.right, ctes)
    434        right.name = right.name or "right"
    435        step = cls(
    436            op=expression.__class__,
    437            left=left.name,
    438            right=right.name,
    439            distinct=bool(expression.args.get("distinct")),
    440        )
    441
    442        step.add_dependency(left)
    443        step.add_dependency(right)
    444
    445        limit = expression.args.get("limit")
    446
    447        if limit:
    448            step.limit = int(limit.text("expression"))
    449
    450        return step
    451
    452    def _to_s(self, indent: str) -> t.List[str]:
    453        lines = []
    454        if self.distinct:
    455            lines.append(f"{indent}Distinct: {self.distinct}")
    456        return lines
    457
    458    @property
    459    def type_name(self) -> str:
    460        return self.op.__name__

SetOperation( op: Type[sqlglot.expressions.Expression], left: str | None, right: str | None, distinct: bool = False) View Source

    411    def __init__(
    412        self,
    413        op: t.Type[exp.Expression],
    414        left: str | None,
    415        right: str | None,
    416        distinct: bool = False,
    417    ) -> None:
    418        super().__init__()
    419        self.op = op
    420        self.left = left
    421        self.right = right
    422        self.distinct = distinct

op
left
right
distinct
@classmethod
def from_expression( cls, expression: sqlglot.expressions.Expression, ctes: Optional[Dict[str, Step]] = None) -> Step: View Source

    424    @classmethod
    425    def from_expression(
    426        cls, expression: exp.Expression, ctes: t.Optional[t.Dict[str, Step]] = None
    427    ) -> Step:
    428        assert isinstance(expression, exp.Union)
    429
    430        left = Step.from_expression(expression.left, ctes)
    431        # SELECT 1 UNION SELECT 2  <-- these subqueries don't have names
    432        left.name = left.name or "left"
    433        right = Step.from_expression(expression.right, ctes)
    434        right.name = right.name or "right"
    435        step = cls(
    436            op=expression.__class__,
    437            left=left.name,
    438            right=right.name,
    439            distinct=bool(expression.args.get("distinct")),
    440        )
    441
    442        step.add_dependency(left)
    443        step.add_dependency(right)
    444
    445        limit = expression.args.get("limit")
    446
    447        if limit:
    448            step.limit = int(limit.text("expression"))
    449
    450        return step

Builds a DAG of Steps from a SQL expression so that it's easier to execute in an engine. Note: the expression's tables and subqueries must be aliased for this method to work. For example, given the following expression:

SELECT x.a, SUM(x.b) FROM x AS x JOIN y AS y ON x.a = y.a GROUP BY x.a

the following DAG is produced (the expression IDs might differ per execution):

- Aggregate: x (4347984624) Context: Aggregations: - SUM(x.b) Group: - x.a Projections:
  - x.a
  - "x"."" Dependencies:
    - Join: x (4347985296) Context: y: On: x.a = y.a Projections: Dependencies:
  - Scan: x (4347983136) Context: Source: x AS x Projections:
  - Scan: y (4343416624) Context: Source: y AS y Projections:

###### Arguments:

- expression: the expression to build the DAG from.
- ctes: a dictionary that maps CTEs to their corresponding Step DAG by name.

###### Returns:

> A Step DAG corresponding to `expression`.

type_name: str

##### Inherited Members

Step
  name
  dependencies
  dependents
  projections
  limit
  condition
  add_dependency
  to_s
  id