relationalai 0.13.2__py3-none-any.whl → 0.13.3__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 (46) hide show
  1. relationalai/clients/client.py +3 -4
  2. relationalai/clients/exec_txn_poller.py +62 -31
  3. relationalai/clients/resources/snowflake/direct_access_resources.py +6 -5
  4. relationalai/clients/resources/snowflake/snowflake.py +47 -51
  5. relationalai/semantics/lqp/algorithms.py +173 -0
  6. relationalai/semantics/lqp/builtins.py +199 -2
  7. relationalai/semantics/lqp/executor.py +65 -36
  8. relationalai/semantics/lqp/ir.py +28 -2
  9. relationalai/semantics/lqp/model2lqp.py +215 -45
  10. relationalai/semantics/lqp/passes.py +13 -658
  11. relationalai/semantics/lqp/rewrite/__init__.py +12 -0
  12. relationalai/semantics/lqp/rewrite/algorithm.py +385 -0
  13. relationalai/semantics/lqp/rewrite/constants_to_vars.py +70 -0
  14. relationalai/semantics/lqp/rewrite/deduplicate_vars.py +104 -0
  15. relationalai/semantics/lqp/rewrite/eliminate_data.py +108 -0
  16. relationalai/semantics/lqp/rewrite/period_math.py +77 -0
  17. relationalai/semantics/lqp/rewrite/quantify_vars.py +65 -31
  18. relationalai/semantics/lqp/rewrite/unify_definitions.py +317 -0
  19. relationalai/semantics/lqp/utils.py +11 -1
  20. relationalai/semantics/lqp/validators.py +14 -1
  21. relationalai/semantics/metamodel/builtins.py +2 -1
  22. relationalai/semantics/metamodel/compiler.py +2 -1
  23. relationalai/semantics/metamodel/dependency.py +12 -3
  24. relationalai/semantics/metamodel/executor.py +11 -1
  25. relationalai/semantics/metamodel/factory.py +2 -2
  26. relationalai/semantics/metamodel/helpers.py +7 -0
  27. relationalai/semantics/metamodel/ir.py +3 -2
  28. relationalai/semantics/metamodel/rewrite/dnf_union_splitter.py +30 -20
  29. relationalai/semantics/metamodel/rewrite/flatten.py +50 -13
  30. relationalai/semantics/metamodel/rewrite/format_outputs.py +9 -3
  31. relationalai/semantics/metamodel/typer/checker.py +6 -4
  32. relationalai/semantics/metamodel/typer/typer.py +2 -5
  33. relationalai/semantics/metamodel/visitor.py +4 -3
  34. relationalai/semantics/reasoners/optimization/solvers_dev.py +1 -1
  35. relationalai/semantics/reasoners/optimization/solvers_pb.py +3 -4
  36. relationalai/semantics/rel/compiler.py +2 -1
  37. relationalai/semantics/rel/executor.py +3 -2
  38. relationalai/semantics/tests/lqp/__init__.py +0 -0
  39. relationalai/semantics/tests/lqp/algorithms.py +345 -0
  40. relationalai/tools/cli_controls.py +216 -67
  41. relationalai/util/format.py +5 -2
  42. {relationalai-0.13.2.dist-info → relationalai-0.13.3.dist-info}/METADATA +1 -1
  43. {relationalai-0.13.2.dist-info → relationalai-0.13.3.dist-info}/RECORD +46 -37
  44. {relationalai-0.13.2.dist-info → relationalai-0.13.3.dist-info}/WHEEL +0 -0
  45. {relationalai-0.13.2.dist-info → relationalai-0.13.3.dist-info}/entry_points.txt +0 -0
  46. {relationalai-0.13.2.dist-info → relationalai-0.13.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,5 @@
1
- from relationalai.semantics.metamodel import factory as f
1
+ from typing import TypeGuard
2
+ from relationalai.semantics.metamodel import factory as f, ir, types
2
3
  from relationalai.semantics.metamodel.util import FrozenOrderedSet
3
4
  from relationalai.semantics.metamodel import builtins
4
5
 
@@ -8,9 +9,205 @@ adhoc = f.relation("adhoc", [])
8
9
  adhoc_annotation = f.annotation(adhoc, [])
9
10
 
10
11
  # We only want to emit attributes for a known set of annotations.
11
- annotations_to_emit = FrozenOrderedSet([
12
+ supported_lqp_annotations = FrozenOrderedSet([
12
13
  adhoc.name,
13
14
  builtins.function.name,
14
15
  builtins.track.name,
15
16
  builtins.recursion_config.name,
16
17
  ])
18
+
19
+ # [LoopyIR] Annotations used to mark metamodel IR elements as Loopy constructs.
20
+ # 1. Programming structures:
21
+ # * @script marks Sequence blocks `begin ... end`
22
+ # * @algorithm additionally marks the top-level script
23
+ # * @while marks Loop as a `while(true) {...}`; its sole Task is a @script @while Sequence
24
+ # 2. Base instructions (Update's with derive Effects)
25
+ # * @global marks instructions that write to a global relation (only used in top-level script)
26
+ # * @empty marks instructions that initialize relations to an empty relation
27
+ # * @assign marks instructions that are standard assignments
28
+ # * @upsert marks instructions that perform in-place upserts
29
+ # * @monoid marks instructions that perform in-place monoid updates
30
+ # * @monus marks instructions that perform in-place monus updates
31
+
32
+ # These tasks require dedicated handling and currently are only supported in LQP.
33
+
34
+ # Here we only provide basic inspection functions. Functions for creating these annotations
35
+ # and more complex analysis are in the module relationalai.semantics.lqp.algorithms
36
+
37
+ # Algorithm: for top-level script of an algorithm
38
+ _algorithm_anno_name = "algorithm"
39
+ algorithm = f.relation(_algorithm_anno_name, [])
40
+
41
+ def algorithm_annotation():
42
+ return f.annotation(algorithm, [])
43
+
44
+ def has_algorithm_annotation(node: ir.Node) -> bool:
45
+ if not hasattr(node, "annotations"):
46
+ return False
47
+ annotations = getattr(node, "annotations", [])
48
+ for anno in annotations:
49
+ if anno.relation.name == _algorithm_anno_name:
50
+ return True
51
+ return False
52
+
53
+ # Script: for Sequence blocks (algorithm or while loop)
54
+ _script_anno_name = "script"
55
+ script = f.relation(_script_anno_name, [])
56
+
57
+ def script_annotation():
58
+ return f.annotation(script, [])
59
+
60
+ def has_script_annotation(node: ir.Node) -> bool:
61
+ if not hasattr(node, "annotations"):
62
+ return False
63
+ annotations = getattr(node, "annotations", [])
64
+ for anno in annotations:
65
+ if anno.relation.name == _script_anno_name:
66
+ return True
67
+ return False
68
+
69
+ # While: for a while Loop or its script body (Sequence)
70
+ _while_anno_name = "while"
71
+ while_ = f.relation(_while_anno_name, [])
72
+
73
+ def while_annotation():
74
+ return f.annotation(while_, [])
75
+
76
+ def has_while_annotation(node: ir.Node) -> bool:
77
+ if not hasattr(node, "annotations"):
78
+ return False
79
+ annotations = getattr(node, "annotations", [])
80
+ for anno in annotations:
81
+ if anno.relation.name == _while_anno_name:
82
+ return True
83
+ return False
84
+
85
+ # Global: marks instructions that write to relation that is the result of an algorithm
86
+ _global_anno_name = "global"
87
+ global_ = f.relation(_global_anno_name, [])
88
+
89
+ def global_annotation():
90
+ return f.annotation(global_, [])
91
+
92
+ def has_global_annotation(node: ir.Node) -> TypeGuard[ir.Update]:
93
+ if not hasattr(node, "annotations"):
94
+ return False
95
+ annotations = getattr(node, "annotations", [])
96
+ for anno in annotations:
97
+ if anno.relation.name == _global_anno_name:
98
+ return True
99
+ return False
100
+
101
+ # Empty: Initializes a relation to an empty relation
102
+ _empty_anno_name = "empty"
103
+ empty = f.relation(_empty_anno_name, [])
104
+
105
+ def empty_annotation():
106
+ return f.annotation(empty, [])
107
+
108
+ def has_empty_annotation(node: ir.Node) -> TypeGuard[ir.Update]:
109
+ if not hasattr(node, "annotations"):
110
+ return False
111
+ annotations = getattr(node, "annotations", [])
112
+ for anno in annotations:
113
+ if anno.relation.name == _empty_anno_name:
114
+ return True
115
+ return False
116
+
117
+ # Assign: overwrites the target relation
118
+ _assign_anno_name = "assign"
119
+ assign = f.relation(_assign_anno_name, [])
120
+
121
+ def assign_annotation():
122
+ return f.annotation(assign, [])
123
+
124
+ def has_assign_annotation(node: ir.Node) -> TypeGuard[ir.Update]:
125
+ if not hasattr(node, "annotations"):
126
+ return False
127
+ annotations = getattr(node, "annotations", [])
128
+ for anno in annotations:
129
+ if anno.relation.name == _assign_anno_name:
130
+ return True
131
+ return False
132
+
133
+ # Upsert: In-place update of relation
134
+ _upsert_anno_name = "upsert"
135
+ upsert = f.relation(_upsert_anno_name, [])
136
+
137
+ def upsert_annotation(arity: int):
138
+ return f.annotation(upsert, [f.literal(arity, type=types.Int64)])
139
+
140
+ def has_upsert_annotation(node: ir.Node) -> TypeGuard[ir.Update]:
141
+ if not hasattr(node, "annotations"):
142
+ return False
143
+ annotations = getattr(node, "annotations", [])
144
+ for anno in annotations:
145
+ if anno.relation.name == _upsert_anno_name:
146
+ return True
147
+ return False
148
+
149
+ def get_upsert_annotation(i: ir.Update):
150
+ for anno in i.annotations:
151
+ if anno.relation.name == _upsert_anno_name:
152
+ return anno
153
+ return None
154
+
155
+ # Monoid: In-place update of relation by another by a monoid operation (e.g. Integer addition)
156
+ _monoid_anno_name = "monoid"
157
+ monoid = f.relation(_monoid_anno_name, [])
158
+
159
+ def monoid_annotation(monoid_type: ir.ScalarType, monoid_op: str, arity: int):
160
+ return f.annotation(monoid, [f.literal(arity, type=types.Int64), monoid_type, f.literal(monoid_op, type=types.String)])
161
+
162
+ def has_monoid_annotation(node: ir.Node) -> TypeGuard[ir.Update]:
163
+ if not hasattr(node, "annotations"):
164
+ return False
165
+ annotations = getattr(node, "annotations", [])
166
+ for anno in annotations:
167
+ if anno.relation.name == _monoid_anno_name:
168
+ return True
169
+ return False
170
+
171
+ def get_monoid_annotation(i: ir.Update):
172
+ for anno in i.annotations:
173
+ if anno.relation.name == _monoid_anno_name:
174
+ return anno
175
+ return None
176
+
177
+ # Monus: In-place update of relation by another by "subtraction" operation, if it exists (e.g. Integer subtraction)
178
+ _monus_anno_name = "monus"
179
+ monus = f.relation(_monus_anno_name, [])
180
+
181
+ def monus_annotation(monoid_type: ir.ScalarType, monoid_op: str, arity: int):
182
+ return f.annotation(monus, [f.literal(arity, type=types.Int64), monoid_type, f.literal(monoid_op, type=types.String)])
183
+
184
+ def has_monus_annotation(node: ir.Node) -> TypeGuard[ir.Update]:
185
+ if not hasattr(node, "annotations"):
186
+ return False
187
+ annotations = getattr(node, "annotations", [])
188
+ for anno in annotations:
189
+ if anno.relation.name == _monus_anno_name:
190
+ return True
191
+ return False
192
+
193
+ def get_monus_annotation(i: ir.Update):
194
+ for anno in i.annotations:
195
+ if anno.relation.name == _monus_anno_name:
196
+ return anno
197
+ return None
198
+
199
+ # Get arity from annotation (for @upsert, @monoid, and @monus)
200
+ def get_arity(i: ir.Annotation):
201
+ for arg in i.args:
202
+ if isinstance(arg, ir.Literal) and (arg.type == types.Int64 or arg.type == types.Int128 or arg.type == types.Number):
203
+ return arg.value
204
+ assert False, "Failed to get arity"
205
+
206
+ # All Loopy instructions
207
+ loopy_instructions = [
208
+ empty,
209
+ assign,
210
+ upsert,
211
+ monoid,
212
+ monus
213
+ ]
@@ -12,6 +12,9 @@ from relationalai import debugging
12
12
  from relationalai.errors import NonDefaultLQPSemanticsVersionWarning
13
13
  from relationalai.semantics.lqp import result_helpers
14
14
  from relationalai.semantics.metamodel import ir, factory as f, executor as e
15
+
16
+ if TYPE_CHECKING:
17
+ from relationalai.semantics.internal.internal import Model as InternalModel
15
18
  from relationalai.semantics.lqp.compiler import Compiler
16
19
  from relationalai.semantics.lqp.intrinsics import mk_intrinsic_datetime_now
17
20
  from relationalai.semantics.lqp.constructors import mk_transaction
@@ -280,10 +283,11 @@ class LQPExecutor(e.Executor):
280
283
  fields.append(f"NULL as \"{name}\"")
281
284
  continue
282
285
 
283
- colname = f"col{ix:03}"
286
+ # Get the actual physical column name from column_fields
287
+ colname = column_fields[ix][0]
284
288
  ix += 1
285
289
 
286
- if colname in sample_keys:
290
+ if colname.lower() in sample_keys:
287
291
  # Actual column exists in sample
288
292
  fields.append(f"{colname} as \"{name}\"")
289
293
  else:
@@ -402,38 +406,15 @@ class LQPExecutor(e.Executor):
402
406
  txn_proto = convert_transaction(txn)
403
407
  return final_model, export_info, txn_proto
404
408
 
405
- # TODO (azreika): This should probably be split up into exporting and other processing. There are quite a lot of arguments here...
406
- def _process_results(self, task: ir.Task, final_model: ir.Model, raw_results: TransactionAsyncResponse, export_info: Optional[tuple], export_to: Optional[Table], update: bool) -> DataFrame:
407
- cols, extra_cols = self._compute_cols(task, final_model)
408
-
409
- df, errs = result_helpers.format_results(raw_results, cols)
410
- self.report_errors(errs)
411
-
412
- # Rename columns if wide outputs is enabled
413
- if self.wide_outputs and len(cols) - len(extra_cols) == len(df.columns):
414
- df.columns = cols[: len(df.columns)]
415
-
416
- # Process exports
417
- if export_to and not self.dry_run:
418
- assert cols, "No columns found in the output"
419
- assert isinstance(raw_results, TransactionAsyncResponse) and raw_results.transaction, "Invalid transaction result"
420
-
421
- result_cols = export_to._col_names
422
-
423
- if result_cols is not None:
424
- assert all(col in result_cols or col in extra_cols for col in cols)
425
- else:
426
- result_cols = [col for col in cols if col not in extra_cols]
427
- assert result_cols
428
-
429
- assert export_info, "Export info should be populated if we are exporting results"
430
- self._export(raw_results.transaction['id'], export_info, export_to, cols, result_cols, update)
431
-
432
- return self._postprocess_df(self.config, df, extra_cols)
433
-
434
- def execute(self, model: ir.Model, task: ir.Task, format: Literal["pandas", "snowpark"] = "pandas",
435
- export_to: Optional[Table] = None,
436
- update: bool = False, meta: dict[str, Any] | None = None) -> DataFrame:
409
+ def execute(
410
+ self,
411
+ model: ir.Model,
412
+ task: ir.Task,
413
+ format: Literal["pandas", "snowpark", "csv"] = "pandas",
414
+ export_to: Optional[Table] = None,
415
+ update: bool = False,
416
+ meta: dict[str, Any] | None = None,
417
+ ) -> DataFrame:
437
418
  self.prepare_data()
438
419
  previous_model = self._last_model
439
420
 
@@ -442,7 +423,7 @@ class LQPExecutor(e.Executor):
442
423
  if self.dry_run:
443
424
  return DataFrame()
444
425
 
445
- if format != "pandas":
426
+ if format == "snowpark":
446
427
  raise ValueError(f"Unsupported format: {format}")
447
428
 
448
429
  # Format meta as headers
@@ -462,8 +443,56 @@ class LQPExecutor(e.Executor):
462
443
  assert isinstance(raw_results, TransactionAsyncResponse)
463
444
 
464
445
  try:
465
- return self._process_results(task, final_model, raw_results, export_info, export_to, update)
446
+ cols, extra_cols = self._compute_cols(task, final_model)
447
+ df, errs = result_helpers.format_results(raw_results, cols)
448
+ self.report_errors(errs)
449
+
450
+ # Rename columns if wide outputs is enabled
451
+ if self.wide_outputs and len(cols) - len(extra_cols) == len(df.columns):
452
+ df.columns = cols[: len(df.columns)]
453
+
454
+ if export_to:
455
+ assert cols, "No columns found in the output"
456
+ assert raw_results.transaction, "Invalid transaction result"
457
+ assert export_info, "Export info should be populated if we are exporting results"
458
+ result_cols = export_to._col_names
459
+ if result_cols is not None:
460
+ assert all(col in result_cols or col in extra_cols for col in cols)
461
+ else:
462
+ result_cols = [col for col in cols if col not in extra_cols]
463
+ assert result_cols
464
+ self._export(raw_results.transaction['id'], export_info, export_to, cols, result_cols, update)
465
+
466
+ if format == "csv":
467
+ if export_info is not None and isinstance(export_info, tuple) and isinstance(export_info[0], str):
468
+ return DataFrame([export_info[0]], columns=["path"])
469
+ else:
470
+ raise ValueError("The CSV export was not successful!")
471
+
472
+ return self._postprocess_df(self.config, df, extra_cols)
473
+
466
474
  except Exception as e:
467
475
  # If processing the results failed, revert to the previous model.
468
476
  self._last_model = previous_model
469
477
  raise e
478
+
479
+ def export_to_csv(self, model: "InternalModel", query) -> str:
480
+ ### Exports the result of the given query fragment to a CSV file in
481
+ ### the Snowflake stage area and returns the path to the CSV file.
482
+
483
+ from relationalai.semantics.internal.internal import Fragment, with_source
484
+ from relationalai.environments import runtime_env
485
+
486
+ if not query._select:
487
+ raise ValueError("Cannot export empty selection to CSV")
488
+
489
+ clone = Fragment(parent=query)
490
+ clone._is_export = True
491
+ clone._source = runtime_env.get_source_pos()
492
+ ir_model = model._to_ir()
493
+ with debugging.span("query", dsl=str(clone), **with_source(clone), meta=clone._meta):
494
+ query_task = model._compiler.fragment(clone)
495
+ csv_info = self.execute(ir_model, query_task, format="csv", meta=clone._meta)
496
+ path = csv_info.at[0, "path"]
497
+ assert isinstance(path, str)
498
+ return path
@@ -6,7 +6,6 @@ __all__ = [
6
6
  "Declaration",
7
7
  "FunctionalDependency",
8
8
  "Def",
9
- "Loop",
10
9
  "Abstraction",
11
10
  "Formula",
12
11
  "Exists",
@@ -55,6 +54,20 @@ __all__ = [
55
54
  "convert_transaction",
56
55
  "validate_lqp",
57
56
  "construct_configure",
57
+ "Algorithm",
58
+ "Script",
59
+ "Construct",
60
+ "Loop",
61
+ "Instruction",
62
+ "Assign",
63
+ "Break",
64
+ "Upsert",
65
+ "MonoidDef",
66
+ "MonusDef",
67
+ "OrMonoid",
68
+ "MinMonoid",
69
+ "MaxMonoid",
70
+ "SumMonoid",
58
71
  ]
59
72
 
60
73
  from lqp.ir import (
@@ -63,7 +76,6 @@ from lqp.ir import (
63
76
  Declaration,
64
77
  FunctionalDependency,
65
78
  Def,
66
- Loop,
67
79
  Abstraction,
68
80
  Formula,
69
81
  Exists,
@@ -108,6 +120,20 @@ from lqp.ir import (
108
120
  Configure,
109
121
  IVMConfig,
110
122
  MaintenanceLevel,
123
+ Algorithm,
124
+ Script,
125
+ Construct,
126
+ Loop,
127
+ Instruction,
128
+ Assign,
129
+ Break,
130
+ Upsert,
131
+ MonoidDef,
132
+ MonusDef,
133
+ OrMonoid,
134
+ MinMonoid,
135
+ MaxMonoid,
136
+ SumMonoid,
111
137
  )
112
138
 
113
139
  from lqp.emit import (