pytrilogy 0.0.2.8__py3-none-any.whl → 0.0.2.9__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.8.dist-info → pytrilogy-0.0.2.9.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.9.dist-info}/RECORD +31 -31
- {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.9.dist-info}/WHEEL +1 -1
- trilogy/__init__.py +1 -1
- trilogy/constants.py +1 -0
- trilogy/core/models.py +144 -54
- trilogy/core/optimization.py +44 -5
- trilogy/core/optimizations/inline_datasource.py +14 -8
- trilogy/core/optimizations/predicate_pushdown.py +73 -44
- trilogy/core/processing/concept_strategies_v3.py +69 -28
- trilogy/core/processing/node_generators/common.py +42 -16
- trilogy/core/processing/node_generators/filter_node.py +89 -48
- trilogy/core/processing/node_generators/group_node.py +3 -1
- trilogy/core/processing/node_generators/rowset_node.py +13 -54
- trilogy/core/processing/node_generators/select_node.py +10 -13
- trilogy/core/processing/node_generators/unnest_node.py +5 -3
- trilogy/core/processing/node_generators/window_node.py +23 -2
- trilogy/core/processing/nodes/__init__.py +34 -6
- trilogy/core/processing/nodes/base_node.py +67 -13
- trilogy/core/processing/nodes/filter_node.py +3 -0
- trilogy/core/processing/nodes/group_node.py +3 -0
- trilogy/core/processing/nodes/merge_node.py +1 -11
- trilogy/core/processing/nodes/select_node_v2.py +1 -0
- trilogy/core/processing/utility.py +29 -10
- trilogy/core/query_processor.py +47 -20
- trilogy/dialect/base.py +11 -4
- trilogy/parsing/parse_engine.py +20 -1
- trilogy/parsing/trilogy.lark +3 -1
- {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.9.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.9.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.9.dist-info}/top_level.txt +0 -0
|
@@ -17,6 +17,7 @@ from trilogy.core.models import (
|
|
|
17
17
|
from trilogy.core.enums import Purpose, JoinType, PurposeLineage, Granularity
|
|
18
18
|
from trilogy.utility import unique
|
|
19
19
|
from dataclasses import dataclass
|
|
20
|
+
from trilogy.core.enums import BooleanOperator
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def concept_list_to_grain(
|
|
@@ -71,7 +72,6 @@ def resolve_concept_map(
|
|
|
71
72
|
concept_map[concept.address].add(input)
|
|
72
73
|
elif concept.address not in concept_map:
|
|
73
74
|
concept_map[concept.address].add(input)
|
|
74
|
-
|
|
75
75
|
# second loop, include partials
|
|
76
76
|
for input in inputs:
|
|
77
77
|
for concept in input.output_concepts:
|
|
@@ -92,14 +92,31 @@ def resolve_concept_map(
|
|
|
92
92
|
return concept_map
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
def get_all_parent_partial(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
def get_all_parent_partial(
|
|
96
|
+
all_concepts: List[Concept], parents: List["StrategyNode"]
|
|
97
|
+
) -> List[Concept]:
|
|
98
|
+
return unique(
|
|
99
|
+
[
|
|
100
|
+
c
|
|
101
|
+
for c in all_concepts
|
|
102
|
+
if len(
|
|
103
|
+
[
|
|
104
|
+
p
|
|
105
|
+
for p in parents
|
|
106
|
+
if c.address in [x.address for x in p.partial_concepts]
|
|
107
|
+
]
|
|
108
|
+
)
|
|
109
|
+
>= 1
|
|
110
|
+
and all(
|
|
111
|
+
[
|
|
112
|
+
c.address in p.partial_lcl
|
|
113
|
+
for p in parents
|
|
114
|
+
if c.address in p.output_lcl
|
|
115
|
+
]
|
|
116
|
+
)
|
|
117
|
+
],
|
|
118
|
+
"address",
|
|
119
|
+
)
|
|
103
120
|
|
|
104
121
|
|
|
105
122
|
class StrategyNode:
|
|
@@ -137,7 +154,7 @@ class StrategyNode:
|
|
|
137
154
|
self.partial_concepts = partial_concepts or get_all_parent_partial(
|
|
138
155
|
self.output_concepts, self.parents
|
|
139
156
|
)
|
|
140
|
-
|
|
157
|
+
|
|
141
158
|
self.depth = depth
|
|
142
159
|
self.conditions = conditions
|
|
143
160
|
self.grain = grain
|
|
@@ -146,13 +163,50 @@ class StrategyNode:
|
|
|
146
163
|
self.hidden_concepts = hidden_concepts or []
|
|
147
164
|
self.existence_concepts = existence_concepts or []
|
|
148
165
|
self.virtual_output_concepts = virtual_output_concepts or []
|
|
166
|
+
self.validate_parents()
|
|
167
|
+
|
|
168
|
+
def add_parents(self, parents: list["StrategyNode"]):
|
|
169
|
+
self.parents += parents
|
|
170
|
+
self.validate_parents()
|
|
171
|
+
|
|
172
|
+
def add_condition(self, condition: Conditional | Comparison | Parenthetical):
|
|
173
|
+
if self.conditions:
|
|
174
|
+
self.conditions = Conditional(
|
|
175
|
+
left=self.conditions, right=condition, operator=BooleanOperator.AND
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
self.conditions = condition
|
|
179
|
+
|
|
180
|
+
def validate_parents(self):
|
|
181
|
+
# validate parents exist
|
|
182
|
+
# assign partial values where needed
|
|
149
183
|
for parent in self.parents:
|
|
150
184
|
if not parent:
|
|
151
185
|
raise SyntaxError("Unresolvable parent")
|
|
152
186
|
|
|
187
|
+
# TODO: make this accurate
|
|
188
|
+
if self.parents:
|
|
189
|
+
self.partial_concepts = get_all_parent_partial(
|
|
190
|
+
self.output_concepts, self.parents
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
self.partial_lcl = LooseConceptList(concepts=self.partial_concepts)
|
|
194
|
+
|
|
153
195
|
def add_output_concepts(self, concepts: List[Concept]):
|
|
154
196
|
for concept in concepts:
|
|
155
|
-
self.
|
|
197
|
+
if concept.address not in self.output_lcl.addresses:
|
|
198
|
+
self.output_concepts.append(concept)
|
|
199
|
+
self.output_lcl = LooseConceptList(concepts=self.output_concepts)
|
|
200
|
+
self.rebuild_cache()
|
|
201
|
+
|
|
202
|
+
def add_existence_concepts(self, concepts: List[Concept]):
|
|
203
|
+
for concept in concepts:
|
|
204
|
+
if concept.address not in [x.address for x in self.output_concepts]:
|
|
205
|
+
self.existence_concepts.append(concept)
|
|
206
|
+
self.rebuild_cache()
|
|
207
|
+
|
|
208
|
+
def set_output_concepts(self, concepts: List[Concept]):
|
|
209
|
+
self.output_concepts = concepts
|
|
156
210
|
self.output_lcl = LooseConceptList(concepts=self.output_concepts)
|
|
157
211
|
self.rebuild_cache()
|
|
158
212
|
|
|
@@ -195,8 +249,8 @@ class StrategyNode:
|
|
|
195
249
|
grain = self.grain if self.grain else Grain(components=self.output_concepts)
|
|
196
250
|
source_map = resolve_concept_map(
|
|
197
251
|
parent_sources,
|
|
198
|
-
self.output_concepts,
|
|
199
|
-
self.input_concepts + self.existence_concepts,
|
|
252
|
+
targets=self.output_concepts,
|
|
253
|
+
inherited_inputs=self.input_concepts + self.existence_concepts,
|
|
200
254
|
)
|
|
201
255
|
return QueryDatasource(
|
|
202
256
|
input_concepts=self.input_concepts,
|
|
@@ -36,6 +36,7 @@ class FilterNode(StrategyNode):
|
|
|
36
36
|
partial_concepts: List[Concept] | None = None,
|
|
37
37
|
force_group: bool | None = False,
|
|
38
38
|
grain: Grain | None = None,
|
|
39
|
+
existence_concepts: List[Concept] | None = None,
|
|
39
40
|
):
|
|
40
41
|
super().__init__(
|
|
41
42
|
output_concepts=output_concepts,
|
|
@@ -49,6 +50,7 @@ class FilterNode(StrategyNode):
|
|
|
49
50
|
partial_concepts=partial_concepts,
|
|
50
51
|
force_group=force_group,
|
|
51
52
|
grain=grain,
|
|
53
|
+
existence_concepts=existence_concepts,
|
|
52
54
|
)
|
|
53
55
|
|
|
54
56
|
def copy(self) -> "FilterNode":
|
|
@@ -64,4 +66,5 @@ class FilterNode(StrategyNode):
|
|
|
64
66
|
partial_concepts=list(self.partial_concepts),
|
|
65
67
|
force_group=self.force_group,
|
|
66
68
|
grain=self.grain,
|
|
69
|
+
existence_concepts=list(self.existence_concepts),
|
|
67
70
|
)
|
|
@@ -39,6 +39,7 @@ class GroupNode(StrategyNode):
|
|
|
39
39
|
partial_concepts: Optional[List[Concept]] = None,
|
|
40
40
|
force_group: bool | None = None,
|
|
41
41
|
conditions: Conditional | Comparison | Parenthetical | None = None,
|
|
42
|
+
existence_concepts: List[Concept] | None = None,
|
|
42
43
|
):
|
|
43
44
|
super().__init__(
|
|
44
45
|
input_concepts=input_concepts,
|
|
@@ -51,6 +52,7 @@ class GroupNode(StrategyNode):
|
|
|
51
52
|
partial_concepts=partial_concepts,
|
|
52
53
|
force_group=force_group,
|
|
53
54
|
conditions=conditions,
|
|
55
|
+
existence_concepts=existence_concepts,
|
|
54
56
|
)
|
|
55
57
|
|
|
56
58
|
def _resolve(self) -> QueryDatasource:
|
|
@@ -173,4 +175,5 @@ class GroupNode(StrategyNode):
|
|
|
173
175
|
partial_concepts=list(self.partial_concepts),
|
|
174
176
|
force_group=self.force_group,
|
|
175
177
|
conditions=self.conditions,
|
|
178
|
+
existence_concepts=list(self.existence_concepts),
|
|
176
179
|
)
|
|
@@ -138,16 +138,6 @@ class MergeNode(StrategyNode):
|
|
|
138
138
|
continue
|
|
139
139
|
final_joins.append(join)
|
|
140
140
|
self.node_joins = final_joins
|
|
141
|
-
partial_lookup: list[Concept] = []
|
|
142
|
-
non_partial: List[Concept] = []
|
|
143
|
-
for node in parents or []:
|
|
144
|
-
partial_lookup += node.partial_concepts
|
|
145
|
-
non_partial += [
|
|
146
|
-
x for x in node.output_concepts if x not in node.partial_concepts
|
|
147
|
-
]
|
|
148
|
-
|
|
149
|
-
final_partial = [x for x in partial_lookup if x not in non_partial]
|
|
150
|
-
self.partial_concepts = final_partial
|
|
151
141
|
|
|
152
142
|
def translate_node_joins(self, node_joins: List[NodeJoin]) -> List[BaseJoin]:
|
|
153
143
|
joins = []
|
|
@@ -265,7 +255,6 @@ class MergeNode(StrategyNode):
|
|
|
265
255
|
for x in final_datasets
|
|
266
256
|
if all([y in self.existence_concepts for y in x.output_concepts])
|
|
267
257
|
]
|
|
268
|
-
|
|
269
258
|
if len(merged.keys()) == 1:
|
|
270
259
|
final: QueryDatasource | Datasource = list(merged.values())[0]
|
|
271
260
|
if (
|
|
@@ -378,4 +367,5 @@ class MergeNode(StrategyNode):
|
|
|
378
367
|
node_joins=self.node_joins,
|
|
379
368
|
join_concepts=list(self.join_concepts) if self.join_concepts else None,
|
|
380
369
|
force_join_type=self.force_join_type,
|
|
370
|
+
existence_concepts=list(self.existence_concepts),
|
|
381
371
|
)
|
|
@@ -166,6 +166,7 @@ class SelectNode(StrategyNode):
|
|
|
166
166
|
resolution = self.resolve_from_provided_datasource()
|
|
167
167
|
if resolution:
|
|
168
168
|
return resolution
|
|
169
|
+
|
|
169
170
|
required = [c.address for c in self.all_concepts]
|
|
170
171
|
raise NoDatasourceException(
|
|
171
172
|
f"Could not find any way to resolve datasources for required concepts {required} with derivation {[x.derivation for x in self.all_concepts]}"
|
|
@@ -20,7 +20,7 @@ from trilogy.core.models import (
|
|
|
20
20
|
DataType,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
-
from trilogy.core.enums import Purpose, Granularity
|
|
23
|
+
from trilogy.core.enums import Purpose, Granularity, BooleanOperator
|
|
24
24
|
from trilogy.core.constants import CONSTANT_DATASET
|
|
25
25
|
from enum import Enum
|
|
26
26
|
from trilogy.utility import unique
|
|
@@ -62,7 +62,7 @@ def create_log_lambda(prefix: str, depth: int, logger: Logger):
|
|
|
62
62
|
pad = padding(depth)
|
|
63
63
|
|
|
64
64
|
def log_lambda(msg: str):
|
|
65
|
-
logger.info(f"{pad}
|
|
65
|
+
logger.info(f"{pad}{prefix} {msg}")
|
|
66
66
|
|
|
67
67
|
return log_lambda
|
|
68
68
|
|
|
@@ -328,14 +328,6 @@ def get_disconnected_components(
|
|
|
328
328
|
for datasource, concepts in concept_map.items():
|
|
329
329
|
graph.add_node(datasource, type=NodeType.NODE)
|
|
330
330
|
for concept in concepts:
|
|
331
|
-
# TODO: determine if this is the right way to handle things
|
|
332
|
-
# if concept.derivation in (PurposeLineage.FILTER, PurposeLineage.WINDOW):
|
|
333
|
-
# if isinstance(concept.lineage, FilterItem):
|
|
334
|
-
# graph.add_node(concept.lineage.content.address, type=NodeType.CONCEPT)
|
|
335
|
-
# graph.add_edge(datasource, concept.lineage.content.address)
|
|
336
|
-
# if isinstance(concept.lineage, WindowItem):
|
|
337
|
-
# graph.add_node(concept.lineage.content.address, type=NodeType.CONCEPT)
|
|
338
|
-
# graph.add_edge(datasource, concept.lineage.content.address)
|
|
339
331
|
graph.add_node(concept.address, type=NodeType.CONCEPT)
|
|
340
332
|
graph.add_edge(datasource, concept.address)
|
|
341
333
|
all_concepts.add(concept)
|
|
@@ -378,3 +370,30 @@ def is_scalar_condition(
|
|
|
378
370
|
elif isinstance(element, Conditional):
|
|
379
371
|
return is_scalar_condition(element.left) and is_scalar_condition(element.right)
|
|
380
372
|
return True
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def decompose_condition(
|
|
376
|
+
conditional: Conditional,
|
|
377
|
+
) -> list[SubselectComparison | Comparison | Conditional | Parenthetical]:
|
|
378
|
+
chunks: list[SubselectComparison | Comparison | Conditional | Parenthetical] = []
|
|
379
|
+
if conditional.operator == BooleanOperator.AND:
|
|
380
|
+
if not (
|
|
381
|
+
isinstance(
|
|
382
|
+
conditional.left,
|
|
383
|
+
(SubselectComparison, Comparison, Conditional, Parenthetical),
|
|
384
|
+
)
|
|
385
|
+
and isinstance(
|
|
386
|
+
conditional.right,
|
|
387
|
+
(SubselectComparison, Comparison, Conditional, Parenthetical),
|
|
388
|
+
)
|
|
389
|
+
):
|
|
390
|
+
chunks.append(conditional)
|
|
391
|
+
else:
|
|
392
|
+
for val in [conditional.left, conditional.right]:
|
|
393
|
+
if isinstance(val, Conditional):
|
|
394
|
+
chunks.extend(decompose_condition(val))
|
|
395
|
+
else:
|
|
396
|
+
chunks.append(val)
|
|
397
|
+
else:
|
|
398
|
+
chunks.append(conditional)
|
|
399
|
+
return chunks
|
trilogy/core/query_processor.py
CHANGED
|
@@ -4,9 +4,9 @@ from trilogy.core.env_processor import generate_graph
|
|
|
4
4
|
from trilogy.core.graph_models import ReferenceGraph
|
|
5
5
|
from trilogy.core.constants import CONSTANT_DATASET
|
|
6
6
|
from trilogy.core.processing.concept_strategies_v3 import source_query_concepts
|
|
7
|
-
from trilogy.core.enums import SelectFiltering
|
|
7
|
+
from trilogy.core.enums import SelectFiltering, BooleanOperator
|
|
8
8
|
from trilogy.constants import CONFIG, DEFAULT_NAMESPACE
|
|
9
|
-
from trilogy.core.processing.nodes import GroupNode, SelectNode, StrategyNode
|
|
9
|
+
from trilogy.core.processing.nodes import GroupNode, SelectNode, StrategyNode, History
|
|
10
10
|
from trilogy.core.models import (
|
|
11
11
|
Concept,
|
|
12
12
|
Environment,
|
|
@@ -24,6 +24,7 @@ from trilogy.core.models import (
|
|
|
24
24
|
Datasource,
|
|
25
25
|
BaseJoin,
|
|
26
26
|
InstantiatedUnnestJoin,
|
|
27
|
+
Conditional,
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
from trilogy.utility import unique
|
|
@@ -307,7 +308,10 @@ def datasource_to_ctes(
|
|
|
307
308
|
|
|
308
309
|
|
|
309
310
|
def append_existence_check(
|
|
310
|
-
node: StrategyNode,
|
|
311
|
+
node: StrategyNode,
|
|
312
|
+
environment: Environment,
|
|
313
|
+
graph: ReferenceGraph,
|
|
314
|
+
history: History | None = None,
|
|
311
315
|
):
|
|
312
316
|
# we if we have a where clause doing an existence check
|
|
313
317
|
# treat that as separate subquery
|
|
@@ -318,25 +322,22 @@ def append_existence_check(
|
|
|
318
322
|
logger.info(
|
|
319
323
|
f"{LOGGER_PREFIX} fetching existance clause inputs {[str(c) for c in subselect]}"
|
|
320
324
|
)
|
|
321
|
-
eds = source_query_concepts(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
for x in final_eds.output_concepts:
|
|
327
|
-
if x.address not in first_parent.existence_source_map:
|
|
328
|
-
first_parent.existence_source_map[x.address] = {final_eds}
|
|
325
|
+
eds = source_query_concepts(
|
|
326
|
+
[*subselect], environment=environment, g=graph, history=history
|
|
327
|
+
)
|
|
328
|
+
node.add_parents([eds])
|
|
329
|
+
node.add_existence_concepts([*subselect])
|
|
329
330
|
|
|
330
331
|
|
|
331
|
-
def
|
|
332
|
+
def get_query_node(
|
|
332
333
|
environment: Environment,
|
|
333
334
|
statement: SelectStatement | MultiSelectStatement,
|
|
334
335
|
graph: Optional[ReferenceGraph] = None,
|
|
335
|
-
|
|
336
|
-
) ->
|
|
336
|
+
history: History | None = None,
|
|
337
|
+
) -> StrategyNode:
|
|
337
338
|
graph = graph or generate_graph(environment)
|
|
338
339
|
logger.info(
|
|
339
|
-
f"{LOGGER_PREFIX} getting source datasource for query with output {[str(c) for c in statement.output_components]}"
|
|
340
|
+
f"{LOGGER_PREFIX} getting source datasource for query with filtering {statement.where_clause_category} and output {[str(c) for c in statement.output_components]}"
|
|
340
341
|
)
|
|
341
342
|
if not statement.output_components:
|
|
342
343
|
raise ValueError(f"Statement has no output components {statement}")
|
|
@@ -353,22 +354,28 @@ def get_query_datasources(
|
|
|
353
354
|
)
|
|
354
355
|
nest_where = True
|
|
355
356
|
|
|
356
|
-
ods = source_query_concepts(
|
|
357
|
+
ods: StrategyNode = source_query_concepts(
|
|
357
358
|
search_concepts,
|
|
358
359
|
environment=environment,
|
|
359
360
|
g=graph,
|
|
361
|
+
conditions=(statement.where_clause if statement.where_clause else None),
|
|
362
|
+
history=history,
|
|
360
363
|
)
|
|
361
|
-
|
|
364
|
+
if not ods:
|
|
365
|
+
raise ValueError(
|
|
366
|
+
f"Could not find source query concepts for {[x.address for x in search_concepts]}"
|
|
367
|
+
)
|
|
368
|
+
ds: StrategyNode
|
|
362
369
|
if nest_where and statement.where_clause:
|
|
363
370
|
if not all_aggregate:
|
|
364
371
|
ods.conditions = statement.where_clause.conditional
|
|
365
|
-
ods.output_concepts =
|
|
372
|
+
ods.output_concepts = statement.output_components
|
|
366
373
|
# ods.hidden_concepts = where_delta
|
|
367
374
|
ods.rebuild_cache()
|
|
368
|
-
append_existence_check(ods, environment, graph)
|
|
375
|
+
append_existence_check(ods, environment, graph, history)
|
|
369
376
|
ds = GroupNode(
|
|
370
377
|
output_concepts=statement.output_components,
|
|
371
|
-
input_concepts=
|
|
378
|
+
input_concepts=statement.output_components,
|
|
372
379
|
parents=[ods],
|
|
373
380
|
environment=ods.environment,
|
|
374
381
|
g=ods.g,
|
|
@@ -390,7 +397,26 @@ def get_query_datasources(
|
|
|
390
397
|
|
|
391
398
|
else:
|
|
392
399
|
ds = ods
|
|
400
|
+
if statement.having_clause:
|
|
401
|
+
if ds.conditions:
|
|
402
|
+
ds.conditions = Conditional(
|
|
403
|
+
left=ds.conditions,
|
|
404
|
+
right=statement.having_clause.conditional,
|
|
405
|
+
operator=BooleanOperator.AND,
|
|
406
|
+
)
|
|
407
|
+
else:
|
|
408
|
+
ds.conditions = statement.having_clause.conditional
|
|
409
|
+
return ds
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def get_query_datasources(
|
|
413
|
+
environment: Environment,
|
|
414
|
+
statement: SelectStatement | MultiSelectStatement,
|
|
415
|
+
graph: Optional[ReferenceGraph] = None,
|
|
416
|
+
hooks: Optional[List[BaseHook]] = None,
|
|
417
|
+
) -> QueryDatasource:
|
|
393
418
|
|
|
419
|
+
ds = get_query_node(environment, statement, graph)
|
|
394
420
|
final_qds = ds.resolve()
|
|
395
421
|
if hooks:
|
|
396
422
|
for hook in hooks:
|
|
@@ -475,6 +501,7 @@ def process_query(
|
|
|
475
501
|
grain=statement.grain,
|
|
476
502
|
limit=statement.limit,
|
|
477
503
|
where_clause=statement.where_clause,
|
|
504
|
+
having_clause=statement.having_clause,
|
|
478
505
|
output_columns=statement.output_components,
|
|
479
506
|
ctes=final_ctes,
|
|
480
507
|
base=root_cte,
|
trilogy/dialect/base.py
CHANGED
|
@@ -386,14 +386,19 @@ class BaseDialect:
|
|
|
386
386
|
e.right.address,
|
|
387
387
|
[
|
|
388
388
|
INVALID_REFERENCE_STRING(
|
|
389
|
-
f"Missing source reference to {e.right.
|
|
389
|
+
f"Missing source reference to {e.right.address}"
|
|
390
390
|
)
|
|
391
391
|
],
|
|
392
392
|
)
|
|
393
393
|
else:
|
|
394
394
|
lookup = lookup_cte.existence_source_map[e.right.address]
|
|
395
|
-
|
|
396
|
-
|
|
395
|
+
if len(lookup) > 0:
|
|
396
|
+
target = lookup[0]
|
|
397
|
+
else:
|
|
398
|
+
target = INVALID_REFERENCE_STRING(
|
|
399
|
+
f"Missing source CTE for {e.right.address}"
|
|
400
|
+
)
|
|
401
|
+
return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map)} {e.operator.value} (select {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} from {target} where {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} is not null)"
|
|
397
402
|
elif isinstance(e.right, (ListWrapper, Parenthetical, list)):
|
|
398
403
|
return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map)}"
|
|
399
404
|
|
|
@@ -514,7 +519,9 @@ class BaseDialect:
|
|
|
514
519
|
):
|
|
515
520
|
source = f"{render_unnest(self.UNNEST_MODE, self.QUOTE_CHARACTER, cte.join_derived_concepts[0], self.render_concept_sql, cte)}"
|
|
516
521
|
# direct - eg DUCK DB - can be directly selected inline
|
|
517
|
-
elif
|
|
522
|
+
elif (
|
|
523
|
+
cte.join_derived_concepts and self.UNNEST_MODE == UnnestMode.DIRECT
|
|
524
|
+
):
|
|
518
525
|
source = None
|
|
519
526
|
else:
|
|
520
527
|
raise SyntaxError("CTE has joins but no from clause")
|
trilogy/parsing/parse_engine.py
CHANGED
|
@@ -108,6 +108,7 @@ from trilogy.core.models import (
|
|
|
108
108
|
list_to_wrapper,
|
|
109
109
|
dict_to_map_wrapper,
|
|
110
110
|
NumericType,
|
|
111
|
+
HavingClause,
|
|
111
112
|
)
|
|
112
113
|
from trilogy.parsing.exceptions import ParseError
|
|
113
114
|
from trilogy.utility import string_to_hash
|
|
@@ -960,6 +961,7 @@ class ParseToObjects(Transformer):
|
|
|
960
961
|
limit = None
|
|
961
962
|
order_by = None
|
|
962
963
|
where = None
|
|
964
|
+
having = None
|
|
963
965
|
for arg in args:
|
|
964
966
|
if isinstance(arg, List):
|
|
965
967
|
select_items = arg
|
|
@@ -967,13 +969,16 @@ class ParseToObjects(Transformer):
|
|
|
967
969
|
limit = arg.count
|
|
968
970
|
elif isinstance(arg, OrderBy):
|
|
969
971
|
order_by = arg
|
|
970
|
-
elif isinstance(arg, WhereClause):
|
|
972
|
+
elif isinstance(arg, WhereClause) and not isinstance(arg, HavingClause):
|
|
971
973
|
where = arg
|
|
974
|
+
elif isinstance(arg, HavingClause):
|
|
975
|
+
having = arg
|
|
972
976
|
if not select_items:
|
|
973
977
|
raise ValueError("Malformed select, missing select items")
|
|
974
978
|
output = SelectStatement(
|
|
975
979
|
selection=select_items,
|
|
976
980
|
where_clause=where,
|
|
981
|
+
having_clause=having,
|
|
977
982
|
limit=limit,
|
|
978
983
|
order_by=order_by,
|
|
979
984
|
meta=Metadata(line_number=meta.line),
|
|
@@ -991,6 +996,7 @@ class ParseToObjects(Transformer):
|
|
|
991
996
|
and output.where_clause_category == SelectFiltering.IMPLICIT
|
|
992
997
|
else None
|
|
993
998
|
),
|
|
999
|
+
environment=self.environment,
|
|
994
1000
|
)
|
|
995
1001
|
self.environment.add_concept(new_concept, meta=meta)
|
|
996
1002
|
item.content.output = new_concept
|
|
@@ -1032,6 +1038,19 @@ class ParseToObjects(Transformer):
|
|
|
1032
1038
|
)
|
|
1033
1039
|
return WhereClause(conditional=root)
|
|
1034
1040
|
|
|
1041
|
+
def having(self, args):
|
|
1042
|
+
root = args[0]
|
|
1043
|
+
if not isinstance(root, (Comparison, Conditional, Parenthetical)):
|
|
1044
|
+
if arg_to_datatype(root) == DataType.BOOL:
|
|
1045
|
+
root = Comparison(left=root, right=True, operator=ComparisonOperator.EQ)
|
|
1046
|
+
else:
|
|
1047
|
+
root = Comparison(
|
|
1048
|
+
left=root,
|
|
1049
|
+
right=MagicConstants.NULL,
|
|
1050
|
+
operator=ComparisonOperator.IS_NOT,
|
|
1051
|
+
)
|
|
1052
|
+
return HavingClause(conditional=root)
|
|
1053
|
+
|
|
1035
1054
|
@v_args(meta=True)
|
|
1036
1055
|
def function_binding_list(self, meta: Meta, args) -> Concept:
|
|
1037
1056
|
return args
|
trilogy/parsing/trilogy.lark
CHANGED
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
persist_statement: "persist"i IDENTIFIER "into"i IDENTIFIER "from"i select_statement grain_clause?
|
|
62
62
|
|
|
63
63
|
// select statement
|
|
64
|
-
select_statement: "select"i select_list
|
|
64
|
+
select_statement: where? "select"i select_list where? having? order_by? limit?
|
|
65
65
|
|
|
66
66
|
// multiple_selects
|
|
67
67
|
multi_select_statement: select_statement ("merge" select_statement)+ "align"i align_clause where? order_by? limit?
|
|
@@ -134,6 +134,8 @@
|
|
|
134
134
|
|
|
135
135
|
where: "WHERE"i conditional
|
|
136
136
|
|
|
137
|
+
having: "HAVING"i conditional
|
|
138
|
+
|
|
137
139
|
!array_comparison: ( ("NOT"i "IN"i) | "IN"i)
|
|
138
140
|
|
|
139
141
|
COMPARISON_OPERATOR: (/is[\s]+not/ | "is" |"=" | ">" | "<" | ">=" | "<=" | "!=")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|