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,99 @@
|
|
|
1
|
+
from trilogy.core.models import Concept, Environment, Function
|
|
2
|
+
from trilogy.core.processing.nodes import (
|
|
3
|
+
GroupNode,
|
|
4
|
+
StrategyNode,
|
|
5
|
+
MergeNode,
|
|
6
|
+
NodeJoin,
|
|
7
|
+
History,
|
|
8
|
+
)
|
|
9
|
+
from typing import List
|
|
10
|
+
from trilogy.core.enums import JoinType
|
|
11
|
+
|
|
12
|
+
from trilogy.constants import logger
|
|
13
|
+
from trilogy.core.processing.utility import padding
|
|
14
|
+
from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
|
|
15
|
+
|
|
16
|
+
LOGGER_PREFIX = "[GEN_GROUP_TO_NODE]"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def gen_group_to_node(
|
|
20
|
+
concept: Concept,
|
|
21
|
+
local_optional,
|
|
22
|
+
environment: Environment,
|
|
23
|
+
g,
|
|
24
|
+
depth: int,
|
|
25
|
+
source_concepts,
|
|
26
|
+
history: History | None = None,
|
|
27
|
+
) -> GroupNode | MergeNode:
|
|
28
|
+
# aggregates MUST always group to the proper grain
|
|
29
|
+
if not isinstance(concept.lineage, Function):
|
|
30
|
+
raise SyntaxError("Group to should have function lineage")
|
|
31
|
+
parent_concepts: List[Concept] = concept.lineage.concept_arguments
|
|
32
|
+
logger.info(
|
|
33
|
+
f"{padding(depth)}{LOGGER_PREFIX} group by node has required parents {[x.address for x in parent_concepts]}"
|
|
34
|
+
)
|
|
35
|
+
parents: List[StrategyNode] = [
|
|
36
|
+
source_concepts(
|
|
37
|
+
mandatory_list=parent_concepts,
|
|
38
|
+
environment=environment,
|
|
39
|
+
g=g,
|
|
40
|
+
depth=depth + 1,
|
|
41
|
+
history=history,
|
|
42
|
+
)
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
group_node = GroupNode(
|
|
46
|
+
output_concepts=parent_concepts + [concept],
|
|
47
|
+
input_concepts=parent_concepts,
|
|
48
|
+
environment=environment,
|
|
49
|
+
g=g,
|
|
50
|
+
parents=parents,
|
|
51
|
+
depth=depth,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# early exit if no optional
|
|
55
|
+
if not local_optional:
|
|
56
|
+
return group_node
|
|
57
|
+
|
|
58
|
+
# the keys we group by
|
|
59
|
+
# are what we can use for enrichment
|
|
60
|
+
enrich_node = source_concepts( # this fetches the parent + join keys
|
|
61
|
+
# to then connect to the rest of the query
|
|
62
|
+
mandatory_list=parent_concepts + local_optional,
|
|
63
|
+
environment=environment,
|
|
64
|
+
g=g,
|
|
65
|
+
depth=depth + 1,
|
|
66
|
+
history=history,
|
|
67
|
+
)
|
|
68
|
+
if not enrich_node:
|
|
69
|
+
logger.info(
|
|
70
|
+
f"{padding(depth)}{LOGGER_PREFIX} group by node enrich node, returning group node only."
|
|
71
|
+
)
|
|
72
|
+
return group_node
|
|
73
|
+
|
|
74
|
+
return MergeNode(
|
|
75
|
+
input_concepts=[concept]
|
|
76
|
+
+ local_optional
|
|
77
|
+
+ [x for x in parent_concepts if x.address != concept.address],
|
|
78
|
+
output_concepts=[concept] + local_optional,
|
|
79
|
+
environment=environment,
|
|
80
|
+
g=g,
|
|
81
|
+
parents=[
|
|
82
|
+
# this node gets the group
|
|
83
|
+
group_node,
|
|
84
|
+
# this node gets enrichment
|
|
85
|
+
enrich_node,
|
|
86
|
+
],
|
|
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
|
+
whole_grain=True,
|
|
97
|
+
depth=depth,
|
|
98
|
+
partial_concepts=group_node.partial_concepts,
|
|
99
|
+
)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from trilogy.core.models import Concept, Environment, Datasource, Conditional
|
|
4
|
+
from trilogy.core.processing.nodes import MergeNode, History
|
|
5
|
+
import networkx as nx
|
|
6
|
+
from trilogy.core.graph_models import concept_to_node, datasource_to_node
|
|
7
|
+
from trilogy.core.processing.utility import PathInfo
|
|
8
|
+
from trilogy.constants import logger
|
|
9
|
+
from trilogy.utility import unique
|
|
10
|
+
from trilogy.core.exceptions import AmbiguousRelationshipResolutionException
|
|
11
|
+
from trilogy.core.processing.utility import padding
|
|
12
|
+
from trilogy.core.processing.graph_utils import extract_mandatory_subgraphs
|
|
13
|
+
|
|
14
|
+
LOGGER_PREFIX = "[GEN_MERGE_NODE]"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def reduce_path_concepts(paths, g) -> set[str]:
|
|
18
|
+
concept_nodes: List[Concept] = []
|
|
19
|
+
# along our path, find all the concepts required
|
|
20
|
+
for _, value in paths.items():
|
|
21
|
+
concept_nodes += [g.nodes[v]["concept"] for v in value if v.startswith("c~")]
|
|
22
|
+
final: List[Concept] = unique(concept_nodes, "address")
|
|
23
|
+
return set([x.address for x in final])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def identify_ds_join_paths(
|
|
27
|
+
all_concepts: List[Concept],
|
|
28
|
+
g,
|
|
29
|
+
datasource: Datasource,
|
|
30
|
+
accept_partial: bool = False,
|
|
31
|
+
fail: bool = False,
|
|
32
|
+
) -> PathInfo | None:
|
|
33
|
+
all_found = True
|
|
34
|
+
any_direct_found = False
|
|
35
|
+
paths = {}
|
|
36
|
+
for bitem in all_concepts:
|
|
37
|
+
item = bitem.with_default_grain()
|
|
38
|
+
target_node = concept_to_node(item)
|
|
39
|
+
try:
|
|
40
|
+
path = nx.shortest_path(
|
|
41
|
+
g,
|
|
42
|
+
source=datasource_to_node(datasource),
|
|
43
|
+
target=target_node,
|
|
44
|
+
)
|
|
45
|
+
paths[target_node] = path
|
|
46
|
+
if sum([1 for x in path if x.startswith("ds~")]) == 1:
|
|
47
|
+
any_direct_found = True
|
|
48
|
+
except nx.exception.NodeNotFound:
|
|
49
|
+
# TODO: support Verbose logging mode configuration and reenable these
|
|
50
|
+
all_found = False
|
|
51
|
+
if fail:
|
|
52
|
+
raise
|
|
53
|
+
return None
|
|
54
|
+
except nx.exception.NetworkXNoPath:
|
|
55
|
+
all_found = False
|
|
56
|
+
if fail:
|
|
57
|
+
raise
|
|
58
|
+
return None
|
|
59
|
+
if all_found and any_direct_found:
|
|
60
|
+
partial = [
|
|
61
|
+
c.concept
|
|
62
|
+
for c in datasource.columns
|
|
63
|
+
if not c.is_complete
|
|
64
|
+
and c.concept.address in [x.address for x in all_concepts]
|
|
65
|
+
]
|
|
66
|
+
if partial and not accept_partial:
|
|
67
|
+
return None
|
|
68
|
+
# join_candidates.append({"paths": paths, "datasource": datasource})
|
|
69
|
+
return PathInfo(
|
|
70
|
+
paths=paths,
|
|
71
|
+
datasource=datasource,
|
|
72
|
+
reduced_concepts=reduce_path_concepts(paths, g),
|
|
73
|
+
concept_subgraphs=extract_mandatory_subgraphs(paths, g),
|
|
74
|
+
) # {"paths": paths, "datasource": datasource}
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def gen_merge_node(
|
|
79
|
+
all_concepts: List[Concept],
|
|
80
|
+
g: nx.DiGraph,
|
|
81
|
+
environment: Environment,
|
|
82
|
+
depth: int,
|
|
83
|
+
source_concepts,
|
|
84
|
+
accept_partial: bool = False,
|
|
85
|
+
history: History | None = None,
|
|
86
|
+
conditions: Conditional | None = None,
|
|
87
|
+
) -> Optional[MergeNode]:
|
|
88
|
+
join_candidates: List[PathInfo] = []
|
|
89
|
+
# anchor on datasources
|
|
90
|
+
for datasource in environment.datasources.values():
|
|
91
|
+
path = identify_ds_join_paths(all_concepts, g, datasource, accept_partial)
|
|
92
|
+
if path and path.reduced_concepts:
|
|
93
|
+
join_candidates.append(path)
|
|
94
|
+
join_candidates.sort(key=lambda x: sum([len(v) for v in x.paths.values()]))
|
|
95
|
+
if not join_candidates:
|
|
96
|
+
return None
|
|
97
|
+
for join_candidate in join_candidates:
|
|
98
|
+
logger.info(
|
|
99
|
+
f"{padding(depth)}{LOGGER_PREFIX} Join candidate: {join_candidate.paths}"
|
|
100
|
+
)
|
|
101
|
+
join_additions: List[set[str]] = []
|
|
102
|
+
for candidate in join_candidates:
|
|
103
|
+
join_additions.append(candidate.reduced_concepts)
|
|
104
|
+
if not all(
|
|
105
|
+
[x.issubset(y) or y.issubset(x) for x in join_additions for y in join_additions]
|
|
106
|
+
):
|
|
107
|
+
raise AmbiguousRelationshipResolutionException(
|
|
108
|
+
f"Ambiguous concept join resolution - possible paths = {join_additions}. Include an additional concept to disambiguate",
|
|
109
|
+
join_additions,
|
|
110
|
+
)
|
|
111
|
+
if not join_candidates:
|
|
112
|
+
logger.info(
|
|
113
|
+
f"{padding(depth)}{LOGGER_PREFIX} No additional join candidates could be found"
|
|
114
|
+
)
|
|
115
|
+
return None
|
|
116
|
+
shortest: PathInfo = sorted(join_candidates, key=lambda x: len(x.reduced_concepts))[
|
|
117
|
+
0
|
|
118
|
+
]
|
|
119
|
+
logger.info(f"{padding(depth)}{LOGGER_PREFIX} final path is {shortest.paths}")
|
|
120
|
+
# logger.info(f'{padding(depth)}{LOGGER_PREFIX} final reduced concepts are {shortest.concs}')
|
|
121
|
+
parents = []
|
|
122
|
+
for graph in shortest.concept_subgraphs:
|
|
123
|
+
logger.info(
|
|
124
|
+
f"{padding(depth)}{LOGGER_PREFIX} fetching subgraph {[c.address for c in graph]}"
|
|
125
|
+
)
|
|
126
|
+
parent = source_concepts(
|
|
127
|
+
mandatory_list=graph,
|
|
128
|
+
environment=environment,
|
|
129
|
+
g=g,
|
|
130
|
+
depth=depth + 1,
|
|
131
|
+
history=history,
|
|
132
|
+
)
|
|
133
|
+
if not parent:
|
|
134
|
+
logger.info(
|
|
135
|
+
f"{padding(depth)}{LOGGER_PREFIX} Unable to instantiate target subgraph"
|
|
136
|
+
)
|
|
137
|
+
return None
|
|
138
|
+
parents.append(parent)
|
|
139
|
+
|
|
140
|
+
return MergeNode(
|
|
141
|
+
input_concepts=[environment.concepts[x] for x in shortest.reduced_concepts],
|
|
142
|
+
output_concepts=all_concepts,
|
|
143
|
+
environment=environment,
|
|
144
|
+
g=g,
|
|
145
|
+
parents=parents,
|
|
146
|
+
depth=depth,
|
|
147
|
+
conditions=conditions,
|
|
148
|
+
)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from trilogy.core.models import (
|
|
2
|
+
Concept,
|
|
3
|
+
Environment,
|
|
4
|
+
MultiSelectStatement,
|
|
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 collections import defaultdict
|
|
15
|
+
from itertools import combinations
|
|
16
|
+
from trilogy.core.enums import Purpose
|
|
17
|
+
from trilogy.core.processing.node_generators.common import resolve_join_order
|
|
18
|
+
|
|
19
|
+
LOGGER_PREFIX = "[GEN_MULTISELECT_NODE]"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def extra_align_joins(
|
|
23
|
+
base: MultiSelectStatement, parents: List[StrategyNode]
|
|
24
|
+
) -> List[NodeJoin]:
|
|
25
|
+
node_merge_concept_map = defaultdict(list)
|
|
26
|
+
output = []
|
|
27
|
+
for align in base.align.items:
|
|
28
|
+
jc = align.gen_concept(base)
|
|
29
|
+
if jc.purpose == Purpose.CONSTANT:
|
|
30
|
+
continue
|
|
31
|
+
for node in parents:
|
|
32
|
+
for item in align.concepts:
|
|
33
|
+
if item in node.output_lcl:
|
|
34
|
+
node_merge_concept_map[node].append(jc)
|
|
35
|
+
|
|
36
|
+
for left, right in combinations(node_merge_concept_map.keys(), 2):
|
|
37
|
+
matched_concepts = [
|
|
38
|
+
x
|
|
39
|
+
for x in node_merge_concept_map[left]
|
|
40
|
+
if x in node_merge_concept_map[right]
|
|
41
|
+
]
|
|
42
|
+
output.append(
|
|
43
|
+
NodeJoin(
|
|
44
|
+
left_node=left,
|
|
45
|
+
right_node=right,
|
|
46
|
+
concepts=matched_concepts,
|
|
47
|
+
join_type=JoinType.FULL,
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
return resolve_join_order(output)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def gen_multiselect_node(
|
|
54
|
+
concept: Concept,
|
|
55
|
+
local_optional: List[Concept],
|
|
56
|
+
environment: Environment,
|
|
57
|
+
g,
|
|
58
|
+
depth: int,
|
|
59
|
+
source_concepts,
|
|
60
|
+
history: History | None = None,
|
|
61
|
+
) -> MergeNode | None:
|
|
62
|
+
if not isinstance(concept.lineage, MultiSelectStatement):
|
|
63
|
+
logger.info(
|
|
64
|
+
f"{padding(depth)}{LOGGER_PREFIX} Cannot generate multiselect node for {concept}"
|
|
65
|
+
)
|
|
66
|
+
return None
|
|
67
|
+
lineage: MultiSelectStatement = concept.lineage
|
|
68
|
+
|
|
69
|
+
base_parents: List[StrategyNode] = []
|
|
70
|
+
for select in lineage.selects:
|
|
71
|
+
snode: StrategyNode = source_concepts(
|
|
72
|
+
mandatory_list=select.output_components,
|
|
73
|
+
environment=environment,
|
|
74
|
+
g=g,
|
|
75
|
+
depth=depth + 1,
|
|
76
|
+
history=history,
|
|
77
|
+
)
|
|
78
|
+
if not snode:
|
|
79
|
+
logger.info(
|
|
80
|
+
f"{padding(depth)}{LOGGER_PREFIX} Cannot generate multiselect node for {concept}"
|
|
81
|
+
)
|
|
82
|
+
return None
|
|
83
|
+
if select.where_clause:
|
|
84
|
+
snode.conditions = select.where_clause.conditional
|
|
85
|
+
merge_concepts = []
|
|
86
|
+
for x in [*snode.output_concepts]:
|
|
87
|
+
merge = lineage.get_merge_concept(x)
|
|
88
|
+
if merge:
|
|
89
|
+
snode.output_concepts.append(merge)
|
|
90
|
+
merge_concepts.append(merge)
|
|
91
|
+
# clear cache so QPS
|
|
92
|
+
snode.rebuild_cache()
|
|
93
|
+
for mc in merge_concepts:
|
|
94
|
+
assert mc in snode.resolve().output_concepts
|
|
95
|
+
base_parents.append(snode)
|
|
96
|
+
|
|
97
|
+
node_joins = extra_align_joins(lineage, base_parents)
|
|
98
|
+
node = MergeNode(
|
|
99
|
+
input_concepts=[x for y in base_parents for x in y.output_concepts],
|
|
100
|
+
output_concepts=[x for y in base_parents for x in y.output_concepts],
|
|
101
|
+
environment=environment,
|
|
102
|
+
g=g,
|
|
103
|
+
depth=depth,
|
|
104
|
+
parents=base_parents,
|
|
105
|
+
node_joins=node_joins,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
enrichment = set([x.address for x in local_optional])
|
|
109
|
+
|
|
110
|
+
rowset_relevant = [
|
|
111
|
+
x
|
|
112
|
+
for x in lineage.derived_concepts
|
|
113
|
+
if x.address == concept.address or x.address in enrichment
|
|
114
|
+
]
|
|
115
|
+
additional_relevant = [
|
|
116
|
+
x for x in select.output_components if x.address in enrichment
|
|
117
|
+
]
|
|
118
|
+
# add in other other concepts
|
|
119
|
+
for item in rowset_relevant:
|
|
120
|
+
node.output_concepts.append(item)
|
|
121
|
+
for item in additional_relevant:
|
|
122
|
+
node.output_concepts.append(item)
|
|
123
|
+
if select.where_clause:
|
|
124
|
+
for item in additional_relevant:
|
|
125
|
+
node.partial_concepts.append(item)
|
|
126
|
+
|
|
127
|
+
# we need a better API for refreshing a nodes QDS
|
|
128
|
+
node.resolution_cache = node._resolve()
|
|
129
|
+
|
|
130
|
+
# assume grain to be output of select
|
|
131
|
+
# but don't include anything aggregate at this point
|
|
132
|
+
node.resolution_cache.grain = concept_list_to_grain(
|
|
133
|
+
node.output_concepts, parent_sources=node.resolution_cache.datasources
|
|
134
|
+
)
|
|
135
|
+
possible_joins = concept_to_relevant_joins(additional_relevant)
|
|
136
|
+
if not local_optional:
|
|
137
|
+
logger.info(
|
|
138
|
+
f"{padding(depth)}{LOGGER_PREFIX} no enriched required for rowset node; exiting early"
|
|
139
|
+
)
|
|
140
|
+
return node
|
|
141
|
+
if not possible_joins:
|
|
142
|
+
logger.info(
|
|
143
|
+
f"{padding(depth)}{LOGGER_PREFIX} no possible joins for rowset node; exiting early"
|
|
144
|
+
)
|
|
145
|
+
return node
|
|
146
|
+
if all(
|
|
147
|
+
[x.address in [y.address for y in node.output_concepts] for x in local_optional]
|
|
148
|
+
):
|
|
149
|
+
logger.info(
|
|
150
|
+
f"{padding(depth)}{LOGGER_PREFIX} all enriched concepts returned from base rowset node; exiting early"
|
|
151
|
+
)
|
|
152
|
+
return node
|
|
153
|
+
enrich_node: MergeNode = source_concepts( # this fetches the parent + join keys
|
|
154
|
+
# to then connect to the rest of the query
|
|
155
|
+
mandatory_list=additional_relevant + local_optional,
|
|
156
|
+
environment=environment,
|
|
157
|
+
g=g,
|
|
158
|
+
depth=depth + 1,
|
|
159
|
+
history=history,
|
|
160
|
+
)
|
|
161
|
+
if not enrich_node:
|
|
162
|
+
logger.info(
|
|
163
|
+
f"{padding(depth)}{LOGGER_PREFIX} Cannot generate rowset enrichment node for {concept} with optional {local_optional}, returning just rowset node"
|
|
164
|
+
)
|
|
165
|
+
return node
|
|
166
|
+
|
|
167
|
+
return MergeNode(
|
|
168
|
+
input_concepts=enrich_node.output_concepts + node.output_concepts,
|
|
169
|
+
output_concepts=node.output_concepts + local_optional,
|
|
170
|
+
environment=environment,
|
|
171
|
+
g=g,
|
|
172
|
+
depth=depth,
|
|
173
|
+
parents=[
|
|
174
|
+
# this node gets the multiselect
|
|
175
|
+
node,
|
|
176
|
+
# this node gets enrichment
|
|
177
|
+
enrich_node,
|
|
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
|
+
partial_concepts=node.partial_concepts,
|
|
189
|
+
)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from trilogy.core.models import (
|
|
2
|
+
Concept,
|
|
3
|
+
Environment,
|
|
4
|
+
SelectStatement,
|
|
5
|
+
RowsetDerivationStatement,
|
|
6
|
+
RowsetItem,
|
|
7
|
+
MultiSelectStatement,
|
|
8
|
+
)
|
|
9
|
+
from trilogy.core.processing.nodes import MergeNode, NodeJoin, History, StrategyNode
|
|
10
|
+
from trilogy.core.processing.nodes.base_node import concept_list_to_grain
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
from trilogy.core.enums import JoinType
|
|
14
|
+
from trilogy.constants import logger
|
|
15
|
+
from trilogy.core.processing.utility import padding
|
|
16
|
+
from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
LOGGER_PREFIX = "[GEN_ROWSET_NODE]"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def gen_rowset_node(
|
|
23
|
+
concept: Concept,
|
|
24
|
+
local_optional: List[Concept],
|
|
25
|
+
environment: Environment,
|
|
26
|
+
g,
|
|
27
|
+
depth: int,
|
|
28
|
+
source_concepts,
|
|
29
|
+
history: History | None = None,
|
|
30
|
+
) -> StrategyNode | None:
|
|
31
|
+
if not isinstance(concept.lineage, RowsetItem):
|
|
32
|
+
raise SyntaxError(
|
|
33
|
+
f"Invalid lineage passed into rowset fetch, got {type(concept.lineage)}, expected {RowsetItem}"
|
|
34
|
+
)
|
|
35
|
+
lineage: RowsetItem = concept.lineage
|
|
36
|
+
rowset: RowsetDerivationStatement = lineage.rowset
|
|
37
|
+
select: SelectStatement | MultiSelectStatement = lineage.rowset.select
|
|
38
|
+
node: StrategyNode = source_concepts(
|
|
39
|
+
mandatory_list=select.output_components,
|
|
40
|
+
environment=environment,
|
|
41
|
+
g=g,
|
|
42
|
+
depth=depth + 1,
|
|
43
|
+
history=history,
|
|
44
|
+
)
|
|
45
|
+
node.conditions = select.where_clause.conditional if select.where_clause else None
|
|
46
|
+
# rebuild any cached info with the new condition clause
|
|
47
|
+
node.rebuild_cache()
|
|
48
|
+
if not node:
|
|
49
|
+
logger.info(
|
|
50
|
+
f"{padding(depth)}{LOGGER_PREFIX} Cannot generate rowset node for {concept}"
|
|
51
|
+
)
|
|
52
|
+
return None
|
|
53
|
+
enrichment = set([x.address for x in local_optional])
|
|
54
|
+
rowset_relevant = [
|
|
55
|
+
x
|
|
56
|
+
for x in rowset.derived_concepts
|
|
57
|
+
if x.address == concept.address or x.address in enrichment
|
|
58
|
+
]
|
|
59
|
+
additional_relevant = [
|
|
60
|
+
x for x in select.output_components if x.address in enrichment
|
|
61
|
+
]
|
|
62
|
+
# add in other other concepts
|
|
63
|
+
for item in rowset_relevant:
|
|
64
|
+
node.output_concepts.append(item)
|
|
65
|
+
for item in additional_relevant:
|
|
66
|
+
node.output_concepts.append(item)
|
|
67
|
+
if select.where_clause:
|
|
68
|
+
for item in additional_relevant:
|
|
69
|
+
node.partial_concepts.append(item)
|
|
70
|
+
|
|
71
|
+
# assume grain to be outoput of select
|
|
72
|
+
# but don't include anything aggregate at this point
|
|
73
|
+
assert node.resolution_cache
|
|
74
|
+
node.resolution_cache.grain = concept_list_to_grain(
|
|
75
|
+
node.output_concepts, parent_sources=node.resolution_cache.datasources
|
|
76
|
+
)
|
|
77
|
+
possible_joins = concept_to_relevant_joins(additional_relevant)
|
|
78
|
+
if not local_optional:
|
|
79
|
+
logger.info(
|
|
80
|
+
f"{padding(depth)}{LOGGER_PREFIX} no enriched required for rowset node; exiting early"
|
|
81
|
+
)
|
|
82
|
+
return node
|
|
83
|
+
if not possible_joins:
|
|
84
|
+
logger.info(
|
|
85
|
+
f"{padding(depth)}{LOGGER_PREFIX} no possible joins for rowset node; exiting early"
|
|
86
|
+
)
|
|
87
|
+
return node
|
|
88
|
+
if all(
|
|
89
|
+
[x.address in [y.address for y in node.output_concepts] for x in local_optional]
|
|
90
|
+
):
|
|
91
|
+
logger.info(
|
|
92
|
+
f"{padding(depth)}{LOGGER_PREFIX} all enriched concepts returned from base rowset node; exiting early"
|
|
93
|
+
)
|
|
94
|
+
return node
|
|
95
|
+
enrich_node: MergeNode = source_concepts( # this fetches the parent + join keys
|
|
96
|
+
# to then connect to the rest of the query
|
|
97
|
+
mandatory_list=additional_relevant + local_optional,
|
|
98
|
+
environment=environment,
|
|
99
|
+
g=g,
|
|
100
|
+
depth=depth + 1,
|
|
101
|
+
)
|
|
102
|
+
if not enrich_node:
|
|
103
|
+
logger.info(
|
|
104
|
+
f"{padding(depth)}{LOGGER_PREFIX} Cannot generate rowset enrichment node for {concept} with optional {local_optional}, returning just rowset node"
|
|
105
|
+
)
|
|
106
|
+
return node
|
|
107
|
+
|
|
108
|
+
return MergeNode(
|
|
109
|
+
input_concepts=enrich_node.output_concepts + node.output_concepts,
|
|
110
|
+
output_concepts=node.output_concepts + local_optional,
|
|
111
|
+
environment=environment,
|
|
112
|
+
g=g,
|
|
113
|
+
depth=depth,
|
|
114
|
+
parents=[
|
|
115
|
+
# this node gets the window
|
|
116
|
+
node,
|
|
117
|
+
# this node gets enrichment
|
|
118
|
+
enrich_node,
|
|
119
|
+
],
|
|
120
|
+
node_joins=[
|
|
121
|
+
NodeJoin(
|
|
122
|
+
left_node=enrich_node,
|
|
123
|
+
right_node=node,
|
|
124
|
+
concepts=concept_to_relevant_joins(additional_relevant),
|
|
125
|
+
filter_to_mutual=False,
|
|
126
|
+
join_type=JoinType.LEFT_OUTER,
|
|
127
|
+
)
|
|
128
|
+
],
|
|
129
|
+
partial_concepts=node.partial_concepts,
|
|
130
|
+
)
|