pytrilogy 0.0.2.47__py3-none-any.whl → 0.0.2.49__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 (69) hide show
  1. {pytrilogy-0.0.2.47.dist-info → pytrilogy-0.0.2.49.dist-info}/METADATA +1 -1
  2. pytrilogy-0.0.2.49.dist-info/RECORD +85 -0
  3. trilogy/__init__.py +2 -2
  4. trilogy/constants.py +4 -2
  5. trilogy/core/enums.py +7 -1
  6. trilogy/core/env_processor.py +1 -2
  7. trilogy/core/environment_helpers.py +5 -5
  8. trilogy/core/functions.py +11 -10
  9. trilogy/core/internal.py +2 -3
  10. trilogy/core/models.py +449 -393
  11. trilogy/core/optimization.py +37 -21
  12. trilogy/core/optimizations/__init__.py +1 -1
  13. trilogy/core/optimizations/base_optimization.py +6 -6
  14. trilogy/core/optimizations/inline_constant.py +7 -4
  15. trilogy/core/optimizations/inline_datasource.py +14 -5
  16. trilogy/core/optimizations/predicate_pushdown.py +20 -10
  17. trilogy/core/processing/concept_strategies_v3.py +43 -24
  18. trilogy/core/processing/graph_utils.py +2 -3
  19. trilogy/core/processing/node_generators/__init__.py +7 -5
  20. trilogy/core/processing/node_generators/basic_node.py +4 -4
  21. trilogy/core/processing/node_generators/common.py +10 -11
  22. trilogy/core/processing/node_generators/filter_node.py +7 -9
  23. trilogy/core/processing/node_generators/group_node.py +10 -11
  24. trilogy/core/processing/node_generators/group_to_node.py +5 -5
  25. trilogy/core/processing/node_generators/multiselect_node.py +10 -12
  26. trilogy/core/processing/node_generators/node_merge_node.py +7 -9
  27. trilogy/core/processing/node_generators/rowset_node.py +36 -15
  28. trilogy/core/processing/node_generators/select_merge_node.py +11 -10
  29. trilogy/core/processing/node_generators/select_node.py +5 -5
  30. trilogy/core/processing/node_generators/union_node.py +75 -0
  31. trilogy/core/processing/node_generators/unnest_node.py +2 -3
  32. trilogy/core/processing/node_generators/window_node.py +3 -4
  33. trilogy/core/processing/nodes/__init__.py +9 -5
  34. trilogy/core/processing/nodes/base_node.py +45 -13
  35. trilogy/core/processing/nodes/filter_node.py +3 -4
  36. trilogy/core/processing/nodes/group_node.py +17 -13
  37. trilogy/core/processing/nodes/merge_node.py +14 -12
  38. trilogy/core/processing/nodes/select_node_v2.py +13 -9
  39. trilogy/core/processing/nodes/union_node.py +50 -0
  40. trilogy/core/processing/nodes/unnest_node.py +2 -3
  41. trilogy/core/processing/nodes/window_node.py +2 -3
  42. trilogy/core/processing/utility.py +38 -41
  43. trilogy/core/query_processor.py +71 -51
  44. trilogy/dialect/base.py +95 -53
  45. trilogy/dialect/bigquery.py +2 -3
  46. trilogy/dialect/common.py +5 -4
  47. trilogy/dialect/config.py +0 -2
  48. trilogy/dialect/duckdb.py +2 -2
  49. trilogy/dialect/enums.py +5 -5
  50. trilogy/dialect/postgres.py +2 -2
  51. trilogy/dialect/presto.py +3 -4
  52. trilogy/dialect/snowflake.py +2 -2
  53. trilogy/dialect/sql_server.py +3 -4
  54. trilogy/engine.py +2 -1
  55. trilogy/executor.py +43 -30
  56. trilogy/hooks/base_hook.py +5 -4
  57. trilogy/hooks/graph_hook.py +2 -1
  58. trilogy/hooks/query_debugger.py +18 -8
  59. trilogy/parsing/common.py +15 -20
  60. trilogy/parsing/parse_engine.py +125 -88
  61. trilogy/parsing/render.py +32 -35
  62. trilogy/parsing/trilogy.lark +8 -1
  63. trilogy/scripts/trilogy.py +6 -4
  64. trilogy/utility.py +1 -1
  65. pytrilogy-0.0.2.47.dist-info/RECORD +0 -83
  66. {pytrilogy-0.0.2.47.dist-info → pytrilogy-0.0.2.49.dist-info}/LICENSE.md +0 -0
  67. {pytrilogy-0.0.2.47.dist-info → pytrilogy-0.0.2.49.dist-info}/WHEEL +0 -0
  68. {pytrilogy-0.0.2.47.dist-info → pytrilogy-0.0.2.49.dist-info}/entry_points.txt +0 -0
  69. {pytrilogy-0.0.2.47.dist-info → pytrilogy-0.0.2.49.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,16 @@
1
1
  from typing import List, Optional
2
2
 
3
- from trilogy.core.models import Concept, Environment, Conditional, WhereClause
4
- from trilogy.core.processing.nodes import MergeNode, History, StrategyNode
5
3
  import networkx as nx
6
- from trilogy.core.graph_models import concept_to_node
4
+ from networkx.algorithms import approximation as ax
5
+
7
6
  from trilogy.constants import logger
8
- from trilogy.utility import unique
7
+ from trilogy.core.enums import PurposeLineage
9
8
  from trilogy.core.exceptions import AmbiguousRelationshipResolutionException
9
+ from trilogy.core.graph_models import concept_to_node
10
+ from trilogy.core.models import Concept, Conditional, Environment, WhereClause
11
+ from trilogy.core.processing.nodes import History, MergeNode, StrategyNode
10
12
  from trilogy.core.processing.utility import padding
11
- from networkx.algorithms import approximation as ax
12
- from trilogy.core.enums import PurposeLineage
13
-
13
+ from trilogy.utility import unique
14
14
 
15
15
  LOGGER_PREFIX = "[GEN_MERGE_NODE]"
16
16
  AMBIGUITY_CHECK_LIMIT = 20
@@ -194,7 +194,6 @@ def resolve_weak_components(
194
194
  filter_downstream: bool = True,
195
195
  accept_partial: bool = False,
196
196
  ) -> list[list[Concept]] | None:
197
-
198
197
  break_flag = False
199
198
  found = []
200
199
  search_graph = environment_graph.copy()
@@ -355,7 +354,6 @@ def gen_merge_node(
355
354
  conditions: Conditional | None = None,
356
355
  search_conditions: WhereClause | None = None,
357
356
  ) -> Optional[MergeNode]:
358
-
359
357
  if search_conditions:
360
358
  all_concepts = unique(all_concepts + search_conditions.row_arguments, "address")
361
359
  for filter_downstream in [True, False]:
@@ -1,19 +1,19 @@
1
+ from typing import List
2
+
3
+ from trilogy.constants import logger
4
+ from trilogy.core.enums import PurposeLineage
1
5
  from trilogy.core.models import (
2
6
  Concept,
3
7
  Environment,
4
- SelectStatement,
8
+ MultiSelectStatement,
5
9
  RowsetDerivationStatement,
6
10
  RowsetItem,
7
- MultiSelectStatement,
11
+ SelectStatement,
8
12
  WhereClause,
9
13
  )
10
- from trilogy.core.processing.nodes import MergeNode, History, StrategyNode
11
- from typing import List
12
-
13
- from trilogy.core.enums import PurposeLineage
14
- from trilogy.constants import logger
15
- from trilogy.core.processing.utility import padding, concept_to_relevant_joins
14
+ from trilogy.core.processing.nodes import History, MergeNode, StrategyNode
16
15
  from trilogy.core.processing.nodes.base_node import concept_list_to_grain
16
+ from trilogy.core.processing.utility import concept_to_relevant_joins, padding
17
17
 
18
18
  LOGGER_PREFIX = "[GEN_ROWSET_NODE]"
19
19
 
@@ -37,7 +37,8 @@ def gen_rowset_node(
37
37
  lineage: RowsetItem = concept.lineage
38
38
  rowset: RowsetDerivationStatement = lineage.rowset
39
39
  select: SelectStatement | MultiSelectStatement = lineage.rowset.select
40
- node = get_query_node(environment, select, graph=g, history=history)
40
+
41
+ node = get_query_node(environment, select)
41
42
 
42
43
  if not node:
43
44
  logger.info(
@@ -93,15 +94,22 @@ def gen_rowset_node(
93
94
  logger.info(
94
95
  f"{padding(depth)}{LOGGER_PREFIX} no enrichment required for rowset node as all optional found or no optional; exiting early."
95
96
  )
96
- # node.set_preexisting_conditions(conditions.conditional if conditions else None)
97
97
  return node
98
-
99
- possible_joins = concept_to_relevant_joins(node.output_concepts)
98
+ possible_joins = concept_to_relevant_joins(
99
+ [x for x in node.output_concepts if x.derivation != PurposeLineage.ROWSET]
100
+ )
101
+ logger.info({x.address: x.keys for x in possible_joins})
100
102
  if not possible_joins:
101
103
  logger.info(
102
104
  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]}"
103
105
  )
104
106
  return node
107
+ if any(x.derivation == PurposeLineage.ROWSET for x in possible_joins):
108
+ logger.info(
109
+ f"{padding(depth)}{LOGGER_PREFIX} cannot enrich rowset node with rowset concepts; exiting early"
110
+ )
111
+ return node
112
+ logger.info([x.address for x in possible_joins + local_optional])
105
113
  enrich_node: MergeNode = source_concepts( # this fetches the parent + join keys
106
114
  # to then connect to the rest of the query
107
115
  mandatory_list=possible_joins + local_optional,
@@ -109,15 +117,28 @@ def gen_rowset_node(
109
117
  g=g,
110
118
  depth=depth + 1,
111
119
  conditions=conditions,
120
+ history=history,
112
121
  )
113
122
  if not enrich_node:
114
123
  logger.info(
115
124
  f"{padding(depth)}{LOGGER_PREFIX} Cannot generate rowset enrichment node for {concept} with optional {local_optional}, returning just rowset node"
116
125
  )
117
126
  return node
127
+
128
+ non_hidden = [
129
+ x for x in node.output_concepts if x.address not in node.hidden_concepts
130
+ ]
131
+ for x in possible_joins:
132
+ if x.address in node.hidden_concepts:
133
+ node.unhide_output_concepts([x])
134
+ non_hidden_enrich = [
135
+ x
136
+ for x in enrich_node.output_concepts
137
+ if x.address not in enrich_node.hidden_concepts
138
+ ]
118
139
  return MergeNode(
119
- input_concepts=enrich_node.output_concepts + node.output_concepts,
120
- output_concepts=node.output_concepts + local_optional,
140
+ input_concepts=non_hidden + non_hidden_enrich,
141
+ output_concepts=non_hidden + local_optional,
121
142
  environment=environment,
122
143
  g=g,
123
144
  depth=depth,
@@ -125,6 +146,6 @@ def gen_rowset_node(
125
146
  node,
126
147
  enrich_node,
127
148
  ],
128
- partial_concepts=node.partial_concepts,
149
+ partial_concepts=node.partial_concepts + enrich_node.partial_concepts,
129
150
  preexisting_conditions=conditions.conditional if conditions else None,
130
151
  )
@@ -1,28 +1,29 @@
1
1
  from typing import List, Optional
2
2
 
3
+ import networkx as nx
4
+
5
+ from trilogy.constants import logger
6
+ from trilogy.core.enums import PurposeLineage
7
+ from trilogy.core.graph_models import concept_to_node
3
8
  from trilogy.core.models import (
4
9
  Concept,
10
+ Datasource,
5
11
  Environment,
6
12
  Grain,
7
- Datasource,
8
- WhereClause,
9
13
  LooseConceptList,
14
+ WhereClause,
10
15
  )
11
16
  from trilogy.core.processing.nodes import (
12
- MergeNode,
13
- StrategyNode,
14
- GroupNode,
15
17
  ConstantNode,
18
+ GroupNode,
19
+ MergeNode,
16
20
  SelectNode,
21
+ StrategyNode,
17
22
  )
18
- import networkx as nx
19
- from trilogy.core.graph_models import concept_to_node
20
- from trilogy.constants import logger
21
- from trilogy.core.processing.utility import padding
22
- from trilogy.core.enums import PurposeLineage
23
23
  from trilogy.core.processing.nodes.base_node import (
24
24
  concept_list_to_grain,
25
25
  )
26
+ from trilogy.core.processing.utility import padding
26
27
 
27
28
  LOGGER_PREFIX = "[GEN_ROOT_MERGE_NODE]"
28
29
 
@@ -1,19 +1,19 @@
1
+ from trilogy.constants import logger
1
2
  from trilogy.core.enums import PurposeLineage
3
+ from trilogy.core.exceptions import NoDatasourceException
2
4
  from trilogy.core.models import (
3
5
  Concept,
4
6
  Environment,
5
7
  LooseConceptList,
6
8
  WhereClause,
7
9
  )
10
+ from trilogy.core.processing.node_generators.select_merge_node import (
11
+ gen_select_merge_node,
12
+ )
8
13
  from trilogy.core.processing.nodes import (
9
14
  StrategyNode,
10
15
  )
11
- from trilogy.core.exceptions import NoDatasourceException
12
- from trilogy.constants import logger
13
16
  from trilogy.core.processing.utility import padding
14
- from trilogy.core.processing.node_generators.select_merge_node import (
15
- gen_select_merge_node,
16
- )
17
17
 
18
18
  LOGGER_PREFIX = "[GEN_SELECT_NODE]"
19
19
 
@@ -0,0 +1,75 @@
1
+ from typing import List
2
+
3
+ from trilogy.constants import logger
4
+ from trilogy.core.enums import FunctionType, Purpose
5
+ from trilogy.core.models import Concept, Function, WhereClause
6
+ from trilogy.core.processing.nodes import History, StrategyNode, UnionNode
7
+ from trilogy.core.processing.utility import padding
8
+
9
+ LOGGER_PREFIX = "[GEN_UNION_NODE]"
10
+
11
+
12
+ def is_union(c: Concept):
13
+ return isinstance(c.lineage, Function) and c.lineage.operator == FunctionType.UNION
14
+
15
+
16
+ def gen_union_node(
17
+ concept: Concept,
18
+ local_optional: List[Concept],
19
+ environment,
20
+ g,
21
+ depth: int,
22
+ source_concepts,
23
+ history: History | None = None,
24
+ conditions: WhereClause | None = None,
25
+ ) -> StrategyNode | None:
26
+ all_unions = [x for x in local_optional if is_union(x)] + [concept]
27
+
28
+ parents = []
29
+ keys = [x for x in all_unions if x.purpose == Purpose.KEY]
30
+ base = keys.pop()
31
+ remaining = [x for x in all_unions if x.address != base.address]
32
+ arguments = []
33
+ if isinstance(base.lineage, Function):
34
+ arguments = base.lineage.concept_arguments
35
+ for arg in arguments:
36
+ relevant_parents: list[Concept] = []
37
+ for other_union in remaining:
38
+ assert other_union.lineage
39
+ potential_parents = [
40
+ z for z in other_union.lineage.arguments if isinstance(z, Concept)
41
+ ]
42
+ relevant_parents += [
43
+ x for x in potential_parents if x.keys and arg.address in x.keys
44
+ ]
45
+ logger.info(
46
+ f"For parent arg {arg.address}, including additional union inputs {[c.address for c in relevant_parents]}"
47
+ )
48
+ parent: StrategyNode = source_concepts(
49
+ mandatory_list=[arg] + relevant_parents,
50
+ environment=environment,
51
+ g=g,
52
+ depth=depth + 1,
53
+ history=history,
54
+ conditions=conditions,
55
+ )
56
+ parent.hide_output_concepts(parent.output_concepts)
57
+ # parent.remove_output_concepts(parent.output_concepts)
58
+ parent.add_output_concept(concept)
59
+ for x in remaining:
60
+ parent.add_output_concept(x)
61
+
62
+ parents.append(parent)
63
+ if not parent:
64
+ logger.info(
65
+ f"{padding(depth)}{LOGGER_PREFIX} could not find union node parents"
66
+ )
67
+ return None
68
+
69
+ return UnionNode(
70
+ input_concepts=[concept] + local_optional,
71
+ output_concepts=[concept] + local_optional,
72
+ environment=environment,
73
+ g=g,
74
+ parents=parents,
75
+ )
@@ -1,10 +1,9 @@
1
1
  from typing import List
2
2
 
3
-
3
+ from trilogy.constants import logger
4
4
  from trilogy.core.models import Concept, Function, WhereClause
5
- from trilogy.core.processing.nodes import UnnestNode, History, StrategyNode
5
+ from trilogy.core.processing.nodes import History, StrategyNode, UnnestNode
6
6
  from trilogy.core.processing.utility import padding
7
- from trilogy.constants import logger
8
7
 
9
8
  LOGGER_PREFIX = "[GEN_UNNEST_NODE]"
10
9
 
@@ -1,11 +1,10 @@
1
1
  from typing import List
2
2
 
3
-
4
- from trilogy.core.models import Concept, WindowItem, Environment, WhereClause
5
- from trilogy.utility import unique
6
- from trilogy.core.processing.nodes import WindowNode, StrategyNode, History
7
3
  from trilogy.constants import logger
4
+ from trilogy.core.models import Concept, Environment, WhereClause, WindowItem
5
+ from trilogy.core.processing.nodes import History, StrategyNode, WindowNode
8
6
  from trilogy.core.processing.utility import padding
7
+ from trilogy.utility import unique
9
8
 
10
9
  LOGGER_PREFIX = "[GEN_WINDOW_NODE]"
11
10
 
@@ -1,12 +1,15 @@
1
+ from pydantic import BaseModel, ConfigDict, Field
2
+
3
+ from trilogy.core.models import Concept, Environment, WhereClause
4
+
5
+ from .base_node import NodeJoin, StrategyNode
1
6
  from .filter_node import FilterNode
2
7
  from .group_node import GroupNode
3
8
  from .merge_node import MergeNode
4
- from .select_node_v2 import SelectNode, ConstantNode
5
- from .window_node import WindowNode
6
- from .base_node import StrategyNode, NodeJoin
9
+ from .select_node_v2 import ConstantNode, SelectNode
10
+ from .union_node import UnionNode
7
11
  from .unnest_node import UnnestNode
8
- from pydantic import BaseModel, Field, ConfigDict
9
- from trilogy.core.models import Concept, Environment, WhereClause
12
+ from .window_node import WindowNode
10
13
 
11
14
 
12
15
  class History(BaseModel):
@@ -160,5 +163,6 @@ __all__ = [
160
163
  "NodeJoin",
161
164
  "ConstantNode",
162
165
  "UnnestNode",
166
+ "UnionNode",
163
167
  "History",
164
168
  ]
@@ -1,24 +1,29 @@
1
- from typing import List, Optional, Sequence
2
1
  from collections import defaultdict
2
+ from dataclasses import dataclass
3
+ from typing import List, Optional, Sequence
3
4
 
5
+ from trilogy.core.enums import (
6
+ BooleanOperator,
7
+ Granularity,
8
+ JoinType,
9
+ Purpose,
10
+ PurposeLineage,
11
+ )
4
12
  from trilogy.core.models import (
5
- Grain,
6
- QueryDatasource,
7
- SourceType,
13
+ Comparison,
8
14
  Concept,
9
- Environment,
15
+ ConceptPair,
10
16
  Conditional,
11
- UnnestJoin,
12
17
  Datasource,
13
- Comparison,
14
- Parenthetical,
18
+ Environment,
19
+ Grain,
15
20
  LooseConceptList,
16
- ConceptPair,
21
+ Parenthetical,
22
+ QueryDatasource,
23
+ SourceType,
24
+ UnnestJoin,
17
25
  )
18
- from trilogy.core.enums import Purpose, JoinType, PurposeLineage, Granularity
19
26
  from trilogy.utility import unique
20
- from dataclasses import dataclass
21
- from trilogy.core.enums import BooleanOperator
22
27
 
23
28
 
24
29
  def concept_list_to_grain(
@@ -67,7 +72,6 @@ def resolve_concept_map(
67
72
  ]:
68
73
  continue
69
74
  if concept.address in full_addresses:
70
-
71
75
  concept_map[concept.address].add(input)
72
76
  elif concept.address not in concept_map:
73
77
  # equi_targets = [x for x in targets if concept.address in x.pseudonyms or x.address in concept.pseudonyms]
@@ -207,8 +211,22 @@ class StrategyNode:
207
211
  operator=BooleanOperator.AND,
208
212
  )
209
213
  self.validate_parents()
214
+ self.validate_inputs()
210
215
  self.log = True
211
216
 
217
+ def validate_inputs(self):
218
+ if not self.parents:
219
+ return
220
+ non_hidden = set()
221
+ for x in self.parents:
222
+ for z in x.usable_outputs:
223
+ non_hidden.add(z.address)
224
+ if not all([x.address in non_hidden for x in self.input_concepts]):
225
+ missing = [x for x in self.input_concepts if x.address not in non_hidden]
226
+ raise ValueError(
227
+ f"Invalid input concepts; {missing} are missing non-hidden parent nodes"
228
+ )
229
+
212
230
  def add_parents(self, parents: list["StrategyNode"]):
213
231
  self.parents += parents
214
232
  self.validate_parents()
@@ -284,6 +302,14 @@ class StrategyNode:
284
302
  self.rebuild_cache()
285
303
  return self
286
304
 
305
+ def unhide_output_concepts(self, concepts: List[Concept], rebuild: bool = True):
306
+ self.hidden_concepts = [
307
+ x for x in self.hidden_concepts if x.address not in concepts
308
+ ]
309
+ if rebuild:
310
+ self.rebuild_cache()
311
+ return self
312
+
287
313
  def remove_output_concepts(self, concepts: List[Concept], rebuild: bool = True):
288
314
  for x in concepts:
289
315
  self.hidden_concepts.append(x)
@@ -295,6 +321,12 @@ class StrategyNode:
295
321
  self.rebuild_cache()
296
322
  return self
297
323
 
324
+ @property
325
+ def usable_outputs(self) -> list[Concept]:
326
+ return [
327
+ x for x in self.output_concepts if x.address not in self.hidden_concepts
328
+ ]
329
+
298
330
  @property
299
331
  def logging_prefix(self) -> str:
300
332
  return "\t" * self.depth
@@ -1,13 +1,12 @@
1
1
  from typing import List
2
2
 
3
-
4
3
  from trilogy.core.models import (
5
- SourceType,
4
+ Comparison,
6
5
  Concept,
7
6
  Conditional,
8
- Comparison,
9
- Parenthetical,
10
7
  Grain,
8
+ Parenthetical,
9
+ SourceType,
11
10
  )
12
11
  from trilogy.core.processing.nodes.base_node import StrategyNode
13
12
 
@@ -2,25 +2,24 @@ from typing import List, Optional
2
2
 
3
3
  from trilogy.constants import logger
4
4
  from trilogy.core.models import (
5
- Grain,
6
- QueryDatasource,
7
- Datasource,
8
- SourceType,
5
+ Comparison,
9
6
  Concept,
7
+ Conditional,
8
+ Datasource,
10
9
  Environment,
10
+ Grain,
11
11
  LooseConceptList,
12
- Conditional,
13
- Comparison,
14
12
  Parenthetical,
13
+ QueryDatasource,
14
+ SourceType,
15
15
  )
16
16
  from trilogy.core.processing.nodes.base_node import (
17
17
  StrategyNode,
18
- resolve_concept_map,
19
18
  concept_list_to_grain,
19
+ resolve_concept_map,
20
20
  )
21
+ from trilogy.core.processing.utility import find_nullable_concepts, is_scalar_condition
21
22
  from trilogy.utility import unique
22
- from trilogy.core.processing.utility import is_scalar_condition
23
- from trilogy.core.processing.utility import find_nullable_concepts
24
23
 
25
24
  LOGGER_PREFIX = "[CONCEPT DETAIL - GROUP NODE]"
26
25
 
@@ -96,7 +95,6 @@ class GroupNode(StrategyNode):
96
95
  # otherwise if no group by, just treat it as a select
97
96
  source_type = SourceType.SELECT
98
97
  else:
99
-
100
98
  logger.info(
101
99
  f"{self.logging_prefix}{LOGGER_PREFIX} Group node has different grain than parents; forcing group"
102
100
  f" upstream grains {[str(source.grain) for source in parent_sources]}"
@@ -107,9 +105,9 @@ class GroupNode(StrategyNode):
107
105
  logger.info(
108
106
  f"{self.logging_prefix}{LOGGER_PREFIX} Parent node"
109
107
  f" {[c.address for c in parent.output_concepts[:2]]}... has"
110
- " grain"
108
+ " set node grain"
111
109
  f" {parent.grain}"
112
- f" resolved grain {parent.resolve().grain}"
110
+ f" and resolved grain {parent.resolve().grain}"
113
111
  f" {type(parent)}"
114
112
  )
115
113
  source_type = SourceType.GROUP
@@ -148,7 +146,13 @@ class GroupNode(StrategyNode):
148
146
  # inject an additional CTE
149
147
  if self.conditions and not is_scalar_condition(self.conditions):
150
148
  base.condition = None
151
- base.output_concepts = self.output_concepts + self.conditions.row_arguments
149
+ base.output_concepts = unique(
150
+ base.output_concepts + self.conditions.row_arguments, "address"
151
+ )
152
+ # re-visible any hidden concepts
153
+ base.hidden_concepts = [
154
+ x for x in base.hidden_concepts if x not in base.output_concepts
155
+ ]
152
156
  source_map = resolve_concept_map(
153
157
  [base],
154
158
  targets=self.output_concepts,
@@ -1,28 +1,27 @@
1
1
  from typing import List, Optional, Tuple
2
2
 
3
-
4
3
  from trilogy.constants import logger
5
4
  from trilogy.core.models import (
6
5
  BaseJoin,
6
+ Comparison,
7
+ Concept,
8
+ Conditional,
9
+ Datasource,
10
+ Environment,
7
11
  Grain,
8
12
  JoinType,
13
+ Parenthetical,
9
14
  QueryDatasource,
10
- Datasource,
11
15
  SourceType,
12
- Concept,
13
16
  UnnestJoin,
14
- Conditional,
15
- Comparison,
16
- Parenthetical,
17
- Environment,
18
17
  )
19
- from trilogy.utility import unique
20
18
  from trilogy.core.processing.nodes.base_node import (
19
+ NodeJoin,
21
20
  StrategyNode,
22
21
  resolve_concept_map,
23
- NodeJoin,
24
22
  )
25
- from trilogy.core.processing.utility import get_node_joins, find_nullable_concepts
23
+ from trilogy.core.processing.utility import find_nullable_concepts, get_node_joins
24
+ from trilogy.utility import unique
26
25
 
27
26
  LOGGER_PREFIX = "[CONCEPT DETAIL - MERGE NODE]"
28
27
 
@@ -37,7 +36,8 @@ def deduplicate_nodes(
37
36
  set_map: dict[str, set[str]] = {}
38
37
  for k, v in merged.items():
39
38
  unique_outputs = [
40
- environment.concepts[x.address].address
39
+ # the concept may be a in a different environment for a rowset.
40
+ (environment.concepts.get(x.address) or x).address
41
41
  for x in v.output_concepts
42
42
  if x not in v.partial_concepts
43
43
  ]
@@ -330,8 +330,9 @@ class MergeNode(StrategyNode):
330
330
  force_group = None
331
331
 
332
332
  qd_joins: List[BaseJoin | UnnestJoin] = [*joins]
333
+
333
334
  source_map = resolve_concept_map(
334
- list(merged.values()),
335
+ final_datasets,
335
336
  targets=self.output_concepts,
336
337
  inherited_inputs=self.input_concepts + self.existence_concepts,
337
338
  full_joins=full_join_concepts,
@@ -339,6 +340,7 @@ class MergeNode(StrategyNode):
339
340
  nullable_concepts = find_nullable_concepts(
340
341
  source_map=source_map, joins=joins, datasources=final_datasets
341
342
  )
343
+
342
344
  qds = QueryDatasource(
343
345
  input_concepts=unique(self.input_concepts, "address"),
344
346
  output_concepts=unique(self.output_concepts, "address"),
@@ -1,25 +1,23 @@
1
1
  from typing import List, Optional
2
2
 
3
-
4
3
  from trilogy.constants import logger
5
4
  from trilogy.core.constants import CONSTANT_DATASET
6
5
  from trilogy.core.enums import Purpose, PurposeLineage
7
6
  from trilogy.core.models import (
7
+ Comparison,
8
+ Concept,
9
+ Conditional,
10
+ Datasource,
11
+ Environment,
8
12
  Function,
9
13
  Grain,
14
+ Parenthetical,
10
15
  QueryDatasource,
11
16
  SourceType,
12
- Concept,
13
- Environment,
14
17
  UnnestJoin,
15
- Datasource,
16
- Conditional,
17
- Comparison,
18
- Parenthetical,
19
18
  )
20
- from trilogy.utility import unique
21
19
  from trilogy.core.processing.nodes.base_node import StrategyNode, resolve_concept_map
22
-
20
+ from trilogy.utility import unique
23
21
 
24
22
  LOGGER_PREFIX = "[CONCEPT DETAIL - SELECT NODE]"
25
23
 
@@ -69,6 +67,11 @@ class SelectNode(StrategyNode):
69
67
  self.accept_partial = accept_partial
70
68
  self.datasource = datasource
71
69
 
70
+ def validate_inputs(self):
71
+ # we do not need to validate inputs for a select node
72
+ # as it will be a root
73
+ return
74
+
72
75
  def resolve_from_provided_datasource(
73
76
  self,
74
77
  ) -> QueryDatasource:
@@ -98,6 +101,7 @@ class SelectNode(StrategyNode):
98
101
  PurposeLineage.BASIC,
99
102
  PurposeLineage.ROWSET,
100
103
  PurposeLineage.BASIC,
104
+ PurposeLineage.UNION,
101
105
  ):
102
106
  source_map[x.address] = set()
103
107