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.
- relationalai/__init__.py +9 -0
- relationalai/clients/__init__.py +2 -2
- relationalai/clients/local.py +571 -0
- relationalai/clients/snowflake.py +106 -83
- relationalai/debugging.py +5 -2
- relationalai/semantics/__init__.py +2 -2
- relationalai/semantics/internal/__init__.py +2 -2
- relationalai/semantics/internal/internal.py +53 -14
- relationalai/semantics/lqp/README.md +34 -0
- relationalai/semantics/lqp/compiler.py +1 -1
- relationalai/semantics/lqp/constructors.py +7 -0
- relationalai/semantics/lqp/executor.py +35 -39
- relationalai/semantics/lqp/intrinsics.py +4 -3
- relationalai/semantics/lqp/ir.py +4 -0
- relationalai/semantics/lqp/model2lqp.py +47 -14
- relationalai/semantics/lqp/passes.py +7 -4
- relationalai/semantics/lqp/rewrite/__init__.py +4 -1
- relationalai/semantics/lqp/rewrite/annotate_constraints.py +55 -0
- relationalai/semantics/lqp/rewrite/extract_keys.py +22 -3
- relationalai/semantics/lqp/rewrite/function_annotations.py +91 -56
- relationalai/semantics/lqp/rewrite/functional_dependencies.py +314 -0
- relationalai/semantics/lqp/rewrite/quantify_vars.py +14 -0
- relationalai/semantics/lqp/validators.py +3 -0
- relationalai/semantics/metamodel/builtins.py +10 -0
- relationalai/semantics/metamodel/rewrite/extract_nested_logicals.py +5 -4
- relationalai/semantics/metamodel/rewrite/flatten.py +10 -4
- relationalai/semantics/metamodel/typer/typer.py +13 -0
- relationalai/semantics/metamodel/types.py +2 -1
- relationalai/semantics/reasoners/graph/core.py +44 -53
- relationalai/semantics/rel/compiler.py +19 -1
- relationalai/semantics/tests/test_snapshot_abstract.py +3 -0
- relationalai/tools/debugger.py +4 -2
- relationalai/tools/qb_debugger.py +5 -3
- relationalai/util/otel_handler.py +10 -4
- {relationalai-0.12.8.dist-info → relationalai-0.12.10.dist-info}/METADATA +2 -2
- {relationalai-0.12.8.dist-info → relationalai-0.12.10.dist-info}/RECORD +39 -35
- {relationalai-0.12.8.dist-info → relationalai-0.12.10.dist-info}/WHEEL +0 -0
- {relationalai-0.12.8.dist-info → relationalai-0.12.10.dist-info}/entry_points.txt +0 -0
- {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:
|
|
@@ -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
|
-
|
|
51
|
+
logical.hoisted and
|
|
52
52
|
not isinstance(parent, (ir.Match, ir.Union)) and
|
|
53
|
-
all(isinstance(v, ir.Var) for v in
|
|
54
|
-
not any(isinstance(c, ir.Rank) for c in
|
|
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,
|
|
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
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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):
|