relationalai 1.0.0a3__py3-none-any.whl → 1.0.0a5__py3-none-any.whl

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 (118) hide show
  1. relationalai/config/config.py +47 -21
  2. relationalai/config/connections/__init__.py +5 -2
  3. relationalai/config/connections/duckdb.py +2 -2
  4. relationalai/config/connections/local.py +31 -0
  5. relationalai/config/connections/snowflake.py +0 -1
  6. relationalai/config/external/raiconfig_converter.py +235 -0
  7. relationalai/config/external/raiconfig_models.py +202 -0
  8. relationalai/config/external/utils.py +31 -0
  9. relationalai/config/shims.py +1 -0
  10. relationalai/semantics/__init__.py +10 -8
  11. relationalai/semantics/backends/sql/sql_compiler.py +1 -4
  12. relationalai/semantics/experimental/__init__.py +0 -0
  13. relationalai/semantics/experimental/builder.py +295 -0
  14. relationalai/semantics/experimental/builtins.py +154 -0
  15. relationalai/semantics/frontend/base.py +67 -42
  16. relationalai/semantics/frontend/core.py +34 -6
  17. relationalai/semantics/frontend/front_compiler.py +209 -37
  18. relationalai/semantics/frontend/pprint.py +6 -2
  19. relationalai/semantics/metamodel/__init__.py +7 -0
  20. relationalai/semantics/metamodel/metamodel.py +2 -0
  21. relationalai/semantics/metamodel/metamodel_analyzer.py +58 -16
  22. relationalai/semantics/metamodel/pprint.py +6 -1
  23. relationalai/semantics/metamodel/rewriter.py +11 -7
  24. relationalai/semantics/metamodel/typer.py +116 -41
  25. relationalai/semantics/reasoners/__init__.py +11 -0
  26. relationalai/semantics/reasoners/graph/__init__.py +35 -0
  27. relationalai/semantics/reasoners/graph/core.py +9028 -0
  28. relationalai/semantics/std/__init__.py +30 -10
  29. relationalai/semantics/std/aggregates.py +641 -12
  30. relationalai/semantics/std/common.py +146 -13
  31. relationalai/semantics/std/constraints.py +71 -1
  32. relationalai/semantics/std/datetime.py +904 -21
  33. relationalai/semantics/std/decimals.py +143 -2
  34. relationalai/semantics/std/floats.py +57 -4
  35. relationalai/semantics/std/integers.py +98 -4
  36. relationalai/semantics/std/math.py +857 -35
  37. relationalai/semantics/std/numbers.py +216 -20
  38. relationalai/semantics/std/re.py +213 -5
  39. relationalai/semantics/std/strings.py +437 -44
  40. relationalai/shims/executor.py +60 -52
  41. relationalai/shims/fixtures.py +85 -0
  42. relationalai/shims/helpers.py +26 -2
  43. relationalai/shims/hoister.py +28 -9
  44. relationalai/shims/mm2v0.py +204 -173
  45. relationalai/tools/cli/cli.py +192 -10
  46. relationalai/tools/cli/components/progress_reader.py +1 -1
  47. relationalai/tools/cli/docs.py +394 -0
  48. relationalai/tools/debugger.py +11 -4
  49. relationalai/tools/qb_debugger.py +435 -0
  50. relationalai/tools/typer_debugger.py +1 -2
  51. relationalai/util/dataclasses.py +3 -5
  52. relationalai/util/docutils.py +1 -2
  53. relationalai/util/error.py +2 -5
  54. relationalai/util/python.py +23 -0
  55. relationalai/util/runtime.py +1 -2
  56. relationalai/util/schema.py +2 -4
  57. relationalai/util/structures.py +4 -2
  58. relationalai/util/tracing.py +8 -2
  59. {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/METADATA +8 -5
  60. {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/RECORD +118 -95
  61. {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/WHEEL +1 -1
  62. v0/relationalai/__init__.py +1 -1
  63. v0/relationalai/clients/client.py +52 -18
  64. v0/relationalai/clients/exec_txn_poller.py +122 -0
  65. v0/relationalai/clients/local.py +23 -8
  66. v0/relationalai/clients/resources/azure/azure.py +36 -11
  67. v0/relationalai/clients/resources/snowflake/__init__.py +4 -4
  68. v0/relationalai/clients/resources/snowflake/cli_resources.py +12 -1
  69. v0/relationalai/clients/resources/snowflake/direct_access_resources.py +124 -100
  70. v0/relationalai/clients/resources/snowflake/engine_service.py +381 -0
  71. v0/relationalai/clients/resources/snowflake/engine_state_handlers.py +35 -29
  72. v0/relationalai/clients/resources/snowflake/error_handlers.py +43 -2
  73. v0/relationalai/clients/resources/snowflake/snowflake.py +277 -179
  74. v0/relationalai/clients/resources/snowflake/use_index_poller.py +8 -0
  75. v0/relationalai/clients/types.py +5 -0
  76. v0/relationalai/errors.py +19 -1
  77. v0/relationalai/semantics/lqp/algorithms.py +173 -0
  78. v0/relationalai/semantics/lqp/builtins.py +199 -2
  79. v0/relationalai/semantics/lqp/executor.py +68 -37
  80. v0/relationalai/semantics/lqp/ir.py +28 -2
  81. v0/relationalai/semantics/lqp/model2lqp.py +215 -45
  82. v0/relationalai/semantics/lqp/passes.py +13 -658
  83. v0/relationalai/semantics/lqp/rewrite/__init__.py +12 -0
  84. v0/relationalai/semantics/lqp/rewrite/algorithm.py +385 -0
  85. v0/relationalai/semantics/lqp/rewrite/constants_to_vars.py +70 -0
  86. v0/relationalai/semantics/lqp/rewrite/deduplicate_vars.py +104 -0
  87. v0/relationalai/semantics/lqp/rewrite/eliminate_data.py +108 -0
  88. v0/relationalai/semantics/lqp/rewrite/extract_keys.py +25 -3
  89. v0/relationalai/semantics/lqp/rewrite/period_math.py +77 -0
  90. v0/relationalai/semantics/lqp/rewrite/quantify_vars.py +65 -31
  91. v0/relationalai/semantics/lqp/rewrite/unify_definitions.py +317 -0
  92. v0/relationalai/semantics/lqp/utils.py +11 -1
  93. v0/relationalai/semantics/lqp/validators.py +14 -1
  94. v0/relationalai/semantics/metamodel/builtins.py +2 -1
  95. v0/relationalai/semantics/metamodel/compiler.py +2 -1
  96. v0/relationalai/semantics/metamodel/dependency.py +12 -3
  97. v0/relationalai/semantics/metamodel/executor.py +11 -1
  98. v0/relationalai/semantics/metamodel/factory.py +2 -2
  99. v0/relationalai/semantics/metamodel/helpers.py +7 -0
  100. v0/relationalai/semantics/metamodel/ir.py +3 -2
  101. v0/relationalai/semantics/metamodel/rewrite/dnf_union_splitter.py +30 -20
  102. v0/relationalai/semantics/metamodel/rewrite/flatten.py +50 -13
  103. v0/relationalai/semantics/metamodel/rewrite/format_outputs.py +9 -3
  104. v0/relationalai/semantics/metamodel/typer/checker.py +6 -4
  105. v0/relationalai/semantics/metamodel/typer/typer.py +4 -3
  106. v0/relationalai/semantics/metamodel/visitor.py +4 -3
  107. v0/relationalai/semantics/reasoners/optimization/solvers_dev.py +1 -1
  108. v0/relationalai/semantics/reasoners/optimization/solvers_pb.py +336 -86
  109. v0/relationalai/semantics/rel/compiler.py +2 -1
  110. v0/relationalai/semantics/rel/executor.py +3 -2
  111. v0/relationalai/semantics/tests/lqp/__init__.py +0 -0
  112. v0/relationalai/semantics/tests/lqp/algorithms.py +345 -0
  113. v0/relationalai/tools/cli.py +339 -186
  114. v0/relationalai/tools/cli_controls.py +216 -67
  115. v0/relationalai/tools/cli_helpers.py +410 -6
  116. v0/relationalai/util/format.py +5 -2
  117. {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/entry_points.txt +0 -0
  118. {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,31 @@
1
1
  from v0.relationalai.semantics.metamodel import ir, builtins, helpers, types
2
2
  from v0.relationalai.semantics.metamodel.visitor import collect_by_type
3
- from v0.relationalai.semantics.metamodel.util import FrozenOrderedSet
4
- from v0.relationalai.semantics.lqp import ir as lqp, utils, types as lqp_types, builtins as lqp_builtins
3
+ from v0.relationalai.semantics.metamodel.util import FrozenOrderedSet, OrderedSet
4
+ from v0.relationalai.semantics.metamodel.compiler import group_tasks
5
+ from v0.relationalai.semantics.lqp import ir as lqp, utils, types as lqp_types
5
6
  from v0.relationalai.semantics.lqp.primitives import lqp_avg_op, lqp_operator, build_primitive
6
7
  from v0.relationalai.semantics.lqp.pragmas import pragma_to_lqp_name
7
8
  from v0.relationalai.semantics.lqp.types import meta_type_to_lqp
8
9
  from v0.relationalai.semantics.lqp.constructors import (
9
10
  mk_abstraction, mk_and, mk_exists, mk_or, mk_pragma, mk_primitive,
10
- mk_specialized_value, mk_type, mk_value, mk_attribute
11
+ mk_specialized_value, mk_type, mk_value, mk_attribute,
11
12
  )
12
- from v0.relationalai.semantics.lqp.utils import TranslationCtx, gen_unique_var
13
+ from v0.relationalai.semantics.lqp.algorithms import (
14
+ is_script, is_algorithm_script, is_algorithm_logical,
15
+ is_while_loop, is_while_script, construct_monoid, is_empty_instruction
16
+ )
17
+ from v0.relationalai.semantics.lqp.builtins import (
18
+ has_global_annotation, get_upsert_annotation, get_monoid_annotation,
19
+ get_monus_annotation, has_assign_annotation, get_arity, supported_lqp_annotations
20
+ )
21
+ from v0.relationalai.semantics.lqp.utils import TranslationCtx, ExportDescriptor, gen_unique_var, gen_rel_id
13
22
  from v0.relationalai.semantics.lqp.validators import assert_valid_input
14
23
  from v0.relationalai.semantics.lqp.rewrite.functional_dependencies import (
15
24
  normalized_fd, contains_only_declarable_constraints
16
25
  )
17
26
  from decimal import Decimal as PyDecimal
18
27
  from datetime import datetime, date, timezone
19
- from typing import Tuple, cast, Union, Optional
28
+ from typing import Sequence, Tuple, cast, Union, Optional
20
29
  from warnings import warn
21
30
  import re
22
31
  import uuid
@@ -42,8 +51,8 @@ def to_lqp(model: ir.Model, fragment_name: bytes, ctx: TranslationCtx) -> tuple[
42
51
  reads.extend(_get_output_reads(ctx.output_ids))
43
52
 
44
53
  export_info = None
45
- if len(ctx.export_ids) > 0:
46
- export_filename, col_types, export_reads = _get_export_reads(ctx.export_ids)
54
+ if len(ctx.export_descriptors) > 0:
55
+ export_filename, col_types, export_reads = _get_export_reads(ctx.export_descriptors)
47
56
  reads.extend(export_reads)
48
57
  export_info = (export_filename, col_types)
49
58
 
@@ -76,14 +85,17 @@ def _get_output_reads(output_ids: list[tuple[lqp.RelationId, str]]) -> list[lqp.
76
85
  reads.append(lqp.Read(read_type=output, meta=None))
77
86
  return reads
78
87
 
79
- def _get_export_reads(export_ids: list[tuple[lqp.RelationId, int, lqp.Type]]) -> tuple[str, list, list[lqp.Read]]:
88
+ def _get_export_reads(descriptors: list[ExportDescriptor]) -> tuple[str, list, list[lqp.Read]]:
80
89
  reads = []
81
90
  csv_columns = []
82
91
  col_info = []
83
- for (col_id, col_num, col_type) in sorted(export_ids, key=lambda x: x[1]):
84
- col_name = f"col{col_num:03}"
85
- csv_columns.append(lqp.ExportCSVColumn(column_name=col_name, column_data=col_id, meta=None))
86
- col_info.append((col_name, col_type))
92
+ for descriptor in sorted(descriptors, key=lambda x: x.column_number):
93
+ csv_columns.append(lqp.ExportCSVColumn(
94
+ column_name=descriptor.column_name,
95
+ column_data=descriptor.relation_id,
96
+ meta=None,
97
+ ))
98
+ col_info.append((descriptor.column_name, descriptor.column_type))
87
99
 
88
100
  # Generate a random name for the internal export path
89
101
  export_filename = "export_" + str(uuid.uuid4()).replace("-", "_")
@@ -106,6 +118,8 @@ def _get_export_reads(export_ids: list[tuple[lqp.RelationId, int, lqp.Type]]) ->
106
118
  def _translate_to_decls(ctx: TranslationCtx, rule: ir.Logical) -> list[lqp.Declaration]:
107
119
  if contains_only_declarable_constraints(rule):
108
120
  return _translate_to_constraint_decls(ctx, rule)
121
+ elif is_algorithm_logical(rule):
122
+ return _translate_algorithms(ctx, rule)
109
123
  else:
110
124
  return _translate_to_standard_decl(ctx, rule)
111
125
 
@@ -140,39 +154,161 @@ def _translate_to_constraint_decls(ctx: TranslationCtx, rule: ir.Logical) -> lis
140
154
 
141
155
  return constraint_decls
142
156
 
143
- def _translate_to_standard_decl(ctx: TranslationCtx, rule: ir.Logical) -> list[lqp.Declaration]:
144
- effects = collect_by_type((ir.Output, ir.Update), rule)
145
- aggregates = collect_by_type(ir.Aggregate, rule)
146
- ranks = collect_by_type(ir.Rank, rule)
157
+ def _translate_algorithms(ctx: TranslationCtx, task: ir.Logical) -> list[lqp.Declaration]:
158
+ assert is_algorithm_logical(task)
159
+ decls: list[lqp.Declaration] = []
160
+ for subtask in task.body:
161
+ assert is_algorithm_script(subtask), "Expected all subtasks to be algorithm scripts"
162
+ decls.extend(_translate_algorithm_script(ctx, subtask))
163
+ return decls
164
+
165
+ def _translate_algorithm_script(ctx: TranslationCtx, alg_task: ir.Sequence) -> list[lqp.Declaration]:
166
+ assert is_algorithm_script(alg_task), "Expected Sequence @algorithm @script "
167
+
168
+ alg_globals = _find_algorithm_global_relation_ids(ctx, alg_task)
169
+ alg_body = _translate_script(ctx, alg_task)
170
+
171
+ return [lqp.Algorithm(global_=alg_globals, body=alg_body, meta=None)]
172
+
173
+ def _find_algorithm_global_relation_ids(ctx: TranslationCtx, alg_task: ir.Sequence) -> list[lqp.RelationId]:
174
+ result = []
175
+ updates = collect_by_type(ir.Update, alg_task)
176
+ for update in updates:
177
+ if has_global_annotation(update):
178
+ bindings = _effect_bindings(update)
179
+ projection, _ = _translate_bindings(ctx, bindings)
180
+ rel_id = get_relation_id(ctx, update.relation, projection)
181
+ result.append(rel_id)
182
+ return list(dict.fromkeys(result))
183
+
184
+ def _translate_script(ctx: TranslationCtx, task: ir.Sequence) -> lqp.Script:
185
+ assert is_script(task), "Expected a @script Sequence"
186
+
187
+ constructs: list[lqp.Construct] = []
188
+
189
+ for subtask in task.tasks:
190
+ if is_empty_instruction(subtask):
191
+ constructs.append(_translate_empty_instruction(ctx, subtask))
192
+ elif isinstance(subtask, ir.Logical):
193
+ constructs.extend(_translate_instruction(ctx, subtask))
194
+ elif isinstance(subtask, ir.Break):
195
+ constructs.append(_translate_break_instruction(ctx, subtask))
196
+ elif is_while_loop(subtask):
197
+ constructs.append(_translate_while_loop(ctx, subtask))
198
+ else:
199
+ raise Exception(f"Unsupported script instruction: {subtask}")
200
+
201
+ return lqp.Script(constructs=constructs, meta=None)
202
+
203
+ def _translate_while_loop(ctx: TranslationCtx, task: ir.Loop) -> lqp.Loop:
204
+ assert is_while_loop(task), "Expected a @while Loop"
205
+ assert len(task.iter) == 0, "Temporalized loops not supported"
206
+
207
+ while_script_task = task.body
208
+ assert is_while_script(while_script_task), "The body of a @while Loop must be a @while @script Sequence"
209
+ body_script = _translate_script(ctx, while_script_task)
210
+
211
+ # No init instructions in the translation of PyRel Loops to to LQP loops
212
+ return lqp.Loop(init=[], body=body_script, meta=None)
213
+
214
+ def _translate_break_instruction(ctx: TranslationCtx, task: ir.Break) -> lqp.Construct:
215
+ body = _translate_to_formula(ctx, task.check)
216
+
217
+ ctx.break_rule_counter += 1
218
+
219
+ rel_id = gen_rel_id(ctx, "break_cond_" + str(ctx.break_rule_counter))
220
+ return lqp.Break(
221
+ name = rel_id,
222
+ body = mk_abstraction([], body),
223
+ attrs = [],
224
+ meta = None,
225
+ )
226
+
227
+ def _translate_empty_instruction(ctx: TranslationCtx, rule: ir.Logical) -> lqp.Instruction:
228
+ assert is_empty_instruction(rule)
229
+ updates = collect_by_type(ir.Update, rule)
230
+ assert len(updates) == 1
231
+ update = updates[0]
232
+ bindings = _effect_bindings(update)
233
+
234
+ # We need to make sure that variable names have a leading underscore
235
+ normalized_bindings:Sequence[ir.Var] = []
236
+ for v in bindings:
237
+ assert isinstance(v, ir.Var)
238
+ if not v.name.startswith("_"):
239
+ v = ir.Var(v.type, "_" + v.name)
240
+ normalized_bindings.append(v)
241
+
242
+ projection, eqs = _translate_bindings(ctx, normalized_bindings)
243
+ assert len(eqs) == 0
244
+ rel_id = get_relation_id(ctx, update.relation, projection)
245
+ abstraction = mk_abstraction(projection, mk_or([])) # empty body = false
246
+ return lqp.Assign(name = rel_id, body = abstraction, attrs = [], meta = None)
247
+
248
+ def _translate_instruction(ctx: TranslationCtx, rule: ir.Logical) -> list[lqp.Instruction]:
249
+ effects = collect_by_type((ir.Update, ir.Output), rule)
147
250
 
148
- # TODO: should this ever actually come in as input?
149
251
  if len(effects) == 0:
150
252
  return []
151
253
 
152
- assert len(ranks) == 0 or len(aggregates) == 0, "rules cannot have both aggregates and ranks"
254
+ conjuncts = _translate_to_formula(ctx, rule)
255
+ res = []
256
+ for effect in effects:
257
+ assert isinstance(effect, ir.Update), f"Got an effect of type {type(effect)} in a loop, which is invalid."
153
258
 
154
- conjuncts = []
155
- for task in rule.body:
156
- if isinstance(task, (ir.Output, ir.Update)):
157
- continue
158
- conjuncts.append(_translate_to_formula(ctx, task))
259
+ bindings = _effect_bindings(effect)
260
+ projection, eqs = _translate_bindings(ctx, bindings)
261
+
262
+ eqs.append(conjuncts)
263
+ new_body = mk_and(eqs)
264
+
265
+ rel_id = get_relation_id(ctx, effect.relation, projection)
266
+ abstraction = mk_abstraction(projection, new_body)
267
+
268
+ upsert = get_upsert_annotation(effect)
269
+ monoid = get_monoid_annotation(effect)
270
+ monus = get_monus_annotation(effect)
271
+
272
+ if has_assign_annotation(effect):
273
+ res.append(lqp.Assign(name = rel_id, body = abstraction, attrs = [], meta = None))
274
+ elif upsert is not None:
275
+ res.append(lqp.Upsert(value_arity=get_arity(upsert), name = rel_id, body = abstraction, attrs = [], meta = None))
276
+ elif monoid is not None:
277
+ res.append(lqp.MonoidDef(
278
+ value_arity=get_arity(monoid),
279
+ monoid=construct_monoid(monoid),
280
+ name = rel_id,
281
+ body = abstraction,
282
+ attrs = [],
283
+ meta = None
284
+ ))
285
+ elif monus is not None:
286
+ res.append(lqp.MonusDef(
287
+ value_arity=get_arity(monus),
288
+ monoid=construct_monoid(monus),
289
+ name = rel_id,
290
+ body = abstraction,
291
+ attrs = [],
292
+ meta = None
293
+ ))
294
+
295
+ return res
159
296
 
160
- # Aggregates reduce over the body
161
- if aggregates or ranks:
162
- aggr_body = mk_and(conjuncts)
163
- conjuncts = []
164
- for aggr in aggregates:
165
- conjuncts.append(_translate_aggregate(ctx, aggr, aggr_body))
166
- for rank in ranks:
167
- conjuncts.append(_translate_rank(ctx, rank, aggr_body))
297
+ def _translate_to_standard_decl(ctx: TranslationCtx, rule: ir.Logical) -> list[lqp.Declaration]:
298
+ effects = collect_by_type((ir.Output, ir.Update), rule)
168
299
 
169
- return [_translate_effect(ctx, effect, mk_and(conjuncts)) for effect in effects]
300
+ # TODO: should this ever actually come in as input?
301
+ if len(effects) == 0:
302
+ return []
303
+
304
+ conjuncts = _translate_to_formula(ctx, rule)
305
+ return [_translate_effect(ctx, effect, conjuncts) for effect in effects]
170
306
 
171
307
  def _translate_annotations(annotations: FrozenOrderedSet[ir.Annotation]) -> list[lqp.Attribute]:
172
308
  attributes = []
173
309
  for annotation in annotations:
174
310
 
175
- if annotation.relation.name in lqp_builtins.annotations_to_emit:
311
+ if annotation.relation.name in supported_lqp_annotations:
176
312
  if any(not isinstance(a, ir.Literal) for a in annotation.args):
177
313
  warn("LQP currently ignores annotation parameters with non-literal values")
178
314
  continue
@@ -191,14 +327,11 @@ def _translate_annotations(annotations: FrozenOrderedSet[ir.Annotation]) -> list
191
327
  def _translate_effect(ctx: TranslationCtx, effect: Union[ir.Output, ir.Update], body: lqp.Formula) -> lqp.Declaration:
192
328
  bindings = _effect_bindings(effect)
193
329
 
194
- def _is_export(e):
195
- return isinstance(e, ir.Output) and builtins.export_annotation in e.annotations
196
-
197
330
  if isinstance(effect, ir.Output):
198
331
  projection, eqs, suffix = _translate_output_bindings(ctx, bindings)
199
332
  meta_id = effect.id
200
333
 
201
- if _is_export(effect):
334
+ if helpers.is_export(effect):
202
335
  def_name = "export_relation" + suffix
203
336
  else:
204
337
  def_name = "output" + suffix
@@ -214,14 +347,23 @@ def _translate_effect(ctx: TranslationCtx, effect: Union[ir.Output, ir.Update],
214
347
  new_body = mk_and(eqs)
215
348
 
216
349
  # Context bookkeeping for exports and outputs
217
- if _is_export(effect):
350
+ if helpers.is_export(effect):
218
351
  # The row id is the first n-1 elements, and the actual data is the last element. Its
219
352
  # type is stored in the first element of the tuple.
220
353
  col_type = projection[-1][1]
221
354
  _col_num_match = re.search(r"export_relation_col([0-9]+)", def_name)
222
355
  assert _col_num_match, f"Could not find column number in suffix: {def_name}"
223
356
  col_num = int(_col_num_match.group(1))
224
- ctx.export_ids.append((rel_id, col_num, col_type))
357
+ col_name = f"col{col_num}"
358
+ if isinstance(effect, ir.Output) and len(effect.aliases) > 0:
359
+ aliases_list = list(effect.aliases)
360
+ col_name = aliases_list[-1][0]
361
+ ctx.export_descriptors.append(ExportDescriptor(
362
+ relation_id=rel_id,
363
+ column_name=col_name,
364
+ column_number=col_num,
365
+ column_type=col_type,
366
+ ))
225
367
  elif isinstance(effect, ir.Output):
226
368
  ctx.output_ids.append((rel_id, def_name))
227
369
 
@@ -544,7 +686,36 @@ def _translate_aggregate(ctx: TranslationCtx, aggr: ir.Aggregate, body: lqp.Form
544
686
 
545
687
  def _translate_to_formula(ctx: TranslationCtx, task: ir.Task) -> lqp.Formula:
546
688
  if isinstance(task, ir.Logical):
547
- conjuncts = [_translate_to_formula(ctx, child) for child in task.body]
689
+ # For aggregates and ranks, the expected format is:
690
+ #
691
+ # Logical
692
+ # body_task1
693
+ # body_task2
694
+ # ...
695
+ # aggregate/rank task
696
+ #
697
+ # If we see that the Logical is in this format, it should be translated as an
698
+ # aggregate/rank node.
699
+ groups = group_tasks(task.body, {
700
+ "aggregates": ir.Aggregate,
701
+ "ranks": ir.Rank,
702
+ })
703
+
704
+ aggregates = groups.get("aggregates", OrderedSet[ir.Task]())
705
+ ranks = groups.get("ranks", OrderedSet[ir.Task]())
706
+
707
+ if aggregates or ranks:
708
+ conjuncts = []
709
+ body = mk_and([_translate_to_formula(ctx, t) for t in task.body])
710
+ for aggr in aggregates:
711
+ assert(isinstance(aggr, ir.Aggregate))
712
+ conjuncts.append(_translate_aggregate(ctx, aggr, body))
713
+ for rank in ranks:
714
+ assert(isinstance(rank, ir.Rank))
715
+ conjuncts.append(_translate_rank(ctx, rank, body))
716
+ else:
717
+ # If there are no aggregates or ranks, translate as a normal conjunction
718
+ conjuncts = [_translate_to_formula(ctx, child) for child in task.body]
548
719
  return mk_and(conjuncts)
549
720
  elif isinstance(task, ir.Lookup):
550
721
  return _translate_to_atom(ctx, task)
@@ -564,14 +735,13 @@ def _translate_to_formula(ctx: TranslationCtx, task: ir.Task) -> lqp.Formula:
564
735
 
565
736
  return mk_primitive("rel_primitive_hash_tuple_uint128", [v for v, _ in terms])
566
737
  elif isinstance(task, ir.Union):
567
- # TODO: handle hoisted vars if needed
568
738
  disjs = [_translate_to_formula(ctx, child) for child in task.tasks]
569
739
  return mk_or(disjs)
570
- elif isinstance(task, (ir.Aggregate, ir.Output, ir.Update)):
740
+ elif isinstance(task, (ir.Output, ir.Update)):
571
741
  # Nothing to do here, handled in _translate_to_decls
572
742
  return mk_and([])
573
- elif isinstance(task, ir.Rank):
574
- # Nothing to do here, handled in _translate_to_decls
743
+ elif isinstance(task, (ir.Aggregate, ir.Rank)):
744
+ # Nothing to do here, handled at the Logical level
575
745
  return mk_and([])
576
746
  else:
577
747
  raise NotImplementedError(f"Unknown task type (formula): {type(task)}")
@@ -666,7 +836,7 @@ def get_output_id(ctx: TranslationCtx, orig_name: str, metamodel_id: int) -> lqp
666
836
  unique_name = ctx.output_names.get_name_by_id(metamodel_id, orig_name)
667
837
  return utils.gen_rel_id(ctx, unique_name)
668
838
 
669
- def _translate_bindings(ctx: TranslationCtx, bindings: list[ir.Value]) -> Tuple[list[Tuple[lqp.Var, lqp.Type]], list[lqp.Formula]]:
839
+ def _translate_bindings(ctx: TranslationCtx, bindings: Sequence[ir.Value]) -> Tuple[list[Tuple[lqp.Var, lqp.Type]], list[lqp.Formula]]:
670
840
  lqp_vars = []
671
841
  conjuncts = []
672
842
  for binding in bindings: