pytrilogy 0.0.2.12__py3-none-any.whl → 0.0.2.14__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.2.12.dist-info → pytrilogy-0.0.2.14.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.2.12.dist-info → pytrilogy-0.0.2.14.dist-info}/RECORD +31 -31
- {pytrilogy-0.0.2.12.dist-info → pytrilogy-0.0.2.14.dist-info}/WHEEL +1 -1
- trilogy/__init__.py +1 -1
- trilogy/constants.py +16 -1
- trilogy/core/enums.py +3 -0
- trilogy/core/models.py +150 -17
- trilogy/core/optimizations/predicate_pushdown.py +1 -1
- trilogy/core/processing/node_generators/basic_node.py +8 -1
- trilogy/core/processing/node_generators/common.py +13 -36
- trilogy/core/processing/node_generators/filter_node.py +1 -15
- trilogy/core/processing/node_generators/group_node.py +19 -1
- trilogy/core/processing/node_generators/group_to_node.py +0 -12
- trilogy/core/processing/node_generators/multiselect_node.py +1 -10
- trilogy/core/processing/node_generators/rowset_node.py +3 -14
- trilogy/core/processing/node_generators/select_node.py +26 -0
- trilogy/core/processing/node_generators/window_node.py +1 -1
- trilogy/core/processing/nodes/base_node.py +40 -11
- trilogy/core/processing/nodes/group_node.py +31 -18
- trilogy/core/processing/nodes/merge_node.py +14 -5
- trilogy/core/processing/nodes/select_node_v2.py +4 -0
- trilogy/core/processing/utility.py +91 -3
- trilogy/core/query_processor.py +6 -12
- trilogy/dialect/common.py +10 -8
- trilogy/executor.py +8 -2
- trilogy/parsing/common.py +34 -4
- trilogy/parsing/parse_engine.py +31 -19
- trilogy/parsing/trilogy.lark +5 -5
- {pytrilogy-0.0.2.12.dist-info → pytrilogy-0.0.2.14.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.12.dist-info → pytrilogy-0.0.2.14.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.2.12.dist-info → pytrilogy-0.0.2.14.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List, Tuple
|
|
1
|
+
from typing import List, Tuple, Callable
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
from trilogy.core.enums import PurposeLineage, Purpose
|
|
@@ -15,12 +15,10 @@ from trilogy.utility import unique
|
|
|
15
15
|
from trilogy.core.processing.nodes.base_node import StrategyNode
|
|
16
16
|
from trilogy.core.processing.nodes.merge_node import MergeNode
|
|
17
17
|
from trilogy.core.processing.nodes import History
|
|
18
|
-
from trilogy.core.enums import JoinType
|
|
19
18
|
from trilogy.core.processing.nodes import (
|
|
20
19
|
NodeJoin,
|
|
21
20
|
)
|
|
22
21
|
from collections import defaultdict
|
|
23
|
-
from trilogy.core.processing.utility import concept_to_relevant_joins
|
|
24
22
|
|
|
25
23
|
|
|
26
24
|
def resolve_function_parent_concepts(concept: Concept) -> List[Concept]:
|
|
@@ -96,6 +94,7 @@ def gen_property_enrichment_node(
|
|
|
96
94
|
g,
|
|
97
95
|
depth: int,
|
|
98
96
|
source_concepts,
|
|
97
|
+
log_lambda: Callable,
|
|
99
98
|
history: History | None = None,
|
|
100
99
|
conditions: WhereClause | None = None,
|
|
101
100
|
):
|
|
@@ -106,8 +105,8 @@ def gen_property_enrichment_node(
|
|
|
106
105
|
keys = "-".join([y.address for y in x.keys])
|
|
107
106
|
required_keys[keys].add(x.address)
|
|
108
107
|
final_nodes = []
|
|
109
|
-
node_joins = []
|
|
110
108
|
for _k, vs in required_keys.items():
|
|
109
|
+
log_lambda(f"Generating enrichment node for {_k} with {vs}")
|
|
111
110
|
ks = _k.split("-")
|
|
112
111
|
enrich_node: StrategyNode = source_concepts(
|
|
113
112
|
mandatory_list=[environment.concepts[k] for k in ks]
|
|
@@ -119,17 +118,6 @@ def gen_property_enrichment_node(
|
|
|
119
118
|
conditions=conditions,
|
|
120
119
|
)
|
|
121
120
|
final_nodes.append(enrich_node)
|
|
122
|
-
node_joins.append(
|
|
123
|
-
NodeJoin(
|
|
124
|
-
left_node=enrich_node,
|
|
125
|
-
right_node=base_node,
|
|
126
|
-
concepts=concept_to_relevant_joins(
|
|
127
|
-
[environment.concepts[k] for k in ks]
|
|
128
|
-
),
|
|
129
|
-
filter_to_mutual=False,
|
|
130
|
-
join_type=JoinType.LEFT_OUTER,
|
|
131
|
-
)
|
|
132
|
-
)
|
|
133
121
|
return MergeNode(
|
|
134
122
|
input_concepts=unique(
|
|
135
123
|
base_node.output_concepts
|
|
@@ -146,9 +134,8 @@ def gen_property_enrichment_node(
|
|
|
146
134
|
g=g,
|
|
147
135
|
parents=[
|
|
148
136
|
base_node,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
node_joins=node_joins,
|
|
137
|
+
]
|
|
138
|
+
+ final_nodes,
|
|
152
139
|
)
|
|
153
140
|
|
|
154
141
|
|
|
@@ -197,6 +184,7 @@ def gen_enrichment_node(
|
|
|
197
184
|
source_concepts,
|
|
198
185
|
history=history,
|
|
199
186
|
conditions=conditions,
|
|
187
|
+
log_lambda=log_lambda,
|
|
200
188
|
)
|
|
201
189
|
|
|
202
190
|
enrich_node: StrategyNode = source_concepts( # this fetches the parent + join keys
|
|
@@ -216,29 +204,18 @@ def gen_enrichment_node(
|
|
|
216
204
|
log_lambda(
|
|
217
205
|
f"{str(type(base_node).__name__)} returning merge node with group node + enrichment node"
|
|
218
206
|
)
|
|
219
|
-
|
|
207
|
+
non_hidden = [
|
|
208
|
+
x
|
|
209
|
+
for x in base_node.output_concepts
|
|
210
|
+
if x.address not in [y.address for y in base_node.hidden_concepts]
|
|
211
|
+
]
|
|
220
212
|
return MergeNode(
|
|
221
|
-
input_concepts=unique(
|
|
222
|
-
|
|
223
|
-
),
|
|
224
|
-
output_concepts=unique(
|
|
225
|
-
join_keys + extra_required + base_node.output_concepts, "address"
|
|
226
|
-
),
|
|
213
|
+
input_concepts=unique(join_keys + extra_required + non_hidden, "address"),
|
|
214
|
+
output_concepts=unique(join_keys + extra_required + non_hidden, "address"),
|
|
227
215
|
environment=environment,
|
|
228
216
|
g=g,
|
|
229
217
|
parents=[enrich_node, base_node],
|
|
230
218
|
force_group=False,
|
|
231
|
-
node_joins=[
|
|
232
|
-
NodeJoin(
|
|
233
|
-
left_node=enrich_node,
|
|
234
|
-
right_node=base_node,
|
|
235
|
-
concepts=concept_to_relevant_joins(
|
|
236
|
-
[x for x in join_keys if x in enrich_node.output_lcl]
|
|
237
|
-
),
|
|
238
|
-
filter_to_mutual=False,
|
|
239
|
-
join_type=JoinType.LEFT_OUTER,
|
|
240
|
-
)
|
|
241
|
-
],
|
|
242
219
|
)
|
|
243
220
|
|
|
244
221
|
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
from typing import List
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
from trilogy.core.enums import JoinType
|
|
5
4
|
from trilogy.core.models import Concept, Environment, FilterItem, Grain, WhereClause
|
|
6
5
|
from trilogy.core.processing.nodes import (
|
|
7
6
|
FilterNode,
|
|
8
7
|
MergeNode,
|
|
9
|
-
NodeJoin,
|
|
10
8
|
History,
|
|
11
9
|
StrategyNode,
|
|
12
10
|
SelectNode,
|
|
@@ -16,7 +14,6 @@ from trilogy.core.processing.node_generators.common import (
|
|
|
16
14
|
)
|
|
17
15
|
from trilogy.constants import logger
|
|
18
16
|
from trilogy.core.processing.utility import padding, unique
|
|
19
|
-
from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
|
|
20
17
|
from trilogy.core.processing.utility import is_scalar_condition
|
|
21
18
|
|
|
22
19
|
LOGGER_PREFIX = "[GEN_FILTER_NODE]"
|
|
@@ -215,16 +212,5 @@ def gen_filter_node(
|
|
|
215
212
|
# this node fetches only what we need to filter
|
|
216
213
|
filter_node,
|
|
217
214
|
enrich_node,
|
|
218
|
-
]
|
|
219
|
-
node_joins=[
|
|
220
|
-
NodeJoin(
|
|
221
|
-
left_node=enrich_node,
|
|
222
|
-
right_node=filter_node,
|
|
223
|
-
concepts=concept_to_relevant_joins(
|
|
224
|
-
[immediate_parent] + parent_row_concepts
|
|
225
|
-
),
|
|
226
|
-
join_type=JoinType.LEFT_OUTER,
|
|
227
|
-
filter_to_mutual=True,
|
|
228
|
-
)
|
|
229
|
-
],
|
|
215
|
+
]
|
|
230
216
|
)
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
from trilogy.core.models import
|
|
1
|
+
from trilogy.core.models import (
|
|
2
|
+
Concept,
|
|
3
|
+
Environment,
|
|
4
|
+
LooseConceptList,
|
|
5
|
+
WhereClause,
|
|
6
|
+
Function,
|
|
7
|
+
AggregateWrapper,
|
|
8
|
+
)
|
|
2
9
|
from trilogy.utility import unique
|
|
3
10
|
from trilogy.core.processing.nodes import GroupNode, StrategyNode, History
|
|
4
11
|
from typing import List
|
|
@@ -42,6 +49,17 @@ def gen_group_node(
|
|
|
42
49
|
)
|
|
43
50
|
parent_concepts += grain_components
|
|
44
51
|
output_concepts += grain_components
|
|
52
|
+
for possible_agg in local_optional:
|
|
53
|
+
if possible_agg.grain and possible_agg.grain == concept.grain:
|
|
54
|
+
if not isinstance(possible_agg.lineage, (AggregateWrapper, Function)):
|
|
55
|
+
continue
|
|
56
|
+
agg_parents: List[Concept] = resolve_function_parent_concepts(
|
|
57
|
+
possible_agg
|
|
58
|
+
)
|
|
59
|
+
if set([x.address for x in agg_parents]).issubset(
|
|
60
|
+
set([x.address for x in parent_concepts])
|
|
61
|
+
):
|
|
62
|
+
output_concepts.append(possible_agg)
|
|
45
63
|
|
|
46
64
|
if parent_concepts:
|
|
47
65
|
logger.info(
|
|
@@ -3,15 +3,12 @@ from trilogy.core.processing.nodes import (
|
|
|
3
3
|
GroupNode,
|
|
4
4
|
StrategyNode,
|
|
5
5
|
MergeNode,
|
|
6
|
-
NodeJoin,
|
|
7
6
|
History,
|
|
8
7
|
)
|
|
9
8
|
from typing import List
|
|
10
|
-
from trilogy.core.enums import JoinType
|
|
11
9
|
|
|
12
10
|
from trilogy.constants import logger
|
|
13
11
|
from trilogy.core.processing.utility import padding
|
|
14
|
-
from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
|
|
15
12
|
|
|
16
13
|
LOGGER_PREFIX = "[GEN_GROUP_TO_NODE]"
|
|
17
14
|
|
|
@@ -84,15 +81,6 @@ def gen_group_to_node(
|
|
|
84
81
|
# this node gets enrichment
|
|
85
82
|
enrich_node,
|
|
86
83
|
],
|
|
87
|
-
node_joins=[
|
|
88
|
-
NodeJoin(
|
|
89
|
-
left_node=group_node,
|
|
90
|
-
right_node=enrich_node,
|
|
91
|
-
concepts=concept_to_relevant_joins(parent_concepts),
|
|
92
|
-
filter_to_mutual=False,
|
|
93
|
-
join_type=JoinType.LEFT_OUTER,
|
|
94
|
-
)
|
|
95
|
-
],
|
|
96
84
|
whole_grain=True,
|
|
97
85
|
depth=depth,
|
|
98
86
|
)
|
|
@@ -10,7 +10,7 @@ from typing import List
|
|
|
10
10
|
from trilogy.core.enums import JoinType
|
|
11
11
|
from trilogy.constants import logger
|
|
12
12
|
from trilogy.core.processing.utility import padding
|
|
13
|
-
from trilogy.core.processing.
|
|
13
|
+
from trilogy.core.processing.utility import concept_to_relevant_joins
|
|
14
14
|
from collections import defaultdict
|
|
15
15
|
from itertools import combinations
|
|
16
16
|
from trilogy.core.enums import Purpose
|
|
@@ -176,14 +176,5 @@ def gen_multiselect_node(
|
|
|
176
176
|
# this node gets enrichment
|
|
177
177
|
enrich_node,
|
|
178
178
|
],
|
|
179
|
-
node_joins=[
|
|
180
|
-
NodeJoin(
|
|
181
|
-
left_node=enrich_node,
|
|
182
|
-
right_node=node,
|
|
183
|
-
concepts=possible_joins,
|
|
184
|
-
filter_to_mutual=False,
|
|
185
|
-
join_type=JoinType.LEFT_OUTER,
|
|
186
|
-
)
|
|
187
|
-
],
|
|
188
179
|
partial_concepts=node.partial_concepts,
|
|
189
180
|
)
|
|
@@ -6,14 +6,14 @@ from trilogy.core.models import (
|
|
|
6
6
|
RowsetItem,
|
|
7
7
|
MultiSelectStatement,
|
|
8
8
|
)
|
|
9
|
-
from trilogy.core.processing.nodes import MergeNode,
|
|
9
|
+
from trilogy.core.processing.nodes import MergeNode, History, StrategyNode
|
|
10
10
|
from trilogy.core.processing.nodes.base_node import concept_list_to_grain
|
|
11
11
|
from typing import List
|
|
12
12
|
|
|
13
|
-
from trilogy.core.enums import
|
|
13
|
+
from trilogy.core.enums import PurposeLineage
|
|
14
14
|
from trilogy.constants import logger
|
|
15
15
|
from trilogy.core.processing.utility import padding
|
|
16
|
-
from trilogy.core.processing.
|
|
16
|
+
from trilogy.core.processing.utility import concept_to_relevant_joins
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
LOGGER_PREFIX = "[GEN_ROWSET_NODE]"
|
|
@@ -113,19 +113,8 @@ def gen_rowset_node(
|
|
|
113
113
|
g=g,
|
|
114
114
|
depth=depth,
|
|
115
115
|
parents=[
|
|
116
|
-
# this node gets the window
|
|
117
116
|
node,
|
|
118
|
-
# this node gets enrichment
|
|
119
117
|
enrich_node,
|
|
120
118
|
],
|
|
121
|
-
node_joins=[
|
|
122
|
-
NodeJoin(
|
|
123
|
-
left_node=enrich_node,
|
|
124
|
-
right_node=node,
|
|
125
|
-
concepts=concept_to_relevant_joins(additional_relevant),
|
|
126
|
-
filter_to_mutual=False,
|
|
127
|
-
join_type=JoinType.LEFT_OUTER,
|
|
128
|
-
)
|
|
129
|
-
],
|
|
130
119
|
partial_concepts=node.partial_concepts,
|
|
131
120
|
)
|
|
@@ -32,6 +32,7 @@ class DatasourceMatch:
|
|
|
32
32
|
datasource: Datasource
|
|
33
33
|
matched: LooseConceptList
|
|
34
34
|
partial: LooseConceptList
|
|
35
|
+
nullable: LooseConceptList
|
|
35
36
|
|
|
36
37
|
def __repr__(self):
|
|
37
38
|
return f"DatasourceMatch({self.key}, {self.datasource.identifier}, {str(self.matched)}, {str(self.partial)})"
|
|
@@ -76,6 +77,7 @@ def dm_to_strategy_node(
|
|
|
76
77
|
parents=[],
|
|
77
78
|
depth=depth,
|
|
78
79
|
partial_concepts=dm.partial.concepts,
|
|
80
|
+
nullable_concepts=dm.nullable.concepts,
|
|
79
81
|
accept_partial=accept_partial,
|
|
80
82
|
datasource=datasource,
|
|
81
83
|
grain=datasource.grain,
|
|
@@ -189,6 +191,13 @@ def gen_select_nodes_from_tables_v2(
|
|
|
189
191
|
if not c.is_complete and c.concept.address in all_lcl
|
|
190
192
|
]
|
|
191
193
|
),
|
|
194
|
+
nullable=LooseConceptList(
|
|
195
|
+
concepts=[
|
|
196
|
+
c.concept
|
|
197
|
+
for c in datasource.columns
|
|
198
|
+
if c.is_nullable and c.concept in all_lcl
|
|
199
|
+
]
|
|
200
|
+
),
|
|
192
201
|
)
|
|
193
202
|
if not matched:
|
|
194
203
|
continue
|
|
@@ -318,6 +327,12 @@ def gen_select_node_from_table(
|
|
|
318
327
|
if not c.is_complete and c.concept in all_lcl
|
|
319
328
|
]
|
|
320
329
|
partial_lcl = LooseConceptList(concepts=partial_concepts)
|
|
330
|
+
nullable_concepts = [
|
|
331
|
+
c.concept
|
|
332
|
+
for c in datasource.columns
|
|
333
|
+
if c.is_nullable and c.concept in all_lcl
|
|
334
|
+
]
|
|
335
|
+
nullable_lcl = LooseConceptList(concepts=nullable_concepts)
|
|
321
336
|
if not accept_partial and target_concept in partial_lcl:
|
|
322
337
|
continue
|
|
323
338
|
logger.info(
|
|
@@ -359,6 +374,7 @@ def gen_select_node_from_table(
|
|
|
359
374
|
parents=[],
|
|
360
375
|
depth=depth,
|
|
361
376
|
partial_concepts=[c for c in all_concepts if c in partial_lcl],
|
|
377
|
+
nullable_concepts=[c for c in all_concepts if c in nullable_lcl],
|
|
362
378
|
accept_partial=accept_partial,
|
|
363
379
|
datasource=datasource,
|
|
364
380
|
grain=Grain(components=all_concepts),
|
|
@@ -377,6 +393,7 @@ def gen_select_node_from_table(
|
|
|
377
393
|
parents=[bcandidate],
|
|
378
394
|
depth=depth,
|
|
379
395
|
partial_concepts=bcandidate.partial_concepts,
|
|
396
|
+
nullable_concepts=bcandidate.nullable_concepts,
|
|
380
397
|
)
|
|
381
398
|
else:
|
|
382
399
|
candidate = bcandidate
|
|
@@ -469,6 +486,14 @@ def gen_select_node(
|
|
|
469
486
|
)
|
|
470
487
|
]
|
|
471
488
|
|
|
489
|
+
all_nullable = [
|
|
490
|
+
c
|
|
491
|
+
for c in all_concepts
|
|
492
|
+
if any(
|
|
493
|
+
[c.address in [x.address for x in p.nullable_concepts] for p in parents]
|
|
494
|
+
)
|
|
495
|
+
]
|
|
496
|
+
|
|
472
497
|
if all_found:
|
|
473
498
|
logger.info(
|
|
474
499
|
f"{padding(depth)}{LOGGER_PREFIX} found all optional {[c.address for c in local_optional]} via joins"
|
|
@@ -494,6 +519,7 @@ def gen_select_node(
|
|
|
494
519
|
parents=parents,
|
|
495
520
|
depth=depth,
|
|
496
521
|
partial_concepts=all_partial,
|
|
522
|
+
nullable_concepts=all_nullable,
|
|
497
523
|
grain=inferred_grain,
|
|
498
524
|
)
|
|
499
525
|
|
|
@@ -12,8 +12,8 @@ from trilogy.constants import logger
|
|
|
12
12
|
from trilogy.core.processing.utility import padding, create_log_lambda
|
|
13
13
|
from trilogy.core.processing.node_generators.common import (
|
|
14
14
|
gen_enrichment_node,
|
|
15
|
-
concept_to_relevant_joins,
|
|
16
15
|
)
|
|
16
|
+
from trilogy.core.processing.utility import concept_to_relevant_joins
|
|
17
17
|
|
|
18
18
|
LOGGER_PREFIX = "[GEN_WINDOW_NODE]"
|
|
19
19
|
|
|
@@ -13,6 +13,7 @@ from trilogy.core.models import (
|
|
|
13
13
|
Comparison,
|
|
14
14
|
Parenthetical,
|
|
15
15
|
LooseConceptList,
|
|
16
|
+
ConceptPair,
|
|
16
17
|
)
|
|
17
18
|
from trilogy.core.enums import Purpose, JoinType, PurposeLineage, Granularity
|
|
18
19
|
from trilogy.utility import unique
|
|
@@ -61,11 +62,9 @@ def resolve_concept_map(
|
|
|
61
62
|
for concept in input.output_concepts:
|
|
62
63
|
if concept.address not in input.non_partial_concept_addresses:
|
|
63
64
|
continue
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
and concept.address in input.hidden_concepts
|
|
68
|
-
):
|
|
65
|
+
if isinstance(input, QueryDatasource) and concept.address in [
|
|
66
|
+
x.address for x in input.hidden_concepts
|
|
67
|
+
]:
|
|
69
68
|
continue
|
|
70
69
|
if concept.address in full_addresses:
|
|
71
70
|
|
|
@@ -82,10 +81,9 @@ def resolve_concept_map(
|
|
|
82
81
|
for concept in input.output_concepts:
|
|
83
82
|
if concept.address not in [t.address for t in inherited_inputs]:
|
|
84
83
|
continue
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
):
|
|
84
|
+
if isinstance(input, QueryDatasource) and concept.address in [
|
|
85
|
+
x.address for x in input.hidden_concepts
|
|
86
|
+
]:
|
|
89
87
|
continue
|
|
90
88
|
if len(concept_map.get(concept.address, [])) == 0:
|
|
91
89
|
concept_map[concept.address].add(input)
|
|
@@ -94,6 +92,7 @@ def resolve_concept_map(
|
|
|
94
92
|
if target.address not in inherited:
|
|
95
93
|
# an empty source means it is defined in this CTE
|
|
96
94
|
concept_map[target.address] = set()
|
|
95
|
+
|
|
97
96
|
return concept_map
|
|
98
97
|
|
|
99
98
|
|
|
@@ -124,6 +123,26 @@ def get_all_parent_partial(
|
|
|
124
123
|
)
|
|
125
124
|
|
|
126
125
|
|
|
126
|
+
def get_all_parent_nullable(
|
|
127
|
+
all_concepts: List[Concept], parents: List["StrategyNode"]
|
|
128
|
+
) -> List[Concept]:
|
|
129
|
+
return unique(
|
|
130
|
+
[
|
|
131
|
+
c
|
|
132
|
+
for c in all_concepts
|
|
133
|
+
if len(
|
|
134
|
+
[
|
|
135
|
+
p
|
|
136
|
+
for p in parents
|
|
137
|
+
if c.address in [x.address for x in p.nullable_concepts]
|
|
138
|
+
]
|
|
139
|
+
)
|
|
140
|
+
>= 1
|
|
141
|
+
],
|
|
142
|
+
"address",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
127
146
|
class StrategyNode:
|
|
128
147
|
source_type = SourceType.ABSTRACT
|
|
129
148
|
|
|
@@ -136,6 +155,7 @@ class StrategyNode:
|
|
|
136
155
|
whole_grain: bool = False,
|
|
137
156
|
parents: List["StrategyNode"] | None = None,
|
|
138
157
|
partial_concepts: List[Concept] | None = None,
|
|
158
|
+
nullable_concepts: List[Concept] | None = None,
|
|
139
159
|
depth: int = 0,
|
|
140
160
|
conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
141
161
|
force_group: bool | None = None,
|
|
@@ -159,6 +179,9 @@ class StrategyNode:
|
|
|
159
179
|
self.partial_concepts = partial_concepts or get_all_parent_partial(
|
|
160
180
|
self.output_concepts, self.parents
|
|
161
181
|
)
|
|
182
|
+
self.nullable_concepts = nullable_concepts or get_all_parent_nullable(
|
|
183
|
+
self.output_concepts, self.parents
|
|
184
|
+
)
|
|
162
185
|
|
|
163
186
|
self.depth = depth
|
|
164
187
|
self.conditions = conditions
|
|
@@ -226,7 +249,10 @@ class StrategyNode:
|
|
|
226
249
|
def remove_output_concepts(self, concepts: List[Concept]):
|
|
227
250
|
for x in concepts:
|
|
228
251
|
self.hidden_concepts.append(x)
|
|
229
|
-
|
|
252
|
+
addresses = [x.address for x in concepts]
|
|
253
|
+
self.output_concepts = [
|
|
254
|
+
x for x in self.output_concepts if x.address not in addresses
|
|
255
|
+
]
|
|
230
256
|
self.rebuild_cache()
|
|
231
257
|
|
|
232
258
|
@property
|
|
@@ -257,6 +283,7 @@ class StrategyNode:
|
|
|
257
283
|
targets=self.output_concepts,
|
|
258
284
|
inherited_inputs=self.input_concepts + self.existence_concepts,
|
|
259
285
|
)
|
|
286
|
+
|
|
260
287
|
return QueryDatasource(
|
|
261
288
|
input_concepts=self.input_concepts,
|
|
262
289
|
output_concepts=self.output_concepts,
|
|
@@ -267,6 +294,7 @@ class StrategyNode:
|
|
|
267
294
|
grain=grain,
|
|
268
295
|
condition=self.conditions,
|
|
269
296
|
partial_concepts=self.partial_concepts,
|
|
297
|
+
nullable_concepts=self.nullable_concepts,
|
|
270
298
|
force_group=self.force_group,
|
|
271
299
|
hidden_concepts=self.hidden_concepts,
|
|
272
300
|
)
|
|
@@ -295,6 +323,7 @@ class StrategyNode:
|
|
|
295
323
|
whole_grain=self.whole_grain,
|
|
296
324
|
parents=list(self.parents),
|
|
297
325
|
partial_concepts=list(self.partial_concepts),
|
|
326
|
+
nullable_concepts=list(self.nullable_concepts),
|
|
298
327
|
depth=self.depth,
|
|
299
328
|
conditions=self.conditions,
|
|
300
329
|
force_group=self.force_group,
|
|
@@ -312,7 +341,7 @@ class NodeJoin:
|
|
|
312
341
|
concepts: List[Concept]
|
|
313
342
|
join_type: JoinType
|
|
314
343
|
filter_to_mutual: bool = False
|
|
315
|
-
concept_pairs: list[
|
|
344
|
+
concept_pairs: list[ConceptPair] | None = None
|
|
316
345
|
|
|
317
346
|
def __post_init__(self):
|
|
318
347
|
if self.concept_pairs:
|
|
@@ -20,6 +20,7 @@ from trilogy.core.processing.nodes.base_node import (
|
|
|
20
20
|
)
|
|
21
21
|
from trilogy.utility import unique
|
|
22
22
|
from trilogy.core.processing.utility import is_scalar_condition
|
|
23
|
+
from trilogy.core.processing.utility import find_nullable_concepts
|
|
23
24
|
|
|
24
25
|
LOGGER_PREFIX = "[CONCEPT DETAIL - GROUP NODE]"
|
|
25
26
|
|
|
@@ -37,6 +38,7 @@ class GroupNode(StrategyNode):
|
|
|
37
38
|
parents: List["StrategyNode"] | None = None,
|
|
38
39
|
depth: int = 0,
|
|
39
40
|
partial_concepts: Optional[List[Concept]] = None,
|
|
41
|
+
nullable_concepts: Optional[List[Concept]] = None,
|
|
40
42
|
force_group: bool | None = None,
|
|
41
43
|
conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
42
44
|
existence_concepts: List[Concept] | None = None,
|
|
@@ -50,6 +52,7 @@ class GroupNode(StrategyNode):
|
|
|
50
52
|
parents=parents,
|
|
51
53
|
depth=depth,
|
|
52
54
|
partial_concepts=partial_concepts,
|
|
55
|
+
nullable_concepts=nullable_concepts,
|
|
53
56
|
force_group=force_group,
|
|
54
57
|
conditions=conditions,
|
|
55
58
|
existence_concepts=existence_concepts,
|
|
@@ -113,27 +116,34 @@ class GroupNode(StrategyNode):
|
|
|
113
116
|
f" {parent.grain}"
|
|
114
117
|
)
|
|
115
118
|
source_type = SourceType.GROUP
|
|
116
|
-
|
|
119
|
+
source_map = resolve_concept_map(
|
|
120
|
+
parent_sources,
|
|
121
|
+
targets=(
|
|
122
|
+
unique(
|
|
123
|
+
self.output_concepts + self.conditions.concept_arguments,
|
|
124
|
+
"address",
|
|
125
|
+
)
|
|
126
|
+
if self.conditions
|
|
127
|
+
else self.output_concepts
|
|
128
|
+
),
|
|
129
|
+
inherited_inputs=self.input_concepts + self.existence_concepts,
|
|
130
|
+
)
|
|
131
|
+
nullable_addresses = find_nullable_concepts(
|
|
132
|
+
source_map=source_map, joins=[], datasources=parent_sources
|
|
133
|
+
)
|
|
134
|
+
nullable_concepts = [
|
|
135
|
+
x for x in self.output_concepts if x.address in nullable_addresses
|
|
136
|
+
]
|
|
117
137
|
base = QueryDatasource(
|
|
118
138
|
input_concepts=self.input_concepts,
|
|
119
139
|
output_concepts=self.output_concepts,
|
|
120
140
|
datasources=parent_sources,
|
|
121
141
|
source_type=source_type,
|
|
122
|
-
source_map=
|
|
123
|
-
parent_sources,
|
|
124
|
-
targets=(
|
|
125
|
-
unique(
|
|
126
|
-
self.output_concepts + self.conditions.concept_arguments,
|
|
127
|
-
"address",
|
|
128
|
-
)
|
|
129
|
-
if self.conditions
|
|
130
|
-
else self.output_concepts
|
|
131
|
-
),
|
|
132
|
-
inherited_inputs=self.input_concepts + self.existence_concepts,
|
|
133
|
-
),
|
|
142
|
+
source_map=source_map,
|
|
134
143
|
joins=[],
|
|
135
144
|
grain=grain,
|
|
136
145
|
partial_concepts=self.partial_concepts,
|
|
146
|
+
nullable_concepts=nullable_concepts,
|
|
137
147
|
condition=self.conditions,
|
|
138
148
|
)
|
|
139
149
|
# if there is a condition on a group node and it's not scalar
|
|
@@ -141,18 +151,20 @@ class GroupNode(StrategyNode):
|
|
|
141
151
|
if self.conditions and not is_scalar_condition(self.conditions):
|
|
142
152
|
base.condition = None
|
|
143
153
|
base.output_concepts = self.output_concepts + self.conditions.row_arguments
|
|
154
|
+
source_map = resolve_concept_map(
|
|
155
|
+
[base],
|
|
156
|
+
targets=self.output_concepts,
|
|
157
|
+
inherited_inputs=base.output_concepts,
|
|
158
|
+
)
|
|
144
159
|
return QueryDatasource(
|
|
145
160
|
input_concepts=base.output_concepts,
|
|
146
161
|
output_concepts=self.output_concepts,
|
|
147
162
|
datasources=[base],
|
|
148
163
|
source_type=SourceType.SELECT,
|
|
149
|
-
source_map=
|
|
150
|
-
[base],
|
|
151
|
-
targets=self.output_concepts,
|
|
152
|
-
inherited_inputs=base.output_concepts,
|
|
153
|
-
),
|
|
164
|
+
source_map=source_map,
|
|
154
165
|
joins=[],
|
|
155
166
|
grain=grain,
|
|
167
|
+
nullable_concepts=base.nullable_concepts,
|
|
156
168
|
partial_concepts=self.partial_concepts,
|
|
157
169
|
condition=self.conditions,
|
|
158
170
|
)
|
|
@@ -168,6 +180,7 @@ class GroupNode(StrategyNode):
|
|
|
168
180
|
parents=self.parents,
|
|
169
181
|
depth=self.depth,
|
|
170
182
|
partial_concepts=list(self.partial_concepts),
|
|
183
|
+
nullable_concepts=list(self.nullable_concepts),
|
|
171
184
|
force_group=self.force_group,
|
|
172
185
|
conditions=self.conditions,
|
|
173
186
|
existence_concepts=list(self.existence_concepts),
|
|
@@ -22,7 +22,7 @@ from trilogy.core.processing.nodes.base_node import (
|
|
|
22
22
|
resolve_concept_map,
|
|
23
23
|
NodeJoin,
|
|
24
24
|
)
|
|
25
|
-
from trilogy.core.processing.utility import get_node_joins
|
|
25
|
+
from trilogy.core.processing.utility import get_node_joins, find_nullable_concepts
|
|
26
26
|
|
|
27
27
|
LOGGER_PREFIX = "[CONCEPT DETAIL - MERGE NODE]"
|
|
28
28
|
|
|
@@ -110,6 +110,7 @@ class MergeNode(StrategyNode):
|
|
|
110
110
|
join_concepts: Optional[List] = None,
|
|
111
111
|
force_join_type: Optional[JoinType] = None,
|
|
112
112
|
partial_concepts: Optional[List[Concept]] = None,
|
|
113
|
+
nullable_concepts: Optional[List[Concept]] = None,
|
|
113
114
|
force_group: bool | None = None,
|
|
114
115
|
depth: int = 0,
|
|
115
116
|
grain: Grain | None = None,
|
|
@@ -127,6 +128,7 @@ class MergeNode(StrategyNode):
|
|
|
127
128
|
parents=parents,
|
|
128
129
|
depth=depth,
|
|
129
130
|
partial_concepts=partial_concepts,
|
|
131
|
+
nullable_concepts=nullable_concepts,
|
|
130
132
|
force_group=force_group,
|
|
131
133
|
grain=grain,
|
|
132
134
|
conditions=conditions,
|
|
@@ -192,7 +194,7 @@ class MergeNode(StrategyNode):
|
|
|
192
194
|
pregrain: Grain,
|
|
193
195
|
grain: Grain,
|
|
194
196
|
environment: Environment,
|
|
195
|
-
) -> List[BaseJoin]:
|
|
197
|
+
) -> List[BaseJoin | UnnestJoin]:
|
|
196
198
|
# only finally, join between them for unique values
|
|
197
199
|
dataset_list: List[QueryDatasource] = sorted(
|
|
198
200
|
final_datasets, key=lambda x: -len(x.grain.components_copy)
|
|
@@ -308,7 +310,7 @@ class MergeNode(StrategyNode):
|
|
|
308
310
|
)
|
|
309
311
|
join_candidates = [x for x in final_datasets if x not in existence_final]
|
|
310
312
|
if len(join_candidates) > 1:
|
|
311
|
-
joins = self.generate_joins(
|
|
313
|
+
joins: List[BaseJoin | UnnestJoin] = self.generate_joins(
|
|
312
314
|
join_candidates, final_joins, pregrain, grain, self.environment
|
|
313
315
|
)
|
|
314
316
|
else:
|
|
@@ -318,7 +320,7 @@ class MergeNode(StrategyNode):
|
|
|
318
320
|
)
|
|
319
321
|
full_join_concepts = []
|
|
320
322
|
for join in joins:
|
|
321
|
-
if join.join_type == JoinType.FULL:
|
|
323
|
+
if isinstance(join, BaseJoin) and join.join_type == JoinType.FULL:
|
|
322
324
|
full_join_concepts += join.concepts
|
|
323
325
|
if self.whole_grain:
|
|
324
326
|
force_group = False
|
|
@@ -341,6 +343,9 @@ class MergeNode(StrategyNode):
|
|
|
341
343
|
inherited_inputs=self.input_concepts + self.existence_concepts,
|
|
342
344
|
full_joins=full_join_concepts,
|
|
343
345
|
)
|
|
346
|
+
nullable_concepts = find_nullable_concepts(
|
|
347
|
+
source_map=source_map, joins=joins, datasources=final_datasets
|
|
348
|
+
)
|
|
344
349
|
qds = QueryDatasource(
|
|
345
350
|
input_concepts=unique(self.input_concepts, "address"),
|
|
346
351
|
output_concepts=unique(self.output_concepts, "address"),
|
|
@@ -349,6 +354,9 @@ class MergeNode(StrategyNode):
|
|
|
349
354
|
source_map=source_map,
|
|
350
355
|
joins=qd_joins,
|
|
351
356
|
grain=grain,
|
|
357
|
+
nullable_concepts=[
|
|
358
|
+
x for x in self.output_concepts if x.address in nullable_concepts
|
|
359
|
+
],
|
|
352
360
|
partial_concepts=self.partial_concepts,
|
|
353
361
|
force_group=force_group,
|
|
354
362
|
condition=self.conditions,
|
|
@@ -369,9 +377,10 @@ class MergeNode(StrategyNode):
|
|
|
369
377
|
force_group=self.force_group,
|
|
370
378
|
grain=self.grain,
|
|
371
379
|
conditions=self.conditions,
|
|
380
|
+
nullable_concepts=list(self.nullable_concepts),
|
|
372
381
|
hidden_concepts=list(self.hidden_concepts),
|
|
373
382
|
virtual_output_concepts=list(self.virtual_output_concepts),
|
|
374
|
-
node_joins=self.node_joins,
|
|
383
|
+
node_joins=list(self.node_joins) if self.node_joins else None,
|
|
375
384
|
join_concepts=list(self.join_concepts) if self.join_concepts else None,
|
|
376
385
|
force_join_type=self.force_join_type,
|
|
377
386
|
existence_concepts=list(self.existence_concepts),
|