relationalai 0.12.8__py3-none-any.whl → 0.12.10__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 (39) hide show
  1. relationalai/__init__.py +9 -0
  2. relationalai/clients/__init__.py +2 -2
  3. relationalai/clients/local.py +571 -0
  4. relationalai/clients/snowflake.py +106 -83
  5. relationalai/debugging.py +5 -2
  6. relationalai/semantics/__init__.py +2 -2
  7. relationalai/semantics/internal/__init__.py +2 -2
  8. relationalai/semantics/internal/internal.py +53 -14
  9. relationalai/semantics/lqp/README.md +34 -0
  10. relationalai/semantics/lqp/compiler.py +1 -1
  11. relationalai/semantics/lqp/constructors.py +7 -0
  12. relationalai/semantics/lqp/executor.py +35 -39
  13. relationalai/semantics/lqp/intrinsics.py +4 -3
  14. relationalai/semantics/lqp/ir.py +4 -0
  15. relationalai/semantics/lqp/model2lqp.py +47 -14
  16. relationalai/semantics/lqp/passes.py +7 -4
  17. relationalai/semantics/lqp/rewrite/__init__.py +4 -1
  18. relationalai/semantics/lqp/rewrite/annotate_constraints.py +55 -0
  19. relationalai/semantics/lqp/rewrite/extract_keys.py +22 -3
  20. relationalai/semantics/lqp/rewrite/function_annotations.py +91 -56
  21. relationalai/semantics/lqp/rewrite/functional_dependencies.py +314 -0
  22. relationalai/semantics/lqp/rewrite/quantify_vars.py +14 -0
  23. relationalai/semantics/lqp/validators.py +3 -0
  24. relationalai/semantics/metamodel/builtins.py +10 -0
  25. relationalai/semantics/metamodel/rewrite/extract_nested_logicals.py +5 -4
  26. relationalai/semantics/metamodel/rewrite/flatten.py +10 -4
  27. relationalai/semantics/metamodel/typer/typer.py +13 -0
  28. relationalai/semantics/metamodel/types.py +2 -1
  29. relationalai/semantics/reasoners/graph/core.py +44 -53
  30. relationalai/semantics/rel/compiler.py +19 -1
  31. relationalai/semantics/tests/test_snapshot_abstract.py +3 -0
  32. relationalai/tools/debugger.py +4 -2
  33. relationalai/tools/qb_debugger.py +5 -3
  34. relationalai/util/otel_handler.py +10 -4
  35. {relationalai-0.12.8.dist-info → relationalai-0.12.10.dist-info}/METADATA +2 -2
  36. {relationalai-0.12.8.dist-info → relationalai-0.12.10.dist-info}/RECORD +39 -35
  37. {relationalai-0.12.8.dist-info → relationalai-0.12.10.dist-info}/WHEEL +0 -0
  38. {relationalai-0.12.8.dist-info → relationalai-0.12.10.dist-info}/entry_points.txt +0 -0
  39. {relationalai-0.12.8.dist-info → relationalai-0.12.10.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,314 @@
1
+ from __future__ import annotations
2
+ from typing import Optional, Sequence
3
+ from relationalai.semantics.internal import internal
4
+ from relationalai.semantics.metamodel.ir import (
5
+ Node, Require, Logical, Var, Relation, Lookup, ScalarType
6
+ )
7
+ from relationalai.semantics.metamodel import builtins
8
+
9
+
10
+ """
11
+ Helper functions for converting `Require` nodes with unique constraints to functional
12
+ dependencies. The main functionalities provided are:
13
+ 1. Check whether a `Require` node is a valid unique constraint representation
14
+ 2. Represent the uniqueness constraint as a functional dependency
15
+ 3. Check if the functional dependency is structural i.e., can be represented with
16
+ `@function(k)` annotation on a single relation.
17
+
18
+ =========================== Structure of unique constraints ================================
19
+ A `Require` node represents a _unique constraint_ if it meets the following criteria:
20
+ * the `Require` node's `domain` is an empty `Logical` node
21
+ * the `Require` node's `checks` has a single `Check` node
22
+ * the single `Check` node has `Logical` task that is a list of `Lookup` tasks
23
+ * precisely one `Lookup` task in the `Check` uses the `unique` builtin relation name
24
+ * the `unique` lookup has precisely one argument, which is a `TupleArg` or a `tuple`
25
+ containing at least one `Var`
26
+ * all `Lookup` nodes use variables only (no constants)
27
+ * the variables used in the `unique` lookup are a subset of the variables used in other
28
+ lookups
29
+ ============================================================================================
30
+
31
+ We use the following unique constraint as the running example.
32
+
33
+ ```
34
+ Require
35
+ domain
36
+ Logical
37
+ checks:
38
+ Check
39
+ check:
40
+ Logical
41
+ Person(person::Person)
42
+ first_name(person::Person, firstname::String)
43
+ last_name(person::Person, lastname::String)
44
+ unique((firstname::String, lastname::String))
45
+ error:
46
+ ...
47
+ ```
48
+
49
+ =========================== Semantics of unique constraints ================================
50
+ A unique constraint states that the columns declared in the `unique` predicate must be
51
+ unique in the result of the conjunctive query consisting of all remaining predicates.
52
+ ============================================================================================
53
+
54
+ In the running example, the conjunctive query computes a table with 3 columns, the person id
55
+ `person::Person`, the first name `firstname::String`, and the last name `lastname::String`.
56
+ The uniqueness predicate `unique((firstname::String, lastname::String))` states that no person
57
+ can have more than a single combination of first and last name.
58
+
59
+ The unique constraint in the running example above corresponds to the following functional
60
+ dependency.
61
+
62
+ ```
63
+ Person(x) ∧ first_name(x, y) ∧ last_name(x, z): {y, z} -> {x}
64
+ ```
65
+
66
+ ------------------------------ Redundant Type Atoms ----------------------------------------
67
+ At the time of writing, PyRel does not yet remove redundant unary atoms. For instance, in
68
+ the running example, the atom `Person(person::Person)` is redundant because the type of the
69
+ `person` variable is specified in the other two atoms `first_name` and `last_name`.
70
+ Consequently, we identify redundant atoms and remove them from the definition of the
71
+ corresponding functional dependency.
72
+
73
+ Formally, a _guard_ atom is any `Lookup` node whose relation name is not `unique`. Now, a
74
+ unary guard atom `T(x::T)` is _redundant_ if the uniqueness constraint has a non-unary guard
75
+ atom `R(...,x::T,...)`.
76
+
77
+ ================================ Normalized FDs ============================================
78
+ Now, the _(normalized)_ functional dependency_ corresponding to a unique constraint is an
79
+ object of the form `φ: X → Y`, where :
80
+ 1. `φ` is the set of all non-redundant guard atoms.
81
+ 2. `X` is the set of variables used in the `unique` atom
82
+ 3. `Y` is the set of all other variables used in the constraint
83
+ ============================================================================================
84
+
85
+ The normalized functional dependency corresponding to the unique constraints from the running
86
+ example is :
87
+ ```
88
+ first_name(person::Person, firstname::String) ∧ last_name(person::Person, lastname::String): {firstname:String, lastname:String} -> {person:Person}
89
+ ```
90
+ Note that the unary atom `Person(person::Person)` is redundant and thus omitted from the
91
+ decomposition.
92
+
93
+ Some simple functional dependencies can, however, be expressed simply with `@function(k)`
94
+ attribute of a single relation. Specifically, a functional dependency `φ: X → Y` is
95
+ _structural_ if φ consists of a single atom `R(x1,...,xm,y1,...,yk)` and `X = {x1,...,xm}`.
96
+ """
97
+
98
+ #
99
+ # Checks that an input `Require` node is a valid unique constraint. Returns `None` if not.
100
+ # If yes, we return the decomposition of the unique constraint as a tuple
101
+ # `(all_vars, unique_vars, guard)`, where
102
+ # - `all_vars` is the list of all variables used in the constraint
103
+ # - `unique_vars` is the list of variables used in the `unique` atom
104
+ # - `guard` is the list of all other `Lookup` atoms
105
+ #
106
+ def _split_unique_require_node(node: Require) -> Optional[tuple[list[Var], list[Var], list[Lookup]]]:
107
+ if not isinstance(node.domain, Logical):
108
+ return None
109
+ if len(node.domain.body) != 0:
110
+ return None
111
+ if len(node.checks) != 1:
112
+ return None
113
+ check = node.checks[0]
114
+ if not isinstance(check.check, Logical):
115
+ return None
116
+
117
+ unique_atom: Optional[Lookup] = None
118
+ guard: list[Lookup] = []
119
+ for task in check.check.body:
120
+ if not isinstance(task, Lookup):
121
+ return None
122
+ if task.relation.name == builtins.unique.name:
123
+ if unique_atom is not None:
124
+ return None
125
+ unique_atom = task
126
+ else:
127
+ guard.append(task)
128
+
129
+ if unique_atom is None:
130
+ return None
131
+
132
+ # collect variables
133
+ all_vars: list[Var] = []
134
+ for lookup in guard:
135
+ for arg in lookup.args:
136
+ if not isinstance(arg, Var):
137
+ return None
138
+ if arg in all_vars:
139
+ continue
140
+ all_vars.append(arg)
141
+
142
+ unique_vars: list[Var] = []
143
+ if len(unique_atom.args) != 1:
144
+ return None
145
+ if not isinstance(unique_atom.args[0], (internal.TupleArg, tuple)):
146
+ return None
147
+ if len(unique_atom.args[0]) == 0:
148
+ return None
149
+ for arg in unique_atom.args[0]:
150
+ if not isinstance(arg, Var):
151
+ return None
152
+ if arg in unique_vars:
153
+ return None
154
+ unique_vars.append(arg)
155
+
156
+ # check that unique vars are a subset of other vars
157
+ if not set(unique_vars).issubset(set(all_vars)):
158
+ return None
159
+
160
+ return list(all_vars), list(unique_vars), guard
161
+
162
+
163
+ def is_valid_unique_constraint(node: Require) -> bool:
164
+ """
165
+ Checks whether the input `Require` node is a valid unique constraint. See description at
166
+ the top of the file for details.
167
+ """
168
+ return _split_unique_require_node(node) is not None
169
+
170
+ #
171
+ # A unary guard atom `T(x::T)` is redundant if the constraint contains a non-unary atom
172
+ # `R(...,x::T,...)`. We discard all redundant guard atoms in the constructed fd.
173
+ #
174
+ def normalized_fd(node: Require) -> Optional[FunctionalDependency]:
175
+ """
176
+ If the input `Require` node is a uniqueness constraint, constructs its reduced
177
+ functional dependency `φ: X -> Y`, where `φ` contains all non-redundant guard atoms,
178
+ `X` are the variables used in the `unique` atom, and `Y` are the remaining variables.
179
+ Returns `None` if the input node is not a valid uniqueness constraint.
180
+ """
181
+ parts = _split_unique_require_node(node)
182
+ if parts is None:
183
+ return None
184
+ all_vars, unique_vars, guard_atoms = parts
185
+
186
+ # remove redundant lookups
187
+ redundant_guard_atoms: list[Lookup] = []
188
+ for atom in guard_atoms:
189
+ # the atom is unary A(x::T)
190
+ if len(atom.args) != 1:
191
+ continue
192
+ var = atom.args[0]
193
+ assert isinstance(var, Var)
194
+ # T is a scalar type (which includes entity types)
195
+ var_type = var.type
196
+ if not isinstance(var_type, ScalarType):
197
+ continue
198
+ # the atom is a entity typing T(x::T) i.e., T = A (and hence not a Boolean property)
199
+ var_type_name = var_type.name
200
+ rel_name = atom.relation.name
201
+ if rel_name != var_type_name:
202
+ continue
203
+ # Found an atom of the form T(x::T)
204
+ # check if there is another atom R(...,x::T,...)
205
+ for typed_atom in guard_atoms:
206
+ if len(typed_atom.args) == 1:
207
+ continue
208
+ if var in typed_atom.args:
209
+ redundant_guard_atoms.append(atom)
210
+ break
211
+
212
+ guard = [atom for atom in guard_atoms if atom not in redundant_guard_atoms]
213
+ keys = unique_vars
214
+ values = [v for v in all_vars if v not in keys]
215
+
216
+ return FunctionalDependency(guard, keys, values)
217
+
218
+ class FunctionalDependency:
219
+ """
220
+ Represents a functional dependency of the form `φ: X -> Y`, where
221
+ - `φ` is a set of `Lookup` atoms
222
+ - `X` and `Y` are disjoint and covering sets of variables used in `φ`
223
+ """
224
+ def __init__(self, guard: Sequence[Lookup], keys: Sequence[Var], values: Sequence[Var]):
225
+ self.guard = tuple(guard)
226
+ self.keys = tuple(keys)
227
+ self.values = tuple(values)
228
+ assert set(self.keys).isdisjoint(set(self.values)), "Keys and values must be disjoint"
229
+
230
+ # for structural fd check
231
+ self._is_structural:bool = False
232
+ self._structural_relation:Optional[Relation] = None
233
+ self._structural_rank:Optional[int] = None
234
+
235
+ self._determine_is_structural()
236
+
237
+ # A functional dependency `φ: X → Y` is _k-functional_ if `φ` consists of a single atom
238
+ # `R(x1,...,xm,y1,...,yk)` and `X = {x1,...,xm}`. Not all functional dependencies are
239
+ # k-functional. For instance, `R(x, y, z): {y, z} → {x}` cannot be expressed with
240
+ # `@function`. neither can `R(x, y) ∧ P(x, z) : {x} → {y, z}`.
241
+ def _determine_is_structural(self):
242
+ if len(self.guard) != 1:
243
+ self._is_structural = False
244
+ return
245
+ atom = next(iter(self.guard))
246
+ atom_vars = atom.args
247
+ if len(atom_vars) <= len(self.keys): # @function(0) provides no information
248
+ self._is_structural = False
249
+ return
250
+ prefix_vars = atom_vars[:len(self.keys)]
251
+ if set(prefix_vars) != set(self.keys):
252
+ self._is_structural = False
253
+ return
254
+ self._is_structural = True
255
+ self._structural_relation = atom.relation
256
+ self._structural_rank = len(atom_vars) - len(self.keys)
257
+
258
+ @property
259
+ def is_structural(self) -> bool:
260
+ """
261
+ Whether the functional dependency is functional, i.e., can be represented
262
+ with `@function(k)` annotation on a single relation.
263
+ """
264
+ return self._is_structural
265
+
266
+ @property
267
+ def structural_relation(self) -> Relation:
268
+ """
269
+ The structural relation of a functional dependency. Raises ValueError if the functional
270
+ dependency is not structural.
271
+ """
272
+ if not self._is_structural:
273
+ raise ValueError("Functional dependency is not structural")
274
+ assert self._structural_relation is not None
275
+ return self._structural_relation
276
+
277
+ @property
278
+ def structural_rank(self) -> int:
279
+ """
280
+ The structural rank k of k-structural fd. Raises ValueError if the structural
281
+ dependency is not k-structural.
282
+ """
283
+ if not self._is_structural:
284
+ raise ValueError("Functional dependency is not structural")
285
+ assert self._structural_rank is not None
286
+ return self._structural_rank
287
+
288
+ def __str__(self) -> str:
289
+ guard_str = " ∧ ".join([str(atom) for atom in self.guard]).strip()
290
+ keys_str = ", ".join([str(var) for var in self.keys]).strip()
291
+ values_str = ", ".join([str(var) for var in self.values]).strip()
292
+ return f"{guard_str}: {{{keys_str}}} -> {{{values_str}}}"
293
+
294
+ def contains_only_declarable_constraints(node: Node) -> bool:
295
+ """
296
+ Checks whether the input `Logical` node contains only `Require` nodes annotated with
297
+ `declare_constraint`.
298
+ """
299
+ if not isinstance(node, Logical):
300
+ return False
301
+ if len(node.body) == 0:
302
+ return False
303
+ for task in node.body:
304
+ if not isinstance(task, Require):
305
+ return False
306
+ if not is_declarable_constraint(task):
307
+ return False
308
+ return True
309
+
310
+ def is_declarable_constraint(node: Require) -> bool:
311
+ """
312
+ Checks whether the input `Require` node is annotated with `declare_constraint`.
313
+ """
314
+ return builtins.declare_constraint_annotation in node.annotations
@@ -5,6 +5,7 @@ from relationalai.semantics.metamodel.compiler import Pass
5
5
  from relationalai.semantics.metamodel.visitor import Visitor, Rewriter
6
6
  from relationalai.semantics.metamodel.util import OrderedSet, ordered_set
7
7
  from typing import Optional, Any, Tuple, Iterable
8
+ from .functional_dependencies import contains_only_declarable_constraints
8
9
 
9
10
  class QuantifyVars(Pass):
10
11
  """
@@ -67,6 +68,7 @@ class VarScopeInfo(Visitor):
67
68
  IGNORED_NODES = (ir.Type,
68
69
  ir.Var, ir.Literal, ir.Relation, ir.Field,
69
70
  ir.Default, ir.Output, ir.Update, ir.Aggregate,
71
+ ir.Check, ir.Require,
70
72
  ir.Annotation, ir.Rank)
71
73
 
72
74
  def __init__(self):
@@ -74,6 +76,9 @@ class VarScopeInfo(Visitor):
74
76
  self._vars_in_scope = {}
75
77
 
76
78
  def leave(self, node: ir.Node, parent: Optional[ir.Node]=None):
79
+ if contains_only_declarable_constraints(node):
80
+ return node
81
+
77
82
  if isinstance(node, ir.Lookup):
78
83
  self._record(node, helpers.vars(node.args))
79
84
 
@@ -189,6 +194,9 @@ class FindQuantificationNodes(Visitor):
189
194
  self.node_quantifies_vars = {}
190
195
 
191
196
  def enter(self, node: ir.Node, parent: Optional[ir.Node]=None) -> "Visitor":
197
+ if contains_only_declarable_constraints(node):
198
+ return self
199
+
192
200
  if isinstance(node, (ir.Logical, ir.Not)):
193
201
  ignored_vars = _ignored_vars(node)
194
202
  self._handled_vars.update(ignored_vars)
@@ -202,6 +210,9 @@ class FindQuantificationNodes(Visitor):
202
210
  return self
203
211
 
204
212
  def leave(self, node: ir.Node, parent: Optional[ir.Node]=None) -> ir.Node:
213
+ if contains_only_declarable_constraints(node):
214
+ return node
215
+
205
216
  if isinstance(node, (ir.Logical, ir.Not)):
206
217
  ignored_vars = _ignored_vars(node)
207
218
  self._handled_vars.difference_update(ignored_vars)
@@ -221,6 +232,9 @@ class QuantifyVarsRewriter(Rewriter):
221
232
  self.node_quantifies_vars = quant.node_quantifies_vars
222
233
 
223
234
  def handle_logical(self, node: ir.Logical, parent: ir.Node, ctx:Optional[Any]=None) -> ir.Logical:
235
+ if contains_only_declarable_constraints(node):
236
+ return node
237
+
224
238
  new_body = self.walk_list(node.body, node)
225
239
 
226
240
  if node.id in self.node_quantifies_vars:
@@ -21,6 +21,9 @@ CompilableType = Union[
21
21
  # Effects
22
22
  ir.Output,
23
23
  ir.Update,
24
+
25
+ # Constraints
26
+ ir.Require,
24
27
  ]
25
28
 
26
29
  # Preconditions
@@ -495,6 +495,11 @@ output_keys_annotation = f.annotation(output_keys, [])
495
495
  function = f.relation("function", [f.input_field("code", types.Symbol)])
496
496
  function_checked_annotation = f.annotation(function, [f.lit("checked")])
497
497
  function_annotation = f.annotation(function, [])
498
+ function_ranked = f.relation("function", [f.input_field("code", types.Symbol), f.input_field("rank", types.Int64)])
499
+ def function_ranked_checked_annotation(k:int) -> ir.Annotation:
500
+ return f.annotation(function_ranked, [f.lit("checked"), f.lit(k)])
501
+ def function_ranked_annotation(k:int) -> ir.Annotation:
502
+ return f.annotation(function_ranked, [f.lit(k)])
498
503
 
499
504
  # Indicates this relation should be tracked in telemetry. Supported for Relationships and Concepts.
500
505
  # `RAI_BackIR.with_relation_tracking` produces log messages at the start and end of each
@@ -519,6 +524,11 @@ recursion_config_annotation = f.annotation(recursion_config, [])
519
524
  discharged = f.relation("discharged", [])
520
525
  discharged_annotation = f.annotation(discharged, [])
521
526
 
527
+ # Require nodes with this annotation will be kept in the final metamodel to be emitted as
528
+ # constraint declarations (LQP)
529
+ declare_constraint = f.relation("declare_constraint", [])
530
+ declare_constraint_annotation = f.annotation(declare_constraint, [])
531
+
522
532
  #
523
533
  # Aggregations
524
534
  #
@@ -48,10 +48,10 @@ class LogicalExtractor(Rewriter):
48
48
  # variables (which is currently done by flatten), such as when the parent is a Match
49
49
  # or a Union, of if the logical has a Rank.
50
50
  if not (
51
- node.hoisted and
51
+ logical.hoisted and
52
52
  not isinstance(parent, (ir.Match, ir.Union)) and
53
- all(isinstance(v, ir.Var) for v in node.hoisted) and
54
- not any(isinstance(c, ir.Rank) for c in node.body)
53
+ all(isinstance(v, ir.Var) for v in logical.hoisted) and
54
+ not any(isinstance(c, ir.Rank) for c in logical.body)
55
55
  ):
56
56
  return logical
57
57
 
@@ -61,10 +61,11 @@ class LogicalExtractor(Rewriter):
61
61
 
62
62
  # if there are aggregations, make sure we don't expose the projected and input vars,
63
63
  # but expose groupbys
64
- for agg in collect_by_type(ir.Aggregate, node):
64
+ for agg in collect_by_type(ir.Aggregate, logical):
65
65
  exposed_vars.difference_update(agg.projection)
66
66
  exposed_vars.difference_update(helpers.aggregate_inputs(agg))
67
67
  exposed_vars.update(agg.group)
68
+
68
69
  # add the values (hoisted)
69
70
  exposed_vars.update(helpers.hoisted_vars(logical.hoisted))
70
71
 
@@ -5,7 +5,7 @@ from typing import Tuple
5
5
 
6
6
  from relationalai.semantics.metamodel import builtins, ir, factory as f, helpers
7
7
  from relationalai.semantics.metamodel.compiler import Pass, group_tasks
8
- from relationalai.semantics.metamodel.util import OrderedSet, ordered_set, NameCache
8
+ from relationalai.semantics.metamodel.util import NameCache, OrderedSet, ordered_set
9
9
  from relationalai.semantics.metamodel import dependency
10
10
  from relationalai.semantics.metamodel.typer.typer import to_type
11
11
 
@@ -419,9 +419,15 @@ class Flatten(Pass):
419
419
  def handle_require(self, req: ir.Require, ctx: Context):
420
420
  # only extract the domain if it is a somewhat complex Logical and there's more than
421
421
  # one check, otherwise insert it straight into all checks
422
- domain = req.domain
423
- # only generate logic for not discharged requires
424
- if builtins.discharged_annotation not in req.annotations:
422
+ if builtins.discharged_annotation in req.annotations:
423
+ # remove discharged Requires
424
+ return Flatten.HandleResult(None)
425
+ elif builtins.declare_constraint_annotation in req.annotations:
426
+ # leave Requires that are declared constraints
427
+ return Flatten.HandleResult(req)
428
+ else:
429
+ # generate logic for remaining requires
430
+ domain = req.domain
425
431
  if len(req.checks) > 1 and isinstance(domain, ir.Logical) and len(domain.body) > 1:
426
432
  body = OrderedSet.from_iterable(domain.body)
427
433
  vars = helpers.hoisted_vars(domain.hoisted)
@@ -156,6 +156,10 @@ def type_matches(actual:ir.Type, expected:ir.Type, allow_expected_parents=False)
156
156
  if actual == types.Any or expected == types.Any:
157
157
  return True
158
158
 
159
+ # any entity matches any entity (surprise surprise!)
160
+ if extends_any_entity(expected) and not is_primitive(actual):
161
+ return True
162
+
159
163
  # all decimals match across each other
160
164
  if types.is_decimal(actual) and types.is_decimal(expected):
161
165
  return True
@@ -288,6 +292,15 @@ def is_base_primitive(type:ir.Type) -> bool:
288
292
  def is_primitive(type:ir.Type) -> bool:
289
293
  return to_base_primitive(type) is not None
290
294
 
295
+ def extends_any_entity(type:ir.Type) -> bool:
296
+ if type == types.AnyEntity:
297
+ return True
298
+ if isinstance(type, ir.ScalarType):
299
+ for parent in type.super_types:
300
+ if extends_any_entity(parent):
301
+ return True
302
+ return False
303
+
291
304
  def invalid_type(type:ir.Type) -> bool:
292
305
  if isinstance(type, ir.UnionType):
293
306
  # if there are multiple primitives, or a primtive and a non-primitive
@@ -80,6 +80,7 @@ GenericDecimal = ir.ScalarType("GenericDecimal", util.frozen())
80
80
  #
81
81
  Null = ir.ScalarType("Null", util.frozen())
82
82
  Any = ir.ScalarType("Any", util.frozen())
83
+ AnyEntity = ir.ScalarType("AnyEntity", util.frozen())
83
84
  Hash = ir.ScalarType("Hash", util.frozen())
84
85
  String = ir.ScalarType("String", util.frozen())
85
86
  Int64 = ir.ScalarType("Int64")
@@ -144,7 +145,7 @@ def is_null(t: ir.Type) -> bool:
144
145
 
145
146
  def is_abstract_type(t: ir.Type) -> bool:
146
147
  if isinstance(t, ir.ScalarType):
147
- return t in [Any, Number, GenericDecimal]
148
+ return t in [Any, AnyEntity, Number, GenericDecimal]
148
149
  elif isinstance(t, ir.ListType):
149
150
  return is_abstract_type(t.element_type)
150
151
  elif isinstance(t, ir.TupleType):