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,326 @@
|
|
|
1
|
+
from typing import List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from trilogy.constants import logger
|
|
5
|
+
from trilogy.core.models import (
|
|
6
|
+
BaseJoin,
|
|
7
|
+
Grain,
|
|
8
|
+
JoinType,
|
|
9
|
+
QueryDatasource,
|
|
10
|
+
SourceType,
|
|
11
|
+
Concept,
|
|
12
|
+
UnnestJoin,
|
|
13
|
+
Conditional,
|
|
14
|
+
)
|
|
15
|
+
from trilogy.utility import unique
|
|
16
|
+
from trilogy.core.processing.nodes.base_node import (
|
|
17
|
+
StrategyNode,
|
|
18
|
+
resolve_concept_map,
|
|
19
|
+
NodeJoin,
|
|
20
|
+
)
|
|
21
|
+
from trilogy.core.processing.utility import get_node_joins
|
|
22
|
+
|
|
23
|
+
LOGGER_PREFIX = "[CONCEPT DETAIL - MERGE NODE]"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def deduplicate_nodes(
|
|
27
|
+
merged: dict[str, QueryDatasource], logging_prefix: str
|
|
28
|
+
) -> tuple[bool, dict[str, QueryDatasource], set[str]]:
|
|
29
|
+
duplicates = False
|
|
30
|
+
removed: set[str] = set()
|
|
31
|
+
set_map: dict[str, set[str]] = {}
|
|
32
|
+
for k, v in merged.items():
|
|
33
|
+
unique_outputs = [
|
|
34
|
+
x.address for x in v.output_concepts if x not in v.partial_concepts
|
|
35
|
+
]
|
|
36
|
+
set_map[k] = set(unique_outputs)
|
|
37
|
+
for k1, v1 in set_map.items():
|
|
38
|
+
found = False
|
|
39
|
+
for k2, v2 in set_map.items():
|
|
40
|
+
if k1 == k2:
|
|
41
|
+
continue
|
|
42
|
+
if (
|
|
43
|
+
v1.issubset(v2)
|
|
44
|
+
and merged[k1].grain.issubset(merged[k2].grain)
|
|
45
|
+
and not merged[k2].partial_concepts
|
|
46
|
+
and not merged[k1].partial_concepts
|
|
47
|
+
and not merged[k2].condition
|
|
48
|
+
and not merged[k1].condition
|
|
49
|
+
):
|
|
50
|
+
og = merged[k1]
|
|
51
|
+
subset_to = merged[k2]
|
|
52
|
+
logger.info(
|
|
53
|
+
f"{logging_prefix}{LOGGER_PREFIX} extraneous parent node that is subset of another parent node {og.grain.issubset(subset_to.grain)} {og.grain.set} {subset_to.grain.set}"
|
|
54
|
+
)
|
|
55
|
+
merged = {k: v for k, v in merged.items() if k != k1}
|
|
56
|
+
removed.add(k1)
|
|
57
|
+
duplicates = True
|
|
58
|
+
found = True
|
|
59
|
+
break
|
|
60
|
+
if found:
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
return duplicates, merged, removed
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def deduplicate_nodes_and_joins(
|
|
67
|
+
joins: List[NodeJoin] | None,
|
|
68
|
+
merged: dict[str, QueryDatasource],
|
|
69
|
+
logging_prefix: str,
|
|
70
|
+
) -> Tuple[List[NodeJoin] | None, dict[str, QueryDatasource]]:
|
|
71
|
+
# it's possible that we have more sources than we need
|
|
72
|
+
duplicates = True
|
|
73
|
+
while duplicates:
|
|
74
|
+
duplicates = False
|
|
75
|
+
duplicates, merged, removed = deduplicate_nodes(merged, logging_prefix)
|
|
76
|
+
# filter out any removed joins
|
|
77
|
+
if joins:
|
|
78
|
+
joins = [
|
|
79
|
+
j
|
|
80
|
+
for j in joins
|
|
81
|
+
if j.left_node.resolve().full_name not in removed
|
|
82
|
+
and j.right_node.resolve().full_name not in removed
|
|
83
|
+
]
|
|
84
|
+
return joins, merged
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class MergeNode(StrategyNode):
|
|
88
|
+
source_type = SourceType.MERGE
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
input_concepts: List[Concept],
|
|
93
|
+
output_concepts: List[Concept],
|
|
94
|
+
environment,
|
|
95
|
+
g,
|
|
96
|
+
whole_grain: bool = False,
|
|
97
|
+
parents: List["StrategyNode"] | None = None,
|
|
98
|
+
node_joins: List[NodeJoin] | None = None,
|
|
99
|
+
join_concepts: Optional[List] = None,
|
|
100
|
+
force_join_type: Optional[JoinType] = None,
|
|
101
|
+
partial_concepts: Optional[List[Concept]] = None,
|
|
102
|
+
force_group: bool | None = None,
|
|
103
|
+
depth: int = 0,
|
|
104
|
+
grain: Grain | None = None,
|
|
105
|
+
conditions: Conditional | None = None,
|
|
106
|
+
):
|
|
107
|
+
super().__init__(
|
|
108
|
+
input_concepts=input_concepts,
|
|
109
|
+
output_concepts=output_concepts,
|
|
110
|
+
environment=environment,
|
|
111
|
+
g=g,
|
|
112
|
+
whole_grain=whole_grain,
|
|
113
|
+
parents=parents,
|
|
114
|
+
depth=depth,
|
|
115
|
+
partial_concepts=partial_concepts,
|
|
116
|
+
force_group=force_group,
|
|
117
|
+
grain=grain,
|
|
118
|
+
conditions=conditions,
|
|
119
|
+
)
|
|
120
|
+
self.join_concepts = join_concepts
|
|
121
|
+
self.force_join_type = force_join_type
|
|
122
|
+
self.node_joins = node_joins
|
|
123
|
+
final_joins = []
|
|
124
|
+
if self.node_joins:
|
|
125
|
+
for join in self.node_joins:
|
|
126
|
+
if join.left_node.resolve().name == join.right_node.resolve().name:
|
|
127
|
+
continue
|
|
128
|
+
final_joins.append(join)
|
|
129
|
+
self.node_joins = final_joins
|
|
130
|
+
|
|
131
|
+
def translate_node_joins(self, node_joins: List[NodeJoin]) -> List[BaseJoin]:
|
|
132
|
+
joins = []
|
|
133
|
+
for join in node_joins:
|
|
134
|
+
left = join.left_node.resolve()
|
|
135
|
+
right = join.right_node.resolve()
|
|
136
|
+
if left.full_name == right.full_name:
|
|
137
|
+
raise SyntaxError(f"Cannot join node {left.full_name} to itself")
|
|
138
|
+
joins.append(
|
|
139
|
+
BaseJoin(
|
|
140
|
+
left_datasource=left,
|
|
141
|
+
right_datasource=right,
|
|
142
|
+
join_type=join.join_type,
|
|
143
|
+
concepts=join.concepts,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
return joins
|
|
147
|
+
|
|
148
|
+
def create_full_joins(self, dataset_list: List[QueryDatasource]):
|
|
149
|
+
joins = []
|
|
150
|
+
seen = set()
|
|
151
|
+
for left_value in dataset_list:
|
|
152
|
+
for right_value in dataset_list:
|
|
153
|
+
if left_value.identifier == right_value.identifier:
|
|
154
|
+
continue
|
|
155
|
+
if left_value.identifier in seen and right_value.identifier in seen:
|
|
156
|
+
continue
|
|
157
|
+
joins.append(
|
|
158
|
+
BaseJoin(
|
|
159
|
+
left_datasource=left_value,
|
|
160
|
+
right_datasource=right_value,
|
|
161
|
+
join_type=JoinType.FULL,
|
|
162
|
+
concepts=[],
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
seen.add(left_value.identifier)
|
|
166
|
+
seen.add(right_value.identifier)
|
|
167
|
+
return joins
|
|
168
|
+
|
|
169
|
+
def generate_joins(
|
|
170
|
+
self, final_datasets, final_joins, pregrain: Grain, grain: Grain
|
|
171
|
+
) -> List[BaseJoin]:
|
|
172
|
+
# only finally, join between them for unique values
|
|
173
|
+
dataset_list: List[QueryDatasource] = sorted(
|
|
174
|
+
final_datasets, key=lambda x: -len(x.grain.components_copy)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
logger.info(
|
|
178
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} Merge node has {len(dataset_list)} parents, starting merge"
|
|
179
|
+
)
|
|
180
|
+
for item in dataset_list:
|
|
181
|
+
logger.info(f"{self.logging_prefix}{LOGGER_PREFIX} for {item.full_name}")
|
|
182
|
+
logger.info(
|
|
183
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} partial concepts {[x.address for x in item.partial_concepts]}"
|
|
184
|
+
)
|
|
185
|
+
logger.info(
|
|
186
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} potential merge keys {[x.address+str(x.purpose) for x in item.output_concepts]} partial {[x.address for x in item.partial_concepts]}"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if not final_joins:
|
|
190
|
+
if not pregrain.components:
|
|
191
|
+
logger.info(
|
|
192
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} no grain components, doing full join"
|
|
193
|
+
)
|
|
194
|
+
joins = self.create_full_joins(dataset_list)
|
|
195
|
+
else:
|
|
196
|
+
logger.info(
|
|
197
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} inferring node joins to target grain {str(grain)}"
|
|
198
|
+
)
|
|
199
|
+
joins = get_node_joins(dataset_list, grain.components)
|
|
200
|
+
elif final_joins:
|
|
201
|
+
logger.info(
|
|
202
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} translating provided node joins {len(final_joins)}"
|
|
203
|
+
)
|
|
204
|
+
joins = self.translate_node_joins(final_joins)
|
|
205
|
+
else:
|
|
206
|
+
return []
|
|
207
|
+
for join in joins:
|
|
208
|
+
logger.info(
|
|
209
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} final join {join.join_type} {[str(c) for c in join.concepts]}"
|
|
210
|
+
)
|
|
211
|
+
return joins
|
|
212
|
+
|
|
213
|
+
def _resolve(self) -> QueryDatasource:
|
|
214
|
+
parent_sources = [p.resolve() for p in self.parents]
|
|
215
|
+
merged: dict[str, QueryDatasource] = {}
|
|
216
|
+
final_joins = self.node_joins
|
|
217
|
+
for source in parent_sources:
|
|
218
|
+
if source.full_name in merged:
|
|
219
|
+
logger.info(
|
|
220
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} parent node with {source.full_name} into existing"
|
|
221
|
+
)
|
|
222
|
+
merged[source.full_name] = merged[source.full_name] + source
|
|
223
|
+
else:
|
|
224
|
+
merged[source.full_name] = source
|
|
225
|
+
|
|
226
|
+
# it's possible that we have more sources than we need
|
|
227
|
+
final_joins, merged = deduplicate_nodes_and_joins(
|
|
228
|
+
final_joins, merged, self.logging_prefix
|
|
229
|
+
)
|
|
230
|
+
# early exit if we can just return the parent
|
|
231
|
+
final_datasets: List[QueryDatasource] = list(merged.values())
|
|
232
|
+
|
|
233
|
+
if len(merged.keys()) == 1:
|
|
234
|
+
final: QueryDatasource = list(merged.values())[0]
|
|
235
|
+
if (
|
|
236
|
+
set([c.address for c in final.output_concepts])
|
|
237
|
+
== set([c.address for c in self.output_concepts])
|
|
238
|
+
and not self.conditions
|
|
239
|
+
):
|
|
240
|
+
logger.info(
|
|
241
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} Merge node has only one parent with the same"
|
|
242
|
+
" outputs as this merge node, dropping merge node "
|
|
243
|
+
)
|
|
244
|
+
return final
|
|
245
|
+
|
|
246
|
+
# if we have multiple candidates, see if one is good enough
|
|
247
|
+
for dataset in final_datasets:
|
|
248
|
+
output_set = set(
|
|
249
|
+
[
|
|
250
|
+
c.address
|
|
251
|
+
for c in dataset.output_concepts
|
|
252
|
+
if c.address not in [x.address for x in dataset.partial_concepts]
|
|
253
|
+
]
|
|
254
|
+
)
|
|
255
|
+
if (
|
|
256
|
+
all([c.address in output_set for c in self.all_concepts])
|
|
257
|
+
and not self.conditions
|
|
258
|
+
):
|
|
259
|
+
logger.info(
|
|
260
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} Merge node not required as parent node {dataset.source_type}"
|
|
261
|
+
f" has all required output properties with partial {[c.address for c in dataset.partial_concepts]}"
|
|
262
|
+
f" and self has no conditions ({self.conditions})"
|
|
263
|
+
)
|
|
264
|
+
return dataset
|
|
265
|
+
|
|
266
|
+
pregrain = Grain()
|
|
267
|
+
for source in final_datasets:
|
|
268
|
+
pregrain += source.grain
|
|
269
|
+
|
|
270
|
+
grain = Grain(
|
|
271
|
+
components=[
|
|
272
|
+
c
|
|
273
|
+
for c in pregrain.components
|
|
274
|
+
if c.address in [x.address for x in self.output_concepts]
|
|
275
|
+
]
|
|
276
|
+
)
|
|
277
|
+
logger.info(
|
|
278
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} has pre grain {pregrain} and final merge node grain {grain}"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if len(final_datasets) > 1:
|
|
282
|
+
joins = self.generate_joins(final_datasets, final_joins, pregrain, grain)
|
|
283
|
+
else:
|
|
284
|
+
joins = []
|
|
285
|
+
|
|
286
|
+
full_join_concepts = []
|
|
287
|
+
for join in joins:
|
|
288
|
+
if join.join_type == JoinType.FULL:
|
|
289
|
+
full_join_concepts += join.concepts
|
|
290
|
+
|
|
291
|
+
if self.whole_grain:
|
|
292
|
+
force_group = False
|
|
293
|
+
elif self.force_group is False:
|
|
294
|
+
force_group = False
|
|
295
|
+
elif not any(
|
|
296
|
+
[d.grain.issubset(grain) for d in final_datasets]
|
|
297
|
+
) and not pregrain.issubset(grain):
|
|
298
|
+
logger.info(
|
|
299
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} no parents include full grain {grain} and pregrain {pregrain} does not match, assume must group to grain. Have {[str(d.grain) for d in final_datasets]}"
|
|
300
|
+
)
|
|
301
|
+
force_group = True
|
|
302
|
+
# Grain<returns.customer.id,returns.store.id,returns.item.id,returns.store_sales.ticket_number>
|
|
303
|
+
# Grain<returns.customer.id,returns.store.id,returns.return_date.id,returns.item.id,returns.store_sales.ticket_number>
|
|
304
|
+
# Grain<returns.customer.id,returns.store.id,returns.item.id,returns.store_sales.ticket_number>
|
|
305
|
+
else:
|
|
306
|
+
force_group = None
|
|
307
|
+
|
|
308
|
+
qd_joins: List[BaseJoin | UnnestJoin] = [*joins]
|
|
309
|
+
qds = QueryDatasource(
|
|
310
|
+
input_concepts=unique(self.input_concepts, "address"),
|
|
311
|
+
output_concepts=unique(self.output_concepts, "address"),
|
|
312
|
+
datasources=final_datasets,
|
|
313
|
+
source_type=self.source_type,
|
|
314
|
+
source_map=resolve_concept_map(
|
|
315
|
+
parent_sources,
|
|
316
|
+
self.output_concepts,
|
|
317
|
+
self.input_concepts,
|
|
318
|
+
full_joins=full_join_concepts,
|
|
319
|
+
),
|
|
320
|
+
joins=qd_joins,
|
|
321
|
+
grain=grain,
|
|
322
|
+
partial_concepts=self.partial_concepts,
|
|
323
|
+
force_group=force_group,
|
|
324
|
+
condition=self.conditions,
|
|
325
|
+
)
|
|
326
|
+
return qds
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from trilogy.constants import logger
|
|
5
|
+
from trilogy.core.constants import CONSTANT_DATASET
|
|
6
|
+
from trilogy.core.enums import Purpose, PurposeLineage
|
|
7
|
+
from trilogy.core.models import (
|
|
8
|
+
Datasource,
|
|
9
|
+
QueryDatasource,
|
|
10
|
+
SourceType,
|
|
11
|
+
Environment,
|
|
12
|
+
Concept,
|
|
13
|
+
Grain,
|
|
14
|
+
Function,
|
|
15
|
+
UnnestJoin,
|
|
16
|
+
)
|
|
17
|
+
from trilogy.utility import unique
|
|
18
|
+
from trilogy.core.processing.nodes.base_node import StrategyNode
|
|
19
|
+
from trilogy.core.exceptions import NoDatasourceException
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
LOGGER_PREFIX = "[CONCEPT DETAIL - SELECT NODE]"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StaticSelectNode(StrategyNode):
|
|
26
|
+
"""Static select nodes."""
|
|
27
|
+
|
|
28
|
+
source_type = SourceType.SELECT
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
input_concepts: List[Concept],
|
|
33
|
+
output_concepts: List[Concept],
|
|
34
|
+
environment: Environment,
|
|
35
|
+
g,
|
|
36
|
+
datasource: QueryDatasource,
|
|
37
|
+
depth: int = 0,
|
|
38
|
+
partial_concepts: List[Concept] | None = None,
|
|
39
|
+
):
|
|
40
|
+
super().__init__(
|
|
41
|
+
input_concepts=input_concepts,
|
|
42
|
+
output_concepts=output_concepts,
|
|
43
|
+
environment=environment,
|
|
44
|
+
g=g,
|
|
45
|
+
whole_grain=True,
|
|
46
|
+
parents=[],
|
|
47
|
+
depth=depth,
|
|
48
|
+
partial_concepts=partial_concepts,
|
|
49
|
+
)
|
|
50
|
+
self.datasource = datasource
|
|
51
|
+
|
|
52
|
+
def _resolve(self):
|
|
53
|
+
if self.datasource.grain == Grain():
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
return self.datasource
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SelectNode(StrategyNode):
|
|
59
|
+
"""Select nodes actually fetch raw data from a table
|
|
60
|
+
Responsible for selecting the cheapest option from which to select.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
source_type = SourceType.SELECT
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
input_concepts: List[Concept],
|
|
68
|
+
output_concepts: List[Concept],
|
|
69
|
+
environment: Environment,
|
|
70
|
+
g,
|
|
71
|
+
datasource: Datasource | None = None,
|
|
72
|
+
whole_grain: bool = False,
|
|
73
|
+
parents: List["StrategyNode"] | None = None,
|
|
74
|
+
depth: int = 0,
|
|
75
|
+
partial_concepts: List[Concept] | None = None,
|
|
76
|
+
accept_partial: bool = False,
|
|
77
|
+
grain: Optional[Grain] = None,
|
|
78
|
+
force_group: bool = False,
|
|
79
|
+
):
|
|
80
|
+
super().__init__(
|
|
81
|
+
input_concepts=input_concepts,
|
|
82
|
+
output_concepts=output_concepts,
|
|
83
|
+
environment=environment,
|
|
84
|
+
g=g,
|
|
85
|
+
whole_grain=whole_grain,
|
|
86
|
+
parents=parents,
|
|
87
|
+
depth=depth,
|
|
88
|
+
partial_concepts=partial_concepts,
|
|
89
|
+
force_group=force_group,
|
|
90
|
+
grain=grain,
|
|
91
|
+
)
|
|
92
|
+
self.accept_partial = accept_partial
|
|
93
|
+
self.datasource = datasource
|
|
94
|
+
|
|
95
|
+
def resolve_from_provided_datasource(
|
|
96
|
+
self,
|
|
97
|
+
) -> QueryDatasource:
|
|
98
|
+
if not self.datasource:
|
|
99
|
+
raise ValueError("Datasource not provided")
|
|
100
|
+
datasource: Datasource = self.datasource
|
|
101
|
+
|
|
102
|
+
all_concepts_final: List[Concept] = unique(self.all_concepts, "address")
|
|
103
|
+
source_map: dict[str, set[Datasource | QueryDatasource | UnnestJoin]] = {
|
|
104
|
+
concept.address: {datasource} for concept in self.input_concepts
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
derived_concepts = [
|
|
108
|
+
c
|
|
109
|
+
for c in datasource.columns
|
|
110
|
+
if isinstance(c.alias, Function) and c.concept.address in source_map
|
|
111
|
+
]
|
|
112
|
+
for c in derived_concepts:
|
|
113
|
+
if not isinstance(c.alias, Function):
|
|
114
|
+
continue
|
|
115
|
+
for x in c.alias.concept_arguments:
|
|
116
|
+
source_map[x.address] = {datasource}
|
|
117
|
+
for x in all_concepts_final:
|
|
118
|
+
# add in any derived concepts to support a merge node
|
|
119
|
+
if x.address not in source_map and x.derivation in (
|
|
120
|
+
PurposeLineage.MULTISELECT,
|
|
121
|
+
PurposeLineage.MERGE,
|
|
122
|
+
):
|
|
123
|
+
source_map[x.address] = set()
|
|
124
|
+
|
|
125
|
+
# if we're not grouping
|
|
126
|
+
# force grain to datasource grain
|
|
127
|
+
# so that we merge on the same grain
|
|
128
|
+
if self.force_group is False:
|
|
129
|
+
grain = datasource.grain
|
|
130
|
+
else:
|
|
131
|
+
grain = self.grain or Grain()
|
|
132
|
+
return QueryDatasource(
|
|
133
|
+
input_concepts=self.input_concepts,
|
|
134
|
+
output_concepts=all_concepts_final,
|
|
135
|
+
source_map=source_map,
|
|
136
|
+
datasources=[datasource],
|
|
137
|
+
grain=grain,
|
|
138
|
+
joins=[],
|
|
139
|
+
partial_concepts=[
|
|
140
|
+
c.concept for c in datasource.columns if not c.is_complete
|
|
141
|
+
],
|
|
142
|
+
source_type=SourceType.DIRECT_SELECT,
|
|
143
|
+
# select nodes should never group
|
|
144
|
+
force_group=self.force_group,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def resolve_from_constant_datasources(self) -> QueryDatasource:
|
|
148
|
+
datasource = Datasource(
|
|
149
|
+
identifier=CONSTANT_DATASET, address=CONSTANT_DATASET, columns=[]
|
|
150
|
+
)
|
|
151
|
+
return QueryDatasource(
|
|
152
|
+
input_concepts=[],
|
|
153
|
+
output_concepts=unique(self.all_concepts, "address"),
|
|
154
|
+
source_map={concept.address: set() for concept in self.all_concepts},
|
|
155
|
+
datasources=[datasource],
|
|
156
|
+
grain=datasource.grain,
|
|
157
|
+
joins=[],
|
|
158
|
+
partial_concepts=[],
|
|
159
|
+
source_type=SourceType.CONSTANT,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _resolve(self) -> QueryDatasource:
|
|
163
|
+
# if we have parent nodes, we do not need to go to a datasource
|
|
164
|
+
if self.parents:
|
|
165
|
+
return super()._resolve()
|
|
166
|
+
resolution: QueryDatasource | None
|
|
167
|
+
if all(
|
|
168
|
+
[
|
|
169
|
+
(
|
|
170
|
+
c.derivation == PurposeLineage.CONSTANT
|
|
171
|
+
or (
|
|
172
|
+
c.purpose == Purpose.CONSTANT
|
|
173
|
+
and c.derivation == PurposeLineage.MULTISELECT
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
for c in self.all_concepts
|
|
177
|
+
]
|
|
178
|
+
):
|
|
179
|
+
logger.info(
|
|
180
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} have a constant datasource"
|
|
181
|
+
)
|
|
182
|
+
resolution = self.resolve_from_constant_datasources()
|
|
183
|
+
if resolution:
|
|
184
|
+
return resolution
|
|
185
|
+
if self.datasource:
|
|
186
|
+
resolution = self.resolve_from_provided_datasource()
|
|
187
|
+
if resolution:
|
|
188
|
+
return resolution
|
|
189
|
+
required = [c.address for c in self.all_concepts]
|
|
190
|
+
raise NoDatasourceException(
|
|
191
|
+
f"Could not find any way to associate required concepts {required}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ConstantNode(SelectNode):
|
|
196
|
+
"""Represents a constant value."""
|
|
197
|
+
|
|
198
|
+
pass
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from trilogy.core.models import (
|
|
5
|
+
QueryDatasource,
|
|
6
|
+
SourceType,
|
|
7
|
+
Concept,
|
|
8
|
+
UnnestJoin,
|
|
9
|
+
)
|
|
10
|
+
from trilogy.core.processing.nodes.base_node import StrategyNode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnnestNode(StrategyNode):
|
|
14
|
+
"""Unnest nodes represent an expansion of an array or other
|
|
15
|
+
column into rows.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
source_type = SourceType.UNNEST
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
unnest_concept: Concept,
|
|
23
|
+
input_concepts: List[Concept],
|
|
24
|
+
output_concepts: List[Concept],
|
|
25
|
+
environment,
|
|
26
|
+
g,
|
|
27
|
+
whole_grain: bool = False,
|
|
28
|
+
parents: List["StrategyNode"] | None = None,
|
|
29
|
+
depth: int = 0,
|
|
30
|
+
):
|
|
31
|
+
super().__init__(
|
|
32
|
+
input_concepts=input_concepts,
|
|
33
|
+
output_concepts=output_concepts,
|
|
34
|
+
environment=environment,
|
|
35
|
+
g=g,
|
|
36
|
+
whole_grain=whole_grain,
|
|
37
|
+
parents=parents,
|
|
38
|
+
depth=depth,
|
|
39
|
+
)
|
|
40
|
+
self.unnest_concept = unnest_concept
|
|
41
|
+
|
|
42
|
+
def _resolve(self) -> QueryDatasource:
|
|
43
|
+
"""We need to ensure that any filtered values are removed from the output to avoid inappropriate references"""
|
|
44
|
+
base = super()._resolve()
|
|
45
|
+
|
|
46
|
+
unnest = UnnestJoin(
|
|
47
|
+
concept=self.unnest_concept,
|
|
48
|
+
alias=f'unnest_{self.unnest_concept.address.replace(".", "_")}',
|
|
49
|
+
)
|
|
50
|
+
base.joins.append(unnest)
|
|
51
|
+
|
|
52
|
+
base.source_map[self.unnest_concept.address] = {unnest}
|
|
53
|
+
base.join_derived_concepts = [self.unnest_concept]
|
|
54
|
+
return base
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from trilogy.core.models import SourceType, Concept, Grain
|
|
5
|
+
from trilogy.core.processing.nodes.base_node import StrategyNode, QueryDatasource
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WindowNode(StrategyNode):
|
|
9
|
+
source_type = SourceType.WINDOW
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
input_concepts: List[Concept],
|
|
14
|
+
output_concepts: List[Concept],
|
|
15
|
+
environment,
|
|
16
|
+
g,
|
|
17
|
+
whole_grain: bool = False,
|
|
18
|
+
parents: List["StrategyNode"] | None = None,
|
|
19
|
+
depth: int = 0,
|
|
20
|
+
):
|
|
21
|
+
super().__init__(
|
|
22
|
+
input_concepts=input_concepts,
|
|
23
|
+
output_concepts=output_concepts,
|
|
24
|
+
environment=environment,
|
|
25
|
+
g=g,
|
|
26
|
+
whole_grain=whole_grain,
|
|
27
|
+
parents=parents,
|
|
28
|
+
depth=depth,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def _resolve(self) -> QueryDatasource:
|
|
32
|
+
base = super()._resolve()
|
|
33
|
+
base.grain = Grain(components=self.input_concepts)
|
|
34
|
+
return base
|