pytrilogy 0.0.1.102__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.1.102.dist-info/LICENSE.md +19 -0
- pytrilogy-0.0.1.102.dist-info/METADATA +277 -0
- pytrilogy-0.0.1.102.dist-info/RECORD +77 -0
- pytrilogy-0.0.1.102.dist-info/WHEEL +5 -0
- pytrilogy-0.0.1.102.dist-info/entry_points.txt +2 -0
- pytrilogy-0.0.1.102.dist-info/top_level.txt +1 -0
- trilogy/__init__.py +8 -0
- trilogy/compiler.py +0 -0
- trilogy/constants.py +30 -0
- trilogy/core/__init__.py +0 -0
- trilogy/core/constants.py +3 -0
- trilogy/core/enums.py +270 -0
- trilogy/core/env_processor.py +33 -0
- trilogy/core/environment_helpers.py +156 -0
- trilogy/core/ergonomics.py +187 -0
- trilogy/core/exceptions.py +23 -0
- trilogy/core/functions.py +320 -0
- trilogy/core/graph_models.py +55 -0
- trilogy/core/internal.py +37 -0
- trilogy/core/models.py +3145 -0
- trilogy/core/processing/__init__.py +0 -0
- trilogy/core/processing/concept_strategies_v3.py +603 -0
- trilogy/core/processing/graph_utils.py +44 -0
- trilogy/core/processing/node_generators/__init__.py +25 -0
- trilogy/core/processing/node_generators/basic_node.py +71 -0
- trilogy/core/processing/node_generators/common.py +239 -0
- trilogy/core/processing/node_generators/concept_merge.py +152 -0
- trilogy/core/processing/node_generators/filter_node.py +83 -0
- trilogy/core/processing/node_generators/group_node.py +92 -0
- trilogy/core/processing/node_generators/group_to_node.py +99 -0
- trilogy/core/processing/node_generators/merge_node.py +148 -0
- trilogy/core/processing/node_generators/multiselect_node.py +189 -0
- trilogy/core/processing/node_generators/rowset_node.py +130 -0
- trilogy/core/processing/node_generators/select_node.py +328 -0
- trilogy/core/processing/node_generators/unnest_node.py +37 -0
- trilogy/core/processing/node_generators/window_node.py +85 -0
- trilogy/core/processing/nodes/__init__.py +76 -0
- trilogy/core/processing/nodes/base_node.py +251 -0
- trilogy/core/processing/nodes/filter_node.py +49 -0
- trilogy/core/processing/nodes/group_node.py +110 -0
- trilogy/core/processing/nodes/merge_node.py +326 -0
- trilogy/core/processing/nodes/select_node_v2.py +198 -0
- trilogy/core/processing/nodes/unnest_node.py +54 -0
- trilogy/core/processing/nodes/window_node.py +34 -0
- trilogy/core/processing/utility.py +278 -0
- trilogy/core/query_processor.py +331 -0
- trilogy/dialect/__init__.py +0 -0
- trilogy/dialect/base.py +679 -0
- trilogy/dialect/bigquery.py +80 -0
- trilogy/dialect/common.py +43 -0
- trilogy/dialect/config.py +55 -0
- trilogy/dialect/duckdb.py +83 -0
- trilogy/dialect/enums.py +95 -0
- trilogy/dialect/postgres.py +86 -0
- trilogy/dialect/presto.py +82 -0
- trilogy/dialect/snowflake.py +82 -0
- trilogy/dialect/sql_server.py +89 -0
- trilogy/docs/__init__.py +0 -0
- trilogy/engine.py +48 -0
- trilogy/executor.py +242 -0
- trilogy/hooks/__init__.py +0 -0
- trilogy/hooks/base_hook.py +37 -0
- trilogy/hooks/graph_hook.py +24 -0
- trilogy/hooks/query_debugger.py +133 -0
- trilogy/metadata/__init__.py +0 -0
- trilogy/parser.py +10 -0
- trilogy/parsing/__init__.py +0 -0
- trilogy/parsing/common.py +176 -0
- trilogy/parsing/config.py +5 -0
- trilogy/parsing/exceptions.py +2 -0
- trilogy/parsing/helpers.py +1 -0
- trilogy/parsing/parse_engine.py +1951 -0
- trilogy/parsing/render.py +483 -0
- trilogy/py.typed +0 -0
- trilogy/scripts/__init__.py +0 -0
- trilogy/scripts/trilogy.py +127 -0
- trilogy/utility.py +31 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# directly select out a basic derivation
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from trilogy.core.models import (
|
|
5
|
+
Concept,
|
|
6
|
+
)
|
|
7
|
+
from trilogy.core.processing.nodes import StrategyNode, History, MergeNode
|
|
8
|
+
from trilogy.core.processing.node_generators.common import (
|
|
9
|
+
resolve_function_parent_concepts,
|
|
10
|
+
)
|
|
11
|
+
from trilogy.constants import logger
|
|
12
|
+
|
|
13
|
+
LOGGER_PREFIX = "[GEN_BASIC_NODE]"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def gen_basic_node(
|
|
17
|
+
concept: Concept,
|
|
18
|
+
local_optional: List[Concept],
|
|
19
|
+
environment,
|
|
20
|
+
g,
|
|
21
|
+
depth: int,
|
|
22
|
+
source_concepts,
|
|
23
|
+
history: History | None = None,
|
|
24
|
+
):
|
|
25
|
+
depth_prefix = "\t" * depth
|
|
26
|
+
parent_concepts = resolve_function_parent_concepts(concept)
|
|
27
|
+
|
|
28
|
+
logger.info(
|
|
29
|
+
f"{depth_prefix}{LOGGER_PREFIX} basic node for {concept} has parents {[x.address for x in parent_concepts]}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
output_concepts = [concept] + local_optional
|
|
33
|
+
partials = []
|
|
34
|
+
|
|
35
|
+
attempts = [(parent_concepts, [concept])]
|
|
36
|
+
if local_optional:
|
|
37
|
+
attempts.append((parent_concepts + local_optional, local_optional + [concept]))
|
|
38
|
+
|
|
39
|
+
for attempt, output in reversed(attempts):
|
|
40
|
+
parent_node = source_concepts(
|
|
41
|
+
mandatory_list=attempt,
|
|
42
|
+
environment=environment,
|
|
43
|
+
g=g,
|
|
44
|
+
depth=depth + 1,
|
|
45
|
+
history=history,
|
|
46
|
+
)
|
|
47
|
+
if not parent_node:
|
|
48
|
+
continue
|
|
49
|
+
parents: List[StrategyNode] = [parent_node]
|
|
50
|
+
for x in output_concepts:
|
|
51
|
+
sources = [p for p in parents if x in p.output_concepts]
|
|
52
|
+
if not sources:
|
|
53
|
+
continue
|
|
54
|
+
if all(x in source.partial_concepts for source in sources):
|
|
55
|
+
partials.append(x)
|
|
56
|
+
logger.info(
|
|
57
|
+
f"{depth_prefix}{LOGGER_PREFIX} Returning basic select for {concept} with attempted extra {[x.address for x in attempt]}"
|
|
58
|
+
)
|
|
59
|
+
return MergeNode(
|
|
60
|
+
input_concepts=attempt,
|
|
61
|
+
output_concepts=output,
|
|
62
|
+
environment=environment,
|
|
63
|
+
g=g,
|
|
64
|
+
parents=parents,
|
|
65
|
+
depth=depth,
|
|
66
|
+
partial_concepts=partials,
|
|
67
|
+
)
|
|
68
|
+
logger.info(
|
|
69
|
+
f"{depth_prefix}{LOGGER_PREFIX} No basic node could be generated for {concept}"
|
|
70
|
+
)
|
|
71
|
+
return None
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from typing import List, Tuple
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from trilogy.core.enums import PurposeLineage, Purpose
|
|
5
|
+
from trilogy.core.models import (
|
|
6
|
+
Concept,
|
|
7
|
+
Function,
|
|
8
|
+
AggregateWrapper,
|
|
9
|
+
FilterItem,
|
|
10
|
+
Environment,
|
|
11
|
+
LooseConceptList,
|
|
12
|
+
)
|
|
13
|
+
from trilogy.utility import unique
|
|
14
|
+
from trilogy.core.processing.nodes.base_node import StrategyNode
|
|
15
|
+
from trilogy.core.processing.nodes.merge_node import MergeNode
|
|
16
|
+
from trilogy.core.processing.nodes import History
|
|
17
|
+
from trilogy.core.enums import JoinType
|
|
18
|
+
from trilogy.core.processing.nodes import (
|
|
19
|
+
NodeJoin,
|
|
20
|
+
)
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
from trilogy.core.processing.utility import concept_to_relevant_joins
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def resolve_function_parent_concepts(concept: Concept) -> List[Concept]:
|
|
26
|
+
if not isinstance(concept.lineage, (Function, AggregateWrapper)):
|
|
27
|
+
raise ValueError(f"Concept {concept} lineage is not function or aggregate")
|
|
28
|
+
if concept.derivation == PurposeLineage.AGGREGATE:
|
|
29
|
+
if not concept.grain.abstract:
|
|
30
|
+
base = concept.lineage.concept_arguments + concept.grain.components_copy
|
|
31
|
+
# if the base concept being aggregated is a property with a key
|
|
32
|
+
# keep the key as a parent
|
|
33
|
+
else:
|
|
34
|
+
base = concept.lineage.concept_arguments
|
|
35
|
+
if isinstance(concept.lineage, AggregateWrapper):
|
|
36
|
+
# for aggregate wrapper, don't include the by
|
|
37
|
+
extra_property_grain = concept.lineage.function.concept_arguments
|
|
38
|
+
else:
|
|
39
|
+
extra_property_grain = concept.lineage.concept_arguments
|
|
40
|
+
for x in extra_property_grain:
|
|
41
|
+
if isinstance(x, Concept) and x.purpose == Purpose.PROPERTY and x.keys:
|
|
42
|
+
base += x.keys
|
|
43
|
+
return unique(base, "address")
|
|
44
|
+
# TODO: handle basic lineage chains?
|
|
45
|
+
return unique(concept.lineage.concept_arguments, "address")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def resolve_filter_parent_concepts(concept: Concept) -> Tuple[Concept, List[Concept]]:
|
|
49
|
+
if not isinstance(concept.lineage, FilterItem):
|
|
50
|
+
raise ValueError
|
|
51
|
+
direct_parent = concept.lineage.content
|
|
52
|
+
base = [direct_parent]
|
|
53
|
+
base += concept.lineage.where.concept_arguments
|
|
54
|
+
if direct_parent.grain:
|
|
55
|
+
base += direct_parent.grain.components_copy
|
|
56
|
+
if (
|
|
57
|
+
isinstance(direct_parent, Concept)
|
|
58
|
+
and direct_parent.purpose == Purpose.PROPERTY
|
|
59
|
+
and direct_parent.keys
|
|
60
|
+
):
|
|
61
|
+
base += direct_parent.keys
|
|
62
|
+
return concept.lineage.content, unique(base, "address")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def gen_property_enrichment_node(
|
|
66
|
+
base_node: StrategyNode,
|
|
67
|
+
extra_properties: list[Concept],
|
|
68
|
+
environment: Environment,
|
|
69
|
+
g,
|
|
70
|
+
depth: int,
|
|
71
|
+
source_concepts,
|
|
72
|
+
history: History | None = None,
|
|
73
|
+
):
|
|
74
|
+
required_keys: dict[str, set[str]] = defaultdict(set)
|
|
75
|
+
for x in extra_properties:
|
|
76
|
+
if not x.keys:
|
|
77
|
+
raise SyntaxError(f"Property {x.address} missing keys in lookup")
|
|
78
|
+
keys = "-".join([y.address for y in x.keys])
|
|
79
|
+
required_keys[keys].add(x.address)
|
|
80
|
+
final_nodes = []
|
|
81
|
+
node_joins = []
|
|
82
|
+
for _k, vs in required_keys.items():
|
|
83
|
+
ks = _k.split("-")
|
|
84
|
+
enrich_node: StrategyNode = source_concepts(
|
|
85
|
+
mandatory_list=[environment.concepts[k] for k in ks]
|
|
86
|
+
+ [environment.concepts[v] for v in vs],
|
|
87
|
+
environment=environment,
|
|
88
|
+
g=g,
|
|
89
|
+
depth=depth + 1,
|
|
90
|
+
history=history,
|
|
91
|
+
)
|
|
92
|
+
final_nodes.append(enrich_node)
|
|
93
|
+
node_joins.append(
|
|
94
|
+
NodeJoin(
|
|
95
|
+
left_node=enrich_node,
|
|
96
|
+
right_node=base_node,
|
|
97
|
+
concepts=concept_to_relevant_joins(
|
|
98
|
+
[environment.concepts[k] for k in ks]
|
|
99
|
+
),
|
|
100
|
+
filter_to_mutual=False,
|
|
101
|
+
join_type=JoinType.LEFT_OUTER,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
return MergeNode(
|
|
105
|
+
input_concepts=unique(
|
|
106
|
+
base_node.output_concepts
|
|
107
|
+
+ extra_properties
|
|
108
|
+
+ [
|
|
109
|
+
environment.concepts[v]
|
|
110
|
+
for k, values in required_keys.items()
|
|
111
|
+
for v in values
|
|
112
|
+
],
|
|
113
|
+
"address",
|
|
114
|
+
),
|
|
115
|
+
output_concepts=base_node.output_concepts + extra_properties,
|
|
116
|
+
environment=environment,
|
|
117
|
+
g=g,
|
|
118
|
+
parents=[
|
|
119
|
+
base_node,
|
|
120
|
+
enrich_node,
|
|
121
|
+
],
|
|
122
|
+
node_joins=node_joins,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def gen_enrichment_node(
|
|
127
|
+
base_node: StrategyNode,
|
|
128
|
+
join_keys: List[Concept],
|
|
129
|
+
local_optional: list[Concept],
|
|
130
|
+
environment: Environment,
|
|
131
|
+
g,
|
|
132
|
+
depth: int,
|
|
133
|
+
source_concepts,
|
|
134
|
+
log_lambda,
|
|
135
|
+
history: History | None = None,
|
|
136
|
+
):
|
|
137
|
+
|
|
138
|
+
local_opts = LooseConceptList(concepts=local_optional)
|
|
139
|
+
|
|
140
|
+
if local_opts.issubset(LooseConceptList(concepts=base_node.output_concepts)):
|
|
141
|
+
log_lambda(
|
|
142
|
+
f"{str(type(base_node).__name__)} has all optional { base_node.output_lcl}, skipping enrichmennt"
|
|
143
|
+
)
|
|
144
|
+
return base_node
|
|
145
|
+
extra_required = [
|
|
146
|
+
x
|
|
147
|
+
for x in local_opts
|
|
148
|
+
if x not in base_node.output_lcl or x in base_node.partial_lcl
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
# property lookup optimization
|
|
152
|
+
# this helps when evaluating a normalized star schema as you only want to lookup the missing properties based on the relevant keys
|
|
153
|
+
if all([x.purpose == Purpose.PROPERTY for x in extra_required]):
|
|
154
|
+
if all(
|
|
155
|
+
x.keys and all([key in base_node.output_lcl for key in x.keys])
|
|
156
|
+
for x in extra_required
|
|
157
|
+
):
|
|
158
|
+
log_lambda(
|
|
159
|
+
f"{str(type(base_node).__name__)} returning property optimized enrichment node"
|
|
160
|
+
)
|
|
161
|
+
return gen_property_enrichment_node(
|
|
162
|
+
base_node,
|
|
163
|
+
extra_required,
|
|
164
|
+
environment,
|
|
165
|
+
g,
|
|
166
|
+
depth,
|
|
167
|
+
source_concepts,
|
|
168
|
+
history=history,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
enrich_node: StrategyNode = source_concepts( # this fetches the parent + join keys
|
|
172
|
+
# to then connect to the rest of the query
|
|
173
|
+
mandatory_list=join_keys + extra_required,
|
|
174
|
+
environment=environment,
|
|
175
|
+
g=g,
|
|
176
|
+
depth=depth,
|
|
177
|
+
history=history,
|
|
178
|
+
)
|
|
179
|
+
if not enrich_node:
|
|
180
|
+
log_lambda(
|
|
181
|
+
f"{str(type(base_node).__name__)} enrichment node unresolvable, returning just group node"
|
|
182
|
+
)
|
|
183
|
+
return base_node
|
|
184
|
+
log_lambda(
|
|
185
|
+
f"{str(type(base_node).__name__)} returning merge node with group node + enrichment node"
|
|
186
|
+
)
|
|
187
|
+
return MergeNode(
|
|
188
|
+
input_concepts=unique(
|
|
189
|
+
join_keys + extra_required + base_node.output_concepts, "address"
|
|
190
|
+
),
|
|
191
|
+
output_concepts=unique(
|
|
192
|
+
join_keys + extra_required + base_node.output_concepts, "address"
|
|
193
|
+
),
|
|
194
|
+
environment=environment,
|
|
195
|
+
g=g,
|
|
196
|
+
parents=[enrich_node, base_node],
|
|
197
|
+
node_joins=[
|
|
198
|
+
NodeJoin(
|
|
199
|
+
left_node=enrich_node,
|
|
200
|
+
right_node=base_node,
|
|
201
|
+
concepts=concept_to_relevant_joins(
|
|
202
|
+
[x for x in join_keys if x in enrich_node.output_lcl]
|
|
203
|
+
),
|
|
204
|
+
filter_to_mutual=False,
|
|
205
|
+
join_type=JoinType.LEFT_OUTER,
|
|
206
|
+
)
|
|
207
|
+
],
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def resolve_join_order(joins: List[NodeJoin]) -> List[NodeJoin]:
|
|
212
|
+
available_aliases: set[StrategyNode] = set()
|
|
213
|
+
final_joins_pre = [*joins]
|
|
214
|
+
final_joins = []
|
|
215
|
+
while final_joins_pre:
|
|
216
|
+
new_final_joins_pre: List[NodeJoin] = []
|
|
217
|
+
for join in final_joins_pre:
|
|
218
|
+
if not available_aliases:
|
|
219
|
+
final_joins.append(join)
|
|
220
|
+
available_aliases.add(join.left_node)
|
|
221
|
+
available_aliases.add(join.right_node)
|
|
222
|
+
elif join.left_node in available_aliases:
|
|
223
|
+
# we don't need to join twice
|
|
224
|
+
# so whatever join we found first, works
|
|
225
|
+
if join.right_node in available_aliases:
|
|
226
|
+
continue
|
|
227
|
+
final_joins.append(join)
|
|
228
|
+
available_aliases.add(join.left_node)
|
|
229
|
+
available_aliases.add(join.right_node)
|
|
230
|
+
else:
|
|
231
|
+
new_final_joins_pre.append(join)
|
|
232
|
+
if len(new_final_joins_pre) == len(final_joins_pre):
|
|
233
|
+
remaining = [join.left_node for join in new_final_joins_pre]
|
|
234
|
+
remaining_right = [join.right_node for join in new_final_joins_pre]
|
|
235
|
+
raise SyntaxError(
|
|
236
|
+
f"did not find any new joins, available {available_aliases} remaining is {remaining + remaining_right} "
|
|
237
|
+
)
|
|
238
|
+
final_joins_pre = new_final_joins_pre
|
|
239
|
+
return final_joins
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from trilogy.core.models import (
|
|
2
|
+
Concept,
|
|
3
|
+
Environment,
|
|
4
|
+
MergeStatement,
|
|
5
|
+
)
|
|
6
|
+
from trilogy.core.processing.nodes import MergeNode, NodeJoin, History
|
|
7
|
+
from trilogy.core.processing.nodes.base_node import concept_list_to_grain, StrategyNode
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
from trilogy.core.enums import JoinType
|
|
11
|
+
from trilogy.constants import logger
|
|
12
|
+
from trilogy.core.processing.utility import padding
|
|
13
|
+
from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
|
|
14
|
+
from itertools import combinations
|
|
15
|
+
from trilogy.core.processing.node_generators.common import resolve_join_order
|
|
16
|
+
|
|
17
|
+
LOGGER_PREFIX = "[GEN_CONCEPT_MERGE_NODE]"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def merge_joins(base: MergeStatement, parents: List[StrategyNode]) -> List[NodeJoin]:
|
|
21
|
+
output = []
|
|
22
|
+
for left, right in combinations(parents, 2):
|
|
23
|
+
output.append(
|
|
24
|
+
NodeJoin(
|
|
25
|
+
left_node=left,
|
|
26
|
+
right_node=right,
|
|
27
|
+
concepts=[
|
|
28
|
+
base.merge_concept,
|
|
29
|
+
],
|
|
30
|
+
join_type=JoinType.FULL,
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
return resolve_join_order(output)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def gen_concept_merge_node(
|
|
37
|
+
concept: Concept,
|
|
38
|
+
local_optional: List[Concept],
|
|
39
|
+
environment: Environment,
|
|
40
|
+
g,
|
|
41
|
+
depth: int,
|
|
42
|
+
source_concepts,
|
|
43
|
+
history: History | None = None,
|
|
44
|
+
) -> MergeNode | None:
|
|
45
|
+
if not isinstance(concept.lineage, MergeStatement):
|
|
46
|
+
logger.info(
|
|
47
|
+
f"{padding(depth)}{LOGGER_PREFIX} Cannot generate merge node for {concept}"
|
|
48
|
+
)
|
|
49
|
+
return None
|
|
50
|
+
lineage: MergeStatement = concept.lineage
|
|
51
|
+
|
|
52
|
+
base_parents: List[StrategyNode] = []
|
|
53
|
+
for select in lineage.concepts:
|
|
54
|
+
# if it's a merge concept, filter it out of the optional
|
|
55
|
+
sub_optional = [
|
|
56
|
+
x
|
|
57
|
+
for x in local_optional
|
|
58
|
+
if x.address not in lineage.concepts_lcl and x.namespace == select.namespace
|
|
59
|
+
]
|
|
60
|
+
snode: StrategyNode = source_concepts(
|
|
61
|
+
mandatory_list=[select] + sub_optional,
|
|
62
|
+
environment=environment,
|
|
63
|
+
g=g,
|
|
64
|
+
depth=depth + 1,
|
|
65
|
+
history=history,
|
|
66
|
+
)
|
|
67
|
+
if not snode:
|
|
68
|
+
logger.info(
|
|
69
|
+
f"{padding(depth)}{LOGGER_PREFIX} Cannot generate merge node for {concept}"
|
|
70
|
+
)
|
|
71
|
+
return None
|
|
72
|
+
snode.add_output_concept(lineage.merge_concept)
|
|
73
|
+
base_parents.append(snode)
|
|
74
|
+
|
|
75
|
+
node_joins = merge_joins(lineage, base_parents)
|
|
76
|
+
|
|
77
|
+
enrichment = set([x.address for x in local_optional])
|
|
78
|
+
outputs = [x for y in base_parents for x in y.output_concepts]
|
|
79
|
+
|
|
80
|
+
additional_relevant = [x for x in outputs if x.address in enrichment]
|
|
81
|
+
node = MergeNode(
|
|
82
|
+
input_concepts=[x for y in base_parents for x in y.output_concepts],
|
|
83
|
+
output_concepts=outputs + additional_relevant + [concept],
|
|
84
|
+
environment=environment,
|
|
85
|
+
g=g,
|
|
86
|
+
depth=depth,
|
|
87
|
+
parents=base_parents,
|
|
88
|
+
node_joins=node_joins,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
qds = node.rebuild_cache()
|
|
92
|
+
|
|
93
|
+
# assume grain to be outoput of select
|
|
94
|
+
# but don't include anything aggregate at this point
|
|
95
|
+
qds.grain = concept_list_to_grain(
|
|
96
|
+
node.output_concepts, parent_sources=qds.datasources
|
|
97
|
+
)
|
|
98
|
+
possible_joins = concept_to_relevant_joins(additional_relevant)
|
|
99
|
+
if not local_optional:
|
|
100
|
+
logger.info(
|
|
101
|
+
f"{padding(depth)}{LOGGER_PREFIX} no enriched required for merge concept node; exiting early"
|
|
102
|
+
)
|
|
103
|
+
return node
|
|
104
|
+
if not possible_joins:
|
|
105
|
+
logger.info(
|
|
106
|
+
f"{padding(depth)}{LOGGER_PREFIX} no possible joins for merge concept node; exiting early"
|
|
107
|
+
)
|
|
108
|
+
return node
|
|
109
|
+
if all(
|
|
110
|
+
[x.address in [y.address for y in node.output_concepts] for x in local_optional]
|
|
111
|
+
):
|
|
112
|
+
logger.info(
|
|
113
|
+
f"{padding(depth)}{LOGGER_PREFIX} all enriched concepts returned from base merge concept node; exiting early"
|
|
114
|
+
)
|
|
115
|
+
return node
|
|
116
|
+
enrich_node: MergeNode = source_concepts( # this fetches the parent + join keys
|
|
117
|
+
# to then connect to the rest of the query
|
|
118
|
+
mandatory_list=additional_relevant + local_optional,
|
|
119
|
+
environment=environment,
|
|
120
|
+
g=g,
|
|
121
|
+
depth=depth + 1,
|
|
122
|
+
history=history,
|
|
123
|
+
)
|
|
124
|
+
if not enrich_node:
|
|
125
|
+
logger.info(
|
|
126
|
+
f"{padding(depth)}{LOGGER_PREFIX} Cannot generate merge concept enrichment node for {concept} with optional {local_optional}, returning just merge concept"
|
|
127
|
+
)
|
|
128
|
+
return node
|
|
129
|
+
|
|
130
|
+
return MergeNode(
|
|
131
|
+
input_concepts=enrich_node.output_concepts + node.output_concepts,
|
|
132
|
+
output_concepts=node.output_concepts + local_optional,
|
|
133
|
+
environment=environment,
|
|
134
|
+
g=g,
|
|
135
|
+
depth=depth,
|
|
136
|
+
parents=[
|
|
137
|
+
# this node gets the window
|
|
138
|
+
node,
|
|
139
|
+
# this node gets enrichment
|
|
140
|
+
enrich_node,
|
|
141
|
+
],
|
|
142
|
+
node_joins=[
|
|
143
|
+
NodeJoin(
|
|
144
|
+
left_node=enrich_node,
|
|
145
|
+
right_node=node,
|
|
146
|
+
concepts=possible_joins,
|
|
147
|
+
filter_to_mutual=False,
|
|
148
|
+
join_type=JoinType.LEFT_OUTER,
|
|
149
|
+
)
|
|
150
|
+
],
|
|
151
|
+
partial_concepts=node.partial_concepts,
|
|
152
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from trilogy.core.enums import JoinType
|
|
5
|
+
from trilogy.core.models import (
|
|
6
|
+
Concept,
|
|
7
|
+
Environment,
|
|
8
|
+
)
|
|
9
|
+
from trilogy.core.processing.nodes import FilterNode, MergeNode, NodeJoin, History
|
|
10
|
+
from trilogy.core.processing.node_generators.common import (
|
|
11
|
+
resolve_filter_parent_concepts,
|
|
12
|
+
)
|
|
13
|
+
from trilogy.constants import logger
|
|
14
|
+
from trilogy.core.processing.utility import padding
|
|
15
|
+
from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
|
|
16
|
+
|
|
17
|
+
LOGGER_PREFIX = "[GEN_FILTER_NODE]"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def gen_filter_node(
|
|
21
|
+
concept: Concept,
|
|
22
|
+
local_optional: List[Concept],
|
|
23
|
+
environment: Environment,
|
|
24
|
+
g,
|
|
25
|
+
depth: int,
|
|
26
|
+
source_concepts,
|
|
27
|
+
history: History | None = None,
|
|
28
|
+
) -> MergeNode | FilterNode | None:
|
|
29
|
+
immediate_parent, parent_concepts = resolve_filter_parent_concepts(concept)
|
|
30
|
+
|
|
31
|
+
logger.info(f"{padding(depth)}{LOGGER_PREFIX} fetching filter node parents")
|
|
32
|
+
parent = source_concepts(
|
|
33
|
+
mandatory_list=parent_concepts,
|
|
34
|
+
environment=environment,
|
|
35
|
+
g=g,
|
|
36
|
+
depth=depth + 1,
|
|
37
|
+
history=history,
|
|
38
|
+
)
|
|
39
|
+
if not parent:
|
|
40
|
+
return None
|
|
41
|
+
filter_node = FilterNode(
|
|
42
|
+
input_concepts=[immediate_parent] + parent_concepts,
|
|
43
|
+
output_concepts=[concept, immediate_parent] + parent_concepts,
|
|
44
|
+
environment=environment,
|
|
45
|
+
g=g,
|
|
46
|
+
parents=[parent],
|
|
47
|
+
)
|
|
48
|
+
if not local_optional:
|
|
49
|
+
return filter_node
|
|
50
|
+
enrich_node = source_concepts( # this fetches the parent + join keys
|
|
51
|
+
# to then connect to the rest of the query
|
|
52
|
+
mandatory_list=[immediate_parent] + parent_concepts + local_optional,
|
|
53
|
+
environment=environment,
|
|
54
|
+
g=g,
|
|
55
|
+
depth=depth + 1,
|
|
56
|
+
history=history,
|
|
57
|
+
)
|
|
58
|
+
x = MergeNode(
|
|
59
|
+
input_concepts=[concept, immediate_parent] + local_optional,
|
|
60
|
+
output_concepts=[
|
|
61
|
+
concept,
|
|
62
|
+
]
|
|
63
|
+
+ local_optional,
|
|
64
|
+
environment=environment,
|
|
65
|
+
g=g,
|
|
66
|
+
parents=[
|
|
67
|
+
# this node fetches only what we need to filter
|
|
68
|
+
filter_node,
|
|
69
|
+
enrich_node,
|
|
70
|
+
],
|
|
71
|
+
node_joins=[
|
|
72
|
+
NodeJoin(
|
|
73
|
+
left_node=enrich_node,
|
|
74
|
+
right_node=filter_node,
|
|
75
|
+
concepts=concept_to_relevant_joins(
|
|
76
|
+
[immediate_parent] + parent_concepts
|
|
77
|
+
),
|
|
78
|
+
join_type=JoinType.LEFT_OUTER,
|
|
79
|
+
filter_to_mutual=False,
|
|
80
|
+
)
|
|
81
|
+
],
|
|
82
|
+
)
|
|
83
|
+
return x
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from trilogy.core.models import Concept, Environment, LooseConceptList
|
|
2
|
+
from trilogy.utility import unique
|
|
3
|
+
from trilogy.core.processing.nodes import GroupNode, StrategyNode, History
|
|
4
|
+
from typing import List
|
|
5
|
+
from trilogy.core.processing.node_generators.common import (
|
|
6
|
+
resolve_function_parent_concepts,
|
|
7
|
+
)
|
|
8
|
+
from trilogy.constants import logger
|
|
9
|
+
from trilogy.core.processing.utility import padding, create_log_lambda
|
|
10
|
+
from trilogy.core.processing.node_generators.common import (
|
|
11
|
+
gen_enrichment_node,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
LOGGER_PREFIX = "[GEN_GROUP_NODE]"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def gen_group_node(
|
|
18
|
+
concept: Concept,
|
|
19
|
+
local_optional: List[Concept],
|
|
20
|
+
environment: Environment,
|
|
21
|
+
g,
|
|
22
|
+
depth: int,
|
|
23
|
+
source_concepts,
|
|
24
|
+
history: History | None = None,
|
|
25
|
+
):
|
|
26
|
+
# aggregates MUST always group to the proper grain
|
|
27
|
+
# except when the
|
|
28
|
+
parent_concepts: List[Concept] = unique(
|
|
29
|
+
resolve_function_parent_concepts(concept), "address"
|
|
30
|
+
)
|
|
31
|
+
logger.info(
|
|
32
|
+
f"{padding(depth)}{LOGGER_PREFIX} parent_concepts are {[x.address for x in parent_concepts]} from group grain {concept.grain}"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# if the aggregation has a grain, we need to ensure these are the ONLY optional in the output of the select
|
|
36
|
+
output_concepts = [concept]
|
|
37
|
+
|
|
38
|
+
if concept.grain and len(concept.grain.components_copy) > 0:
|
|
39
|
+
grain_components = (
|
|
40
|
+
concept.grain.components_copy if not concept.grain.abstract else []
|
|
41
|
+
)
|
|
42
|
+
parent_concepts += grain_components
|
|
43
|
+
output_concepts += grain_components
|
|
44
|
+
|
|
45
|
+
if parent_concepts:
|
|
46
|
+
logger.info(
|
|
47
|
+
f"{padding(depth)}{LOGGER_PREFIX} fetching group node parents {LooseConceptList(concepts=parent_concepts)}"
|
|
48
|
+
)
|
|
49
|
+
parent_concepts = unique(parent_concepts, "address")
|
|
50
|
+
parent = source_concepts(
|
|
51
|
+
mandatory_list=parent_concepts,
|
|
52
|
+
environment=environment,
|
|
53
|
+
g=g,
|
|
54
|
+
depth=depth,
|
|
55
|
+
history=history,
|
|
56
|
+
)
|
|
57
|
+
if not parent:
|
|
58
|
+
logger.info(
|
|
59
|
+
f"{padding(depth)}{LOGGER_PREFIX} group by node parents unresolvable"
|
|
60
|
+
)
|
|
61
|
+
return None
|
|
62
|
+
parents: List[StrategyNode] = [parent]
|
|
63
|
+
else:
|
|
64
|
+
parents = []
|
|
65
|
+
|
|
66
|
+
# the keys we group by
|
|
67
|
+
# are what we can use for enrichment
|
|
68
|
+
group_key_parents = concept.grain.components_copy
|
|
69
|
+
|
|
70
|
+
group_node = GroupNode(
|
|
71
|
+
output_concepts=output_concepts,
|
|
72
|
+
input_concepts=parent_concepts,
|
|
73
|
+
environment=environment,
|
|
74
|
+
g=g,
|
|
75
|
+
parents=parents,
|
|
76
|
+
depth=depth,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# early exit if no optional
|
|
80
|
+
if not local_optional:
|
|
81
|
+
return group_node
|
|
82
|
+
logger.info(f"{padding(depth)}{LOGGER_PREFIX} group node requires enrichment")
|
|
83
|
+
return gen_enrichment_node(
|
|
84
|
+
group_node,
|
|
85
|
+
join_keys=group_key_parents,
|
|
86
|
+
local_optional=local_optional,
|
|
87
|
+
environment=environment,
|
|
88
|
+
g=g,
|
|
89
|
+
depth=depth,
|
|
90
|
+
source_concepts=source_concepts,
|
|
91
|
+
log_lambda=create_log_lambda(LOGGER_PREFIX, depth, logger),
|
|
92
|
+
)
|