pytrilogy 0.0.2.58__py3-none-any.whl → 0.0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/METADATA +9 -2
  2. pytrilogy-0.0.3.0.dist-info/RECORD +99 -0
  3. {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/WHEEL +1 -1
  4. trilogy/__init__.py +2 -2
  5. trilogy/core/enums.py +1 -7
  6. trilogy/core/env_processor.py +17 -5
  7. trilogy/core/environment_helpers.py +11 -25
  8. trilogy/core/exceptions.py +4 -0
  9. trilogy/core/functions.py +695 -261
  10. trilogy/core/graph_models.py +10 -10
  11. trilogy/core/internal.py +11 -2
  12. trilogy/core/models/__init__.py +0 -0
  13. trilogy/core/models/author.py +2110 -0
  14. trilogy/core/models/build.py +1845 -0
  15. trilogy/core/models/build_environment.py +151 -0
  16. trilogy/core/models/core.py +370 -0
  17. trilogy/core/models/datasource.py +297 -0
  18. trilogy/core/models/environment.py +696 -0
  19. trilogy/core/models/execute.py +931 -0
  20. trilogy/core/optimization.py +14 -16
  21. trilogy/core/optimizations/base_optimization.py +1 -1
  22. trilogy/core/optimizations/inline_constant.py +6 -6
  23. trilogy/core/optimizations/inline_datasource.py +17 -11
  24. trilogy/core/optimizations/predicate_pushdown.py +17 -16
  25. trilogy/core/processing/concept_strategies_v3.py +180 -145
  26. trilogy/core/processing/graph_utils.py +1 -1
  27. trilogy/core/processing/node_generators/basic_node.py +19 -18
  28. trilogy/core/processing/node_generators/common.py +50 -44
  29. trilogy/core/processing/node_generators/filter_node.py +26 -13
  30. trilogy/core/processing/node_generators/group_node.py +26 -21
  31. trilogy/core/processing/node_generators/group_to_node.py +11 -8
  32. trilogy/core/processing/node_generators/multiselect_node.py +60 -43
  33. trilogy/core/processing/node_generators/node_merge_node.py +76 -38
  34. trilogy/core/processing/node_generators/rowset_node.py +57 -36
  35. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +27 -34
  36. trilogy/core/processing/node_generators/select_merge_node.py +161 -64
  37. trilogy/core/processing/node_generators/select_node.py +13 -13
  38. trilogy/core/processing/node_generators/union_node.py +12 -11
  39. trilogy/core/processing/node_generators/unnest_node.py +9 -7
  40. trilogy/core/processing/node_generators/window_node.py +19 -16
  41. trilogy/core/processing/nodes/__init__.py +21 -18
  42. trilogy/core/processing/nodes/base_node.py +82 -66
  43. trilogy/core/processing/nodes/filter_node.py +19 -13
  44. trilogy/core/processing/nodes/group_node.py +50 -35
  45. trilogy/core/processing/nodes/merge_node.py +45 -36
  46. trilogy/core/processing/nodes/select_node_v2.py +53 -39
  47. trilogy/core/processing/nodes/union_node.py +5 -7
  48. trilogy/core/processing/nodes/unnest_node.py +7 -11
  49. trilogy/core/processing/nodes/window_node.py +9 -4
  50. trilogy/core/processing/utility.py +103 -75
  51. trilogy/core/query_processor.py +65 -47
  52. trilogy/core/statements/__init__.py +0 -0
  53. trilogy/core/statements/author.py +413 -0
  54. trilogy/core/statements/build.py +0 -0
  55. trilogy/core/statements/common.py +30 -0
  56. trilogy/core/statements/execute.py +42 -0
  57. trilogy/dialect/base.py +146 -106
  58. trilogy/dialect/common.py +9 -10
  59. trilogy/dialect/duckdb.py +1 -1
  60. trilogy/dialect/enums.py +4 -2
  61. trilogy/dialect/presto.py +1 -1
  62. trilogy/dialect/sql_server.py +1 -1
  63. trilogy/executor.py +44 -32
  64. trilogy/hooks/base_hook.py +6 -4
  65. trilogy/hooks/query_debugger.py +110 -93
  66. trilogy/parser.py +1 -1
  67. trilogy/parsing/common.py +303 -64
  68. trilogy/parsing/parse_engine.py +263 -617
  69. trilogy/parsing/render.py +50 -26
  70. trilogy/scripts/trilogy.py +2 -1
  71. pytrilogy-0.0.2.58.dist-info/RECORD +0 -87
  72. trilogy/core/models.py +0 -4960
  73. {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/LICENSE.md +0 -0
  74. {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/entry_points.txt +0 -0
  75. {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/top_level.txt +0 -0
@@ -3,15 +3,14 @@ from itertools import combinations
3
3
  from typing import List
4
4
 
5
5
  from trilogy.constants import logger
6
- from trilogy.core.enums import BooleanOperator, JoinType, Purpose
7
- from trilogy.core.models import (
8
- Concept,
9
- Conditional,
10
- Environment,
11
- Grain,
12
- MultiSelectStatement,
13
- WhereClause,
6
+ from trilogy.core.enums import JoinType, Purpose
7
+ from trilogy.core.models.build import (
8
+ BuildConcept,
9
+ BuildGrain,
10
+ BuildMultiSelectLineage,
11
+ BuildWhereClause,
14
12
  )
13
+ from trilogy.core.models.build_environment import BuildEnvironment
15
14
  from trilogy.core.processing.node_generators.common import resolve_join_order
16
15
  from trilogy.core.processing.nodes import History, MergeNode, NodeJoin
17
16
  from trilogy.core.processing.nodes.base_node import StrategyNode
@@ -21,12 +20,14 @@ LOGGER_PREFIX = "[GEN_MULTISELECT_NODE]"
21
20
 
22
21
 
23
22
  def extra_align_joins(
24
- base: MultiSelectStatement, parents: List[StrategyNode]
23
+ base: BuildMultiSelectLineage,
24
+ environment: BuildEnvironment,
25
+ parents: List[StrategyNode],
25
26
  ) -> List[NodeJoin]:
26
27
  node_merge_concept_map = defaultdict(list)
27
28
  output = []
28
29
  for align in base.align.items:
29
- jc = align.gen_concept(base)
30
+ jc = environment.concepts[align.aligned_concept]
30
31
  if jc.purpose == Purpose.CONSTANT:
31
32
  continue
32
33
  for node in parents:
@@ -52,51 +53,43 @@ def extra_align_joins(
52
53
 
53
54
 
54
55
  def gen_multiselect_node(
55
- concept: Concept,
56
- local_optional: List[Concept],
57
- environment: Environment,
56
+ concept: BuildConcept,
57
+ local_optional: List[BuildConcept],
58
+ environment: BuildEnvironment,
58
59
  g,
59
60
  depth: int,
60
61
  source_concepts,
61
- history: History | None = None,
62
- conditions: WhereClause | None = None,
62
+ history: History,
63
+ conditions: BuildWhereClause | None = None,
63
64
  ) -> MergeNode | None:
64
- if not isinstance(concept.lineage, MultiSelectStatement):
65
+ from trilogy.core.query_processor import get_query_node
66
+
67
+ if not isinstance(concept.lineage, BuildMultiSelectLineage):
65
68
  logger.info(
66
69
  f"{padding(depth)}{LOGGER_PREFIX} Cannot generate multiselect node for {concept}"
67
70
  )
68
71
  return None
69
- lineage: MultiSelectStatement = concept.lineage
72
+ lineage: BuildMultiSelectLineage = concept.lineage
70
73
 
71
74
  base_parents: List[StrategyNode] = []
72
75
  partial = []
73
76
  for select in lineage.selects:
74
- snode: StrategyNode = source_concepts(
75
- mandatory_list=select.output_components,
76
- environment=environment,
77
- g=g,
78
- depth=depth + 1,
79
- history=history,
80
- conditions=select.where_clause,
77
+
78
+ snode: StrategyNode = get_query_node(history.base_environment, select)
79
+ # raise SyntaxError(select.output_components)
80
+ logger.info(
81
+ f"{padding(depth)}{LOGGER_PREFIX} Fetched parent node with outputs {select.output_components}"
81
82
  )
82
83
  if not snode:
83
84
  logger.info(
84
85
  f"{padding(depth)}{LOGGER_PREFIX} Cannot generate multiselect node for {concept}"
85
86
  )
86
87
  return None
87
- if select.having_clause:
88
- if snode.conditions:
89
- snode.conditions = Conditional(
90
- left=snode.conditions,
91
- right=select.having_clause.conditional,
92
- operator=BooleanOperator.AND,
93
- )
94
- else:
95
- snode.conditions = select.having_clause.conditional
96
88
  merge_concepts = []
97
89
  for x in [*snode.output_concepts]:
98
- merge = lineage.get_merge_concept(x)
99
- if merge:
90
+ merge_name = lineage.get_merge_concept(x)
91
+ if merge_name:
92
+ merge = environment.concepts[merge_name]
100
93
  snode.output_concepts.append(merge)
101
94
  merge_concepts.append(merge)
102
95
  # clear cache so QPS
@@ -107,24 +100,46 @@ def gen_multiselect_node(
107
100
  if select.where_clause:
108
101
  for item in select.output_components:
109
102
  partial.append(item)
103
+ logger.info(snode.hidden_concepts)
110
104
 
111
- node_joins = extra_align_joins(lineage, base_parents)
105
+ node_joins = extra_align_joins(lineage, environment, base_parents)
106
+ logger.info(
107
+ f"Non-hidden {[x for y in base_parents for x in y.output_concepts if x.address not in y.hidden_concepts]}"
108
+ )
112
109
  node = MergeNode(
113
- input_concepts=[x for y in base_parents for x in y.output_concepts],
114
- output_concepts=[x for y in base_parents for x in y.output_concepts],
110
+ input_concepts=[
111
+ x
112
+ for y in base_parents
113
+ for x in y.output_concepts
114
+ if x.address not in y.hidden_concepts
115
+ ],
116
+ output_concepts=[
117
+ x
118
+ for y in base_parents
119
+ for x in y.output_concepts
120
+ if x.address not in y.hidden_concepts
121
+ ],
115
122
  environment=environment,
116
123
  depth=depth,
117
124
  parents=base_parents,
118
125
  node_joins=node_joins,
119
- hidden_concepts=set([x for y in base_parents for x in y.hidden_concepts]),
126
+ grain=BuildGrain.from_concepts(
127
+ [
128
+ x
129
+ for y in base_parents
130
+ for x in y.output_concepts
131
+ if x.address not in y.hidden_concepts
132
+ ],
133
+ environment=environment,
134
+ ),
120
135
  )
121
136
 
122
137
  enrichment = set([x.address for x in local_optional])
123
138
 
124
139
  multiselect_relevant = [
125
- x
140
+ environment.concepts[x]
126
141
  for x in lineage.derived_concepts
127
- if x.address == concept.address or x.address in enrichment
142
+ if x == concept.address or x in enrichment
128
143
  ]
129
144
  additional_relevant = [x for x in node.output_concepts if x.address in enrichment]
130
145
  # add in other other concepts
@@ -135,7 +150,7 @@ def gen_multiselect_node(
135
150
  # if select.where_clause:
136
151
  # for item in additional_relevant:
137
152
  # node.partial_concepts.append(item)
138
- node.grain = Grain.from_concepts(node.output_concepts, environment=environment)
153
+ node.grain = BuildGrain.from_concepts(node.output_concepts, environment=environment)
139
154
  node.rebuild_cache()
140
155
  # we need a better API for refreshing a nodes QDS
141
156
  possible_joins = concept_to_relevant_joins(additional_relevant)
@@ -156,7 +171,9 @@ def gen_multiselect_node(
156
171
  f"{padding(depth)}{LOGGER_PREFIX} all enriched concepts returned from base rowset node; exiting early"
157
172
  )
158
173
  return node
159
-
174
+ logger.info(
175
+ f"{padding(depth)}{LOGGER_PREFIX} Missing required concepts {[x for x in local_optional if x.address not in [y.address for y in node.output_concepts]]}"
176
+ )
160
177
  enrich_node: MergeNode = source_concepts( # this fetches the parent + join keys
161
178
  # to then connect to the rest of the query
162
179
  mandatory_list=additional_relevant + local_optional,
@@ -4,10 +4,11 @@ import networkx as nx
4
4
  from networkx.algorithms import approximation as ax
5
5
 
6
6
  from trilogy.constants import logger
7
- from trilogy.core.enums import PurposeLineage
7
+ from trilogy.core.enums import Derivation
8
8
  from trilogy.core.exceptions import AmbiguousRelationshipResolutionException
9
9
  from trilogy.core.graph_models import concept_to_node
10
- from trilogy.core.models import Concept, Conditional, Environment, WhereClause
10
+ from trilogy.core.models.build import BuildConcept, BuildConditional, BuildWhereClause
11
+ from trilogy.core.models.build_environment import BuildEnvironment
11
12
  from trilogy.core.processing.nodes import History, MergeNode, StrategyNode
12
13
  from trilogy.core.processing.utility import padding
13
14
  from trilogy.utility import unique
@@ -33,7 +34,7 @@ def extract_address(node: str):
33
34
  return node.split("~")[1].split("@")[0]
34
35
 
35
36
 
36
- def extract_concept(node: str, env: Environment):
37
+ def extract_concept(node: str, env: BuildEnvironment):
37
38
  if node in env.alias_origin_lookup:
38
39
  return env.alias_origin_lookup[node]
39
40
  return env.concepts[node]
@@ -76,7 +77,7 @@ def extract_ds_components(g: nx.DiGraph, nodelist: list[str]) -> list[list[str]]
76
77
  def determine_induced_minimal_nodes(
77
78
  G: nx.DiGraph,
78
79
  nodelist: list[str],
79
- environment: Environment,
80
+ environment: BuildEnvironment,
80
81
  filter_downstream: bool,
81
82
  accept_partial: bool = False,
82
83
  ) -> nx.DiGraph | None:
@@ -86,11 +87,11 @@ def determine_induced_minimal_nodes(
86
87
 
87
88
  for node in G.nodes:
88
89
  if concepts.get(node):
89
- lookup: Concept = concepts[node]
90
- if lookup.derivation in (PurposeLineage.CONSTANT,):
90
+ lookup: BuildConcept = concepts[node]
91
+ if lookup.derivation in (Derivation.CONSTANT,):
91
92
  nodes_to_remove.append(node)
92
93
  # purge a node if we're already looking for all it's parents
93
- if filter_downstream and lookup.derivation not in (PurposeLineage.ROOT,):
94
+ if filter_downstream and lookup.derivation not in (Derivation.ROOT,):
94
95
  nodes_to_remove.append(node)
95
96
 
96
97
  H.remove_nodes_from(nodes_to_remove)
@@ -106,8 +107,8 @@ def determine_induced_minimal_nodes(
106
107
 
107
108
  try:
108
109
  paths = nx.multi_source_dijkstra_path(H, nodelist)
109
- except nx.exception.NodeNotFound:
110
- logger.debug(f"Unable to find paths for {nodelist}")
110
+ except nx.exception.NodeNotFound as e:
111
+ logger.debug(f"Unable to find paths for {nodelist}- {str(e)}")
111
112
  return None
112
113
  H.remove_nodes_from(list(x for x in H.nodes if x not in paths))
113
114
  sG: nx.Graph = ax.steinertree.steiner_tree(H, nodelist).copy()
@@ -140,14 +141,17 @@ def determine_induced_minimal_nodes(
140
141
  return None
141
142
 
142
143
  if not all([node in final.nodes for node in nodelist]):
143
- logger.debug(f"Skipping graph for {nodelist} as missing nodes")
144
+ missing = [node for node in nodelist if node not in final.nodes]
145
+ logger.debug(
146
+ f"Skipping graph for {nodelist} as missing nodes {missing} from {final.nodes}"
147
+ )
144
148
  return None
145
149
  logger.debug(f"Found final graph {final.nodes}")
146
150
  return final
147
151
 
148
152
 
149
153
  def detect_ambiguity_and_raise(
150
- all_concepts: list[Concept], reduced_concept_sets: list[set[str]]
154
+ all_concepts: list[BuildConcept], reduced_concept_sets: list[set[str]]
151
155
  ) -> None:
152
156
  final_candidates: list[set[str]] = []
153
157
  common: set[str] = set()
@@ -167,7 +171,7 @@ def detect_ambiguity_and_raise(
167
171
  )
168
172
 
169
173
 
170
- def has_synonym(concept: Concept, others: list[list[Concept]]) -> bool:
174
+ def has_synonym(concept: BuildConcept, others: list[list[BuildConcept]]) -> bool:
171
175
  return any(
172
176
  c.address in concept.pseudonyms or concept.address in c.pseudonyms
173
177
  for sublist in others
@@ -175,7 +179,9 @@ def has_synonym(concept: Concept, others: list[list[Concept]]) -> bool:
175
179
  )
176
180
 
177
181
 
178
- def filter_relevant_subgraphs(subgraphs: list[list[Concept]]) -> list[list[Concept]]:
182
+ def filter_relevant_subgraphs(
183
+ subgraphs: list[list[BuildConcept]],
184
+ ) -> list[list[BuildConcept]]:
179
185
  return [
180
186
  subgraph
181
187
  for subgraph in subgraphs
@@ -187,20 +193,39 @@ def filter_relevant_subgraphs(subgraphs: list[list[Concept]]) -> list[list[Conce
187
193
  ]
188
194
 
189
195
 
196
+ def filter_duplicate_subgraphs(
197
+ subgraphs: list[list[BuildConcept]],
198
+ ) -> list[list[BuildConcept]]:
199
+ seen: list[set[str]] = []
200
+
201
+ for graph in subgraphs:
202
+ seen.append(set([x.address for x in graph]))
203
+ final = []
204
+ # sometimes w can get two subcomponents that are the same
205
+ # due to alias resolution
206
+ # if so, drop any that are strict subsets.
207
+ for graph in subgraphs:
208
+ set_x = set([x.address for x in graph])
209
+ if any([set_x.issubset(y) and set_x != y for y in seen]):
210
+ continue
211
+ final.append(graph)
212
+ return final
213
+
214
+
190
215
  def resolve_weak_components(
191
- all_concepts: List[Concept],
192
- environment: Environment,
216
+ all_concepts: List[BuildConcept],
217
+ environment: BuildEnvironment,
193
218
  environment_graph: nx.DiGraph,
194
219
  filter_downstream: bool = True,
195
220
  accept_partial: bool = False,
196
- ) -> list[list[Concept]] | None:
221
+ ) -> list[list[BuildConcept]] | None:
197
222
  break_flag = False
198
223
  found = []
199
224
  search_graph = environment_graph.copy()
200
225
  reduced_concept_sets: list[set[str]] = []
201
226
 
202
227
  # loop through, removing new nodes we find
203
- # to ensure there are not ambiguous loops
228
+ # to ensure there are not ambiguous discovery paths
204
229
  # (if we did not care about raising ambiguity errors, we could just use the first one)
205
230
  count = 0
206
231
  node_list = [
@@ -211,6 +236,8 @@ def resolve_weak_components(
211
236
  synonyms: set[str] = set()
212
237
  for x in all_concepts:
213
238
  synonyms = synonyms.union(x.pseudonyms)
239
+ # from trilogy.hooks.graph_hook import GraphHook
240
+ # GraphHook().query_graph_built(search_graph, highlight_nodes=[concept_to_node(c.with_default_grain()) for c in all_concepts if "__preql_internal" not in c.address])
214
241
  while break_flag is not True:
215
242
  count += 1
216
243
  if count > AMBIGUITY_CHECK_LIMIT:
@@ -239,6 +266,7 @@ def resolve_weak_components(
239
266
  new = [x for x in all_graph_concepts if x.address not in all_concepts]
240
267
 
241
268
  new_addresses = set([x.address for x in new if x.address not in synonyms])
269
+
242
270
  if not new:
243
271
  break_flag = True
244
272
  # remove our new nodes for the next search path
@@ -264,7 +292,7 @@ def resolve_weak_components(
264
292
  # take our first one as the actual graph
265
293
  g = found[0]
266
294
 
267
- subgraphs: list[list[Concept]] = []
295
+ subgraphs: list[list[BuildConcept]] = []
268
296
  # components = nx.strongly_connected_components(g)
269
297
  node_list = [x for x in g.nodes if x.startswith("c~")]
270
298
  components = extract_ds_components(g, node_list)
@@ -280,20 +308,22 @@ def resolve_weak_components(
280
308
  if not sub_component:
281
309
  continue
282
310
  subgraphs.append(sub_component)
283
- return subgraphs
311
+ final = filter_duplicate_subgraphs(subgraphs)
312
+ return final
284
313
  # return filter_relevant_subgraphs(subgraphs)
285
314
 
286
315
 
287
316
  def subgraphs_to_merge_node(
288
- concept_subgraphs: list[list[Concept]],
317
+ concept_subgraphs: list[list[BuildConcept]],
289
318
  depth: int,
290
- all_concepts: List[Concept],
319
+ all_concepts: List[BuildConcept],
291
320
  environment,
292
321
  g,
293
322
  source_concepts,
294
323
  history,
295
324
  conditions,
296
- search_conditions: WhereClause | None = None,
325
+ output_concepts: List[BuildConcept],
326
+ search_conditions: BuildWhereClause | None = None,
297
327
  enable_early_exit: bool = True,
298
328
  ):
299
329
  parents: List[StrategyNode] = []
@@ -331,16 +361,13 @@ def subgraphs_to_merge_node(
331
361
  f"{padding(depth)}{LOGGER_PREFIX} only one parent node, exiting early w/ {[c.address for c in parents[0].output_concepts]}"
332
362
  )
333
363
  return parents[0]
334
- base_output = [x for x in all_concepts]
335
- # for x in base_output:
336
- # if x not in input_c:
337
- # input_c.append(x)
338
364
  return MergeNode(
339
365
  input_concepts=unique(input_c, "address"),
340
- output_concepts=base_output,
366
+ output_concepts=output_concepts,
341
367
  environment=environment,
342
368
  parents=parents,
343
369
  depth=depth,
370
+ # hidden_concepts=[]
344
371
  # conditions=conditions,
345
372
  # conditions=search_conditions.conditional,
346
373
  # preexisting_conditions=search_conditions.conditional,
@@ -349,33 +376,41 @@ def subgraphs_to_merge_node(
349
376
 
350
377
 
351
378
  def gen_merge_node(
352
- all_concepts: List[Concept],
379
+ all_concepts: List[BuildConcept],
353
380
  g: nx.DiGraph,
354
- environment: Environment,
381
+ environment: BuildEnvironment,
355
382
  depth: int,
356
383
  source_concepts,
357
384
  accept_partial: bool = False,
358
385
  history: History | None = None,
359
- conditions: Conditional | None = None,
360
- search_conditions: WhereClause | None = None,
386
+ conditions: BuildConditional | None = None,
387
+ search_conditions: BuildWhereClause | None = None,
361
388
  ) -> Optional[MergeNode]:
362
389
  if search_conditions:
363
- all_concepts = unique(all_concepts + search_conditions.row_arguments, "address")
390
+ all_search_concepts = unique(
391
+ all_concepts + list(search_conditions.row_arguments), "address"
392
+ )
393
+ else:
394
+ all_search_concepts = all_concepts
364
395
  for filter_downstream in [True, False]:
365
396
  weak_resolve = resolve_weak_components(
366
- all_concepts,
397
+ all_search_concepts,
367
398
  environment,
368
399
  g,
369
400
  filter_downstream=filter_downstream,
370
401
  accept_partial=accept_partial,
371
402
  )
372
- if weak_resolve:
403
+ if not weak_resolve:
404
+ logger.info(
405
+ f"{padding(depth)}{LOGGER_PREFIX} wasn't able to resolve graph through intermediate concept injection with accept_partial {accept_partial}, filter_downstream {filter_downstream}"
406
+ )
407
+ else:
373
408
  log_graph = [[y.address for y in x] for x in weak_resolve]
374
409
  logger.info(
375
410
  f"{padding(depth)}{LOGGER_PREFIX} Was able to resolve graph through weak component resolution - final graph {log_graph}"
376
411
  )
377
412
  for flat in log_graph:
378
- if set(flat) == set([x.address for x in all_concepts]):
413
+ if set(flat) == set([x.address for x in all_search_concepts]):
379
414
  logger.info(
380
415
  f"{padding(depth)}{LOGGER_PREFIX} expanded concept resolution was identical to search resolution; breaking to avoid recursion error."
381
416
  )
@@ -383,17 +418,19 @@ def gen_merge_node(
383
418
  return subgraphs_to_merge_node(
384
419
  weak_resolve,
385
420
  depth=depth,
386
- all_concepts=all_concepts,
421
+ all_concepts=all_search_concepts,
387
422
  environment=environment,
388
423
  g=g,
389
424
  source_concepts=source_concepts,
390
425
  history=history,
391
426
  conditions=conditions,
392
427
  search_conditions=search_conditions,
428
+ output_concepts=all_concepts,
393
429
  )
430
+
394
431
  # one concept handling may need to be kicked to alias
395
- if len(all_concepts) == 1:
396
- concept = all_concepts[0]
432
+ if len(all_search_concepts) == 1:
433
+ concept = all_search_concepts[0]
397
434
  for v in concept.pseudonyms:
398
435
  test = subgraphs_to_merge_node(
399
436
  [[concept, environment.alias_origin_lookup[v]]],
@@ -406,6 +443,7 @@ def gen_merge_node(
406
443
  conditions=conditions,
407
444
  enable_early_exit=False,
408
445
  search_conditions=search_conditions,
446
+ output_concepts=[concept],
409
447
  )
410
448
  if test:
411
449
  return test
@@ -1,17 +1,18 @@
1
1
  from typing import List
2
2
 
3
3
  from trilogy.constants import logger
4
- from trilogy.core.enums import PurposeLineage
5
- from trilogy.core.models import (
6
- Concept,
7
- Environment,
8
- Grain,
9
- MultiSelectStatement,
10
- RowsetDerivationStatement,
11
- RowsetItem,
12
- SelectStatement,
13
- WhereClause,
4
+ from trilogy.core.enums import Derivation
5
+ from trilogy.core.exceptions import UnresolvableQueryException
6
+ from trilogy.core.models.author import MultiSelectLineage, SelectLineage
7
+ from trilogy.core.models.build import (
8
+ BuildConcept,
9
+ BuildGrain,
10
+ BuildRowsetItem,
11
+ BuildRowsetLineage,
12
+ BuildWhereClause,
13
+ Factory,
14
14
  )
15
+ from trilogy.core.models.build_environment import BuildEnvironment
15
16
  from trilogy.core.processing.nodes import History, MergeNode, StrategyNode
16
17
  from trilogy.core.processing.utility import concept_to_relevant_joins, padding
17
18
 
@@ -19,62 +20,76 @@ LOGGER_PREFIX = "[GEN_ROWSET_NODE]"
19
20
 
20
21
 
21
22
  def gen_rowset_node(
22
- concept: Concept,
23
- local_optional: List[Concept],
24
- environment: Environment,
23
+ concept: BuildConcept,
24
+ local_optional: List[BuildConcept],
25
+ environment: BuildEnvironment,
25
26
  g,
26
27
  depth: int,
27
28
  source_concepts,
28
- history: History | None = None,
29
- conditions: WhereClause | None = None,
29
+ history: History,
30
+ conditions: BuildWhereClause | None = None,
30
31
  ) -> StrategyNode | None:
31
32
  from trilogy.core.query_processor import get_query_node
32
33
 
33
- if not isinstance(concept.lineage, RowsetItem):
34
+ if not isinstance(concept.lineage, BuildRowsetItem):
34
35
  raise SyntaxError(
35
- f"Invalid lineage passed into rowset fetch, got {type(concept.lineage)}, expected {RowsetItem}"
36
+ f"Invalid lineage passed into rowset fetch, got {type(concept.lineage)}, expected {BuildRowsetItem}"
36
37
  )
37
- lineage: RowsetItem = concept.lineage
38
- rowset: RowsetDerivationStatement = lineage.rowset
39
- select: SelectStatement | MultiSelectStatement = lineage.rowset.select
38
+ lineage: BuildRowsetItem = concept.lineage
39
+ rowset: BuildRowsetLineage = lineage.rowset
40
+ select: SelectLineage | MultiSelectLineage = lineage.rowset.select
40
41
 
41
- node = get_query_node(environment, select)
42
+ node = get_query_node(history.base_environment, select)
42
43
 
43
44
  if not node:
44
45
  logger.info(
45
46
  f"{padding(depth)}{LOGGER_PREFIX} Cannot generate parent rowset node for {concept}"
46
47
  )
47
- return None
48
+ raise UnresolvableQueryException(
49
+ f"Cannot generate parent select for concept {concept} in rowset {rowset.name}; ensure the rowset is a valid statement."
50
+ )
48
51
  enrichment = set([x.address for x in local_optional])
49
- rowset_relevant = [x for x in rowset.derived_concepts]
50
- select_hidden = select.hidden_components
52
+
53
+ factory = Factory(environment=history.base_environment, grain=select.grain)
54
+ rowset_relevant: list[BuildConcept] = [
55
+ v
56
+ for v in environment.concepts.values()
57
+ if isinstance(v.lineage, BuildRowsetItem)
58
+ and v.lineage.rowset.name == rowset.name
59
+ ]
60
+ # logger.info(
61
+ # f"{padding(depth)}{LOGGER_PREFIX} rowset relevant nodes are {rowset_relevant}"
62
+ # )
63
+ select_hidden = node.hidden_concepts
51
64
  rowset_hidden = [
52
65
  x
53
- for x in rowset.derived_concepts
54
- if isinstance(x.lineage, RowsetItem)
66
+ for x in rowset_relevant
67
+ if isinstance(x.lineage, BuildRowsetItem)
55
68
  and x.lineage.content.address in select_hidden
56
69
  ]
57
70
  additional_relevant = [
58
- x for x in select.output_components if x.address in enrichment
71
+ factory.build(x) for x in select.output_components if x.address in enrichment
59
72
  ]
60
73
  # add in other other concepts
61
-
62
74
  node.add_output_concepts(rowset_relevant + additional_relevant)
63
75
  if select.where_clause:
64
76
  for item in additional_relevant:
77
+ logger.info(
78
+ f"{padding(depth)}{LOGGER_PREFIX} adding {item} to partial concepts"
79
+ )
65
80
  node.partial_concepts.append(item)
66
81
 
67
82
  final_hidden = rowset_hidden + [
68
83
  x
69
84
  for x in node.output_concepts
70
85
  if x.address not in local_optional + [concept]
71
- and x.derivation != PurposeLineage.ROWSET
86
+ and x.derivation != Derivation.ROWSET
72
87
  ]
73
88
  node.hide_output_concepts(final_hidden)
74
89
  assert node.resolution_cache
75
90
  # assume grain to be output of select
76
91
  # but don't include anything hidden(the non-rowset concepts)
77
- node.grain = Grain.from_concepts(
92
+ node.grain = BuildGrain.from_concepts(
78
93
  [
79
94
  x
80
95
  for x in node.output_concepts
@@ -82,29 +97,35 @@ def gen_rowset_node(
82
97
  not in [
83
98
  y
84
99
  for y in node.hidden_concepts
85
- if environment.concepts[y].derivation != PurposeLineage.ROWSET
100
+ if y in environment.concepts
101
+ and environment.concepts[y].derivation != Derivation.ROWSET
86
102
  ]
87
103
  ],
88
104
  )
89
105
 
90
106
  node.rebuild_cache()
91
-
92
107
  if not local_optional or all(
93
- x.address in node.output_concepts for x in local_optional
108
+ x.address in node.output_concepts and x.address not in node.partial_concepts
109
+ for x in local_optional
94
110
  ):
95
111
  logger.info(
96
112
  f"{padding(depth)}{LOGGER_PREFIX} no enrichment required for rowset node as all optional {[x.address for x in local_optional]} found or no optional; exiting early."
97
113
  )
98
114
  return node
115
+ remaining = [
116
+ x
117
+ for x in local_optional
118
+ if x not in node.output_concepts or x in node.partial_concepts
119
+ ]
99
120
  possible_joins = concept_to_relevant_joins(
100
- [x for x in node.output_concepts if x.derivation != PurposeLineage.ROWSET]
121
+ [x for x in node.output_concepts if x.derivation != Derivation.ROWSET]
101
122
  )
102
123
  if not possible_joins:
103
124
  logger.info(
104
125
  f"{padding(depth)}{LOGGER_PREFIX} no possible joins for rowset node to get {[x.address for x in local_optional]}; have {[x.address for x in node.output_concepts]}"
105
126
  )
106
127
  return node
107
- if any(x.derivation == PurposeLineage.ROWSET for x in possible_joins):
128
+ if any(x.derivation == Derivation.ROWSET for x in possible_joins):
108
129
 
109
130
  logger.info(
110
131
  f"{padding(depth)}{LOGGER_PREFIX} cannot enrich rowset node with rowset concepts; exiting early"
@@ -113,7 +134,7 @@ def gen_rowset_node(
113
134
  logger.info([x.address for x in possible_joins + local_optional])
114
135
  enrich_node: MergeNode = source_concepts( # this fetches the parent + join keys
115
136
  # to then connect to the rest of the query
116
- mandatory_list=possible_joins + local_optional,
137
+ mandatory_list=possible_joins + remaining,
117
138
  environment=environment,
118
139
  g=g,
119
140
  depth=depth + 1,