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.

@@ -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 + local_optional,
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
- unnest_concept=concept,
42
- input_concepts=arguments + local_optional,
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
- if concept.address not in inherited:
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:
@@ -121,7 +121,6 @@ class GroupNode(StrategyNode):
121
121
  source_type=source_type,
122
122
  source_map=resolve_concept_map(
123
123
  parent_sources,
124
- # targets = self.output_concepts,
125
124
  targets=(
126
125
  unique(
127
126
  self.output_concepts + self.conditions.concept_arguments,
@@ -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], logging_prefix: str
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 for x in v.output_concepts if x not in v.partial_concepts
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(merged, logging_prefix)
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
- unnest_concept: Concept,
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.unnest_concept = unnest_concept
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
- concept=self.unnest_concept,
48
- alias=f'unnest_{self.unnest_concept.address.replace(".", "_")}',
50
+ concepts=self.unnest_concepts,
51
+ parent=lineage,
52
+ alias=f'unnest_{final.replace(".", "_")}',
49
53
  )
50
54
  base.joins.append(unnest)
51
-
52
- base.source_map[self.unnest_concept.address] = {unnest}
53
- base.join_derived_concepts = [self.unnest_concept]
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
- unnest_concept=self.unnest_concept,
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
- join_tuples.append((left_arg, right_arg))
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],
@@ -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(concept=base_join.concept, alias=base_join.alias)
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 x.address not in cte.source_map and CONFIG.validate_missing:
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
- for k, v in c.pseudonyms.items():
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(self, c: Concept, cte: CTE, alias: bool = True) -> str:
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(x, cte, alias=False) for x in c.lineage.over
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(v, cte) # , alias=False)
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(raw_content, cte=cte)
348
+ rval = self.render_expr(
349
+ raw_content, cte=cte, raise_invalid=raise_invalid
350
+ )
328
351
  else:
329
- rval = f"{safe_get_cte_value(self.FUNCTION_MAP[FunctionType.COALESCE], cte, c, self.QUOTE_CHARACTER)}"
330
- if alias:
331
- return (
332
- f"{rval} as"
333
- f" {self.QUOTE_CHARACTER}{c.safe_address}{self.QUOTE_CHARACTER}"
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
- if e.operator == ComparisonOperator.BETWEEN:
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) for x in e.over
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
- [self.render_expr(z, cte=cte, cte_map=cte_map) for z in e.arguments]
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
- [self.render_expr(z, cte=cte, cte_map=cte_map) for z in e.arguments]
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(e.function, cte, cte_map=cte_map)
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(e, cte, alias=False)
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.concept, render_func, cte)}"
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.concept, render_func, cte)}"
43
- return f"FULL JOIN {render_unnest(unnest_mode, quote_character, join.concept, render_func, cte)}"
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(