pytrilogy 0.0.1.109__py3-none-any.whl → 0.0.1.111__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.109.dist-info → pytrilogy-0.0.1.111.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.1.109.dist-info → pytrilogy-0.0.1.111.dist-info}/RECORD +34 -34
- {pytrilogy-0.0.1.109.dist-info → pytrilogy-0.0.1.111.dist-info}/WHEEL +1 -1
- trilogy/__init__.py +1 -1
- trilogy/constants.py +11 -3
- trilogy/core/enums.py +1 -0
- trilogy/core/models.py +94 -67
- trilogy/core/optimization.py +134 -12
- trilogy/core/processing/concept_strategies_v3.py +44 -19
- trilogy/core/processing/node_generators/basic_node.py +2 -0
- trilogy/core/processing/node_generators/common.py +3 -1
- trilogy/core/processing/node_generators/concept_merge_node.py +24 -8
- trilogy/core/processing/node_generators/filter_node.py +36 -6
- trilogy/core/processing/node_generators/node_merge_node.py +34 -23
- trilogy/core/processing/node_generators/rowset_node.py +37 -8
- trilogy/core/processing/node_generators/select_node.py +23 -9
- trilogy/core/processing/node_generators/unnest_node.py +24 -3
- trilogy/core/processing/node_generators/window_node.py +4 -2
- trilogy/core/processing/nodes/__init__.py +7 -6
- trilogy/core/processing/nodes/base_node.py +40 -6
- trilogy/core/processing/nodes/filter_node.py +15 -1
- trilogy/core/processing/nodes/group_node.py +20 -1
- trilogy/core/processing/nodes/merge_node.py +37 -10
- trilogy/core/processing/nodes/select_node_v2.py +34 -39
- trilogy/core/processing/nodes/unnest_node.py +12 -0
- trilogy/core/processing/nodes/window_node.py +11 -0
- trilogy/core/processing/utility.py +0 -14
- trilogy/core/query_processor.py +125 -29
- trilogy/dialect/base.py +45 -40
- trilogy/executor.py +31 -3
- trilogy/parsing/parse_engine.py +49 -17
- {pytrilogy-0.0.1.109.dist-info → pytrilogy-0.0.1.111.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.1.109.dist-info → pytrilogy-0.0.1.111.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.1.109.dist-info → pytrilogy-0.0.1.111.dist-info}/top_level.txt +0 -0
|
@@ -10,6 +10,7 @@ from trilogy.utility import unique
|
|
|
10
10
|
from trilogy.core.exceptions import AmbiguousRelationshipResolutionException
|
|
11
11
|
from trilogy.core.processing.utility import padding
|
|
12
12
|
from trilogy.core.processing.graph_utils import extract_mandatory_subgraphs
|
|
13
|
+
from trilogy.core.enums import PurposeLineage
|
|
13
14
|
|
|
14
15
|
LOGGER_PREFIX = "[GEN_MERGE_NODE]"
|
|
15
16
|
|
|
@@ -65,13 +66,13 @@ def identify_ds_join_paths(
|
|
|
65
66
|
]
|
|
66
67
|
if partial and not accept_partial:
|
|
67
68
|
return None
|
|
68
|
-
|
|
69
|
+
|
|
69
70
|
return PathInfo(
|
|
70
71
|
paths=paths,
|
|
71
72
|
datasource=datasource,
|
|
72
73
|
reduced_concepts=reduce_path_concepts(paths, g),
|
|
73
74
|
concept_subgraphs=extract_mandatory_subgraphs(paths, g),
|
|
74
|
-
)
|
|
75
|
+
)
|
|
75
76
|
return None
|
|
76
77
|
|
|
77
78
|
|
|
@@ -88,14 +89,7 @@ def gen_merge_node(
|
|
|
88
89
|
join_candidates: List[PathInfo] = []
|
|
89
90
|
# anchor on datasources
|
|
90
91
|
final_all_concepts = []
|
|
91
|
-
# implicit_upstream = {}
|
|
92
92
|
for x in all_concepts:
|
|
93
|
-
# if x.derivation in (PurposeLineage.AGGREGATE, PurposeLineage.BASIC):
|
|
94
|
-
# final_all_concepts +=resolve_function_parent_concepts(x)
|
|
95
|
-
# elif x.derivation == PurposeLineage.FILTER:
|
|
96
|
-
# final_all_concepts +=resolve_filter_parent_concepts(x)
|
|
97
|
-
# else:
|
|
98
|
-
# final_all_concepts.append(x)
|
|
99
93
|
final_all_concepts.append(x)
|
|
100
94
|
for datasource in environment.datasources.values():
|
|
101
95
|
path = identify_ds_join_paths(final_all_concepts, g, datasource, accept_partial)
|
|
@@ -104,18 +98,25 @@ def gen_merge_node(
|
|
|
104
98
|
join_candidates.sort(key=lambda x: sum([len(v) for v in x.paths.values()]))
|
|
105
99
|
if not join_candidates:
|
|
106
100
|
return None
|
|
107
|
-
|
|
108
|
-
logger.info(
|
|
109
|
-
f"{padding(depth)}{LOGGER_PREFIX} Join candidate: {join_candidate.paths}"
|
|
110
|
-
)
|
|
111
|
-
join_additions: List[set[str]] = []
|
|
101
|
+
join_additions: list[set[str]] = []
|
|
112
102
|
for candidate in join_candidates:
|
|
113
103
|
join_additions.append(candidate.reduced_concepts)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
104
|
+
|
|
105
|
+
common: set[str] = set()
|
|
106
|
+
final_candidates: list[set[str]] = []
|
|
107
|
+
# find all values that show up in every join_additions
|
|
108
|
+
for ja in join_additions:
|
|
109
|
+
if not common:
|
|
110
|
+
common = ja
|
|
111
|
+
else:
|
|
112
|
+
common = common.intersection(ja)
|
|
113
|
+
if all(ja.issubset(y) for y in join_additions):
|
|
114
|
+
final_candidates.append(ja)
|
|
115
|
+
|
|
116
|
+
if not final_candidates:
|
|
117
|
+
filtered_paths = [x.difference(common) for x in join_additions]
|
|
117
118
|
raise AmbiguousRelationshipResolutionException(
|
|
118
|
-
f"Ambiguous concept join resolution - possible paths =
|
|
119
|
+
f"Ambiguous concept join resolution fetching {[x.address for x in all_concepts]} - unique values in possible paths = {filtered_paths}. Include an additional concept to disambiguate",
|
|
119
120
|
join_additions,
|
|
120
121
|
)
|
|
121
122
|
if not join_candidates:
|
|
@@ -123,9 +124,10 @@ def gen_merge_node(
|
|
|
123
124
|
f"{padding(depth)}{LOGGER_PREFIX} No additional join candidates could be found"
|
|
124
125
|
)
|
|
125
126
|
return None
|
|
126
|
-
shortest: PathInfo = sorted(
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
shortest: PathInfo = sorted(
|
|
128
|
+
[x for x in join_candidates if x.reduced_concepts in final_candidates],
|
|
129
|
+
key=lambda x: len(x.reduced_concepts),
|
|
130
|
+
)[0]
|
|
129
131
|
logger.info(f"{padding(depth)}{LOGGER_PREFIX} final path is {shortest.paths}")
|
|
130
132
|
# logger.info(f'{padding(depth)}{LOGGER_PREFIX} final reduced concepts are {shortest.concs}')
|
|
131
133
|
parents = []
|
|
@@ -145,11 +147,20 @@ def gen_merge_node(
|
|
|
145
147
|
f"{padding(depth)}{LOGGER_PREFIX} Unable to instantiate target subgraph"
|
|
146
148
|
)
|
|
147
149
|
return None
|
|
150
|
+
logger.info(
|
|
151
|
+
f"{padding(depth)}{LOGGER_PREFIX} finished subgraph fetch for {[c.address for c in graph]}, have parent {type(parent)}"
|
|
152
|
+
)
|
|
148
153
|
parents.append(parent)
|
|
149
154
|
|
|
150
155
|
return MergeNode(
|
|
151
|
-
input_concepts=[
|
|
152
|
-
|
|
156
|
+
input_concepts=[
|
|
157
|
+
environment.concepts[x]
|
|
158
|
+
for x in shortest.reduced_concepts
|
|
159
|
+
if environment.concepts[x].derivation != PurposeLineage.MERGE
|
|
160
|
+
],
|
|
161
|
+
output_concepts=[
|
|
162
|
+
x for x in all_concepts if x.derivation != PurposeLineage.MERGE
|
|
163
|
+
],
|
|
153
164
|
environment=environment,
|
|
154
165
|
g=g,
|
|
155
166
|
parents=parents,
|
|
@@ -12,7 +12,7 @@ from typing import List
|
|
|
12
12
|
|
|
13
13
|
from trilogy.core.enums import JoinType, PurposeLineage
|
|
14
14
|
from trilogy.constants import logger
|
|
15
|
-
from trilogy.core.processing.utility import padding
|
|
15
|
+
from trilogy.core.processing.utility import padding, unique
|
|
16
16
|
from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
|
|
17
17
|
|
|
18
18
|
|
|
@@ -35,30 +35,57 @@ def gen_rowset_node(
|
|
|
35
35
|
lineage: RowsetItem = concept.lineage
|
|
36
36
|
rowset: RowsetDerivationStatement = lineage.rowset
|
|
37
37
|
select: SelectStatement | MultiSelectStatement = lineage.rowset.select
|
|
38
|
+
parents: List[StrategyNode] = []
|
|
38
39
|
if where := select.where_clause:
|
|
39
|
-
targets = select.output_components + where.conditional.
|
|
40
|
+
targets = select.output_components + where.conditional.row_arguments
|
|
41
|
+
for sub_select in where.conditional.existence_arguments:
|
|
42
|
+
logger.info(
|
|
43
|
+
f"{padding(depth)}{LOGGER_PREFIX} generating parent existence node with {[x.address for x in sub_select]}"
|
|
44
|
+
)
|
|
45
|
+
parent_check = source_concepts(
|
|
46
|
+
mandatory_list=sub_select,
|
|
47
|
+
environment=environment,
|
|
48
|
+
g=g,
|
|
49
|
+
depth=depth + 1,
|
|
50
|
+
history=history,
|
|
51
|
+
)
|
|
52
|
+
if not parent_check:
|
|
53
|
+
logger.info(
|
|
54
|
+
f"{padding(depth)}{LOGGER_PREFIX} Cannot generate parent existence node for rowset node for {concept}"
|
|
55
|
+
)
|
|
56
|
+
return None
|
|
57
|
+
parents.append(parent_check)
|
|
40
58
|
else:
|
|
41
59
|
targets = select.output_components
|
|
42
60
|
node: StrategyNode = source_concepts(
|
|
43
|
-
mandatory_list=targets,
|
|
61
|
+
mandatory_list=unique(targets, "address"),
|
|
44
62
|
environment=environment,
|
|
45
63
|
g=g,
|
|
46
64
|
depth=depth + 1,
|
|
47
65
|
history=history,
|
|
48
66
|
)
|
|
67
|
+
|
|
68
|
+
# add our existence concepts in
|
|
69
|
+
if parents:
|
|
70
|
+
node.parents += parents
|
|
71
|
+
for parent in parents:
|
|
72
|
+
for x in parent.output_concepts:
|
|
73
|
+
if x.address not in node.output_lcl:
|
|
74
|
+
node.existence_concepts.append(x)
|
|
49
75
|
if not node:
|
|
50
76
|
logger.info(
|
|
51
77
|
f"{padding(depth)}{LOGGER_PREFIX} Cannot generate rowset node for {concept}"
|
|
52
78
|
)
|
|
53
79
|
return None
|
|
54
80
|
node.conditions = select.where_clause.conditional if select.where_clause else None
|
|
55
|
-
# rebuild any cached info with the new condition clause
|
|
56
|
-
|
|
57
81
|
enrichment = set([x.address for x in local_optional])
|
|
58
|
-
rowset_relevant = [
|
|
82
|
+
rowset_relevant = [x for x in rowset.derived_concepts]
|
|
83
|
+
select_hidden = set([x.address for x in select.hidden_components])
|
|
84
|
+
rowset_hidden = [
|
|
59
85
|
x
|
|
60
86
|
for x in rowset.derived_concepts
|
|
61
|
-
|
|
87
|
+
if isinstance(x.lineage, RowsetItem)
|
|
88
|
+
and x.lineage.content.address in select_hidden
|
|
62
89
|
]
|
|
63
90
|
additional_relevant = [
|
|
64
91
|
x for x in select.output_components if x.address in enrichment
|
|
@@ -71,7 +98,7 @@ def gen_rowset_node(
|
|
|
71
98
|
if select.where_clause:
|
|
72
99
|
for item in additional_relevant:
|
|
73
100
|
node.partial_concepts.append(item)
|
|
74
|
-
node.hidden_concepts = [
|
|
101
|
+
node.hidden_concepts = rowset_hidden + [
|
|
75
102
|
x
|
|
76
103
|
for x in node.output_concepts
|
|
77
104
|
if x.address not in [y.address for y in local_optional + [concept]]
|
|
@@ -81,9 +108,11 @@ def gen_rowset_node(
|
|
|
81
108
|
# but don't include anything aggregate at this point
|
|
82
109
|
node.rebuild_cache()
|
|
83
110
|
assert node.resolution_cache
|
|
111
|
+
|
|
84
112
|
node.resolution_cache.grain = concept_list_to_grain(
|
|
85
113
|
node.output_concepts, parent_sources=node.resolution_cache.datasources
|
|
86
114
|
)
|
|
115
|
+
|
|
87
116
|
possible_joins = concept_to_relevant_joins(additional_relevant)
|
|
88
117
|
if not local_optional:
|
|
89
118
|
logger.info(
|
|
@@ -53,7 +53,7 @@ def dm_to_strategy_node(
|
|
|
53
53
|
# we have to group
|
|
54
54
|
else:
|
|
55
55
|
logger.info(
|
|
56
|
-
f"{padding(depth)}{LOGGER_PREFIX} not all grain components are in output {str(dm.matched)}, group to actual grain"
|
|
56
|
+
f"{padding(depth)}{LOGGER_PREFIX} not all grain components {target_grain} are in output {str(dm.matched)}, group to actual grain"
|
|
57
57
|
)
|
|
58
58
|
force_group = True
|
|
59
59
|
elif all([x in dm.matched for x in datasource.grain.components]):
|
|
@@ -76,7 +76,7 @@ def dm_to_strategy_node(
|
|
|
76
76
|
partial_concepts=dm.partial.concepts,
|
|
77
77
|
accept_partial=accept_partial,
|
|
78
78
|
datasource=datasource,
|
|
79
|
-
grain=
|
|
79
|
+
grain=datasource.grain,
|
|
80
80
|
)
|
|
81
81
|
# we need to nest the group node one further
|
|
82
82
|
if force_group is True:
|
|
@@ -317,13 +317,19 @@ def gen_select_node_from_table(
|
|
|
317
317
|
)
|
|
318
318
|
if target_grain and target_grain.issubset(datasource.grain):
|
|
319
319
|
|
|
320
|
-
if
|
|
320
|
+
if (
|
|
321
|
+
all([x in all_lcl for x in target_grain.components])
|
|
322
|
+
and target_grain == datasource.grain
|
|
323
|
+
):
|
|
324
|
+
logger.info(
|
|
325
|
+
f"{padding(depth)}{LOGGER_PREFIX} target grain components match all lcl, group to false"
|
|
326
|
+
)
|
|
321
327
|
force_group = False
|
|
322
328
|
# if we are not returning the grain
|
|
323
329
|
# we have to group
|
|
324
330
|
else:
|
|
325
331
|
logger.info(
|
|
326
|
-
f"{padding(depth)}{LOGGER_PREFIX} not all grain components are in output {str(all_lcl)}, group to actual grain"
|
|
332
|
+
f"{padding(depth)}{LOGGER_PREFIX} not all grain components {target_grain} are in output {str(all_lcl)}, group to actual grain"
|
|
327
333
|
)
|
|
328
334
|
force_group = True
|
|
329
335
|
elif all([x in all_lcl for x in datasource.grain.components]):
|
|
@@ -363,7 +369,7 @@ def gen_select_node_from_table(
|
|
|
363
369
|
else:
|
|
364
370
|
candidate = bcandidate
|
|
365
371
|
logger.info(
|
|
366
|
-
f"{padding(depth)}{LOGGER_PREFIX} found select node with {datasource.identifier}, returning {candidate.output_lcl}"
|
|
372
|
+
f"{padding(depth)}{LOGGER_PREFIX} found select node with {datasource.identifier}, force group is {force_group}, returning {candidate.output_lcl}"
|
|
367
373
|
)
|
|
368
374
|
candidates[datasource.identifier] = candidate
|
|
369
375
|
scores[datasource.identifier] = -len(partial_concepts)
|
|
@@ -467,6 +473,8 @@ def gen_select_node(
|
|
|
467
473
|
target_grain = Grain()
|
|
468
474
|
for ac in all_concepts:
|
|
469
475
|
target_grain += ac.grain
|
|
476
|
+
if target_grain.abstract:
|
|
477
|
+
target_grain = Grain(components=all_concepts)
|
|
470
478
|
if materialized_lcl != all_lcl:
|
|
471
479
|
logger.info(
|
|
472
480
|
f"{padding(depth)}{LOGGER_PREFIX} Skipping select node generation for {concept.address} "
|
|
@@ -513,13 +521,15 @@ def gen_select_node(
|
|
|
513
521
|
[c.address in [x.address for x in p.partial_concepts] for p in parents]
|
|
514
522
|
)
|
|
515
523
|
]
|
|
516
|
-
force_group =
|
|
524
|
+
force_group = None
|
|
525
|
+
inferred_grain = sum([x.grain for x in parents if x.grain], Grain())
|
|
517
526
|
for candidate in parents:
|
|
518
527
|
if candidate.grain and not candidate.grain.issubset(target_grain):
|
|
519
528
|
force_group = True
|
|
520
529
|
if len(parents) == 1:
|
|
521
530
|
candidate = parents[0]
|
|
522
531
|
else:
|
|
532
|
+
|
|
523
533
|
candidate = MergeNode(
|
|
524
534
|
output_concepts=[concept] + found,
|
|
525
535
|
input_concepts=[concept] + found,
|
|
@@ -528,13 +538,13 @@ def gen_select_node(
|
|
|
528
538
|
parents=parents,
|
|
529
539
|
depth=depth,
|
|
530
540
|
partial_concepts=all_partial,
|
|
531
|
-
grain=
|
|
541
|
+
grain=inferred_grain,
|
|
532
542
|
)
|
|
533
543
|
candidate.depth += 1
|
|
534
|
-
source_grain = candidate.grain
|
|
544
|
+
# source_grain = candidate.grain
|
|
535
545
|
if force_group:
|
|
536
546
|
logger.info(
|
|
537
|
-
f"{padding(depth)}{LOGGER_PREFIX} datasource grain {
|
|
547
|
+
f"{padding(depth)}{LOGGER_PREFIX} datasource grain {inferred_grain} does not match target grain {target_grain} for select, adding group node"
|
|
538
548
|
)
|
|
539
549
|
return GroupNode(
|
|
540
550
|
output_concepts=candidate.output_concepts,
|
|
@@ -545,6 +555,10 @@ def gen_select_node(
|
|
|
545
555
|
depth=depth,
|
|
546
556
|
partial_concepts=candidate.partial_concepts,
|
|
547
557
|
)
|
|
558
|
+
else:
|
|
559
|
+
logger.info(
|
|
560
|
+
f"{padding(depth)}{LOGGER_PREFIX} datasource grain {inferred_grain} matches target grain {target_grain} for select, returning without group"
|
|
561
|
+
)
|
|
548
562
|
return candidate
|
|
549
563
|
|
|
550
564
|
if not accept_partial_optional:
|
|
@@ -2,7 +2,11 @@ from typing import List
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
from trilogy.core.models import Concept, Function
|
|
5
|
-
from trilogy.core.processing.nodes import UnnestNode, History
|
|
5
|
+
from trilogy.core.processing.nodes import SelectNode, UnnestNode, History, StrategyNode
|
|
6
|
+
from trilogy.core.processing.utility import padding
|
|
7
|
+
from trilogy.constants import logger
|
|
8
|
+
|
|
9
|
+
LOGGER_PREFIX = "[GEN_ROWSET_NODE]"
|
|
6
10
|
|
|
7
11
|
|
|
8
12
|
def gen_unnest_node(
|
|
@@ -13,7 +17,7 @@ def gen_unnest_node(
|
|
|
13
17
|
depth: int,
|
|
14
18
|
source_concepts,
|
|
15
19
|
history: History | None = None,
|
|
16
|
-
) ->
|
|
20
|
+
) -> StrategyNode | None:
|
|
17
21
|
arguments = []
|
|
18
22
|
if isinstance(concept.lineage, Function):
|
|
19
23
|
arguments = concept.lineage.concept_arguments
|
|
@@ -26,8 +30,12 @@ def gen_unnest_node(
|
|
|
26
30
|
history=history,
|
|
27
31
|
)
|
|
28
32
|
if not parent:
|
|
33
|
+
logger.info(
|
|
34
|
+
f"{padding(depth)}{LOGGER_PREFIX} could not find unnest node parents"
|
|
35
|
+
)
|
|
29
36
|
return None
|
|
30
|
-
|
|
37
|
+
|
|
38
|
+
base = UnnestNode(
|
|
31
39
|
unnest_concept=concept,
|
|
32
40
|
input_concepts=arguments + local_optional,
|
|
33
41
|
output_concepts=[concept] + local_optional,
|
|
@@ -35,3 +43,16 @@ def gen_unnest_node(
|
|
|
35
43
|
g=g,
|
|
36
44
|
parents=([parent] if (arguments or local_optional) else []),
|
|
37
45
|
)
|
|
46
|
+
# we need to sometimes nest an unnest node,
|
|
47
|
+
# as unnest operations are not valid in all situations
|
|
48
|
+
# TODO: inline this node when we can detect it's safe
|
|
49
|
+
new = SelectNode(
|
|
50
|
+
input_concepts=[concept] + local_optional,
|
|
51
|
+
output_concepts=[concept] + local_optional,
|
|
52
|
+
environment=environment,
|
|
53
|
+
g=g,
|
|
54
|
+
parents=[base],
|
|
55
|
+
)
|
|
56
|
+
qds = new.resolve()
|
|
57
|
+
assert qds.source_map[concept.address] == {base.resolve()}
|
|
58
|
+
return new
|
|
@@ -59,19 +59,21 @@ def gen_window_node(
|
|
|
59
59
|
parents=[
|
|
60
60
|
parent_node,
|
|
61
61
|
],
|
|
62
|
+
depth=depth,
|
|
62
63
|
)
|
|
63
64
|
window_node = MergeNode(
|
|
64
65
|
parents=[_window_node],
|
|
65
66
|
environment=environment,
|
|
66
67
|
g=g,
|
|
67
|
-
input_concepts=_window_node.input_concepts,
|
|
68
|
+
input_concepts=[concept] + _window_node.input_concepts,
|
|
68
69
|
output_concepts=_window_node.output_concepts,
|
|
69
70
|
grain=_window_node.grain,
|
|
70
71
|
force_group=False,
|
|
72
|
+
depth=depth,
|
|
71
73
|
)
|
|
72
74
|
if not local_optional:
|
|
73
75
|
return window_node
|
|
74
|
-
logger.info(f"{padding(depth)}{LOGGER_PREFIX}
|
|
76
|
+
logger.info(f"{padding(depth)}{LOGGER_PREFIX} window node requires enrichment")
|
|
75
77
|
return gen_enrichment_node(
|
|
76
78
|
window_node,
|
|
77
79
|
join_keys=concept_to_relevant_joins(parent_concepts),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from .filter_node import FilterNode
|
|
2
2
|
from .group_node import GroupNode
|
|
3
3
|
from .merge_node import MergeNode
|
|
4
|
-
from .select_node_v2 import SelectNode,
|
|
4
|
+
from .select_node_v2 import SelectNode, ConstantNode
|
|
5
5
|
from .window_node import WindowNode
|
|
6
6
|
from .base_node import StrategyNode, NodeJoin
|
|
7
7
|
from .unnest_node import UnnestNode
|
|
@@ -37,10 +37,12 @@ class History(BaseModel):
|
|
|
37
37
|
raise ValueError(
|
|
38
38
|
f"Parent key {parent_key} is the same as the current key {key}"
|
|
39
39
|
)
|
|
40
|
-
|
|
41
|
-
key
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
if key in self.history:
|
|
41
|
+
node = self.history[key]
|
|
42
|
+
if node:
|
|
43
|
+
return node.copy()
|
|
44
|
+
return node
|
|
45
|
+
return False
|
|
44
46
|
|
|
45
47
|
def log_start(
|
|
46
48
|
self,
|
|
@@ -125,7 +127,6 @@ __all__ = [
|
|
|
125
127
|
"GroupNode",
|
|
126
128
|
"MergeNode",
|
|
127
129
|
"SelectNode",
|
|
128
|
-
"StaticSelectNode",
|
|
129
130
|
"WindowNode",
|
|
130
131
|
"StrategyNode",
|
|
131
132
|
"NodeJoin",
|
|
@@ -17,6 +17,7 @@ from trilogy.core.models import (
|
|
|
17
17
|
from trilogy.core.enums import Purpose, JoinType, PurposeLineage, Granularity
|
|
18
18
|
from trilogy.utility import unique
|
|
19
19
|
from dataclasses import dataclass
|
|
20
|
+
from trilogy.constants import logger
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def concept_list_to_grain(
|
|
@@ -55,11 +56,18 @@ def resolve_concept_map(
|
|
|
55
56
|
defaultdict(set)
|
|
56
57
|
)
|
|
57
58
|
full_addresses = {c.address for c in full_joins} if full_joins else set()
|
|
59
|
+
inherited = set([t.address for t in inherited_inputs])
|
|
58
60
|
for input in inputs:
|
|
59
61
|
for concept in input.output_concepts:
|
|
62
|
+
logger.info(concept.address)
|
|
60
63
|
if concept.address not in input.non_partial_concept_addresses:
|
|
61
64
|
continue
|
|
62
|
-
if concept.address not in
|
|
65
|
+
if concept.address not in inherited:
|
|
66
|
+
continue
|
|
67
|
+
if (
|
|
68
|
+
isinstance(input, QueryDatasource)
|
|
69
|
+
and concept.address in input.hidden_concepts
|
|
70
|
+
):
|
|
63
71
|
continue
|
|
64
72
|
if concept.address in full_addresses:
|
|
65
73
|
concept_map[concept.address].add(input)
|
|
@@ -71,11 +79,16 @@ def resolve_concept_map(
|
|
|
71
79
|
for concept in input.output_concepts:
|
|
72
80
|
if concept.address not in [t.address for t in inherited_inputs]:
|
|
73
81
|
continue
|
|
82
|
+
if (
|
|
83
|
+
isinstance(input, QueryDatasource)
|
|
84
|
+
and concept.address in input.hidden_concepts
|
|
85
|
+
):
|
|
86
|
+
continue
|
|
74
87
|
if len(concept_map.get(concept.address, [])) == 0:
|
|
75
88
|
concept_map[concept.address].add(input)
|
|
76
89
|
# this adds our new derived metrics, which are not created in this CTE
|
|
77
90
|
for target in targets:
|
|
78
|
-
if target not in
|
|
91
|
+
if target.address not in inherited:
|
|
79
92
|
# an empty source means it is defined in this CTE
|
|
80
93
|
concept_map[target.address] = set()
|
|
81
94
|
return concept_map
|
|
@@ -108,6 +121,8 @@ class StrategyNode:
|
|
|
108
121
|
force_group: bool | None = None,
|
|
109
122
|
grain: Optional[Grain] = None,
|
|
110
123
|
hidden_concepts: List[Concept] | None = None,
|
|
124
|
+
existence_concepts: List[Concept] | None = None,
|
|
125
|
+
virtual_output_concepts: List[Concept] | None = None,
|
|
111
126
|
):
|
|
112
127
|
self.input_concepts: List[Concept] = (
|
|
113
128
|
unique(input_concepts, "address") if input_concepts else []
|
|
@@ -131,6 +146,8 @@ class StrategyNode:
|
|
|
131
146
|
self.force_group = force_group
|
|
132
147
|
self.tainted = False
|
|
133
148
|
self.hidden_concepts = hidden_concepts or []
|
|
149
|
+
self.existence_concepts = existence_concepts or []
|
|
150
|
+
self.virtual_output_concepts = virtual_output_concepts or []
|
|
134
151
|
for parent in self.parents:
|
|
135
152
|
if not parent:
|
|
136
153
|
raise SyntaxError("Unresolvable parent")
|
|
@@ -162,12 +179,11 @@ class StrategyNode:
|
|
|
162
179
|
p.resolve() for p in self.parents
|
|
163
180
|
]
|
|
164
181
|
|
|
165
|
-
# if conditional:
|
|
166
|
-
# for condition in conditions[1:]:
|
|
167
|
-
# conditional += condition
|
|
168
182
|
grain = Grain(components=self.output_concepts)
|
|
169
183
|
source_map = resolve_concept_map(
|
|
170
|
-
parent_sources,
|
|
184
|
+
parent_sources,
|
|
185
|
+
self.output_concepts,
|
|
186
|
+
self.input_concepts + self.existence_concepts,
|
|
171
187
|
)
|
|
172
188
|
return QueryDatasource(
|
|
173
189
|
input_concepts=self.input_concepts,
|
|
@@ -197,6 +213,24 @@ class StrategyNode:
|
|
|
197
213
|
self.resolution_cache = qds
|
|
198
214
|
return qds
|
|
199
215
|
|
|
216
|
+
def copy(self) -> "StrategyNode":
|
|
217
|
+
return self.__class__(
|
|
218
|
+
input_concepts=list(self.input_concepts),
|
|
219
|
+
output_concepts=list(self.output_concepts),
|
|
220
|
+
environment=self.environment,
|
|
221
|
+
g=self.g,
|
|
222
|
+
whole_grain=self.whole_grain,
|
|
223
|
+
parents=list(self.parents),
|
|
224
|
+
partial_concepts=list(self.partial_concepts),
|
|
225
|
+
depth=self.depth,
|
|
226
|
+
conditions=self.conditions,
|
|
227
|
+
force_group=self.force_group,
|
|
228
|
+
grain=self.grain,
|
|
229
|
+
hidden_concepts=list(self.hidden_concepts),
|
|
230
|
+
existence_concepts=list(self.existence_concepts),
|
|
231
|
+
virtual_output_concepts=list(self.virtual_output_concepts),
|
|
232
|
+
)
|
|
233
|
+
|
|
200
234
|
|
|
201
235
|
@dataclass
|
|
202
236
|
class NodeJoin:
|
|
@@ -33,7 +33,7 @@ class FilterNode(StrategyNode):
|
|
|
33
33
|
depth: int = 0,
|
|
34
34
|
conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
35
35
|
partial_concepts: List[Concept] | None = None,
|
|
36
|
-
force_group: bool = False,
|
|
36
|
+
force_group: bool | None = False,
|
|
37
37
|
):
|
|
38
38
|
super().__init__(
|
|
39
39
|
output_concepts=output_concepts,
|
|
@@ -47,3 +47,17 @@ class FilterNode(StrategyNode):
|
|
|
47
47
|
partial_concepts=partial_concepts,
|
|
48
48
|
force_group=force_group,
|
|
49
49
|
)
|
|
50
|
+
|
|
51
|
+
def copy(self) -> "FilterNode":
|
|
52
|
+
return FilterNode(
|
|
53
|
+
input_concepts=list(self.input_concepts),
|
|
54
|
+
output_concepts=list(self.output_concepts),
|
|
55
|
+
environment=self.environment,
|
|
56
|
+
g=self.g,
|
|
57
|
+
whole_grain=self.whole_grain,
|
|
58
|
+
parents=self.parents,
|
|
59
|
+
depth=self.depth,
|
|
60
|
+
conditions=self.conditions,
|
|
61
|
+
partial_concepts=list(self.partial_concepts),
|
|
62
|
+
force_group=self.force_group,
|
|
63
|
+
)
|
|
@@ -33,6 +33,7 @@ class GroupNode(StrategyNode):
|
|
|
33
33
|
parents: List["StrategyNode"] | None = None,
|
|
34
34
|
depth: int = 0,
|
|
35
35
|
partial_concepts: Optional[List[Concept]] = None,
|
|
36
|
+
force_group: bool | None = None,
|
|
36
37
|
):
|
|
37
38
|
super().__init__(
|
|
38
39
|
input_concepts=input_concepts,
|
|
@@ -43,6 +44,7 @@ class GroupNode(StrategyNode):
|
|
|
43
44
|
parents=parents,
|
|
44
45
|
depth=depth,
|
|
45
46
|
partial_concepts=partial_concepts,
|
|
47
|
+
force_group=force_group,
|
|
46
48
|
)
|
|
47
49
|
|
|
48
50
|
def _resolve(self) -> QueryDatasource:
|
|
@@ -57,7 +59,11 @@ class GroupNode(StrategyNode):
|
|
|
57
59
|
|
|
58
60
|
# dynamically select if we need to group
|
|
59
61
|
# because sometimes, we are already at required grain
|
|
60
|
-
if
|
|
62
|
+
if (
|
|
63
|
+
comp_grain == grain
|
|
64
|
+
and self.output_lcl == self.input_lcl
|
|
65
|
+
and self.force_group is not True
|
|
66
|
+
):
|
|
61
67
|
# if there is no group by, and inputs equal outputs
|
|
62
68
|
# return the parent
|
|
63
69
|
logger.info(
|
|
@@ -111,3 +117,16 @@ class GroupNode(StrategyNode):
|
|
|
111
117
|
partial_concepts=self.partial_concepts,
|
|
112
118
|
condition=self.conditions,
|
|
113
119
|
)
|
|
120
|
+
|
|
121
|
+
def copy(self) -> "GroupNode":
|
|
122
|
+
return GroupNode(
|
|
123
|
+
input_concepts=list(self.input_concepts),
|
|
124
|
+
output_concepts=list(self.output_concepts),
|
|
125
|
+
environment=self.environment,
|
|
126
|
+
g=self.g,
|
|
127
|
+
whole_grain=self.whole_grain,
|
|
128
|
+
parents=self.parents,
|
|
129
|
+
depth=self.depth,
|
|
130
|
+
partial_concepts=list(self.partial_concepts),
|
|
131
|
+
force_group=self.force_group,
|
|
132
|
+
)
|