pytrilogy 0.0.2.11__py3-none-any.whl → 0.0.2.12__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.11.dist-info → pytrilogy-0.0.2.12.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.12.dist-info}/RECORD +27 -27
- trilogy/__init__.py +1 -1
- trilogy/core/enums.py +0 -1
- trilogy/core/environment_helpers.py +44 -6
- trilogy/core/models.py +21 -21
- trilogy/core/optimization.py +31 -3
- trilogy/core/optimizations/__init__.py +2 -1
- trilogy/core/optimizations/predicate_pushdown.py +60 -42
- trilogy/core/processing/concept_strategies_v3.py +6 -4
- trilogy/core/processing/node_generators/basic_node.py +15 -9
- trilogy/core/processing/node_generators/node_merge_node.py +22 -1
- trilogy/core/processing/node_generators/unnest_node.py +10 -3
- trilogy/core/processing/nodes/base_node.py +7 -2
- trilogy/core/processing/nodes/group_node.py +0 -1
- trilogy/core/processing/nodes/merge_node.py +11 -4
- trilogy/core/processing/nodes/unnest_node.py +13 -9
- trilogy/core/processing/utility.py +3 -1
- trilogy/core/query_processor.py +9 -2
- trilogy/dialect/base.py +95 -52
- trilogy/dialect/common.py +3 -3
- trilogy/parsing/common.py +58 -1
- trilogy/parsing/parse_engine.py +70 -122
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.12.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.12.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.12.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.12.dist-info}/top_level.txt +0 -0
|
@@ -22,9 +22,14 @@ def gen_unnest_node(
|
|
|
22
22
|
arguments = []
|
|
23
23
|
if isinstance(concept.lineage, Function):
|
|
24
24
|
arguments = concept.lineage.concept_arguments
|
|
25
|
+
|
|
26
|
+
equivalent_optional = [x for x in local_optional if x.lineage == concept.lineage]
|
|
27
|
+
non_equivalent_optional = [
|
|
28
|
+
x for x in local_optional if x not in equivalent_optional
|
|
29
|
+
]
|
|
25
30
|
if arguments or local_optional:
|
|
26
31
|
parent = source_concepts(
|
|
27
|
-
mandatory_list=arguments +
|
|
32
|
+
mandatory_list=arguments + non_equivalent_optional,
|
|
28
33
|
environment=environment,
|
|
29
34
|
g=g,
|
|
30
35
|
depth=depth + 1,
|
|
@@ -38,8 +43,8 @@ def gen_unnest_node(
|
|
|
38
43
|
return None
|
|
39
44
|
|
|
40
45
|
base = UnnestNode(
|
|
41
|
-
|
|
42
|
-
input_concepts=arguments +
|
|
46
|
+
unnest_concepts=[concept] + equivalent_optional,
|
|
47
|
+
input_concepts=arguments + non_equivalent_optional,
|
|
43
48
|
output_concepts=[concept] + local_optional,
|
|
44
49
|
environment=environment,
|
|
45
50
|
g=g,
|
|
@@ -57,4 +62,6 @@ def gen_unnest_node(
|
|
|
57
62
|
)
|
|
58
63
|
qds = new.resolve()
|
|
59
64
|
assert qds.source_map[concept.address] == {base.resolve()}
|
|
65
|
+
for x in equivalent_optional:
|
|
66
|
+
assert qds.source_map[x.address] == {base.resolve()}
|
|
60
67
|
return new
|
|
@@ -61,17 +61,22 @@ def resolve_concept_map(
|
|
|
61
61
|
for concept in input.output_concepts:
|
|
62
62
|
if concept.address not in input.non_partial_concept_addresses:
|
|
63
63
|
continue
|
|
64
|
-
|
|
65
|
-
continue
|
|
64
|
+
|
|
66
65
|
if (
|
|
67
66
|
isinstance(input, QueryDatasource)
|
|
68
67
|
and concept.address in input.hidden_concepts
|
|
69
68
|
):
|
|
70
69
|
continue
|
|
71
70
|
if concept.address in full_addresses:
|
|
71
|
+
|
|
72
72
|
concept_map[concept.address].add(input)
|
|
73
73
|
elif concept.address not in concept_map:
|
|
74
|
+
# equi_targets = [x for x in targets if concept.address in x.pseudonyms or x.address in concept.pseudonyms]
|
|
75
|
+
# if equi_targets:
|
|
76
|
+
# for equi in equi_targets:
|
|
77
|
+
# concept_map[equi.address] = set()
|
|
74
78
|
concept_map[concept.address].add(input)
|
|
79
|
+
|
|
75
80
|
# second loop, include partials
|
|
76
81
|
for input in inputs:
|
|
77
82
|
for concept in input.output_concepts:
|
|
@@ -28,14 +28,18 @@ LOGGER_PREFIX = "[CONCEPT DETAIL - MERGE NODE]"
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def deduplicate_nodes(
|
|
31
|
-
merged: dict[str, QueryDatasource | Datasource],
|
|
31
|
+
merged: dict[str, QueryDatasource | Datasource],
|
|
32
|
+
logging_prefix: str,
|
|
33
|
+
environment: Environment,
|
|
32
34
|
) -> tuple[bool, dict[str, QueryDatasource | Datasource], set[str]]:
|
|
33
35
|
duplicates = False
|
|
34
36
|
removed: set[str] = set()
|
|
35
37
|
set_map: dict[str, set[str]] = {}
|
|
36
38
|
for k, v in merged.items():
|
|
37
39
|
unique_outputs = [
|
|
38
|
-
x.address
|
|
40
|
+
environment.concepts[x.address].address
|
|
41
|
+
for x in v.output_concepts
|
|
42
|
+
if x not in v.partial_concepts
|
|
39
43
|
]
|
|
40
44
|
set_map[k] = set(unique_outputs)
|
|
41
45
|
for k1, v1 in set_map.items():
|
|
@@ -71,12 +75,15 @@ def deduplicate_nodes_and_joins(
|
|
|
71
75
|
joins: List[NodeJoin] | None,
|
|
72
76
|
merged: dict[str, QueryDatasource | Datasource],
|
|
73
77
|
logging_prefix: str,
|
|
78
|
+
environment: Environment,
|
|
74
79
|
) -> Tuple[List[NodeJoin] | None, dict[str, QueryDatasource | Datasource]]:
|
|
75
80
|
# it's possible that we have more sources than we need
|
|
76
81
|
duplicates = True
|
|
77
82
|
while duplicates:
|
|
78
83
|
duplicates = False
|
|
79
|
-
duplicates, merged, removed = deduplicate_nodes(
|
|
84
|
+
duplicates, merged, removed = deduplicate_nodes(
|
|
85
|
+
merged, logging_prefix, environment=environment
|
|
86
|
+
)
|
|
80
87
|
# filter out any removed joins
|
|
81
88
|
if joins is not None:
|
|
82
89
|
joins = [
|
|
@@ -245,7 +252,7 @@ class MergeNode(StrategyNode):
|
|
|
245
252
|
|
|
246
253
|
# it's possible that we have more sources than we need
|
|
247
254
|
final_joins, merged = deduplicate_nodes_and_joins(
|
|
248
|
-
final_joins, merged, self.logging_prefix
|
|
255
|
+
final_joins, merged, self.logging_prefix, self.environment
|
|
249
256
|
)
|
|
250
257
|
# early exit if we can just return the parent
|
|
251
258
|
final_datasets: List[QueryDatasource | Datasource] = list(merged.values())
|
|
@@ -6,6 +6,7 @@ from trilogy.core.models import (
|
|
|
6
6
|
SourceType,
|
|
7
7
|
Concept,
|
|
8
8
|
UnnestJoin,
|
|
9
|
+
Function,
|
|
9
10
|
)
|
|
10
11
|
from trilogy.core.processing.nodes.base_node import StrategyNode
|
|
11
12
|
|
|
@@ -19,7 +20,7 @@ class UnnestNode(StrategyNode):
|
|
|
19
20
|
|
|
20
21
|
def __init__(
|
|
21
22
|
self,
|
|
22
|
-
|
|
23
|
+
unnest_concepts: List[Concept],
|
|
23
24
|
input_concepts: List[Concept],
|
|
24
25
|
output_concepts: List[Concept],
|
|
25
26
|
environment,
|
|
@@ -37,25 +38,28 @@ class UnnestNode(StrategyNode):
|
|
|
37
38
|
parents=parents,
|
|
38
39
|
depth=depth,
|
|
39
40
|
)
|
|
40
|
-
self.
|
|
41
|
+
self.unnest_concepts = unnest_concepts
|
|
41
42
|
|
|
42
43
|
def _resolve(self) -> QueryDatasource:
|
|
43
44
|
"""We need to ensure that any filtered values are removed from the output to avoid inappropriate references"""
|
|
44
45
|
base = super()._resolve()
|
|
45
|
-
|
|
46
|
+
lineage = self.unnest_concepts[0].lineage
|
|
47
|
+
assert isinstance(lineage, Function)
|
|
48
|
+
final = "_".join(set([c.address for c in self.unnest_concepts]))
|
|
46
49
|
unnest = UnnestJoin(
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
concepts=self.unnest_concepts,
|
|
51
|
+
parent=lineage,
|
|
52
|
+
alias=f'unnest_{final.replace(".", "_")}',
|
|
49
53
|
)
|
|
50
54
|
base.joins.append(unnest)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
for unnest_concept in self.unnest_concepts:
|
|
56
|
+
base.source_map[unnest_concept.address] = {unnest}
|
|
57
|
+
base.join_derived_concepts = [unnest_concept]
|
|
54
58
|
return base
|
|
55
59
|
|
|
56
60
|
def copy(self) -> "UnnestNode":
|
|
57
61
|
return UnnestNode(
|
|
58
|
-
|
|
62
|
+
unnest_concepts=self.unnest_concepts,
|
|
59
63
|
input_concepts=list(self.input_concepts),
|
|
60
64
|
output_concepts=list(self.output_concepts),
|
|
61
65
|
environment=self.environment,
|
|
@@ -285,7 +285,9 @@ def get_node_joins(
|
|
|
285
285
|
raise SyntaxError(
|
|
286
286
|
f"Could not find {joinc.address} in {right_datasource.identifier} output {[c.address for c in right_datasource.output_concepts]}"
|
|
287
287
|
)
|
|
288
|
-
|
|
288
|
+
narg = (left_arg, right_arg)
|
|
289
|
+
if narg not in join_tuples:
|
|
290
|
+
join_tuples.append((left_arg, right_arg))
|
|
289
291
|
final_joins_pre.append(
|
|
290
292
|
BaseJoin(
|
|
291
293
|
left_datasource=identifier_map[left],
|
trilogy/core/query_processor.py
CHANGED
|
@@ -46,7 +46,10 @@ def base_join_to_join(
|
|
|
46
46
|
"""This function converts joins at the datasource level
|
|
47
47
|
to joins at the CTE level"""
|
|
48
48
|
if isinstance(base_join, UnnestJoin):
|
|
49
|
-
return InstantiatedUnnestJoin(
|
|
49
|
+
return InstantiatedUnnestJoin(
|
|
50
|
+
concept_to_unnest=base_join.parent.concept_arguments[0],
|
|
51
|
+
alias=base_join.alias,
|
|
52
|
+
)
|
|
50
53
|
if base_join.left_datasource.identifier == base_join.right_datasource.identifier:
|
|
51
54
|
raise ValueError(f"Joining on same datasource {base_join}")
|
|
52
55
|
left_ctes = [
|
|
@@ -306,7 +309,11 @@ def datasource_to_ctes(
|
|
|
306
309
|
if cte.grain != query_datasource.grain:
|
|
307
310
|
raise ValueError("Grain was corrupted in CTE generation")
|
|
308
311
|
for x in cte.output_columns:
|
|
309
|
-
if
|
|
312
|
+
if (
|
|
313
|
+
x.address not in cte.source_map
|
|
314
|
+
and not any(y in cte.source_map for y in x.pseudonyms)
|
|
315
|
+
and CONFIG.validate_missing
|
|
316
|
+
):
|
|
310
317
|
raise ValueError(
|
|
311
318
|
f"Missing {x.address} in {cte.source_map}, source map {cte.source.source_map.keys()} "
|
|
312
319
|
)
|
trilogy/dialect/base.py
CHANGED
|
@@ -9,7 +9,6 @@ from trilogy.core.enums import (
|
|
|
9
9
|
FunctionType,
|
|
10
10
|
WindowType,
|
|
11
11
|
DatePart,
|
|
12
|
-
ComparisonOperator,
|
|
13
12
|
)
|
|
14
13
|
from trilogy.core.models import (
|
|
15
14
|
ListType,
|
|
@@ -227,13 +226,7 @@ def safe_get_cte_value(coalesce, cte: CTE, c: Concept, quote_char: str):
|
|
|
227
226
|
raw = cte.source_map.get(address, None)
|
|
228
227
|
|
|
229
228
|
if not raw:
|
|
230
|
-
|
|
231
|
-
if cte.source_map.get(k):
|
|
232
|
-
c = v
|
|
233
|
-
raw = cte.source_map[k]
|
|
234
|
-
break
|
|
235
|
-
if not raw:
|
|
236
|
-
return INVALID_REFERENCE_STRING("Missing source reference")
|
|
229
|
+
return None
|
|
237
230
|
if isinstance(raw, str):
|
|
238
231
|
rendered = cte.get_alias(c, raw)
|
|
239
232
|
return f"{raw}.{safe_quote(rendered, quote_char)}"
|
|
@@ -260,35 +253,64 @@ class BaseDialect:
|
|
|
260
253
|
|
|
261
254
|
return f"{self.render_concept_sql(order_item.expr, cte=cte, alias=False)} {order_item.order.value}"
|
|
262
255
|
|
|
263
|
-
def render_concept_sql(
|
|
256
|
+
def render_concept_sql(
|
|
257
|
+
self, c: Concept, cte: CTE, alias: bool = True, raise_invalid: bool = False
|
|
258
|
+
) -> str:
|
|
259
|
+
result = None
|
|
260
|
+
if c.pseudonyms:
|
|
261
|
+
for candidate in [c] + list(c.pseudonyms.values()):
|
|
262
|
+
try:
|
|
263
|
+
logger.debug(
|
|
264
|
+
f"{LOGGER_PREFIX} [{c.address}] Attempting rendering w/ candidate {candidate.address}"
|
|
265
|
+
)
|
|
266
|
+
result = self._render_concept_sql(
|
|
267
|
+
candidate, cte, raise_invalid=True
|
|
268
|
+
)
|
|
269
|
+
if result:
|
|
270
|
+
break
|
|
271
|
+
except ValueError:
|
|
272
|
+
continue
|
|
273
|
+
if not result:
|
|
274
|
+
result = self._render_concept_sql(c, cte, raise_invalid=raise_invalid)
|
|
275
|
+
if alias:
|
|
276
|
+
return f"{result} as {self.QUOTE_CHARACTER}{c.safe_address}{self.QUOTE_CHARACTER}"
|
|
277
|
+
return result
|
|
278
|
+
|
|
279
|
+
def _render_concept_sql(
|
|
280
|
+
self, c: Concept, cte: CTE, raise_invalid: bool = False
|
|
281
|
+
) -> str:
|
|
264
282
|
# only recurse while it's in sources of the current cte
|
|
265
283
|
logger.debug(
|
|
266
284
|
f"{LOGGER_PREFIX} [{c.address}] Starting rendering loop on cte: {cte.name}"
|
|
267
285
|
)
|
|
268
286
|
|
|
287
|
+
# check if it's not inherited AND no pseudonyms are inherited
|
|
269
288
|
if c.lineage and cte.source_map.get(c.address, []) == []:
|
|
270
289
|
logger.debug(
|
|
271
|
-
f"{LOGGER_PREFIX} [{c.address}] rendering concept with lineage that is not already existing"
|
|
290
|
+
f"{LOGGER_PREFIX} [{c.address}] rendering concept with lineage that is not already existing, have {cte.source_map}"
|
|
272
291
|
)
|
|
273
292
|
if isinstance(c.lineage, WindowItem):
|
|
274
293
|
rendered_order_components = [
|
|
275
|
-
f"{self.render_concept_sql(x.expr, cte, alias=False)} {x.order.value}"
|
|
294
|
+
f"{self.render_concept_sql(x.expr, cte, alias=False, raise_invalid=raise_invalid)} {x.order.value}"
|
|
276
295
|
for x in c.lineage.order_by
|
|
277
296
|
]
|
|
278
297
|
rendered_over_components = [
|
|
279
|
-
self.render_concept_sql(
|
|
298
|
+
self.render_concept_sql(
|
|
299
|
+
x, cte, alias=False, raise_invalid=raise_invalid
|
|
300
|
+
)
|
|
301
|
+
for x in c.lineage.over
|
|
280
302
|
]
|
|
281
|
-
rval = f"{self.WINDOW_FUNCTION_MAP[c.lineage.type](concept = self.render_concept_sql(c.lineage.content, cte=cte, alias=False), window=','.join(rendered_over_components), sort=','.join(rendered_order_components))}" # noqa: E501
|
|
303
|
+
rval = f"{self.WINDOW_FUNCTION_MAP[c.lineage.type](concept = self.render_concept_sql(c.lineage.content, cte=cte, alias=False, raise_invalid=raise_invalid), window=','.join(rendered_over_components), sort=','.join(rendered_order_components))}" # noqa: E501
|
|
282
304
|
elif isinstance(c.lineage, FilterItem):
|
|
283
305
|
# for cases when we've optimized this
|
|
284
306
|
if cte.condition == c.lineage.where.conditional:
|
|
285
307
|
rval = self.render_expr(c.lineage.content, cte=cte)
|
|
286
308
|
else:
|
|
287
|
-
rval = f"CASE WHEN {self.render_expr(c.lineage.where.conditional, cte=cte)} THEN {self.render_concept_sql(c.lineage.content, cte=cte, alias=False)} ELSE NULL END"
|
|
309
|
+
rval = f"CASE WHEN {self.render_expr(c.lineage.where.conditional, cte=cte)} THEN {self.render_concept_sql(c.lineage.content, cte=cte, alias=False, raise_invalid=raise_invalid)} ELSE NULL END"
|
|
288
310
|
elif isinstance(c.lineage, RowsetItem):
|
|
289
|
-
rval = f"{self.render_concept_sql(c.lineage.content, cte=cte, alias=False)}"
|
|
311
|
+
rval = f"{self.render_concept_sql(c.lineage.content, cte=cte, alias=False, raise_invalid=raise_invalid)}"
|
|
290
312
|
elif isinstance(c.lineage, MultiSelectStatement):
|
|
291
|
-
rval = f"{self.render_concept_sql(c.lineage.find_source(c, cte), cte=cte, alias=False)}"
|
|
313
|
+
rval = f"{self.render_concept_sql(c.lineage.find_source(c, cte), cte=cte, alias=False, raise_invalid=raise_invalid)}"
|
|
292
314
|
elif isinstance(c.lineage, AggregateWrapper):
|
|
293
315
|
args = [
|
|
294
316
|
self.render_expr(v, cte) # , alias=False)
|
|
@@ -304,16 +326,15 @@ class BaseDialect:
|
|
|
304
326
|
rval = f"{self.FUNCTION_GRAIN_MATCH_MAP[c.lineage.function.operator](args)}"
|
|
305
327
|
else:
|
|
306
328
|
args = [
|
|
307
|
-
self.render_expr(
|
|
329
|
+
self.render_expr(
|
|
330
|
+
v, cte=cte, raise_invalid=raise_invalid
|
|
331
|
+
) # , alias=False)
|
|
308
332
|
for v in c.lineage.arguments
|
|
309
333
|
]
|
|
310
334
|
|
|
311
335
|
if cte.group_to_grain:
|
|
312
336
|
rval = f"{self.FUNCTION_MAP[c.lineage.operator](args)}"
|
|
313
337
|
else:
|
|
314
|
-
logger.debug(
|
|
315
|
-
f"{LOGGER_PREFIX} [{c.address}] ignoring optimazable aggregate function, at grain so optimizing"
|
|
316
|
-
)
|
|
317
338
|
rval = f"{self.FUNCTION_GRAIN_MATCH_MAP[c.lineage.operator](args)}"
|
|
318
339
|
else:
|
|
319
340
|
logger.debug(
|
|
@@ -324,14 +345,24 @@ class BaseDialect:
|
|
|
324
345
|
if isinstance(raw_content, RawColumnExpr):
|
|
325
346
|
rval = raw_content.text
|
|
326
347
|
elif isinstance(raw_content, Function):
|
|
327
|
-
rval = self.render_expr(
|
|
348
|
+
rval = self.render_expr(
|
|
349
|
+
raw_content, cte=cte, raise_invalid=raise_invalid
|
|
350
|
+
)
|
|
328
351
|
else:
|
|
329
|
-
rval =
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
352
|
+
rval = safe_get_cte_value(
|
|
353
|
+
self.FUNCTION_MAP[FunctionType.COALESCE],
|
|
354
|
+
cte,
|
|
355
|
+
c,
|
|
356
|
+
self.QUOTE_CHARACTER,
|
|
357
|
+
)
|
|
358
|
+
if not rval:
|
|
359
|
+
if raise_invalid:
|
|
360
|
+
raise ValueError(
|
|
361
|
+
f"Invalid reference string found in query: {rval}, this should never occur. Please report this issue."
|
|
362
|
+
)
|
|
363
|
+
rval = INVALID_REFERENCE_STRING(
|
|
364
|
+
f"Missing source reference to {c.address}"
|
|
365
|
+
)
|
|
335
366
|
return rval
|
|
336
367
|
|
|
337
368
|
def render_expr(
|
|
@@ -367,6 +398,7 @@ class BaseDialect:
|
|
|
367
398
|
],
|
|
368
399
|
cte: Optional[CTE] = None,
|
|
369
400
|
cte_map: Optional[Dict[str, CTE]] = None,
|
|
401
|
+
raise_invalid: bool = False,
|
|
370
402
|
) -> str:
|
|
371
403
|
|
|
372
404
|
if isinstance(e, SubselectComparison):
|
|
@@ -395,9 +427,9 @@ class BaseDialect:
|
|
|
395
427
|
target = INVALID_REFERENCE_STRING(
|
|
396
428
|
f"Missing source CTE for {e.right.address}"
|
|
397
429
|
)
|
|
398
|
-
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)"
|
|
430
|
+
return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {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)"
|
|
399
431
|
elif isinstance(e.right, (ListWrapper, Parenthetical, list)):
|
|
400
|
-
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)}"
|
|
432
|
+
return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}"
|
|
401
433
|
|
|
402
434
|
elif isinstance(
|
|
403
435
|
e.right,
|
|
@@ -408,53 +440,64 @@ class BaseDialect:
|
|
|
408
440
|
float,
|
|
409
441
|
),
|
|
410
442
|
):
|
|
411
|
-
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)})"
|
|
443
|
+
return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} ({self.render_expr(e.right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)})"
|
|
412
444
|
else:
|
|
413
|
-
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)}"
|
|
445
|
+
return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}"
|
|
414
446
|
elif isinstance(e, Comparison):
|
|
415
|
-
|
|
416
|
-
right_comp = e.right
|
|
417
|
-
assert isinstance(right_comp, Conditional)
|
|
418
|
-
return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map)} {e.operator.value} {self.render_expr(right_comp.left, cte=cte, cte_map=cte_map) and self.render_expr(right_comp.right, cte=cte, cte_map=cte_map)}"
|
|
419
|
-
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)}"
|
|
447
|
+
return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}"
|
|
420
448
|
elif isinstance(e, Conditional):
|
|
421
449
|
# conditions need to be nested in parentheses
|
|
422
|
-
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)}"
|
|
450
|
+
return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}"
|
|
423
451
|
elif isinstance(e, WindowItem):
|
|
424
452
|
rendered_order_components = [
|
|
425
|
-
f"{self.render_expr(x.expr, cte, cte_map=cte_map)} {x.order.value}"
|
|
453
|
+
f"{self.render_expr(x.expr, cte, cte_map=cte_map, raise_invalid=raise_invalid)} {x.order.value}"
|
|
426
454
|
for x in e.order_by
|
|
427
455
|
]
|
|
428
456
|
rendered_over_components = [
|
|
429
|
-
self.render_expr(x, cte, cte_map=cte_map)
|
|
457
|
+
self.render_expr(x, cte, cte_map=cte_map, raise_invalid=raise_invalid)
|
|
458
|
+
for x in e.over
|
|
430
459
|
]
|
|
431
|
-
return f"{self.WINDOW_FUNCTION_MAP[e.type](concept = self.render_expr(e.content, cte=cte, cte_map=cte_map), window=','.join(rendered_over_components), sort=','.join(rendered_order_components))}" # noqa: E501
|
|
460
|
+
return f"{self.WINDOW_FUNCTION_MAP[e.type](concept = self.render_expr(e.content, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid), window=','.join(rendered_over_components), sort=','.join(rendered_order_components))}" # noqa: E501
|
|
432
461
|
elif isinstance(e, Parenthetical):
|
|
433
462
|
# conditions need to be nested in parentheses
|
|
434
463
|
if isinstance(e.content, list):
|
|
435
|
-
return f"( {','.join([self.render_expr(x, cte=cte, cte_map=cte_map) for x in e.content])} )"
|
|
436
|
-
return f"( {self.render_expr(e.content, cte=cte, cte_map=cte_map)} )"
|
|
464
|
+
return f"( {','.join([self.render_expr(x, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) for x in e.content])} )"
|
|
465
|
+
return f"( {self.render_expr(e.content, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} )"
|
|
437
466
|
elif isinstance(e, CaseWhen):
|
|
438
|
-
return f"WHEN {self.render_expr(e.comparison, cte=cte, cte_map=cte_map) } THEN {self.render_expr(e.expr, cte=cte, cte_map=cte_map) }"
|
|
467
|
+
return f"WHEN {self.render_expr(e.comparison, cte=cte, cte_map=cte_map) } THEN {self.render_expr(e.expr, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) }"
|
|
439
468
|
elif isinstance(e, CaseElse):
|
|
440
|
-
return f"ELSE {self.render_expr(e.expr, cte=cte, cte_map=cte_map) }"
|
|
469
|
+
return f"ELSE {self.render_expr(e.expr, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) }"
|
|
441
470
|
elif isinstance(e, Function):
|
|
442
471
|
|
|
443
472
|
if cte and cte.group_to_grain:
|
|
444
473
|
return self.FUNCTION_MAP[e.operator](
|
|
445
|
-
[
|
|
474
|
+
[
|
|
475
|
+
self.render_expr(
|
|
476
|
+
z, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid
|
|
477
|
+
)
|
|
478
|
+
for z in e.arguments
|
|
479
|
+
]
|
|
446
480
|
)
|
|
447
481
|
|
|
448
482
|
return self.FUNCTION_GRAIN_MATCH_MAP[e.operator](
|
|
449
|
-
[
|
|
483
|
+
[
|
|
484
|
+
self.render_expr(
|
|
485
|
+
z, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid
|
|
486
|
+
)
|
|
487
|
+
for z in e.arguments
|
|
488
|
+
]
|
|
450
489
|
)
|
|
451
490
|
elif isinstance(e, AggregateWrapper):
|
|
452
|
-
return self.render_expr(
|
|
491
|
+
return self.render_expr(
|
|
492
|
+
e.function, cte, cte_map=cte_map, raise_invalid=raise_invalid
|
|
493
|
+
)
|
|
453
494
|
elif isinstance(e, FilterItem):
|
|
454
|
-
return f"CASE WHEN {self.render_expr(e.where.conditional,cte=cte, cte_map=cte_map)} THEN {self.render_expr(e.content, cte, cte_map=cte_map)} ELSE NULL END"
|
|
495
|
+
return f"CASE WHEN {self.render_expr(e.where.conditional,cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} THEN {self.render_expr(e.content, cte, cte_map=cte_map, raise_invalid=raise_invalid)} ELSE NULL END"
|
|
455
496
|
elif isinstance(e, Concept):
|
|
456
497
|
if cte:
|
|
457
|
-
return self.render_concept_sql(
|
|
498
|
+
return self.render_concept_sql(
|
|
499
|
+
e, cte, alias=False, raise_invalid=raise_invalid
|
|
500
|
+
)
|
|
458
501
|
elif cte_map:
|
|
459
502
|
return f"{cte_map[e.address].name}.{self.QUOTE_CHARACTER}{e.safe_address}{self.QUOTE_CHARACTER}"
|
|
460
503
|
return f"{self.QUOTE_CHARACTER}{e.safe_address}{self.QUOTE_CHARACTER}"
|
|
@@ -465,11 +508,11 @@ class BaseDialect:
|
|
|
465
508
|
elif isinstance(e, (int, float)):
|
|
466
509
|
return str(e)
|
|
467
510
|
elif isinstance(e, ListWrapper):
|
|
468
|
-
return f"[{','.join([self.render_expr(x, cte=cte, cte_map=cte_map) for x in e])}]"
|
|
511
|
+
return f"[{','.join([self.render_expr(x, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) for x in e])}]"
|
|
469
512
|
elif isinstance(e, MapWrapper):
|
|
470
|
-
return f"MAP {{{','.join([f'{self.render_expr(k, cte=cte, cte_map=cte_map)}:{self.render_expr(v, cte=cte, cte_map=cte_map)}' for k, v in e.items()])}}}"
|
|
513
|
+
return f"MAP {{{','.join([f'{self.render_expr(k, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}:{self.render_expr(v, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}' for k, v in e.items()])}}}"
|
|
471
514
|
elif isinstance(e, list):
|
|
472
|
-
return f"{self.FUNCTION_MAP[FunctionType.ARRAY]([self.render_expr(x, cte=cte, cte_map=cte_map) for x in e])}"
|
|
515
|
+
return f"{self.FUNCTION_MAP[FunctionType.ARRAY]([self.render_expr(x, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) for x in e])}"
|
|
473
516
|
elif isinstance(e, DataType):
|
|
474
517
|
return str(e.value)
|
|
475
518
|
elif isinstance(e, DatePart):
|
trilogy/dialect/common.py
CHANGED
|
@@ -37,10 +37,10 @@ def render_join(
|
|
|
37
37
|
if not cte:
|
|
38
38
|
raise ValueError("must provide a cte to build an unnest joins")
|
|
39
39
|
if unnest_mode == UnnestMode.CROSS_JOIN:
|
|
40
|
-
return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.
|
|
40
|
+
return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
|
|
41
41
|
if unnest_mode == UnnestMode.CROSS_JOIN_ALIAS:
|
|
42
|
-
return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.
|
|
43
|
-
return f"FULL JOIN {render_unnest(unnest_mode, quote_character, join.
|
|
42
|
+
return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
|
|
43
|
+
return f"FULL JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
|
|
44
44
|
left_name = join.left_name
|
|
45
45
|
right_name = join.right_name
|
|
46
46
|
right_base = join.right_ref
|
trilogy/parsing/common.py
CHANGED
|
@@ -9,6 +9,10 @@ from trilogy.core.models import (
|
|
|
9
9
|
ListWrapper,
|
|
10
10
|
MapWrapper,
|
|
11
11
|
WindowItem,
|
|
12
|
+
Meta,
|
|
13
|
+
Parenthetical,
|
|
14
|
+
FunctionClass,
|
|
15
|
+
Environment,
|
|
12
16
|
)
|
|
13
17
|
from typing import List, Tuple
|
|
14
18
|
from trilogy.core.functions import (
|
|
@@ -16,8 +20,61 @@ from trilogy.core.functions import (
|
|
|
16
20
|
FunctionType,
|
|
17
21
|
arg_to_datatype,
|
|
18
22
|
)
|
|
19
|
-
from trilogy.utility import unique
|
|
23
|
+
from trilogy.utility import unique, string_to_hash
|
|
20
24
|
from trilogy.core.enums import PurposeLineage
|
|
25
|
+
from trilogy.constants import (
|
|
26
|
+
VIRTUAL_CONCEPT_PREFIX,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def process_function_args(
|
|
31
|
+
args,
|
|
32
|
+
meta: Meta | None,
|
|
33
|
+
environment: Environment,
|
|
34
|
+
):
|
|
35
|
+
final: List[Concept | Function] = []
|
|
36
|
+
for arg in args:
|
|
37
|
+
# if a function has an anonymous function argument
|
|
38
|
+
# create an implicit concept
|
|
39
|
+
while isinstance(arg, Parenthetical):
|
|
40
|
+
arg = arg.content
|
|
41
|
+
if isinstance(arg, Function):
|
|
42
|
+
# if it's not an aggregate function, we can skip the virtual concepts
|
|
43
|
+
# to simplify anonymous function handling
|
|
44
|
+
if (
|
|
45
|
+
arg.operator not in FunctionClass.AGGREGATE_FUNCTIONS.value
|
|
46
|
+
and arg.operator != FunctionType.UNNEST
|
|
47
|
+
):
|
|
48
|
+
final.append(arg)
|
|
49
|
+
continue
|
|
50
|
+
id_hash = string_to_hash(str(arg))
|
|
51
|
+
concept = function_to_concept(
|
|
52
|
+
arg,
|
|
53
|
+
name=f"{VIRTUAL_CONCEPT_PREFIX}_{id_hash}",
|
|
54
|
+
namespace=environment.namespace,
|
|
55
|
+
)
|
|
56
|
+
# to satisfy mypy, concept will always have metadata
|
|
57
|
+
if concept.metadata and meta:
|
|
58
|
+
concept.metadata.line_number = meta.line
|
|
59
|
+
environment.add_concept(concept, meta=meta)
|
|
60
|
+
final.append(concept)
|
|
61
|
+
elif isinstance(
|
|
62
|
+
arg, (FilterItem, WindowItem, AggregateWrapper, ListWrapper, MapWrapper)
|
|
63
|
+
):
|
|
64
|
+
id_hash = string_to_hash(str(arg))
|
|
65
|
+
concept = arbitrary_to_concept(
|
|
66
|
+
arg,
|
|
67
|
+
name=f"{VIRTUAL_CONCEPT_PREFIX}_{id_hash}",
|
|
68
|
+
namespace=environment.namespace,
|
|
69
|
+
)
|
|
70
|
+
if concept.metadata and meta:
|
|
71
|
+
concept.metadata.line_number = meta.line
|
|
72
|
+
environment.add_concept(concept, meta=meta)
|
|
73
|
+
final.append(concept)
|
|
74
|
+
|
|
75
|
+
else:
|
|
76
|
+
final.append(arg)
|
|
77
|
+
return final
|
|
21
78
|
|
|
22
79
|
|
|
23
80
|
def get_purpose_and_keys(
|