pytrilogy 0.0.2.8__py3-none-any.whl → 0.0.2.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pytrilogy might be problematic. Click here for more details.

Files changed (31) hide show
  1. {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.9.dist-info}/METADATA +1 -1
  2. {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.9.dist-info}/RECORD +31 -31
  3. {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.9.dist-info}/WHEEL +1 -1
  4. trilogy/__init__.py +1 -1
  5. trilogy/constants.py +1 -0
  6. trilogy/core/models.py +144 -54
  7. trilogy/core/optimization.py +44 -5
  8. trilogy/core/optimizations/inline_datasource.py +14 -8
  9. trilogy/core/optimizations/predicate_pushdown.py +73 -44
  10. trilogy/core/processing/concept_strategies_v3.py +69 -28
  11. trilogy/core/processing/node_generators/common.py +42 -16
  12. trilogy/core/processing/node_generators/filter_node.py +89 -48
  13. trilogy/core/processing/node_generators/group_node.py +3 -1
  14. trilogy/core/processing/node_generators/rowset_node.py +13 -54
  15. trilogy/core/processing/node_generators/select_node.py +10 -13
  16. trilogy/core/processing/node_generators/unnest_node.py +5 -3
  17. trilogy/core/processing/node_generators/window_node.py +23 -2
  18. trilogy/core/processing/nodes/__init__.py +34 -6
  19. trilogy/core/processing/nodes/base_node.py +67 -13
  20. trilogy/core/processing/nodes/filter_node.py +3 -0
  21. trilogy/core/processing/nodes/group_node.py +3 -0
  22. trilogy/core/processing/nodes/merge_node.py +1 -11
  23. trilogy/core/processing/nodes/select_node_v2.py +1 -0
  24. trilogy/core/processing/utility.py +29 -10
  25. trilogy/core/query_processor.py +47 -20
  26. trilogy/dialect/base.py +11 -4
  27. trilogy/parsing/parse_engine.py +20 -1
  28. trilogy/parsing/trilogy.lark +3 -1
  29. {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.9.dist-info}/LICENSE.md +0 -0
  30. {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.9.dist-info}/entry_points.txt +0 -0
  31. {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.9.dist-info}/top_level.txt +0 -0
@@ -2,13 +2,14 @@ from typing import List
2
2
 
3
3
 
4
4
  from trilogy.core.enums import JoinType
5
- from trilogy.core.models import Concept, Environment, FilterItem, Grain
5
+ from trilogy.core.models import Concept, Environment, FilterItem, Grain, WhereClause
6
6
  from trilogy.core.processing.nodes import (
7
7
  FilterNode,
8
8
  MergeNode,
9
9
  NodeJoin,
10
10
  History,
11
11
  StrategyNode,
12
+ SelectNode,
12
13
  )
13
14
  from trilogy.core.processing.node_generators.common import (
14
15
  resolve_filter_parent_concepts,
@@ -28,6 +29,7 @@ def gen_filter_node(
28
29
  depth: int,
29
30
  source_concepts,
30
31
  history: History | None = None,
32
+ conditions: WhereClause | None = None,
31
33
  ) -> StrategyNode | None:
32
34
  immediate_parent, parent_row_concepts, parent_existence_concepts = (
33
35
  resolve_filter_parent_concepts(concept)
@@ -37,10 +39,10 @@ def gen_filter_node(
37
39
  where = concept.lineage.where
38
40
 
39
41
  logger.info(
40
- f"{padding(depth)}{LOGGER_PREFIX} fetching filter node row parents {[x.address for x in parent_row_concepts]}"
42
+ f"{padding(depth)}{LOGGER_PREFIX} filter {concept.address} derived from {immediate_parent.address} row parents {[x.address for x in parent_row_concepts]} and {[[y.address] for x in parent_existence_concepts for y in x]} existence parents"
41
43
  )
42
44
  core_parents = []
43
- parent: StrategyNode = source_concepts(
45
+ row_parent: StrategyNode = source_concepts(
44
46
  mandatory_list=parent_row_concepts,
45
47
  environment=environment,
46
48
  g=g,
@@ -48,33 +50,7 @@ def gen_filter_node(
48
50
  history=history,
49
51
  )
50
52
 
51
- if not parent:
52
- logger.info(
53
- f"{padding(depth)}{LOGGER_PREFIX} filter node row parents {[x.address for x in parent_row_concepts]} could not be found"
54
- )
55
- return None
56
-
57
- if not local_optional and not parent_existence_concepts:
58
- optimized_pushdown = True
59
- else:
60
- optimized_pushdown = False
61
-
62
- if optimized_pushdown:
63
- if parent.conditions:
64
- parent.conditions = parent.conditions + where.conditional
65
- else:
66
- parent.conditions = where.conditional
67
- parent.output_concepts = [concept]
68
- parent.grain = Grain(components=[concept])
69
- parent.rebuild_cache()
70
-
71
- logger.info(
72
- f"{padding(depth)}{LOGGER_PREFIX} returning optimized filter node with pushdown to parent with condition {where.conditional}"
73
- )
74
- return parent
75
-
76
- core_parents.append(parent)
77
-
53
+ flattened_existence = [x for y in parent_existence_concepts for x in y]
78
54
  if parent_existence_concepts:
79
55
  for existence_tuple in parent_existence_concepts:
80
56
  if not existence_tuple:
@@ -95,26 +71,91 @@ def gen_filter_node(
95
71
  )
96
72
  return None
97
73
  core_parents.append(parent_existence)
98
- flattened_existence = [x for y in parent_existence_concepts for x in y]
99
- filter_node = FilterNode(
100
- input_concepts=unique(
101
- [immediate_parent] + parent_row_concepts + flattened_existence,
102
- "address",
103
- ),
104
- output_concepts=[concept, immediate_parent] + parent_row_concepts,
105
- environment=environment,
106
- g=g,
107
- parents=core_parents,
108
- grain=Grain(
109
- components=[immediate_parent] + parent_row_concepts,
110
- ),
111
- )
74
+ if not row_parent:
75
+ logger.info(
76
+ f"{padding(depth)}{LOGGER_PREFIX} filter node row parents {[x.address for x in parent_row_concepts]} could not be found"
77
+ )
78
+ return None
79
+
80
+ optimized_pushdown = False
81
+ if not local_optional:
82
+ optimized_pushdown = True
83
+ elif conditions and conditions == where:
84
+ logger.info(
85
+ f"{padding(depth)}{LOGGER_PREFIX} query conditions are the same as filter conditions, can optimize across all concepts"
86
+ )
87
+ optimized_pushdown = True
88
+
89
+ if optimized_pushdown:
90
+ if isinstance(row_parent, SelectNode):
91
+ parent = StrategyNode(
92
+ input_concepts=row_parent.output_concepts,
93
+ output_concepts=[concept] + row_parent.output_concepts,
94
+ environment=row_parent.environment,
95
+ g=row_parent.g,
96
+ parents=[row_parent] + core_parents,
97
+ depth=row_parent.depth,
98
+ partial_concepts=row_parent.partial_concepts,
99
+ force_group=False,
100
+ conditions=(
101
+ row_parent.conditions + where.conditional
102
+ if row_parent.conditions
103
+ else where.conditional
104
+ ),
105
+ existence_concepts=row_parent.existence_concepts,
106
+ )
107
+ else:
108
+ parent = row_parent
109
+
110
+ expected_output = [concept] + [
111
+ x
112
+ for x in local_optional
113
+ if x.address in [y.address for y in parent.output_concepts]
114
+ ]
115
+ parent.add_parents(core_parents)
116
+ parent.add_condition(where.conditional)
117
+ parent.add_existence_concepts(flattened_existence)
118
+ parent.set_output_concepts(expected_output)
119
+ parent.grain = Grain(
120
+ components=(
121
+ list(immediate_parent.keys)
122
+ if immediate_parent.keys
123
+ else [immediate_parent]
124
+ )
125
+ + [
126
+ x
127
+ for x in local_optional
128
+ if x.address in [y.address for y in parent.output_concepts]
129
+ ]
130
+ )
131
+ parent.rebuild_cache()
132
+
133
+ logger.info(
134
+ f"{padding(depth)}{LOGGER_PREFIX} returning optimized filter node with pushdown to parent with condition {where.conditional}"
135
+ )
136
+ filter_node = parent
137
+ else:
138
+ core_parents.append(row_parent)
139
+
140
+ filter_node = FilterNode(
141
+ input_concepts=unique(
142
+ [immediate_parent] + parent_row_concepts + flattened_existence,
143
+ "address",
144
+ ),
145
+ output_concepts=[concept, immediate_parent] + parent_row_concepts,
146
+ environment=environment,
147
+ g=g,
148
+ parents=core_parents,
149
+ grain=Grain(
150
+ components=[immediate_parent] + parent_row_concepts,
151
+ ),
152
+ )
112
153
 
113
- assert filter_node.resolve().grain == Grain(
114
- components=[immediate_parent] + parent_row_concepts,
115
- )
116
154
  if not local_optional or all(
117
- [x.address in [y.address for y in parent_row_concepts] for x in local_optional]
155
+ [
156
+ x.address in [y.address for y in filter_node.output_concepts]
157
+ for x in local_optional
158
+ ]
118
159
  ):
119
160
  outputs = [
120
161
  x
@@ -1,4 +1,4 @@
1
- from trilogy.core.models import Concept, Environment, LooseConceptList
1
+ from trilogy.core.models import Concept, Environment, LooseConceptList, WhereClause
2
2
  from trilogy.utility import unique
3
3
  from trilogy.core.processing.nodes import GroupNode, StrategyNode, History
4
4
  from typing import List
@@ -22,6 +22,7 @@ def gen_group_node(
22
22
  depth: int,
23
23
  source_concepts,
24
24
  history: History | None = None,
25
+ conditions: WhereClause | None = None,
25
26
  ):
26
27
  # aggregates MUST always group to the proper grain
27
28
  # except when the
@@ -53,6 +54,7 @@ def gen_group_node(
53
54
  g=g,
54
55
  depth=depth,
55
56
  history=history,
57
+ conditions=conditions,
56
58
  )
57
59
  if not parent:
58
60
  logger.info(
@@ -12,7 +12,7 @@ from typing import List
12
12
 
13
13
  from trilogy.core.enums import JoinType, PurposeLineage
14
14
  from trilogy.constants import logger
15
- from trilogy.core.processing.utility import padding, unique
15
+ from trilogy.core.processing.utility import padding
16
16
  from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
17
17
 
18
18
 
@@ -28,6 +28,8 @@ def gen_rowset_node(
28
28
  source_concepts,
29
29
  history: History | None = None,
30
30
  ) -> StrategyNode | None:
31
+ from trilogy.core.query_processor import get_query_node
32
+
31
33
  if not isinstance(concept.lineage, RowsetItem):
32
34
  raise SyntaxError(
33
35
  f"Invalid lineage passed into rowset fetch, got {type(concept.lineage)}, expected {RowsetItem}"
@@ -35,54 +37,14 @@ def gen_rowset_node(
35
37
  lineage: RowsetItem = concept.lineage
36
38
  rowset: RowsetDerivationStatement = lineage.rowset
37
39
  select: SelectStatement | MultiSelectStatement = lineage.rowset.select
38
- existence_parents: List[StrategyNode] = []
39
- if where := select.where_clause:
40
- targets = select.output_components + where.conditional.row_arguments
41
- for sub_select in where.conditional.existence_arguments:
42
- logger.info(
43
- f"{padding(depth)}{LOGGER_PREFIX} generating parent existence node with {[x.address for x in sub_select]}"
44
- )
45
- parent_check = source_concepts(
46
- mandatory_list=sub_select,
47
- environment=environment,
48
- g=g,
49
- depth=depth + 1,
50
- history=history,
51
- )
52
- if not parent_check:
53
- logger.info(
54
- f"{padding(depth)}{LOGGER_PREFIX} Cannot generate parent existence node for rowset node for {concept}"
55
- )
56
- return None
57
- existence_parents.append(parent_check)
58
- else:
59
- targets = select.output_components
60
- node: StrategyNode = source_concepts(
61
- mandatory_list=unique(targets, "address"),
62
- environment=environment,
63
- g=g,
64
- depth=depth + 1,
65
- history=history,
66
- )
40
+ node = get_query_node(environment, select, graph=g, history=history)
67
41
 
68
42
  if not node:
69
43
  logger.info(
70
- f"{padding(depth)}{LOGGER_PREFIX} Cannot generate rowset node for {concept}"
44
+ f"{padding(depth)}{LOGGER_PREFIX} Cannot generate parent rowset node for {concept}"
71
45
  )
72
46
  return None
73
- # add our existence concepts in
74
- if existence_parents:
75
- node.parents += existence_parents
76
- # we don't need to join to any existence parents
77
- # if isinstance(node, MergeNode) and node.node_joins is None:
78
- # # set it explicitly to empty to avoid inference
79
- # node.node_joins = []
80
- for parent in existence_parents:
81
- for x in parent.output_concepts:
82
- if x.address not in node.output_lcl:
83
- node.existence_concepts.append(x)
84
47
 
85
- node.conditions = select.where_clause.conditional if select.where_clause else None
86
48
  enrichment = set([x.address for x in local_optional])
87
49
  rowset_relevant = [x for x in rowset.derived_concepts]
88
50
  select_hidden = set([x.address for x in select.hidden_components])
@@ -113,26 +75,23 @@ def gen_rowset_node(
113
75
  # but don't include anything aggregate at this point
114
76
  assert node.resolution_cache
115
77
 
116
- node.resolution_cache.grain = concept_list_to_grain(
78
+ node.grain = concept_list_to_grain(
117
79
  node.output_concepts, parent_sources=node.resolution_cache.datasources
118
80
  )
119
81
 
82
+ node.rebuild_cache()
83
+
120
84
  possible_joins = concept_to_relevant_joins(additional_relevant)
121
- if not local_optional:
85
+ if not local_optional or all(
86
+ x.address in [y.address for y in node.output_concepts] for x in local_optional
87
+ ):
122
88
  logger.info(
123
- f"{padding(depth)}{LOGGER_PREFIX} no enriched required for rowset node; exiting early"
89
+ f"{padding(depth)}{LOGGER_PREFIX} no enrichment required for rowset node as all optional found or no optional; exiting early."
124
90
  )
125
91
  return node
126
92
  if not possible_joins:
127
93
  logger.info(
128
- f"{padding(depth)}{LOGGER_PREFIX} no possible joins for rowset node; exiting early"
129
- )
130
- return node
131
- if all(
132
- [x.address in [y.address for y in node.output_concepts] for x in local_optional]
133
- ):
134
- logger.info(
135
- f"{padding(depth)}{LOGGER_PREFIX} all enriched concepts returned from base rowset node; exiting early"
94
+ 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]}"
136
95
  )
137
96
  return node
138
97
  enrich_node: MergeNode = source_concepts( # this fetches the parent + join keys
@@ -7,6 +7,7 @@ from trilogy.core.models import (
7
7
  Grain,
8
8
  LooseConceptList,
9
9
  Datasource,
10
+ WhereClause,
10
11
  )
11
12
  from trilogy.core.processing.nodes import (
12
13
  StrategyNode,
@@ -67,17 +68,6 @@ def dm_to_strategy_node(
67
68
  f"{padding(depth)}{LOGGER_PREFIX} target grain is not subset of datasource grain {datasource.grain}, required to group"
68
69
  )
69
70
  force_group = True
70
- # if isinstance(datasource, MergeDatasource):
71
- # # if we're within a namespace, don't find merge nodes
72
- # bcandidate: StrategyNode = gen_environment_merge_node(
73
- # all_concepts=dm.matched.concepts,
74
- # environment=environment,
75
- # g=g,
76
- # depth=depth,
77
- # datasource=datasource,
78
- # source_concepts=source_concepts,
79
- # )
80
- # else:
81
71
  bcandidate: StrategyNode = SelectNode(
82
72
  input_concepts=[c.concept for c in datasource.columns],
83
73
  output_concepts=dm.matched.concepts,
@@ -116,6 +106,7 @@ def gen_select_nodes_from_tables_v2(
116
106
  target_grain: Grain,
117
107
  source_concepts: Callable,
118
108
  accept_partial: bool = False,
109
+ conditions: WhereClause | None = None,
119
110
  ) -> tuple[bool, list[Concept], list[StrategyNode]]:
120
111
  # if we have only constants
121
112
  # we don't need a table
@@ -252,6 +243,7 @@ def gen_select_node_from_table(
252
243
  target_grain: Grain,
253
244
  source_concepts,
254
245
  accept_partial: bool = False,
246
+ conditions: WhereClause | None = None,
255
247
  ) -> Optional[StrategyNode]:
256
248
  # if we have only constants
257
249
  # we don't need a table
@@ -372,6 +364,9 @@ def gen_select_node_from_table(
372
364
  grain=Grain(components=all_concepts),
373
365
  conditions=datasource.where.conditional if datasource.where else None,
374
366
  )
367
+ # if conditions:
368
+ # for component in conditions.components:
369
+ # if
375
370
  # we need to nest the group node one further
376
371
  if force_group is True:
377
372
  candidate: StrategyNode = GroupNode(
@@ -407,6 +402,7 @@ def gen_select_node(
407
402
  fail_if_not_found: bool = True,
408
403
  accept_partial_optional: bool = True,
409
404
  target_grain: Grain | None = None,
405
+ conditions: WhereClause | None = None,
410
406
  ) -> StrategyNode | None:
411
407
  all_concepts = [concept] + local_optional
412
408
  all_lcl = LooseConceptList(concepts=all_concepts)
@@ -445,6 +441,7 @@ def gen_select_node(
445
441
  accept_partial=accept_partial,
446
442
  target_grain=target_grain,
447
443
  source_concepts=source_concepts,
444
+ conditions=conditions,
448
445
  )
449
446
  if ds:
450
447
  logger.info(
@@ -461,6 +458,7 @@ def gen_select_node(
461
458
  target_grain=target_grain,
462
459
  accept_partial=accept_partial,
463
460
  source_concepts=source_concepts,
461
+ conditions=conditions,
464
462
  )
465
463
  if parents and (all_found or accept_partial_optional):
466
464
  all_partial = [
@@ -488,7 +486,6 @@ def gen_select_node(
488
486
  if len(parents) == 1:
489
487
  candidate = parents[0]
490
488
  else:
491
-
492
489
  candidate = MergeNode(
493
490
  output_concepts=[concept] + found,
494
491
  input_concepts=[concept] + found,
@@ -499,8 +496,8 @@ def gen_select_node(
499
496
  partial_concepts=all_partial,
500
497
  grain=inferred_grain,
501
498
  )
499
+
502
500
  candidate.depth += 1
503
- # source_grain = candidate.grain
504
501
  if force_group:
505
502
  logger.info(
506
503
  f"{padding(depth)}{LOGGER_PREFIX} datasource grain {inferred_grain} does not match target grain {target_grain} for select, adding group node"
@@ -1,8 +1,8 @@
1
1
  from typing import List
2
2
 
3
3
 
4
- from trilogy.core.models import Concept, Function
5
- from trilogy.core.processing.nodes import SelectNode, UnnestNode, History, StrategyNode
4
+ from trilogy.core.models import Concept, Function, WhereClause
5
+ from trilogy.core.processing.nodes import UnnestNode, History, StrategyNode
6
6
  from trilogy.core.processing.utility import padding
7
7
  from trilogy.constants import logger
8
8
 
@@ -17,6 +17,7 @@ def gen_unnest_node(
17
17
  depth: int,
18
18
  source_concepts,
19
19
  history: History | None = None,
20
+ conditions: WhereClause | None = None,
20
21
  ) -> StrategyNode | None:
21
22
  arguments = []
22
23
  if isinstance(concept.lineage, Function):
@@ -28,6 +29,7 @@ def gen_unnest_node(
28
29
  g=g,
29
30
  depth=depth + 1,
30
31
  history=history,
32
+ conditions=conditions,
31
33
  )
32
34
  if not parent:
33
35
  logger.info(
@@ -46,7 +48,7 @@ def gen_unnest_node(
46
48
  # we need to sometimes nest an unnest node,
47
49
  # as unnest operations are not valid in all situations
48
50
  # TODO: inline this node when we can detect it's safe
49
- new = SelectNode(
51
+ new = StrategyNode(
50
52
  input_concepts=[concept] + local_optional,
51
53
  output_concepts=[concept] + local_optional,
52
54
  environment=environment,
@@ -1,7 +1,7 @@
1
1
  from typing import List
2
2
 
3
3
 
4
- from trilogy.core.models import Concept, WindowItem, Environment
4
+ from trilogy.core.models import Concept, WindowItem, Environment, WhereClause
5
5
  from trilogy.utility import unique
6
6
  from trilogy.core.processing.nodes import (
7
7
  WindowNode,
@@ -38,19 +38,36 @@ def gen_window_node(
38
38
  depth: int,
39
39
  source_concepts,
40
40
  history: History | None = None,
41
+ conditions: WhereClause | None = None,
41
42
  ) -> WindowNode | MergeNode | None:
42
43
  parent_concepts = resolve_window_parent_concepts(concept)
43
-
44
44
  parent_node = source_concepts(
45
45
  mandatory_list=parent_concepts,
46
46
  environment=environment,
47
47
  g=g,
48
48
  depth=depth + 1,
49
49
  history=history,
50
+ conditions=conditions,
50
51
  )
51
52
  if not parent_node:
52
53
  logger.info(f"{padding(depth)}{LOGGER_PREFIX} window node parents unresolvable")
53
54
  return None
55
+ parent_node.resolve()
56
+ if not all(
57
+ [
58
+ x.address in [y.address for y in parent_node.output_concepts]
59
+ for x in parent_concepts
60
+ ]
61
+ ):
62
+ missing = [
63
+ x
64
+ for x in parent_concepts
65
+ if x.address not in [y.address for y in parent_node.output_concepts]
66
+ ]
67
+ logger.info(
68
+ f"{padding(depth)}{LOGGER_PREFIX} window node parents unresolvable, missing {missing}"
69
+ )
70
+ raise SyntaxError
54
71
  _window_node = WindowNode(
55
72
  input_concepts=parent_concepts,
56
73
  output_concepts=[concept] + parent_concepts,
@@ -61,6 +78,8 @@ def gen_window_node(
61
78
  ],
62
79
  depth=depth,
63
80
  )
81
+ _window_node.rebuild_cache()
82
+ _window_node.resolve()
64
83
  window_node = MergeNode(
65
84
  parents=[_window_node],
66
85
  environment=environment,
@@ -71,6 +90,7 @@ def gen_window_node(
71
90
  force_group=False,
72
91
  depth=depth,
73
92
  )
93
+ window_node.resolve()
74
94
  if not local_optional:
75
95
  return window_node
76
96
  logger.info(f"{padding(depth)}{LOGGER_PREFIX} window node requires enrichment")
@@ -84,4 +104,5 @@ def gen_window_node(
84
104
  source_concepts=source_concepts,
85
105
  log_lambda=create_log_lambda(LOGGER_PREFIX, depth, logger),
86
106
  history=history,
107
+ conditions=conditions,
87
108
  )
@@ -6,7 +6,7 @@ from .window_node import WindowNode
6
6
  from .base_node import StrategyNode, NodeJoin
7
7
  from .unnest_node import UnnestNode
8
8
  from pydantic import BaseModel, Field, ConfigDict
9
- from trilogy.core.models import Concept, Environment
9
+ from trilogy.core.models import Concept, Environment, WhereClause
10
10
 
11
11
 
12
12
  class History(BaseModel):
@@ -15,23 +15,42 @@ class History(BaseModel):
15
15
  started: set[str] = Field(default_factory=set)
16
16
  model_config = ConfigDict(arbitrary_types_allowed=True)
17
17
 
18
- def _concepts_to_lookup(self, search: list[Concept], accept_partial: bool) -> str:
18
+ def _concepts_to_lookup(
19
+ self,
20
+ search: list[Concept],
21
+ accept_partial: bool,
22
+ conditions: WhereClause | None = None,
23
+ ) -> str:
24
+ if conditions:
25
+ return (
26
+ "-".join([c.address for c in search])
27
+ + str(accept_partial)
28
+ + str(conditions)
29
+ )
19
30
  return "-".join([c.address for c in search]) + str(accept_partial)
20
31
 
21
32
  def search_to_history(
22
- self, search: list[Concept], accept_partial: bool, output: StrategyNode | None
33
+ self,
34
+ search: list[Concept],
35
+ accept_partial: bool,
36
+ output: StrategyNode | None,
37
+ conditions: WhereClause | None = None,
23
38
  ):
24
- self.history[self._concepts_to_lookup(search, accept_partial)] = output
39
+ self.history[
40
+ self._concepts_to_lookup(search, accept_partial, conditions=conditions)
41
+ ] = output
25
42
 
26
43
  def get_history(
27
44
  self,
28
45
  search: list[Concept],
46
+ conditions: WhereClause | None = None,
29
47
  accept_partial: bool = False,
30
48
  parent_key: str = "",
31
49
  ) -> StrategyNode | None | bool:
32
50
  key = self._concepts_to_lookup(
33
51
  search,
34
52
  accept_partial,
53
+ conditions,
35
54
  )
36
55
  if parent_key and parent_key == key:
37
56
  raise ValueError(
@@ -48,11 +67,13 @@ class History(BaseModel):
48
67
  self,
49
68
  search: list[Concept],
50
69
  accept_partial: bool = False,
70
+ conditions: WhereClause | None = None,
51
71
  ):
52
72
  self.started.add(
53
73
  self._concepts_to_lookup(
54
74
  search,
55
- accept_partial,
75
+ accept_partial=accept_partial,
76
+ conditions=conditions,
56
77
  )
57
78
  )
58
79
 
@@ -60,11 +81,13 @@ class History(BaseModel):
60
81
  self,
61
82
  search: list[Concept],
62
83
  accept_partial: bool = False,
84
+ conditions: WhereClause | None = None,
63
85
  ):
64
86
  return (
65
87
  self._concepts_to_lookup(
66
88
  search,
67
89
  accept_partial,
90
+ conditions=conditions,
68
91
  )
69
92
  in self.started
70
93
  )
@@ -76,6 +99,7 @@ class History(BaseModel):
76
99
  accept_partial: bool,
77
100
  fail_if_not_found: bool,
78
101
  accept_partial_optional: bool,
102
+ conditions: WhereClause | None = None,
79
103
  ) -> str:
80
104
  return (
81
105
  str(main.address)
@@ -84,6 +108,7 @@ class History(BaseModel):
84
108
  + str(accept_partial)
85
109
  + str(fail_if_not_found)
86
110
  + str(accept_partial_optional)
111
+ + str(conditions)
87
112
  )
88
113
 
89
114
  def gen_select_node(
@@ -97,6 +122,7 @@ class History(BaseModel):
97
122
  fail_if_not_found: bool = False,
98
123
  accept_partial: bool = False,
99
124
  accept_partial_optional: bool = False,
125
+ conditions: WhereClause | None = None,
100
126
  ) -> StrategyNode | None:
101
127
  from trilogy.core.processing.node_generators.select_node import gen_select_node
102
128
 
@@ -105,7 +131,8 @@ class History(BaseModel):
105
131
  local_optional,
106
132
  accept_partial,
107
133
  fail_if_not_found,
108
- accept_partial_optional,
134
+ accept_partial_optional=accept_partial_optional,
135
+ conditions=conditions,
109
136
  )
110
137
  if fingerprint in self.select_history:
111
138
  return self.select_history[fingerprint]
@@ -119,6 +146,7 @@ class History(BaseModel):
119
146
  accept_partial=accept_partial,
120
147
  accept_partial_optional=accept_partial_optional,
121
148
  source_concepts=source_concepts,
149
+ conditions=conditions,
122
150
  )
123
151
  self.select_history[fingerprint] = gen
124
152
  return gen