pytrilogy 0.0.2.11__py3-none-any.whl → 0.0.2.13__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.13.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.13.dist-info}/RECORD +31 -31
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.13.dist-info}/WHEEL +1 -1
- trilogy/__init__.py +1 -1
- trilogy/constants.py +5 -0
- trilogy/core/enums.py +3 -1
- trilogy/core/environment_helpers.py +44 -6
- trilogy/core/models.py +51 -27
- 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 +22 -9
- trilogy/core/processing/node_generators/common.py +13 -23
- 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 +18 -11
- trilogy/core/processing/nodes/group_node.py +0 -1
- trilogy/core/processing/nodes/merge_node.py +12 -5
- trilogy/core/processing/nodes/unnest_node.py +13 -9
- trilogy/core/processing/utility.py +3 -1
- trilogy/core/query_processor.py +14 -12
- trilogy/dialect/base.py +95 -52
- trilogy/dialect/common.py +3 -3
- trilogy/executor.py +8 -2
- trilogy/parsing/common.py +73 -2
- trilogy/parsing/parse_engine.py +88 -132
- trilogy/parsing/trilogy.lark +3 -3
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.13.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.13.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.13.dist-info}/top_level.txt +0 -0
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/executor.py
CHANGED
|
@@ -300,10 +300,16 @@ class Executor(object):
|
|
|
300
300
|
self.environment.add_datasource(x.datasource)
|
|
301
301
|
yield x
|
|
302
302
|
|
|
303
|
-
def execute_raw_sql(
|
|
303
|
+
def execute_raw_sql(
|
|
304
|
+
self, command: str, variables: dict | None = None
|
|
305
|
+
) -> CursorResult:
|
|
304
306
|
"""Run a command against the raw underlying
|
|
305
307
|
execution engine"""
|
|
306
|
-
|
|
308
|
+
if variables:
|
|
309
|
+
return self.connection.execute(text(command), variables)
|
|
310
|
+
return self.connection.execute(
|
|
311
|
+
text(command),
|
|
312
|
+
)
|
|
307
313
|
|
|
308
314
|
def execute_text(self, command: str) -> List[CursorResult]:
|
|
309
315
|
"""Run a preql text command"""
|
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}_{arg.operator.value}_{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(
|
|
@@ -198,20 +255,34 @@ def arbitrary_to_concept(
|
|
|
198
255
|
| str
|
|
199
256
|
),
|
|
200
257
|
namespace: str,
|
|
201
|
-
name: str,
|
|
258
|
+
name: str | None = None,
|
|
202
259
|
metadata: Metadata | None = None,
|
|
203
260
|
purpose: Purpose | None = None,
|
|
204
261
|
) -> Concept:
|
|
205
262
|
|
|
206
263
|
if isinstance(parent, AggregateWrapper):
|
|
264
|
+
if not name:
|
|
265
|
+
name = (
|
|
266
|
+
f"_agg_{parent.function.operator.value}_{string_to_hash(str(parent))}"
|
|
267
|
+
)
|
|
207
268
|
return agg_wrapper_to_concept(parent, namespace, name, metadata, purpose)
|
|
208
269
|
elif isinstance(parent, WindowItem):
|
|
270
|
+
if not name:
|
|
271
|
+
name = f"_window_{parent.type.value}_{string_to_hash(str(parent))}"
|
|
209
272
|
return window_item_to_concept(parent, name, namespace, purpose, metadata)
|
|
210
273
|
elif isinstance(parent, FilterItem):
|
|
274
|
+
if not name:
|
|
275
|
+
name = f"_filter_{parent.content.name}_{string_to_hash(str(parent))}"
|
|
211
276
|
return filter_item_to_concept(parent, name, namespace, purpose, metadata)
|
|
212
277
|
elif isinstance(parent, Function):
|
|
278
|
+
if not name:
|
|
279
|
+
name = f"_func_{parent.operator.value}_{string_to_hash(str(parent))}"
|
|
213
280
|
return function_to_concept(parent, name, namespace)
|
|
214
281
|
elif isinstance(parent, ListWrapper):
|
|
282
|
+
if not name:
|
|
283
|
+
name = f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(parent))}"
|
|
215
284
|
return constant_to_concept(parent, name, namespace, purpose, metadata)
|
|
216
285
|
else:
|
|
286
|
+
if not name:
|
|
287
|
+
name = f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(parent))}"
|
|
217
288
|
return constant_to_concept(parent, name, namespace, purpose, metadata)
|