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.
- {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/METADATA +2 -1
- {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/RECORD +33 -31
- trilogy/__init__.py +1 -1
- trilogy/constants.py +1 -1
- trilogy/core/models/execute.py +1 -6
- trilogy/core/optimization.py +13 -4
- trilogy/core/optimizations/__init__.py +2 -0
- trilogy/core/optimizations/hide_unused_concept.py +51 -0
- trilogy/core/optimizations/predicate_pushdown.py +9 -1
- trilogy/core/processing/concept_strategies_v3.py +35 -14
- trilogy/core/processing/discovery_node_factory.py +6 -6
- trilogy/core/processing/discovery_utility.py +163 -14
- trilogy/core/processing/node_generators/basic_node.py +1 -0
- trilogy/core/processing/node_generators/common.py +1 -0
- trilogy/core/processing/node_generators/filter_node.py +0 -10
- trilogy/core/processing/node_generators/group_node.py +36 -0
- trilogy/core/processing/node_generators/multiselect_node.py +1 -1
- trilogy/core/processing/node_generators/node_merge_node.py +2 -6
- trilogy/core/processing/node_generators/rowset_node.py +1 -21
- trilogy/core/processing/node_generators/union_node.py +1 -1
- trilogy/core/processing/node_generators/unnest_node.py +24 -8
- trilogy/core/processing/nodes/base_node.py +13 -3
- trilogy/core/processing/nodes/group_node.py +9 -91
- trilogy/core/processing/nodes/merge_node.py +9 -0
- trilogy/core/processing/utility.py +8 -0
- trilogy/dialect/base.py +20 -7
- trilogy/dialect/common.py +5 -0
- trilogy/std/color.preql +3 -0
- trilogy/std/display.preql +3 -3
- {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.3.102.dist-info → pytrilogy-0.0.3.104.dist-info}/licenses/LICENSE.md +0 -0
- {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
|
|
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
|
-
|
|
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
|
|
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
|
(
|
|
@@ -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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 [])
|
trilogy/std/color.preql
ADDED
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::
|
|
7
|
-
(a/b)::
|
|
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)::
|
|
9
|
+
(a/b)::float::percent end):: numeric::percent, digits) end;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|