pytrilogy 0.3.138__cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.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.
Files changed (182) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cpython-311-x86_64-linux-gnu.so +0 -0
  4. pytrilogy-0.3.138.dist-info/METADATA +525 -0
  5. pytrilogy-0.3.138.dist-info/RECORD +182 -0
  6. pytrilogy-0.3.138.dist-info/WHEEL +5 -0
  7. pytrilogy-0.3.138.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.138.dist-info/licenses/LICENSE.md +19 -0
  9. trilogy/__init__.py +9 -0
  10. trilogy/ai/README.md +10 -0
  11. trilogy/ai/__init__.py +19 -0
  12. trilogy/ai/constants.py +92 -0
  13. trilogy/ai/conversation.py +107 -0
  14. trilogy/ai/enums.py +7 -0
  15. trilogy/ai/execute.py +50 -0
  16. trilogy/ai/models.py +34 -0
  17. trilogy/ai/prompts.py +87 -0
  18. trilogy/ai/providers/__init__.py +0 -0
  19. trilogy/ai/providers/anthropic.py +106 -0
  20. trilogy/ai/providers/base.py +24 -0
  21. trilogy/ai/providers/google.py +146 -0
  22. trilogy/ai/providers/openai.py +89 -0
  23. trilogy/ai/providers/utils.py +68 -0
  24. trilogy/authoring/README.md +3 -0
  25. trilogy/authoring/__init__.py +143 -0
  26. trilogy/constants.py +113 -0
  27. trilogy/core/README.md +52 -0
  28. trilogy/core/__init__.py +0 -0
  29. trilogy/core/constants.py +6 -0
  30. trilogy/core/enums.py +443 -0
  31. trilogy/core/env_processor.py +120 -0
  32. trilogy/core/environment_helpers.py +320 -0
  33. trilogy/core/ergonomics.py +193 -0
  34. trilogy/core/exceptions.py +123 -0
  35. trilogy/core/functions.py +1227 -0
  36. trilogy/core/graph_models.py +139 -0
  37. trilogy/core/internal.py +85 -0
  38. trilogy/core/models/__init__.py +0 -0
  39. trilogy/core/models/author.py +2672 -0
  40. trilogy/core/models/build.py +2521 -0
  41. trilogy/core/models/build_environment.py +180 -0
  42. trilogy/core/models/core.py +494 -0
  43. trilogy/core/models/datasource.py +322 -0
  44. trilogy/core/models/environment.py +748 -0
  45. trilogy/core/models/execute.py +1177 -0
  46. trilogy/core/optimization.py +251 -0
  47. trilogy/core/optimizations/__init__.py +12 -0
  48. trilogy/core/optimizations/base_optimization.py +17 -0
  49. trilogy/core/optimizations/hide_unused_concept.py +47 -0
  50. trilogy/core/optimizations/inline_datasource.py +102 -0
  51. trilogy/core/optimizations/predicate_pushdown.py +245 -0
  52. trilogy/core/processing/README.md +94 -0
  53. trilogy/core/processing/READMEv2.md +121 -0
  54. trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
  55. trilogy/core/processing/__init__.py +0 -0
  56. trilogy/core/processing/concept_strategies_v3.py +508 -0
  57. trilogy/core/processing/constants.py +15 -0
  58. trilogy/core/processing/discovery_node_factory.py +451 -0
  59. trilogy/core/processing/discovery_utility.py +517 -0
  60. trilogy/core/processing/discovery_validation.py +167 -0
  61. trilogy/core/processing/graph_utils.py +43 -0
  62. trilogy/core/processing/node_generators/README.md +9 -0
  63. trilogy/core/processing/node_generators/__init__.py +31 -0
  64. trilogy/core/processing/node_generators/basic_node.py +160 -0
  65. trilogy/core/processing/node_generators/common.py +268 -0
  66. trilogy/core/processing/node_generators/constant_node.py +38 -0
  67. trilogy/core/processing/node_generators/filter_node.py +315 -0
  68. trilogy/core/processing/node_generators/group_node.py +213 -0
  69. trilogy/core/processing/node_generators/group_to_node.py +117 -0
  70. trilogy/core/processing/node_generators/multiselect_node.py +205 -0
  71. trilogy/core/processing/node_generators/node_merge_node.py +653 -0
  72. trilogy/core/processing/node_generators/recursive_node.py +88 -0
  73. trilogy/core/processing/node_generators/rowset_node.py +165 -0
  74. trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  75. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
  76. trilogy/core/processing/node_generators/select_merge_node.py +748 -0
  77. trilogy/core/processing/node_generators/select_node.py +95 -0
  78. trilogy/core/processing/node_generators/synonym_node.py +98 -0
  79. trilogy/core/processing/node_generators/union_node.py +91 -0
  80. trilogy/core/processing/node_generators/unnest_node.py +182 -0
  81. trilogy/core/processing/node_generators/window_node.py +201 -0
  82. trilogy/core/processing/nodes/README.md +28 -0
  83. trilogy/core/processing/nodes/__init__.py +179 -0
  84. trilogy/core/processing/nodes/base_node.py +519 -0
  85. trilogy/core/processing/nodes/filter_node.py +75 -0
  86. trilogy/core/processing/nodes/group_node.py +194 -0
  87. trilogy/core/processing/nodes/merge_node.py +420 -0
  88. trilogy/core/processing/nodes/recursive_node.py +46 -0
  89. trilogy/core/processing/nodes/select_node_v2.py +242 -0
  90. trilogy/core/processing/nodes/union_node.py +53 -0
  91. trilogy/core/processing/nodes/unnest_node.py +62 -0
  92. trilogy/core/processing/nodes/window_node.py +56 -0
  93. trilogy/core/processing/utility.py +823 -0
  94. trilogy/core/query_processor.py +596 -0
  95. trilogy/core/statements/README.md +35 -0
  96. trilogy/core/statements/__init__.py +0 -0
  97. trilogy/core/statements/author.py +536 -0
  98. trilogy/core/statements/build.py +0 -0
  99. trilogy/core/statements/common.py +20 -0
  100. trilogy/core/statements/execute.py +155 -0
  101. trilogy/core/table_processor.py +66 -0
  102. trilogy/core/utility.py +8 -0
  103. trilogy/core/validation/README.md +46 -0
  104. trilogy/core/validation/__init__.py +0 -0
  105. trilogy/core/validation/common.py +161 -0
  106. trilogy/core/validation/concept.py +146 -0
  107. trilogy/core/validation/datasource.py +227 -0
  108. trilogy/core/validation/environment.py +73 -0
  109. trilogy/core/validation/fix.py +106 -0
  110. trilogy/dialect/__init__.py +32 -0
  111. trilogy/dialect/base.py +1359 -0
  112. trilogy/dialect/bigquery.py +256 -0
  113. trilogy/dialect/common.py +147 -0
  114. trilogy/dialect/config.py +144 -0
  115. trilogy/dialect/dataframe.py +50 -0
  116. trilogy/dialect/duckdb.py +177 -0
  117. trilogy/dialect/enums.py +147 -0
  118. trilogy/dialect/metadata.py +173 -0
  119. trilogy/dialect/mock.py +190 -0
  120. trilogy/dialect/postgres.py +91 -0
  121. trilogy/dialect/presto.py +104 -0
  122. trilogy/dialect/results.py +89 -0
  123. trilogy/dialect/snowflake.py +90 -0
  124. trilogy/dialect/sql_server.py +92 -0
  125. trilogy/engine.py +48 -0
  126. trilogy/execution/config.py +75 -0
  127. trilogy/executor.py +568 -0
  128. trilogy/hooks/__init__.py +4 -0
  129. trilogy/hooks/base_hook.py +40 -0
  130. trilogy/hooks/graph_hook.py +139 -0
  131. trilogy/hooks/query_debugger.py +166 -0
  132. trilogy/metadata/__init__.py +0 -0
  133. trilogy/parser.py +10 -0
  134. trilogy/parsing/README.md +21 -0
  135. trilogy/parsing/__init__.py +0 -0
  136. trilogy/parsing/common.py +1069 -0
  137. trilogy/parsing/config.py +5 -0
  138. trilogy/parsing/exceptions.py +8 -0
  139. trilogy/parsing/helpers.py +1 -0
  140. trilogy/parsing/parse_engine.py +2813 -0
  141. trilogy/parsing/render.py +750 -0
  142. trilogy/parsing/trilogy.lark +540 -0
  143. trilogy/py.typed +0 -0
  144. trilogy/render.py +42 -0
  145. trilogy/scripts/README.md +7 -0
  146. trilogy/scripts/__init__.py +0 -0
  147. trilogy/scripts/dependency/Cargo.lock +617 -0
  148. trilogy/scripts/dependency/Cargo.toml +39 -0
  149. trilogy/scripts/dependency/README.md +131 -0
  150. trilogy/scripts/dependency/build.sh +25 -0
  151. trilogy/scripts/dependency/src/directory_resolver.rs +162 -0
  152. trilogy/scripts/dependency/src/lib.rs +16 -0
  153. trilogy/scripts/dependency/src/main.rs +770 -0
  154. trilogy/scripts/dependency/src/parser.rs +435 -0
  155. trilogy/scripts/dependency/src/preql.pest +208 -0
  156. trilogy/scripts/dependency/src/python_bindings.rs +289 -0
  157. trilogy/scripts/dependency/src/resolver.rs +716 -0
  158. trilogy/scripts/dependency/tests/base.preql +3 -0
  159. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  160. trilogy/scripts/dependency/tests/customer.preql +6 -0
  161. trilogy/scripts/dependency/tests/main.preql +9 -0
  162. trilogy/scripts/dependency/tests/orders.preql +7 -0
  163. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  164. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  165. trilogy/scripts/dependency.py +323 -0
  166. trilogy/scripts/display.py +460 -0
  167. trilogy/scripts/environment.py +46 -0
  168. trilogy/scripts/parallel_execution.py +483 -0
  169. trilogy/scripts/single_execution.py +131 -0
  170. trilogy/scripts/trilogy.py +772 -0
  171. trilogy/std/__init__.py +0 -0
  172. trilogy/std/color.preql +3 -0
  173. trilogy/std/date.preql +13 -0
  174. trilogy/std/display.preql +18 -0
  175. trilogy/std/geography.preql +22 -0
  176. trilogy/std/metric.preql +15 -0
  177. trilogy/std/money.preql +67 -0
  178. trilogy/std/net.preql +14 -0
  179. trilogy/std/ranking.preql +7 -0
  180. trilogy/std/report.preql +5 -0
  181. trilogy/std/semantic.preql +6 -0
  182. trilogy/utility.py +34 -0
@@ -0,0 +1,179 @@
1
+ from pydantic import BaseModel, ConfigDict, Field
2
+
3
+ from trilogy.core.exceptions import UnresolvableQueryException
4
+ from trilogy.core.models.author import Concept
5
+ from trilogy.core.models.build import BuildConcept, BuildWhereClause
6
+ from trilogy.core.models.build_environment import BuildEnvironment
7
+ from trilogy.core.models.environment import Environment
8
+
9
+ from .base_node import NodeJoin, StrategyNode, WhereSafetyNode
10
+ from .filter_node import FilterNode
11
+ from .group_node import GroupNode
12
+ from .merge_node import MergeNode
13
+ from .recursive_node import RecursiveNode
14
+ from .select_node_v2 import ConstantNode, SelectNode
15
+ from .union_node import UnionNode
16
+ from .unnest_node import UnnestNode
17
+ from .window_node import WindowNode
18
+
19
+
20
+ class History(BaseModel):
21
+ base_environment: Environment
22
+ local_base_concepts: dict[str, Concept] = Field(default_factory=dict)
23
+ history: dict[str, StrategyNode | None] = Field(default_factory=dict)
24
+ select_history: dict[str, StrategyNode | None] = Field(default_factory=dict)
25
+ started: dict[str, int] = Field(default_factory=dict)
26
+ model_config = ConfigDict(arbitrary_types_allowed=True)
27
+
28
+ def _concepts_to_lookup(
29
+ self,
30
+ search: list[BuildConcept],
31
+ accept_partial: bool,
32
+ conditions: BuildWhereClause | None = None,
33
+ ) -> str:
34
+ base = sorted([c.address for c in search])
35
+ if conditions:
36
+ return "-".join(base) + str(accept_partial) + str(conditions)
37
+ return "-".join(base) + str(accept_partial)
38
+
39
+ def search_to_history(
40
+ self,
41
+ search: list[BuildConcept],
42
+ accept_partial: bool,
43
+ output: StrategyNode | None,
44
+ conditions: BuildWhereClause | None = None,
45
+ ):
46
+ self.history[
47
+ self._concepts_to_lookup(search, accept_partial, conditions=conditions)
48
+ ] = output
49
+ self.log_end(
50
+ search,
51
+ accept_partial=accept_partial,
52
+ conditions=conditions,
53
+ )
54
+
55
+ def get_history(
56
+ self,
57
+ search: list[BuildConcept],
58
+ conditions: BuildWhereClause | None = None,
59
+ accept_partial: bool = False,
60
+ parent_key: str = "",
61
+ ) -> StrategyNode | None | bool:
62
+ key = self._concepts_to_lookup(
63
+ search,
64
+ accept_partial,
65
+ conditions,
66
+ )
67
+ if parent_key and parent_key == key:
68
+ raise ValueError(
69
+ f"Parent key {parent_key} is the same as the current key {key}"
70
+ )
71
+ if key in self.history:
72
+ node = self.history[key]
73
+ if node:
74
+ return node.copy()
75
+ return node
76
+ return False
77
+
78
+ def log_start(
79
+ self,
80
+ search: list[BuildConcept],
81
+ accept_partial: bool = False,
82
+ conditions: BuildWhereClause | None = None,
83
+ ):
84
+ key = self._concepts_to_lookup(
85
+ search,
86
+ accept_partial=accept_partial,
87
+ conditions=conditions,
88
+ )
89
+ if key in self.started:
90
+ self.started[key] += 1
91
+ else:
92
+ self.started[key] = 1
93
+ if self.started[key] > 5:
94
+ raise UnresolvableQueryException(
95
+ f"Was unable to resolve datasources to serve this query from model; unresolvable set was {search}. You may be querying unrelated concepts."
96
+ )
97
+
98
+ def log_end(
99
+ self,
100
+ search: list[BuildConcept],
101
+ accept_partial: bool = False,
102
+ conditions: BuildWhereClause | None = None,
103
+ ):
104
+ key = self._concepts_to_lookup(
105
+ search,
106
+ accept_partial=accept_partial,
107
+ conditions=conditions,
108
+ )
109
+ if key in self.started:
110
+ del self.started[key]
111
+
112
+ def check_started(
113
+ self,
114
+ search: list[BuildConcept],
115
+ accept_partial: bool = False,
116
+ conditions: BuildWhereClause | None = None,
117
+ ):
118
+ return (
119
+ self._concepts_to_lookup(
120
+ search,
121
+ accept_partial,
122
+ conditions=conditions,
123
+ )
124
+ in self.started
125
+ )
126
+
127
+ def gen_select_node(
128
+ self,
129
+ concepts: list[BuildConcept],
130
+ environment: BuildEnvironment,
131
+ g,
132
+ depth: int,
133
+ fail_if_not_found: bool = False,
134
+ accept_partial: bool = False,
135
+ conditions: BuildWhereClause | None = None,
136
+ ) -> StrategyNode | None:
137
+ from trilogy.core.processing.node_generators.select_node import gen_select_node
138
+
139
+ fingerprint = self._concepts_to_lookup(
140
+ concepts,
141
+ accept_partial,
142
+ conditions=conditions,
143
+ )
144
+ if fingerprint in self.select_history:
145
+ rval = self.select_history[fingerprint]
146
+ if rval:
147
+ # all nodes must be copied before returning
148
+ return rval.copy()
149
+ return rval
150
+ gen = gen_select_node(
151
+ concepts,
152
+ environment,
153
+ g,
154
+ depth + 1,
155
+ fail_if_not_found=fail_if_not_found,
156
+ accept_partial=accept_partial,
157
+ conditions=conditions,
158
+ )
159
+ self.select_history[fingerprint] = gen
160
+ if gen:
161
+ return gen.copy()
162
+ return gen
163
+
164
+
165
+ __all__ = [
166
+ "FilterNode",
167
+ "GroupNode",
168
+ "MergeNode",
169
+ "SelectNode",
170
+ "WindowNode",
171
+ "StrategyNode",
172
+ "NodeJoin",
173
+ "UnnestNode",
174
+ "ConstantNode",
175
+ "UnionNode",
176
+ "History",
177
+ "WhereSafetyNode",
178
+ "RecursiveNode",
179
+ ]
@@ -0,0 +1,519 @@
1
+ from collections import defaultdict
2
+ from dataclasses import dataclass, field
3
+ from typing import List, Optional
4
+
5
+ from trilogy.core.enums import (
6
+ BooleanOperator,
7
+ Derivation,
8
+ JoinType,
9
+ Modifier,
10
+ SourceType,
11
+ )
12
+ from trilogy.core.models.build import (
13
+ BuildComparison,
14
+ BuildConcept,
15
+ BuildConditional,
16
+ BuildDatasource,
17
+ BuildGrain,
18
+ BuildOrderBy,
19
+ BuildParenthetical,
20
+ LooseBuildConceptList,
21
+ )
22
+ from trilogy.core.models.build_environment import BuildEnvironment
23
+ from trilogy.core.models.execute import ConceptPair, QueryDatasource, UnnestJoin
24
+ from trilogy.utility import unique
25
+
26
+
27
+ def resolve_concept_map(
28
+ inputs: List[QueryDatasource | BuildDatasource],
29
+ targets: List[BuildConcept],
30
+ inherited_inputs: List[BuildConcept],
31
+ full_joins: List[BuildConcept] | None = None,
32
+ ) -> dict[str, set[BuildDatasource | QueryDatasource | UnnestJoin]]:
33
+
34
+ targets = targets or []
35
+ concept_map: dict[str, set[BuildDatasource | QueryDatasource | UnnestJoin]] = (
36
+ defaultdict(set)
37
+ )
38
+ full_addresses = {c.address for c in full_joins} if full_joins else set()
39
+ inherited = set([t.address for t in inherited_inputs])
40
+ for input in inputs:
41
+ for concept in input.output_concepts:
42
+ if concept.address not in input.full_concepts:
43
+ continue
44
+ if (
45
+ isinstance(input, QueryDatasource)
46
+ and concept.address in input.hidden_concepts
47
+ ):
48
+ continue
49
+ if concept.address in full_addresses:
50
+ concept_map[concept.address].add(input)
51
+ elif concept.address not in concept_map:
52
+ # equi_targets = [x for x in targets if concept.address in x.pseudonyms or x.address in concept.pseudonyms]
53
+ # if equi_targets:
54
+ # for equi in equi_targets:
55
+ # concept_map[equi.address] = set()
56
+ concept_map[concept.address].add(input)
57
+
58
+ # second loop, include partials
59
+ for input in inputs:
60
+ for concept in input.output_concepts:
61
+ if concept.address not in inherited and not (
62
+ concept.pseudonyms and any(s in inherited for s in concept.pseudonyms)
63
+ ):
64
+ continue
65
+ if (
66
+ isinstance(input, QueryDatasource)
67
+ and concept.address in input.hidden_concepts
68
+ ):
69
+ continue
70
+ if len(concept_map.get(concept.address, [])) == 0:
71
+ concept_map[concept.address].add(input)
72
+ # this adds our new derived metrics, which are not created in this CTE
73
+ for target in targets:
74
+ if target.address not in inherited:
75
+ # an empty source means it is defined in this CTE
76
+ concept_map[target.address] = set()
77
+ return concept_map
78
+
79
+
80
+ def get_all_parent_partial(
81
+ all_concepts: List[BuildConcept], parents: List["StrategyNode"]
82
+ ) -> List[BuildConcept]:
83
+ return unique(
84
+ [
85
+ c
86
+ for c in all_concepts
87
+ if len(
88
+ [
89
+ p
90
+ for p in parents
91
+ if c.address in [x.address for x in p.partial_concepts]
92
+ ]
93
+ )
94
+ >= 1
95
+ and all(
96
+ [
97
+ c.address in p.partial_lcl
98
+ for p in parents
99
+ if c.address in p.output_lcl
100
+ ]
101
+ )
102
+ ],
103
+ "address",
104
+ )
105
+
106
+
107
+ def get_all_parent_nullable(
108
+ all_concepts: List[BuildConcept], parents: List["StrategyNode"]
109
+ ) -> List[BuildConcept]:
110
+ return unique(
111
+ [
112
+ c
113
+ for c in all_concepts
114
+ if len(
115
+ [
116
+ p
117
+ for p in parents
118
+ if c.address in [x.address for x in p.nullable_concepts]
119
+ ]
120
+ )
121
+ >= 1
122
+ ],
123
+ "address",
124
+ )
125
+
126
+
127
+ class StrategyNode:
128
+ source_type = SourceType.ABSTRACT
129
+
130
+ def __init__(
131
+ self,
132
+ input_concepts: List[BuildConcept],
133
+ output_concepts: List[BuildConcept],
134
+ environment: BuildEnvironment,
135
+ whole_grain: bool = False,
136
+ parents: List["StrategyNode"] | None = None,
137
+ partial_concepts: List[BuildConcept] | None = None,
138
+ nullable_concepts: List[BuildConcept] | None = None,
139
+ depth: int = 0,
140
+ conditions: (
141
+ BuildConditional | BuildComparison | BuildParenthetical | None
142
+ ) = None,
143
+ preexisting_conditions: (
144
+ BuildConditional | BuildComparison | BuildParenthetical | None
145
+ ) = None,
146
+ force_group: bool | None = None,
147
+ grain: Optional[BuildGrain] = None,
148
+ hidden_concepts: set[str] | None = None,
149
+ existence_concepts: List[BuildConcept] | None = None,
150
+ virtual_output_concepts: List[BuildConcept] | None = None,
151
+ ordering: BuildOrderBy | None = None,
152
+ ):
153
+ self.input_concepts: List[BuildConcept] = (
154
+ unique(input_concepts, "address") if input_concepts else []
155
+ )
156
+ self.input_lcl = LooseBuildConceptList(concepts=self.input_concepts)
157
+ self.output_concepts: List[BuildConcept] = unique(output_concepts, "address")
158
+ self.output_lcl = LooseBuildConceptList(concepts=self.output_concepts)
159
+
160
+ self.environment = environment
161
+ self.whole_grain = whole_grain
162
+ self.parents = parents or []
163
+ self.resolution_cache: Optional[QueryDatasource] = None
164
+
165
+ self.nullable_concepts = nullable_concepts or get_all_parent_nullable(
166
+ self.output_concepts, self.parents
167
+ )
168
+ self.ordering = ordering
169
+ self.depth = depth
170
+ self.conditions = conditions
171
+ self.grain = grain
172
+ self.force_group = force_group
173
+ self.tainted = False
174
+ self.hidden_concepts = hidden_concepts or set()
175
+ self.existence_concepts = existence_concepts or []
176
+ self.virtual_output_concepts = virtual_output_concepts or []
177
+ self.preexisting_conditions = preexisting_conditions
178
+ if self.conditions and not self.preexisting_conditions:
179
+ self.preexisting_conditions = self.conditions
180
+ elif (
181
+ self.conditions
182
+ and self.preexisting_conditions
183
+ and self.conditions != self.preexisting_conditions
184
+ ):
185
+ self.preexisting_conditions = BuildConditional(
186
+ left=self.conditions,
187
+ right=self.preexisting_conditions,
188
+ operator=BooleanOperator.AND,
189
+ )
190
+ self.partial_concepts: list[BuildConcept] = self.derive_partials(
191
+ partial_concepts
192
+ )
193
+ self.validate_inputs()
194
+ self.log = True
195
+
196
+ def validate_inputs(self):
197
+ if not self.parents:
198
+ return
199
+ non_hidden = set()
200
+ hidden = set()
201
+ usable_outputs = set()
202
+ for x in self.parents:
203
+ for z in x.usable_outputs:
204
+ usable_outputs.add(z.address)
205
+ non_hidden.add(z.address)
206
+ for psd in z.pseudonyms:
207
+ non_hidden.add(psd)
208
+ for z in x.hidden_concepts:
209
+ hidden.add(z)
210
+ if not all([x.address in non_hidden for x in self.input_concepts]):
211
+ missing = [x for x in self.input_concepts if x.address not in non_hidden]
212
+ raise ValueError(
213
+ f"Invalid input concepts; {missing} are missing non-hidden parent nodes; have {non_hidden} and hidden {hidden} from root {usable_outputs}"
214
+ )
215
+
216
+ def add_parents(self, parents: list["StrategyNode"]):
217
+ self.parents += parents
218
+ self.partial_concepts = self.derive_partials(None)
219
+ return self
220
+
221
+ def set_preexisting_conditions(
222
+ self, conditions: BuildConditional | BuildComparison | BuildParenthetical
223
+ ):
224
+ self.preexisting_conditions = conditions
225
+ return self
226
+
227
+ def add_condition(
228
+ self, condition: BuildConditional | BuildComparison | BuildParenthetical
229
+ ):
230
+ if self.conditions and condition == self.conditions:
231
+ return self
232
+ if self.conditions:
233
+ self.conditions = BuildConditional(
234
+ left=self.conditions, right=condition, operator=BooleanOperator.AND
235
+ )
236
+ else:
237
+ self.conditions = condition
238
+ self.set_preexisting_conditions(condition)
239
+ self.rebuild_cache()
240
+ return self
241
+
242
+ def derive_partials(
243
+ self, partial_concepts: List[BuildConcept] | None = None
244
+ ) -> List[BuildConcept]:
245
+ # validate parents exist
246
+ # assign partial values where needed
247
+ for parent in self.parents:
248
+ if not parent:
249
+ raise SyntaxError("Unresolvable parent")
250
+
251
+ # TODO: make this accurate
252
+ if self.parents and partial_concepts is None:
253
+ partials = get_all_parent_partial(self.output_concepts, self.parents)
254
+ elif partial_concepts is None:
255
+ partials = []
256
+ else:
257
+ partials = partial_concepts
258
+ self.partial_lcl = LooseBuildConceptList(concepts=partials)
259
+ return partials
260
+
261
+ def add_output_concepts(
262
+ self, concepts: List[BuildConcept], rebuild: bool = True, unhide: bool = True
263
+ ):
264
+ for concept in concepts:
265
+ if concept.address not in self.output_lcl.addresses:
266
+ self.output_concepts.append(concept)
267
+ if unhide and concept.address in self.hidden_concepts:
268
+ self.hidden_concepts.remove(concept.address)
269
+ self.output_lcl = LooseBuildConceptList(concepts=self.output_concepts)
270
+ if rebuild:
271
+ self.rebuild_cache()
272
+ return self
273
+
274
+ def add_partial_concepts(self, concepts: List[BuildConcept], rebuild: bool = True):
275
+ for concept in concepts:
276
+ if concept.address not in self.partial_lcl.addresses:
277
+ self.partial_concepts.append(concept)
278
+ self.partial_lcl = LooseBuildConceptList(concepts=self.partial_concepts)
279
+ if rebuild:
280
+ self.rebuild_cache()
281
+ return self
282
+
283
+ def add_existence_concepts(
284
+ self, concepts: List[BuildConcept], rebuild: bool = True
285
+ ):
286
+ for concept in concepts:
287
+ if concept.address not in self.output_concepts:
288
+ self.existence_concepts.append(concept)
289
+ if rebuild:
290
+ self.rebuild_cache()
291
+ return self
292
+
293
+ def set_visible_concepts(self, concepts: List[BuildConcept]):
294
+ for x in self.output_concepts:
295
+ if x.address not in [c.address for c in concepts]:
296
+ self.hidden_concepts.add(x.address)
297
+ return self
298
+
299
+ def set_output_concepts(
300
+ self,
301
+ concepts: List[BuildConcept],
302
+ rebuild: bool = True,
303
+ change_visibility: bool = True,
304
+ ):
305
+ # exit if no changes
306
+ if self.output_concepts == concepts:
307
+ return self
308
+ self.output_concepts = concepts
309
+ if self.hidden_concepts and change_visibility:
310
+ self.hidden_concepts = set(
311
+ x for x in self.hidden_concepts if x not in concepts
312
+ )
313
+
314
+ self.output_lcl = LooseBuildConceptList(concepts=self.output_concepts)
315
+
316
+ if rebuild:
317
+ self.rebuild_cache()
318
+ return self
319
+
320
+ def add_output_concept(self, concept: BuildConcept, rebuild: bool = True):
321
+ return self.add_output_concepts([concept], rebuild)
322
+
323
+ def hide_output_concepts(
324
+ self, concepts: List[BuildConcept] | list[str] | set[str], rebuild: bool = True
325
+ ):
326
+ for x in concepts:
327
+ if isinstance(x, BuildConcept):
328
+ self.hidden_concepts.add(x.address)
329
+ else:
330
+ self.hidden_concepts.add(x)
331
+ if rebuild:
332
+ self.rebuild_cache()
333
+ return self
334
+
335
+ def unhide_output_concepts(
336
+ self, concepts: List[BuildConcept], rebuild: bool = True
337
+ ):
338
+ self.hidden_concepts = set(x for x in self.hidden_concepts if x not in concepts)
339
+ if rebuild:
340
+ self.rebuild_cache()
341
+ return self
342
+
343
+ @property
344
+ def usable_outputs(self) -> list[BuildConcept]:
345
+ return [
346
+ x for x in self.output_concepts if x.address not in self.hidden_concepts
347
+ ]
348
+
349
+ @property
350
+ def logging_prefix(self) -> str:
351
+ return "\t" * self.depth
352
+
353
+ @property
354
+ def all_concepts(self) -> list[BuildConcept]:
355
+ return [*self.output_concepts]
356
+
357
+ @property
358
+ def all_used_concepts(self) -> list[BuildConcept]:
359
+ return [*self.input_concepts, *self.existence_concepts]
360
+
361
+ def __repr__(self):
362
+ concepts = self.all_concepts
363
+ addresses = [c.address for c in concepts]
364
+ contents = ",".join(sorted(addresses[:3]))
365
+ if len(addresses) > 3:
366
+ extra = len(addresses) - 3
367
+ contents += f"...{extra} more"
368
+ return f"{self.__class__.__name__}<{contents}>"
369
+
370
+ def _resolve(self) -> QueryDatasource:
371
+ parent_sources: List[QueryDatasource | BuildDatasource] = [
372
+ p.resolve() for p in self.parents
373
+ ]
374
+
375
+ grain = (
376
+ self.grain if self.grain else BuildGrain.from_concepts(self.output_concepts)
377
+ )
378
+ source_map = resolve_concept_map(
379
+ parent_sources,
380
+ targets=self.output_concepts,
381
+ inherited_inputs=self.input_concepts + self.existence_concepts,
382
+ )
383
+
384
+ return QueryDatasource(
385
+ input_concepts=self.input_concepts,
386
+ output_concepts=self.output_concepts,
387
+ datasources=parent_sources,
388
+ source_type=self.source_type,
389
+ source_map=source_map,
390
+ joins=[],
391
+ grain=grain,
392
+ condition=self.conditions,
393
+ partial_concepts=self.partial_concepts,
394
+ nullable_concepts=self.nullable_concepts,
395
+ force_group=self.force_group,
396
+ hidden_concepts=self.hidden_concepts,
397
+ ordering=self.ordering,
398
+ )
399
+
400
+ def rebuild_cache(self) -> QueryDatasource:
401
+ self.tainted = True
402
+ self.output_lcl = LooseBuildConceptList(concepts=self.output_concepts)
403
+ if not self.resolution_cache:
404
+ return self.resolve()
405
+ self.resolution_cache = None
406
+ return self.resolve()
407
+
408
+ def resolve(self) -> QueryDatasource:
409
+ if self.resolution_cache:
410
+ return self.resolution_cache
411
+ qds = self._resolve()
412
+ self.resolution_cache = qds
413
+ return qds
414
+
415
+ def copy(self) -> "StrategyNode":
416
+ return self.__class__(
417
+ input_concepts=list(self.input_concepts),
418
+ output_concepts=list(self.output_concepts),
419
+ environment=self.environment,
420
+ whole_grain=self.whole_grain,
421
+ parents=list(self.parents),
422
+ partial_concepts=list(self.partial_concepts),
423
+ nullable_concepts=list(self.nullable_concepts),
424
+ depth=self.depth,
425
+ conditions=self.conditions,
426
+ preexisting_conditions=self.preexisting_conditions,
427
+ force_group=self.force_group,
428
+ grain=self.grain,
429
+ hidden_concepts=set(self.hidden_concepts),
430
+ existence_concepts=list(self.existence_concepts),
431
+ virtual_output_concepts=list(self.virtual_output_concepts),
432
+ ordering=self.ordering,
433
+ )
434
+
435
+
436
+ @dataclass
437
+ class NodeJoin:
438
+ left_node: StrategyNode
439
+ right_node: StrategyNode
440
+ concepts: List[BuildConcept]
441
+ join_type: JoinType
442
+ filter_to_mutual: bool = False
443
+ concept_pairs: list[ConceptPair] | None = None
444
+ modifiers: List[Modifier] = field(default_factory=list)
445
+
446
+ def __post_init__(self):
447
+ if self.left_node == self.right_node:
448
+ raise SyntaxError("Invalid join, left and right nodes are the same")
449
+ if self.concept_pairs:
450
+ return
451
+ final_concepts = []
452
+ for concept in self.concepts:
453
+ include = True
454
+ for ds in [self.left_node, self.right_node]:
455
+ if concept.address not in [c.address for c in ds.all_concepts]:
456
+ if self.filter_to_mutual:
457
+ include = False
458
+ else:
459
+ raise SyntaxError(
460
+ f"Invalid join, missing {concept} on {str(ds)}, have"
461
+ f" {[c.address for c in ds.all_concepts]}"
462
+ )
463
+ if include:
464
+ final_concepts.append(concept)
465
+ if not final_concepts and self.concepts:
466
+ # if one datasource only has constants
467
+ # we can join on 1=1
468
+ for ds in [self.left_node, self.right_node]:
469
+ if all([c.derivation == Derivation.CONSTANT for c in ds.all_concepts]):
470
+ self.concepts = []
471
+ return
472
+
473
+ left_keys = [c.address for c in self.left_node.all_concepts]
474
+ right_keys = [c.address for c in self.right_node.all_concepts]
475
+ match_concepts = [c.address for c in self.concepts]
476
+ raise SyntaxError(
477
+ "No mutual join keys found between"
478
+ f" {self.left_node} and"
479
+ f" {self.right_node}, left_keys {left_keys},"
480
+ f" right_keys {right_keys},"
481
+ f" provided join concepts {match_concepts}"
482
+ )
483
+ self.concepts = final_concepts
484
+
485
+ @property
486
+ def unique_id(self) -> str:
487
+ nodes = sorted([self.left_node, self.right_node], key=lambda x: str(x))
488
+ return str(nodes) + self.join_type.value
489
+
490
+ def __str__(self):
491
+ return (
492
+ f"{self.join_type.value} JOIN {self.left_node} and"
493
+ f" {self.right_node} on"
494
+ f" {','.join([str(k) for k in self.concepts])}"
495
+ )
496
+
497
+
498
+ class WhereSafetyNode(StrategyNode):
499
+ """Specialized node to be used to pad certain
500
+ select outputs that can't be immediately used in a where
501
+ clause; eg window functions. Will remove itself if not required."""
502
+
503
+ def resolve(self) -> QueryDatasource:
504
+ if not self.conditions and len(self.parents) == 1:
505
+ parent = self.parents[0]
506
+ parent = parent.copy()
507
+ # avoid performance hit by not rebuilding until end
508
+ parent.set_output_concepts(self.output_concepts, rebuild=False)
509
+
510
+ # these conditions
511
+ if self.preexisting_conditions:
512
+ parent.set_preexisting_conditions(self.preexisting_conditions)
513
+ # TODO: add a helper for this
514
+ parent.ordering = self.ordering
515
+
516
+ # actually build the node
517
+ parent.rebuild_cache()
518
+ return parent.resolve()
519
+ return super().resolve()