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,315 @@
1
+ from typing import List
2
+
3
+ from trilogy.constants import logger
4
+ from trilogy.core.models.build import (
5
+ BuildConcept,
6
+ BuildFilterItem,
7
+ BuildWhereClause,
8
+ )
9
+ from trilogy.core.models.build_environment import BuildEnvironment
10
+ from trilogy.core.processing.node_generators.common import (
11
+ resolve_filter_parent_concepts,
12
+ )
13
+ from trilogy.core.processing.nodes import (
14
+ FilterNode,
15
+ History,
16
+ MergeNode,
17
+ SelectNode,
18
+ StrategyNode,
19
+ )
20
+ from trilogy.core.processing.utility import is_scalar_condition, padding, unique
21
+
22
+ LOGGER_PREFIX = "[GEN_FILTER_NODE]"
23
+
24
+ FILTER_TYPES = (BuildFilterItem,)
25
+
26
+
27
+ def pushdown_filter_to_parent(
28
+ local_optional: List[BuildConcept],
29
+ conditions: BuildWhereClause | None,
30
+ filter_where: BuildWhereClause,
31
+ same_filter_optional: list[BuildConcept],
32
+ depth: int,
33
+ ) -> bool:
34
+ optimized_pushdown = False
35
+ if not is_scalar_condition(filter_where.conditional):
36
+ optimized_pushdown = False
37
+ elif not local_optional:
38
+ optimized_pushdown = True
39
+ elif conditions and conditions == filter_where:
40
+ logger.info(
41
+ f"{padding(depth)}{LOGGER_PREFIX} query conditions are the same as filter conditions, can optimize across all concepts"
42
+ )
43
+ optimized_pushdown = True
44
+ elif same_filter_optional == local_optional:
45
+ logger.info(
46
+ f"{padding(depth)}{LOGGER_PREFIX} all optional concepts are included in the filter, can optimize across all concepts"
47
+ )
48
+ optimized_pushdown = True
49
+
50
+ return optimized_pushdown
51
+
52
+
53
+ def build_parent_concepts(
54
+ concept: BuildConcept,
55
+ environment: BuildEnvironment,
56
+ local_optional: List[BuildConcept],
57
+ conditions: BuildWhereClause | None = None,
58
+ depth: int = 0,
59
+ ) -> tuple[
60
+ list[BuildConcept],
61
+ list[tuple[BuildConcept, ...]],
62
+ list[BuildConcept],
63
+ bool,
64
+ bool,
65
+ ]:
66
+ parent_row_concepts, parent_existence_concepts = resolve_filter_parent_concepts(
67
+ concept, environment
68
+ )
69
+ if not isinstance(concept.lineage, FILTER_TYPES):
70
+ raise SyntaxError('Filter node must have a filter type lineage"')
71
+ filter_where = concept.lineage.where
72
+
73
+ same_filter_optional: list[BuildConcept] = []
74
+ # mypy struggled here? we shouldn't need explicit bools
75
+ global_filter_is_local_filter: bool = (
76
+ True if (conditions and conditions == filter_where) else False
77
+ )
78
+
79
+ exact_partial_matches = True
80
+ for x in local_optional:
81
+ if isinstance(x.lineage, FILTER_TYPES):
82
+ if set([x.address for x in x.lineage.where.concept_arguments]) == set(
83
+ [x.address for x in filter_where.concept_arguments]
84
+ ):
85
+ exact_partial_matches = (
86
+ exact_partial_matches and x.lineage.where == filter_where
87
+ )
88
+ logger.info(
89
+ f"{padding(depth)}{LOGGER_PREFIX} fetching parents for peer {x.address} (of {concept.address})"
90
+ )
91
+
92
+ for arg in x.lineage.content_concept_arguments:
93
+ if arg.address not in parent_row_concepts:
94
+ parent_row_concepts.append(arg)
95
+ same_filter_optional.append(x)
96
+ continue
97
+ elif global_filter_is_local_filter:
98
+ same_filter_optional.append(x)
99
+ # also append it to the parent row concepts
100
+ parent_row_concepts.append(x)
101
+
102
+ # sometimes, it's okay to include other local optional above the filter
103
+ # in case it is, prep our list
104
+ extra_row_level_optional: list[BuildConcept] = []
105
+
106
+ for x in local_optional:
107
+ if x.address in same_filter_optional:
108
+ continue
109
+ extra_row_level_optional.append(x)
110
+ is_optimized_pushdown = exact_partial_matches and pushdown_filter_to_parent(
111
+ local_optional, conditions, filter_where, same_filter_optional, depth
112
+ )
113
+ if not is_optimized_pushdown:
114
+ parent_row_concepts += extra_row_level_optional
115
+ return (
116
+ parent_row_concepts,
117
+ parent_existence_concepts,
118
+ same_filter_optional,
119
+ is_optimized_pushdown,
120
+ global_filter_is_local_filter,
121
+ )
122
+
123
+
124
+ def add_existence_sources(
125
+ core_parent_nodes: list[StrategyNode],
126
+ parent_existence_concepts: list[tuple[BuildConcept, ...]],
127
+ source_concepts,
128
+ environment,
129
+ g,
130
+ depth,
131
+ history,
132
+ ):
133
+ for existence_tuple in parent_existence_concepts:
134
+ if not existence_tuple:
135
+ continue
136
+ logger.info(
137
+ f"{padding(depth)}{LOGGER_PREFIX} fetching filter node existence parents {[x.address for x in existence_tuple]}"
138
+ )
139
+ parent_existence = source_concepts(
140
+ mandatory_list=list(existence_tuple),
141
+ environment=environment,
142
+ g=g,
143
+ depth=depth + 1,
144
+ history=history,
145
+ )
146
+ if not parent_existence:
147
+ logger.info(
148
+ f"{padding(depth)}{LOGGER_PREFIX} filter existence node parents could not be found"
149
+ )
150
+ return None
151
+ core_parent_nodes.append(parent_existence)
152
+
153
+
154
+ def gen_filter_node(
155
+ concept: BuildConcept,
156
+ local_optional: List[BuildConcept],
157
+ environment: BuildEnvironment,
158
+ g,
159
+ depth: int,
160
+ source_concepts,
161
+ history: History | None = None,
162
+ conditions: BuildWhereClause | None = None,
163
+ ) -> StrategyNode | None:
164
+ if not isinstance(concept.lineage, FILTER_TYPES):
165
+ raise SyntaxError('Filter node must have a filter type lineage"')
166
+ where = concept.lineage.where
167
+
168
+ (
169
+ parent_row_concepts,
170
+ parent_existence_concepts,
171
+ same_filter_optional,
172
+ optimized_pushdown,
173
+ global_filter_is_local_filter,
174
+ ) = build_parent_concepts(
175
+ concept,
176
+ environment=environment,
177
+ local_optional=local_optional,
178
+ conditions=conditions,
179
+ depth=depth,
180
+ )
181
+
182
+ row_parent: StrategyNode = source_concepts(
183
+ mandatory_list=parent_row_concepts,
184
+ environment=environment,
185
+ g=g,
186
+ depth=depth + 1,
187
+ history=history,
188
+ conditions=conditions,
189
+ )
190
+
191
+ core_parent_nodes: list[StrategyNode] = []
192
+ flattened_existence = [x for y in parent_existence_concepts for x in y]
193
+ if parent_existence_concepts:
194
+ add_existence_sources(
195
+ core_parent_nodes,
196
+ parent_existence_concepts,
197
+ source_concepts,
198
+ environment,
199
+ g,
200
+ depth,
201
+ history,
202
+ )
203
+
204
+ if not row_parent:
205
+ logger.info(
206
+ f"{padding(depth)}{LOGGER_PREFIX} filter node row parents {[x.address for x in parent_row_concepts]} could not be found"
207
+ )
208
+ return None
209
+ else:
210
+ logger.info(
211
+ f"{padding(depth)}{LOGGER_PREFIX} filter node has row parents {[x.address for x in parent_row_concepts]} from node with output [{[x.address for x in row_parent.output_concepts]}] partial {row_parent.partial_concepts}"
212
+ )
213
+ if global_filter_is_local_filter:
214
+ logger.info(
215
+ f"{padding(depth)}{LOGGER_PREFIX} filter node conditions match global conditions adding row parent {row_parent.output_concepts} with condition {where.conditional}"
216
+ )
217
+ row_parent.add_parents(core_parent_nodes)
218
+ # all local optional will be in the parent already, so we can set outputs
219
+ row_parent.set_output_concepts([concept] + local_optional)
220
+ return row_parent
221
+ if optimized_pushdown:
222
+ logger.info(
223
+ f"{padding(depth)}{LOGGER_PREFIX} returning optimized filter node with pushdown to parent with condition {where.conditional} across {[concept] + same_filter_optional + row_parent.output_concepts} "
224
+ )
225
+ if isinstance(row_parent, SelectNode):
226
+ logger.info(
227
+ f"{padding(depth)}{LOGGER_PREFIX} nesting select node in strategy node"
228
+ )
229
+ parent = StrategyNode(
230
+ input_concepts=row_parent.output_concepts,
231
+ output_concepts=[concept]
232
+ + same_filter_optional
233
+ + row_parent.output_concepts,
234
+ environment=row_parent.environment,
235
+ parents=[row_parent],
236
+ depth=row_parent.depth,
237
+ partial_concepts=row_parent.partial_concepts,
238
+ force_group=False,
239
+ )
240
+ else:
241
+ parent = row_parent
242
+ parent.add_output_concepts([concept] + same_filter_optional)
243
+ parent.add_parents(core_parent_nodes)
244
+ if not parent.preexisting_conditions == where.conditional:
245
+ parent.add_condition(where.conditional)
246
+ parent.add_existence_concepts(flattened_existence, False)
247
+ # parent.grain = BuildGrain.from_concepts(
248
+ # parent.output_concepts,
249
+ # environment=environment,
250
+ # )
251
+ parent.rebuild_cache()
252
+ filter_node = parent
253
+ else:
254
+ core_parent_nodes.append(row_parent)
255
+ filter_node = FilterNode(
256
+ input_concepts=unique(
257
+ parent_row_concepts + flattened_existence,
258
+ "address",
259
+ ),
260
+ output_concepts=[concept] + same_filter_optional + parent_row_concepts,
261
+ environment=environment,
262
+ parents=core_parent_nodes,
263
+ preexisting_conditions=conditions.conditional if conditions else None,
264
+ )
265
+
266
+ if not local_optional or all(
267
+ [x.address in filter_node.output_concepts for x in local_optional]
268
+ ):
269
+ optional_outputs = [
270
+ x for x in filter_node.output_concepts if x.address in local_optional
271
+ ]
272
+ logger.info(
273
+ f"{padding(depth)}{LOGGER_PREFIX} no extra enrichment needed for filter node, has all of {[x.address for x in local_optional]}"
274
+ )
275
+ filter_node.set_output_concepts(
276
+ [
277
+ concept,
278
+ ]
279
+ + optional_outputs
280
+ )
281
+ return filter_node
282
+ logger.info(
283
+ f"{padding(depth)}{LOGGER_PREFIX} need to enrich filter node with additional concepts {[x.address for x in local_optional if x.address not in filter_node.output_concepts]}"
284
+ )
285
+ enrich_node: StrategyNode = source_concepts( # this fetches the parent + join keys
286
+ # to then connect to the rest of the query
287
+ mandatory_list=parent_row_concepts
288
+ + [x for x in local_optional if x.address not in filter_node.output_concepts],
289
+ environment=environment,
290
+ g=g,
291
+ depth=depth + 1,
292
+ history=history,
293
+ conditions=conditions,
294
+ )
295
+ if not enrich_node:
296
+ logger.error(
297
+ f"{padding(depth)}{LOGGER_PREFIX} filter node enrichment node could not be found"
298
+ )
299
+ return filter_node
300
+ logger.info(
301
+ f"{padding(depth)}{LOGGER_PREFIX} returning filter node and enrich node with {enrich_node.output_concepts} and {enrich_node.input_concepts}"
302
+ )
303
+ return MergeNode(
304
+ input_concepts=filter_node.output_concepts + enrich_node.output_concepts,
305
+ output_concepts=[
306
+ concept,
307
+ ]
308
+ + local_optional,
309
+ environment=environment,
310
+ parents=[
311
+ filter_node,
312
+ enrich_node,
313
+ ],
314
+ preexisting_conditions=conditions.conditional if conditions else None,
315
+ )
@@ -0,0 +1,213 @@
1
+ from typing import List
2
+
3
+ from trilogy.constants import logger
4
+ from trilogy.core.internal import ALL_ROWS_CONCEPT
5
+ from trilogy.core.models.build import (
6
+ BuildAggregateWrapper,
7
+ BuildConcept,
8
+ BuildFunction,
9
+ BuildGrain,
10
+ BuildWhereClause,
11
+ LooseBuildConceptList,
12
+ )
13
+ from trilogy.core.models.build_environment import BuildEnvironment
14
+ from trilogy.core.processing.node_generators.common import (
15
+ gen_enrichment_node,
16
+ resolve_function_parent_concepts,
17
+ )
18
+ from trilogy.core.processing.nodes import GroupNode, History, StrategyNode
19
+ from trilogy.core.processing.utility import create_log_lambda, padding
20
+ from trilogy.utility import unique
21
+
22
+ LOGGER_PREFIX = "[GEN_GROUP_NODE]"
23
+
24
+
25
+ def get_aggregate_grain(
26
+ concept: BuildConcept, environment: BuildEnvironment
27
+ ) -> BuildGrain:
28
+ parent_concepts: List[BuildConcept] = unique(
29
+ resolve_function_parent_concepts(concept, environment=environment), "address"
30
+ )
31
+
32
+ if (
33
+ concept.grain
34
+ and len(concept.grain.components) > 0
35
+ and not concept.grain.abstract
36
+ ):
37
+ grain_components = [environment.concepts[c] for c in concept.grain.components]
38
+ parent_concepts += grain_components
39
+ return BuildGrain.from_concepts(parent_concepts)
40
+ else:
41
+ return BuildGrain.from_concepts(parent_concepts)
42
+
43
+
44
+ def gen_group_node(
45
+ concept: BuildConcept,
46
+ local_optional: List[BuildConcept],
47
+ environment: BuildEnvironment,
48
+ g,
49
+ depth: int,
50
+ source_concepts,
51
+ history: History,
52
+ conditions: BuildWhereClause | None = None,
53
+ ) -> StrategyNode | None:
54
+ # aggregates MUST always group to the proper grain
55
+ # except when the
56
+ parent_concepts: List[BuildConcept] = unique(
57
+ resolve_function_parent_concepts(concept, environment=environment), "address"
58
+ )
59
+ logger.info(
60
+ f"{padding(depth)}{LOGGER_PREFIX} parent concepts for {concept} {concept.lineage} are {[x.address for x in parent_concepts]} from group grain {concept.grain}"
61
+ )
62
+
63
+ # if the aggregation has a grain, we need to ensure these are the ONLY optional in the output of the select
64
+ output_concepts = [concept]
65
+ grain_components = [environment.concepts[c] for c in concept.grain.components]
66
+ if (
67
+ concept.grain
68
+ and len(concept.grain.components) > 0
69
+ and not concept.grain.abstract
70
+ ):
71
+
72
+ parent_concepts += grain_components
73
+ build_grain_parents = get_aggregate_grain(concept, environment)
74
+ output_concepts += grain_components
75
+ for possible_agg in local_optional:
76
+
77
+ if not isinstance(
78
+ possible_agg.lineage,
79
+ (BuildAggregateWrapper, BuildFunction),
80
+ ):
81
+ continue
82
+ if possible_agg.grain and possible_agg.grain != concept.grain:
83
+ logger.info(
84
+ f"{padding(depth)}{LOGGER_PREFIX} mismatched equivalent group by with grain {possible_agg.grain} for {concept.address}"
85
+ )
86
+
87
+ if possible_agg.grain and possible_agg.grain == concept.grain:
88
+ agg_parents: List[BuildConcept] = resolve_function_parent_concepts(
89
+ possible_agg,
90
+ environment=environment,
91
+ )
92
+ comp_grain = get_aggregate_grain(possible_agg, environment)
93
+ if set([x.address for x in agg_parents]).issubset(
94
+ set([x.address for x in parent_concepts])
95
+ ):
96
+ output_concepts.append(possible_agg)
97
+ logger.info(
98
+ f"{padding(depth)}{LOGGER_PREFIX} found equivalent group by optional concept {possible_agg.address} for {concept.address}"
99
+ )
100
+ elif comp_grain == build_grain_parents:
101
+ extra = [x for x in agg_parents if x.address not in parent_concepts]
102
+ parent_concepts += extra
103
+ output_concepts.append(possible_agg)
104
+ logger.info(
105
+ f"{padding(depth)}{LOGGER_PREFIX} found equivalent group by optional concept {possible_agg.address} for {concept.address}"
106
+ )
107
+ else:
108
+ logger.info(
109
+ f"{padding(depth)}{LOGGER_PREFIX} cannot include optional agg {possible_agg.address}; it has mismatched parent grain {comp_grain } vs local parent {build_grain_parents}"
110
+ )
111
+ elif concept.grain.abstract:
112
+ for possible_agg in local_optional:
113
+ if not isinstance(
114
+ possible_agg.lineage,
115
+ (BuildAggregateWrapper, BuildFunction),
116
+ ):
117
+
118
+ continue
119
+ logger.info(
120
+ f"{padding(depth)}{LOGGER_PREFIX} considering optional agg {possible_agg.address} for {concept.address}"
121
+ )
122
+ agg_parents = resolve_function_parent_concepts(
123
+ possible_agg,
124
+ environment=environment,
125
+ )
126
+ comp_grain = get_aggregate_grain(possible_agg, environment)
127
+ if not possible_agg.grain.abstract:
128
+ continue
129
+ if set([x.address for x in agg_parents]) == set(
130
+ [x.address for x in parent_concepts]
131
+ ):
132
+ output_concepts.append(possible_agg)
133
+ logger.info(
134
+ f"{padding(depth)}{LOGGER_PREFIX} found equivalent group by optional concept {possible_agg.address} for {concept.address}"
135
+ )
136
+ elif comp_grain == get_aggregate_grain(concept, environment):
137
+ extra = [x for x in agg_parents if x.address not in parent_concepts]
138
+ parent_concepts += extra
139
+ output_concepts.append(possible_agg)
140
+ logger.info(
141
+ f"{padding(depth)}{LOGGER_PREFIX} found equivalent group by optional concept {possible_agg.address} for {concept.address}"
142
+ )
143
+ else:
144
+ logger.info(
145
+ f"{padding(depth)}{LOGGER_PREFIX} cannot include optional agg {possible_agg.address}; it has mismatched parent grain {comp_grain } vs local parent {get_aggregate_grain(concept, environment)}"
146
+ )
147
+ if parent_concepts:
148
+ target_grain = BuildGrain.from_concepts(parent_concepts)
149
+ logger.info(
150
+ f"{padding(depth)}{LOGGER_PREFIX} fetching group node parents {LooseBuildConceptList(concepts=parent_concepts)} with expected grain {target_grain}"
151
+ )
152
+ parent_concepts = unique(
153
+ [x for x in parent_concepts if not x.name == ALL_ROWS_CONCEPT], "address"
154
+ )
155
+ parent: StrategyNode | None = source_concepts(
156
+ mandatory_list=parent_concepts,
157
+ environment=environment,
158
+ g=g,
159
+ depth=depth,
160
+ history=history,
161
+ conditions=conditions,
162
+ )
163
+ if not parent:
164
+ logger.info(
165
+ f"{padding(depth)}{LOGGER_PREFIX} group by node parents unresolvable"
166
+ )
167
+ return None
168
+ parents: List[StrategyNode] = [parent]
169
+ else:
170
+ parents = []
171
+
172
+ group_node = GroupNode(
173
+ output_concepts=output_concepts,
174
+ input_concepts=parent_concepts,
175
+ environment=environment,
176
+ parents=parents,
177
+ depth=depth,
178
+ preexisting_conditions=conditions.conditional if conditions else None,
179
+ required_outputs=parent_concepts,
180
+ )
181
+
182
+ # early exit if no optional
183
+
184
+ if not local_optional:
185
+ logger.info(
186
+ f"{padding(depth)}{LOGGER_PREFIX} no optional concepts, returning group node"
187
+ )
188
+ return group_node
189
+ missing_optional = [
190
+ x.address for x in local_optional if x.address not in group_node.usable_outputs
191
+ ]
192
+ if not missing_optional:
193
+ logger.info(
194
+ f"{padding(depth)}{LOGGER_PREFIX} no extra enrichment needed for group node, has all of {[x.address for x in local_optional]}"
195
+ )
196
+ return group_node
197
+ logger.info(
198
+ f"{padding(depth)}{LOGGER_PREFIX} group node for {concept.address} requires enrichment, missing {missing_optional}"
199
+ )
200
+ return gen_enrichment_node(
201
+ group_node,
202
+ join_keys=grain_components,
203
+ local_optional=local_optional,
204
+ environment=environment,
205
+ g=g,
206
+ depth=depth,
207
+ source_concepts=source_concepts,
208
+ log_lambda=create_log_lambda(
209
+ LOGGER_PREFIX + f" for {concept.address}", depth, logger
210
+ ),
211
+ history=history,
212
+ conditions=conditions,
213
+ )
@@ -0,0 +1,117 @@
1
+ from typing import List
2
+
3
+ from trilogy.constants import logger
4
+ from trilogy.core.models.build import BuildConcept, BuildFunction, BuildWhereClause
5
+ from trilogy.core.models.build_environment import BuildEnvironment
6
+
7
+ # C:\Users\ethan\coding_projects\pytrilogy\trilogy\core\processing\node_generators\group_to_node.py
8
+ from trilogy.core.processing.nodes import (
9
+ GroupNode,
10
+ History,
11
+ MergeNode,
12
+ StrategyNode,
13
+ )
14
+ from trilogy.core.processing.utility import padding
15
+
16
+ LOGGER_PREFIX = "[GEN_GROUP_TO_NODE]"
17
+
18
+
19
+ def gen_group_to_node(
20
+ concept: BuildConcept,
21
+ local_optional,
22
+ environment: BuildEnvironment,
23
+ g,
24
+ depth: int,
25
+ source_concepts,
26
+ history: History | None = None,
27
+ conditions: BuildWhereClause | None = None,
28
+ ) -> GroupNode | MergeNode:
29
+ # aggregates MUST always group to the proper grain
30
+ if not isinstance(concept.lineage, BuildFunction):
31
+ raise SyntaxError(
32
+ f"Group to should have function lineage, is {type(concept.lineage)}"
33
+ )
34
+
35
+ parent_concepts: List[BuildConcept] = concept.lineage.concept_arguments
36
+ root = parent_concepts[0]
37
+ logger.info(
38
+ f"{padding(depth)}{LOGGER_PREFIX} group by node has required parents {[x.address for x in parent_concepts]}"
39
+ )
40
+ parents: List[StrategyNode] = [
41
+ source_concepts(
42
+ mandatory_list=parent_concepts,
43
+ environment=environment,
44
+ g=g,
45
+ depth=depth + 1,
46
+ history=history,
47
+ conditions=conditions,
48
+ )
49
+ ]
50
+ outputs = parent_concepts + [concept]
51
+ group_node = GroupNode(
52
+ output_concepts=parent_concepts + [concept],
53
+ input_concepts=parent_concepts,
54
+ environment=environment,
55
+ parents=parents,
56
+ depth=depth,
57
+ preexisting_conditions=conditions.conditional if conditions else None,
58
+ hidden_concepts=set(
59
+ [
60
+ x.address
61
+ for x in outputs
62
+ if x.address not in local_optional
63
+ and x.address != concept.address
64
+ and x.address != root.address
65
+ ]
66
+ ),
67
+ )
68
+
69
+ # early exit if no optional
70
+ missing_local_option: list[BuildConcept] = [
71
+ x for x in local_optional if x not in group_node.output_concepts
72
+ ]
73
+ if not missing_local_option:
74
+ logger.info(
75
+ f"{padding(depth)}{LOGGER_PREFIX} no missing local optional required, returning group node only."
76
+ )
77
+ return group_node
78
+
79
+ logger.info(
80
+ f"{padding(depth)}{LOGGER_PREFIX} group by node is missing required optional {[x.address for x in missing_local_option]}"
81
+ )
82
+
83
+ # the keys we group by
84
+ # are what we can use for enrichment
85
+ enrich_node = source_concepts( # this fetches the parent + join keys
86
+ # to then connect to the rest of the query
87
+ mandatory_list=parent_concepts + local_optional,
88
+ environment=environment,
89
+ g=g,
90
+ depth=depth + 1,
91
+ history=history,
92
+ conditions=conditions,
93
+ )
94
+ if not enrich_node:
95
+ logger.info(
96
+ f"{padding(depth)}{LOGGER_PREFIX} group by node enrich node, returning group node only."
97
+ )
98
+ return group_node
99
+ logger.info(
100
+ f"{padding(depth)}{LOGGER_PREFIX} returning group to node with enrichment."
101
+ )
102
+ return MergeNode(
103
+ input_concepts=[concept]
104
+ + local_optional
105
+ + [x for x in parent_concepts if x.address != concept.address],
106
+ output_concepts=[concept] + local_optional,
107
+ environment=environment,
108
+ parents=[
109
+ # this node gets the group
110
+ group_node,
111
+ # this node gets enrichment
112
+ enrich_node,
113
+ ],
114
+ whole_grain=True,
115
+ depth=depth,
116
+ preexisting_conditions=conditions.conditional if conditions else None,
117
+ )