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.

Files changed (77) hide show
  1. pytrilogy-0.0.1.102.dist-info/LICENSE.md +19 -0
  2. pytrilogy-0.0.1.102.dist-info/METADATA +277 -0
  3. pytrilogy-0.0.1.102.dist-info/RECORD +77 -0
  4. pytrilogy-0.0.1.102.dist-info/WHEEL +5 -0
  5. pytrilogy-0.0.1.102.dist-info/entry_points.txt +2 -0
  6. pytrilogy-0.0.1.102.dist-info/top_level.txt +1 -0
  7. trilogy/__init__.py +8 -0
  8. trilogy/compiler.py +0 -0
  9. trilogy/constants.py +30 -0
  10. trilogy/core/__init__.py +0 -0
  11. trilogy/core/constants.py +3 -0
  12. trilogy/core/enums.py +270 -0
  13. trilogy/core/env_processor.py +33 -0
  14. trilogy/core/environment_helpers.py +156 -0
  15. trilogy/core/ergonomics.py +187 -0
  16. trilogy/core/exceptions.py +23 -0
  17. trilogy/core/functions.py +320 -0
  18. trilogy/core/graph_models.py +55 -0
  19. trilogy/core/internal.py +37 -0
  20. trilogy/core/models.py +3145 -0
  21. trilogy/core/processing/__init__.py +0 -0
  22. trilogy/core/processing/concept_strategies_v3.py +603 -0
  23. trilogy/core/processing/graph_utils.py +44 -0
  24. trilogy/core/processing/node_generators/__init__.py +25 -0
  25. trilogy/core/processing/node_generators/basic_node.py +71 -0
  26. trilogy/core/processing/node_generators/common.py +239 -0
  27. trilogy/core/processing/node_generators/concept_merge.py +152 -0
  28. trilogy/core/processing/node_generators/filter_node.py +83 -0
  29. trilogy/core/processing/node_generators/group_node.py +92 -0
  30. trilogy/core/processing/node_generators/group_to_node.py +99 -0
  31. trilogy/core/processing/node_generators/merge_node.py +148 -0
  32. trilogy/core/processing/node_generators/multiselect_node.py +189 -0
  33. trilogy/core/processing/node_generators/rowset_node.py +130 -0
  34. trilogy/core/processing/node_generators/select_node.py +328 -0
  35. trilogy/core/processing/node_generators/unnest_node.py +37 -0
  36. trilogy/core/processing/node_generators/window_node.py +85 -0
  37. trilogy/core/processing/nodes/__init__.py +76 -0
  38. trilogy/core/processing/nodes/base_node.py +251 -0
  39. trilogy/core/processing/nodes/filter_node.py +49 -0
  40. trilogy/core/processing/nodes/group_node.py +110 -0
  41. trilogy/core/processing/nodes/merge_node.py +326 -0
  42. trilogy/core/processing/nodes/select_node_v2.py +198 -0
  43. trilogy/core/processing/nodes/unnest_node.py +54 -0
  44. trilogy/core/processing/nodes/window_node.py +34 -0
  45. trilogy/core/processing/utility.py +278 -0
  46. trilogy/core/query_processor.py +331 -0
  47. trilogy/dialect/__init__.py +0 -0
  48. trilogy/dialect/base.py +679 -0
  49. trilogy/dialect/bigquery.py +80 -0
  50. trilogy/dialect/common.py +43 -0
  51. trilogy/dialect/config.py +55 -0
  52. trilogy/dialect/duckdb.py +83 -0
  53. trilogy/dialect/enums.py +95 -0
  54. trilogy/dialect/postgres.py +86 -0
  55. trilogy/dialect/presto.py +82 -0
  56. trilogy/dialect/snowflake.py +82 -0
  57. trilogy/dialect/sql_server.py +89 -0
  58. trilogy/docs/__init__.py +0 -0
  59. trilogy/engine.py +48 -0
  60. trilogy/executor.py +242 -0
  61. trilogy/hooks/__init__.py +0 -0
  62. trilogy/hooks/base_hook.py +37 -0
  63. trilogy/hooks/graph_hook.py +24 -0
  64. trilogy/hooks/query_debugger.py +133 -0
  65. trilogy/metadata/__init__.py +0 -0
  66. trilogy/parser.py +10 -0
  67. trilogy/parsing/__init__.py +0 -0
  68. trilogy/parsing/common.py +176 -0
  69. trilogy/parsing/config.py +5 -0
  70. trilogy/parsing/exceptions.py +2 -0
  71. trilogy/parsing/helpers.py +1 -0
  72. trilogy/parsing/parse_engine.py +1951 -0
  73. trilogy/parsing/render.py +483 -0
  74. trilogy/py.typed +0 -0
  75. trilogy/scripts/__init__.py +0 -0
  76. trilogy/scripts/trilogy.py +127 -0
  77. 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
+ )