pytrilogy 0.0.2.15__py3-none-any.whl → 0.0.2.18__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.15.dist-info → pytrilogy-0.0.2.18.dist-info}/METADATA +12 -8
- pytrilogy-0.0.2.18.dist-info/RECORD +83 -0
- trilogy/__init__.py +1 -1
- trilogy/constants.py +1 -1
- trilogy/core/enums.py +1 -0
- trilogy/core/functions.py +11 -0
- trilogy/core/models.py +105 -59
- trilogy/core/optimization.py +15 -9
- trilogy/core/processing/concept_strategies_v3.py +372 -145
- trilogy/core/processing/node_generators/basic_node.py +27 -55
- trilogy/core/processing/node_generators/common.py +6 -7
- trilogy/core/processing/node_generators/filter_node.py +28 -31
- trilogy/core/processing/node_generators/group_node.py +14 -2
- trilogy/core/processing/node_generators/group_to_node.py +3 -1
- trilogy/core/processing/node_generators/multiselect_node.py +3 -0
- trilogy/core/processing/node_generators/node_merge_node.py +14 -9
- trilogy/core/processing/node_generators/rowset_node.py +12 -12
- trilogy/core/processing/node_generators/select_merge_node.py +302 -0
- trilogy/core/processing/node_generators/select_node.py +7 -511
- trilogy/core/processing/node_generators/unnest_node.py +4 -3
- trilogy/core/processing/node_generators/window_node.py +12 -37
- trilogy/core/processing/nodes/__init__.py +0 -2
- trilogy/core/processing/nodes/base_node.py +69 -20
- trilogy/core/processing/nodes/filter_node.py +3 -0
- trilogy/core/processing/nodes/group_node.py +18 -17
- trilogy/core/processing/nodes/merge_node.py +4 -10
- trilogy/core/processing/nodes/select_node_v2.py +28 -14
- trilogy/core/processing/nodes/window_node.py +1 -2
- trilogy/core/processing/utility.py +51 -3
- trilogy/core/query_processor.py +17 -73
- trilogy/dialect/base.py +8 -3
- trilogy/dialect/common.py +65 -10
- trilogy/dialect/duckdb.py +4 -1
- trilogy/dialect/sql_server.py +3 -3
- trilogy/executor.py +5 -0
- trilogy/hooks/query_debugger.py +5 -3
- trilogy/parsing/parse_engine.py +67 -39
- trilogy/parsing/render.py +2 -0
- trilogy/parsing/trilogy.lark +6 -3
- pytrilogy-0.0.2.15.dist-info/RECORD +0 -82
- {pytrilogy-0.0.2.15.dist-info → pytrilogy-0.0.2.18.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.15.dist-info → pytrilogy-0.0.2.18.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.2.15.dist-info → pytrilogy-0.0.2.18.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.2.15.dist-info → pytrilogy-0.0.2.18.dist-info}/top_level.txt +0 -0
|
@@ -79,11 +79,12 @@ def resolve_concept_map(
|
|
|
79
79
|
# second loop, include partials
|
|
80
80
|
for input in inputs:
|
|
81
81
|
for concept in input.output_concepts:
|
|
82
|
-
if concept.address not in [t
|
|
82
|
+
if concept.address not in [t for t in inherited_inputs]:
|
|
83
83
|
continue
|
|
84
|
-
if
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
if (
|
|
85
|
+
isinstance(input, QueryDatasource)
|
|
86
|
+
and concept.address in input.hidden_concepts
|
|
87
|
+
):
|
|
87
88
|
continue
|
|
88
89
|
if len(concept_map.get(concept.address, [])) == 0:
|
|
89
90
|
concept_map[concept.address].add(input)
|
|
@@ -158,6 +159,7 @@ class StrategyNode:
|
|
|
158
159
|
nullable_concepts: List[Concept] | None = None,
|
|
159
160
|
depth: int = 0,
|
|
160
161
|
conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
162
|
+
preexisting_conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
161
163
|
force_group: bool | None = None,
|
|
162
164
|
grain: Optional[Grain] = None,
|
|
163
165
|
hidden_concepts: List[Concept] | None = None,
|
|
@@ -191,11 +193,32 @@ class StrategyNode:
|
|
|
191
193
|
self.hidden_concepts = hidden_concepts or []
|
|
192
194
|
self.existence_concepts = existence_concepts or []
|
|
193
195
|
self.virtual_output_concepts = virtual_output_concepts or []
|
|
196
|
+
self.preexisting_conditions = preexisting_conditions
|
|
197
|
+
if self.conditions and not self.preexisting_conditions:
|
|
198
|
+
self.preexisting_conditions = self.conditions
|
|
199
|
+
elif (
|
|
200
|
+
self.conditions
|
|
201
|
+
and self.preexisting_conditions
|
|
202
|
+
and self.conditions != self.preexisting_conditions
|
|
203
|
+
):
|
|
204
|
+
self.preexisting_conditions = Conditional(
|
|
205
|
+
left=self.conditions,
|
|
206
|
+
right=self.preexisting_conditions,
|
|
207
|
+
operator=BooleanOperator.AND,
|
|
208
|
+
)
|
|
194
209
|
self.validate_parents()
|
|
210
|
+
self.log = True
|
|
195
211
|
|
|
196
212
|
def add_parents(self, parents: list["StrategyNode"]):
|
|
197
213
|
self.parents += parents
|
|
198
214
|
self.validate_parents()
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
def set_preexisting_conditions(
|
|
218
|
+
self, conditions: Conditional | Comparison | Parenthetical
|
|
219
|
+
):
|
|
220
|
+
self.preexisting_conditions = conditions
|
|
221
|
+
return self
|
|
199
222
|
|
|
200
223
|
def add_condition(self, condition: Conditional | Comparison | Parenthetical):
|
|
201
224
|
if self.conditions:
|
|
@@ -204,6 +227,9 @@ class StrategyNode:
|
|
|
204
227
|
)
|
|
205
228
|
else:
|
|
206
229
|
self.conditions = condition
|
|
230
|
+
self.set_preexisting_conditions(condition)
|
|
231
|
+
self.rebuild_cache()
|
|
232
|
+
return self
|
|
207
233
|
|
|
208
234
|
def validate_parents(self):
|
|
209
235
|
# validate parents exist
|
|
@@ -220,40 +246,54 @@ class StrategyNode:
|
|
|
220
246
|
|
|
221
247
|
self.partial_lcl = LooseConceptList(concepts=self.partial_concepts)
|
|
222
248
|
|
|
223
|
-
def add_output_concepts(self, concepts: List[Concept]):
|
|
249
|
+
def add_output_concepts(self, concepts: List[Concept], rebuild: bool = True):
|
|
224
250
|
for concept in concepts:
|
|
225
251
|
if concept.address not in self.output_lcl.addresses:
|
|
226
252
|
self.output_concepts.append(concept)
|
|
227
253
|
self.output_lcl = LooseConceptList(concepts=self.output_concepts)
|
|
228
|
-
|
|
254
|
+
if rebuild:
|
|
255
|
+
self.rebuild_cache()
|
|
256
|
+
return self
|
|
229
257
|
|
|
230
|
-
def add_existence_concepts(self, concepts: List[Concept]):
|
|
258
|
+
def add_existence_concepts(self, concepts: List[Concept], rebuild: bool = True):
|
|
231
259
|
for concept in concepts:
|
|
232
|
-
if concept.address not in
|
|
260
|
+
if concept.address not in self.output_concepts:
|
|
233
261
|
self.existence_concepts.append(concept)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
262
|
+
if rebuild:
|
|
263
|
+
self.rebuild_cache()
|
|
264
|
+
return self
|
|
265
|
+
|
|
266
|
+
def set_output_concepts(self, concepts: List[Concept], rebuild: bool = True):
|
|
267
|
+
# exit if no changes
|
|
268
|
+
if self.output_concepts == concepts:
|
|
269
|
+
return self
|
|
237
270
|
self.output_concepts = concepts
|
|
238
271
|
self.output_lcl = LooseConceptList(concepts=self.output_concepts)
|
|
239
|
-
self.rebuild_cache()
|
|
240
272
|
|
|
241
|
-
|
|
242
|
-
|
|
273
|
+
if rebuild:
|
|
274
|
+
self.rebuild_cache()
|
|
275
|
+
return self
|
|
243
276
|
|
|
244
|
-
def
|
|
277
|
+
def add_output_concept(self, concept: Concept, rebuild: bool = True):
|
|
278
|
+
return self.add_output_concepts([concept], rebuild)
|
|
279
|
+
|
|
280
|
+
def hide_output_concepts(self, concepts: List[Concept], rebuild: bool = True):
|
|
245
281
|
for x in concepts:
|
|
246
282
|
self.hidden_concepts.append(x)
|
|
247
|
-
|
|
283
|
+
if rebuild:
|
|
284
|
+
self.rebuild_cache()
|
|
285
|
+
return self
|
|
248
286
|
|
|
249
|
-
def remove_output_concepts(self, concepts: List[Concept]):
|
|
287
|
+
def remove_output_concepts(self, concepts: List[Concept], rebuild: bool = True):
|
|
250
288
|
for x in concepts:
|
|
251
289
|
self.hidden_concepts.append(x)
|
|
252
290
|
addresses = [x.address for x in concepts]
|
|
253
291
|
self.output_concepts = [
|
|
254
292
|
x for x in self.output_concepts if x.address not in addresses
|
|
255
293
|
]
|
|
256
|
-
|
|
294
|
+
if rebuild:
|
|
295
|
+
self.rebuild_cache()
|
|
296
|
+
return self
|
|
257
297
|
|
|
258
298
|
@property
|
|
259
299
|
def logging_prefix(self) -> str:
|
|
@@ -269,7 +309,11 @@ class StrategyNode:
|
|
|
269
309
|
|
|
270
310
|
def __repr__(self):
|
|
271
311
|
concepts = self.all_concepts
|
|
272
|
-
|
|
312
|
+
addresses = [c.address for c in concepts]
|
|
313
|
+
contents = ",".join(sorted(addresses[:3]))
|
|
314
|
+
if len(addresses) > 3:
|
|
315
|
+
extra = len(addresses) - 3
|
|
316
|
+
contents += f"...{extra} more"
|
|
273
317
|
return f"{self.__class__.__name__}<{contents}>"
|
|
274
318
|
|
|
275
319
|
def _resolve(self) -> QueryDatasource:
|
|
@@ -277,7 +321,11 @@ class StrategyNode:
|
|
|
277
321
|
p.resolve() for p in self.parents
|
|
278
322
|
]
|
|
279
323
|
|
|
280
|
-
grain =
|
|
324
|
+
grain = (
|
|
325
|
+
self.grain
|
|
326
|
+
if self.grain
|
|
327
|
+
else concept_list_to_grain(self.output_concepts, [])
|
|
328
|
+
)
|
|
281
329
|
source_map = resolve_concept_map(
|
|
282
330
|
parent_sources,
|
|
283
331
|
targets=self.output_concepts,
|
|
@@ -326,6 +374,7 @@ class StrategyNode:
|
|
|
326
374
|
nullable_concepts=list(self.nullable_concepts),
|
|
327
375
|
depth=self.depth,
|
|
328
376
|
conditions=self.conditions,
|
|
377
|
+
preexisting_conditions=self.preexisting_conditions,
|
|
329
378
|
force_group=self.force_group,
|
|
330
379
|
grain=self.grain,
|
|
331
380
|
hidden_concepts=list(self.hidden_concepts),
|
|
@@ -33,6 +33,7 @@ class FilterNode(StrategyNode):
|
|
|
33
33
|
parents: List["StrategyNode"] | None = None,
|
|
34
34
|
depth: int = 0,
|
|
35
35
|
conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
36
|
+
preexisting_conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
36
37
|
partial_concepts: List[Concept] | None = None,
|
|
37
38
|
force_group: bool | None = False,
|
|
38
39
|
grain: Grain | None = None,
|
|
@@ -47,6 +48,7 @@ class FilterNode(StrategyNode):
|
|
|
47
48
|
depth=depth,
|
|
48
49
|
input_concepts=input_concepts,
|
|
49
50
|
conditions=conditions,
|
|
51
|
+
preexisting_conditions=preexisting_conditions,
|
|
50
52
|
partial_concepts=partial_concepts,
|
|
51
53
|
force_group=force_group,
|
|
52
54
|
grain=grain,
|
|
@@ -63,6 +65,7 @@ class FilterNode(StrategyNode):
|
|
|
63
65
|
parents=self.parents,
|
|
64
66
|
depth=self.depth,
|
|
65
67
|
conditions=self.conditions,
|
|
68
|
+
preexisting_conditions=self.preexisting_conditions,
|
|
66
69
|
partial_concepts=list(self.partial_concepts),
|
|
67
70
|
force_group=self.force_group,
|
|
68
71
|
grain=self.grain,
|
|
@@ -41,7 +41,9 @@ class GroupNode(StrategyNode):
|
|
|
41
41
|
nullable_concepts: Optional[List[Concept]] = None,
|
|
42
42
|
force_group: bool | None = None,
|
|
43
43
|
conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
44
|
+
preexisting_conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
44
45
|
existence_concepts: List[Concept] | None = None,
|
|
46
|
+
hidden_concepts: List[Concept] | None = None,
|
|
45
47
|
):
|
|
46
48
|
super().__init__(
|
|
47
49
|
input_concepts=input_concepts,
|
|
@@ -56,6 +58,8 @@ class GroupNode(StrategyNode):
|
|
|
56
58
|
force_group=force_group,
|
|
57
59
|
conditions=conditions,
|
|
58
60
|
existence_concepts=existence_concepts,
|
|
61
|
+
preexisting_conditions=preexisting_conditions,
|
|
62
|
+
hidden_concepts=hidden_concepts,
|
|
59
63
|
)
|
|
60
64
|
|
|
61
65
|
def _resolve(self) -> QueryDatasource:
|
|
@@ -63,23 +67,18 @@ class GroupNode(StrategyNode):
|
|
|
63
67
|
p.resolve() for p in self.parents
|
|
64
68
|
]
|
|
65
69
|
|
|
66
|
-
grain = concept_list_to_grain(self.output_concepts, [])
|
|
70
|
+
grain = self.grain or concept_list_to_grain(self.output_concepts, [])
|
|
67
71
|
comp_grain = Grain()
|
|
68
72
|
for source in parent_sources:
|
|
69
73
|
comp_grain += source.grain
|
|
70
74
|
|
|
71
75
|
# dynamically select if we need to group
|
|
72
76
|
# because sometimes, we are already at required grain
|
|
73
|
-
if
|
|
74
|
-
comp_grain == grain
|
|
75
|
-
and self.output_lcl == self.input_lcl
|
|
76
|
-
and self.force_group is not True
|
|
77
|
-
):
|
|
77
|
+
if comp_grain == grain and self.force_group is not True:
|
|
78
78
|
# if there is no group by, and inputs equal outputs
|
|
79
79
|
# return the parent
|
|
80
80
|
logger.info(
|
|
81
|
-
f"{self.logging_prefix}{LOGGER_PREFIX}
|
|
82
|
-
f" {self.output_lcl}"
|
|
81
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} Grain of group by equals output"
|
|
83
82
|
f" grains {comp_grain} and {grain}"
|
|
84
83
|
)
|
|
85
84
|
if (
|
|
@@ -88,7 +87,7 @@ class GroupNode(StrategyNode):
|
|
|
88
87
|
== self.output_lcl
|
|
89
88
|
) and isinstance(parent_sources[0], QueryDatasource):
|
|
90
89
|
logger.info(
|
|
91
|
-
f"{self.logging_prefix}{LOGGER_PREFIX} No group by required
|
|
90
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} No group by required as inputs match outputs of parent; returning parent node"
|
|
92
91
|
)
|
|
93
92
|
will_return: QueryDatasource = parent_sources[0]
|
|
94
93
|
if self.conditions:
|
|
@@ -99,21 +98,19 @@ class GroupNode(StrategyNode):
|
|
|
99
98
|
else:
|
|
100
99
|
|
|
101
100
|
logger.info(
|
|
102
|
-
f"{self.logging_prefix}{LOGGER_PREFIX} Group node has different
|
|
103
|
-
f" {self.input_lcl}"
|
|
104
|
-
" vs"
|
|
105
|
-
f" {self.output_lcl}"
|
|
106
|
-
" and"
|
|
101
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} Group node has different grain than parents; forcing group"
|
|
107
102
|
f" upstream grains {[str(source.grain) for source in parent_sources]}"
|
|
108
|
-
" vs"
|
|
103
|
+
f" with final grain {comp_grain} vs"
|
|
109
104
|
f" target grain {grain}"
|
|
110
105
|
)
|
|
111
|
-
for parent in
|
|
106
|
+
for parent in self.parents:
|
|
112
107
|
logger.info(
|
|
113
108
|
f"{self.logging_prefix}{LOGGER_PREFIX} Parent node"
|
|
114
|
-
f" {[c.address for c in parent.output_concepts]}"
|
|
109
|
+
f" {[c.address for c in parent.output_concepts[:2]]}... has"
|
|
115
110
|
" grain"
|
|
116
111
|
f" {parent.grain}"
|
|
112
|
+
f" resolved grain {parent.resolve().grain}"
|
|
113
|
+
f" {type(parent)}"
|
|
117
114
|
)
|
|
118
115
|
source_type = SourceType.GROUP
|
|
119
116
|
source_map = resolve_concept_map(
|
|
@@ -144,6 +141,7 @@ class GroupNode(StrategyNode):
|
|
|
144
141
|
grain=grain,
|
|
145
142
|
partial_concepts=self.partial_concepts,
|
|
146
143
|
nullable_concepts=nullable_concepts,
|
|
144
|
+
hidden_concepts=self.hidden_concepts,
|
|
147
145
|
condition=self.conditions,
|
|
148
146
|
)
|
|
149
147
|
# if there is a condition on a group node and it's not scalar
|
|
@@ -167,6 +165,7 @@ class GroupNode(StrategyNode):
|
|
|
167
165
|
nullable_concepts=base.nullable_concepts,
|
|
168
166
|
partial_concepts=self.partial_concepts,
|
|
169
167
|
condition=self.conditions,
|
|
168
|
+
hidden_concepts=self.hidden_concepts,
|
|
170
169
|
)
|
|
171
170
|
return base
|
|
172
171
|
|
|
@@ -183,5 +182,7 @@ class GroupNode(StrategyNode):
|
|
|
183
182
|
nullable_concepts=list(self.nullable_concepts),
|
|
184
183
|
force_group=self.force_group,
|
|
185
184
|
conditions=self.conditions,
|
|
185
|
+
preexisting_conditions=self.preexisting_conditions,
|
|
186
186
|
existence_concepts=list(self.existence_concepts),
|
|
187
|
+
hidden_concepts=list(self.hidden_concepts),
|
|
187
188
|
)
|
|
@@ -115,6 +115,7 @@ class MergeNode(StrategyNode):
|
|
|
115
115
|
depth: int = 0,
|
|
116
116
|
grain: Grain | None = None,
|
|
117
117
|
conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
118
|
+
preexisting_conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
118
119
|
hidden_concepts: List[Concept] | None = None,
|
|
119
120
|
virtual_output_concepts: List[Concept] | None = None,
|
|
120
121
|
existence_concepts: List[Concept] | None = None,
|
|
@@ -132,6 +133,7 @@ class MergeNode(StrategyNode):
|
|
|
132
133
|
force_group=force_group,
|
|
133
134
|
grain=grain,
|
|
134
135
|
conditions=conditions,
|
|
136
|
+
preexisting_conditions=preexisting_conditions,
|
|
135
137
|
hidden_concepts=hidden_concepts,
|
|
136
138
|
virtual_output_concepts=virtual_output_concepts,
|
|
137
139
|
existence_concepts=existence_concepts,
|
|
@@ -203,14 +205,6 @@ class MergeNode(StrategyNode):
|
|
|
203
205
|
logger.info(
|
|
204
206
|
f"{self.logging_prefix}{LOGGER_PREFIX} Merge node has {len(dataset_list)} parents, starting merge"
|
|
205
207
|
)
|
|
206
|
-
for item in dataset_list:
|
|
207
|
-
logger.info(
|
|
208
|
-
f"{self.logging_prefix}{LOGGER_PREFIX} for {item.full_name} partial concepts {[x.address for x in item.partial_concepts]}"
|
|
209
|
-
)
|
|
210
|
-
logger.info(
|
|
211
|
-
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]}"
|
|
212
|
-
)
|
|
213
|
-
|
|
214
208
|
if final_joins is None:
|
|
215
209
|
if not pregrain.components:
|
|
216
210
|
logger.info(
|
|
@@ -246,7 +240,7 @@ class MergeNode(StrategyNode):
|
|
|
246
240
|
for source in parent_sources:
|
|
247
241
|
if source.full_name in merged:
|
|
248
242
|
logger.info(
|
|
249
|
-
f"{self.logging_prefix}{LOGGER_PREFIX} parent node with {source.full_name} into existing"
|
|
243
|
+
f"{self.logging_prefix}{LOGGER_PREFIX} merging parent node with {source.full_name} into existing"
|
|
250
244
|
)
|
|
251
245
|
merged[source.full_name] = merged[source.full_name] + source
|
|
252
246
|
else:
|
|
@@ -304,7 +298,6 @@ class MergeNode(StrategyNode):
|
|
|
304
298
|
pregrain += source.grain
|
|
305
299
|
|
|
306
300
|
grain = self.grain if self.grain else pregrain
|
|
307
|
-
|
|
308
301
|
logger.info(
|
|
309
302
|
f"{self.logging_prefix}{LOGGER_PREFIX} has pre grain {pregrain} and final merge node grain {grain}"
|
|
310
303
|
)
|
|
@@ -377,6 +370,7 @@ class MergeNode(StrategyNode):
|
|
|
377
370
|
force_group=self.force_group,
|
|
378
371
|
grain=self.grain,
|
|
379
372
|
conditions=self.conditions,
|
|
373
|
+
preexisting_conditions=self.preexisting_conditions,
|
|
380
374
|
nullable_concepts=list(self.nullable_concepts),
|
|
381
375
|
hidden_concepts=list(self.hidden_concepts),
|
|
382
376
|
virtual_output_concepts=list(self.virtual_output_concepts),
|
|
@@ -18,8 +18,7 @@ from trilogy.core.models import (
|
|
|
18
18
|
Parenthetical,
|
|
19
19
|
)
|
|
20
20
|
from trilogy.utility import unique
|
|
21
|
-
from trilogy.core.processing.nodes.base_node import StrategyNode
|
|
22
|
-
from trilogy.core.exceptions import NoDatasourceException
|
|
21
|
+
from trilogy.core.processing.nodes.base_node import StrategyNode, resolve_concept_map
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
LOGGER_PREFIX = "[CONCEPT DETAIL - SELECT NODE]"
|
|
@@ -48,6 +47,7 @@ class SelectNode(StrategyNode):
|
|
|
48
47
|
grain: Optional[Grain] = None,
|
|
49
48
|
force_group: bool | None = False,
|
|
50
49
|
conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
50
|
+
preexisting_conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
51
51
|
hidden_concepts: List[Concept] | None = None,
|
|
52
52
|
):
|
|
53
53
|
super().__init__(
|
|
@@ -63,6 +63,7 @@ class SelectNode(StrategyNode):
|
|
|
63
63
|
force_group=force_group,
|
|
64
64
|
grain=grain,
|
|
65
65
|
conditions=conditions,
|
|
66
|
+
preexisting_conditions=preexisting_conditions,
|
|
66
67
|
hidden_concepts=hidden_concepts,
|
|
67
68
|
)
|
|
68
69
|
self.accept_partial = accept_partial
|
|
@@ -144,9 +145,7 @@ class SelectNode(StrategyNode):
|
|
|
144
145
|
|
|
145
146
|
def _resolve(self) -> QueryDatasource:
|
|
146
147
|
# if we have parent nodes, we do not need to go to a datasource
|
|
147
|
-
|
|
148
|
-
return super()._resolve()
|
|
149
|
-
resolution: QueryDatasource | None
|
|
148
|
+
resolution: QueryDatasource | None = None
|
|
150
149
|
if all(
|
|
151
150
|
[
|
|
152
151
|
(
|
|
@@ -163,17 +162,30 @@ class SelectNode(StrategyNode):
|
|
|
163
162
|
f"{self.logging_prefix}{LOGGER_PREFIX} have a constant datasource"
|
|
164
163
|
)
|
|
165
164
|
resolution = self.resolve_from_constant_datasources()
|
|
166
|
-
|
|
167
|
-
return resolution
|
|
168
|
-
if self.datasource:
|
|
165
|
+
if self.datasource and not resolution:
|
|
169
166
|
resolution = self.resolve_from_provided_datasource()
|
|
170
|
-
if resolution:
|
|
171
|
-
return resolution
|
|
172
167
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
168
|
+
if self.parents:
|
|
169
|
+
if not resolution:
|
|
170
|
+
return super()._resolve()
|
|
171
|
+
# zip in our parent source map
|
|
172
|
+
parent_sources: List[QueryDatasource | Datasource] = [
|
|
173
|
+
p.resolve() for p in self.parents
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
resolution.datasources += parent_sources
|
|
177
|
+
|
|
178
|
+
source_map = resolve_concept_map(
|
|
179
|
+
parent_sources,
|
|
180
|
+
targets=self.output_concepts,
|
|
181
|
+
inherited_inputs=self.input_concepts + self.existence_concepts,
|
|
182
|
+
)
|
|
183
|
+
for k, v in source_map.items():
|
|
184
|
+
if v and k not in resolution.source_map:
|
|
185
|
+
resolution.source_map[k] = v
|
|
186
|
+
if not resolution:
|
|
187
|
+
raise ValueError("No select node could be generated")
|
|
188
|
+
return resolution
|
|
177
189
|
|
|
178
190
|
def copy(self) -> "SelectNode":
|
|
179
191
|
return SelectNode(
|
|
@@ -191,6 +203,7 @@ class SelectNode(StrategyNode):
|
|
|
191
203
|
grain=self.grain,
|
|
192
204
|
force_group=self.force_group,
|
|
193
205
|
conditions=self.conditions,
|
|
206
|
+
preexisting_conditions=self.preexisting_conditions,
|
|
194
207
|
hidden_concepts=self.hidden_concepts,
|
|
195
208
|
)
|
|
196
209
|
|
|
@@ -208,5 +221,6 @@ class ConstantNode(SelectNode):
|
|
|
208
221
|
depth=self.depth,
|
|
209
222
|
partial_concepts=list(self.partial_concepts),
|
|
210
223
|
conditions=self.conditions,
|
|
224
|
+
preexisting_conditions=self.preexisting_conditions,
|
|
211
225
|
hidden_concepts=self.hidden_concepts,
|
|
212
226
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import List
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
from trilogy.core.models import SourceType, Concept
|
|
4
|
+
from trilogy.core.models import SourceType, Concept
|
|
5
5
|
from trilogy.core.processing.nodes.base_node import StrategyNode, QueryDatasource
|
|
6
6
|
|
|
7
7
|
|
|
@@ -30,7 +30,6 @@ class WindowNode(StrategyNode):
|
|
|
30
30
|
|
|
31
31
|
def _resolve(self) -> QueryDatasource:
|
|
32
32
|
base = super()._resolve()
|
|
33
|
-
base.grain = Grain(components=self.input_concepts)
|
|
34
33
|
return base
|
|
35
34
|
|
|
36
35
|
def copy(self) -> "WindowNode":
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List, Tuple, Dict, Set
|
|
1
|
+
from typing import List, Tuple, Dict, Set, Any
|
|
2
2
|
import networkx as nx
|
|
3
3
|
from trilogy.core.models import (
|
|
4
4
|
Datasource,
|
|
@@ -20,6 +20,14 @@ from trilogy.core.models import (
|
|
|
20
20
|
DataType,
|
|
21
21
|
ConceptPair,
|
|
22
22
|
UnnestJoin,
|
|
23
|
+
CaseWhen,
|
|
24
|
+
CaseElse,
|
|
25
|
+
MapWrapper,
|
|
26
|
+
ListWrapper,
|
|
27
|
+
MapType,
|
|
28
|
+
DatePart,
|
|
29
|
+
NumericType,
|
|
30
|
+
ListType,
|
|
23
31
|
)
|
|
24
32
|
|
|
25
33
|
from trilogy.core.enums import Purpose, Granularity, BooleanOperator, Modifier
|
|
@@ -107,9 +115,19 @@ def resolve_join_order(joins: List[BaseJoin]) -> List[BaseJoin]:
|
|
|
107
115
|
available_aliases: set[str] = set()
|
|
108
116
|
final_joins_pre = [*joins]
|
|
109
117
|
final_joins = []
|
|
118
|
+
partial = set()
|
|
110
119
|
while final_joins_pre:
|
|
111
120
|
new_final_joins_pre: List[BaseJoin] = []
|
|
112
121
|
for join in final_joins_pre:
|
|
122
|
+
if join.join_type != JoinType.INNER:
|
|
123
|
+
partial.add(join.right_datasource.identifier)
|
|
124
|
+
# an inner join after a left outer implicitly makes that outer an inner
|
|
125
|
+
# so fix that
|
|
126
|
+
if (
|
|
127
|
+
join.left_datasource.identifier in partial
|
|
128
|
+
and join.join_type == JoinType.INNER
|
|
129
|
+
):
|
|
130
|
+
join.join_type = JoinType.LEFT_OUTER
|
|
113
131
|
if not available_aliases:
|
|
114
132
|
final_joins.append(join)
|
|
115
133
|
available_aliases.add(join.left_datasource.identifier)
|
|
@@ -270,7 +288,7 @@ def get_node_joins(
|
|
|
270
288
|
relevant = concept_to_relevant_joins(local_concepts)
|
|
271
289
|
left_datasource = identifier_map[left]
|
|
272
290
|
right_datasource = identifier_map[right]
|
|
273
|
-
join_tuples = []
|
|
291
|
+
join_tuples: list[ConceptPair] = []
|
|
274
292
|
for joinc in relevant:
|
|
275
293
|
left_arg = joinc
|
|
276
294
|
right_arg = joinc
|
|
@@ -316,6 +334,19 @@ def get_node_joins(
|
|
|
316
334
|
left=left_arg, right=right_arg, modifiers=list(modifiers)
|
|
317
335
|
)
|
|
318
336
|
)
|
|
337
|
+
|
|
338
|
+
# deduplication
|
|
339
|
+
all_right = []
|
|
340
|
+
for tuple in join_tuples:
|
|
341
|
+
all_right.append(tuple.right.address)
|
|
342
|
+
right_grain = identifier_map[right].grain
|
|
343
|
+
# if the join includes all the right grain components
|
|
344
|
+
# we only need to join on those, not everything
|
|
345
|
+
if all([x.address in all_right for x in right_grain.components]):
|
|
346
|
+
join_tuples = [
|
|
347
|
+
x for x in join_tuples if x.right.address in right_grain.components
|
|
348
|
+
]
|
|
349
|
+
|
|
319
350
|
final_joins_pre.append(
|
|
320
351
|
BaseJoin(
|
|
321
352
|
left_datasource=identifier_map[left],
|
|
@@ -373,7 +404,7 @@ def is_scalar_condition(
|
|
|
373
404
|
int
|
|
374
405
|
| str
|
|
375
406
|
| float
|
|
376
|
-
| list
|
|
407
|
+
| list[Any]
|
|
377
408
|
| WindowItem
|
|
378
409
|
| FilterItem
|
|
379
410
|
| Concept
|
|
@@ -384,6 +415,14 @@ def is_scalar_condition(
|
|
|
384
415
|
| AggregateWrapper
|
|
385
416
|
| MagicConstants
|
|
386
417
|
| DataType
|
|
418
|
+
| CaseWhen
|
|
419
|
+
| CaseElse
|
|
420
|
+
| MapWrapper[Any, Any]
|
|
421
|
+
| ListType
|
|
422
|
+
| MapType
|
|
423
|
+
| NumericType
|
|
424
|
+
| DatePart
|
|
425
|
+
| ListWrapper[Any]
|
|
387
426
|
),
|
|
388
427
|
materialized: set[str] | None = None,
|
|
389
428
|
) -> bool:
|
|
@@ -398,6 +437,7 @@ def is_scalar_condition(
|
|
|
398
437
|
elif isinstance(element, Function):
|
|
399
438
|
if element.operator in FunctionClass.AGGREGATE_FUNCTIONS.value:
|
|
400
439
|
return False
|
|
440
|
+
return all([is_scalar_condition(x, materialized) for x in element.arguments])
|
|
401
441
|
elif isinstance(element, Concept):
|
|
402
442
|
if materialized and element.address in materialized:
|
|
403
443
|
return True
|
|
@@ -410,6 +450,14 @@ def is_scalar_condition(
|
|
|
410
450
|
return is_scalar_condition(element.left, materialized) and is_scalar_condition(
|
|
411
451
|
element.right, materialized
|
|
412
452
|
)
|
|
453
|
+
elif isinstance(element, CaseWhen):
|
|
454
|
+
return is_scalar_condition(
|
|
455
|
+
element.comparison, materialized
|
|
456
|
+
) and is_scalar_condition(element.expr, materialized)
|
|
457
|
+
elif isinstance(element, CaseElse):
|
|
458
|
+
return is_scalar_condition(element.expr, materialized)
|
|
459
|
+
elif isinstance(element, MagicConstants):
|
|
460
|
+
return True
|
|
413
461
|
return True
|
|
414
462
|
|
|
415
463
|
|