pytrilogy 0.0.3.55__py3-none-any.whl → 0.0.3.57__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.55.dist-info → pytrilogy-0.0.3.57.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.3.55.dist-info → pytrilogy-0.0.3.57.dist-info}/RECORD +39 -34
- {pytrilogy-0.0.3.55.dist-info → pytrilogy-0.0.3.57.dist-info}/WHEEL +1 -1
- trilogy/__init__.py +1 -1
- trilogy/authoring/__init__.py +12 -1
- trilogy/core/enums.py +1 -0
- trilogy/core/models/author.py +6 -4
- trilogy/core/models/execute.py +4 -1
- trilogy/core/optimization.py +4 -4
- trilogy/core/processing/concept_strategies_v3.py +324 -895
- trilogy/core/processing/discovery_loop.py +0 -0
- trilogy/core/processing/discovery_node_factory.py +475 -0
- trilogy/core/processing/discovery_utility.py +123 -0
- trilogy/core/processing/discovery_validation.py +155 -0
- trilogy/core/processing/node_generators/basic_node.py +29 -11
- trilogy/core/processing/node_generators/node_merge_node.py +1 -1
- trilogy/core/processing/node_generators/select_node.py +6 -8
- trilogy/core/processing/node_generators/synonym_node.py +2 -1
- trilogy/core/processing/node_generators/unnest_node.py +7 -1
- trilogy/core/processing/nodes/__init__.py +2 -4
- trilogy/core/processing/nodes/base_node.py +0 -13
- trilogy/core/processing/nodes/group_node.py +1 -1
- trilogy/core/processing/utility.py +38 -11
- trilogy/core/query_processor.py +3 -3
- trilogy/core/statements/author.py +6 -2
- trilogy/core/statements/execute.py +3 -2
- trilogy/dialect/base.py +3 -30
- trilogy/dialect/snowflake.py +1 -1
- trilogy/executor.py +13 -4
- trilogy/parsing/common.py +1 -3
- trilogy/parsing/parse_engine.py +14 -2
- trilogy/parsing/trilogy.lark +1 -1
- trilogy/std/date.preql +3 -1
- trilogy/std/geography.preql +4 -0
- trilogy/std/money.preql +65 -4
- trilogy/std/net.preql +8 -0
- {pytrilogy-0.0.3.55.dist-info → pytrilogy-0.0.3.57.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.3.55.dist-info → pytrilogy-0.0.3.57.dist-info}/licenses/LICENSE.md +0 -0
- {pytrilogy-0.0.3.55.dist-info → pytrilogy-0.0.3.57.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from trilogy.core.models.build import (
|
|
6
|
+
BuildConcept,
|
|
7
|
+
BuildWhereClause,
|
|
8
|
+
)
|
|
9
|
+
from trilogy.core.models.build_environment import BuildEnvironment
|
|
10
|
+
from trilogy.core.processing.nodes import (
|
|
11
|
+
StrategyNode,
|
|
12
|
+
)
|
|
13
|
+
from trilogy.core.processing.utility import (
|
|
14
|
+
get_disconnected_components,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ValidationResult(Enum):
|
|
19
|
+
COMPLETE = 1
|
|
20
|
+
DISCONNECTED = 2
|
|
21
|
+
INCOMPLETE = 3
|
|
22
|
+
INCOMPLETE_CONDITION = 4
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def validate_concept(
|
|
26
|
+
concept: BuildConcept,
|
|
27
|
+
node: StrategyNode,
|
|
28
|
+
found_addresses: set[str],
|
|
29
|
+
non_partial_addresses: set[str],
|
|
30
|
+
partial_addresses: set[str],
|
|
31
|
+
virtual_addresses: set[str],
|
|
32
|
+
found_map: dict[str, set[BuildConcept]],
|
|
33
|
+
accept_partial: bool,
|
|
34
|
+
seen: set[str],
|
|
35
|
+
environment: BuildEnvironment,
|
|
36
|
+
):
|
|
37
|
+
found_map[str(node)].add(concept)
|
|
38
|
+
seen.add(concept.address)
|
|
39
|
+
if concept not in node.partial_concepts:
|
|
40
|
+
found_addresses.add(concept.address)
|
|
41
|
+
non_partial_addresses.add(concept.address)
|
|
42
|
+
# remove it from our partial tracking
|
|
43
|
+
if concept.address in partial_addresses:
|
|
44
|
+
partial_addresses.remove(concept.address)
|
|
45
|
+
if concept.address in virtual_addresses:
|
|
46
|
+
virtual_addresses.remove(concept.address)
|
|
47
|
+
if concept in node.partial_concepts:
|
|
48
|
+
if concept.address in non_partial_addresses:
|
|
49
|
+
return None
|
|
50
|
+
partial_addresses.add(concept.address)
|
|
51
|
+
if accept_partial:
|
|
52
|
+
found_addresses.add(concept.address)
|
|
53
|
+
found_map[str(node)].add(concept)
|
|
54
|
+
for v_address in concept.pseudonyms:
|
|
55
|
+
if v_address in seen:
|
|
56
|
+
return
|
|
57
|
+
v = environment.concepts[v_address]
|
|
58
|
+
if v.address in seen:
|
|
59
|
+
return
|
|
60
|
+
if v.address == concept.address:
|
|
61
|
+
return
|
|
62
|
+
validate_concept(
|
|
63
|
+
v,
|
|
64
|
+
node,
|
|
65
|
+
found_addresses,
|
|
66
|
+
non_partial_addresses,
|
|
67
|
+
partial_addresses,
|
|
68
|
+
virtual_addresses,
|
|
69
|
+
found_map,
|
|
70
|
+
accept_partial,
|
|
71
|
+
seen=seen,
|
|
72
|
+
environment=environment,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def validate_stack(
|
|
77
|
+
environment: BuildEnvironment,
|
|
78
|
+
stack: List[StrategyNode],
|
|
79
|
+
concepts: List[BuildConcept],
|
|
80
|
+
mandatory_with_filter: List[BuildConcept],
|
|
81
|
+
conditions: BuildWhereClause | None = None,
|
|
82
|
+
accept_partial: bool = False,
|
|
83
|
+
) -> tuple[ValidationResult, set[str], set[str], set[str], set[str]]:
|
|
84
|
+
found_map: dict[str, set[BuildConcept]] = defaultdict(set)
|
|
85
|
+
found_addresses: set[str] = set()
|
|
86
|
+
non_partial_addresses: set[str] = set()
|
|
87
|
+
partial_addresses: set[str] = set()
|
|
88
|
+
virtual_addresses: set[str] = set()
|
|
89
|
+
seen: set[str] = set()
|
|
90
|
+
|
|
91
|
+
for node in stack:
|
|
92
|
+
resolved = node.resolve()
|
|
93
|
+
|
|
94
|
+
for concept in resolved.output_concepts:
|
|
95
|
+
if concept.address in resolved.hidden_concepts:
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
validate_concept(
|
|
99
|
+
concept,
|
|
100
|
+
node,
|
|
101
|
+
found_addresses,
|
|
102
|
+
non_partial_addresses,
|
|
103
|
+
partial_addresses,
|
|
104
|
+
virtual_addresses,
|
|
105
|
+
found_map,
|
|
106
|
+
accept_partial,
|
|
107
|
+
seen,
|
|
108
|
+
environment,
|
|
109
|
+
)
|
|
110
|
+
for concept in node.virtual_output_concepts:
|
|
111
|
+
if concept.address in non_partial_addresses:
|
|
112
|
+
continue
|
|
113
|
+
found_addresses.add(concept.address)
|
|
114
|
+
virtual_addresses.add(concept.address)
|
|
115
|
+
if not conditions:
|
|
116
|
+
conditions_met = True
|
|
117
|
+
else:
|
|
118
|
+
conditions_met = all(
|
|
119
|
+
[node.preexisting_conditions == conditions.conditional for node in stack]
|
|
120
|
+
) or all([c.address in found_addresses for c in mandatory_with_filter])
|
|
121
|
+
# zip in those we know we found
|
|
122
|
+
if not all([c.address in found_addresses for c in concepts]) or not conditions_met:
|
|
123
|
+
if not all([c.address in found_addresses for c in concepts]):
|
|
124
|
+
return (
|
|
125
|
+
ValidationResult.INCOMPLETE,
|
|
126
|
+
found_addresses,
|
|
127
|
+
{c.address for c in concepts if c.address not in found_addresses},
|
|
128
|
+
partial_addresses,
|
|
129
|
+
virtual_addresses,
|
|
130
|
+
)
|
|
131
|
+
return (
|
|
132
|
+
ValidationResult.INCOMPLETE_CONDITION,
|
|
133
|
+
found_addresses,
|
|
134
|
+
{c.address for c in concepts if c.address not in mandatory_with_filter},
|
|
135
|
+
partial_addresses,
|
|
136
|
+
virtual_addresses,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
graph_count, _ = get_disconnected_components(found_map)
|
|
140
|
+
if graph_count in (0, 1):
|
|
141
|
+
return (
|
|
142
|
+
ValidationResult.COMPLETE,
|
|
143
|
+
found_addresses,
|
|
144
|
+
set(),
|
|
145
|
+
partial_addresses,
|
|
146
|
+
virtual_addresses,
|
|
147
|
+
)
|
|
148
|
+
# if we have too many subgraphs, we need to keep searching
|
|
149
|
+
return (
|
|
150
|
+
ValidationResult.DISCONNECTED,
|
|
151
|
+
found_addresses,
|
|
152
|
+
set(),
|
|
153
|
+
partial_addresses,
|
|
154
|
+
virtual_addresses,
|
|
155
|
+
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import List
|
|
2
2
|
|
|
3
3
|
from trilogy.constants import logger
|
|
4
|
-
from trilogy.core.enums import FunctionClass, SourceType
|
|
4
|
+
from trilogy.core.enums import FunctionClass, FunctionType, SourceType
|
|
5
5
|
from trilogy.core.models.build import BuildConcept, BuildFunction, BuildWhereClause
|
|
6
6
|
from trilogy.core.models.build_environment import BuildEnvironment
|
|
7
7
|
from trilogy.core.processing.node_generators.common import (
|
|
@@ -47,13 +47,25 @@ def gen_basic_node(
|
|
|
47
47
|
logger.info(
|
|
48
48
|
f"{depth_prefix}{LOGGER_PREFIX} basic node for {concept} with lineage {concept.lineage} has parents {[x for x in parent_concepts]}"
|
|
49
49
|
)
|
|
50
|
-
|
|
50
|
+
synonyms: list[BuildConcept] = []
|
|
51
|
+
ignored_optional: set[str] = set()
|
|
52
|
+
assert isinstance(concept.lineage, BuildFunction)
|
|
53
|
+
if concept.lineage.operator == FunctionType.ATTR_ACCESS:
|
|
54
|
+
logger.info(
|
|
55
|
+
f"{depth_prefix}{LOGGER_PREFIX} checking for synonyms for attribute access"
|
|
56
|
+
)
|
|
57
|
+
for x in local_optional:
|
|
58
|
+
for z in x.pseudonyms:
|
|
59
|
+
s_concept = environment.alias_origin_lookup[z]
|
|
60
|
+
if is_equivalent_basic_function_lineage(concept, s_concept):
|
|
61
|
+
synonyms.append(s_concept)
|
|
62
|
+
ignored_optional.add(x.address)
|
|
51
63
|
equivalent_optional = [
|
|
52
64
|
x
|
|
53
65
|
for x in local_optional
|
|
54
66
|
if is_equivalent_basic_function_lineage(concept, x)
|
|
55
67
|
and x.address != concept.address
|
|
56
|
-
]
|
|
68
|
+
] + synonyms
|
|
57
69
|
|
|
58
70
|
if equivalent_optional:
|
|
59
71
|
logger.info(
|
|
@@ -66,6 +78,7 @@ def gen_basic_node(
|
|
|
66
78
|
for x in local_optional
|
|
67
79
|
if x not in equivalent_optional
|
|
68
80
|
and not any(x.address in y.pseudonyms for y in equivalent_optional)
|
|
81
|
+
and x.address not in ignored_optional
|
|
69
82
|
]
|
|
70
83
|
all_parents: list[BuildConcept] = unique(
|
|
71
84
|
parent_concepts + non_equivalent_optional, "address"
|
|
@@ -73,7 +86,7 @@ def gen_basic_node(
|
|
|
73
86
|
logger.info(
|
|
74
87
|
f"{depth_prefix}{LOGGER_PREFIX} Fetching parents {[x.address for x in all_parents]}"
|
|
75
88
|
)
|
|
76
|
-
parent_node: StrategyNode = source_concepts(
|
|
89
|
+
parent_node: StrategyNode | None = source_concepts(
|
|
77
90
|
mandatory_list=all_parents,
|
|
78
91
|
environment=environment,
|
|
79
92
|
g=g,
|
|
@@ -92,14 +105,19 @@ def gen_basic_node(
|
|
|
92
105
|
parent_node.add_output_concept(concept)
|
|
93
106
|
for x in equivalent_optional:
|
|
94
107
|
parent_node.add_output_concept(x)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
[
|
|
98
|
-
x
|
|
99
|
-
for x in parent_node.output_concepts
|
|
100
|
-
if x.address not in [concept] + local_optional
|
|
101
|
-
]
|
|
108
|
+
targets = [concept] + local_optional
|
|
109
|
+
logger.info(
|
|
110
|
+
f"{depth_prefix}{LOGGER_PREFIX} Returning basic select for {concept}: output {[x.address for x in parent_node.output_concepts]}"
|
|
102
111
|
)
|
|
112
|
+
should_hide = [
|
|
113
|
+
x
|
|
114
|
+
for x in parent_node.output_concepts
|
|
115
|
+
if (
|
|
116
|
+
x.address not in targets
|
|
117
|
+
and not any(x.address in y.pseudonyms for y in targets)
|
|
118
|
+
)
|
|
119
|
+
]
|
|
120
|
+
parent_node.hide_output_concepts(should_hide)
|
|
103
121
|
|
|
104
122
|
logger.info(
|
|
105
123
|
f"{depth_prefix}{LOGGER_PREFIX} Returning basic select for {concept}: output {[x.address for x in parent_node.output_concepts]}"
|
|
@@ -143,7 +143,7 @@ def determine_induced_minimal_nodes(
|
|
|
143
143
|
if not all([node in final.nodes for node in nodelist]):
|
|
144
144
|
missing = [node for node in nodelist if node not in final.nodes]
|
|
145
145
|
logger.debug(
|
|
146
|
-
f"Skipping graph for {nodelist} as missing nodes {missing} from {final.nodes}"
|
|
146
|
+
f"Skipping graph for initial list {nodelist} as missing nodes {missing} from final graph {final.nodes}"
|
|
147
147
|
)
|
|
148
148
|
return None
|
|
149
149
|
logger.debug(f"Found final graph {final.nodes}")
|
|
@@ -19,8 +19,7 @@ LOGGER_PREFIX = "[GEN_SELECT_NODE]"
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def gen_select_node(
|
|
22
|
-
|
|
23
|
-
local_optional: list[BuildConcept],
|
|
22
|
+
concepts: list[BuildConcept],
|
|
24
23
|
environment: BuildEnvironment,
|
|
25
24
|
g,
|
|
26
25
|
depth: int,
|
|
@@ -28,12 +27,11 @@ def gen_select_node(
|
|
|
28
27
|
fail_if_not_found: bool = True,
|
|
29
28
|
conditions: BuildWhereClause | None = None,
|
|
30
29
|
) -> StrategyNode | None:
|
|
31
|
-
|
|
32
|
-
all_lcl = LooseBuildConceptList(concepts=all_concepts)
|
|
30
|
+
all_lcl = LooseBuildConceptList(concepts=concepts)
|
|
33
31
|
materialized_lcl = LooseBuildConceptList(
|
|
34
32
|
concepts=[
|
|
35
33
|
x
|
|
36
|
-
for x in
|
|
34
|
+
for x in concepts
|
|
37
35
|
if x.address in environment.materialized_concepts
|
|
38
36
|
or x.derivation == Derivation.CONSTANT
|
|
39
37
|
]
|
|
@@ -41,15 +39,15 @@ def gen_select_node(
|
|
|
41
39
|
if materialized_lcl != all_lcl:
|
|
42
40
|
missing = all_lcl.difference(materialized_lcl)
|
|
43
41
|
logger.info(
|
|
44
|
-
f"{padding(depth)}{LOGGER_PREFIX} Skipping select node generation for {
|
|
42
|
+
f"{padding(depth)}{LOGGER_PREFIX} Skipping select node generation for {concepts}"
|
|
45
43
|
f" as it + optional includes non-materialized concepts (looking for all {all_lcl}, missing {missing}) "
|
|
46
44
|
)
|
|
47
45
|
if fail_if_not_found:
|
|
48
|
-
raise NoDatasourceException(f"No datasource exists for {
|
|
46
|
+
raise NoDatasourceException(f"No datasource exists for {concepts}")
|
|
49
47
|
return None
|
|
50
48
|
|
|
51
49
|
return gen_select_merge_node(
|
|
52
|
-
|
|
50
|
+
concepts,
|
|
53
51
|
g=g,
|
|
54
52
|
environment=environment,
|
|
55
53
|
depth=depth,
|
|
@@ -29,7 +29,7 @@ def gen_synonym_node(
|
|
|
29
29
|
conditions: BuildWhereClause | None = None,
|
|
30
30
|
accept_partial: bool = False,
|
|
31
31
|
) -> StrategyNode | None:
|
|
32
|
-
local_prefix = f"
|
|
32
|
+
local_prefix = f"{padding(depth)}[GEN_SYNONYM_NODE]"
|
|
33
33
|
base_fingerprint = tuple([x.address for x in all_concepts])
|
|
34
34
|
synonyms = defaultdict(list)
|
|
35
35
|
synonym_count = 0
|
|
@@ -64,5 +64,6 @@ def gen_synonym_node(
|
|
|
64
64
|
)
|
|
65
65
|
if attempt:
|
|
66
66
|
logger.info(f"{local_prefix} found inputs with {combo}")
|
|
67
|
+
print(attempt.output_concepts)
|
|
67
68
|
return attempt
|
|
68
69
|
return None
|
|
@@ -20,16 +20,22 @@ def gen_unnest_node(
|
|
|
20
20
|
conditions: BuildWhereClause | None = None,
|
|
21
21
|
) -> StrategyNode | None:
|
|
22
22
|
arguments = []
|
|
23
|
+
depth_prefix = "\t" * depth
|
|
23
24
|
if isinstance(concept.lineage, BuildFunction):
|
|
24
25
|
arguments = concept.lineage.concept_arguments
|
|
25
26
|
|
|
26
27
|
equivalent_optional = [x for x in local_optional if x.lineage == concept.lineage]
|
|
28
|
+
|
|
27
29
|
non_equivalent_optional = [
|
|
28
30
|
x for x in local_optional if x not in equivalent_optional
|
|
29
31
|
]
|
|
32
|
+
all_parents = arguments + non_equivalent_optional
|
|
33
|
+
logger.info(
|
|
34
|
+
f"{depth_prefix}{LOGGER_PREFIX} unnest node for {concept} with lineage {concept.lineage} has parents {all_parents} and equivalent optional {equivalent_optional}"
|
|
35
|
+
)
|
|
30
36
|
if arguments or local_optional:
|
|
31
37
|
parent = source_concepts(
|
|
32
|
-
mandatory_list=
|
|
38
|
+
mandatory_list=all_parents,
|
|
33
39
|
environment=environment,
|
|
34
40
|
g=g,
|
|
35
41
|
depth=depth + 1,
|
|
@@ -150,7 +150,6 @@ class History(BaseModel):
|
|
|
150
150
|
environment: BuildEnvironment,
|
|
151
151
|
g,
|
|
152
152
|
depth: int,
|
|
153
|
-
source_concepts,
|
|
154
153
|
fail_if_not_found: bool = False,
|
|
155
154
|
accept_partial: bool = False,
|
|
156
155
|
accept_partial_optional: bool = False,
|
|
@@ -169,8 +168,7 @@ class History(BaseModel):
|
|
|
169
168
|
if fingerprint in self.select_history:
|
|
170
169
|
return self.select_history[fingerprint]
|
|
171
170
|
gen = gen_select_node(
|
|
172
|
-
concept,
|
|
173
|
-
local_optional,
|
|
171
|
+
[concept] + local_optional,
|
|
174
172
|
environment,
|
|
175
173
|
g,
|
|
176
174
|
depth + 1,
|
|
@@ -190,8 +188,8 @@ __all__ = [
|
|
|
190
188
|
"WindowNode",
|
|
191
189
|
"StrategyNode",
|
|
192
190
|
"NodeJoin",
|
|
193
|
-
"ConstantNode",
|
|
194
191
|
"UnnestNode",
|
|
192
|
+
"ConstantNode",
|
|
195
193
|
"UnionNode",
|
|
196
194
|
"History",
|
|
197
195
|
"WhereSafetyNode",
|
|
@@ -311,19 +311,6 @@ class StrategyNode:
|
|
|
311
311
|
self.rebuild_cache()
|
|
312
312
|
return self
|
|
313
313
|
|
|
314
|
-
def remove_output_concepts(
|
|
315
|
-
self, concepts: List[BuildConcept], rebuild: bool = True
|
|
316
|
-
):
|
|
317
|
-
for x in concepts:
|
|
318
|
-
self.hidden_concepts.add(x.address)
|
|
319
|
-
addresses = [x.address for x in concepts]
|
|
320
|
-
self.output_concepts = [
|
|
321
|
-
x for x in self.output_concepts if x.address not in addresses
|
|
322
|
-
]
|
|
323
|
-
if rebuild:
|
|
324
|
-
self.rebuild_cache()
|
|
325
|
-
return self
|
|
326
|
-
|
|
327
314
|
@property
|
|
328
315
|
def usable_outputs(self) -> list[BuildConcept]:
|
|
329
316
|
return [
|
|
@@ -105,7 +105,7 @@ class GroupNode(StrategyNode):
|
|
|
105
105
|
if comp_grain.issubset(target_grain):
|
|
106
106
|
|
|
107
107
|
logger.info(
|
|
108
|
-
f"{padding}{LOGGER_PREFIX} Group requirement check: {comp_grain}, {target_grain}, is subset, no
|
|
108
|
+
f"{padding}{LOGGER_PREFIX} Group requirement check: {comp_grain}, {target_grain}, grain is subset of target, no group node required"
|
|
109
109
|
)
|
|
110
110
|
return GroupRequiredResponse(target_grain, comp_grain, False)
|
|
111
111
|
# find out what extra is in the comp grain vs target grain
|
|
@@ -409,7 +409,7 @@ def get_node_joins(
|
|
|
409
409
|
|
|
410
410
|
|
|
411
411
|
def get_disconnected_components(
|
|
412
|
-
concept_map: Dict[str, Set[BuildConcept]]
|
|
412
|
+
concept_map: Dict[str, Set[BuildConcept]],
|
|
413
413
|
) -> Tuple[int, List]:
|
|
414
414
|
"""Find if any of the datasources are not linked"""
|
|
415
415
|
import networkx as nx
|
|
@@ -608,8 +608,24 @@ def sort_select_output_processed(
|
|
|
608
608
|
mapping = {x.address: x for x in cte.output_columns}
|
|
609
609
|
|
|
610
610
|
new_output: list[BuildConcept] = []
|
|
611
|
-
for x in
|
|
612
|
-
|
|
611
|
+
for x in query.output_columns:
|
|
612
|
+
if x.address in mapping:
|
|
613
|
+
new_output.append(mapping[x.address])
|
|
614
|
+
for oc in cte.output_columns:
|
|
615
|
+
if x.address in oc.pseudonyms:
|
|
616
|
+
# create a wrapper BuildConcept to render the pseudonym under the original name
|
|
617
|
+
new_output.append(
|
|
618
|
+
BuildConcept(
|
|
619
|
+
name=x.name,
|
|
620
|
+
namespace=x.namespace,
|
|
621
|
+
pseudonyms={oc.address},
|
|
622
|
+
datatype=oc.datatype,
|
|
623
|
+
purpose=oc.purpose,
|
|
624
|
+
grain=oc.grain,
|
|
625
|
+
build_is_aggregate=oc.build_is_aggregate,
|
|
626
|
+
)
|
|
627
|
+
)
|
|
628
|
+
break
|
|
613
629
|
|
|
614
630
|
for oc in cte.output_columns:
|
|
615
631
|
# add hidden back
|
|
@@ -637,17 +653,28 @@ def sort_select_output(
|
|
|
637
653
|
if isinstance(query, ProcessedQuery):
|
|
638
654
|
return sort_select_output_processed(cte, query)
|
|
639
655
|
|
|
640
|
-
output_addresses = [
|
|
641
|
-
c.address
|
|
642
|
-
for c in query.output_components
|
|
643
|
-
# if c.address not in query.hidden_components
|
|
644
|
-
]
|
|
645
|
-
|
|
646
656
|
mapping = {x.address: x for x in cte.output_columns}
|
|
647
657
|
|
|
648
658
|
new_output: list[BuildConcept] = []
|
|
649
|
-
for x in
|
|
650
|
-
|
|
659
|
+
for x in query.output_components:
|
|
660
|
+
if x.address in mapping:
|
|
661
|
+
new_output.append(mapping[x.address])
|
|
662
|
+
else:
|
|
663
|
+
for oc in cte.output_columns:
|
|
664
|
+
if x.address in oc.pseudonyms:
|
|
665
|
+
# create a wrapper BuildConcept to render the pseudonym under the original name
|
|
666
|
+
new_output.append(
|
|
667
|
+
BuildConcept(
|
|
668
|
+
name=x.name,
|
|
669
|
+
namespace=x.namespace,
|
|
670
|
+
pseudonyms={oc.address},
|
|
671
|
+
datatype=oc.datatype,
|
|
672
|
+
purpose=oc.purpose,
|
|
673
|
+
grain=oc.grain,
|
|
674
|
+
build_is_aggregate=oc.build_is_aggregate,
|
|
675
|
+
)
|
|
676
|
+
)
|
|
677
|
+
break
|
|
651
678
|
cte.output_columns = new_output
|
|
652
679
|
cte.hidden_concepts = set(
|
|
653
680
|
[
|
trilogy/core/query_processor.py
CHANGED
|
@@ -432,7 +432,7 @@ def get_query_node(
|
|
|
432
432
|
)
|
|
433
433
|
ds = SelectNode(
|
|
434
434
|
output_concepts=build_statement.output_components,
|
|
435
|
-
input_concepts=ds.
|
|
435
|
+
input_concepts=ds.usable_outputs,
|
|
436
436
|
parents=[ds],
|
|
437
437
|
environment=ds.environment,
|
|
438
438
|
partial_concepts=ds.partial_concepts,
|
|
@@ -553,11 +553,11 @@ def process_query(
|
|
|
553
553
|
root_cte.hidden_concepts = statement.hidden_components
|
|
554
554
|
|
|
555
555
|
final_ctes = optimize_ctes(deduped_ctes, root_cte, statement)
|
|
556
|
-
|
|
556
|
+
|
|
557
557
|
return ProcessedQuery(
|
|
558
558
|
order_by=root_cte.order_by,
|
|
559
559
|
limit=statement.limit,
|
|
560
|
-
output_columns=
|
|
560
|
+
output_columns=statement.output_components,
|
|
561
561
|
ctes=final_ctes,
|
|
562
562
|
base=root_cte,
|
|
563
563
|
hidden_columns=set([x for x in statement.hidden_components]),
|
|
@@ -5,7 +5,7 @@ from typing import Annotated, List, Optional, Union
|
|
|
5
5
|
from pydantic import BaseModel, Field, computed_field, field_validator
|
|
6
6
|
from pydantic.functional_validators import PlainValidator
|
|
7
7
|
|
|
8
|
-
from trilogy.constants import CONFIG
|
|
8
|
+
from trilogy.constants import CONFIG, DEFAULT_NAMESPACE
|
|
9
9
|
from trilogy.core.enums import (
|
|
10
10
|
ConceptSource,
|
|
11
11
|
FunctionClass,
|
|
@@ -281,7 +281,11 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
|
|
|
281
281
|
# if the concept is a locally derived concept, it cannot ever be partial
|
|
282
282
|
# but if it's a concept pulled in from upstream and we have a where clause, it should be partial
|
|
283
283
|
ColumnAssignment(
|
|
284
|
-
alias=
|
|
284
|
+
alias=(
|
|
285
|
+
c.address.replace(".", "_")
|
|
286
|
+
if c.namespace != DEFAULT_NAMESPACE
|
|
287
|
+
else c.name
|
|
288
|
+
),
|
|
285
289
|
concept=environment.concepts[c.address].reference,
|
|
286
290
|
modifiers=modifiers if c.address not in self.locally_derived else [],
|
|
287
291
|
)
|
|
@@ -3,6 +3,7 @@ from typing import Annotated, List, Optional, Union
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
from pydantic.functional_validators import PlainValidator
|
|
5
5
|
|
|
6
|
+
from trilogy.core.models.author import ConceptRef
|
|
6
7
|
from trilogy.core.models.build import (
|
|
7
8
|
BuildConcept,
|
|
8
9
|
BuildDatasource,
|
|
@@ -14,7 +15,7 @@ from trilogy.core.statements.common import CopyQueryMixin, PersistQueryMixin
|
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class ProcessedQuery(BaseModel):
|
|
17
|
-
output_columns: List[
|
|
18
|
+
output_columns: List[ConceptRef]
|
|
18
19
|
ctes: List[CTE | UnionCTE]
|
|
19
20
|
base: CTE | UnionCTE
|
|
20
21
|
hidden_columns: set[str] = Field(default_factory=set)
|
|
@@ -38,5 +39,5 @@ class ProcessedRawSQLStatement(BaseModel):
|
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
class ProcessedShowStatement(BaseModel):
|
|
41
|
-
output_columns: List[
|
|
42
|
+
output_columns: List[ConceptRef]
|
|
42
43
|
output_values: List[Union[BuildConcept, BuildDatasource, ProcessedQuery]]
|
trilogy/dialect/base.py
CHANGED
|
@@ -33,7 +33,6 @@ from trilogy.core.models.build import (
|
|
|
33
33
|
BuildRowsetItem,
|
|
34
34
|
BuildSubselectComparison,
|
|
35
35
|
BuildWindowItem,
|
|
36
|
-
Factory,
|
|
37
36
|
)
|
|
38
37
|
from trilogy.core.models.core import (
|
|
39
38
|
DataType,
|
|
@@ -904,7 +903,6 @@ class BaseDialect:
|
|
|
904
903
|
| ProcessedRawSQLStatement
|
|
905
904
|
| ProcessedCopyStatement
|
|
906
905
|
] = []
|
|
907
|
-
factory = Factory(environment=environment)
|
|
908
906
|
for statement in statements:
|
|
909
907
|
if isinstance(statement, PersistStatement):
|
|
910
908
|
if hooks:
|
|
@@ -939,11 +937,9 @@ class BaseDialect:
|
|
|
939
937
|
output.append(
|
|
940
938
|
ProcessedShowStatement(
|
|
941
939
|
output_columns=[
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
]
|
|
946
|
-
)
|
|
940
|
+
environment.concepts[
|
|
941
|
+
DEFAULT_CONCEPTS["query_text"].address
|
|
942
|
+
].reference
|
|
947
943
|
],
|
|
948
944
|
output_values=[
|
|
949
945
|
process_query(
|
|
@@ -984,29 +980,6 @@ class BaseDialect:
|
|
|
984
980
|
return ";\n".join([str(x) for x in query.output_values])
|
|
985
981
|
elif isinstance(query, ProcessedRawSQLStatement):
|
|
986
982
|
return query.text
|
|
987
|
-
select_columns: Dict[str, str] = {}
|
|
988
|
-
cte_output_map = {}
|
|
989
|
-
selected = set()
|
|
990
|
-
output_addresses = [
|
|
991
|
-
c.address
|
|
992
|
-
for c in query.output_columns
|
|
993
|
-
if c.address not in query.hidden_columns
|
|
994
|
-
]
|
|
995
|
-
|
|
996
|
-
for c in query.base.output_columns:
|
|
997
|
-
if c.address not in selected:
|
|
998
|
-
select_columns[c.address] = (
|
|
999
|
-
f"{query.base.name}.{safe_quote(c.safe_address, self.QUOTE_CHARACTER)}"
|
|
1000
|
-
)
|
|
1001
|
-
cte_output_map[c.address] = query.base
|
|
1002
|
-
if c.address not in query.hidden_columns:
|
|
1003
|
-
selected.add(c.address)
|
|
1004
|
-
if not all([x in selected for x in output_addresses]):
|
|
1005
|
-
missing = [x for x in output_addresses if x not in selected]
|
|
1006
|
-
raise ValueError(
|
|
1007
|
-
f"Did not get all output addresses in select - missing: {missing}, have"
|
|
1008
|
-
f" {selected}"
|
|
1009
|
-
)
|
|
1010
983
|
|
|
1011
984
|
recursive = any(isinstance(x, RecursiveCTE) for x in query.ctes)
|
|
1012
985
|
|
trilogy/dialect/snowflake.py
CHANGED
|
@@ -47,7 +47,7 @@ SNOWFLAKE_SQL_TEMPLATE = Template(
|
|
|
47
47
|
CREATE OR REPLACE TABLE {{ output.address.location }} AS
|
|
48
48
|
{% endif %}{%- if ctes %}
|
|
49
49
|
WITH {% if recursive%}RECURSIVE{% endif %}{% for cte in ctes %}
|
|
50
|
-
{{cte.name}} as ({{cte.statement}}){% if not loop.last %},{% endif %}{% else %}
|
|
50
|
+
"{{cte.name}}" as ({{cte.statement}}){% if not loop.last %},{% endif %}{% else %}
|
|
51
51
|
{% endfor %}{% endif %}
|
|
52
52
|
{%- if full_select -%}
|
|
53
53
|
{{full_select}}
|
trilogy/executor.py
CHANGED
|
@@ -8,8 +8,8 @@ from sqlalchemy.engine import CursorResult
|
|
|
8
8
|
|
|
9
9
|
from trilogy.constants import Rendering, logger
|
|
10
10
|
from trilogy.core.enums import FunctionType, Granularity, IOType
|
|
11
|
-
from trilogy.core.models.author import Concept, Function
|
|
12
|
-
from trilogy.core.models.build import
|
|
11
|
+
from trilogy.core.models.author import Concept, ConceptRef, Function
|
|
12
|
+
from trilogy.core.models.build import BuildFunction
|
|
13
13
|
from trilogy.core.models.core import ListWrapper, MapWrapper
|
|
14
14
|
from trilogy.core.models.datasource import Datasource
|
|
15
15
|
from trilogy.core.models.environment import Environment
|
|
@@ -61,7 +61,7 @@ class MockResult:
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def generate_result_set(
|
|
64
|
-
columns: List[
|
|
64
|
+
columns: List[ConceptRef], output_data: list[Any]
|
|
65
65
|
) -> MockResult:
|
|
66
66
|
names = [x.address.replace(".", "_") for x in columns]
|
|
67
67
|
return MockResult(
|
|
@@ -90,7 +90,16 @@ class Executor(object):
|
|
|
90
90
|
if self.dialect == Dialects.DATAFRAME:
|
|
91
91
|
self.engine.setup(self.environment, self.connection)
|
|
92
92
|
|
|
93
|
-
def execute_statement(
|
|
93
|
+
def execute_statement(
|
|
94
|
+
self,
|
|
95
|
+
statement: (
|
|
96
|
+
ProcessedQuery
|
|
97
|
+
| ProcessedCopyStatement
|
|
98
|
+
| ProcessedRawSQLStatement
|
|
99
|
+
| ProcessedQueryPersist
|
|
100
|
+
| ProcessedShowStatement
|
|
101
|
+
),
|
|
102
|
+
) -> Optional[CursorResult]:
|
|
94
103
|
if not isinstance(
|
|
95
104
|
statement,
|
|
96
105
|
(
|
trilogy/parsing/common.py
CHANGED
|
@@ -397,7 +397,7 @@ def group_function_to_concept(
|
|
|
397
397
|
modifiers=modifiers,
|
|
398
398
|
grain=grain,
|
|
399
399
|
metadata=fmetadata,
|
|
400
|
-
derivation=Derivation.
|
|
400
|
+
derivation=Derivation.GROUP_TO,
|
|
401
401
|
granularity=granularity,
|
|
402
402
|
)
|
|
403
403
|
return r
|
|
@@ -654,7 +654,6 @@ def agg_wrapper_to_concept(
|
|
|
654
654
|
fmetadata = metadata or Metadata()
|
|
655
655
|
aggfunction = parent.function
|
|
656
656
|
modifiers = get_upstream_modifiers(parent.concept_arguments, environment)
|
|
657
|
-
# derivation = Concept.calculate_derivation(parent, Purpose.PROPERTY)
|
|
658
657
|
grain = Grain.from_concepts(parent.by, environment) if parent.by else Grain()
|
|
659
658
|
granularity = Concept.calculate_granularity(Derivation.AGGREGATE, grain, parent)
|
|
660
659
|
|
|
@@ -778,7 +777,6 @@ def rowset_to_concepts(rowset: RowsetDerivationStatement, environment: Environme
|
|
|
778
777
|
for x in pre_output:
|
|
779
778
|
x.lineage = RowsetItem(
|
|
780
779
|
content=orig_map[x.address].reference,
|
|
781
|
-
# where=rowset.select.where_clause,
|
|
782
780
|
rowset=RowsetLineage(
|
|
783
781
|
name=rowset.name,
|
|
784
782
|
derived_concepts=[x.reference for x in pre_output],
|