pytrilogy 0.3.142__cp313-cp313-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 (200) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cpython-313-x86_64-linux-gnu.so +0 -0
  4. pytrilogy-0.3.142.dist-info/METADATA +555 -0
  5. pytrilogy-0.3.142.dist-info/RECORD +200 -0
  6. pytrilogy-0.3.142.dist-info/WHEEL +5 -0
  7. pytrilogy-0.3.142.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.142.dist-info/licenses/LICENSE.md +19 -0
  9. trilogy/__init__.py +16 -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 +100 -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 +148 -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 +2669 -0
  40. trilogy/core/models/build.py +2521 -0
  41. trilogy/core/models/build_environment.py +180 -0
  42. trilogy/core/models/core.py +501 -0
  43. trilogy/core/models/datasource.py +322 -0
  44. trilogy/core/models/environment.py +751 -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 +548 -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 +256 -0
  110. trilogy/dialect/__init__.py +32 -0
  111. trilogy/dialect/base.py +1392 -0
  112. trilogy/dialect/bigquery.py +308 -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 +231 -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 +117 -0
  121. trilogy/dialect/presto.py +110 -0
  122. trilogy/dialect/results.py +89 -0
  123. trilogy/dialect/snowflake.py +129 -0
  124. trilogy/dialect/sql_server.py +137 -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 +769 -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 +9 -0
  146. trilogy/scripts/__init__.py +0 -0
  147. trilogy/scripts/agent.py +41 -0
  148. trilogy/scripts/agent_info.py +303 -0
  149. trilogy/scripts/common.py +355 -0
  150. trilogy/scripts/dependency/Cargo.lock +617 -0
  151. trilogy/scripts/dependency/Cargo.toml +39 -0
  152. trilogy/scripts/dependency/README.md +131 -0
  153. trilogy/scripts/dependency/build.sh +25 -0
  154. trilogy/scripts/dependency/src/directory_resolver.rs +177 -0
  155. trilogy/scripts/dependency/src/lib.rs +16 -0
  156. trilogy/scripts/dependency/src/main.rs +770 -0
  157. trilogy/scripts/dependency/src/parser.rs +435 -0
  158. trilogy/scripts/dependency/src/preql.pest +208 -0
  159. trilogy/scripts/dependency/src/python_bindings.rs +303 -0
  160. trilogy/scripts/dependency/src/resolver.rs +716 -0
  161. trilogy/scripts/dependency/tests/base.preql +3 -0
  162. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  163. trilogy/scripts/dependency/tests/customer.preql +6 -0
  164. trilogy/scripts/dependency/tests/main.preql +9 -0
  165. trilogy/scripts/dependency/tests/orders.preql +7 -0
  166. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  167. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  168. trilogy/scripts/dependency.py +323 -0
  169. trilogy/scripts/display.py +512 -0
  170. trilogy/scripts/environment.py +46 -0
  171. trilogy/scripts/fmt.py +32 -0
  172. trilogy/scripts/ingest.py +471 -0
  173. trilogy/scripts/ingest_helpers/__init__.py +1 -0
  174. trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
  175. trilogy/scripts/ingest_helpers/formatting.py +93 -0
  176. trilogy/scripts/ingest_helpers/typing.py +161 -0
  177. trilogy/scripts/init.py +105 -0
  178. trilogy/scripts/parallel_execution.py +713 -0
  179. trilogy/scripts/plan.py +189 -0
  180. trilogy/scripts/run.py +63 -0
  181. trilogy/scripts/serve.py +140 -0
  182. trilogy/scripts/serve_helpers/__init__.py +41 -0
  183. trilogy/scripts/serve_helpers/file_discovery.py +142 -0
  184. trilogy/scripts/serve_helpers/index_generation.py +206 -0
  185. trilogy/scripts/serve_helpers/models.py +38 -0
  186. trilogy/scripts/single_execution.py +131 -0
  187. trilogy/scripts/testing.py +119 -0
  188. trilogy/scripts/trilogy.py +68 -0
  189. trilogy/std/__init__.py +0 -0
  190. trilogy/std/color.preql +3 -0
  191. trilogy/std/date.preql +13 -0
  192. trilogy/std/display.preql +18 -0
  193. trilogy/std/geography.preql +22 -0
  194. trilogy/std/metric.preql +15 -0
  195. trilogy/std/money.preql +67 -0
  196. trilogy/std/net.preql +14 -0
  197. trilogy/std/ranking.preql +7 -0
  198. trilogy/std/report.preql +5 -0
  199. trilogy/std/semantic.preql +6 -0
  200. trilogy/utility.py +34 -0
@@ -0,0 +1,2521 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC
4
+ from collections import defaultdict
5
+ from dataclasses import dataclass, field
6
+ from datetime import date, datetime
7
+ from functools import cached_property, singledispatchmethod
8
+ from typing import (
9
+ TYPE_CHECKING,
10
+ Any,
11
+ Iterable,
12
+ List,
13
+ Optional,
14
+ Self,
15
+ Sequence,
16
+ Set,
17
+ Tuple,
18
+ Union,
19
+ )
20
+
21
+ from pydantic import (
22
+ ConfigDict,
23
+ )
24
+
25
+ from trilogy.constants import DEFAULT_NAMESPACE, VIRTUAL_CONCEPT_PREFIX, MagicConstants
26
+ from trilogy.core.constants import ALL_ROWS_CONCEPT
27
+ from trilogy.core.enums import (
28
+ BooleanOperator,
29
+ ComparisonOperator,
30
+ DatasourceState,
31
+ DatePart,
32
+ Derivation,
33
+ FunctionClass,
34
+ FunctionType,
35
+ Granularity,
36
+ Modifier,
37
+ Ordering,
38
+ Purpose,
39
+ WindowType,
40
+ )
41
+ from trilogy.core.models.author import (
42
+ AggregateWrapper,
43
+ AlignClause,
44
+ AlignItem,
45
+ ArgBinding,
46
+ CaseElse,
47
+ CaseWhen,
48
+ Comparison,
49
+ Concept,
50
+ ConceptRef,
51
+ Conditional,
52
+ DeriveClause,
53
+ DeriveItem,
54
+ FilterItem,
55
+ FuncArgs,
56
+ Function,
57
+ FunctionCallWrapper,
58
+ Grain,
59
+ HavingClause,
60
+ Metadata,
61
+ MultiSelectLineage,
62
+ OrderBy,
63
+ OrderItem,
64
+ Parenthetical,
65
+ RowsetItem,
66
+ RowsetLineage,
67
+ SelectLineage,
68
+ SubselectComparison,
69
+ UndefinedConcept,
70
+ WhereClause,
71
+ WindowItem,
72
+ )
73
+ from trilogy.core.models.core import (
74
+ Addressable,
75
+ ArrayType,
76
+ DataType,
77
+ DataTyped,
78
+ ListWrapper,
79
+ MapType,
80
+ MapWrapper,
81
+ NumericType,
82
+ StructType,
83
+ TraitDataType,
84
+ TupleWrapper,
85
+ arg_to_datatype,
86
+ )
87
+ from trilogy.core.models.datasource import (
88
+ Address,
89
+ ColumnAssignment,
90
+ Datasource,
91
+ DatasourceMetadata,
92
+ RawColumnExpr,
93
+ )
94
+ from trilogy.core.models.environment import Environment
95
+ from trilogy.utility import string_to_hash
96
+
97
+ # TODO: refactor to avoid these
98
+ if TYPE_CHECKING:
99
+ from trilogy.core.models.build_environment import BuildEnvironment
100
+ from trilogy.core.models.execute import CTE, UnionCTE
101
+
102
+ LOGGER_PREFIX = "[MODELS_BUILD]"
103
+
104
+
105
+ def generate_concept_name(parent: Any, debug: bool = False) -> str:
106
+ """Generate a name for a concept based on its parent type and content."""
107
+ if isinstance(parent, BuildAggregateWrapper):
108
+ if debug:
109
+ print(parent)
110
+ if parent.is_abstract:
111
+ return f"{VIRTUAL_CONCEPT_PREFIX}_agg_{parent.function.operator.value}_{string_to_hash(str(parent.with_abstract_by()))}"
112
+ return f"{VIRTUAL_CONCEPT_PREFIX}_agg_{parent.function.operator.value}_{string_to_hash(str(parent))}"
113
+ elif isinstance(parent, BuildWindowItem):
114
+ return f"{VIRTUAL_CONCEPT_PREFIX}_window_{parent.type.value}_{string_to_hash(str(parent))}"
115
+ elif isinstance(parent, BuildFilterItem):
116
+ return f"{VIRTUAL_CONCEPT_PREFIX}_filter_{string_to_hash(str(parent))}"
117
+ elif isinstance(parent, BuildFunction):
118
+ if parent.operator == FunctionType.GROUP:
119
+ return f"{VIRTUAL_CONCEPT_PREFIX}_group_to_{string_to_hash(str(parent))}"
120
+ else:
121
+ return f"{VIRTUAL_CONCEPT_PREFIX}_func_{parent.operator.value}_{string_to_hash(str(parent))}"
122
+ elif isinstance(parent, BuildParenthetical):
123
+ return f"{VIRTUAL_CONCEPT_PREFIX}_paren_{string_to_hash(str(parent))}"
124
+ elif isinstance(parent, BuildComparison):
125
+ return f"{VIRTUAL_CONCEPT_PREFIX}_comp_{string_to_hash(str(parent))}"
126
+ else: # ListWrapper, MapWrapper, or primitive types
127
+ return f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(parent))}"
128
+
129
+
130
+ class BuildConceptArgs(ABC):
131
+ @property
132
+ def concept_arguments(self) -> Sequence["BuildConcept"]:
133
+ raise NotImplementedError
134
+
135
+ @property
136
+ def rendered_concept_arguments(self) -> Sequence["BuildConcept"]:
137
+ return self.concept_arguments
138
+
139
+ @property
140
+ def existence_arguments(self) -> Sequence[tuple["BuildConcept", ...]]:
141
+ return []
142
+
143
+ @property
144
+ def row_arguments(self) -> Sequence["BuildConcept"]:
145
+ return self.concept_arguments
146
+
147
+
148
+ def concept_is_relevant(
149
+ concept: BuildConcept,
150
+ others: list[BuildConcept],
151
+ ) -> bool:
152
+
153
+ if concept.is_aggregate and not (
154
+ isinstance(concept.lineage, BuildAggregateWrapper) and concept.lineage.by
155
+ ):
156
+
157
+ return False
158
+ if concept.purpose in (Purpose.PROPERTY, Purpose.METRIC) and concept.keys:
159
+ if all([c in others for c in concept.keys]):
160
+ return False
161
+ if (
162
+ concept.purpose == Purpose.KEY
163
+ and concept.keys
164
+ and all([c in others and c != concept.address for c in concept.keys])
165
+ ):
166
+ return False
167
+ if concept.purpose in (Purpose.METRIC,):
168
+ if all([c in others for c in concept.grain.components]):
169
+ return False
170
+ if concept.derivation in (Derivation.UNNEST,):
171
+ return True
172
+ if concept.derivation in (Derivation.BASIC,):
173
+ return any(concept_is_relevant(c, others) for c in concept.concept_arguments)
174
+ if concept.granularity == Granularity.SINGLE_ROW:
175
+ return False
176
+ return True
177
+
178
+
179
+ def concepts_to_build_grain_concepts(
180
+ concepts: Iterable[BuildConcept | str], environment: "BuildEnvironment" | None
181
+ ) -> set[str]:
182
+ pconcepts: list[BuildConcept] = []
183
+ for c in concepts:
184
+ if isinstance(c, BuildConcept):
185
+ pconcepts.append(c)
186
+ elif environment:
187
+ pconcepts.append(environment.concepts[c])
188
+
189
+ else:
190
+ raise ValueError(
191
+ f"Unable to resolve input {c} without environment provided to concepts_to_grain call"
192
+ )
193
+
194
+ final: set[str] = set()
195
+ for sub in pconcepts:
196
+ if not concept_is_relevant(sub, pconcepts):
197
+ continue
198
+ final.add(sub.address)
199
+
200
+ return final
201
+
202
+
203
+ @dataclass
204
+ class LooseBuildConceptList:
205
+ concepts: Sequence[BuildConcept]
206
+
207
+ @cached_property
208
+ def addresses(self) -> set[str]:
209
+ return {s.address for s in self.concepts}
210
+
211
+ @cached_property
212
+ def sorted_addresses(self) -> List[str]:
213
+ return sorted(list(self.addresses))
214
+
215
+ def __str__(self) -> str:
216
+ return f"lcl{str(self.sorted_addresses)}"
217
+
218
+ def __iter__(self):
219
+ return iter(self.concepts)
220
+
221
+ def __eq__(self, other):
222
+ if not isinstance(other, LooseBuildConceptList):
223
+ return False
224
+ return self.addresses == other.addresses
225
+
226
+ def issubset(self, other):
227
+ if not isinstance(other, LooseBuildConceptList):
228
+ return False
229
+ return self.addresses.issubset(other.addresses)
230
+
231
+ def __contains__(self, other):
232
+ if isinstance(other, str):
233
+ return other in self.addresses
234
+ if not isinstance(other, BuildConcept):
235
+ return False
236
+ return other.address in self.addresses
237
+
238
+ def difference(self, other):
239
+ if not isinstance(other, LooseBuildConceptList):
240
+ return False
241
+ return self.addresses.difference(other.addresses)
242
+
243
+ def isdisjoint(self, other):
244
+ if not isinstance(other, LooseBuildConceptList):
245
+ return False
246
+ return self.addresses.isdisjoint(other.addresses)
247
+
248
+
249
+ @dataclass
250
+ class CanonicalBuildConceptList:
251
+ concepts: Sequence[BuildConcept]
252
+
253
+ @cached_property
254
+ def addresses(self) -> set[str]:
255
+ return {s.canonical_address for s in self.concepts}
256
+
257
+ @cached_property
258
+ def sorted_addresses(self) -> List[str]:
259
+ return sorted(list(self.addresses))
260
+
261
+ def __str__(self) -> str:
262
+ return f"lcl{str(self.sorted_addresses)}"
263
+
264
+ def __iter__(self):
265
+ return iter(self.concepts)
266
+
267
+ def __eq__(self, other):
268
+ if not isinstance(other, CanonicalBuildConceptList):
269
+ return False
270
+ return self.addresses == other.addresses
271
+
272
+ def issubset(self, other):
273
+ if not isinstance(other, CanonicalBuildConceptList):
274
+ return False
275
+ return self.addresses.issubset(other.addresses)
276
+
277
+ def __contains__(self, other):
278
+ if isinstance(other, str):
279
+ return other in self.addresses
280
+ if not isinstance(other, BuildConcept):
281
+ return False
282
+ return other.canonical_address in self.addresses
283
+
284
+ def difference(self, other):
285
+ if not isinstance(other, CanonicalBuildConceptList):
286
+ return False
287
+ return self.addresses.difference(other.addresses)
288
+
289
+ def isdisjoint(self, other):
290
+ if not isinstance(other, CanonicalBuildConceptList):
291
+ return False
292
+ return self.addresses.isdisjoint(other.addresses)
293
+
294
+
295
+ class ConstantInlineable(ABC):
296
+
297
+ def inline_constant(self, concept: BuildConcept):
298
+ raise NotImplementedError
299
+
300
+
301
+ def get_concept_row_arguments(expr) -> List["BuildConcept"]:
302
+ output = []
303
+ if isinstance(expr, BuildConcept):
304
+ output += [expr]
305
+
306
+ elif isinstance(expr, BuildConceptArgs):
307
+ output += expr.row_arguments
308
+ return output
309
+
310
+
311
+ def get_concept_arguments(expr) -> List["BuildConcept"]:
312
+ output = []
313
+ if isinstance(expr, BuildConcept):
314
+ output += [expr]
315
+
316
+ elif isinstance(
317
+ expr,
318
+ BuildConceptArgs,
319
+ ):
320
+ output += expr.concept_arguments
321
+ return output
322
+
323
+
324
+ def get_rendered_concept_arguments(expr) -> List["BuildConcept"]:
325
+ output = []
326
+ if isinstance(expr, BuildConcept):
327
+ output += [expr]
328
+
329
+ elif isinstance(
330
+ expr,
331
+ BuildConceptArgs,
332
+ ):
333
+ output += expr.rendered_concept_arguments
334
+ return output
335
+
336
+
337
+ @dataclass
338
+ class BuildParamaterizedConceptReference(DataTyped):
339
+ concept: BuildConcept
340
+
341
+ def __str__(self):
342
+ return f":{self.concept.address.replace('.', '_')}"
343
+
344
+ @property
345
+ def safe_address(self) -> str:
346
+ return self.concept.safe_address
347
+
348
+ @property
349
+ def output_datatype(self) -> DataType:
350
+ return self.concept.output_datatype
351
+
352
+
353
+ @dataclass
354
+ class BuildGrain:
355
+ components: set[str] = field(default_factory=set)
356
+ where_clause: Optional[BuildWhereClause] = None
357
+ _str: str | None = None
358
+ _str_no_condition: str | None = None
359
+
360
+ def without_condition(self):
361
+ if not self.where_clause:
362
+ return self
363
+ return BuildGrain(components=self.components)
364
+
365
+ @classmethod
366
+ def from_concepts(
367
+ cls,
368
+ concepts: Iterable[BuildConcept | str],
369
+ environment: BuildEnvironment | None = None,
370
+ where_clause: BuildWhereClause | None = None,
371
+ ) -> "BuildGrain":
372
+
373
+ return BuildGrain(
374
+ components=concepts_to_build_grain_concepts(
375
+ concepts, environment=environment
376
+ ),
377
+ where_clause=where_clause,
378
+ )
379
+
380
+ def __add__(self, other: "BuildGrain") -> "BuildGrain":
381
+ if not other:
382
+ return self
383
+ where = self.where_clause
384
+ if other.where_clause:
385
+ if not self.where_clause:
386
+ where = other.where_clause
387
+ elif not other.where_clause == self.where_clause:
388
+ where = BuildWhereClause(
389
+ conditional=BuildConditional(
390
+ left=self.where_clause.conditional,
391
+ right=other.where_clause.conditional,
392
+ operator=BooleanOperator.AND,
393
+ )
394
+ )
395
+ # raise NotImplementedError(
396
+ # f"Cannot merge grains with where clauses, self {self.where_clause} other {other.where_clause}"
397
+ # )
398
+ return BuildGrain(
399
+ components=self.components.union(other.components), where_clause=where
400
+ )
401
+
402
+ def __sub__(self, other: "BuildGrain") -> "BuildGrain":
403
+ return BuildGrain(
404
+ components=self.components.difference(other.components),
405
+ where_clause=self.where_clause,
406
+ )
407
+
408
+ @property
409
+ def abstract(self):
410
+ return not self.components or all(
411
+ [c.endswith(ALL_ROWS_CONCEPT) for c in self.components]
412
+ )
413
+
414
+ def __eq__(self, other: object):
415
+ if isinstance(other, list):
416
+ if all([isinstance(c, BuildConcept) for c in other]):
417
+ return self.components == set([c.address for c in other])
418
+ return False
419
+ if not isinstance(other, BuildGrain):
420
+ return False
421
+ if self.components == other.components:
422
+ return True
423
+ if self.abstract is True and other.abstract is True:
424
+ return True
425
+ return False
426
+
427
+ def issubset(self, other: "BuildGrain"):
428
+ return self.components.issubset(other.components)
429
+
430
+ def union(self, other: "BuildGrain"):
431
+ addresses = self.components.union(other.components)
432
+ return BuildGrain(components=addresses, where_clause=self.where_clause)
433
+
434
+ def isdisjoint(self, other: "BuildGrain"):
435
+ return self.components.isdisjoint(other.components)
436
+
437
+ def intersection(self, other: "BuildGrain") -> "BuildGrain":
438
+ intersection = self.components.intersection(other.components)
439
+ return BuildGrain(components=intersection)
440
+
441
+ def _calculate_string(self):
442
+ if self.abstract:
443
+ base = "Grain<Abstract>"
444
+ else:
445
+ base = "Grain<" + ",".join(sorted(self.components)) + ">"
446
+ if self.where_clause:
447
+ base += f"|{str(self.where_clause)}"
448
+ return base
449
+
450
+ def _calculate_string_no_condition(self):
451
+ if self.abstract:
452
+ base = "Grain<Abstract>"
453
+ else:
454
+ base = "Grain<" + ",".join(sorted(self.components)) + ">"
455
+ return base
456
+
457
+ @property
458
+ def str_no_condition(self):
459
+ if self._str_no_condition:
460
+ return self._str_no_condition
461
+ self._str_no_condition = self._calculate_string_no_condition()
462
+ return self._str_no_condition
463
+
464
+ def __str__(self):
465
+ if self._str:
466
+ return self._str
467
+ self._str = self._calculate_string()
468
+ return self._str
469
+
470
+ def __radd__(self, other) -> "BuildGrain":
471
+ if other == 0:
472
+ return self
473
+ else:
474
+ return self.__add__(other)
475
+
476
+
477
+ @dataclass
478
+ class BuildParenthetical(DataTyped, ConstantInlineable, BuildConceptArgs):
479
+ content: "BuildExpr"
480
+
481
+ def __add__(self, other) -> Union["BuildParenthetical", "BuildConditional"]:
482
+ if other is None:
483
+ return self
484
+ elif isinstance(other, (BuildComparison, BuildConditional, BuildParenthetical)):
485
+ return BuildConditional(
486
+ left=self, right=other, operator=BooleanOperator.AND
487
+ )
488
+ raise ValueError(f"Cannot add {self.__class__} and {type(other)}")
489
+
490
+ def __str__(self):
491
+ return self.__repr__()
492
+
493
+ def __repr__(self):
494
+ return f"({str(self.content)})"
495
+
496
+ def inline_constant(self, BuildConcept: BuildConcept):
497
+ return BuildParenthetical(
498
+ content=(
499
+ self.content.inline_constant(BuildConcept)
500
+ if isinstance(self.content, ConstantInlineable)
501
+ else self.content
502
+ )
503
+ )
504
+
505
+ @property
506
+ def concept_arguments(self) -> List[BuildConcept]:
507
+ return get_concept_arguments(self.content)
508
+
509
+ @property
510
+ def rendered_concept_arguments(self) -> Sequence[BuildConcept]:
511
+ return get_rendered_concept_arguments(self.content)
512
+
513
+ @property
514
+ def row_arguments(self) -> Sequence[BuildConcept]:
515
+ if isinstance(self.content, BuildConceptArgs):
516
+ return self.content.row_arguments
517
+ return self.concept_arguments
518
+
519
+ @property
520
+ def existence_arguments(self) -> Sequence[tuple["BuildConcept", ...]]:
521
+ if isinstance(self.content, BuildConceptArgs):
522
+ return self.content.existence_arguments
523
+ return []
524
+
525
+ @property
526
+ def output_datatype(self):
527
+ return arg_to_datatype(self.content)
528
+
529
+
530
+ @dataclass
531
+ class BuildConditional(DataTyped, BuildConceptArgs, ConstantInlineable):
532
+ left: Union[
533
+ int,
534
+ str,
535
+ float,
536
+ list,
537
+ bool,
538
+ MagicConstants,
539
+ BuildConcept,
540
+ BuildComparison,
541
+ BuildConditional,
542
+ BuildParenthetical,
543
+ BuildSubselectComparison,
544
+ BuildFunction,
545
+ BuildFilterItem,
546
+ ]
547
+ right: Union[
548
+ int,
549
+ str,
550
+ float,
551
+ list,
552
+ bool,
553
+ MagicConstants,
554
+ BuildConcept,
555
+ BuildComparison,
556
+ BuildConditional,
557
+ BuildParenthetical,
558
+ BuildSubselectComparison,
559
+ BuildFunction,
560
+ BuildFilterItem,
561
+ ]
562
+ operator: BooleanOperator
563
+
564
+ def __add__(self, other) -> "BuildConditional":
565
+ if other is None:
566
+ return self
567
+ elif str(other) == str(self):
568
+ return self
569
+ elif isinstance(other, (BuildComparison, BuildConditional, BuildParenthetical)):
570
+ return BuildConditional(
571
+ left=self, right=other, operator=BooleanOperator.AND
572
+ )
573
+ raise ValueError(f"Cannot add {self.__class__} and {type(other)}")
574
+
575
+ def __str__(self):
576
+ return self.__repr__()
577
+
578
+ def __repr__(self):
579
+ return f"{str(self.left)} {self.operator.value} {str(self.right)}"
580
+
581
+ def __eq__(self, other):
582
+ if not isinstance(other, BuildConditional):
583
+ return False
584
+ return (
585
+ self.left == other.left
586
+ and self.right == other.right
587
+ and self.operator == other.operator
588
+ )
589
+
590
+ def inline_constant(self, constant: BuildConcept) -> "BuildConditional":
591
+ assert isinstance(constant.lineage, BuildFunction)
592
+ new_val = constant.lineage.arguments[0]
593
+ if isinstance(self.left, ConstantInlineable):
594
+ new_left = self.left.inline_constant(constant)
595
+ elif (
596
+ isinstance(self.left, BuildConcept)
597
+ and self.left.address == constant.address
598
+ ):
599
+ new_left = new_val
600
+ else:
601
+ new_left = self.left
602
+
603
+ if isinstance(self.right, ConstantInlineable):
604
+ new_right = self.right.inline_constant(constant)
605
+ elif (
606
+ isinstance(self.right, BuildConcept)
607
+ and self.right.address == constant.address
608
+ ):
609
+ new_right = new_val
610
+ else:
611
+ new_right = self.right
612
+
613
+ if self.right == constant:
614
+ new_right = new_val
615
+
616
+ return BuildConditional(
617
+ left=new_left,
618
+ right=new_right,
619
+ operator=self.operator,
620
+ )
621
+
622
+ @property
623
+ def concept_arguments(self) -> List[BuildConcept]:
624
+ """Return BuildConcepts directly referenced in where clause"""
625
+ output = []
626
+ output += get_concept_arguments(self.left)
627
+ output += get_concept_arguments(self.right)
628
+ return output
629
+
630
+ @property
631
+ def row_arguments(self) -> List[BuildConcept]:
632
+ output = []
633
+ output += get_concept_row_arguments(self.left)
634
+ output += get_concept_row_arguments(self.right)
635
+ return output
636
+
637
+ @property
638
+ def existence_arguments(self) -> list[tuple[BuildConcept, ...]]:
639
+ output: list[tuple[BuildConcept, ...]] = []
640
+ if isinstance(self.left, BuildConceptArgs):
641
+ output += self.left.existence_arguments
642
+ if isinstance(self.right, BuildConceptArgs):
643
+ output += self.right.existence_arguments
644
+ return output
645
+
646
+ def decompose(self):
647
+ chunks = []
648
+ if self.operator == BooleanOperator.AND:
649
+ for val in [self.left, self.right]:
650
+ if isinstance(val, BuildConditional):
651
+ chunks.extend(val.decompose())
652
+ else:
653
+ chunks.append(val)
654
+ else:
655
+ chunks.append(self)
656
+ return chunks
657
+
658
+ @property
659
+ def output_datatype(self):
660
+ return DataType.BOOL
661
+
662
+
663
+ @dataclass
664
+ class BuildWhereClause(BuildConceptArgs):
665
+ conditional: Union[
666
+ BuildSubselectComparison,
667
+ BuildComparison,
668
+ BuildConditional,
669
+ BuildParenthetical,
670
+ ]
671
+
672
+ def __eq__(self, other):
673
+ if not isinstance(other, (BuildWhereClause, WhereClause)):
674
+ return False
675
+ return self.conditional == other.conditional
676
+
677
+ def __repr__(self):
678
+ return str(self.conditional)
679
+
680
+ def __str__(self):
681
+ return self.__repr__()
682
+
683
+ @property
684
+ def concept_arguments(self) -> List[BuildConcept]:
685
+ return self.conditional.concept_arguments
686
+
687
+ @property
688
+ def row_arguments(self) -> Sequence[BuildConcept]:
689
+ return self.conditional.row_arguments
690
+
691
+ @property
692
+ def existence_arguments(self) -> Sequence[tuple["BuildConcept", ...]]:
693
+ return self.conditional.existence_arguments
694
+
695
+
696
+ class BuildHavingClause(BuildWhereClause):
697
+ pass
698
+
699
+
700
+ @dataclass
701
+ class BuildComparison(DataTyped, BuildConceptArgs, ConstantInlineable):
702
+
703
+ left: Union[
704
+ int,
705
+ str,
706
+ float,
707
+ bool,
708
+ datetime,
709
+ date,
710
+ BuildFunction,
711
+ BuildConcept,
712
+ BuildConditional,
713
+ DataType,
714
+ BuildComparison,
715
+ BuildParenthetical,
716
+ MagicConstants,
717
+ BuildWindowItem,
718
+ BuildAggregateWrapper,
719
+ ListWrapper,
720
+ TupleWrapper,
721
+ ]
722
+ right: Union[
723
+ int,
724
+ str,
725
+ float,
726
+ bool,
727
+ date,
728
+ datetime,
729
+ BuildConcept,
730
+ BuildFunction,
731
+ BuildConditional,
732
+ DataType,
733
+ BuildComparison,
734
+ BuildParenthetical,
735
+ MagicConstants,
736
+ BuildWindowItem,
737
+ BuildAggregateWrapper,
738
+ TupleWrapper,
739
+ ListWrapper,
740
+ ]
741
+ operator: ComparisonOperator
742
+
743
+ def __add__(self, other):
744
+ if other is None:
745
+ return self
746
+ if not isinstance(
747
+ other, (BuildComparison, BuildConditional, BuildParenthetical)
748
+ ):
749
+ raise ValueError(f"Cannot add {type(other)} to {__class__}")
750
+ if other == self:
751
+ return self
752
+ return BuildConditional(left=self, right=other, operator=BooleanOperator.AND)
753
+
754
+ def __repr__(self):
755
+ if isinstance(self.left, BuildConcept):
756
+ left = self.left.address
757
+ else:
758
+ left = str(self.left)
759
+ if isinstance(self.right, BuildConcept):
760
+ right = self.right.address
761
+ else:
762
+ right = str(self.right)
763
+ return f"{left} {self.operator.value} {right}"
764
+
765
+ def __str__(self):
766
+ return self.__repr__()
767
+
768
+ def __eq__(self, other):
769
+ if not isinstance(other, BuildComparison):
770
+ return False
771
+ return (
772
+ self.left == other.left
773
+ and self.right == other.right
774
+ and self.operator == other.operator
775
+ )
776
+
777
+ def inline_constant(self, constant: BuildConcept):
778
+ assert isinstance(constant.lineage, BuildFunction)
779
+ new_val = constant.lineage.arguments[0]
780
+ if isinstance(self.left, ConstantInlineable):
781
+ new_left = self.left.inline_constant(constant)
782
+ elif (
783
+ isinstance(self.left, BuildConcept)
784
+ and self.left.address == constant.address
785
+ ):
786
+ new_left = new_val
787
+ else:
788
+ new_left = self.left
789
+ if isinstance(self.right, ConstantInlineable):
790
+ new_right = self.right.inline_constant(constant)
791
+ elif (
792
+ isinstance(self.right, BuildConcept)
793
+ and self.right.address == constant.address
794
+ ):
795
+ new_right = new_val
796
+ else:
797
+ new_right = self.right
798
+
799
+ return self.__class__(
800
+ left=new_left,
801
+ right=new_right,
802
+ operator=self.operator,
803
+ )
804
+
805
+ @property
806
+ def concept_arguments(self) -> List[BuildConcept]:
807
+ """Return BuildConcepts directly referenced in where clause"""
808
+ output = []
809
+ output += get_concept_arguments(self.left)
810
+ output += get_concept_arguments(self.right)
811
+ return output
812
+
813
+ @property
814
+ def row_arguments(self) -> List[BuildConcept]:
815
+ output = []
816
+ output += get_concept_row_arguments(self.left)
817
+ output += get_concept_row_arguments(self.right)
818
+ return output
819
+
820
+ @property
821
+ def existence_arguments(self) -> List[Tuple[BuildConcept, ...]]:
822
+ """Return BuildConcepts directly referenced in where clause"""
823
+ output: List[Tuple[BuildConcept, ...]] = []
824
+ if isinstance(self.left, BuildConceptArgs):
825
+ output += self.left.existence_arguments
826
+ if isinstance(self.right, BuildConceptArgs):
827
+ output += self.right.existence_arguments
828
+ return output
829
+
830
+ @property
831
+ def output_datatype(self):
832
+ return DataType.BOOL
833
+
834
+
835
+ @dataclass
836
+ class BuildSubselectComparison(BuildComparison):
837
+ left: Union[
838
+ int,
839
+ str,
840
+ float,
841
+ bool,
842
+ datetime,
843
+ date,
844
+ BuildFunction,
845
+ BuildConcept,
846
+ "BuildConditional",
847
+ DataType,
848
+ "BuildComparison",
849
+ "BuildParenthetical",
850
+ MagicConstants,
851
+ BuildWindowItem,
852
+ BuildAggregateWrapper,
853
+ ListWrapper,
854
+ TupleWrapper,
855
+ ]
856
+ right: Union[
857
+ int,
858
+ str,
859
+ float,
860
+ bool,
861
+ date,
862
+ datetime,
863
+ BuildConcept,
864
+ BuildFunction,
865
+ "BuildConditional",
866
+ DataType,
867
+ "BuildComparison",
868
+ "BuildParenthetical",
869
+ MagicConstants,
870
+ BuildWindowItem,
871
+ BuildAggregateWrapper,
872
+ TupleWrapper,
873
+ ListWrapper,
874
+ ]
875
+ operator: ComparisonOperator
876
+
877
+ def __eq__(self, other):
878
+ if not isinstance(other, BuildSubselectComparison):
879
+ return False
880
+
881
+ comp = (
882
+ self.left == other.left
883
+ and self.right == other.right
884
+ and self.operator == other.operator
885
+ )
886
+ return comp
887
+
888
+ @property
889
+ def row_arguments(self) -> List[BuildConcept]:
890
+ return get_concept_row_arguments(self.left)
891
+
892
+ @property
893
+ def existence_arguments(self) -> list[tuple["BuildConcept", ...]]:
894
+ return [tuple(get_concept_arguments(self.right))]
895
+
896
+
897
+ @dataclass
898
+ class BuildConcept(Addressable, BuildConceptArgs, DataTyped):
899
+ model_config = ConfigDict(extra="forbid")
900
+ name: str
901
+ canonical_name: str
902
+ datatype: DataType | ArrayType | StructType | MapType | NumericType | TraitDataType
903
+ purpose: Purpose
904
+ build_is_aggregate: bool
905
+ derivation: Derivation = Derivation.ROOT
906
+ granularity: Granularity = Granularity.MULTI_ROW
907
+ namespace: Optional[str] = field(default=DEFAULT_NAMESPACE)
908
+ metadata: Metadata = field(
909
+ default_factory=lambda: Metadata(description=None, line_number=None),
910
+ )
911
+ lineage: Optional[
912
+ Union[
913
+ BuildFunction,
914
+ BuildWindowItem,
915
+ BuildFilterItem,
916
+ BuildAggregateWrapper,
917
+ BuildRowsetItem,
918
+ BuildMultiSelectLineage,
919
+ ]
920
+ ] = None
921
+ keys: Optional[set[str]] = None
922
+ grain: BuildGrain = field(default=None) # type: ignore
923
+ modifiers: List[Modifier] = field(default_factory=list) # type: ignore
924
+ pseudonyms: set[str] = field(default_factory=set)
925
+
926
+ @property
927
+ def is_aggregate(self) -> bool:
928
+ return self.build_is_aggregate
929
+
930
+ @cached_property
931
+ def hash(self) -> int:
932
+ return hash(
933
+ f"{self.name}+{self.datatype}+ {self.purpose} + {str(self.lineage)} + {self.namespace} + {str(self.grain)} + {str(self.keys)}"
934
+ )
935
+
936
+ def __hash__(self):
937
+ return self.hash
938
+
939
+ def __repr__(self):
940
+ base = f"{self.address}@{self.grain}"
941
+ return base
942
+
943
+ @property
944
+ def output_datatype(self):
945
+ return self.datatype
946
+
947
+ def __eq__(self, other: object):
948
+ if isinstance(other, str):
949
+ if self.address == other:
950
+ return True
951
+ if not isinstance(other, (BuildConcept, Concept)):
952
+ return False
953
+ return (
954
+ self.name == other.name
955
+ and self.datatype == other.datatype
956
+ and self.purpose == other.purpose
957
+ and self.namespace == other.namespace
958
+ and self.grain == other.grain
959
+ # and self.keys == other.keys
960
+ )
961
+
962
+ def __str__(self):
963
+ grain = str(self.grain) if self.grain else "Grain<>"
964
+ return f"{self.namespace}.{self.name}@{grain}"
965
+
966
+ @cached_property
967
+ def address(self) -> str:
968
+ return f"{self.namespace}.{self.name}"
969
+
970
+ @cached_property
971
+ def canonical_address(self) -> str:
972
+ return f"{self.namespace}.{self.canonical_name}"
973
+
974
+ @property
975
+ def safe_address(self) -> str:
976
+ if self.namespace == DEFAULT_NAMESPACE:
977
+ return self.name.replace(".", "_")
978
+ elif self.namespace:
979
+ return f"{self.namespace.replace('.','_')}_{self.name.replace('.','_')}"
980
+ return self.name.replace(".", "_")
981
+
982
+ def with_materialized_source(self) -> Self:
983
+ return self.__class__(
984
+ name=self.name,
985
+ canonical_name=self.canonical_name,
986
+ datatype=self.datatype,
987
+ purpose=self.purpose,
988
+ metadata=self.metadata,
989
+ lineage=None,
990
+ grain=self.grain,
991
+ namespace=self.namespace,
992
+ keys=self.keys,
993
+ modifiers=self.modifiers,
994
+ pseudonyms=self.pseudonyms,
995
+ ## bound
996
+ derivation=Derivation.ROOT,
997
+ granularity=self.granularity,
998
+ build_is_aggregate=self.build_is_aggregate,
999
+ )
1000
+
1001
+ def with_grain(self, grain: Optional["BuildGrain" | BuildConcept] = None) -> Self:
1002
+ if isinstance(grain, BuildConcept):
1003
+ grain = BuildGrain(
1004
+ components=set(
1005
+ [
1006
+ grain.address,
1007
+ ]
1008
+ )
1009
+ )
1010
+ return self.__class__(
1011
+ name=self.name,
1012
+ canonical_name=self.canonical_name,
1013
+ datatype=self.datatype,
1014
+ purpose=self.purpose,
1015
+ metadata=self.metadata,
1016
+ lineage=self.lineage,
1017
+ grain=grain if grain else BuildGrain(components=set()),
1018
+ namespace=self.namespace,
1019
+ keys=self.keys,
1020
+ modifiers=self.modifiers,
1021
+ pseudonyms=self.pseudonyms,
1022
+ ## bound
1023
+ derivation=self.derivation,
1024
+ granularity=self.granularity,
1025
+ build_is_aggregate=self.build_is_aggregate,
1026
+ )
1027
+
1028
+ @property
1029
+ def _with_default_grain(self) -> Self:
1030
+ if self.purpose == Purpose.KEY:
1031
+ # we need to make this abstract
1032
+ grain = BuildGrain(components={self.address})
1033
+ elif self.purpose == Purpose.PROPERTY:
1034
+ components = []
1035
+ if self.keys:
1036
+ components = [*self.keys]
1037
+ if self.lineage:
1038
+ for item in self.lineage.concept_arguments:
1039
+ components += [x.address for x in item.sources]
1040
+ # TODO: set synonyms
1041
+ grain = BuildGrain(
1042
+ components=set([x for x in components]),
1043
+ ) # synonym_set=generate_BuildConcept_synonyms(components))
1044
+ elif self.purpose == Purpose.METRIC:
1045
+ grain = BuildGrain()
1046
+ elif self.purpose == Purpose.CONSTANT:
1047
+ if self.derivation != Derivation.CONSTANT:
1048
+ grain = BuildGrain(components={self.address})
1049
+ else:
1050
+ grain = self.grain
1051
+ else:
1052
+ grain = self.grain # type: ignore
1053
+ return self.__class__(
1054
+ name=self.name,
1055
+ canonical_name=self.canonical_name,
1056
+ datatype=self.datatype,
1057
+ purpose=self.purpose,
1058
+ metadata=self.metadata,
1059
+ lineage=self.lineage,
1060
+ grain=grain,
1061
+ keys=self.keys,
1062
+ namespace=self.namespace,
1063
+ modifiers=self.modifiers,
1064
+ pseudonyms=self.pseudonyms,
1065
+ ## bound
1066
+ derivation=self.derivation,
1067
+ granularity=self.granularity,
1068
+ build_is_aggregate=self.build_is_aggregate,
1069
+ )
1070
+
1071
+ def with_default_grain(self) -> "BuildConcept":
1072
+ return self._with_default_grain
1073
+
1074
+ @property
1075
+ def sources(self) -> List["BuildConcept"]:
1076
+ if self.lineage:
1077
+ output: List[BuildConcept] = []
1078
+
1079
+ def get_sources(
1080
+ expr: Union[
1081
+ BuildFunction,
1082
+ BuildWindowItem,
1083
+ BuildFilterItem,
1084
+ BuildAggregateWrapper,
1085
+ BuildRowsetItem,
1086
+ BuildMultiSelectLineage,
1087
+ ],
1088
+ output: List[BuildConcept],
1089
+ ):
1090
+ if isinstance(expr, BuildMultiSelectLineage):
1091
+ return
1092
+ for item in expr.concept_arguments:
1093
+ if isinstance(item, BuildConcept):
1094
+ if item.address == self.address:
1095
+ raise SyntaxError(
1096
+ f"BuildConcept {self.address} references itself"
1097
+ )
1098
+ output.append(item)
1099
+ output += item.sources
1100
+
1101
+ get_sources(self.lineage, output)
1102
+ return output
1103
+ return []
1104
+
1105
+ @property
1106
+ def concept_arguments(self) -> List[BuildConcept]:
1107
+ return self.lineage.concept_arguments if self.lineage else []
1108
+
1109
+
1110
+ @dataclass
1111
+ class BuildOrderItem(DataTyped, BuildConceptArgs):
1112
+ expr: BuildExpr
1113
+ order: Ordering
1114
+
1115
+ @property
1116
+ def concept_arguments(self) -> List[BuildConcept]:
1117
+ base: List[BuildConcept] = []
1118
+ x = self.expr
1119
+ if isinstance(x, BuildConcept):
1120
+ base += [x]
1121
+ elif isinstance(x, BuildConceptArgs):
1122
+ base += x.concept_arguments
1123
+ return base
1124
+
1125
+ @property
1126
+ def row_arguments(self) -> Sequence[BuildConcept]:
1127
+ if isinstance(self.expr, BuildConceptArgs):
1128
+ return self.expr.row_arguments
1129
+ return self.concept_arguments
1130
+
1131
+ @property
1132
+ def existence_arguments(self) -> Sequence[tuple["BuildConcept", ...]]:
1133
+ if isinstance(self.expr, BuildConceptArgs):
1134
+ return self.expr.existence_arguments
1135
+ return []
1136
+
1137
+ @property
1138
+ def output_datatype(self):
1139
+ return arg_to_datatype(self.expr)
1140
+
1141
+
1142
+ @dataclass
1143
+ class BuildWindowItem(DataTyped, BuildConceptArgs):
1144
+ type: WindowType
1145
+ content: BuildConcept
1146
+ order_by: List[BuildOrderItem]
1147
+ over: List["BuildConcept"] = field(default_factory=list)
1148
+ index: Optional[int] = None
1149
+
1150
+ def __repr__(self) -> str:
1151
+ return f"{self.type}({self.content} {self.index}, {self.over}, {self.order_by})"
1152
+
1153
+ def __str__(self):
1154
+ return self.__repr__()
1155
+
1156
+ @property
1157
+ def concept_arguments(self) -> List[BuildConcept]:
1158
+ output = [self.content]
1159
+ for order in self.order_by:
1160
+ output += order.concept_arguments
1161
+ for item in self.over:
1162
+ output += [item]
1163
+ return output
1164
+
1165
+ @property
1166
+ def output_datatype(self):
1167
+ if self.type in (WindowType.RANK, WindowType.ROW_NUMBER):
1168
+ return DataType.INTEGER
1169
+ return self.content.output_datatype
1170
+
1171
+ @property
1172
+ def output_purpose(self):
1173
+ return Purpose.PROPERTY
1174
+
1175
+
1176
+ @dataclass
1177
+ class BuildCaseWhen(DataTyped, BuildConceptArgs):
1178
+ comparison: BuildConditional | BuildSubselectComparison | BuildComparison
1179
+ expr: "BuildExpr"
1180
+
1181
+ def __str__(self):
1182
+ return self.__repr__()
1183
+
1184
+ def __repr__(self):
1185
+ return f"WHEN {str(self.comparison)} THEN {str(self.expr)}"
1186
+
1187
+ @property
1188
+ def concept_arguments(self):
1189
+ return get_concept_arguments(self.comparison) + get_concept_arguments(self.expr)
1190
+
1191
+ @property
1192
+ def concept_row_arguments(self):
1193
+ return get_concept_row_arguments(self.comparison) + get_concept_row_arguments(
1194
+ self.expr
1195
+ )
1196
+
1197
+ @property
1198
+ def output_datatype(self):
1199
+ return DataType.BOOL
1200
+
1201
+
1202
+ @dataclass
1203
+ class BuildCaseElse(DataTyped, BuildConceptArgs):
1204
+ expr: "BuildExpr"
1205
+
1206
+ @property
1207
+ def concept_arguments(self):
1208
+ return get_concept_arguments(self.expr)
1209
+
1210
+ def output_datatype(self):
1211
+ return arg_to_datatype(self.expr)
1212
+
1213
+
1214
+ @dataclass
1215
+ class BuildFunction(DataTyped, BuildConceptArgs):
1216
+ operator: FunctionType
1217
+ arguments: Sequence[
1218
+ Union[
1219
+ int,
1220
+ float,
1221
+ str,
1222
+ date,
1223
+ datetime,
1224
+ MapWrapper[Any, Any],
1225
+ TraitDataType,
1226
+ DataType,
1227
+ ArrayType,
1228
+ MapType,
1229
+ NumericType,
1230
+ DatePart,
1231
+ BuildConcept,
1232
+ BuildAggregateWrapper,
1233
+ BuildFunction,
1234
+ BuildWindowItem,
1235
+ BuildParenthetical,
1236
+ BuildCaseWhen,
1237
+ BuildCaseElse,
1238
+ list,
1239
+ ListWrapper[Any],
1240
+ ]
1241
+ ]
1242
+ output_data_type: (
1243
+ DataType | ArrayType | StructType | MapType | NumericType | TraitDataType
1244
+ )
1245
+ output_purpose: Purpose = field(default=Purpose.KEY)
1246
+ arg_count: int = field(default=1)
1247
+ valid_inputs: Optional[
1248
+ Union[
1249
+ Set[DataType],
1250
+ List[Set[DataType]],
1251
+ ]
1252
+ ] = None
1253
+
1254
+ def __repr__(self):
1255
+ return f'{self.operator.value}({",".join([str(a) for a in self.arguments])})'
1256
+
1257
+ def __str__(self):
1258
+ return self.__repr__()
1259
+
1260
+ @property
1261
+ def datatype(self):
1262
+ return self.output_data_type
1263
+
1264
+ @property
1265
+ def output_datatype(self):
1266
+ return self.output_data_type
1267
+
1268
+ @property
1269
+ def concept_arguments(self) -> List[BuildConcept]:
1270
+ base = []
1271
+ for arg in self.arguments:
1272
+ base += get_concept_arguments(arg)
1273
+ return base
1274
+
1275
+ @property
1276
+ def rendered_concept_arguments(self) -> List[BuildConcept]:
1277
+ if self.operator == FunctionType.GROUP:
1278
+ base = self.arguments[0]
1279
+ return get_rendered_concept_arguments(base)
1280
+ base = []
1281
+ for arg in self.arguments:
1282
+ base += get_rendered_concept_arguments(arg)
1283
+ return base
1284
+
1285
+ @property
1286
+ def output_grain(self):
1287
+ # aggregates have an abstract grain
1288
+ if self.operator in FunctionClass.AGGREGATE_FUNCTIONS.value:
1289
+ return BuildGrain(components=[])
1290
+ # scalars have implicit grain of all arguments
1291
+ args = set()
1292
+ for input in self.concept_arguments:
1293
+ args += input.grain.components
1294
+ return BuildGrain(components=args)
1295
+
1296
+
1297
+ @dataclass
1298
+ class BuildAggregateWrapper(BuildConceptArgs, DataTyped):
1299
+ function: BuildFunction
1300
+ by: List[BuildConcept] = field(default_factory=list)
1301
+
1302
+ def __str__(self):
1303
+ grain_str = [str(c) for c in self.by] if self.by else "abstract"
1304
+ return f"{str(self.function)}<{grain_str}>"
1305
+
1306
+ @property
1307
+ def is_abstract(self):
1308
+ if not self.by:
1309
+ return True
1310
+ if all(x.name == ALL_ROWS_CONCEPT for x in self.by):
1311
+ return True
1312
+ return False
1313
+
1314
+ def with_abstract_by(self) -> "BuildAggregateWrapper":
1315
+ return BuildAggregateWrapper(function=self.function, by=[])
1316
+
1317
+ @property
1318
+ def datatype(self):
1319
+ return self.function.datatype
1320
+
1321
+ @property
1322
+ def concept_arguments(self) -> List[BuildConcept]:
1323
+ return self.function.concept_arguments + self.by
1324
+
1325
+ @property
1326
+ def output_datatype(self):
1327
+ return self.function.output_datatype
1328
+
1329
+ @property
1330
+ def output_purpose(self):
1331
+ return self.function.output_purpose
1332
+
1333
+
1334
+ @dataclass
1335
+ class BuildFilterItem(BuildConceptArgs):
1336
+ content: "BuildExpr"
1337
+ where: BuildWhereClause
1338
+
1339
+ def __str__(self):
1340
+ return f"<Filter: {str(self.content)} where {str(self.where)}>"
1341
+
1342
+ @property
1343
+ def output_datatype(self):
1344
+ return arg_to_datatype(self.content)
1345
+
1346
+ @property
1347
+ def output_purpose(self):
1348
+ return self.content.purpose
1349
+
1350
+ @property
1351
+ def content_concept_arguments(self):
1352
+ if isinstance(self.content, BuildConcept):
1353
+ return [self.content]
1354
+ elif isinstance(self.content, BuildConceptArgs):
1355
+ return self.content.concept_arguments
1356
+ return []
1357
+
1358
+ @property
1359
+ def concept_arguments(self):
1360
+ return self.where.concept_arguments + get_concept_arguments(self.content)
1361
+
1362
+ @property
1363
+ def rendered_concept_arguments(self):
1364
+ return (
1365
+ get_rendered_concept_arguments(self.content)
1366
+ + self.where.rendered_concept_arguments
1367
+ )
1368
+
1369
+
1370
+ @dataclass
1371
+ class BuildRowsetLineage(BuildConceptArgs):
1372
+ name: str
1373
+ derived_concepts: List[str]
1374
+ select: SelectLineage | MultiSelectLineage
1375
+
1376
+
1377
+ @dataclass
1378
+ class BuildRowsetItem(DataTyped, BuildConceptArgs):
1379
+ content: BuildConcept
1380
+ rowset: BuildRowsetLineage
1381
+
1382
+ def __repr__(self):
1383
+ return f"<Rowset<{self.rowset.name}>: {str(self.content)}>"
1384
+
1385
+ def __str__(self):
1386
+ return self.__repr__()
1387
+
1388
+ @property
1389
+ def output_datatype(self):
1390
+ return self.content.datatype
1391
+
1392
+ @property
1393
+ def output_purpose(self):
1394
+ return self.content.purpose
1395
+
1396
+ @property
1397
+ def concept_arguments(self):
1398
+ return [self.content]
1399
+
1400
+
1401
+ @dataclass
1402
+ class BuildOrderBy:
1403
+ items: List[BuildOrderItem]
1404
+
1405
+ @property
1406
+ def concept_arguments(self):
1407
+ return [x.expr for x in self.items]
1408
+
1409
+
1410
+ @dataclass
1411
+ class BuildAlignClause:
1412
+ items: List[BuildAlignItem]
1413
+
1414
+
1415
+ @dataclass
1416
+ class BuildDeriveClause:
1417
+ items: List[BuildDeriveItem]
1418
+
1419
+
1420
+ @dataclass
1421
+ class BuildDeriveItem:
1422
+ expr: BuildExpr
1423
+ name: str
1424
+ namespace: str = field(default=DEFAULT_NAMESPACE)
1425
+
1426
+ @property
1427
+ def address(self) -> str:
1428
+ return f"{self.namespace}.{self.name}"
1429
+
1430
+
1431
+ @dataclass
1432
+ class BuildSelectLineage:
1433
+ selection: List[BuildConcept]
1434
+ hidden_components: set[str]
1435
+ local_concepts: dict[str, BuildConcept]
1436
+ order_by: Optional[BuildOrderBy] = None
1437
+ limit: Optional[int] = None
1438
+ meta: Metadata = field(default_factory=lambda: Metadata())
1439
+ grain: BuildGrain = field(default_factory=BuildGrain)
1440
+ where_clause: BuildWhereClause | None = field(default=None)
1441
+ having_clause: BuildHavingClause | None = field(default=None)
1442
+
1443
+ @property
1444
+ def output_components(self) -> List[BuildConcept]:
1445
+ return self.selection
1446
+
1447
+
1448
+ @dataclass
1449
+ class BuildMultiSelectLineage(BuildConceptArgs):
1450
+ selects: List[SelectLineage]
1451
+ grain: BuildGrain
1452
+ align: BuildAlignClause
1453
+ namespace: str
1454
+ local_concepts: dict[str, BuildConcept]
1455
+ build_concept_arguments: list[BuildConcept]
1456
+ build_output_components: list[BuildConcept]
1457
+ hidden_components: set[str]
1458
+ order_by: Optional[BuildOrderBy] = None
1459
+ limit: Optional[int] = None
1460
+ where_clause: Union["BuildWhereClause", None] = field(default=None)
1461
+ having_clause: Union["BuildHavingClause", None] = field(default=None)
1462
+ derive: BuildDeriveClause | None = None
1463
+
1464
+ @property
1465
+ def derived_concepts(self) -> set[str]:
1466
+ output = set()
1467
+ for item in self.align.items:
1468
+ output.add(item.aligned_concept)
1469
+ if self.derive:
1470
+ for ditem in self.derive.items:
1471
+ output.add(ditem.address)
1472
+ return output
1473
+
1474
+ @property
1475
+ def output_components(self) -> list[BuildConcept]:
1476
+ return self.build_output_components
1477
+
1478
+ @property
1479
+ def calculated_derivations(self) -> set[str]:
1480
+ output: set[str] = set()
1481
+ if not self.derive:
1482
+ return output
1483
+ for item in self.derive.items:
1484
+ output.add(item.address)
1485
+ return output
1486
+
1487
+ @property
1488
+ def concept_arguments(self) -> list[BuildConcept]:
1489
+ return self.build_concept_arguments
1490
+
1491
+ # these are needed to help disambiguate between parents
1492
+ def get_merge_concept(self, check: BuildConcept) -> str | None:
1493
+ for item in self.align.items:
1494
+ if check in item.concepts_lcl:
1495
+ return f"{item.namespace}.{item.alias}"
1496
+ return None
1497
+
1498
+ def find_source(self, concept: BuildConcept, cte: CTE | UnionCTE) -> BuildConcept:
1499
+ for x in self.align.items:
1500
+ if concept.name == x.alias:
1501
+ for c in x.concepts:
1502
+ if c.address in cte.output_lcl:
1503
+ return c
1504
+
1505
+ raise SyntaxError(
1506
+ f"Could not find upstream map for multiselect {str(concept)} on cte ({cte})"
1507
+ )
1508
+
1509
+
1510
+ @dataclass
1511
+ class BuildAlignItem:
1512
+ alias: str
1513
+ concepts: List[BuildConcept]
1514
+ namespace: str = field(default=DEFAULT_NAMESPACE)
1515
+
1516
+ @cached_property
1517
+ def concepts_lcl(self) -> LooseBuildConceptList:
1518
+ return LooseBuildConceptList(concepts=self.concepts)
1519
+
1520
+ @property
1521
+ def aligned_concept(self) -> str:
1522
+ return f"{self.namespace}.{self.alias}"
1523
+
1524
+
1525
+ @dataclass
1526
+ class BuildColumnAssignment:
1527
+ alias: str | RawColumnExpr | BuildFunction | BuildAggregateWrapper
1528
+ concept: BuildConcept
1529
+ modifiers: List[Modifier] = field(default_factory=list)
1530
+
1531
+ @property
1532
+ def is_complete(self) -> bool:
1533
+ return Modifier.PARTIAL not in self.modifiers
1534
+
1535
+ @property
1536
+ def is_nullable(self) -> bool:
1537
+ return Modifier.NULLABLE in self.modifiers
1538
+
1539
+
1540
+ @dataclass
1541
+ class BuildDatasource:
1542
+ name: str
1543
+ columns: List[BuildColumnAssignment]
1544
+ address: Union[Address, str]
1545
+ grain: BuildGrain = field(
1546
+ default_factory=lambda: BuildGrain(components=set()),
1547
+ )
1548
+ namespace: Optional[str] = field(default=DEFAULT_NAMESPACE)
1549
+ metadata: DatasourceMetadata = field(
1550
+ default_factory=lambda: DatasourceMetadata(freshness_concept=None)
1551
+ )
1552
+ where: Optional[BuildWhereClause] = None
1553
+ non_partial_for: Optional[BuildWhereClause] = None
1554
+
1555
+ def __hash__(self):
1556
+ return self.identifier.__hash__()
1557
+
1558
+ def __add__(self, other):
1559
+ if not other == self:
1560
+ raise ValueError(
1561
+ "Attempted to add two datasources that are not identical, this is not a valid operation"
1562
+ )
1563
+ return self
1564
+
1565
+ @property
1566
+ def condition(self):
1567
+ return None
1568
+
1569
+ @property
1570
+ def can_be_inlined(self) -> bool:
1571
+ if isinstance(self.address, Address) and self.address.is_query:
1572
+ return False
1573
+ return True
1574
+
1575
+ @property
1576
+ def identifier(self) -> str:
1577
+ if not self.namespace or self.namespace == DEFAULT_NAMESPACE:
1578
+ return self.name
1579
+ return f"{self.namespace}.{self.name}"
1580
+
1581
+ @property
1582
+ def safe_identifier(self) -> str:
1583
+ return self.identifier.replace(".", "_")
1584
+
1585
+ @property
1586
+ def concepts(self) -> List[BuildConcept]:
1587
+ return [c.concept for c in self.columns]
1588
+
1589
+ @property
1590
+ def group_required(self):
1591
+ return False
1592
+
1593
+ @property
1594
+ def output_concepts(self) -> List[BuildConcept]:
1595
+ return self.concepts
1596
+
1597
+ @property
1598
+ def full_concepts(self) -> List[BuildConcept]:
1599
+ return [c.concept for c in self.columns if Modifier.PARTIAL not in c.modifiers]
1600
+
1601
+ @property
1602
+ def nullable_concepts(self) -> List[BuildConcept]:
1603
+ return [c.concept for c in self.columns if Modifier.NULLABLE in c.modifiers]
1604
+
1605
+ @property
1606
+ def hidden_concepts(self) -> List[BuildConcept]:
1607
+ return [c.concept for c in self.columns if Modifier.HIDDEN in c.modifiers]
1608
+
1609
+ @property
1610
+ def partial_concepts(self) -> List[BuildConcept]:
1611
+ return [c.concept for c in self.columns if Modifier.PARTIAL in c.modifiers]
1612
+
1613
+ def get_alias(
1614
+ self,
1615
+ concept: BuildConcept,
1616
+ use_raw_name: bool = True,
1617
+ force_alias: bool = False,
1618
+ ) -> Optional[str | RawColumnExpr] | BuildFunction | BuildAggregateWrapper:
1619
+ # 2022-01-22
1620
+ # this logic needs to be refined.
1621
+ # if concept.lineage:
1622
+ # # return None
1623
+ for x in self.columns:
1624
+ if (
1625
+ x.concept.canonical_address == concept.canonical_address
1626
+ or x.concept == concept
1627
+ or x.concept.with_grain(concept.grain) == concept
1628
+ ):
1629
+ if use_raw_name:
1630
+ return x.alias
1631
+ return concept.safe_address
1632
+ existing = [str(c.concept) for c in self.columns]
1633
+ raise ValueError(
1634
+ f"{LOGGER_PREFIX} Concept {concept} not found on {self.identifier}; have"
1635
+ f" {existing}."
1636
+ )
1637
+
1638
+ @property
1639
+ def safe_location(self) -> str:
1640
+ if isinstance(self.address, Address):
1641
+ return self.address.location
1642
+ return self.address
1643
+
1644
+ @property
1645
+ def output_lcl(self) -> LooseBuildConceptList:
1646
+ return LooseBuildConceptList(concepts=self.output_concepts)
1647
+
1648
+
1649
+ BuildExpr = (
1650
+ BuildWindowItem
1651
+ | BuildFilterItem
1652
+ | BuildConcept
1653
+ | BuildComparison
1654
+ | BuildConditional
1655
+ | BuildParenthetical
1656
+ | BuildFunction
1657
+ | BuildAggregateWrapper
1658
+ | bool
1659
+ | MagicConstants
1660
+ | int
1661
+ | str
1662
+ | float
1663
+ | list
1664
+ )
1665
+
1666
+
1667
+ def get_canonical_pseudonyms(environment: Environment) -> dict[str, set[str]]:
1668
+ roots: dict[str, set[str]] = defaultdict(set)
1669
+ for k, v in environment.concepts.items():
1670
+ roots[v.address].add(k)
1671
+ for x in v.pseudonyms:
1672
+ roots[v.address].add(x)
1673
+ for k, v in environment.alias_origin_lookup.items():
1674
+ lookup = environment.concepts[k].address
1675
+ roots[lookup].add(v.address)
1676
+ for x2 in v.pseudonyms:
1677
+ roots[lookup].add(x2)
1678
+ return roots
1679
+
1680
+
1681
+ def requires_concept_nesting(
1682
+ expr,
1683
+ ) -> AggregateWrapper | WindowItem | FilterItem | Function | None:
1684
+ if isinstance(expr, (AggregateWrapper, WindowItem, FilterItem)):
1685
+ return expr
1686
+ if isinstance(expr, Function) and expr.operator in (
1687
+ FunctionType.GROUP,
1688
+ FunctionType.PARENTHETICAL,
1689
+ ):
1690
+ # group by requires nesting
1691
+ return expr
1692
+ return None
1693
+
1694
+
1695
+ class Factory:
1696
+
1697
+ def __init__(
1698
+ self,
1699
+ environment: Environment,
1700
+ local_concepts: dict[str, BuildConcept] | None = None,
1701
+ grain: Grain | None = None,
1702
+ pseudonym_map: dict[str, set[str]] | None = None,
1703
+ ):
1704
+ self.grain = grain or Grain()
1705
+ self.environment = environment
1706
+ self.local_concepts: dict[str, BuildConcept] = (
1707
+ {} if local_concepts is None else local_concepts
1708
+ )
1709
+ self.local_non_build_concepts: dict[str, Concept] = {}
1710
+ self.pseudonym_map = pseudonym_map or get_canonical_pseudonyms(environment)
1711
+ self.build_grain = self.build(self.grain) if self.grain else None
1712
+
1713
+ def instantiate_concept(
1714
+ self,
1715
+ arg: (
1716
+ AggregateWrapper
1717
+ | FunctionCallWrapper
1718
+ | WindowItem
1719
+ | FilterItem
1720
+ | Function
1721
+ | ListWrapper
1722
+ | MapWrapper
1723
+ | int
1724
+ | float
1725
+ | str
1726
+ | date
1727
+ ),
1728
+ ) -> tuple[Concept, BuildConcept]:
1729
+ from trilogy.parsing.common import arbitrary_to_concept, generate_concept_name
1730
+
1731
+ name = generate_concept_name(arg)
1732
+ if name in self.local_concepts and name in self.local_non_build_concepts:
1733
+ # if we already have this concept, return it
1734
+ return self.local_non_build_concepts[name], self.local_concepts[name]
1735
+ new = arbitrary_to_concept(
1736
+ arg,
1737
+ environment=self.environment,
1738
+ )
1739
+ built = self._build_concept(new)
1740
+ self.local_concepts[name] = built
1741
+ self.local_non_build_concepts[name] = new
1742
+ return new, built
1743
+
1744
+ @singledispatchmethod
1745
+ def build(self, base):
1746
+ raise NotImplementedError("Cannot build {}".format(type(base)))
1747
+
1748
+ @build.register
1749
+ def _(
1750
+ self,
1751
+ base: (
1752
+ int
1753
+ | str
1754
+ | float
1755
+ | list
1756
+ | date
1757
+ | TupleWrapper
1758
+ | ListWrapper
1759
+ | MagicConstants
1760
+ | MapWrapper
1761
+ | DataType
1762
+ | DatePart
1763
+ | NumericType
1764
+ ),
1765
+ ) -> (
1766
+ int
1767
+ | str
1768
+ | float
1769
+ | list
1770
+ | date
1771
+ | TupleWrapper
1772
+ | ListWrapper
1773
+ | MagicConstants
1774
+ | MapWrapper
1775
+ | DataType
1776
+ | DatePart
1777
+ | NumericType
1778
+ ):
1779
+ return self._build_primitive(base)
1780
+
1781
+ def _build_primitive(self, base):
1782
+ return base
1783
+
1784
+ @build.register
1785
+ def _(self, base: None) -> None:
1786
+ return self._build_none(base)
1787
+
1788
+ def _build_none(self, base):
1789
+ return base
1790
+
1791
+ @build.register
1792
+ def _(self, base: Function) -> BuildFunction | BuildAggregateWrapper:
1793
+ return self._build_function(base)
1794
+
1795
+ def _build_function(self, base: Function) -> BuildFunction | BuildAggregateWrapper:
1796
+ raw_args: list[Concept | FuncArgs] = []
1797
+ for arg in base.arguments:
1798
+ # to do proper discovery, we need to inject virtual intermediate concepts
1799
+ # we don't use requires_concept_nesting here by design
1800
+ if isinstance(arg, (AggregateWrapper, FilterItem, WindowItem)):
1801
+ narg, _ = self.instantiate_concept(arg)
1802
+ raw_args.append(narg)
1803
+ else:
1804
+ raw_args.append(arg)
1805
+ if base.operator == FunctionType.GROUP:
1806
+ group_base = raw_args[0]
1807
+ final_args: List[Concept | ConceptRef] = []
1808
+ if isinstance(group_base, ConceptRef):
1809
+ if group_base.address in self.environment.concepts and not isinstance(
1810
+ self.environment.concepts[group_base.address], UndefinedConcept
1811
+ ):
1812
+ group_base = self.environment.concepts[group_base.address]
1813
+ if (
1814
+ isinstance(group_base, Concept)
1815
+ and isinstance(group_base.lineage, AggregateWrapper)
1816
+ and not group_base.lineage.by
1817
+ ):
1818
+ arguments = raw_args[1:]
1819
+ for x in arguments:
1820
+ if isinstance(x, (ConceptRef, Concept)):
1821
+ final_args.append(x)
1822
+ else:
1823
+ # constants, etc, can be ignored for group
1824
+ continue
1825
+ _, rval = self.instantiate_concept(
1826
+ AggregateWrapper(
1827
+ function=group_base.lineage.function,
1828
+ by=final_args,
1829
+ )
1830
+ )
1831
+
1832
+ return BuildFunction(
1833
+ operator=base.operator,
1834
+ arguments=[
1835
+ rval,
1836
+ *[self.handle_constant(self.build(c)) for c in raw_args[1:]],
1837
+ ],
1838
+ output_data_type=base.output_datatype,
1839
+ output_purpose=base.output_purpose,
1840
+ valid_inputs=base.valid_inputs,
1841
+ arg_count=base.arg_count,
1842
+ )
1843
+ new = BuildFunction(
1844
+ operator=base.operator,
1845
+ arguments=[self.handle_constant(self.build(c)) for c in raw_args],
1846
+ output_data_type=base.output_datatype,
1847
+ output_purpose=base.output_purpose,
1848
+ valid_inputs=base.valid_inputs,
1849
+ arg_count=base.arg_count,
1850
+ )
1851
+ return new
1852
+
1853
+ @build.register
1854
+ def _(self, base: ConceptRef) -> BuildConcept:
1855
+ return self._build_concept_ref(base)
1856
+
1857
+ def _build_concept_ref(self, base: ConceptRef) -> BuildConcept:
1858
+ if base.address in self.local_concepts:
1859
+ full = self.local_concepts[base.address]
1860
+ if isinstance(full, BuildConcept):
1861
+ return full
1862
+ if base.address in self.environment.concepts:
1863
+ raw = self.environment.concepts[base.address]
1864
+ return self._build_concept(raw)
1865
+ # this will error by design - TODO - more helpful message?
1866
+ return self._build_concept(self.environment.concepts[base.address])
1867
+
1868
+ @build.register
1869
+ def _(self, base: CaseWhen) -> BuildCaseWhen:
1870
+ return self._build_case_when(base)
1871
+
1872
+ def _build_case_when(self, base: CaseWhen) -> BuildCaseWhen:
1873
+ expr: Concept | FuncArgs = base.expr
1874
+ validation = requires_concept_nesting(expr)
1875
+ if validation:
1876
+ expr, _ = self.instantiate_concept(validation)
1877
+ return BuildCaseWhen(
1878
+ comparison=self.build(base.comparison),
1879
+ expr=self.build(expr),
1880
+ )
1881
+
1882
+ @build.register
1883
+ def _(self, base: CaseElse) -> BuildCaseElse:
1884
+ return self._build_case_else(base)
1885
+
1886
+ def _build_case_else(self, base: CaseElse) -> BuildCaseElse:
1887
+ expr: Concept | FuncArgs = base.expr
1888
+ validation = requires_concept_nesting(expr)
1889
+ if validation:
1890
+ expr, _ = self.instantiate_concept(validation)
1891
+ return BuildCaseElse(expr=self.build(expr))
1892
+
1893
+ @build.register
1894
+ def _(self, base: Concept) -> BuildConcept:
1895
+ return self._build_concept(base)
1896
+
1897
+ def _build_concept(self, base: Concept) -> BuildConcept:
1898
+ try:
1899
+ return self.__build_concept(base)
1900
+ except RecursionError as e:
1901
+ raise RecursionError(
1902
+ f"Recursion error building concept {base.address} with grain {base.grain} and lineage {base.lineage}. This is likely due to a circular reference."
1903
+ ) from e
1904
+
1905
+ def __build_concept(self, base: Concept) -> BuildConcept:
1906
+ # TODO: if we are using parameters, wrap it in a new model and use that in rendering
1907
+ # this doesn't work for persisted addresses.
1908
+ # we need to early exit if we have it in local concepts, because in that case,
1909
+ # it is built with appropriate grain only in that dictionary
1910
+ # if base.address in self.local_concepts:
1911
+ # return self.local_concepts[base.address]
1912
+ new_lineage, final_grain, _ = base.get_select_grain_and_keys(
1913
+ self.grain, self.environment
1914
+ )
1915
+ if new_lineage:
1916
+ build_lineage = self.build(new_lineage)
1917
+ else:
1918
+ build_lineage = None
1919
+ derivation = Concept.calculate_derivation(build_lineage, base.purpose)
1920
+
1921
+ granularity = Concept.calculate_granularity(
1922
+ derivation, final_grain, build_lineage
1923
+ )
1924
+
1925
+ def calculate_is_aggregate(lineage):
1926
+ if lineage and isinstance(lineage, BuildFunction):
1927
+ if lineage.operator in FunctionClass.AGGREGATE_FUNCTIONS.value:
1928
+ return True
1929
+ if (
1930
+ lineage
1931
+ and isinstance(lineage, BuildAggregateWrapper)
1932
+ and lineage.function.operator in FunctionClass.AGGREGATE_FUNCTIONS.value
1933
+ ):
1934
+ return True
1935
+ return False
1936
+
1937
+ is_aggregate = calculate_is_aggregate(build_lineage)
1938
+ # if this is a pseudonym, we need to look up the base address
1939
+ if base.address in self.environment.alias_origin_lookup:
1940
+ lookup_address = self.environment.concepts[base.address].address
1941
+ # map only to the canonical concept, not to other merged concepts
1942
+ base_pseudonyms = {lookup_address}
1943
+ else:
1944
+ base_pseudonyms = {
1945
+ x
1946
+ for x in self.pseudonym_map.get(base.address, set())
1947
+ if x != base.address
1948
+ }
1949
+
1950
+ rval = BuildConcept(
1951
+ name=base.name,
1952
+ canonical_name=(
1953
+ generate_concept_name(build_lineage) if build_lineage else base.name
1954
+ ),
1955
+ datatype=base.datatype,
1956
+ purpose=base.purpose,
1957
+ metadata=base.metadata,
1958
+ lineage=build_lineage,
1959
+ grain=self._build_grain(final_grain),
1960
+ namespace=base.namespace,
1961
+ keys=base.keys,
1962
+ modifiers=base.modifiers,
1963
+ pseudonyms=base_pseudonyms,
1964
+ ## instantiated values
1965
+ derivation=derivation,
1966
+ granularity=granularity,
1967
+ build_is_aggregate=is_aggregate,
1968
+ )
1969
+ if base.address in self.local_concepts:
1970
+ # comp = self.local_concepts[base.address]
1971
+ # if comp.canonical_address != rval.canonical_address:
1972
+ # raise ValueError(
1973
+ # f"Conflicting concepts for address {base.address}: {comp.canonical_address} vs {rval.canonical_address} {comp.lineage} vs {rval.lineage}"
1974
+ # )
1975
+ return self.local_concepts[base.address]
1976
+ self.local_concepts[base.address] = rval
1977
+ return rval
1978
+
1979
+ @build.register
1980
+ def _(self, base: AggregateWrapper) -> BuildAggregateWrapper:
1981
+ return self._build_aggregate_wrapper(base)
1982
+
1983
+ def _build_aggregate_wrapper(self, base: AggregateWrapper) -> BuildAggregateWrapper:
1984
+ if not base.by:
1985
+ by = [
1986
+ self._build_concept(self.environment.concepts[c])
1987
+ for c in self.grain.components
1988
+ ]
1989
+ else:
1990
+ by = [self.build(x) for x in base.by]
1991
+
1992
+ parent: BuildFunction = self._build_function(base.function) # type: ignore
1993
+ return BuildAggregateWrapper(
1994
+ function=parent, by=sorted(by, key=lambda x: x.address)
1995
+ )
1996
+
1997
+ @build.register
1998
+ def _(self, base: ColumnAssignment) -> BuildColumnAssignment:
1999
+ return self._build_column_assignment(base)
2000
+
2001
+ def _build_column_assignment(self, base: ColumnAssignment) -> BuildColumnAssignment:
2002
+ address = base.concept.address
2003
+ fetched = (
2004
+ self._build_concept(
2005
+ self.environment.alias_origin_lookup[address]
2006
+ ).with_grain(self.build_grain)
2007
+ if address in self.environment.alias_origin_lookup
2008
+ else self._build_concept(self.environment.concepts[address]).with_grain(
2009
+ self.build_grain
2010
+ )
2011
+ )
2012
+
2013
+ return BuildColumnAssignment(
2014
+ alias=(
2015
+ self._build_function(base.alias)
2016
+ if isinstance(base.alias, Function)
2017
+ else base.alias
2018
+ ),
2019
+ concept=fetched,
2020
+ modifiers=base.modifiers,
2021
+ )
2022
+
2023
+ @build.register
2024
+ def _(self, base: OrderBy) -> BuildOrderBy:
2025
+ return self._build_order_by(base)
2026
+
2027
+ def _build_order_by(self, base: OrderBy) -> BuildOrderBy:
2028
+ return BuildOrderBy(items=[self._build_order_item(x) for x in base.items])
2029
+
2030
+ @build.register
2031
+ def _(self, base: FunctionCallWrapper) -> BuildExpr:
2032
+ return self._build_function_call_wrapper(base)
2033
+
2034
+ def _build_function_call_wrapper(self, base: FunctionCallWrapper) -> BuildExpr:
2035
+ # function calls are kept around purely for the parse tree
2036
+ # so discard at the build point
2037
+ return self.build(base.content)
2038
+
2039
+ @build.register
2040
+ def _(self, base: OrderItem) -> BuildOrderItem:
2041
+ return self._build_order_item(base)
2042
+
2043
+ def _build_order_item(self, base: OrderItem) -> BuildOrderItem:
2044
+ bexpr: Any
2045
+ validation = requires_concept_nesting(base.expr)
2046
+ if validation:
2047
+ bexpr, _ = self.instantiate_concept(validation)
2048
+ else:
2049
+ bexpr = base.expr
2050
+ return BuildOrderItem(
2051
+ expr=(self.build(bexpr)),
2052
+ order=base.order,
2053
+ )
2054
+
2055
+ @build.register
2056
+ def _(self, base: WhereClause) -> BuildWhereClause:
2057
+ return self._build_where_clause(base)
2058
+
2059
+ def _build_where_clause(self, base: WhereClause) -> BuildWhereClause:
2060
+ return BuildWhereClause(conditional=self.build(base.conditional))
2061
+
2062
+ @build.register
2063
+ def _(self, base: HavingClause) -> BuildHavingClause:
2064
+ return self._build_having_clause(base)
2065
+
2066
+ def _build_having_clause(self, base: HavingClause) -> BuildHavingClause:
2067
+ return BuildHavingClause(conditional=self.build(base.conditional))
2068
+
2069
+ @build.register
2070
+ def _(self, base: WindowItem) -> BuildWindowItem:
2071
+ return self._build_window_item(base)
2072
+
2073
+ def _build_window_item(self, base: WindowItem) -> BuildWindowItem:
2074
+ content: Concept | FuncArgs = base.content
2075
+ validation = requires_concept_nesting(base.content)
2076
+ if validation:
2077
+ content, _ = self.instantiate_concept(validation)
2078
+ final_by = []
2079
+ for x in base.order_by:
2080
+ if (
2081
+ isinstance(x.expr, AggregateWrapper)
2082
+ and not x.expr.by
2083
+ and isinstance(content, (ConceptRef, Concept))
2084
+ ):
2085
+ x.expr.by = [content]
2086
+ final_by.append(x)
2087
+ return BuildWindowItem(
2088
+ type=base.type,
2089
+ content=self.build(content),
2090
+ order_by=[self.build(x) for x in final_by],
2091
+ over=[self._build_concept_ref(x) for x in base.over],
2092
+ index=base.index,
2093
+ )
2094
+
2095
+ @build.register
2096
+ def _(self, base: Conditional) -> BuildConditional:
2097
+ return self._build_conditional(base)
2098
+
2099
+ def _build_conditional(self, base: Conditional) -> BuildConditional:
2100
+ return BuildConditional(
2101
+ left=self.handle_constant(self.build(base.left)),
2102
+ right=self.handle_constant(self.build(base.right)),
2103
+ operator=base.operator,
2104
+ )
2105
+
2106
+ @build.register
2107
+ def _(self, base: SubselectComparison) -> BuildSubselectComparison:
2108
+ return self._build_subselect_comparison(base)
2109
+
2110
+ def _build_subselect_comparison(
2111
+ self, base: SubselectComparison
2112
+ ) -> BuildSubselectComparison:
2113
+ right: Any = base.right
2114
+ # this has specialized logic - include all Functions
2115
+ if isinstance(base.right, (AggregateWrapper, WindowItem, FilterItem, Function)):
2116
+ right_c, _ = self.instantiate_concept(base.right)
2117
+ right = right_c
2118
+ return BuildSubselectComparison(
2119
+ left=self.handle_constant(self.build(base.left)),
2120
+ right=self.handle_constant(self.build(right)),
2121
+ operator=base.operator,
2122
+ )
2123
+
2124
+ @build.register
2125
+ def _(self, base: Comparison) -> BuildComparison:
2126
+ return self._build_comparison(base)
2127
+
2128
+ def _build_comparison(self, base: Comparison) -> BuildComparison:
2129
+ left = base.left
2130
+ validation = requires_concept_nesting(base.left)
2131
+ if validation:
2132
+ left_c, _ = self.instantiate_concept(validation)
2133
+ left = left_c # type: ignore
2134
+ right = base.right
2135
+ validation = requires_concept_nesting(base.right)
2136
+ if validation:
2137
+ right_c, _ = self.instantiate_concept(validation)
2138
+ right = right_c # type: ignore
2139
+ return BuildComparison(
2140
+ left=self.handle_constant(self.build(left)),
2141
+ right=self.handle_constant(self.build(right)),
2142
+ operator=base.operator,
2143
+ )
2144
+
2145
+ @build.register
2146
+ def _(self, base: AlignItem) -> BuildAlignItem:
2147
+ return self._build_align_item(base)
2148
+
2149
+ def _build_align_item(self, base: AlignItem) -> BuildAlignItem:
2150
+ return BuildAlignItem(
2151
+ alias=base.alias,
2152
+ concepts=[self._build_concept_ref(x) for x in base.concepts],
2153
+ namespace=base.namespace,
2154
+ )
2155
+
2156
+ @build.register
2157
+ def _(self, base: AlignClause) -> BuildAlignClause:
2158
+ return self._build_align_clause(base)
2159
+
2160
+ def _build_align_clause(self, base: AlignClause) -> BuildAlignClause:
2161
+ return BuildAlignClause(items=[self._build_align_item(x) for x in base.items])
2162
+
2163
+ @build.register
2164
+ def _(self, base: DeriveItem) -> BuildDeriveItem:
2165
+ return self._build_derive_item(base)
2166
+
2167
+ def _build_derive_item(self, base: DeriveItem) -> BuildDeriveItem:
2168
+ expr: Concept | FuncArgs = base.expr
2169
+ validation = requires_concept_nesting(expr)
2170
+ if validation:
2171
+ expr, _ = self.instantiate_concept(validation)
2172
+ return BuildDeriveItem(
2173
+ expr=self.build(expr),
2174
+ name=base.name,
2175
+ namespace=base.namespace,
2176
+ )
2177
+
2178
+ @build.register
2179
+ def _(self, base: DeriveClause) -> BuildDeriveClause:
2180
+ return self._build_derive_clause(base)
2181
+
2182
+ def _build_derive_clause(self, base: DeriveClause) -> BuildDeriveClause:
2183
+ return BuildDeriveClause(items=[self.build(x) for x in base.items])
2184
+
2185
+ @build.register
2186
+ def _(self, base: RowsetItem) -> BuildRowsetItem:
2187
+ return self._build_rowset_item(base)
2188
+
2189
+ def _build_rowset_item(self, base: RowsetItem) -> BuildRowsetItem:
2190
+ factory = Factory(
2191
+ environment=self.environment,
2192
+ local_concepts={},
2193
+ grain=base.rowset.select.grain,
2194
+ pseudonym_map=self.pseudonym_map,
2195
+ )
2196
+ return BuildRowsetItem(
2197
+ content=factory._build_concept_ref(base.content),
2198
+ rowset=factory._build_rowset_lineage(base.rowset),
2199
+ )
2200
+
2201
+ @build.register
2202
+ def _(self, base: RowsetLineage) -> BuildRowsetLineage:
2203
+ return self._build_rowset_lineage(base)
2204
+
2205
+ def _build_rowset_lineage(self, base: RowsetLineage) -> BuildRowsetLineage:
2206
+ out = BuildRowsetLineage(
2207
+ name=base.name,
2208
+ derived_concepts=[x.address for x in base.derived_concepts],
2209
+ select=base.select,
2210
+ )
2211
+ return out
2212
+
2213
+ @build.register
2214
+ def _(self, base: Grain) -> BuildGrain:
2215
+ return self._build_grain(base)
2216
+
2217
+ def _build_grain(self, base: Grain) -> BuildGrain:
2218
+ if base.where_clause:
2219
+ factory = Factory(
2220
+ environment=self.environment, pseudonym_map=self.pseudonym_map
2221
+ )
2222
+ where = factory._build_where_clause(base.where_clause)
2223
+ else:
2224
+ where = None
2225
+ return BuildGrain(components=base.components, where_clause=where)
2226
+
2227
+ @build.register
2228
+ def _(self, base: TupleWrapper) -> TupleWrapper:
2229
+ return self._build_tuple_wrapper(base)
2230
+
2231
+ def _build_tuple_wrapper(self, base: TupleWrapper) -> TupleWrapper:
2232
+ return TupleWrapper(val=[self.build(x) for x in base.val], type=base.type)
2233
+
2234
+ @build.register
2235
+ def _(self, base: ListWrapper) -> ListWrapper:
2236
+ return self._build_list_wrapper(base)
2237
+
2238
+ def _build_list_wrapper(self, base: ListWrapper) -> ListWrapper:
2239
+ return ListWrapper([self.build(x) for x in base], type=base.type)
2240
+
2241
+ @build.register
2242
+ def _(self, base: FilterItem) -> BuildFilterItem:
2243
+ return self._build_filter_item(base)
2244
+
2245
+ def _build_filter_item(self, base: FilterItem) -> BuildFilterItem:
2246
+ if isinstance(
2247
+ base.content, (Function, AggregateWrapper, WindowItem, FilterItem)
2248
+ ):
2249
+ _, built = self.instantiate_concept(base.content)
2250
+ return BuildFilterItem(content=built, where=self.build(base.where))
2251
+ return BuildFilterItem(
2252
+ content=self.build(base.content), where=self.build(base.where)
2253
+ )
2254
+
2255
+ @build.register
2256
+ def _(self, base: Parenthetical) -> BuildParenthetical:
2257
+ return self._build_parenthetical(base)
2258
+
2259
+ def _build_parenthetical(self, base: Parenthetical) -> BuildParenthetical:
2260
+ validate = requires_concept_nesting(base.content)
2261
+ if validate:
2262
+ content, _ = self.instantiate_concept(validate)
2263
+ return BuildParenthetical(content=self.build(content))
2264
+ else:
2265
+ return BuildParenthetical(content=self.build(base.content))
2266
+
2267
+ @build.register
2268
+ def _(self, base: SelectLineage) -> BuildSelectLineage:
2269
+ return self._build_select_lineage(base)
2270
+
2271
+ def _build_select_lineage(self, base: SelectLineage) -> BuildSelectLineage:
2272
+ from trilogy.core.models.build import (
2273
+ BuildSelectLineage,
2274
+ Factory,
2275
+ )
2276
+
2277
+ materialized: dict[str, BuildConcept] = {}
2278
+ factory = Factory(
2279
+ grain=base.grain,
2280
+ environment=self.environment,
2281
+ local_concepts=materialized,
2282
+ pseudonym_map=self.pseudonym_map,
2283
+ )
2284
+ for k, v in base.local_concepts.items():
2285
+
2286
+ materialized[k] = factory.build(v)
2287
+ where_factory = Factory(
2288
+ grain=Grain(),
2289
+ environment=self.environment,
2290
+ local_concepts={},
2291
+ pseudonym_map=self.pseudonym_map,
2292
+ )
2293
+ where_clause = (
2294
+ where_factory.build(base.where_clause) if base.where_clause else None
2295
+ )
2296
+ # if the where clause derives new concepts
2297
+ # we need to ensure these are accessible from the general factory
2298
+ # post resolution
2299
+ for bk, bv in where_factory.local_concepts.items():
2300
+ # but do not override any local cahced grains
2301
+ if bk in materialized:
2302
+ continue
2303
+ materialized[bk] = bv
2304
+ final: List[BuildConcept] = []
2305
+ for original in base.selection:
2306
+ new = original
2307
+ # we don't know the grain of an aggregate at assignment time
2308
+ # so rebuild at this point in the tree
2309
+ # TODO: simplify
2310
+ if new.address in materialized:
2311
+ built = materialized[new.address]
2312
+ else:
2313
+ # Sometimes cached values here don't have the latest info
2314
+ # but we can't just use environment, as it might not have the right grain.
2315
+ built = factory.build(new)
2316
+ materialized[new.address] = built
2317
+ final.append(built)
2318
+ return BuildSelectLineage(
2319
+ selection=final,
2320
+ hidden_components=base.hidden_components,
2321
+ order_by=(factory.build(base.order_by) if base.order_by else None),
2322
+ limit=base.limit,
2323
+ meta=base.meta,
2324
+ local_concepts=materialized,
2325
+ grain=self.build(base.grain),
2326
+ having_clause=(
2327
+ factory.build(base.having_clause) if base.having_clause else None
2328
+ ),
2329
+ # this uses a different grain factory
2330
+ where_clause=where_clause,
2331
+ )
2332
+
2333
+ @build.register
2334
+ def _(self, base: MultiSelectLineage) -> BuildMultiSelectLineage:
2335
+ return self._build_multi_select_lineage(base)
2336
+
2337
+ def _build_multi_select_lineage(
2338
+ self, base: MultiSelectLineage
2339
+ ) -> BuildMultiSelectLineage:
2340
+ local_build_cache: dict[str, BuildConcept] = {}
2341
+
2342
+ parents: list[BuildSelectLineage] = [self.build(x) for x in base.selects]
2343
+ base_local = parents[0].local_concepts
2344
+
2345
+ for select in parents[1:]:
2346
+ for k, v in select.local_concepts.items():
2347
+ base_local[k] = v
2348
+
2349
+ # this requires custom handling to avoid circular dependencies
2350
+ final_grain = self.build(base.grain)
2351
+ derived_base = []
2352
+ for k in base.derived_concepts:
2353
+ base_concept = self.environment.concepts[k]
2354
+ x = BuildConcept(
2355
+ name=base_concept.name,
2356
+ canonical_name=base_concept.name,
2357
+ datatype=base_concept.datatype,
2358
+ purpose=base_concept.purpose,
2359
+ build_is_aggregate=False,
2360
+ derivation=Derivation.MULTISELECT,
2361
+ lineage=None,
2362
+ grain=final_grain,
2363
+ namespace=base_concept.namespace,
2364
+ )
2365
+ local_build_cache[k] = x
2366
+ derived_base.append(x)
2367
+ all_input: list[BuildConcept] = []
2368
+ for parent in parents:
2369
+ all_input += parent.output_components
2370
+ all_output: list[BuildConcept] = derived_base + all_input
2371
+ final: list[BuildConcept] = [
2372
+ x for x in all_output if x.address not in base.hidden_components
2373
+ ]
2374
+ factory = Factory(
2375
+ grain=base.grain,
2376
+ environment=self.environment,
2377
+ local_concepts=local_build_cache,
2378
+ pseudonym_map=self.pseudonym_map,
2379
+ )
2380
+ where_factory = Factory(
2381
+ environment=self.environment, pseudonym_map=self.pseudonym_map
2382
+ )
2383
+ lineage = BuildMultiSelectLineage(
2384
+ # we don't build selects here; they'll be built automatically in query discovery
2385
+ selects=base.selects,
2386
+ grain=final_grain,
2387
+ align=factory.build(base.align),
2388
+ derive=factory.build(base.derive) if base.derive else None,
2389
+ # self.align.with_select_context(
2390
+ # local_build_cache, self.grain, environment
2391
+ # ),
2392
+ namespace=base.namespace,
2393
+ hidden_components=base.hidden_components,
2394
+ order_by=factory.build(base.order_by) if base.order_by else None,
2395
+ limit=base.limit,
2396
+ where_clause=(
2397
+ where_factory.build(base.where_clause) if base.where_clause else None
2398
+ ),
2399
+ having_clause=(
2400
+ factory.build(base.having_clause) if base.having_clause else None
2401
+ ),
2402
+ local_concepts=base_local,
2403
+ build_output_components=final,
2404
+ build_concept_arguments=all_input,
2405
+ )
2406
+ for k in base.derived_concepts:
2407
+ local_build_cache[k].lineage = lineage
2408
+ return lineage
2409
+
2410
+ @build.register
2411
+ def _(self, base: Environment):
2412
+ return self._build_environment(base)
2413
+
2414
+ def _build_environment(self, base: Environment):
2415
+ from trilogy.core.models.build_environment import BuildEnvironment
2416
+
2417
+ new = BuildEnvironment(
2418
+ namespace=base.namespace,
2419
+ cte_name_map=base.cte_name_map,
2420
+ )
2421
+
2422
+ for k, v in base.concepts.items():
2423
+ v_build = self._build_concept(v)
2424
+ new.concepts[k] = v_build
2425
+ new.canonical_concepts[v_build.canonical_address] = v_build
2426
+ for (
2427
+ k,
2428
+ d,
2429
+ ) in base.datasources.items():
2430
+ if d.status != DatasourceState.PUBLISHED:
2431
+ continue
2432
+ new.datasources[k] = self._build_datasource(d)
2433
+ for k, a in base.alias_origin_lookup.items():
2434
+ a_build = self._build_concept(a)
2435
+ new.alias_origin_lookup[k] = a_build
2436
+ new.canonical_concepts[a_build.canonical_address] = a_build
2437
+ # add in anything that was built as a side-effect
2438
+ for bk, bv in self.local_concepts.items():
2439
+ if bk not in new.concepts:
2440
+ new.concepts[bk] = bv
2441
+ new.canonical_concepts[bv.canonical_address] = bv
2442
+ new.gen_concept_list_caches()
2443
+ return new
2444
+
2445
+ @build.register
2446
+ def _(self, base: TraitDataType):
2447
+ return self._build_trait_data_type(base)
2448
+
2449
+ def _build_trait_data_type(self, base: TraitDataType):
2450
+ return base
2451
+
2452
+ @build.register
2453
+ def _(self, base: ArrayType):
2454
+ return self._build_array_type(base)
2455
+
2456
+ def _build_array_type(self, base: ArrayType):
2457
+ return base
2458
+
2459
+ @build.register
2460
+ def _(self, base: StructType):
2461
+ return self._build_struct_type(base)
2462
+
2463
+ def _build_struct_type(self, base: StructType):
2464
+ return base
2465
+
2466
+ @build.register
2467
+ def _(self, base: MapType):
2468
+ return self._build_map_type(base)
2469
+
2470
+ def _build_map_type(self, base: MapType):
2471
+ return base
2472
+
2473
+ @build.register
2474
+ def _(self, base: ArgBinding):
2475
+ return self._build_arg_binding(base)
2476
+
2477
+ def _build_arg_binding(self, base: ArgBinding):
2478
+ return base
2479
+
2480
+ @build.register
2481
+ def _(self, base: Ordering):
2482
+ return self._build_ordering(base)
2483
+
2484
+ def _build_ordering(self, base: Ordering):
2485
+ return base
2486
+
2487
+ @build.register
2488
+ def _(self, base: Datasource):
2489
+ return self._build_datasource(base)
2490
+
2491
+ def _build_datasource(self, base: Datasource):
2492
+ local_cache: dict[str, BuildConcept] = {}
2493
+ factory = Factory(
2494
+ grain=base.grain,
2495
+ environment=self.environment,
2496
+ local_concepts=local_cache,
2497
+ pseudonym_map=self.pseudonym_map,
2498
+ )
2499
+ return BuildDatasource(
2500
+ name=base.name,
2501
+ columns=[factory._build_column_assignment(c) for c in base.columns],
2502
+ address=base.address,
2503
+ grain=factory._build_grain(base.grain),
2504
+ namespace=base.namespace,
2505
+ metadata=base.metadata,
2506
+ where=(factory.build(base.where) if base.where else None),
2507
+ non_partial_for=(
2508
+ factory.build(base.non_partial_for) if base.non_partial_for else None
2509
+ ),
2510
+ )
2511
+
2512
+ def handle_constant(self, base):
2513
+ if (
2514
+ isinstance(base, BuildConcept)
2515
+ and isinstance(base.lineage, BuildFunction)
2516
+ and base.lineage.operator == FunctionType.CONSTANT
2517
+ ):
2518
+ return BuildParamaterizedConceptReference(concept=base)
2519
+ elif isinstance(base, ConceptRef):
2520
+ return self.handle_constant(self.build(base))
2521
+ return base