relationalai 0.12.4__py3-none-any.whl → 0.12.7__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 (116) hide show
  1. relationalai/__init__.py +4 -0
  2. relationalai/clients/snowflake.py +34 -13
  3. relationalai/early_access/lqp/constructors/__init__.py +2 -2
  4. relationalai/early_access/metamodel/rewrite/__init__.py +2 -2
  5. relationalai/{semantics/reasoners/graph → experimental}/paths/README.md +2 -2
  6. relationalai/experimental/paths/__init__.py +14 -309
  7. relationalai/{semantics/reasoners/graph → experimental}/paths/examples/basic_example.py +2 -2
  8. relationalai/{semantics/reasoners/graph → experimental}/paths/examples/paths_benchmark.py +2 -2
  9. relationalai/{semantics/reasoners/graph → experimental}/paths/examples/paths_example.py +2 -2
  10. relationalai/{semantics/reasoners/graph → experimental}/paths/examples/pattern_to_automaton.py +1 -1
  11. relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/one_sided_ball_repetition.py +1 -1
  12. relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/single.py +3 -3
  13. relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/two_sided_balls_repetition.py +1 -1
  14. relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/two_sided_balls_upto.py +2 -2
  15. relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/usp-old.py +3 -3
  16. relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/usp-tuple.py +3 -3
  17. relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/usp.py +3 -3
  18. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_sp_max_length.py +2 -2
  19. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_sp_multiple.py +2 -2
  20. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_sp_single.py +2 -2
  21. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_walks_multiple.py +2 -2
  22. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_walks_single.py +2 -2
  23. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_one_sided_ball_repetition_multiple.py +2 -2
  24. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_one_sided_ball_repetition_single.py +2 -2
  25. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_one_sided_ball_upto_multiple.py +2 -2
  26. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_one_sided_ball_upto_single.py +2 -2
  27. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_single_paths.py +2 -2
  28. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_single_walks.py +2 -2
  29. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_single_walks_undirected.py +2 -2
  30. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_two_sided_balls_repetition_multiple.py +2 -2
  31. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_two_sided_balls_repetition_single.py +2 -2
  32. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_two_sided_balls_upto_multiple.py +2 -2
  33. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_two_sided_balls_upto_single.py +2 -2
  34. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_usp_nsp_multiple.py +2 -2
  35. relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_usp_nsp_single.py +2 -2
  36. relationalai/semantics/__init__.py +4 -0
  37. relationalai/semantics/internal/annotations.py +1 -0
  38. relationalai/semantics/internal/internal.py +3 -4
  39. relationalai/semantics/internal/snowflake.py +14 -1
  40. relationalai/semantics/lqp/builtins.py +1 -0
  41. relationalai/semantics/lqp/constructors.py +0 -5
  42. relationalai/semantics/lqp/executor.py +34 -10
  43. relationalai/semantics/lqp/intrinsics.py +2 -2
  44. relationalai/semantics/lqp/model2lqp.py +105 -9
  45. relationalai/semantics/lqp/passes.py +27 -8
  46. relationalai/semantics/lqp/primitives.py +18 -15
  47. relationalai/semantics/lqp/rewrite/__init__.py +2 -2
  48. relationalai/semantics/lqp/rewrite/{fd_constraints.py → function_annotations.py} +4 -4
  49. relationalai/semantics/lqp/utils.py +17 -13
  50. relationalai/semantics/metamodel/builtins.py +50 -1
  51. relationalai/semantics/metamodel/typer/typer.py +3 -0
  52. relationalai/semantics/reasoners/__init__.py +4 -0
  53. relationalai/semantics/reasoners/experimental/__init__.py +7 -0
  54. relationalai/semantics/reasoners/graph/core.py +1154 -122
  55. relationalai/semantics/rel/builtins.py +3 -1
  56. relationalai/semantics/rel/compiler.py +2 -2
  57. relationalai/semantics/rel/executor.py +30 -8
  58. relationalai/semantics/rel/rel_utils.py +5 -0
  59. relationalai/semantics/snowflake/__init__.py +2 -2
  60. relationalai/semantics/sql/compiler.py +6 -0
  61. relationalai/semantics/sql/executor/snowflake.py +6 -2
  62. relationalai/semantics/tests/test_snapshot_abstract.py +5 -4
  63. {relationalai-0.12.4.dist-info → relationalai-0.12.7.dist-info}/METADATA +2 -2
  64. {relationalai-0.12.4.dist-info → relationalai-0.12.7.dist-info}/RECORD +99 -115
  65. {relationalai-0.12.4.dist-info → relationalai-0.12.7.dist-info}/WHEEL +1 -1
  66. relationalai/early_access/paths/__init__.py +0 -22
  67. relationalai/early_access/paths/api/__init__.py +0 -12
  68. relationalai/early_access/paths/benchmarks/__init__.py +0 -13
  69. relationalai/early_access/paths/graph/__init__.py +0 -12
  70. relationalai/early_access/paths/path_algorithms/find_paths/__init__.py +0 -12
  71. relationalai/early_access/paths/path_algorithms/one_sided_ball_repetition/__init__.py +0 -12
  72. relationalai/early_access/paths/path_algorithms/one_sided_ball_upto/__init__.py +0 -12
  73. relationalai/early_access/paths/path_algorithms/single/__init__.py +0 -12
  74. relationalai/early_access/paths/path_algorithms/two_sided_balls_repetition/__init__.py +0 -12
  75. relationalai/early_access/paths/path_algorithms/two_sided_balls_upto/__init__.py +0 -12
  76. relationalai/early_access/paths/path_algorithms/usp/__init__.py +0 -12
  77. relationalai/early_access/paths/rpq/__init__.py +0 -13
  78. relationalai/early_access/paths/utilities/iterators/__init__.py +0 -12
  79. relationalai/experimental/paths/pathfinder.rel +0 -2560
  80. relationalai/semantics/reasoners/graph/paths/__init__.py +0 -16
  81. relationalai/semantics/reasoners/graph/paths/path_algorithms/__init__.py +0 -3
  82. relationalai/semantics/reasoners/graph/paths/utilities/__init__.py +0 -3
  83. /relationalai/{semantics/reasoners/graph → experimental}/paths/api.py +0 -0
  84. /relationalai/{semantics/reasoners/graph → experimental}/paths/benchmarks/__init__.py +0 -0
  85. /relationalai/{semantics/reasoners/graph → experimental}/paths/benchmarks/grid_graph.py +0 -0
  86. /relationalai/{semantics/reasoners/graph → experimental}/paths/code_organization.md +0 -0
  87. /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/Movies.ipynb +0 -0
  88. /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/minimal_engine_warmup.py +0 -0
  89. /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movie_example.py +0 -0
  90. /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/actedin.csv +0 -0
  91. /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/directed.csv +0 -0
  92. /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/follows.csv +0 -0
  93. /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/movies.csv +0 -0
  94. /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/person.csv +0 -0
  95. /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/produced.csv +0 -0
  96. /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/ratings.csv +0 -0
  97. /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/wrote.csv +0 -0
  98. /relationalai/{semantics/reasoners/graph → experimental}/paths/find_paths_via_automaton.py +0 -0
  99. /relationalai/{semantics/reasoners/graph → experimental}/paths/graph.py +0 -0
  100. /relationalai/{early_access → experimental}/paths/path_algorithms/__init__.py +0 -0
  101. /relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/find_paths.py +0 -0
  102. /relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/one_sided_ball_upto.py +0 -0
  103. /relationalai/{semantics/reasoners/graph → experimental}/paths/product_graph.py +0 -0
  104. /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/__init__.py +0 -0
  105. /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/automaton.py +0 -0
  106. /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/diagnostics.py +0 -0
  107. /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/filter.py +0 -0
  108. /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/glushkov.py +0 -0
  109. /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/rpq.py +0 -0
  110. /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/transition.py +0 -0
  111. /relationalai/{early_access → experimental}/paths/utilities/__init__.py +0 -0
  112. /relationalai/{semantics/reasoners/graph → experimental}/paths/utilities/iterators.py +0 -0
  113. /relationalai/{semantics/reasoners/graph → experimental}/paths/utilities/prefix_sum.py +0 -0
  114. /relationalai/{semantics/reasoners/graph → experimental}/paths/utilities/utilities.py +0 -0
  115. {relationalai-0.12.4.dist-info → relationalai-0.12.7.dist-info}/entry_points.txt +0 -0
  116. {relationalai-0.12.4.dist-info → relationalai-0.12.7.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  from relationalai.semantics import Model, Integer, define, select
2
- from relationalai.semantics.reasoners.graph.paths.graph import Graph
3
- from relationalai.semantics.reasoners.graph.paths.path_algorithms.one_sided_ball_upto import ball_upto
2
+ from relationalai.experimental.paths.graph import Graph
3
+ from relationalai.experimental.paths.path_algorithms.one_sided_ball_upto import ball_upto
4
4
 
5
5
 
6
6
  # First test with grid graph and multiple source and target nodes
@@ -1,6 +1,6 @@
1
1
  from relationalai.semantics import Model, Integer, define, select
2
- from relationalai.semantics.reasoners.graph.paths.graph import Graph
3
- from relationalai.semantics.reasoners.graph.paths.path_algorithms.one_sided_ball_upto import ball_upto
2
+ from relationalai.experimental.paths.graph import Graph
3
+ from relationalai.experimental.paths.path_algorithms.one_sided_ball_upto import ball_upto
4
4
 
5
5
 
6
6
  # First test with grid graph
@@ -1,6 +1,6 @@
1
1
  from relationalai.semantics import Model, Integer, define, select
2
- from relationalai.semantics.reasoners.graph.paths.graph import Graph
3
- from relationalai.semantics.reasoners.graph.paths.path_algorithms.single import single_shortest_path
2
+ from relationalai.experimental.paths.graph import Graph
3
+ from relationalai.experimental.paths.path_algorithms.single import single_shortest_path
4
4
 
5
5
 
6
6
  model = Model("test_single_paths", dry_run=False)
@@ -1,6 +1,6 @@
1
1
  from relationalai.semantics import Model, Integer, define, select
2
- from relationalai.semantics.reasoners.graph.paths.graph import Graph
3
- from relationalai.semantics.reasoners.graph.paths.path_algorithms.single import single_walk
2
+ from relationalai.experimental.paths.graph import Graph
3
+ from relationalai.experimental.paths.path_algorithms.single import single_walk
4
4
 
5
5
 
6
6
  model = Model("test_single_paths", dry_run=False)
@@ -1,6 +1,6 @@
1
1
  from relationalai.semantics import Model, Integer, define, select
2
- from relationalai.semantics.reasoners.graph.paths.graph import Graph
3
- from relationalai.semantics.reasoners.graph.paths.path_algorithms.single import single_walk
2
+ from relationalai.experimental.paths.graph import Graph
3
+ from relationalai.experimental.paths.path_algorithms.single import single_walk
4
4
 
5
5
 
6
6
  model = Model("test_single_paths", dry_run=False)
@@ -1,6 +1,6 @@
1
1
  from relationalai.semantics import Model, Integer, define, select
2
- from relationalai.semantics.reasoners.graph.paths.graph import Graph
3
- from relationalai.semantics.reasoners.graph.paths.path_algorithms.two_sided_balls_repetition import two_balls_repetition
2
+ from relationalai.experimental.paths.graph import Graph
3
+ from relationalai.experimental.paths.path_algorithms.two_sided_balls_repetition import two_balls_repetition
4
4
 
5
5
 
6
6
  # Test with diamond graph
@@ -1,6 +1,6 @@
1
1
  from relationalai.semantics import Model, Integer, define, select
2
- from relationalai.semantics.reasoners.graph.paths.graph import Graph
3
- from relationalai.semantics.reasoners.graph.paths.path_algorithms.two_sided_balls_repetition import two_balls_repetition
2
+ from relationalai.experimental.paths.graph import Graph
3
+ from relationalai.experimental.paths.path_algorithms.two_sided_balls_repetition import two_balls_repetition
4
4
 
5
5
 
6
6
  # Test with diamond graph
@@ -1,6 +1,6 @@
1
1
  from relationalai.semantics import Model, Integer, define, select
2
- from relationalai.semantics.reasoners.graph.paths.graph import Graph
3
- from relationalai.semantics.reasoners.graph.paths.path_algorithms.two_sided_balls_upto import two_balls_upto
2
+ from relationalai.experimental.paths.graph import Graph
3
+ from relationalai.experimental.paths.path_algorithms.two_sided_balls_upto import two_balls_upto
4
4
 
5
5
 
6
6
  # Test with grid graph
@@ -1,6 +1,6 @@
1
1
  from relationalai.semantics import Model, Integer, define, select
2
- from relationalai.semantics.reasoners.graph.paths.graph import Graph
3
- from relationalai.semantics.reasoners.graph.paths.path_algorithms.two_sided_balls_upto import two_balls_upto
2
+ from relationalai.experimental.paths.graph import Graph
3
+ from relationalai.experimental.paths.path_algorithms.two_sided_balls_upto import two_balls_upto
4
4
 
5
5
 
6
6
  # Test with grid graph
@@ -1,6 +1,6 @@
1
1
  from relationalai.semantics import Model, Integer, define, select
2
- from relationalai.semantics.reasoners.graph.paths.graph import Graph
3
- from relationalai.semantics.reasoners.graph.paths.path_algorithms.usp import compute_usp, compute_nsp
2
+ from relationalai.experimental.paths.graph import Graph
3
+ from relationalai.experimental.paths.path_algorithms.usp import compute_usp, compute_nsp
4
4
 
5
5
 
6
6
  # First test for usp with grid graph
@@ -1,6 +1,6 @@
1
1
  from relationalai.semantics import Model, Integer, define, select
2
- from relationalai.semantics.reasoners.graph.paths.graph import Graph
3
- from relationalai.semantics.reasoners.graph.paths.path_algorithms.usp import compute_usp, compute_nsp
2
+ from relationalai.experimental.paths.graph import Graph
3
+ from relationalai.experimental.paths.path_algorithms.usp import compute_usp, compute_nsp
4
4
 
5
5
 
6
6
  # First test for usp with grid graph
@@ -1,3 +1,7 @@
1
+ """
2
+ The RelationalAI Semantics Module.
3
+ """
4
+
1
5
  # Mark this package's docstrings for inclusion
2
6
  # in automatically generated web documentation.
3
7
  __include_in_docs__ = True
@@ -6,3 +6,4 @@ concept_population = Relationship.builtins["concept_population"]
6
6
  function = Relationship.builtins["function"]
7
7
  from_cdc = Relationship.builtins["from_cdc"]
8
8
  track = Relationship.builtins["track"]
9
+ recursion_config = Relationship.builtins["recursion_config"]
@@ -2550,8 +2550,6 @@ class Fragment():
2550
2550
  from .snowflake import Table
2551
2551
  assert isinstance(table, Table), "Only Snowflake tables are supported for now"
2552
2552
 
2553
- result_cols = table._col_names
2554
-
2555
2553
  clone = Fragment(parent=self)
2556
2554
  clone._is_export = True
2557
2555
  qb_model = clone._model or Model("anon")
@@ -2559,8 +2557,7 @@ class Fragment():
2559
2557
  clone._source = runtime_env.get_source_pos()
2560
2558
  with debugging.span("query", dsl=str(clone), **with_source(clone), meta=clone._meta):
2561
2559
  query_task = qb_model._compiler.fragment(clone)
2562
- qb_model._to_executor().execute(ir_model, query_task, result_cols=result_cols, export_to=table._fqn, update=update, meta=clone._meta)
2563
-
2560
+ qb_model._to_executor().execute(ir_model, query_task, export_to=table, update=update, meta=clone._meta)
2564
2561
 
2565
2562
  #--------------------------------------------------
2566
2563
  # Select / Where
@@ -3419,6 +3416,8 @@ class Compiler():
3419
3416
  return out
3420
3417
 
3421
3418
  elif isinstance(item, TypeRef):
3419
+ if isinstance(item._thing, Relationship):
3420
+ return self.to_relation(item._thing)
3422
3421
  concept = to_type(item)
3423
3422
  if not concept:
3424
3423
  raise ValueError(f"Cannot find concept for {item}, {type(item)}")
@@ -12,7 +12,18 @@ from . import internal as b, annotations as anns
12
12
  from relationalai import debugging
13
13
  from relationalai.errors import UnsupportedColumnTypesWarning
14
14
  from snowflake.snowpark.context import get_active_session
15
+ from typing import ClassVar, Optional
15
16
 
17
+ #--------------------------------------------------
18
+ # Iceberg Configuration
19
+ #--------------------------------------------------
20
+ @dataclass
21
+ class IcebergConfig:
22
+ """Configuration for exporting to Iceberg tables."""
23
+ external_volume: str | None = None
24
+ default: ClassVar[Optional["IcebergConfig"]]
25
+
26
+ IcebergConfig.default = IcebergConfig()
16
27
  #--------------------------------------------------
17
28
  # Helpers
18
29
  #--------------------------------------------------
@@ -191,7 +202,7 @@ class Table():
191
202
  _schemas:dict[tuple[str, str], SchemaInfo] = {}
192
203
  _used_sources:OrderedSet[Table] = ordered_set()
193
204
 
194
- def __init__(self, fqn:str, cols:list[str]|None=None, schema:dict[str, str|b.Concept]|None=None) -> None:
205
+ def __init__(self, fqn:str, cols:list[str]|None=None, schema:dict[str, str|b.Concept]|None=None, config: IcebergConfig|None=None) -> None:
195
206
  self._fqn = fqn
196
207
  parser = IdentityParser(fqn, require_all_parts=True)
197
208
  self._database, self._schema, self._table, self._fqn = parser.to_list()
@@ -201,6 +212,8 @@ class Table():
201
212
  self._ref = self._concept.ref("row_id")
202
213
  self._cols = {}
203
214
  self._col_names = cols
215
+ self._iceberg_config = config
216
+ self._is_iceberg = config is not None
204
217
  info = self._schemas.get((self._database, self._schema))
205
218
  if not info:
206
219
  info = self._schemas[(self._database, self._schema)] = SchemaInfo(self._database, self._schema)
@@ -12,4 +12,5 @@ annotations_to_emit = FrozenOrderedSet([
12
12
  adhoc.name,
13
13
  builtins.function.name,
14
14
  builtins.track.name,
15
+ builtins.recursion_config.name,
15
16
  ])
@@ -1,6 +1,5 @@
1
1
  from typing import Tuple
2
2
  from relationalai.semantics.lqp import ir as lqp
3
- from relationalai.semantics.metamodel.ir import sanitize
4
3
 
5
4
  def mk_and(args: list[lqp.Formula]) -> lqp.Formula:
6
5
  # Flatten nested conjunctions
@@ -49,10 +48,6 @@ def mk_specialized_value(value) -> lqp.SpecializedValue:
49
48
  def mk_value(value) -> lqp.Value:
50
49
  return lqp.Value(value=value, meta=None)
51
50
 
52
- def mk_var(name: str) -> lqp.Var:
53
- _name = '_' if name == '_' else sanitize(name)
54
- return lqp.Var(name=_name, meta=None)
55
-
56
51
  def mk_type(typename: lqp.TypeName, parameters: list[lqp.Value]=[]) -> lqp.Type:
57
52
  return lqp.Type(type_name=typename, parameters=parameters, meta=None)
58
53
 
@@ -4,7 +4,7 @@ import atexit
4
4
  import re
5
5
 
6
6
  from pandas import DataFrame
7
- from typing import Any, Optional, Literal
7
+ from typing import Any, Optional, Literal, TYPE_CHECKING
8
8
  from snowflake.snowpark import Session
9
9
  import relationalai as rai
10
10
 
@@ -20,10 +20,14 @@ from relationalai.semantics.lqp.ir import convert_transaction, validate_lqp
20
20
  from relationalai.clients.config import Config
21
21
  from relationalai.clients.snowflake import APP_NAME
22
22
  from relationalai.clients.types import TransactionAsyncResponse
23
- from relationalai.clients.util import IdentityParser
23
+ from relationalai.clients.util import IdentityParser, escape_for_f_string
24
24
  from relationalai.tools.constants import USE_DIRECT_ACCESS, QUERY_ATTRIBUTES_HEADER
25
25
  from relationalai.tools.query_utils import prepare_metadata_for_headers
26
26
 
27
+ if TYPE_CHECKING:
28
+ from relationalai.semantics.snowflake import Table
29
+
30
+
27
31
  class LQPExecutor(e.Executor):
28
32
  """Executes LQP using the RAI client."""
29
33
 
@@ -172,12 +176,12 @@ class LQPExecutor(e.Executor):
172
176
  elif len(all_errors) > 1:
173
177
  raise errors.RAIExceptionSet(all_errors)
174
178
 
175
- def _export(self, txn_id: str, export_info: tuple, dest_fqn: str, actual_cols: list[str], declared_cols: list[str], update:bool):
179
+ def _export(self, txn_id: str, export_info: tuple, dest: Table, actual_cols: list[str], declared_cols: list[str], update: bool):
176
180
  # At this point of the export, we assume that a CSV file has already been written
177
181
  # to the Snowflake Native App stage area. Thus, the purpose of this method is to
178
182
  # copy the data from the CSV file to the destination table.
179
183
  _exec = self.resources._exec
180
- dest_database, dest_schema, dest_table, _ = IdentityParser(dest_fqn, require_all_parts=True).to_list()
184
+ dest_database, dest_schema, dest_table, _ = IdentityParser(dest._fqn, require_all_parts=True).to_list()
181
185
  filename = export_info[0]
182
186
  result_table_name = filename + "_table"
183
187
 
@@ -203,8 +207,28 @@ class LQPExecutor(e.Executor):
203
207
  # destination table. This step also cleans up the result table.
204
208
  out_sample = _exec(f"select * from {APP_NAME}.results.{result_table_name} limit 1;")
205
209
  names = self._build_projection(declared_cols, actual_cols, column_fields, out_sample)
210
+ dest_fqn = dest._fqn
206
211
  try:
207
212
  if not update:
213
+ createTableLogic = f"""
214
+ CREATE TABLE {dest_fqn} AS
215
+ SELECT {names}
216
+ FROM {APP_NAME}.results.{result_table_name};
217
+ """
218
+ if dest._is_iceberg:
219
+ assert dest._iceberg_config is not None
220
+ external_volume_clause = ""
221
+ if dest._iceberg_config.external_volume:
222
+ external_volume_clause = f"EXTERNAL_VOLUME = '{dest._iceberg_config.external_volume}'"
223
+ createTableLogic = f"""
224
+ CREATE ICEBERG TABLE {dest_fqn}
225
+ CATALOG = "SNOWFLAKE"
226
+ {external_volume_clause}
227
+ AS
228
+ SELECT {names}
229
+ FROM {APP_NAME}.results.{result_table_name};
230
+ """
231
+
208
232
  _exec(f"""
209
233
  BEGIN
210
234
  -- Check if table exists
@@ -227,9 +251,7 @@ class LQPExecutor(e.Executor):
227
251
  ELSE
228
252
  -- Create table based on the SELECT
229
253
  EXECUTE IMMEDIATE '
230
- CREATE TABLE {dest_fqn} AS
231
- SELECT {names}
232
- FROM {APP_NAME}.results.{result_table_name};
254
+ {escape_for_f_string(createTableLogic)}
233
255
  ';
234
256
  END IF;
235
257
  END;
@@ -376,7 +398,7 @@ class LQPExecutor(e.Executor):
376
398
  return final_model, export_info, txn_proto
377
399
 
378
400
  # TODO (azreika): This should probably be split up into exporting and other processing. There are quite a lot of arguments here...
379
- def _process_results(self, task: ir.Task, final_model: ir.Model, raw_results: TransactionAsyncResponse, result_cols: Optional[list[str]], export_info: Optional[tuple], export_to: Optional[str], update: bool) -> DataFrame:
401
+ 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:
380
402
  cols, extra_cols = self._compute_cols(task, final_model)
381
403
 
382
404
  df, errs = result_helpers.format_results(raw_results, cols)
@@ -391,6 +413,8 @@ class LQPExecutor(e.Executor):
391
413
  assert cols, "No columns found in the output"
392
414
  assert isinstance(raw_results, TransactionAsyncResponse) and raw_results.transaction, "Invalid transaction result"
393
415
 
416
+ result_cols = export_to._col_names
417
+
394
418
  if result_cols is not None:
395
419
  assert all(col in result_cols or col in extra_cols for col in cols)
396
420
  else:
@@ -403,7 +427,7 @@ class LQPExecutor(e.Executor):
403
427
  return self._postprocess_df(self.config, df, extra_cols)
404
428
 
405
429
  def execute(self, model: ir.Model, task: ir.Task, format: Literal["pandas", "snowpark"] = "pandas",
406
- result_cols: Optional[list[str]] = None, export_to: Optional[str] = None,
430
+ export_to: Optional[Table] = None,
407
431
  update: bool = False, meta: dict[str, Any] | None = None) -> DataFrame:
408
432
  self.prepare_data()
409
433
  previous_model = self._last_model
@@ -433,7 +457,7 @@ class LQPExecutor(e.Executor):
433
457
  assert isinstance(raw_results, TransactionAsyncResponse)
434
458
 
435
459
  try:
436
- return self._process_results(task, final_model, raw_results, result_cols, export_info, export_to, update)
460
+ return self._process_results(task, final_model, raw_results, export_info, export_to, update)
437
461
  except Exception as e:
438
462
  # If processing the results failed, revert to the previous model.
439
463
  self._last_model = previous_model
@@ -1,13 +1,13 @@
1
1
  from datetime import datetime, timezone
2
2
 
3
3
  from relationalai.semantics.lqp import ir as lqp
4
- from relationalai.semantics.lqp.constructors import mk_abstraction, mk_value, mk_var, mk_type, mk_primitive
4
+ from relationalai.semantics.lqp.constructors import mk_abstraction, mk_value, mk_type, mk_primitive
5
5
  from relationalai.semantics.lqp.utils import lqp_hash
6
6
 
7
7
  def mk_intrinsic_datetime_now() -> lqp.Def:
8
8
  """Constructs a definition of the current datetime."""
9
9
  id = lqp_hash("__pyrel_lqp_intrinsic_datetime_now")
10
- out = mk_var("out")
10
+ out = lqp.Var(name="out", meta=None)
11
11
  out_type = mk_type(lqp.TypeName.DATETIME)
12
12
  now = mk_value(lqp.DateTimeValue(value=datetime.now(timezone.utc), meta=None))
13
13
  datetime_now = mk_abstraction(
@@ -7,7 +7,7 @@ from relationalai.semantics.lqp.pragmas import pragma_to_lqp_name
7
7
  from relationalai.semantics.lqp.types import meta_type_to_lqp
8
8
  from relationalai.semantics.lqp.constructors import (
9
9
  mk_abstraction, mk_and, mk_exists, mk_or, mk_pragma, mk_primitive,
10
- mk_specialized_value, mk_type, mk_value, mk_var, mk_attribute
10
+ mk_specialized_value, mk_type, mk_value, mk_attribute
11
11
  )
12
12
  from relationalai.semantics.lqp.utils import TranslationCtx, gen_unique_var
13
13
  from relationalai.semantics.lqp.validators import assert_valid_input
@@ -253,7 +253,7 @@ def _translate_rank(ctx: TranslationCtx, rank: ir.Rank, body: lqp.Formula) -> lq
253
253
  # to convert it to Int128.
254
254
  result_var, _ = _translate_term(ctx, rank.result)
255
255
  # The primitive will return an Int64 result, so we need a var to hold the intermediary.
256
- result_64_var = gen_unique_var(ctx, "result_64")
256
+ result_64_var = gen_unique_var(ctx, "v_rank")
257
257
  result_64_type = mk_type(lqp.TypeName.INT)
258
258
 
259
259
  cast = lqp.Cast(input=result_64_var, result=result_var, meta=None)
@@ -273,7 +273,7 @@ def _translate_descending_rank(ctx: TranslationCtx, limit: int, result: lqp.Var,
273
273
  result_type = mk_type(lqp.TypeName.INT)
274
274
 
275
275
  # Rename abstracted args in the body to new variable names
276
- var_map = {var.name: gen_unique_var(ctx, var.name) for (var, _) in abstr_args}
276
+ var_map = {var.name: gen_unique_var(ctx, 't_' + var.name) for (var, _) in abstr_args}
277
277
  body = utils.rename_vars_formula(body, var_map)
278
278
  new_abstr_args = [(var_map[var.name], typ) for (var, typ) in abstr_args]
279
279
 
@@ -306,7 +306,7 @@ def _translate_descending_rank(ctx: TranslationCtx, limit: int, result: lqp.Var,
306
306
  aggr_abstr_args = new_abstr_args + [(count_var, count_type)]
307
307
  count_aggr = lqp.Reduce(
308
308
  op=lqp_operator(
309
- ctx.var_names,
309
+ ctx,
310
310
  "count",
311
311
  "count",
312
312
  mk_type(lqp.TypeName.INT)
@@ -340,7 +340,7 @@ def _translate_ascending_rank(ctx: TranslationCtx, limit: int, result_var: lqp.V
340
340
  terms = [result_var] + [v[0] for v in abstr_args]
341
341
 
342
342
  # Rename abstracted args in the body to new variable names
343
- var_map = {var.name: gen_unique_var(ctx, var.name) for (var, _) in abstr_args}
343
+ var_map = {var.name: gen_unique_var(ctx, 't_' + var.name) for (var, _) in abstr_args}
344
344
  body = utils.rename_vars_formula(body, var_map)
345
345
  new_abstr_args = [(var_map[var.name], typ) for (var, typ) in abstr_args]
346
346
  sort_abstr = mk_abstraction(new_abstr_args, body)
@@ -431,7 +431,7 @@ def _translate_aggregate(ctx: TranslationCtx, aggr: ir.Aggregate, body: lqp.Form
431
431
  (sum_var, sum_type) = abstr_args[-2]
432
432
 
433
433
  result = lqp.Reduce(
434
- op=lqp_avg_op(ctx.var_names, aggr.aggregation.name, sum_var.name, sum_type),
434
+ op=lqp_avg_op(ctx, aggr.aggregation.name, sum_var.name, sum_type),
435
435
  body=mk_abstraction(abstr_args, body),
436
436
  terms=[sum_result, count_result],
437
437
  meta=None,
@@ -461,9 +461,10 @@ def _translate_aggregate(ctx: TranslationCtx, aggr: ir.Aggregate, body: lqp.Form
461
461
  # `input_args`` hold the types of the input arguments, but they may have been modified
462
462
  # if we're dealing with a count, so we use `abstr_args` to find the type.
463
463
  (aggr_arg, aggr_arg_type) = abstr_args[-1]
464
+
464
465
  # Group-bys do not need to be handled at all, since they are introduced outside already
465
466
  reduce = lqp.Reduce(
466
- op=lqp_operator(ctx.var_names, aggr.aggregation.name, aggr_arg.name, aggr_arg_type),
467
+ op=lqp_operator(ctx, aggr.aggregation.name, aggr_arg.name, aggr_arg_type),
467
468
  body=mk_abstraction(abstr_args, body),
468
469
  terms=output_vars,
469
470
  meta=None
@@ -522,9 +523,8 @@ def _translate_term(ctx: TranslationCtx, term: ir.Value) -> Tuple[lqp.Term, lqp.
522
523
  # TODO: ScalarType is not like other terms, should be handled separately.
523
524
  return to_lqp_value(term.name, types.String), meta_type_to_lqp(types.String)
524
525
  elif isinstance(term, ir.Var):
525
- name = ctx.var_names.get_name_by_id(term.id, term.name)
526
526
  t = meta_type_to_lqp(term.type)
527
- return mk_var(name), t
527
+ return _translate_var(ctx, term), t
528
528
  else:
529
529
  assert isinstance(term, ir.Literal), f"Cannot translate value {term!r} of type {type(term)} to LQP Term; neither Var nor Literal."
530
530
  v = to_lqp_value(term.value, term.type)
@@ -542,6 +542,12 @@ def _translate_to_atom(ctx: TranslationCtx, task: ir.Lookup) -> lqp.Formula:
542
542
 
543
543
  if task.relation == builtins.join:
544
544
  return _translate_join(ctx, task)
545
+ elif task.relation == builtins.infomap:
546
+ return _translate_infomap(ctx, task)
547
+ elif task.relation == builtins.louvain:
548
+ return _translate_louvain(ctx, task)
549
+ elif task.relation == builtins.label_propagation:
550
+ return _translate_label_propagation(ctx, task)
545
551
 
546
552
  terms = []
547
553
  term_types = []
@@ -667,6 +673,92 @@ def _extract_pyrel_error_ids(ctx: TranslationCtx, model: ir.Model) -> list[Tuple
667
673
 
668
674
  return pyrel_error_attrs
669
675
 
676
+ # Translate a relation reference into an abstraction over its fields.
677
+ def _translate_relation_ref(ctx: TranslationCtx, relation: ir.Relation) -> lqp.Abstraction:
678
+ projection = []
679
+ for field in relation.fields:
680
+ var = gen_unique_var(ctx, field.name)
681
+ typ = meta_type_to_lqp(field.type)
682
+ projection.append((var, typ))
683
+
684
+ rid = get_relation_id(ctx, relation, projection)
685
+ atom = lqp.Atom(name=rid, terms=[var for (var, _) in projection], meta=None)
686
+ return mk_abstraction(projection, atom)
687
+
688
+ # Common translation logic for graph algorithms.
689
+ # task.args[0] : normalized weight list (int64, int64, float)
690
+ # task.args[1] : normalized node count (relation or constant int64)
691
+ # task.args[2] : normalized edge count (relation or constant int64)
692
+ # task.args[3:-3] : algorithm parameters (var or constant)
693
+ # task.args[-3] : diagnostic info
694
+ # task.args[-2] : node index
695
+ # task.args[-1] : community ident
696
+ def _translate_graph_common(name: str, ctx: TranslationCtx, task: ir.Lookup):
697
+ abstractions = []
698
+
699
+ assert isinstance(task.args[0], ir.Relation), \
700
+ f"Expected relation as first arg to {name}, got {task.args[0]}:{type(task.args[0])}"
701
+ abstractions.append(_translate_relation_ref(ctx, task.args[0]))
702
+
703
+ # Allow constant args for node and edge count
704
+ for arg in task.args[1:3]:
705
+ if isinstance(arg, ir.Relation):
706
+ abst = _translate_relation_ref(ctx, arg)
707
+ typ = abst.vars[0][1]
708
+ assert typ.type_name == lqp.TypeName.INT, \
709
+ f"Expected Int64 types for node and edge counts, got type {typ.type_name}"
710
+ abstractions.append(abst)
711
+ else:
712
+ var, typ, eq = binding_to_lqp_var(ctx, arg)
713
+ assert eq is not None, \
714
+ f"Expected equality formula for {name} arg {arg}:{type(arg)}"
715
+ abstractions.append(mk_abstraction([(var, typ)], mk_and([eq])))
716
+
717
+ for arg in task.args[3:-3]:
718
+ var, typ, eq = binding_to_lqp_var(ctx, arg)
719
+ if eq:
720
+ abstractions.append(mk_abstraction([(var, typ)], mk_and([eq])))
721
+ else:
722
+ print(f"Primitive graph algorithm arg without eq:\n var:{var}, typ:{typ}\n arg:{arg}:{type(arg)}")
723
+ abstractions.append(mk_abstraction([(var, typ)], mk_and([])))
724
+
725
+ terms = []
726
+ for arg in task.args[-3:]:
727
+ term, _ = _translate_relterm(ctx, arg)
728
+ terms.append(term)
729
+
730
+ return (abstractions, terms)
731
+
732
+ def _translate_infomap(ctx: TranslationCtx, task: ir.Lookup) -> lqp.Formula:
733
+ abstractions, terms = _translate_graph_common("infomap", ctx, task)
734
+
735
+ return lqp.FFI(
736
+ meta=None,
737
+ name="rel_primitive_infomap",
738
+ args=abstractions,
739
+ terms=terms,
740
+ )
741
+
742
+ def _translate_louvain(ctx: TranslationCtx, task: ir.Lookup) -> lqp.Formula:
743
+ abstractions, terms = _translate_graph_common("louvain", ctx, task)
744
+
745
+ return lqp.FFI(
746
+ meta=None,
747
+ name="rel_primitive_louvain",
748
+ args=abstractions,
749
+ terms=terms,
750
+ )
751
+
752
+ def _translate_label_propagation(ctx: TranslationCtx, task: ir.Lookup) -> lqp.Formula:
753
+ abstractions, terms = _translate_graph_common("label_propagation", ctx, task)
754
+
755
+ return lqp.FFI(
756
+ meta=None,
757
+ name="rel_primitive_async_label_propagation",
758
+ args=abstractions,
759
+ terms=terms,
760
+ )
761
+
670
762
  # Hard-coded implementation of Rel's string_join
671
763
  def _translate_join(ctx: TranslationCtx, task: ir.Lookup) -> lqp.Formula:
672
764
  assert len(task.args) == 3
@@ -708,3 +800,7 @@ def _translate_join(ctx: TranslationCtx, task: ir.Lookup) -> lqp.Formula:
708
800
  output_term = _translate_term(ctx, target)[0]
709
801
 
710
802
  return lqp.Reduce(meta=None, op=op, body=body, terms=[output_term])
803
+
804
+ def _translate_var(ctx: TranslationCtx, term: ir.Var) -> lqp.Var:
805
+ name = ctx.var_names.get_name_by_id(term.id, term.name)
806
+ return lqp.Var(name=name, meta=None)
@@ -7,16 +7,18 @@ from relationalai.semantics.metamodel.util import FrozenOrderedSet
7
7
  from relationalai.semantics.metamodel.rewrite import Flatten
8
8
 
9
9
  from ..metamodel.rewrite import DischargeConstraints, DNFUnionSplitter, ExtractNestedLogicals
10
- from .rewrite import CDC, ExtractCommon, ExtractKeys, FDConstraints, QuantifyVars, Splinter
10
+ from .rewrite import CDC, ExtractCommon, ExtractKeys, FunctionAnnotations, QuantifyVars, Splinter
11
11
 
12
12
  from relationalai.semantics.lqp.utils import output_names
13
13
 
14
14
  from typing import cast, List, Sequence, Tuple, Union, Optional, Iterable
15
15
  from collections import defaultdict
16
+ import pandas as pd
17
+ import hashlib
16
18
 
17
19
  def lqp_passes() -> list[Pass]:
18
20
  return [
19
- FDConstraints(),
21
+ FunctionAnnotations(),
20
22
  DischargeConstraints(),
21
23
  Checker(),
22
24
  CDC(), # specialize to physical relations before extracting nested and typing
@@ -337,7 +339,7 @@ class UnifyDefinitions(Pass):
337
339
  )
338
340
 
339
341
  # Creates intermediary relations for all Data nodes and replaces said Data nodes
340
- # with a Lookup into these created relations.
342
+ # with a Lookup into these created relations. Reuse duplicate created relations.
341
343
  class EliminateData(Pass):
342
344
  def rewrite(self, model: ir.Model, options:dict={}) -> ir.Model:
343
345
  r = self.DataRewriter()
@@ -350,17 +352,25 @@ class EliminateData(Pass):
350
352
  # Counter for naming new relations.
351
353
  # It must be that new_count == len new_updates == len new_relations.
352
354
  new_count: int
355
+ # Cache for Data nodes to avoid creating duplicate intermediary relations
356
+ data_cache: dict[str, ir.Relation]
353
357
 
354
358
  def __init__(self):
355
359
  self.new_relations = []
356
360
  self.new_updates = []
357
361
  self.new_count = 0
362
+ self.data_cache = {}
358
363
  super().__init__()
359
364
 
360
- # Create a new intermediary relation representing the Data (and pop it in
361
- # new_updates/new_relations) and replace this Data with a Lookup of said
362
- # intermediary.
363
- def handle_data(self, node: ir.Data, parent: ir.Node) -> ir.Lookup:
365
+ # Create a cache key for a Data node based on its structure and content
366
+ def _data_cache_key(self, node: ir.Data) -> str:
367
+ values = pd.util.hash_pandas_object(node.data).values
368
+ return hashlib.sha256(bytes(values)).hexdigest()
369
+
370
+ def _intermediary_relation(self, node: ir.Data) -> ir.Relation:
371
+ cache_key = self._data_cache_key(node)
372
+ if cache_key in self.data_cache:
373
+ return self.data_cache[cache_key]
364
374
  self.new_count += 1
365
375
  intermediary_name = f"formerly_Data_{self.new_count}"
366
376
 
@@ -379,7 +389,6 @@ class EliminateData(Pass):
379
389
  f.lookup(rel_builtins.eq, [f.literal(val), var])
380
390
  for (val, var) in zip(row, node.vars)
381
391
  ],
382
- hoisted = node.vars,
383
392
  )
384
393
  for row in node
385
394
  ],
@@ -390,6 +399,16 @@ class EliminateData(Pass):
390
399
  ])
391
400
  self.new_updates.append(intermediary_update)
392
401
 
402
+ # Cache the result for reuse
403
+ self.data_cache[cache_key] = intermediary_relation
404
+
405
+ return intermediary_relation
406
+
407
+ # Create a new intermediary relation representing the Data (and pop it in
408
+ # new_updates/new_relations) and replace this Data with a Lookup of said
409
+ # intermediary.
410
+ def handle_data(self, node: ir.Data, parent: ir.Node) -> ir.Lookup:
411
+ intermediary_relation = self._intermediary_relation(node)
393
412
  replacement_lookup = f.lookup(intermediary_relation, node.vars)
394
413
 
395
414
  return replacement_lookup