pytrilogy 0.0.3.102__py3-none-any.whl → 0.0.3.104__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.

Files changed (33) hide show
  1. {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/METADATA +2 -1
  2. {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/RECORD +33 -31
  3. trilogy/__init__.py +1 -1
  4. trilogy/constants.py +1 -1
  5. trilogy/core/models/execute.py +1 -6
  6. trilogy/core/optimization.py +13 -4
  7. trilogy/core/optimizations/__init__.py +2 -0
  8. trilogy/core/optimizations/hide_unused_concept.py +51 -0
  9. trilogy/core/optimizations/predicate_pushdown.py +9 -1
  10. trilogy/core/processing/concept_strategies_v3.py +35 -14
  11. trilogy/core/processing/discovery_node_factory.py +6 -6
  12. trilogy/core/processing/discovery_utility.py +163 -14
  13. trilogy/core/processing/node_generators/basic_node.py +1 -0
  14. trilogy/core/processing/node_generators/common.py +1 -0
  15. trilogy/core/processing/node_generators/filter_node.py +0 -10
  16. trilogy/core/processing/node_generators/group_node.py +36 -0
  17. trilogy/core/processing/node_generators/multiselect_node.py +1 -1
  18. trilogy/core/processing/node_generators/node_merge_node.py +2 -6
  19. trilogy/core/processing/node_generators/rowset_node.py +1 -21
  20. trilogy/core/processing/node_generators/union_node.py +1 -1
  21. trilogy/core/processing/node_generators/unnest_node.py +24 -8
  22. trilogy/core/processing/nodes/base_node.py +13 -3
  23. trilogy/core/processing/nodes/group_node.py +9 -91
  24. trilogy/core/processing/nodes/merge_node.py +9 -0
  25. trilogy/core/processing/utility.py +8 -0
  26. trilogy/dialect/base.py +20 -7
  27. trilogy/dialect/common.py +5 -0
  28. trilogy/std/color.preql +3 -0
  29. trilogy/std/display.preql +3 -3
  30. {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/WHEEL +0 -0
  31. {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/entry_points.txt +0 -0
  32. {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/licenses/LICENSE.md +0 -0
  33. {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/top_level.txt +0 -0
@@ -246,13 +246,6 @@ def gen_filter_node(
246
246
  filter_node = parent
247
247
  else:
248
248
  core_parent_nodes.append(row_parent)
249
- filters = [concept] + same_filter_optional
250
- parents_for_grain = [
251
- x.lineage.content
252
- for x in filters
253
- if isinstance(x.lineage, BuildFilterItem)
254
- and isinstance(x.lineage.content, BuildConcept)
255
- ]
256
249
  filter_node = FilterNode(
257
250
  input_concepts=unique(
258
251
  parent_row_concepts + flattened_existence,
@@ -261,9 +254,6 @@ def gen_filter_node(
261
254
  output_concepts=[concept] + same_filter_optional + parent_row_concepts,
262
255
  environment=environment,
263
256
  parents=core_parent_nodes,
264
- grain=BuildGrain.from_concepts(
265
- parents_for_grain + parent_row_concepts, environment=environment
266
- ),
267
257
  preexisting_conditions=conditions.conditional if conditions else None,
268
258
  )
269
259
 
@@ -108,6 +108,42 @@ def gen_group_node(
108
108
  logger.info(
109
109
  f"{padding(depth)}{LOGGER_PREFIX} cannot include optional agg {possible_agg.address}; it has mismatched parent grain {comp_grain } vs local parent {build_grain_parents}"
110
110
  )
111
+ elif concept.grain.abstract:
112
+ for possible_agg in local_optional:
113
+ if not isinstance(
114
+ possible_agg.lineage,
115
+ (BuildAggregateWrapper, BuildFunction),
116
+ ):
117
+
118
+ continue
119
+ logger.info(
120
+ f"{padding(depth)}{LOGGER_PREFIX} considering optional agg {possible_agg.address} for {concept.address}"
121
+ )
122
+ agg_parents = resolve_function_parent_concepts(
123
+ possible_agg,
124
+ environment=environment,
125
+ )
126
+ comp_grain = get_aggregate_grain(possible_agg, environment)
127
+ if not possible_agg.grain.abstract:
128
+ continue
129
+ if set([x.address for x in agg_parents]).issubset(
130
+ set([x.address for x in parent_concepts])
131
+ ):
132
+ output_concepts.append(possible_agg)
133
+ logger.info(
134
+ f"{padding(depth)}{LOGGER_PREFIX} found equivalent group by optional concept {possible_agg.address} for {concept.address}"
135
+ )
136
+ elif comp_grain == get_aggregate_grain(concept, environment):
137
+ extra = [x for x in agg_parents if x.address not in parent_concepts]
138
+ parent_concepts += extra
139
+ output_concepts.append(possible_agg)
140
+ logger.info(
141
+ f"{padding(depth)}{LOGGER_PREFIX} found equivalent group by optional concept {possible_agg.address} for {concept.address}"
142
+ )
143
+ else:
144
+ logger.info(
145
+ f"{padding(depth)}{LOGGER_PREFIX} cannot include optional agg {possible_agg.address}; it has mismatched parent grain {comp_grain } vs local parent {get_aggregate_grain(concept, environment)}"
146
+ )
111
147
  if parent_concepts:
112
148
  target_grain = BuildGrain.from_concepts(parent_concepts)
113
149
  logger.info(
@@ -156,7 +156,7 @@ def gen_multiselect_node(
156
156
  possible_joins = concept_to_relevant_joins(additional_relevant)
157
157
  if not local_optional:
158
158
  logger.info(
159
- f"{padding(depth)}{LOGGER_PREFIX} no enriched required for rowset node; exiting early"
159
+ f"{padding(depth)}{LOGGER_PREFIX} no enrichment required for rowset node; exiting early"
160
160
  )
161
161
  return node
162
162
  if not possible_joins:
@@ -20,9 +20,6 @@ from trilogy.core.models.build import (
20
20
  BuildWhereClause,
21
21
  )
22
22
  from trilogy.core.models.build_environment import BuildEnvironment
23
- from trilogy.core.processing.discovery_utility import (
24
- group_if_required,
25
- )
26
23
  from trilogy.core.processing.nodes import History, MergeNode, StrategyNode
27
24
  from trilogy.core.processing.utility import padding
28
25
  from trilogy.utility import unique
@@ -510,7 +507,7 @@ def subgraphs_to_merge_node(
510
507
  search_conditions: BuildWhereClause | None = None,
511
508
  enable_early_exit: bool = True,
512
509
  ):
513
- target_grain = BuildGrain.from_concepts(output_concepts, environment=environment)
510
+
514
511
  parents: List[StrategyNode] = []
515
512
  logger.info(
516
513
  f"{padding(depth)}{LOGGER_PREFIX} fetching subgraphs {[[c.address for c in subgraph] for subgraph in concept_subgraphs]}"
@@ -555,7 +552,7 @@ def subgraphs_to_merge_node(
555
552
  f"{padding(depth)}{LOGGER_PREFIX} only one parent node, exiting early w/ {[c.address for c in parents[0].output_concepts]}"
556
553
  )
557
554
  parent = parents[0]
558
- return group_if_required(parent, output_concepts, environment)
555
+ return parent
559
556
 
560
557
  rval = MergeNode(
561
558
  input_concepts=unique(input_c, "address"),
@@ -563,7 +560,6 @@ def subgraphs_to_merge_node(
563
560
  environment=environment,
564
561
  parents=parents,
565
562
  depth=depth,
566
- grain=target_grain,
567
563
  # hidden_concepts=[]
568
564
  # conditions=conditions,
569
565
  # conditions=search_conditions.conditional,
@@ -64,14 +64,6 @@ def gen_rowset_node(
64
64
  v for v in concept_pool if v.address in rowset_outputs
65
65
  ]
66
66
 
67
- select_hidden = node.hidden_concepts
68
- rowset_hidden = [
69
- x
70
- for x in rowset_relevant
71
- if x.address in lineage.rowset.derived_concepts
72
- and isinstance(x.lineage, BuildRowsetItem)
73
- and x.lineage.content.address in select_hidden
74
- ]
75
67
  additional_relevant = [
76
68
  factory.build(x) for x in select.output_components if x.address in enrichment
77
69
  ]
@@ -84,18 +76,6 @@ def gen_rowset_node(
84
76
  )
85
77
  node.partial_concepts.append(item)
86
78
 
87
- final_hidden = rowset_hidden + [
88
- x
89
- for x in node.output_concepts
90
- if x.address not in local_optional + [concept]
91
- and x.derivation != Derivation.ROWSET
92
- and not any(z in lineage.rowset.derived_concepts for z in x.pseudonyms)
93
- ]
94
- logger.info(
95
- f"{padding(depth)}{LOGGER_PREFIX} hiding {final_hidden} local optional {local_optional}"
96
- )
97
- node.hide_output_concepts(final_hidden)
98
-
99
79
  node.grain = BuildGrain.from_concepts(
100
80
  [
101
81
  x
@@ -112,7 +92,7 @@ def gen_rowset_node(
112
92
 
113
93
  node.rebuild_cache()
114
94
  logger.info(
115
- f"{padding(depth)}{LOGGER_PREFIX} final output is {[x.address for x in node.output_concepts]}"
95
+ f"{padding(depth)}{LOGGER_PREFIX} final output is {[x.address for x in node.output_concepts]} with grain {node.grain}"
116
96
  )
117
97
  if not local_optional or all(
118
98
  (
@@ -74,7 +74,7 @@ def gen_union_node(
74
74
  history=history,
75
75
  conditions=conditions,
76
76
  )
77
- parent.hide_output_concepts(parent.output_concepts)
77
+
78
78
  parent.add_output_concepts(resolved)
79
79
  parent_nodes.append(parent)
80
80
  if not parent:
@@ -1,9 +1,19 @@
1
1
  from typing import List
2
2
 
3
3
  from trilogy.constants import logger
4
- from trilogy.core.models.build import BuildConcept, BuildFunction, BuildWhereClause
4
+ from trilogy.core.models.build import (
5
+ BuildConcept,
6
+ BuildFunction,
7
+ BuildGrain,
8
+ BuildWhereClause,
9
+ )
5
10
  from trilogy.core.models.build_environment import BuildEnvironment
6
- from trilogy.core.processing.nodes import History, StrategyNode, UnnestNode
11
+ from trilogy.core.processing.nodes import (
12
+ History,
13
+ StrategyNode,
14
+ UnnestNode,
15
+ WhereSafetyNode,
16
+ )
7
17
  from trilogy.core.processing.utility import padding
8
18
 
9
19
  LOGGER_PREFIX = "[GEN_UNNEST_NODE]"
@@ -71,7 +81,9 @@ def gen_unnest_node(
71
81
  return None
72
82
  else:
73
83
  parent = None
74
-
84
+ logger.info(
85
+ f"{depth_prefix}{LOGGER_PREFIX} unnest node for {concept} got parent {parent}"
86
+ )
75
87
  base = UnnestNode(
76
88
  unnest_concepts=[concept] + equivalent_optional,
77
89
  input_concepts=arguments + non_equivalent_optional,
@@ -83,7 +95,7 @@ def gen_unnest_node(
83
95
  # as unnest operations are not valid in all situations
84
96
  # TODO: inline this node when we can detect it's safe
85
97
  conditional = conditions.conditional if conditions else None
86
- new = StrategyNode(
98
+ new = WhereSafetyNode(
87
99
  input_concepts=base.output_concepts,
88
100
  output_concepts=base.output_concepts,
89
101
  environment=environment,
@@ -92,9 +104,13 @@ def gen_unnest_node(
92
104
  preexisting_conditions=(
93
105
  conditional if conditional and local_conditions is False else None
94
106
  ),
107
+ grain=BuildGrain.from_concepts(
108
+ concepts=base.output_concepts,
109
+ environment=environment,
110
+ ),
95
111
  )
96
- qds = new.resolve()
97
- assert qds.source_map[concept.address] == {base.resolve()}
98
- for x in equivalent_optional:
99
- assert qds.source_map[x.address] == {base.resolve()}
112
+ # qds = new.resolve()
113
+ # assert qds.source_map[concept.address] == {base.resolve()}
114
+ # for x in equivalent_optional:
115
+ # assert qds.source_map[x.address] == {base.resolve()}
100
116
  return new
@@ -285,12 +285,23 @@ class StrategyNode:
285
285
  self.rebuild_cache()
286
286
  return self
287
287
 
288
- def set_output_concepts(self, concepts: List[BuildConcept], rebuild: bool = True):
288
+ def set_visible_concepts(self, concepts: List[BuildConcept]):
289
+ for x in self.output_concepts:
290
+ if x.address not in [c.address for c in concepts]:
291
+ self.hidden_concepts.add(x.address)
292
+ return self
293
+
294
+ def set_output_concepts(
295
+ self,
296
+ concepts: List[BuildConcept],
297
+ rebuild: bool = True,
298
+ change_visibility: bool = True,
299
+ ):
289
300
  # exit if no changes
290
301
  if self.output_concepts == concepts:
291
302
  return self
292
303
  self.output_concepts = concepts
293
- if self.hidden_concepts:
304
+ if self.hidden_concepts and change_visibility:
294
305
  self.hidden_concepts = set(
295
306
  x for x in self.hidden_concepts if x not in concepts
296
307
  )
@@ -489,7 +500,6 @@ class WhereSafetyNode(StrategyNode):
489
500
  parent = parent.copy()
490
501
  # avoid performance hit by not rebuilding until end
491
502
  parent.set_output_concepts(self.output_concepts, rebuild=False)
492
- parent.hide_output_concepts(self.hidden_concepts, rebuild=False)
493
503
 
494
504
  # these conditions
495
505
  if self.preexisting_conditions:
@@ -1,14 +1,12 @@
1
- from dataclasses import dataclass
2
1
  from typing import List, Optional
3
2
 
4
3
  from trilogy.constants import logger
5
- from trilogy.core.enums import Purpose, SourceType
4
+ from trilogy.core.enums import SourceType
6
5
  from trilogy.core.models.build import (
7
6
  BuildComparison,
8
7
  BuildConcept,
9
8
  BuildConditional,
10
9
  BuildDatasource,
11
- BuildGrain,
12
10
  BuildOrderBy,
13
11
  BuildParenthetical,
14
12
  )
@@ -18,19 +16,16 @@ from trilogy.core.processing.nodes.base_node import (
18
16
  StrategyNode,
19
17
  resolve_concept_map,
20
18
  )
21
- from trilogy.core.processing.utility import find_nullable_concepts, is_scalar_condition
19
+ from trilogy.core.processing.utility import (
20
+ GroupRequiredResponse,
21
+ find_nullable_concepts,
22
+ is_scalar_condition,
23
+ )
22
24
  from trilogy.utility import unique
23
25
 
24
26
  LOGGER_PREFIX = "[CONCEPT DETAIL - GROUP NODE]"
25
27
 
26
28
 
27
- @dataclass
28
- class GroupRequiredResponse:
29
- target: BuildGrain
30
- upstream: BuildGrain
31
- required: bool
32
-
33
-
34
29
  class GroupNode(StrategyNode):
35
30
  source_type = SourceType.GROUP
36
31
 
@@ -80,92 +75,15 @@ class GroupNode(StrategyNode):
80
75
  environment: BuildEnvironment,
81
76
  depth: int = 0,
82
77
  ) -> GroupRequiredResponse:
83
- padding = "\t" * depth
84
- target_grain = BuildGrain.from_concepts(
85
- downstream_concepts,
86
- environment=environment,
87
- )
88
-
89
- # the concepts of the souce grain might not exist in the output environment
90
- # so we need to construct a new
91
- concept_map: dict[str, BuildConcept] = {}
92
- comp_grain = BuildGrain()
93
- for source in parents:
94
- comp_grain += source.grain
95
- for x in source.output_concepts:
96
- concept_map[x.address] = x
97
- lookups: list[BuildConcept | str] = [
98
- concept_map[x] if x in concept_map else x for x in comp_grain.components
99
- ]
100
-
101
- comp_grain = BuildGrain.from_concepts(lookups, environment=environment)
102
-
103
- # dynamically select if we need to group
104
- # because sometimes, we are already at required grain
105
- if comp_grain.issubset(target_grain):
106
-
107
- logger.info(
108
- f"{padding}{LOGGER_PREFIX} Group requirement check: {comp_grain}, {target_grain}, grain is subset of target, no group node required"
109
- )
110
- return GroupRequiredResponse(target_grain, comp_grain, False)
111
- # find out what extra is in the comp grain vs target grain
112
- difference = [
113
- environment.concepts[c] for c in (comp_grain - target_grain).components
114
- ]
115
- logger.info(
116
- f"{padding}{LOGGER_PREFIX} Group requirement check: {comp_grain}, {target_grain}, difference {[x.address for x in difference]}"
117
- )
78
+ from trilogy.core.processing.discovery_utility import check_if_group_required
118
79
 
119
- # if the difference is all unique properties whose keys are in the source grain
120
- # we can also suppress the group
121
- if all(
122
- [
123
- x.keys
124
- and all(
125
- environment.concepts[z].address in comp_grain.components
126
- for z in x.keys
127
- )
128
- for x in difference
129
- ]
130
- ):
131
- logger.info(
132
- f"{padding}{LOGGER_PREFIX} Group requirement check: skipped due to unique property validation"
133
- )
134
- return GroupRequiredResponse(target_grain, comp_grain, False)
135
- if all([x.purpose == Purpose.KEY for x in difference]):
136
- logger.info(
137
- f"{padding}{LOGGER_PREFIX} checking if downstream is unique properties of key"
138
- )
139
- replaced_grain_raw: list[set[str]] = [
140
- (
141
- x.keys or set()
142
- if x.purpose == Purpose.UNIQUE_PROPERTY
143
- else set([x.address])
144
- )
145
- for x in downstream_concepts
146
- if x.address in target_grain.components
147
- ]
148
- # flatten the list of lists
149
- replaced_grain = [
150
- item for sublist in replaced_grain_raw for item in sublist
151
- ]
152
- # if the replaced grain is a subset of the comp grain, we can skip the group
153
- unique_grain_comp = BuildGrain.from_concepts(
154
- replaced_grain, environment=environment
155
- )
156
- if comp_grain.issubset(unique_grain_comp):
157
- logger.info(
158
- f"{padding}{LOGGER_PREFIX} Group requirement check: skipped due to unique property validation"
159
- )
160
- return GroupRequiredResponse(target_grain, comp_grain, False)
161
-
162
- logger.info(f"{padding}{LOGGER_PREFIX} Group requirement check: group required")
163
- return GroupRequiredResponse(target_grain, comp_grain, True)
80
+ return check_if_group_required(downstream_concepts, parents, environment, depth)
164
81
 
165
82
  def _resolve(self) -> QueryDatasource:
166
83
  parent_sources: List[QueryDatasource | BuildDatasource] = [
167
84
  p.resolve() for p in self.parents
168
85
  ]
86
+
169
87
  grains = self.check_if_required(
170
88
  self.output_concepts, parent_sources, self.environment, self.depth
171
89
  )
@@ -341,6 +341,7 @@ class MergeNode(StrategyNode):
341
341
  full_join_concepts += join.input_concepts
342
342
 
343
343
  if self.force_group is True:
344
+
344
345
  force_group = True
345
346
  elif self.whole_grain:
346
347
  force_group = False
@@ -367,6 +368,14 @@ class MergeNode(StrategyNode):
367
368
  nullable_concepts = find_nullable_concepts(
368
369
  source_map=source_map, joins=joins, datasources=final_datasets
369
370
  )
371
+ if force_group:
372
+
373
+ grain = BuildGrain.from_concepts(
374
+ self.output_concepts, environment=self.environment
375
+ )
376
+ logger.info(
377
+ f"{self.logging_prefix}{LOGGER_PREFIX} forcing group by to achieve grain {grain}"
378
+ )
370
379
  qds = QueryDatasource(
371
380
  input_concepts=unique(self.input_concepts, "address"),
372
381
  output_concepts=unique(self.output_concepts, "address"),
@@ -27,6 +27,7 @@ from trilogy.core.models.build import (
27
27
  BuildDatasource,
28
28
  BuildFilterItem,
29
29
  BuildFunction,
30
+ BuildGrain,
30
31
  BuildParenthetical,
31
32
  BuildSubselectComparison,
32
33
  BuildWindowItem,
@@ -82,6 +83,13 @@ class JoinOrderOutput:
82
83
  return set(self.keys.keys())
83
84
 
84
85
 
86
+ @dataclass
87
+ class GroupRequiredResponse:
88
+ target: BuildGrain
89
+ upstream: BuildGrain
90
+ required: bool
91
+
92
+
85
93
  def resolve_join_order_v2(
86
94
  g: nx.Graph, partials: dict[str, list[str]], nullables: dict[str, list[str]]
87
95
  ) -> list[JoinOrderOutput]:
trilogy/dialect/base.py CHANGED
@@ -1,3 +1,4 @@
1
+ from collections import defaultdict
1
2
  from datetime import date, datetime
2
3
  from typing import Any, Callable, Dict, List, Optional, Sequence, Union
3
4
 
@@ -311,6 +312,7 @@ def safe_get_cte_value(
311
312
  c: BuildConcept,
312
313
  quote_char: str,
313
314
  render_expr: Callable,
315
+ use_map: dict[str, set[str]],
314
316
  ) -> Optional[str]:
315
317
  address = c.address
316
318
  raw = cte.source_map.get(address, None)
@@ -319,13 +321,17 @@ def safe_get_cte_value(
319
321
  return None
320
322
  if isinstance(raw, str):
321
323
  rendered = cte.get_alias(c, raw)
324
+ use_map[raw].add(c.address)
322
325
  return f"{quote_char}{raw}{quote_char}.{safe_quote(rendered, quote_char)}"
323
326
  if isinstance(raw, list) and len(raw) == 1:
324
327
  rendered = cte.get_alias(c, raw[0])
325
328
  if isinstance(rendered, FUNCTION_ITEMS):
326
329
  # if it's a function, we need to render it as a function
327
330
  return f"{render_expr(rendered, cte=cte, raise_invalid=True)}"
331
+ use_map[raw[0]].add(c.address)
328
332
  return f"{quote_char}{raw[0]}{quote_char}.{safe_quote(rendered, quote_char)}"
333
+ for x in raw:
334
+ use_map[x].add(c.address)
329
335
  return coalesce(
330
336
  sorted(
331
337
  [
@@ -350,13 +356,12 @@ class BaseDialect:
350
356
 
351
357
  def __init__(self, rendering: Rendering | None = None):
352
358
  self.rendering = rendering or CONFIG.rendering
359
+ self.used_map: dict[str, set[str]] = defaultdict(set)
353
360
 
354
361
  def render_order_item(
355
362
  self,
356
363
  order_item: BuildOrderItem,
357
364
  cte: CTE | UnionCTE,
358
- final: bool = False,
359
- alias: bool = True,
360
365
  ) -> str:
361
366
  # if final:
362
367
  # if not alias:
@@ -527,6 +532,9 @@ class BaseDialect:
527
532
  )
528
533
 
529
534
  raw_content = cte.get_alias(c)
535
+ parent = cte.source_map.get(c.address, None)
536
+ if parent:
537
+ self.used_map[parent[0]].add(c.address)
530
538
  if isinstance(raw_content, RawColumnExpr):
531
539
  rval = raw_content.text
532
540
  elif isinstance(raw_content, FUNCTION_ITEMS):
@@ -540,6 +548,7 @@ class BaseDialect:
540
548
  c,
541
549
  self.QUOTE_CHARACTER,
542
550
  self.render_expr,
551
+ self.used_map,
543
552
  )
544
553
  if not rval:
545
554
  # unions won't have a specific source mapped; just use a generic column reference
@@ -615,6 +624,7 @@ class BaseDialect:
615
624
  lookup_cte = cte
616
625
  if cte_map and not lookup_cte:
617
626
  lookup_cte = cte_map.get(e.right.address)
627
+
618
628
  assert lookup_cte, "Subselects must be rendered with a CTE in context"
619
629
  if e.right.address not in lookup_cte.existence_source_map:
620
630
  lookup = lookup_cte.source_map.get(
@@ -634,6 +644,7 @@ class BaseDialect:
634
644
  f"Missing source CTE for {e.right.address}"
635
645
  )
636
646
  assert cte, "CTE must be provided for inlined CTEs"
647
+ self.used_map[target].add(e.right.address)
637
648
  if target in cte.inlined_ctes:
638
649
  info = cte.inlined_ctes[target]
639
650
  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 {info.new_base} as {target} where {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} is not null)"
@@ -738,6 +749,7 @@ class BaseDialect:
738
749
  raise_invalid=raise_invalid,
739
750
  )
740
751
  elif cte_map:
752
+ self.used_map[cte_map[e.address].name].add(e.address)
741
753
  return f"{cte_map[e.address].name}.{self.QUOTE_CHARACTER}{e.safe_address}{self.QUOTE_CHARACTER}"
742
754
  return f"{self.QUOTE_CHARACTER}{e.safe_address}{self.QUOTE_CHARACTER}"
743
755
  elif isinstance(e, bool):
@@ -774,6 +786,8 @@ class BaseDialect:
774
786
  return str(e.value)
775
787
  elif isinstance(e, ArrayType):
776
788
  return f"{self.COMPLEX_DATATYPE_MAP[DataType.ARRAY](self.render_expr(e.value_data_type, cte=cte, cte_map=cte_map))}"
789
+ elif isinstance(e, list):
790
+ return f"{self.FUNCTION_MAP[FunctionType.ARRAY]([self.render_expr(x, cte=cte, cte_map=cte_map) for x in e])}"
777
791
  elif isinstance(e, BuildParamaterizedConceptReference):
778
792
  if self.rendering.parameters:
779
793
  if e.concept.namespace == DEFAULT_NAMESPACE:
@@ -782,6 +796,7 @@ class BaseDialect:
782
796
  elif e.concept.lineage:
783
797
  return self.render_expr(e.concept.lineage, cte=cte, cte_map=cte_map)
784
798
  return f"{self.QUOTE_CHARACTER}{e.concept.address}{self.QUOTE_CHARACTER}"
799
+
785
800
  else:
786
801
  raise ValueError(f"Unable to render type {type(e)} {e}")
787
802
 
@@ -822,10 +837,7 @@ class BaseDialect:
822
837
  )
823
838
  if cte.order_by:
824
839
 
825
- ordering = [
826
- self.render_order_item(i, cte, final=True, alias=False)
827
- for i in cte.order_by.items
828
- ]
840
+ ordering = [self.render_order_item(i, cte) for i in cte.order_by.items]
829
841
  base_statement += "\nORDER BY " + ",".join(ordering)
830
842
  return CompiledCTE(name=cte.name, statement=base_statement)
831
843
  elif isinstance(cte, RecursiveCTE):
@@ -950,7 +962,8 @@ class BaseDialect:
950
962
  self.QUOTE_CHARACTER,
951
963
  self.render_expr,
952
964
  cte,
953
- self.UNNEST_MODE,
965
+ use_map=self.used_map,
966
+ unnest_mode=self.UNNEST_MODE,
954
967
  )
955
968
  for join in final_joins
956
969
  ]
trilogy/dialect/common.py CHANGED
@@ -62,10 +62,12 @@ def render_join_concept(
62
62
  concept: BuildConcept,
63
63
  render_expr,
64
64
  inlined_ctes: set[str],
65
+ use_map: dict[str, set[str]],
65
66
  ):
66
67
  if cte.name in inlined_ctes:
67
68
  base = render_expr(concept, cte)
68
69
  return base
70
+ use_map[name].add(concept.address)
69
71
  return f"{quote_character}{name}{quote_character}.{quote_character}{concept.safe_address}{quote_character}"
70
72
 
71
73
 
@@ -85,6 +87,7 @@ def render_join(
85
87
  str,
86
88
  ],
87
89
  cte: CTE,
90
+ use_map: dict[str, set[str]],
88
91
  unnest_mode: UnnestMode = UnnestMode.CROSS_APPLY,
89
92
  ) -> str | None:
90
93
  # {% for key in join.joinkeys %}{{ key.inner }} = {{ key.outer}}{% endfor %}
@@ -121,6 +124,7 @@ def render_join(
121
124
  pair.left,
122
125
  render_expr_func,
123
126
  join.inlined_ctes,
127
+ use_map=use_map,
124
128
  ),
125
129
  render_join_concept(
126
130
  right_name,
@@ -129,6 +133,7 @@ def render_join(
129
133
  pair.right,
130
134
  render_expr_func,
131
135
  join.inlined_ctes,
136
+ use_map=use_map,
132
137
  ),
133
138
  modifiers=pair.modifiers
134
139
  + (pair.left.modifiers or [])
@@ -0,0 +1,3 @@
1
+
2
+
3
+ type hex string; # Hex color value, e.g. #FF5733
trilogy/std/display.preql CHANGED
@@ -3,7 +3,7 @@
3
3
  type percent float; # Percentage value
4
4
 
5
5
  def calc_percent(a, b, digits=-1) -> case when digits =-1 then
6
- case when b = 0 then 0.0::float::percent else
7
- (a/b)::float::percent end
6
+ case when b = 0 then 0.0::numeric::percent else
7
+ (a/b)::numeric::percent end
8
8
  else round((case when b = 0 then 0.0::float::percent else
9
- (a/b)::float::percent end):: float::percent, digits) end;
9
+ (a/b)::float::percent end):: numeric::percent, digits) end;