pytrilogy 0.0.2.49__py3-none-any.whl → 0.0.2.51__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.49.dist-info → pytrilogy-0.0.2.51.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/RECORD +43 -41
- trilogy/__init__.py +1 -1
- trilogy/core/enums.py +11 -0
- trilogy/core/functions.py +4 -1
- trilogy/core/internal.py +5 -1
- trilogy/core/models.py +135 -263
- trilogy/core/processing/concept_strategies_v3.py +14 -7
- trilogy/core/processing/node_generators/basic_node.py +7 -3
- trilogy/core/processing/node_generators/common.py +8 -5
- trilogy/core/processing/node_generators/filter_node.py +5 -8
- trilogy/core/processing/node_generators/group_node.py +24 -9
- trilogy/core/processing/node_generators/group_to_node.py +0 -2
- trilogy/core/processing/node_generators/multiselect_node.py +4 -5
- trilogy/core/processing/node_generators/node_merge_node.py +14 -3
- trilogy/core/processing/node_generators/rowset_node.py +3 -5
- trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
- trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +203 -0
- trilogy/core/processing/node_generators/select_merge_node.py +153 -66
- trilogy/core/processing/node_generators/union_node.py +0 -1
- trilogy/core/processing/node_generators/unnest_node.py +0 -2
- trilogy/core/processing/node_generators/window_node.py +0 -2
- trilogy/core/processing/nodes/base_node.py +2 -36
- trilogy/core/processing/nodes/filter_node.py +0 -3
- trilogy/core/processing/nodes/group_node.py +19 -13
- trilogy/core/processing/nodes/merge_node.py +2 -5
- trilogy/core/processing/nodes/select_node_v2.py +0 -4
- trilogy/core/processing/nodes/union_node.py +0 -3
- trilogy/core/processing/nodes/unnest_node.py +0 -3
- trilogy/core/processing/nodes/window_node.py +0 -3
- trilogy/core/processing/utility.py +3 -0
- trilogy/core/query_processor.py +0 -1
- trilogy/dialect/base.py +14 -2
- trilogy/dialect/duckdb.py +7 -0
- trilogy/hooks/graph_hook.py +17 -1
- trilogy/parsing/common.py +68 -17
- trilogy/parsing/parse_engine.py +70 -20
- trilogy/parsing/render.py +8 -1
- trilogy/parsing/trilogy.lark +3 -1
- {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/top_level.txt +0 -0
|
@@ -359,7 +359,6 @@ def generate_node(
|
|
|
359
359
|
input_concepts=[],
|
|
360
360
|
output_concepts=constant_targets,
|
|
361
361
|
environment=environment,
|
|
362
|
-
g=g,
|
|
363
362
|
parents=[],
|
|
364
363
|
depth=depth + 1,
|
|
365
364
|
)
|
|
@@ -450,6 +449,7 @@ def generate_node(
|
|
|
450
449
|
conditions=conditions,
|
|
451
450
|
)
|
|
452
451
|
if not check:
|
|
452
|
+
|
|
453
453
|
logger.info(
|
|
454
454
|
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Could not resolve root concepts, checking for expanded concepts"
|
|
455
455
|
)
|
|
@@ -471,7 +471,6 @@ def generate_node(
|
|
|
471
471
|
x
|
|
472
472
|
for x in ex_resolve.output_concepts
|
|
473
473
|
if x.address not in [y.address for y in root_targets]
|
|
474
|
-
and x not in ex_resolve.grain.components
|
|
475
474
|
]
|
|
476
475
|
|
|
477
476
|
pseudonyms = [
|
|
@@ -479,10 +478,19 @@ def generate_node(
|
|
|
479
478
|
for x in extra
|
|
480
479
|
if any(x.address in y.pseudonyms for y in root_targets)
|
|
481
480
|
]
|
|
482
|
-
|
|
483
|
-
|
|
481
|
+
logger.info(
|
|
482
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} reducing final outputs, was {[c.address for c in ex_resolve.output_concepts]} with extra {[c.address for c in extra]}"
|
|
483
|
+
)
|
|
484
|
+
base = [
|
|
485
|
+
x for x in ex_resolve.output_concepts if x.address not in extra
|
|
486
|
+
]
|
|
487
|
+
for x in root_targets:
|
|
488
|
+
if x.address not in base:
|
|
489
|
+
base.append(x)
|
|
490
|
+
expanded.set_output_concepts(base)
|
|
484
491
|
# but hide them
|
|
485
492
|
if pseudonyms:
|
|
493
|
+
expanded.add_output_concepts(pseudonyms)
|
|
486
494
|
logger.info(
|
|
487
495
|
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Hiding pseudonyms{[c.address for c in pseudonyms]}"
|
|
488
496
|
)
|
|
@@ -906,10 +914,10 @@ def _search_concepts(
|
|
|
906
914
|
input_concepts=non_virtual,
|
|
907
915
|
output_concepts=non_virtual,
|
|
908
916
|
environment=environment,
|
|
909
|
-
g=g,
|
|
910
917
|
parents=stack,
|
|
911
918
|
depth=depth,
|
|
912
919
|
)
|
|
920
|
+
|
|
913
921
|
# ensure we can resolve our final merge
|
|
914
922
|
output.resolve()
|
|
915
923
|
if condition_required and conditions:
|
|
@@ -919,7 +927,7 @@ def _search_concepts(
|
|
|
919
927
|
output, environment, g, where=conditions, history=history
|
|
920
928
|
)
|
|
921
929
|
logger.info(
|
|
922
|
-
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Graph is connected, returning
|
|
930
|
+
f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Graph is connected, returning {type(output)} node partial {[c.address for c in output.partial_concepts]}"
|
|
923
931
|
)
|
|
924
932
|
return output
|
|
925
933
|
|
|
@@ -987,7 +995,6 @@ def source_query_concepts(
|
|
|
987
995
|
x for x in root.output_concepts if x.address not in root.hidden_concepts
|
|
988
996
|
],
|
|
989
997
|
environment=environment,
|
|
990
|
-
g=g,
|
|
991
998
|
parents=[root],
|
|
992
999
|
partial_concepts=root.partial_concepts,
|
|
993
1000
|
)
|
|
@@ -44,7 +44,7 @@ def gen_basic_node(
|
|
|
44
44
|
conditions: WhereClause | None = None,
|
|
45
45
|
):
|
|
46
46
|
depth_prefix = "\t" * depth
|
|
47
|
-
parent_concepts = resolve_function_parent_concepts(concept)
|
|
47
|
+
parent_concepts = resolve_function_parent_concepts(concept, environment=environment)
|
|
48
48
|
|
|
49
49
|
logger.info(
|
|
50
50
|
f"{depth_prefix}{LOGGER_PREFIX} basic node for {concept} has parents {[x.address for x in parent_concepts]}"
|
|
@@ -61,12 +61,16 @@ def gen_basic_node(
|
|
|
61
61
|
f"{depth_prefix}{LOGGER_PREFIX} basic node for {concept} has equivalent optional {[x.address for x in equivalent_optional]}"
|
|
62
62
|
)
|
|
63
63
|
for eo in equivalent_optional:
|
|
64
|
-
parent_concepts += resolve_function_parent_concepts(eo)
|
|
64
|
+
parent_concepts += resolve_function_parent_concepts(eo, environment=environment)
|
|
65
65
|
non_equivalent_optional = [
|
|
66
66
|
x for x in local_optional if x not in equivalent_optional
|
|
67
67
|
]
|
|
68
|
+
all_parents = parent_concepts + non_equivalent_optional
|
|
69
|
+
logger.info(
|
|
70
|
+
f"{depth_prefix}{LOGGER_PREFIX} Fetching parents {[x.address for x in all_parents]}"
|
|
71
|
+
)
|
|
68
72
|
parent_node: StrategyNode = source_concepts(
|
|
69
|
-
mandatory_list=
|
|
73
|
+
mandatory_list=all_parents,
|
|
70
74
|
environment=environment,
|
|
71
75
|
g=g,
|
|
72
76
|
depth=depth + 1,
|
|
@@ -20,12 +20,16 @@ from trilogy.core.processing.nodes.merge_node import MergeNode
|
|
|
20
20
|
from trilogy.utility import unique
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def resolve_function_parent_concepts(
|
|
23
|
+
def resolve_function_parent_concepts(
|
|
24
|
+
concept: Concept, environment: Environment
|
|
25
|
+
) -> List[Concept]:
|
|
24
26
|
if not isinstance(concept.lineage, (Function, AggregateWrapper)):
|
|
25
27
|
raise ValueError(f"Concept {concept} lineage is not function or aggregate")
|
|
26
28
|
if concept.derivation == PurposeLineage.AGGREGATE:
|
|
27
29
|
if not concept.grain.abstract:
|
|
28
|
-
base = concept.lineage.concept_arguments +
|
|
30
|
+
base = concept.lineage.concept_arguments + [
|
|
31
|
+
environment.concepts[c] for c in concept.grain.components
|
|
32
|
+
]
|
|
29
33
|
# if the base concept being aggregated is a property with a key
|
|
30
34
|
# keep the key as a parent
|
|
31
35
|
else:
|
|
@@ -56,6 +60,7 @@ def resolve_condition_parent_concepts(
|
|
|
56
60
|
|
|
57
61
|
def resolve_filter_parent_concepts(
|
|
58
62
|
concept: Concept,
|
|
63
|
+
environment: Environment,
|
|
59
64
|
) -> Tuple[Concept, List[Concept], List[Tuple[Concept, ...]]]:
|
|
60
65
|
if not isinstance(concept.lineage, FilterItem):
|
|
61
66
|
raise ValueError(
|
|
@@ -70,7 +75,7 @@ def resolve_filter_parent_concepts(
|
|
|
70
75
|
base_rows += condition_rows
|
|
71
76
|
base_existence += condition_existence
|
|
72
77
|
if direct_parent.grain:
|
|
73
|
-
base_rows += direct_parent.grain.
|
|
78
|
+
base_rows += [environment.concepts[c] for c in direct_parent.grain.components]
|
|
74
79
|
if (
|
|
75
80
|
isinstance(direct_parent, Concept)
|
|
76
81
|
and direct_parent.purpose == Purpose.PROPERTY
|
|
@@ -130,7 +135,6 @@ def gen_property_enrichment_node(
|
|
|
130
135
|
),
|
|
131
136
|
output_concepts=base_node.output_concepts + extra_properties,
|
|
132
137
|
environment=environment,
|
|
133
|
-
g=g,
|
|
134
138
|
parents=[
|
|
135
139
|
base_node,
|
|
136
140
|
]
|
|
@@ -209,7 +213,6 @@ def gen_enrichment_node(
|
|
|
209
213
|
input_concepts=unique(join_keys + extra_required + non_hidden, "address"),
|
|
210
214
|
output_concepts=unique(join_keys + extra_required + non_hidden, "address"),
|
|
211
215
|
environment=environment,
|
|
212
|
-
g=g,
|
|
213
216
|
parents=[enrich_node, base_node],
|
|
214
217
|
force_group=False,
|
|
215
218
|
preexisting_conditions=conditions.conditional if conditions else None,
|
|
@@ -28,7 +28,7 @@ def gen_filter_node(
|
|
|
28
28
|
conditions: WhereClause | None = None,
|
|
29
29
|
) -> StrategyNode | None:
|
|
30
30
|
immediate_parent, parent_row_concepts, parent_existence_concepts = (
|
|
31
|
-
resolve_filter_parent_concepts(concept)
|
|
31
|
+
resolve_filter_parent_concepts(concept, environment)
|
|
32
32
|
)
|
|
33
33
|
if not isinstance(concept.lineage, FilterItem):
|
|
34
34
|
raise SyntaxError('Filter node must have a lineage of type "FilterItem"')
|
|
@@ -117,7 +117,6 @@ def gen_filter_node(
|
|
|
117
117
|
input_concepts=row_parent.output_concepts,
|
|
118
118
|
output_concepts=[concept] + row_parent.output_concepts,
|
|
119
119
|
environment=row_parent.environment,
|
|
120
|
-
g=row_parent.g,
|
|
121
120
|
parents=[row_parent],
|
|
122
121
|
depth=row_parent.depth,
|
|
123
122
|
partial_concepts=row_parent.partial_concepts,
|
|
@@ -137,8 +136,8 @@ def gen_filter_node(
|
|
|
137
136
|
parent.add_existence_concepts(flattened_existence, False).set_output_concepts(
|
|
138
137
|
expected_output, False
|
|
139
138
|
)
|
|
140
|
-
parent.grain = Grain(
|
|
141
|
-
|
|
139
|
+
parent.grain = Grain.from_concepts(
|
|
140
|
+
(
|
|
142
141
|
list(immediate_parent.keys)
|
|
143
142
|
if immediate_parent.keys
|
|
144
143
|
else [immediate_parent]
|
|
@@ -161,10 +160,9 @@ def gen_filter_node(
|
|
|
161
160
|
),
|
|
162
161
|
output_concepts=[concept, immediate_parent] + parent_row_concepts,
|
|
163
162
|
environment=environment,
|
|
164
|
-
g=g,
|
|
165
163
|
parents=core_parents,
|
|
166
|
-
grain=Grain(
|
|
167
|
-
|
|
164
|
+
grain=Grain.from_concepts(
|
|
165
|
+
[immediate_parent] + parent_row_concepts,
|
|
168
166
|
),
|
|
169
167
|
preexisting_conditions=conditions.conditional if conditions else None,
|
|
170
168
|
)
|
|
@@ -202,7 +200,6 @@ def gen_filter_node(
|
|
|
202
200
|
]
|
|
203
201
|
+ local_optional,
|
|
204
202
|
environment=environment,
|
|
205
|
-
g=g,
|
|
206
203
|
parents=[
|
|
207
204
|
# this node fetches only what we need to filter
|
|
208
205
|
filter_node,
|
|
@@ -34,7 +34,7 @@ def gen_group_node(
|
|
|
34
34
|
# aggregates MUST always group to the proper grain
|
|
35
35
|
# except when the
|
|
36
36
|
parent_concepts: List[Concept] = unique(
|
|
37
|
-
resolve_function_parent_concepts(concept), "address"
|
|
37
|
+
resolve_function_parent_concepts(concept, environment=environment), "address"
|
|
38
38
|
)
|
|
39
39
|
logger.info(
|
|
40
40
|
f"{padding(depth)}{LOGGER_PREFIX} parent concepts are {[x.address for x in parent_concepts]} from group grain {concept.grain}"
|
|
@@ -43,18 +43,28 @@ def gen_group_node(
|
|
|
43
43
|
# if the aggregation has a grain, we need to ensure these are the ONLY optional in the output of the select
|
|
44
44
|
output_concepts = [concept]
|
|
45
45
|
|
|
46
|
-
if
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
if (
|
|
47
|
+
concept.grain
|
|
48
|
+
and len(concept.grain.components) > 0
|
|
49
|
+
and not concept.grain.abstract
|
|
50
|
+
):
|
|
51
|
+
grain_components = [environment.concepts[c] for c in concept.grain.components]
|
|
50
52
|
parent_concepts += grain_components
|
|
51
53
|
output_concepts += grain_components
|
|
52
54
|
for possible_agg in local_optional:
|
|
55
|
+
|
|
53
56
|
if not isinstance(possible_agg.lineage, (AggregateWrapper, Function)):
|
|
54
57
|
continue
|
|
58
|
+
logger.info(possible_agg)
|
|
59
|
+
if possible_agg.grain and possible_agg.grain != concept.grain:
|
|
60
|
+
logger.info(
|
|
61
|
+
f"{padding(depth)}{LOGGER_PREFIX} mismatched equivalent group by with grain {possible_agg.grain} for {concept.address}"
|
|
62
|
+
)
|
|
63
|
+
|
|
55
64
|
if possible_agg.grain and possible_agg.grain == concept.grain:
|
|
56
65
|
agg_parents: List[Concept] = resolve_function_parent_concepts(
|
|
57
|
-
possible_agg
|
|
66
|
+
possible_agg,
|
|
67
|
+
environment=environment,
|
|
58
68
|
)
|
|
59
69
|
if set([x.address for x in agg_parents]).issubset(
|
|
60
70
|
set([x.address for x in parent_concepts])
|
|
@@ -63,13 +73,19 @@ def gen_group_node(
|
|
|
63
73
|
logger.info(
|
|
64
74
|
f"{padding(depth)}{LOGGER_PREFIX} found equivalent group by optional concept {possible_agg.address} for {concept.address}"
|
|
65
75
|
)
|
|
66
|
-
elif Grain(
|
|
76
|
+
elif Grain.from_concepts(agg_parents) == Grain.from_concepts(
|
|
77
|
+
parent_concepts
|
|
78
|
+
):
|
|
67
79
|
extra = [x for x in agg_parents if x.address not in parent_concepts]
|
|
68
80
|
parent_concepts += extra
|
|
69
81
|
output_concepts.append(possible_agg)
|
|
70
82
|
logger.info(
|
|
71
83
|
f"{padding(depth)}{LOGGER_PREFIX} found equivalent group by optional concept {possible_agg.address} for {concept.address}"
|
|
72
84
|
)
|
|
85
|
+
else:
|
|
86
|
+
logger.info(
|
|
87
|
+
f"{padding(depth)}{LOGGER_PREFIX} mismatched grain {Grain.from_concepts(agg_parents)} vs {Grain.from_concepts(parent_concepts)}"
|
|
88
|
+
)
|
|
73
89
|
if parent_concepts:
|
|
74
90
|
logger.info(
|
|
75
91
|
f"{padding(depth)}{LOGGER_PREFIX} fetching group node parents {LooseConceptList(concepts=parent_concepts)}"
|
|
@@ -94,13 +110,12 @@ def gen_group_node(
|
|
|
94
110
|
|
|
95
111
|
# the keys we group by
|
|
96
112
|
# are what we can use for enrichment
|
|
97
|
-
group_key_parents = concept.grain.
|
|
113
|
+
group_key_parents = [environment.concepts[c] for c in concept.grain.components]
|
|
98
114
|
|
|
99
115
|
group_node = GroupNode(
|
|
100
116
|
output_concepts=output_concepts,
|
|
101
117
|
input_concepts=parent_concepts,
|
|
102
118
|
environment=environment,
|
|
103
|
-
g=g,
|
|
104
119
|
parents=parents,
|
|
105
120
|
depth=depth,
|
|
106
121
|
preexisting_conditions=conditions.conditional if conditions else None,
|
|
@@ -45,7 +45,6 @@ def gen_group_to_node(
|
|
|
45
45
|
output_concepts=parent_concepts + [concept],
|
|
46
46
|
input_concepts=parent_concepts,
|
|
47
47
|
environment=environment,
|
|
48
|
-
g=g,
|
|
49
48
|
parents=parents,
|
|
50
49
|
depth=depth,
|
|
51
50
|
)
|
|
@@ -76,7 +75,6 @@ def gen_group_to_node(
|
|
|
76
75
|
+ [x for x in parent_concepts if x.address != concept.address],
|
|
77
76
|
output_concepts=[concept] + local_optional,
|
|
78
77
|
environment=environment,
|
|
79
|
-
g=g,
|
|
80
78
|
parents=[
|
|
81
79
|
# this node gets the group
|
|
82
80
|
group_node,
|
|
@@ -8,12 +8,13 @@ from trilogy.core.models import (
|
|
|
8
8
|
Concept,
|
|
9
9
|
Conditional,
|
|
10
10
|
Environment,
|
|
11
|
+
Grain,
|
|
11
12
|
MultiSelectStatement,
|
|
12
13
|
WhereClause,
|
|
13
14
|
)
|
|
14
15
|
from trilogy.core.processing.node_generators.common import resolve_join_order
|
|
15
16
|
from trilogy.core.processing.nodes import History, MergeNode, NodeJoin
|
|
16
|
-
from trilogy.core.processing.nodes.base_node import StrategyNode
|
|
17
|
+
from trilogy.core.processing.nodes.base_node import StrategyNode
|
|
17
18
|
from trilogy.core.processing.utility import concept_to_relevant_joins, padding
|
|
18
19
|
|
|
19
20
|
LOGGER_PREFIX = "[GEN_MULTISELECT_NODE]"
|
|
@@ -108,7 +109,6 @@ def gen_multiselect_node(
|
|
|
108
109
|
input_concepts=[x for y in base_parents for x in y.output_concepts],
|
|
109
110
|
output_concepts=[x for y in base_parents for x in y.output_concepts],
|
|
110
111
|
environment=environment,
|
|
111
|
-
g=g,
|
|
112
112
|
depth=depth,
|
|
113
113
|
parents=base_parents,
|
|
114
114
|
node_joins=node_joins,
|
|
@@ -138,8 +138,8 @@ def gen_multiselect_node(
|
|
|
138
138
|
|
|
139
139
|
# assume grain to be output of select
|
|
140
140
|
# but don't include anything aggregate at this point
|
|
141
|
-
node.resolution_cache.grain =
|
|
142
|
-
node.output_concepts,
|
|
141
|
+
node.resolution_cache.grain = Grain.from_concepts(
|
|
142
|
+
node.output_concepts,
|
|
143
143
|
)
|
|
144
144
|
possible_joins = concept_to_relevant_joins(additional_relevant)
|
|
145
145
|
if not local_optional:
|
|
@@ -178,7 +178,6 @@ def gen_multiselect_node(
|
|
|
178
178
|
input_concepts=enrich_node.output_concepts + node.output_concepts,
|
|
179
179
|
output_concepts=node.output_concepts + local_optional,
|
|
180
180
|
environment=environment,
|
|
181
|
-
g=g,
|
|
182
181
|
depth=depth,
|
|
183
182
|
parents=[
|
|
184
183
|
# this node gets the multiselect
|
|
@@ -327,13 +327,18 @@ def subgraphs_to_merge_node(
|
|
|
327
327
|
for y in x.output_concepts:
|
|
328
328
|
input_c.append(y)
|
|
329
329
|
if len(parents) == 1 and enable_early_exit:
|
|
330
|
+
logger.info(
|
|
331
|
+
f"{padding(depth)}{LOGGER_PREFIX} only one parent node, exiting early w/ {[c.address for c in parents[0].output_concepts]}"
|
|
332
|
+
)
|
|
330
333
|
return parents[0]
|
|
331
|
-
|
|
334
|
+
base_output = [x for x in all_concepts]
|
|
335
|
+
# for x in base_output:
|
|
336
|
+
# if x not in input_c:
|
|
337
|
+
# input_c.append(x)
|
|
332
338
|
return MergeNode(
|
|
333
339
|
input_concepts=unique(input_c, "address"),
|
|
334
|
-
output_concepts=
|
|
340
|
+
output_concepts=base_output,
|
|
335
341
|
environment=environment,
|
|
336
|
-
g=g,
|
|
337
342
|
parents=parents,
|
|
338
343
|
depth=depth,
|
|
339
344
|
# conditions=conditions,
|
|
@@ -369,6 +374,12 @@ def gen_merge_node(
|
|
|
369
374
|
logger.info(
|
|
370
375
|
f"{padding(depth)}{LOGGER_PREFIX} Was able to resolve graph through weak component resolution - final graph {log_graph}"
|
|
371
376
|
)
|
|
377
|
+
for flat in log_graph:
|
|
378
|
+
if set(flat) == set([x.address for x in all_concepts]):
|
|
379
|
+
logger.info(
|
|
380
|
+
f"{padding(depth)}{LOGGER_PREFIX} expanded concept resolution was identical to search resolution; breaking to avoid recursion error."
|
|
381
|
+
)
|
|
382
|
+
return None
|
|
372
383
|
return subgraphs_to_merge_node(
|
|
373
384
|
weak_resolve,
|
|
374
385
|
depth=depth,
|
|
@@ -5,6 +5,7 @@ from trilogy.core.enums import PurposeLineage
|
|
|
5
5
|
from trilogy.core.models import (
|
|
6
6
|
Concept,
|
|
7
7
|
Environment,
|
|
8
|
+
Grain,
|
|
8
9
|
MultiSelectStatement,
|
|
9
10
|
RowsetDerivationStatement,
|
|
10
11
|
RowsetItem,
|
|
@@ -12,7 +13,6 @@ from trilogy.core.models import (
|
|
|
12
13
|
WhereClause,
|
|
13
14
|
)
|
|
14
15
|
from trilogy.core.processing.nodes import History, MergeNode, StrategyNode
|
|
15
|
-
from trilogy.core.processing.nodes.base_node import concept_list_to_grain
|
|
16
16
|
from trilogy.core.processing.utility import concept_to_relevant_joins, padding
|
|
17
17
|
|
|
18
18
|
LOGGER_PREFIX = "[GEN_ROWSET_NODE]"
|
|
@@ -74,7 +74,7 @@ def gen_rowset_node(
|
|
|
74
74
|
assert node.resolution_cache
|
|
75
75
|
# assume grain to be output of select
|
|
76
76
|
# but don't include anything hidden(the non-rowset concepts)
|
|
77
|
-
node.grain =
|
|
77
|
+
node.grain = Grain.from_concepts(
|
|
78
78
|
[
|
|
79
79
|
x
|
|
80
80
|
for x in node.output_concepts
|
|
@@ -83,7 +83,6 @@ def gen_rowset_node(
|
|
|
83
83
|
y for y in node.hidden_concepts if y.derivation != PurposeLineage.ROWSET
|
|
84
84
|
]
|
|
85
85
|
],
|
|
86
|
-
parent_sources=node.resolution_cache.datasources,
|
|
87
86
|
)
|
|
88
87
|
|
|
89
88
|
node.rebuild_cache()
|
|
@@ -92,7 +91,7 @@ def gen_rowset_node(
|
|
|
92
91
|
x.address in node.output_concepts for x in local_optional
|
|
93
92
|
):
|
|
94
93
|
logger.info(
|
|
95
|
-
f"{padding(depth)}{LOGGER_PREFIX} no enrichment required for rowset node as all optional found or no optional; exiting early."
|
|
94
|
+
f"{padding(depth)}{LOGGER_PREFIX} no enrichment required for rowset node as all optional {[x.address for x in local_optional]} found or no optional; exiting early."
|
|
96
95
|
)
|
|
97
96
|
return node
|
|
98
97
|
possible_joins = concept_to_relevant_joins(
|
|
@@ -140,7 +139,6 @@ def gen_rowset_node(
|
|
|
140
139
|
input_concepts=non_hidden + non_hidden_enrich,
|
|
141
140
|
output_concepts=non_hidden + local_optional,
|
|
142
141
|
environment=environment,
|
|
143
|
-
g=g,
|
|
144
142
|
depth=depth,
|
|
145
143
|
parents=[
|
|
146
144
|
node,
|
|
File without changes
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from datetime import date, datetime, timedelta
|
|
3
|
+
from typing import List, Tuple, TypeVar
|
|
4
|
+
|
|
5
|
+
from trilogy.core.enums import ComparisonOperator
|
|
6
|
+
from trilogy.core.models import (
|
|
7
|
+
Comparison,
|
|
8
|
+
Concept,
|
|
9
|
+
Conditional,
|
|
10
|
+
Datasource,
|
|
11
|
+
DataType,
|
|
12
|
+
Function,
|
|
13
|
+
FunctionType,
|
|
14
|
+
Parenthetical,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Define a generic type that ensures start and end are the same type
|
|
18
|
+
T = TypeVar("T", int, date, datetime)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def reduce_expression(
|
|
22
|
+
var: Concept, group_tuple: list[tuple[ComparisonOperator, T]]
|
|
23
|
+
) -> bool:
|
|
24
|
+
# Track ranges
|
|
25
|
+
lower_check: T
|
|
26
|
+
upper_check: T
|
|
27
|
+
|
|
28
|
+
# if var.datatype in (DataType.FLOAT,):
|
|
29
|
+
# lower_check = float("-inf") # type: ignore
|
|
30
|
+
# upper_check = float("inf") # type: ignore
|
|
31
|
+
if var.datatype == DataType.INTEGER:
|
|
32
|
+
lower_check = float("-inf") # type: ignore
|
|
33
|
+
upper_check = float("inf") # type: ignore
|
|
34
|
+
elif var.datatype == DataType.DATE:
|
|
35
|
+
lower_check = date.min # type: ignore
|
|
36
|
+
upper_check = date.max # type: ignore
|
|
37
|
+
|
|
38
|
+
elif var.datatype == DataType.DATETIME:
|
|
39
|
+
lower_check = datetime.min # type: ignore
|
|
40
|
+
upper_check = datetime.max # type: ignore
|
|
41
|
+
else:
|
|
42
|
+
raise ValueError(f"Invalid datatype: {var.datatype}")
|
|
43
|
+
|
|
44
|
+
ranges: list[Tuple[T, T]] = []
|
|
45
|
+
for op, value in group_tuple:
|
|
46
|
+
increment: int | timedelta
|
|
47
|
+
if isinstance(value, date):
|
|
48
|
+
increment = timedelta(days=1)
|
|
49
|
+
elif isinstance(value, datetime):
|
|
50
|
+
increment = timedelta(seconds=1)
|
|
51
|
+
elif isinstance(value, int):
|
|
52
|
+
increment = 1
|
|
53
|
+
# elif isinstance(value, float):
|
|
54
|
+
# value = Decimal(value)
|
|
55
|
+
# increment = Decimal(0.0000000001)
|
|
56
|
+
|
|
57
|
+
if op == ">":
|
|
58
|
+
ranges.append(
|
|
59
|
+
(
|
|
60
|
+
value + increment,
|
|
61
|
+
upper_check,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
elif op == ">=":
|
|
65
|
+
ranges.append(
|
|
66
|
+
(
|
|
67
|
+
value,
|
|
68
|
+
upper_check,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
elif op == "<":
|
|
72
|
+
ranges.append(
|
|
73
|
+
(
|
|
74
|
+
lower_check,
|
|
75
|
+
value - increment,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
elif op == "<=":
|
|
79
|
+
ranges.append(
|
|
80
|
+
(
|
|
81
|
+
lower_check,
|
|
82
|
+
value,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
elif op == "=":
|
|
86
|
+
ranges.append(
|
|
87
|
+
(
|
|
88
|
+
value,
|
|
89
|
+
value,
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
raise ValueError(f"Invalid operator: {op}")
|
|
94
|
+
return is_fully_covered(lower_check, upper_check, ranges, increment)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def simplify_conditions(
|
|
98
|
+
conditions: list[Comparison | Conditional | Parenthetical],
|
|
99
|
+
) -> bool:
|
|
100
|
+
# Group conditions by variable
|
|
101
|
+
grouped: dict[Concept, list[tuple[ComparisonOperator, datetime | int | date]]] = (
|
|
102
|
+
defaultdict(list)
|
|
103
|
+
)
|
|
104
|
+
for condition in conditions:
|
|
105
|
+
if not isinstance(condition, Comparison):
|
|
106
|
+
return False
|
|
107
|
+
if not isinstance(
|
|
108
|
+
condition.left, (int, date, datetime, Function)
|
|
109
|
+
) and not isinstance(condition.right, (int, date, datetime, Function)):
|
|
110
|
+
return False
|
|
111
|
+
if not isinstance(condition.left, Concept) and not isinstance(
|
|
112
|
+
condition.right, Concept
|
|
113
|
+
):
|
|
114
|
+
return False
|
|
115
|
+
vars = [condition.left, condition.right]
|
|
116
|
+
concept = [x for x in vars if isinstance(x, Concept)][0]
|
|
117
|
+
comparison = [x for x in vars if not isinstance(x, Concept)][0]
|
|
118
|
+
if isinstance(comparison, Function):
|
|
119
|
+
if not comparison.operator == FunctionType.CONSTANT:
|
|
120
|
+
return False
|
|
121
|
+
first_arg = comparison.arguments[0]
|
|
122
|
+
if not isinstance(first_arg, (int, date, datetime)):
|
|
123
|
+
return False
|
|
124
|
+
comparison = first_arg
|
|
125
|
+
if not isinstance(comparison, (int, date, datetime)):
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
var = concept
|
|
129
|
+
op = condition.operator
|
|
130
|
+
grouped[var].append((op, comparison))
|
|
131
|
+
|
|
132
|
+
simplified = []
|
|
133
|
+
for var, group_tuple in grouped.items():
|
|
134
|
+
simplified.append(reduce_expression(var, group_tuple)) # type: ignore
|
|
135
|
+
|
|
136
|
+
# Final simplification
|
|
137
|
+
return True if all(isinstance(s, bool) and s for s in simplified) else False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def is_fully_covered(
|
|
141
|
+
start: T,
|
|
142
|
+
end: T,
|
|
143
|
+
ranges: List[Tuple[T, T]],
|
|
144
|
+
increment: int | timedelta,
|
|
145
|
+
):
|
|
146
|
+
"""
|
|
147
|
+
Check if the list of range pairs fully covers the set [start, end].
|
|
148
|
+
|
|
149
|
+
Parameters:
|
|
150
|
+
- start (int or float): The starting value of the set to cover.
|
|
151
|
+
- end (int or float): The ending value of the set to cover.
|
|
152
|
+
- ranges (list of tuples): List of range pairs [(start1, end1), (start2, end2), ...].
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
- bool: True if the ranges fully cover [start, end], False otherwise.
|
|
156
|
+
"""
|
|
157
|
+
# Sort ranges by their start values (and by end values for ties)
|
|
158
|
+
ranges.sort()
|
|
159
|
+
|
|
160
|
+
# Check for gaps
|
|
161
|
+
current_end = start
|
|
162
|
+
print(ranges)
|
|
163
|
+
for r_start, r_end in ranges:
|
|
164
|
+
print(r_start, r_end)
|
|
165
|
+
# If there's a gap between the current range and the previous coverage
|
|
166
|
+
print(r_start - current_end)
|
|
167
|
+
if (r_start - current_end) > increment: # type: ignore
|
|
168
|
+
print("gap")
|
|
169
|
+
return False
|
|
170
|
+
print("okay")
|
|
171
|
+
# Extend the current coverage
|
|
172
|
+
current_end = max(current_end, r_end)
|
|
173
|
+
|
|
174
|
+
# If the loop ends and we haven't reached the end, return False
|
|
175
|
+
print(current_end, end)
|
|
176
|
+
print(current_end >= end)
|
|
177
|
+
return current_end >= end
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_union_sources(datasources: list[Datasource], concepts: list[Concept]):
|
|
181
|
+
candidates: list[Datasource] = []
|
|
182
|
+
for x in datasources:
|
|
183
|
+
if all([c.address in x.output_concepts for c in concepts]):
|
|
184
|
+
if (
|
|
185
|
+
any([c.address in x.partial_concepts for c in concepts])
|
|
186
|
+
and x.non_partial_for
|
|
187
|
+
):
|
|
188
|
+
candidates.append(x)
|
|
189
|
+
|
|
190
|
+
assocs: dict[str, list[Datasource]] = defaultdict(list[Datasource])
|
|
191
|
+
for x in candidates:
|
|
192
|
+
if not x.non_partial_for:
|
|
193
|
+
continue
|
|
194
|
+
if not len(x.non_partial_for.concept_arguments) == 1:
|
|
195
|
+
continue
|
|
196
|
+
merge_key = x.non_partial_for.concept_arguments[0]
|
|
197
|
+
assocs[merge_key.address].append(x)
|
|
198
|
+
final: list[list[Datasource]] = []
|
|
199
|
+
for _, dses in assocs.items():
|
|
200
|
+
conditions = [c.non_partial_for.conditional for c in dses if c.non_partial_for]
|
|
201
|
+
if simplify_conditions(conditions):
|
|
202
|
+
final.append(dses)
|
|
203
|
+
return final
|