@altimateai/altimate-code 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/bin/altimate +6 -0
  3. package/bin/altimate-code +6 -0
  4. package/dbt-tools/bin/altimate-dbt +2 -0
  5. package/dbt-tools/dist/altimate_python_packages/altimate_packages/altimate/__init__.py +0 -0
  6. package/dbt-tools/dist/altimate_python_packages/altimate_packages/altimate/fetch_schema.py +35 -0
  7. package/dbt-tools/dist/altimate_python_packages/altimate_packages/altimate/utils.py +353 -0
  8. package/dbt-tools/dist/altimate_python_packages/altimate_packages/altimate/validate_sql.py +114 -0
  9. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/__init__.py +178 -0
  10. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/__main__.py +96 -0
  11. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/_typing.py +17 -0
  12. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/__init__.py +3 -0
  13. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/sql/__init__.py +18 -0
  14. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/sql/_typing.py +18 -0
  15. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/sql/column.py +332 -0
  16. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/sql/dataframe.py +866 -0
  17. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/sql/functions.py +1267 -0
  18. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/sql/group.py +59 -0
  19. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/sql/normalize.py +78 -0
  20. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/sql/operations.py +53 -0
  21. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/sql/readwriter.py +108 -0
  22. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/sql/session.py +190 -0
  23. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/sql/transforms.py +9 -0
  24. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/sql/types.py +212 -0
  25. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/sql/util.py +32 -0
  26. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dataframe/sql/window.py +134 -0
  27. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/__init__.py +118 -0
  28. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/athena.py +166 -0
  29. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/bigquery.py +1331 -0
  30. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/clickhouse.py +1393 -0
  31. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/databricks.py +131 -0
  32. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/dialect.py +1915 -0
  33. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/doris.py +561 -0
  34. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/drill.py +157 -0
  35. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/druid.py +20 -0
  36. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/duckdb.py +1159 -0
  37. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/dune.py +16 -0
  38. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/hive.py +787 -0
  39. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/materialize.py +94 -0
  40. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/mysql.py +1324 -0
  41. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/oracle.py +378 -0
  42. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/postgres.py +778 -0
  43. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/presto.py +788 -0
  44. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/prql.py +203 -0
  45. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/redshift.py +448 -0
  46. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/risingwave.py +78 -0
  47. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/snowflake.py +1464 -0
  48. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/spark.py +202 -0
  49. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/spark2.py +349 -0
  50. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/sqlite.py +320 -0
  51. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/starrocks.py +343 -0
  52. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/tableau.py +61 -0
  53. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/teradata.py +356 -0
  54. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/trino.py +115 -0
  55. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/dialects/tsql.py +1403 -0
  56. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/diff.py +456 -0
  57. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/errors.py +93 -0
  58. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/executor/__init__.py +95 -0
  59. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/executor/context.py +101 -0
  60. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/executor/env.py +246 -0
  61. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/executor/python.py +460 -0
  62. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/executor/table.py +155 -0
  63. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/expressions.py +8870 -0
  64. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/generator.py +4993 -0
  65. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/helper.py +582 -0
  66. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/jsonpath.py +227 -0
  67. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/lineage.py +423 -0
  68. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/__init__.py +11 -0
  69. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/annotate_types.py +589 -0
  70. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/canonicalize.py +222 -0
  71. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/eliminate_ctes.py +43 -0
  72. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/eliminate_joins.py +181 -0
  73. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/eliminate_subqueries.py +189 -0
  74. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/isolate_table_selects.py +50 -0
  75. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/merge_subqueries.py +415 -0
  76. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/normalize.py +200 -0
  77. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/normalize_identifiers.py +64 -0
  78. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/optimize_joins.py +91 -0
  79. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/optimizer.py +94 -0
  80. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/pushdown_predicates.py +222 -0
  81. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/pushdown_projections.py +172 -0
  82. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/qualify.py +104 -0
  83. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/qualify_columns.py +1024 -0
  84. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/qualify_tables.py +155 -0
  85. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/scope.py +904 -0
  86. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/simplify.py +1587 -0
  87. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/optimizer/unnest_subqueries.py +302 -0
  88. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/parser.py +8501 -0
  89. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/planner.py +463 -0
  90. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/schema.py +588 -0
  91. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/serde.py +68 -0
  92. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/time.py +687 -0
  93. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/tokens.py +1520 -0
  94. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/transforms.py +1020 -0
  95. package/dbt-tools/dist/altimate_python_packages/altimate_packages/sqlglot/trie.py +81 -0
  96. package/dbt-tools/dist/altimate_python_packages/dbt_core_integration.py +825 -0
  97. package/dbt-tools/dist/altimate_python_packages/dbt_utils.py +157 -0
  98. package/dbt-tools/dist/index.js +23859 -0
  99. package/package.json +13 -13
  100. package/postinstall.mjs +42 -0
  101. package/skills/altimate-setup/SKILL.md +31 -0
@@ -0,0 +1,463 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import typing as t
5
+
6
+ from sqlglot import alias, exp
7
+ from sqlglot.helper import name_sequence
8
+ from sqlglot.optimizer.eliminate_joins import join_condition
9
+
10
+
11
+ class 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
+
43
+ class 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.SetOperation):
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.source_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 = {}
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[expression] = None
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
+ else:
205
+ aggregate = None
206
+
207
+ order = expression.args.get("order")
208
+
209
+ if order:
210
+ if aggregate and isinstance(step, Aggregate):
211
+ for i, ordered in enumerate(order.expressions):
212
+ if extract_agg_operands(exp.alias_(ordered.this, f"_o_{i}", quoted=True)):
213
+ ordered.this.replace(exp.column(f"_o_{i}", step.name, quoted=True))
214
+
215
+ set_ops_and_aggs(aggregate)
216
+
217
+ sort = Sort()
218
+ sort.name = step.name
219
+ sort.key = order.expressions
220
+ sort.add_dependency(step)
221
+ step = sort
222
+
223
+ step.projections = projections
224
+
225
+ if isinstance(expression, exp.Select) and expression.args.get("distinct"):
226
+ distinct = Aggregate()
227
+ distinct.source = step.name
228
+ distinct.name = step.name
229
+ distinct.group = {
230
+ e.alias_or_name: exp.column(col=e.alias_or_name, table=step.name)
231
+ for e in projections or expression.expressions
232
+ }
233
+ distinct.add_dependency(step)
234
+ step = distinct
235
+
236
+ limit = expression.args.get("limit")
237
+
238
+ if limit:
239
+ step.limit = int(limit.text("expression"))
240
+
241
+ return step
242
+
243
+ def __init__(self) -> None:
244
+ self.name: t.Optional[str] = None
245
+ self.dependencies: t.Set[Step] = set()
246
+ self.dependents: t.Set[Step] = set()
247
+ self.projections: t.Sequence[exp.Expression] = []
248
+ self.limit: float = math.inf
249
+ self.condition: t.Optional[exp.Expression] = None
250
+
251
+ def add_dependency(self, dependency: Step) -> None:
252
+ self.dependencies.add(dependency)
253
+ dependency.dependents.add(self)
254
+
255
+ def __repr__(self) -> str:
256
+ return self.to_s()
257
+
258
+ def to_s(self, level: int = 0) -> str:
259
+ indent = " " * level
260
+ nested = f"{indent} "
261
+
262
+ context = self._to_s(f"{nested} ")
263
+
264
+ if context:
265
+ context = [f"{nested}Context:"] + context
266
+
267
+ lines = [
268
+ f"{indent}- {self.id}",
269
+ *context,
270
+ f"{nested}Projections:",
271
+ ]
272
+
273
+ for expression in self.projections:
274
+ lines.append(f"{nested} - {expression.sql()}")
275
+
276
+ if self.condition:
277
+ lines.append(f"{nested}Condition: {self.condition.sql()}")
278
+
279
+ if self.limit is not math.inf:
280
+ lines.append(f"{nested}Limit: {self.limit}")
281
+
282
+ if self.dependencies:
283
+ lines.append(f"{nested}Dependencies:")
284
+ for dependency in self.dependencies:
285
+ lines.append(" " + dependency.to_s(level + 1))
286
+
287
+ return "\n".join(lines)
288
+
289
+ @property
290
+ def type_name(self) -> str:
291
+ return self.__class__.__name__
292
+
293
+ @property
294
+ def id(self) -> str:
295
+ name = self.name
296
+ name = f" {name}" if name else ""
297
+ return f"{self.type_name}:{name} ({id(self)})"
298
+
299
+ def _to_s(self, _indent: str) -> t.List[str]:
300
+ return []
301
+
302
+
303
+ class Scan(Step):
304
+ @classmethod
305
+ def from_expression(
306
+ cls, expression: exp.Expression, ctes: t.Optional[t.Dict[str, Step]] = None
307
+ ) -> Step:
308
+ table = expression
309
+ alias_ = expression.alias_or_name
310
+
311
+ if isinstance(expression, exp.Subquery):
312
+ table = expression.this
313
+ step = Step.from_expression(table, ctes)
314
+ step.name = alias_
315
+ return step
316
+
317
+ step = Scan()
318
+ step.name = alias_
319
+ step.source = expression
320
+ if ctes and table.name in ctes:
321
+ step.add_dependency(ctes[table.name])
322
+
323
+ return step
324
+
325
+ def __init__(self) -> None:
326
+ super().__init__()
327
+ self.source: t.Optional[exp.Expression] = None
328
+
329
+ def _to_s(self, indent: str) -> t.List[str]:
330
+ return [f"{indent}Source: {self.source.sql() if self.source else '-static-'}"] # type: ignore
331
+
332
+
333
+ class Join(Step):
334
+ @classmethod
335
+ def from_joins(
336
+ cls, joins: t.Iterable[exp.Join], ctes: t.Optional[t.Dict[str, Step]] = None
337
+ ) -> Join:
338
+ step = Join()
339
+
340
+ for join in joins:
341
+ source_key, join_key, condition = join_condition(join)
342
+ step.joins[join.alias_or_name] = {
343
+ "side": join.side, # type: ignore
344
+ "join_key": join_key,
345
+ "source_key": source_key,
346
+ "condition": condition,
347
+ }
348
+
349
+ step.add_dependency(Scan.from_expression(join.this, ctes))
350
+
351
+ return step
352
+
353
+ def __init__(self) -> None:
354
+ super().__init__()
355
+ self.source_name: t.Optional[str] = None
356
+ self.joins: t.Dict[str, t.Dict[str, t.List[str] | exp.Expression]] = {}
357
+
358
+ def _to_s(self, indent: str) -> t.List[str]:
359
+ lines = [f"{indent}Source: {self.source_name or self.name}"]
360
+ for name, join in self.joins.items():
361
+ lines.append(f"{indent}{name}: {join['side'] or 'INNER'}")
362
+ join_key = ", ".join(str(key) for key in t.cast(list, join.get("join_key") or []))
363
+ if join_key:
364
+ lines.append(f"{indent}Key: {join_key}")
365
+ if join.get("condition"):
366
+ lines.append(f"{indent}On: {join['condition'].sql()}") # type: ignore
367
+ return lines
368
+
369
+
370
+ class Aggregate(Step):
371
+ def __init__(self) -> None:
372
+ super().__init__()
373
+ self.aggregations: t.List[exp.Expression] = []
374
+ self.operands: t.Tuple[exp.Expression, ...] = ()
375
+ self.group: t.Dict[str, exp.Expression] = {}
376
+ self.source: t.Optional[str] = None
377
+
378
+ def _to_s(self, indent: str) -> t.List[str]:
379
+ lines = [f"{indent}Aggregations:"]
380
+
381
+ for expression in self.aggregations:
382
+ lines.append(f"{indent} - {expression.sql()}")
383
+
384
+ if self.group:
385
+ lines.append(f"{indent}Group:")
386
+ for expression in self.group.values():
387
+ lines.append(f"{indent} - {expression.sql()}")
388
+ if self.condition:
389
+ lines.append(f"{indent}Having:")
390
+ lines.append(f"{indent} - {self.condition.sql()}")
391
+ if self.operands:
392
+ lines.append(f"{indent}Operands:")
393
+ for expression in self.operands:
394
+ lines.append(f"{indent} - {expression.sql()}")
395
+
396
+ return lines
397
+
398
+
399
+ class Sort(Step):
400
+ def __init__(self) -> None:
401
+ super().__init__()
402
+ self.key = None
403
+
404
+ def _to_s(self, indent: str) -> t.List[str]:
405
+ lines = [f"{indent}Key:"]
406
+
407
+ for expression in self.key: # type: ignore
408
+ lines.append(f"{indent} - {expression.sql()}")
409
+
410
+ return lines
411
+
412
+
413
+ class SetOperation(Step):
414
+ def __init__(
415
+ self,
416
+ op: t.Type[exp.Expression],
417
+ left: str | None,
418
+ right: str | None,
419
+ distinct: bool = False,
420
+ ) -> None:
421
+ super().__init__()
422
+ self.op = op
423
+ self.left = left
424
+ self.right = right
425
+ self.distinct = distinct
426
+
427
+ @classmethod
428
+ def from_expression(
429
+ cls, expression: exp.Expression, ctes: t.Optional[t.Dict[str, Step]] = None
430
+ ) -> SetOperation:
431
+ assert isinstance(expression, exp.SetOperation)
432
+
433
+ left = Step.from_expression(expression.left, ctes)
434
+ # SELECT 1 UNION SELECT 2 <-- these subqueries don't have names
435
+ left.name = left.name or "left"
436
+ right = Step.from_expression(expression.right, ctes)
437
+ right.name = right.name or "right"
438
+ step = cls(
439
+ op=expression.__class__,
440
+ left=left.name,
441
+ right=right.name,
442
+ distinct=bool(expression.args.get("distinct")),
443
+ )
444
+
445
+ step.add_dependency(left)
446
+ step.add_dependency(right)
447
+
448
+ limit = expression.args.get("limit")
449
+
450
+ if limit:
451
+ step.limit = int(limit.text("expression"))
452
+
453
+ return step
454
+
455
+ def _to_s(self, indent: str) -> t.List[str]:
456
+ lines = []
457
+ if self.distinct:
458
+ lines.append(f"{indent}Distinct: {self.distinct}")
459
+ return lines
460
+
461
+ @property
462
+ def type_name(self) -> str:
463
+ return self.op.__name__