pytrilogy 0.0.3.93__py3-none-any.whl → 0.0.3.95__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.
Potentially problematic release.
This version of pytrilogy might be problematic. Click here for more details.
- {pytrilogy-0.0.3.93.dist-info → pytrilogy-0.0.3.95.dist-info}/METADATA +170 -145
- {pytrilogy-0.0.3.93.dist-info → pytrilogy-0.0.3.95.dist-info}/RECORD +38 -34
- trilogy/__init__.py +1 -1
- trilogy/authoring/__init__.py +4 -0
- trilogy/core/enums.py +13 -0
- trilogy/core/env_processor.py +21 -10
- trilogy/core/environment_helpers.py +111 -0
- trilogy/core/exceptions.py +21 -1
- trilogy/core/functions.py +6 -1
- trilogy/core/graph_models.py +60 -67
- trilogy/core/internal.py +18 -0
- trilogy/core/models/author.py +16 -25
- trilogy/core/models/build.py +5 -4
- trilogy/core/models/core.py +3 -0
- trilogy/core/models/environment.py +28 -0
- trilogy/core/models/execute.py +7 -0
- trilogy/core/processing/node_generators/node_merge_node.py +30 -28
- trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +25 -11
- trilogy/core/processing/node_generators/select_merge_node.py +68 -82
- trilogy/core/query_processor.py +2 -1
- trilogy/core/statements/author.py +18 -3
- trilogy/core/statements/common.py +0 -10
- trilogy/core/statements/execute.py +71 -16
- trilogy/core/validation/__init__.py +0 -0
- trilogy/core/validation/common.py +109 -0
- trilogy/core/validation/concept.py +122 -0
- trilogy/core/validation/datasource.py +192 -0
- trilogy/core/validation/environment.py +71 -0
- trilogy/dialect/base.py +40 -21
- trilogy/dialect/sql_server.py +3 -1
- trilogy/engine.py +25 -7
- trilogy/executor.py +145 -83
- trilogy/parsing/parse_engine.py +35 -4
- trilogy/parsing/trilogy.lark +11 -5
- trilogy/core/processing/node_generators/select_merge_node_v2.py +0 -792
- {pytrilogy-0.0.3.93.dist-info → pytrilogy-0.0.3.95.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.3.93.dist-info → pytrilogy-0.0.3.95.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.3.93.dist-info → pytrilogy-0.0.3.95.dist-info}/licenses/LICENSE.md +0 -0
- {pytrilogy-0.0.3.93.dist-info → pytrilogy-0.0.3.95.dist-info}/top_level.txt +0 -0
trilogy/core/env_processor.py
CHANGED
|
@@ -20,7 +20,8 @@ def add_concept(
|
|
|
20
20
|
if node_name in seen:
|
|
21
21
|
return
|
|
22
22
|
seen.add(node_name)
|
|
23
|
-
g.
|
|
23
|
+
g.concepts[node_name] = concept
|
|
24
|
+
g.add_node(node_name)
|
|
24
25
|
if concept.concept_arguments:
|
|
25
26
|
for source in concept.concept_arguments:
|
|
26
27
|
if not isinstance(source, BuildConcept):
|
|
@@ -28,9 +29,10 @@ def add_concept(
|
|
|
28
29
|
f"Invalid non-build concept {source} passed into graph generation from {concept}"
|
|
29
30
|
)
|
|
30
31
|
generic = get_default_grain_concept(source, default_concept_graph)
|
|
32
|
+
generic_node = concept_to_node(generic)
|
|
31
33
|
add_concept(generic, g, concept_mapping, default_concept_graph, seen)
|
|
32
34
|
|
|
33
|
-
g.add_edge(
|
|
35
|
+
g.add_edge(generic_node, node_name, fast=True)
|
|
34
36
|
for ps_address in concept.pseudonyms:
|
|
35
37
|
if ps_address not in concept_mapping:
|
|
36
38
|
raise SyntaxError(f"Concept {concept} has invalid pseudonym {ps_address}")
|
|
@@ -44,8 +46,10 @@ def add_concept(
|
|
|
44
46
|
continue
|
|
45
47
|
if pseudonym_node.split("@")[0] == node_name.split("@")[0]:
|
|
46
48
|
continue
|
|
47
|
-
g.add_edge(pseudonym_node, node_name,
|
|
48
|
-
g.add_edge(node_name, pseudonym_node,
|
|
49
|
+
g.add_edge(pseudonym_node, node_name, fast=True)
|
|
50
|
+
g.add_edge(node_name, pseudonym_node, fast=True)
|
|
51
|
+
g.pseudonyms.add((pseudonym_node, node_name))
|
|
52
|
+
g.pseudonyms.add((node_name, pseudonym_node))
|
|
49
53
|
add_concept(pseudonym, g, concept_mapping, default_concept_graph, seen)
|
|
50
54
|
|
|
51
55
|
|
|
@@ -80,20 +84,27 @@ def generate_adhoc_graph(
|
|
|
80
84
|
|
|
81
85
|
for dataset in datasources:
|
|
82
86
|
node = datasource_to_node(dataset)
|
|
83
|
-
g.
|
|
87
|
+
g.add_datasource_node(node, dataset)
|
|
84
88
|
for concept in dataset.concepts:
|
|
89
|
+
cnode = concept_to_node(concept)
|
|
90
|
+
g.concepts[cnode] = concept
|
|
91
|
+
g.add_node(cnode)
|
|
85
92
|
if restrict_to_listed:
|
|
86
|
-
if
|
|
93
|
+
if cnode not in g.nodes:
|
|
87
94
|
continue
|
|
88
|
-
g.add_edge(node,
|
|
89
|
-
g.add_edge(
|
|
95
|
+
g.add_edge(node, cnode, fast=True)
|
|
96
|
+
g.add_edge(cnode, node, fast=True)
|
|
90
97
|
# if there is a key on a table at a different grain
|
|
91
98
|
# add an FK edge to the canonical source, if it exists
|
|
92
99
|
# for example, order ID on order product table
|
|
93
100
|
default = get_default_grain_concept(concept, default_concept_graph)
|
|
101
|
+
|
|
94
102
|
if concept != default:
|
|
95
|
-
|
|
96
|
-
g.
|
|
103
|
+
dcnode = concept_to_node(default)
|
|
104
|
+
g.concepts[dcnode] = default
|
|
105
|
+
g.add_node(dcnode)
|
|
106
|
+
g.add_edge(cnode, dcnode, fast=True)
|
|
107
|
+
g.add_edge(dcnode, cnode, fast=True)
|
|
97
108
|
return g
|
|
98
109
|
|
|
99
110
|
|
|
@@ -169,6 +169,112 @@ def generate_key_concepts(concept: Concept, environment: Environment):
|
|
|
169
169
|
environment.add_concept(new_concept, add_derived=False)
|
|
170
170
|
|
|
171
171
|
|
|
172
|
+
def remove_date_concepts(concept: Concept, environment: Environment):
|
|
173
|
+
"""Remove auto-generated date-related concepts for the given concept"""
|
|
174
|
+
date_suffixes = ["month", "year", "quarter", "day", "day_of_week"]
|
|
175
|
+
grain_suffixes = ["month_start", "year_start"]
|
|
176
|
+
|
|
177
|
+
for suffix in date_suffixes + grain_suffixes:
|
|
178
|
+
address = concept.address + f".{suffix}"
|
|
179
|
+
if address in environment.concepts:
|
|
180
|
+
derived_concept = environment.concepts[address]
|
|
181
|
+
# Only remove if it was auto-derived from this concept
|
|
182
|
+
if (
|
|
183
|
+
derived_concept.metadata
|
|
184
|
+
and derived_concept.metadata.concept_source
|
|
185
|
+
== ConceptSource.AUTO_DERIVED
|
|
186
|
+
and derived_concept.keys
|
|
187
|
+
and concept.address in derived_concept.keys
|
|
188
|
+
):
|
|
189
|
+
environment.remove_concept(address)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def remove_datetime_concepts(concept: Concept, environment: Environment):
|
|
193
|
+
"""Remove auto-generated datetime-related concepts for the given concept"""
|
|
194
|
+
datetime_suffixes = ["date", "hour", "minute", "second"]
|
|
195
|
+
|
|
196
|
+
for suffix in datetime_suffixes:
|
|
197
|
+
address = concept.address + f".{suffix}"
|
|
198
|
+
if address in environment.concepts:
|
|
199
|
+
derived_concept = environment.concepts[address]
|
|
200
|
+
# Only remove if it was auto-derived from this concept
|
|
201
|
+
if (
|
|
202
|
+
derived_concept.metadata
|
|
203
|
+
and derived_concept.metadata.concept_source
|
|
204
|
+
== ConceptSource.AUTO_DERIVED
|
|
205
|
+
and derived_concept.keys
|
|
206
|
+
and concept.address in derived_concept.keys
|
|
207
|
+
):
|
|
208
|
+
environment.remove_concept(address)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def remove_key_concepts(concept: Concept, environment: Environment):
|
|
212
|
+
"""Remove auto-generated key-related concepts for the given concept"""
|
|
213
|
+
key_suffixes = ["count"]
|
|
214
|
+
|
|
215
|
+
for suffix in key_suffixes:
|
|
216
|
+
address = concept.address + f".{suffix}"
|
|
217
|
+
if address in environment.concepts:
|
|
218
|
+
derived_concept = environment.concepts[address]
|
|
219
|
+
if (
|
|
220
|
+
derived_concept.metadata
|
|
221
|
+
and derived_concept.metadata.concept_source
|
|
222
|
+
== ConceptSource.AUTO_DERIVED
|
|
223
|
+
):
|
|
224
|
+
environment.remove_concept(address)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def remove_struct_concepts(concept: Concept, environment: Environment):
|
|
228
|
+
"""Remove auto-generated struct field concepts for the given concept"""
|
|
229
|
+
if not isinstance(concept.datatype, StructType):
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
target_namespace = (
|
|
233
|
+
environment.namespace + "." + concept.name
|
|
234
|
+
if environment.namespace and environment.namespace != DEFAULT_NAMESPACE
|
|
235
|
+
else concept.name
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Get all concepts in the target namespace that were auto-derived
|
|
239
|
+
concepts_to_remove = []
|
|
240
|
+
for address, derived_concept in environment.concepts.items():
|
|
241
|
+
if (
|
|
242
|
+
derived_concept.namespace == target_namespace
|
|
243
|
+
and derived_concept.metadata
|
|
244
|
+
and derived_concept.metadata.concept_source == ConceptSource.AUTO_DERIVED
|
|
245
|
+
and isinstance(derived_concept.lineage, Function)
|
|
246
|
+
and derived_concept.lineage.operator == FunctionType.ATTR_ACCESS
|
|
247
|
+
and len(derived_concept.lineage.arguments) >= 1
|
|
248
|
+
and derived_concept.lineage.arguments[0] == concept.reference
|
|
249
|
+
):
|
|
250
|
+
concepts_to_remove.append(address)
|
|
251
|
+
|
|
252
|
+
for address in concepts_to_remove:
|
|
253
|
+
environment.remove_concept(address)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def remove_related_concepts(concept: Concept, environment: Environment):
|
|
257
|
+
"""Remove all auto-generated concepts that were derived from the given concept"""
|
|
258
|
+
|
|
259
|
+
# Remove key-related concepts
|
|
260
|
+
if concept.purpose == Purpose.KEY:
|
|
261
|
+
remove_key_concepts(concept, environment)
|
|
262
|
+
|
|
263
|
+
# Remove datatype-specific concepts
|
|
264
|
+
if concept.datatype == DataType.DATE:
|
|
265
|
+
remove_date_concepts(concept, environment)
|
|
266
|
+
elif concept.datatype == DataType.DATETIME:
|
|
267
|
+
remove_date_concepts(concept, environment)
|
|
268
|
+
remove_datetime_concepts(concept, environment)
|
|
269
|
+
elif concept.datatype == DataType.TIMESTAMP:
|
|
270
|
+
remove_date_concepts(concept, environment)
|
|
271
|
+
remove_datetime_concepts(concept, environment)
|
|
272
|
+
|
|
273
|
+
# Remove struct field concepts
|
|
274
|
+
if isinstance(concept.datatype, StructType):
|
|
275
|
+
remove_struct_concepts(concept, environment)
|
|
276
|
+
|
|
277
|
+
|
|
172
278
|
def generate_related_concepts(
|
|
173
279
|
concept: Concept,
|
|
174
280
|
environment: Environment,
|
|
@@ -183,6 +289,7 @@ def generate_related_concepts(
|
|
|
183
289
|
if concept.datatype == DataType.DATE and add_derived:
|
|
184
290
|
generate_date_concepts(concept, environment)
|
|
185
291
|
elif concept.datatype == DataType.DATETIME and add_derived:
|
|
292
|
+
|
|
186
293
|
generate_date_concepts(concept, environment)
|
|
187
294
|
generate_datetime_concepts(concept, environment)
|
|
188
295
|
elif concept.datatype == DataType.TIMESTAMP and add_derived:
|
|
@@ -203,6 +310,10 @@ def generate_related_concepts(
|
|
|
203
310
|
),
|
|
204
311
|
lineage=AttrAccess([concept.reference, key], environment=environment),
|
|
205
312
|
grain=concept.grain,
|
|
313
|
+
metadata=Metadata(
|
|
314
|
+
concept_source=ConceptSource.AUTO_DERIVED,
|
|
315
|
+
),
|
|
316
|
+
keys=concept.keys,
|
|
206
317
|
)
|
|
207
318
|
environment.add_concept(auto, meta=meta)
|
|
208
319
|
if isinstance(value, Concept):
|
trilogy/core/exceptions.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import List, Sequence
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class UndefinedConceptException(Exception):
|
|
@@ -24,6 +24,26 @@ class NoDatasourceException(UnresolvableQueryException):
|
|
|
24
24
|
pass
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class ModelValidationError(Exception):
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
message,
|
|
31
|
+
children: Sequence["ModelValidationError"] | None = None,
|
|
32
|
+
**kwargs
|
|
33
|
+
):
|
|
34
|
+
super().__init__(self, message, **kwargs)
|
|
35
|
+
self.message = message
|
|
36
|
+
self.children = children
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DatasourceModelValidationError(ModelValidationError):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ConceptModelValidationError(ModelValidationError):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
27
47
|
class AmbiguousRelationshipResolutionException(UnresolvableQueryException):
|
|
28
48
|
def __init__(self, message, parents: List[set[str]]):
|
|
29
49
|
super().__init__(self, message)
|
trilogy/core/functions.py
CHANGED
|
@@ -380,7 +380,12 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
|
|
|
380
380
|
),
|
|
381
381
|
FunctionType.CURRENT_DATETIME: FunctionConfig(
|
|
382
382
|
output_purpose=Purpose.CONSTANT,
|
|
383
|
-
output_type=DataType.
|
|
383
|
+
output_type=DataType.DATETIME,
|
|
384
|
+
arg_count=0,
|
|
385
|
+
),
|
|
386
|
+
FunctionType.CURRENT_TIMESTAMP: FunctionConfig(
|
|
387
|
+
output_purpose=Purpose.CONSTANT,
|
|
388
|
+
output_type=DataType.TIMESTAMP,
|
|
384
389
|
arg_count=0,
|
|
385
390
|
),
|
|
386
391
|
FunctionType.BOOL: FunctionConfig(
|
trilogy/core/graph_models.py
CHANGED
|
@@ -1,50 +1,48 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
1
3
|
import networkx as nx
|
|
2
4
|
|
|
3
5
|
from trilogy.core.models.build import BuildConcept, BuildDatasource, BuildWhereClause
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
def get_graph_exact_match(
|
|
7
|
-
g: nx.DiGraph,
|
|
9
|
+
g: Union[nx.DiGraph, "ReferenceGraph"],
|
|
10
|
+
accept_partial: bool,
|
|
11
|
+
conditions: BuildWhereClause | None,
|
|
8
12
|
) -> set[str]:
|
|
9
|
-
datasources: dict[str, BuildDatasource | list[BuildDatasource]] = (
|
|
10
|
-
nx.get_node_attributes(g, "datasource")
|
|
11
|
-
)
|
|
12
13
|
exact: set[str] = set()
|
|
13
|
-
for node in g.
|
|
14
|
-
if
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
for node, ds in g.datasources.items():
|
|
15
|
+
if isinstance(ds, list):
|
|
16
|
+
exact.add(node)
|
|
17
|
+
continue
|
|
18
|
+
|
|
19
|
+
if not conditions and not ds.non_partial_for:
|
|
20
|
+
exact.add(node)
|
|
21
|
+
continue
|
|
22
|
+
elif not conditions and accept_partial and ds.non_partial_for:
|
|
23
|
+
exact.add(node)
|
|
24
|
+
continue
|
|
25
|
+
elif conditions:
|
|
26
|
+
if not ds.non_partial_for:
|
|
22
27
|
continue
|
|
23
|
-
|
|
28
|
+
if ds.non_partial_for and conditions == ds.non_partial_for:
|
|
24
29
|
exact.add(node)
|
|
25
30
|
continue
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
continue
|
|
29
|
-
if ds.non_partial_for and conditions == ds.non_partial_for:
|
|
30
|
-
exact.add(node)
|
|
31
|
-
continue
|
|
32
|
-
else:
|
|
33
|
-
continue
|
|
31
|
+
else:
|
|
32
|
+
continue
|
|
34
33
|
|
|
35
34
|
return exact
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
def prune_sources_for_conditions(
|
|
39
|
-
g:
|
|
38
|
+
g: "ReferenceGraph",
|
|
40
39
|
accept_partial: bool,
|
|
41
40
|
conditions: BuildWhereClause | None,
|
|
42
41
|
):
|
|
43
|
-
|
|
44
42
|
complete = get_graph_exact_match(g, accept_partial, conditions)
|
|
45
43
|
to_remove = []
|
|
46
|
-
for node in g.
|
|
47
|
-
if node
|
|
44
|
+
for node in g.datasources:
|
|
45
|
+
if node not in complete:
|
|
48
46
|
to_remove.append(node)
|
|
49
47
|
|
|
50
48
|
for node in to_remove:
|
|
@@ -68,46 +66,41 @@ def datasource_to_node(input: BuildDatasource) -> str:
|
|
|
68
66
|
class ReferenceGraph(nx.DiGraph):
|
|
69
67
|
def __init__(self, *args, **kwargs):
|
|
70
68
|
super().__init__(*args, **kwargs)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
69
|
+
self.concepts: dict[str, BuildConcept] = {}
|
|
70
|
+
self.datasources: dict[str, BuildDatasource] = {}
|
|
71
|
+
self.pseudonyms: set[tuple[str, str]] = set()
|
|
72
|
+
|
|
73
|
+
def copy(self):
|
|
74
|
+
g = ReferenceGraph()
|
|
75
|
+
g.concepts = self.concepts.copy()
|
|
76
|
+
g.datasources = self.datasources.copy()
|
|
77
|
+
g.pseudonyms = {*self.pseudonyms}
|
|
78
|
+
# g.add_nodes_from(self.nodes(data=True))
|
|
79
|
+
for node in self.nodes:
|
|
80
|
+
g.add_node(node, fast=True)
|
|
81
|
+
for edge in self.edges:
|
|
82
|
+
g.add_edge(edge[0], edge[1], fast=True)
|
|
83
|
+
# g.add_edges_from(self.edges(data=True))
|
|
84
|
+
return g
|
|
85
|
+
|
|
86
|
+
def remove_node(self, n):
|
|
87
|
+
if n in self.concepts:
|
|
88
|
+
del self.concepts[n]
|
|
89
|
+
if n in self.datasources:
|
|
90
|
+
del self.datasources[n]
|
|
91
|
+
super().remove_node(n)
|
|
92
|
+
|
|
93
|
+
def add_node(self, node_for_adding, fast: bool = False, **attr):
|
|
94
|
+
if fast:
|
|
95
|
+
return super().add_node(node_for_adding, **attr)
|
|
96
|
+
node_name = node_for_adding
|
|
97
|
+
if attr.get("datasource"):
|
|
98
|
+
self.datasources[node_name] = attr["datasource"]
|
|
89
99
|
super().add_node(node_name, **attr)
|
|
90
100
|
|
|
91
|
-
def
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
elif isinstance(u_of_edge, BuildDatasource):
|
|
98
|
-
orig = u_of_edge
|
|
99
|
-
u_of_edge = datasource_to_node(u_of_edge)
|
|
100
|
-
if u_of_edge not in self.nodes:
|
|
101
|
-
self.add_node(orig)
|
|
102
|
-
|
|
103
|
-
if isinstance(v_of_edge, BuildConcept):
|
|
104
|
-
orig = v_of_edge
|
|
105
|
-
v_of_edge = concept_to_node(v_of_edge)
|
|
106
|
-
if v_of_edge not in self.nodes:
|
|
107
|
-
self.add_node(orig)
|
|
108
|
-
elif isinstance(v_of_edge, BuildDatasource):
|
|
109
|
-
orig = v_of_edge
|
|
110
|
-
v_of_edge = datasource_to_node(v_of_edge)
|
|
111
|
-
if v_of_edge not in self.nodes:
|
|
112
|
-
self.add_node(orig)
|
|
113
|
-
super().add_edge(u_of_edge, v_of_edge, **attr)
|
|
101
|
+
def add_datasource_node(self, node_name, datasource):
|
|
102
|
+
self.datasources[node_name] = datasource
|
|
103
|
+
super().add_node(node_name, datasource=datasource)
|
|
104
|
+
|
|
105
|
+
def add_edge(self, u_of_edge, v_of_edge, fast: bool = False, **attr):
|
|
106
|
+
return super().add_edge(u_of_edge, v_of_edge, **attr)
|
trilogy/core/internal.py
CHANGED
|
@@ -64,4 +64,22 @@ DEFAULT_CONCEPTS = {
|
|
|
64
64
|
granularity=Granularity.SINGLE_ROW,
|
|
65
65
|
derivation=Derivation.CONSTANT,
|
|
66
66
|
),
|
|
67
|
+
"label": Concept(
|
|
68
|
+
name="label",
|
|
69
|
+
namespace=INTERNAL_NAMESPACE,
|
|
70
|
+
datatype=DataType.STRING,
|
|
71
|
+
purpose=Purpose.KEY,
|
|
72
|
+
grain=Grain(),
|
|
73
|
+
granularity=Granularity.SINGLE_ROW,
|
|
74
|
+
derivation=Derivation.CONSTANT,
|
|
75
|
+
),
|
|
76
|
+
"expected": Concept(
|
|
77
|
+
name="expected_value",
|
|
78
|
+
namespace=INTERNAL_NAMESPACE,
|
|
79
|
+
datatype=DataType.STRING,
|
|
80
|
+
purpose=Purpose.KEY,
|
|
81
|
+
grain=Grain(),
|
|
82
|
+
granularity=Granularity.SINGLE_ROW,
|
|
83
|
+
derivation=Derivation.CONSTANT,
|
|
84
|
+
),
|
|
67
85
|
}
|
trilogy/core/models/author.py
CHANGED
|
@@ -902,7 +902,11 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
|
|
|
902
902
|
|
|
903
903
|
@property
|
|
904
904
|
def is_aggregate(self):
|
|
905
|
-
|
|
905
|
+
base = getattr(self, "_is_aggregate", None)
|
|
906
|
+
if base:
|
|
907
|
+
return base
|
|
908
|
+
setattr(self, "_is_aggregate", self.calculate_is_aggregate(self.lineage))
|
|
909
|
+
return self._is_aggregate
|
|
906
910
|
|
|
907
911
|
def with_merge(self, source: Self, target: Self, modifiers: List[Modifier]) -> Self:
|
|
908
912
|
if self.address == source.address:
|
|
@@ -1069,18 +1073,25 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
|
|
|
1069
1073
|
final_grain = grain if not self.grain.components else self.grain
|
|
1070
1074
|
keys = self.keys
|
|
1071
1075
|
|
|
1072
|
-
if self.is_aggregate and isinstance(new_lineage, Function)
|
|
1076
|
+
if self.is_aggregate and grain.components and isinstance(new_lineage, Function):
|
|
1073
1077
|
grain_components: list[ConceptRef | Concept] = [
|
|
1074
1078
|
environment.concepts[c].reference for c in grain.components
|
|
1075
1079
|
]
|
|
1076
|
-
new_lineage = AggregateWrapper(
|
|
1080
|
+
new_lineage = AggregateWrapper.model_construct(
|
|
1081
|
+
function=new_lineage, by=grain_components
|
|
1082
|
+
)
|
|
1077
1083
|
final_grain = grain
|
|
1078
1084
|
keys = set(grain.components)
|
|
1079
|
-
elif
|
|
1085
|
+
elif (
|
|
1086
|
+
grain
|
|
1087
|
+
and new_lineage
|
|
1088
|
+
and isinstance(new_lineage, AggregateWrapper)
|
|
1089
|
+
and not new_lineage.by
|
|
1090
|
+
):
|
|
1080
1091
|
grain_components = [
|
|
1081
1092
|
environment.concepts[c].reference for c in grain.components
|
|
1082
1093
|
]
|
|
1083
|
-
new_lineage = AggregateWrapper(
|
|
1094
|
+
new_lineage = AggregateWrapper.model_construct(
|
|
1084
1095
|
function=new_lineage.function, by=grain_components
|
|
1085
1096
|
)
|
|
1086
1097
|
final_grain = grain
|
|
@@ -1670,15 +1681,6 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1670
1681
|
def datatype(self):
|
|
1671
1682
|
return self.output_datatype
|
|
1672
1683
|
|
|
1673
|
-
@field_validator("output_datatype")
|
|
1674
|
-
@classmethod
|
|
1675
|
-
def parse_output_datatype(cls, v, info: ValidationInfo):
|
|
1676
|
-
values = info.data
|
|
1677
|
-
if values.get("operator") == FunctionType.ATTR_ACCESS:
|
|
1678
|
-
if isinstance(v, StructType):
|
|
1679
|
-
raise SyntaxError
|
|
1680
|
-
return v
|
|
1681
|
-
|
|
1682
1684
|
@field_validator("arguments", mode="before")
|
|
1683
1685
|
@classmethod
|
|
1684
1686
|
def parse_arguments(cls, v, info: ValidationInfo):
|
|
@@ -1845,17 +1847,6 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
|
|
|
1845
1847
|
base += get_concept_arguments(arg)
|
|
1846
1848
|
return base
|
|
1847
1849
|
|
|
1848
|
-
@property
|
|
1849
|
-
def output_grain(self):
|
|
1850
|
-
# aggregates have an abstract grain
|
|
1851
|
-
base_grain = Grain(components=[])
|
|
1852
|
-
if self.operator in FunctionClass.AGGREGATE_FUNCTIONS.value:
|
|
1853
|
-
return base_grain
|
|
1854
|
-
# scalars have implicit grain of all arguments
|
|
1855
|
-
for input in self.concept_arguments:
|
|
1856
|
-
base_grain += input.grain
|
|
1857
|
-
return base_grain
|
|
1858
|
-
|
|
1859
1850
|
|
|
1860
1851
|
class FunctionCallWrapper(
|
|
1861
1852
|
DataTyped,
|
trilogy/core/models/build.py
CHANGED
|
@@ -1533,6 +1533,7 @@ class Factory:
|
|
|
1533
1533
|
)
|
|
1534
1534
|
self.local_non_build_concepts: dict[str, Concept] = {}
|
|
1535
1535
|
self.pseudonym_map = pseudonym_map or get_canonical_pseudonyms(environment)
|
|
1536
|
+
self.build_grain = self.build(self.grain) if self.grain else None
|
|
1536
1537
|
|
|
1537
1538
|
def instantiate_concept(
|
|
1538
1539
|
self,
|
|
@@ -1792,11 +1793,11 @@ class Factory:
|
|
|
1792
1793
|
address = base.concept.address
|
|
1793
1794
|
fetched = (
|
|
1794
1795
|
self._build_concept(
|
|
1795
|
-
self.environment.alias_origin_lookup[address]
|
|
1796
|
-
)
|
|
1796
|
+
self.environment.alias_origin_lookup[address]
|
|
1797
|
+
).with_grain(self.build_grain)
|
|
1797
1798
|
if address in self.environment.alias_origin_lookup
|
|
1798
|
-
else self._build_concept(
|
|
1799
|
-
self.
|
|
1799
|
+
else self._build_concept(self.environment.concepts[address]).with_grain(
|
|
1800
|
+
self.build_grain
|
|
1800
1801
|
)
|
|
1801
1802
|
)
|
|
1802
1803
|
|
trilogy/core/models/core.py
CHANGED
|
@@ -571,6 +571,34 @@ class Environment(BaseModel):
|
|
|
571
571
|
|
|
572
572
|
return concept
|
|
573
573
|
|
|
574
|
+
def remove_concept(
|
|
575
|
+
self,
|
|
576
|
+
concept: Concept | str,
|
|
577
|
+
) -> bool:
|
|
578
|
+
if self.frozen:
|
|
579
|
+
raise FrozenEnvironmentException(
|
|
580
|
+
"Environment is frozen, cannot remove concepts"
|
|
581
|
+
)
|
|
582
|
+
if isinstance(concept, Concept):
|
|
583
|
+
address = concept.address
|
|
584
|
+
c_instance = concept
|
|
585
|
+
else:
|
|
586
|
+
address = concept
|
|
587
|
+
c_instance_check = self.concepts.get(address)
|
|
588
|
+
if not c_instance_check:
|
|
589
|
+
return False
|
|
590
|
+
c_instance = c_instance_check
|
|
591
|
+
from trilogy.core.environment_helpers import remove_related_concepts
|
|
592
|
+
|
|
593
|
+
remove_related_concepts(c_instance, self)
|
|
594
|
+
if address in self.concepts:
|
|
595
|
+
del self.concepts[address]
|
|
596
|
+
return True
|
|
597
|
+
if address in self.alias_origin_lookup:
|
|
598
|
+
del self.alias_origin_lookup[address]
|
|
599
|
+
|
|
600
|
+
return False
|
|
601
|
+
|
|
574
602
|
def add_datasource(
|
|
575
603
|
self,
|
|
576
604
|
datasource: Datasource,
|
trilogy/core/models/execute.py
CHANGED
|
@@ -23,6 +23,7 @@ from trilogy.core.constants import CONSTANT_DATASET
|
|
|
23
23
|
from trilogy.core.enums import (
|
|
24
24
|
ComparisonOperator,
|
|
25
25
|
Derivation,
|
|
26
|
+
FunctionClass,
|
|
26
27
|
FunctionType,
|
|
27
28
|
JoinType,
|
|
28
29
|
Modifier,
|
|
@@ -375,6 +376,12 @@ class CTE(BaseModel):
|
|
|
375
376
|
return check_is_not_in_group(c.lineage.content)
|
|
376
377
|
if c.derivation == Derivation.CONSTANT:
|
|
377
378
|
return True
|
|
379
|
+
if (
|
|
380
|
+
c.purpose == Purpose.CONSTANT
|
|
381
|
+
and isinstance(c.lineage, BuildFunction)
|
|
382
|
+
and c.lineage.operator in FunctionClass.AGGREGATE_FUNCTIONS.value
|
|
383
|
+
):
|
|
384
|
+
return True
|
|
378
385
|
if c.purpose == Purpose.METRIC:
|
|
379
386
|
return True
|
|
380
387
|
|