pytrilogy 0.0.2.56__tar.gz → 0.0.2.58__tar.gz

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 (114) hide show
  1. {pytrilogy-0.0.2.56/pytrilogy.egg-info → pytrilogy-0.0.2.58}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_models.py +4 -2
  4. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_select.py +1 -1
  5. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/__init__.py +1 -1
  6. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/functions.py +2 -1
  7. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/models.py +38 -30
  8. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/optimization.py +3 -6
  9. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/concept_strategies_v3.py +21 -15
  10. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/basic_node.py +4 -1
  11. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/common.py +1 -1
  12. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/group_to_node.py +10 -0
  13. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/multiselect_node.py +16 -18
  14. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/rowset_node.py +5 -2
  15. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/select_merge_node.py +8 -1
  16. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/nodes/base_node.py +21 -11
  17. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/nodes/group_node.py +42 -45
  18. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/nodes/merge_node.py +3 -2
  19. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/nodes/select_node_v2.py +1 -1
  20. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/utility.py +6 -7
  21. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/query_processor.py +2 -2
  22. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/dialect/base.py +9 -6
  23. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/utility.py +5 -2
  24. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/LICENSE.md +0 -0
  25. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/README.md +0 -0
  26. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/pyproject.toml +0 -0
  27. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/pytrilogy.egg-info/SOURCES.txt +0 -0
  28. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/pytrilogy.egg-info/dependency_links.txt +0 -0
  29. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/pytrilogy.egg-info/entry_points.txt +0 -0
  30. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/pytrilogy.egg-info/requires.txt +0 -0
  31. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/pytrilogy.egg-info/top_level.txt +0 -0
  32. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/setup.cfg +0 -0
  33. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/setup.py +0 -0
  34. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_datatypes.py +0 -0
  35. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_declarations.py +0 -0
  36. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_derived_concepts.py +0 -0
  37. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_discovery_nodes.py +0 -0
  38. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_enums.py +0 -0
  39. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_environment.py +0 -0
  40. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_executor.py +0 -0
  41. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_functions.py +0 -0
  42. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_imports.py +0 -0
  43. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_metadata.py +0 -0
  44. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_multi_join_assignments.py +0 -0
  45. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_parse_engine.py +0 -0
  46. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_parsing.py +0 -0
  47. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_partial_handling.py +0 -0
  48. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_query_processing.py +0 -0
  49. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_show.py +0 -0
  50. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_statements.py +0 -0
  51. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_undefined_concept.py +0 -0
  52. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/tests/test_where_clause.py +0 -0
  53. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/compiler.py +0 -0
  54. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/constants.py +0 -0
  55. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/__init__.py +0 -0
  56. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/constants.py +0 -0
  57. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/enums.py +0 -0
  58. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/env_processor.py +0 -0
  59. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/environment_helpers.py +0 -0
  60. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/ergonomics.py +0 -0
  61. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/exceptions.py +0 -0
  62. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/graph_models.py +0 -0
  63. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/internal.py +0 -0
  64. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/optimizations/__init__.py +0 -0
  65. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/optimizations/base_optimization.py +0 -0
  66. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/optimizations/inline_constant.py +0 -0
  67. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/optimizations/inline_datasource.py +0 -0
  68. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  69. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/__init__.py +0 -0
  70. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/graph_utils.py +0 -0
  71. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/__init__.py +0 -0
  72. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  73. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/group_node.py +0 -0
  74. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  75. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  76. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  77. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/select_node.py +0 -0
  78. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/union_node.py +0 -0
  79. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  80. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/node_generators/window_node.py +0 -0
  81. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/nodes/__init__.py +0 -0
  82. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/nodes/filter_node.py +0 -0
  83. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/nodes/union_node.py +0 -0
  84. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  85. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/core/processing/nodes/window_node.py +0 -0
  86. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/dialect/__init__.py +0 -0
  87. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/dialect/bigquery.py +0 -0
  88. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/dialect/common.py +0 -0
  89. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/dialect/config.py +0 -0
  90. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/dialect/duckdb.py +0 -0
  91. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/dialect/enums.py +0 -0
  92. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/dialect/postgres.py +0 -0
  93. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/dialect/presto.py +0 -0
  94. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/dialect/snowflake.py +0 -0
  95. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/dialect/sql_server.py +0 -0
  96. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/engine.py +0 -0
  97. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/executor.py +0 -0
  98. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/hooks/__init__.py +0 -0
  99. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/hooks/base_hook.py +0 -0
  100. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/hooks/graph_hook.py +0 -0
  101. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/hooks/query_debugger.py +0 -0
  102. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/metadata/__init__.py +0 -0
  103. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/parser.py +0 -0
  104. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/parsing/__init__.py +0 -0
  105. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/parsing/common.py +0 -0
  106. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/parsing/config.py +0 -0
  107. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/parsing/exceptions.py +0 -0
  108. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/parsing/helpers.py +0 -0
  109. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/parsing/parse_engine.py +0 -0
  110. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/parsing/render.py +0 -0
  111. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/parsing/trilogy.lark +0 -0
  112. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/py.typed +0 -0
  113. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/scripts/__init__.py +0 -0
  114. {pytrilogy-0.0.2.56 → pytrilogy-0.0.2.58}/trilogy/scripts/trilogy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.56
3
+ Version: 0.0.2.58
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.56
3
+ Version: 0.0.2.58
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -147,12 +147,14 @@ def test_select(test_environment: Environment):
147
147
  pid = test_environment.concepts["product_id"]
148
148
  cid = test_environment.concepts["category_id"]
149
149
  cname = test_environment.concepts["category_name"]
150
- x = SelectStatement(selection=[oid, pid, cid, cname])
150
+ x = SelectStatement(
151
+ selection=[oid, pid, cid, cname], grain=Grain(components=[oid, pid, cid])
152
+ )
151
153
  ds = x.to_datasource(
152
154
  test_environment.namespace, "test", address=Address(location="test")
153
155
  )
154
156
 
155
- assert ds.grain == Grain(components=[oid, pid, cid])
157
+ assert ds.grain.components == Grain(components=[oid, pid, cid]).components
156
158
 
157
159
 
158
160
  def test_undefined(test_environment: Environment):
@@ -123,7 +123,7 @@ def test_modifiers():
123
123
  ;"""
124
124
  env, parsed = parse(q1)
125
125
  select: SelectStatement = parsed[-1]
126
- assert select.hidden_components == [env.concepts["b"]]
126
+ assert select.hidden_components == set([env.concepts["b"].address])
127
127
  assert select.output_components == [env.concepts["a"], env.concepts["b"]]
128
128
  query = process_query(statement=select, environment=env)
129
129
 
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.2.56"
7
+ __version__ = "0.0.2.58"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -127,10 +127,11 @@ def Unnest(args: list[Concept]) -> Function:
127
127
 
128
128
  def Group(args: list[Concept]) -> Function:
129
129
  output = args[0]
130
+ datatype = arg_to_datatype(output)
130
131
  return Function(
131
132
  operator=FunctionType.GROUP,
132
133
  arguments=args,
133
- output_datatype=output.datatype,
134
+ output_datatype=datatype,
134
135
  output_purpose=Purpose.PROPERTY,
135
136
  arg_count=-1,
136
137
  )
@@ -923,9 +923,16 @@ class Grain(Namespaced, BaseModel):
923
923
  if not self.where_clause:
924
924
  where = other.where_clause
925
925
  elif not other.where_clause == self.where_clause:
926
- raise NotImplementedError(
927
- f"Cannot merge grains with where clauses, self {self.where_clause} other {other.where_clause}"
926
+ where = WhereClause(
927
+ conditional=Conditional(
928
+ left=self.where_clause.conditional,
929
+ right=other.where_clause.conditional,
930
+ operator=BooleanOperator.AND,
931
+ )
928
932
  )
933
+ # raise NotImplementedError(
934
+ # f"Cannot merge grains with where clauses, self {self.where_clause} other {other.where_clause}"
935
+ # )
929
936
  return Grain(
930
937
  components=self.components.union(other.components), where_clause=where
931
938
  )
@@ -1863,11 +1870,11 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1863
1870
  return output
1864
1871
 
1865
1872
  @property
1866
- def hidden_components(self) -> List[Concept]:
1867
- output = []
1873
+ def hidden_components(self) -> set[str]:
1874
+ output = set()
1868
1875
  for item in self.selection:
1869
1876
  if isinstance(item, SelectItem) and Modifier.HIDDEN in item.modifiers:
1870
- output.append(item.output)
1877
+ output.add(item.output.address)
1871
1878
  return output
1872
1879
 
1873
1880
  @property
@@ -2097,10 +2104,10 @@ class MultiSelectStatement(HasUUID, SelectTypeMixin, Mergeable, Namespaced, Base
2097
2104
 
2098
2105
  @computed_field # type: ignore
2099
2106
  @cached_property
2100
- def hidden_components(self) -> List[Concept]:
2101
- output = []
2107
+ def hidden_components(self) -> set[str]:
2108
+ output: set[str] = set()
2102
2109
  for select in self.selects:
2103
- output += select.hidden_components
2110
+ output = output.union(select.hidden_components)
2104
2111
  return output
2105
2112
 
2106
2113
 
@@ -2182,6 +2189,10 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2182
2189
  def duplicate(self) -> Datasource:
2183
2190
  return self.model_copy(deep=True)
2184
2191
 
2192
+ @property
2193
+ def hidden_concepts(self) -> List[Concept]:
2194
+ return []
2195
+
2185
2196
  def merge_concept(
2186
2197
  self, source: Concept, target: Concept, modifiers: List[Modifier]
2187
2198
  ):
@@ -2254,17 +2265,7 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2254
2265
  @field_validator("grain", mode="before")
2255
2266
  @classmethod
2256
2267
  def grain_enforcement(cls, v: Grain, info: ValidationInfo):
2257
- values = info.data
2258
2268
  grain: Grain = safe_grain(v)
2259
- if not grain.components:
2260
- columns: List[ColumnAssignment] = values.get("columns", [])
2261
- grain = Grain.from_concepts(
2262
- [
2263
- c.concept.with_grain(Grain())
2264
- for c in columns
2265
- if c.concept.purpose == Purpose.KEY
2266
- ]
2267
- )
2268
2269
  return grain
2269
2270
 
2270
2271
  def add_column(
@@ -2507,7 +2508,7 @@ class QueryDatasource(BaseModel):
2507
2508
  filter_concepts: List[Concept] = Field(default_factory=list)
2508
2509
  source_type: SourceType = SourceType.SELECT
2509
2510
  partial_concepts: List[Concept] = Field(default_factory=list)
2510
- hidden_concepts: List[Concept] = Field(default_factory=list)
2511
+ hidden_concepts: set[str] = Field(default_factory=set)
2511
2512
  nullable_concepts: List[Concept] = Field(default_factory=list)
2512
2513
  join_derived_concepts: List[Concept] = Field(default_factory=list)
2513
2514
  force_group: bool | None = None
@@ -2659,10 +2660,10 @@ class QueryDatasource(BaseModel):
2659
2660
  final_source_map[k] = set(
2660
2661
  merged_datasources.get(x.safe_identifier, x) for x in list(v)
2661
2662
  )
2662
- self_hidden = self.hidden_concepts or []
2663
- other_hidden = other.hidden_concepts or []
2663
+ self_hidden: set[str] = self.hidden_concepts or set()
2664
+ other_hidden: set[str] = other.hidden_concepts or set()
2664
2665
  # hidden is the minimum overlapping set
2665
- hidden = [x for x in self_hidden if x.address in other_hidden]
2666
+ hidden = self_hidden.intersection(other_hidden)
2666
2667
  qds = QueryDatasource(
2667
2668
  input_concepts=unique(
2668
2669
  self.input_concepts + other.input_concepts, "address"
@@ -2760,7 +2761,7 @@ class CTE(BaseModel):
2760
2761
  partial_concepts: List[Concept] = Field(default_factory=list)
2761
2762
  nullable_concepts: List[Concept] = Field(default_factory=list)
2762
2763
  join_derived_concepts: List[Concept] = Field(default_factory=list)
2763
- hidden_concepts: List[Concept] = Field(default_factory=list)
2764
+ hidden_concepts: set[str] = Field(default_factory=set)
2764
2765
  order_by: Optional[OrderBy] = None
2765
2766
  limit: Optional[int] = None
2766
2767
  base_name_override: Optional[str] = None
@@ -2946,10 +2947,10 @@ class CTE(BaseModel):
2946
2947
  f" {self.name} {other.name} conditions {self.condition} {other.condition}"
2947
2948
  )
2948
2949
  raise ValueError(error)
2949
- mutually_hidden = []
2950
+ mutually_hidden = set()
2950
2951
  for concept in self.hidden_concepts:
2951
- if concept.address in other.hidden_concepts:
2952
- mutually_hidden.append(concept)
2952
+ if concept in other.hidden_concepts:
2953
+ mutually_hidden.add(concept)
2953
2954
  self.partial_concepts = unique(
2954
2955
  self.partial_concepts + other.partial_concepts, "address"
2955
2956
  )
@@ -3073,12 +3074,18 @@ class CTE(BaseModel):
3073
3074
  assert isinstance(c.lineage, RowsetItem)
3074
3075
  return check_is_not_in_group(c.lineage.content)
3075
3076
  if c.derivation == PurposeLineage.CONSTANT:
3076
- return False
3077
+ return True
3077
3078
  if c.purpose == Purpose.METRIC:
3078
3079
  return True
3079
- elif c.derivation == PurposeLineage.BASIC and c.lineage:
3080
+
3081
+ if c.derivation == PurposeLineage.BASIC and c.lineage:
3080
3082
  if all([check_is_not_in_group(x) for x in c.lineage.concept_arguments]):
3081
3083
  return True
3084
+ if (
3085
+ isinstance(c.lineage, Function)
3086
+ and c.lineage.operator == FunctionType.GROUP
3087
+ ):
3088
+ return check_is_not_in_group(c.lineage.concept_arguments[0])
3082
3089
  return False
3083
3090
 
3084
3091
  return (
@@ -3125,7 +3132,7 @@ class UnionCTE(BaseModel):
3125
3132
  operator: str = "UNION ALL"
3126
3133
  order_by: Optional[OrderBy] = None
3127
3134
  limit: Optional[int] = None
3128
- hidden_concepts: list[Concept] = Field(default_factory=list)
3135
+ hidden_concepts: set[str] = Field(default_factory=set)
3129
3136
  partial_concepts: list[Concept] = Field(default_factory=list)
3130
3137
  existence_source_map: Dict[str, list[str]] = Field(default_factory=dict)
3131
3138
 
@@ -3756,6 +3763,7 @@ class Environment(BaseModel):
3756
3763
  for k, v in self.concepts.items():
3757
3764
  if v.address == target.address:
3758
3765
  v.pseudonyms.add(source.address)
3766
+
3759
3767
  if v.address == source.address:
3760
3768
  replacements[k] = target
3761
3769
  v.pseudonyms.add(target.address)
@@ -4496,7 +4504,7 @@ class ProcessedQuery(BaseModel):
4496
4504
  base: CTE | UnionCTE
4497
4505
  joins: List[Join]
4498
4506
  grain: Grain
4499
- hidden_columns: List[Concept] = Field(default_factory=list)
4507
+ hidden_columns: set[str] = Field(default_factory=set)
4500
4508
  limit: Optional[int] = None
4501
4509
  where_clause: Optional[WhereClause] = None
4502
4510
  having_clause: Optional[HavingClause] = None
@@ -136,15 +136,12 @@ def is_direct_return_eligible(cte: CTE | UnionCTE) -> CTE | UnionCTE | None:
136
136
 
137
137
  assert isinstance(cte, CTE)
138
138
  derived_concepts = [
139
- c
140
- for c in cte.source.output_concepts + cte.source.hidden_concepts
141
- if c not in cte.source.input_concepts
139
+ c for c in cte.source.output_concepts if c not in cte.source.input_concepts
142
140
  ]
143
141
 
144
142
  parent_derived_concepts = [
145
143
  c
146
144
  for c in direct_parent.source.output_concepts
147
- + direct_parent.source.hidden_concepts
148
145
  if c not in direct_parent.source.input_concepts
149
146
  ]
150
147
  condition_arguments = cte.condition.row_arguments if cte.condition else []
@@ -180,8 +177,8 @@ def optimize_ctes(
180
177
  ):
181
178
  direct_parent.order_by = root_cte.order_by
182
179
  direct_parent.limit = root_cte.limit
183
- direct_parent.hidden_concepts = (
184
- root_cte.hidden_concepts + direct_parent.hidden_concepts
180
+ direct_parent.hidden_concepts = root_cte.hidden_concepts.union(
181
+ direct_parent.hidden_concepts
185
182
  )
186
183
  if root_cte.condition:
187
184
  if direct_parent.condition:
@@ -539,11 +539,14 @@ def validate_concept(
539
539
  found_addresses.add(concept.address)
540
540
  found_map[str(node)].add(concept)
541
541
  for v_address in concept.pseudonyms:
542
+ if v_address in seen:
543
+ return
542
544
  v = environment.concepts[v_address]
543
- if v == concept.address:
545
+ if v.address in seen:
544
546
  return
545
- if v in seen:
547
+ if v.address == concept.address:
546
548
  return
549
+
547
550
  validate_concept(
548
551
  v,
549
552
  node,
@@ -577,7 +580,7 @@ def validate_stack(
577
580
  resolved = node.resolve()
578
581
 
579
582
  for concept in resolved.output_concepts:
580
- if concept in resolved.hidden_concepts:
583
+ if concept.address in resolved.hidden_concepts:
581
584
  continue
582
585
  validate_concept(
583
586
  concept,
@@ -988,17 +991,20 @@ def source_query_concepts(
988
991
  raise ValueError(
989
992
  f"Could not resolve conections between {error_strings} from environment graph."
990
993
  )
991
- candidate = GroupNode(
992
- output_concepts=[
993
- x for x in root.output_concepts if x.address not in root.hidden_concepts
994
- ],
995
- input_concepts=[
996
- x for x in root.output_concepts if x.address not in root.hidden_concepts
997
- ],
994
+ final = [x for x in root.output_concepts if x.address not in root.hidden_concepts]
995
+ if GroupNode.check_if_required(
996
+ downstream_concepts=final,
997
+ parents=[root.resolve()],
998
998
  environment=environment,
999
- parents=[root],
1000
- partial_concepts=root.partial_concepts,
1001
- )
1002
- if not candidate.resolve().group_required:
1003
- return root
999
+ ).required:
1000
+ candidate: StrategyNode = GroupNode(
1001
+ output_concepts=final,
1002
+ input_concepts=final,
1003
+ environment=environment,
1004
+ parents=[root],
1005
+ partial_concepts=root.partial_concepts,
1006
+ )
1007
+ else:
1008
+ candidate = root
1009
+
1004
1010
  return candidate
@@ -13,6 +13,7 @@ from trilogy.core.processing.node_generators.common import (
13
13
  resolve_function_parent_concepts,
14
14
  )
15
15
  from trilogy.core.processing.nodes import History, StrategyNode
16
+ from trilogy.utility import unique
16
17
 
17
18
  LOGGER_PREFIX = "[GEN_BASIC_NODE]"
18
19
 
@@ -65,7 +66,9 @@ def gen_basic_node(
65
66
  non_equivalent_optional = [
66
67
  x for x in local_optional if x not in equivalent_optional
67
68
  ]
68
- all_parents = parent_concepts + non_equivalent_optional
69
+ all_parents: list[Concept] = unique(
70
+ parent_concepts + non_equivalent_optional, "address"
71
+ )
69
72
  logger.info(
70
73
  f"{depth_prefix}{LOGGER_PREFIX} Fetching parents {[x.address for x in all_parents]}"
71
74
  )
@@ -208,7 +208,7 @@ def gen_enrichment_node(
208
208
  non_hidden = [
209
209
  x
210
210
  for x in base_node.output_concepts
211
- if x.address not in [y.address for y in base_node.hidden_concepts]
211
+ if x.address not in base_node.hidden_concepts
212
212
  ]
213
213
  return MergeNode(
214
214
  input_concepts=unique(join_keys + extra_required + non_hidden, "address"),
@@ -26,6 +26,7 @@ def gen_group_to_node(
26
26
  # aggregates MUST always group to the proper grain
27
27
  if not isinstance(concept.lineage, Function):
28
28
  raise SyntaxError("Group to should have function lineage")
29
+ group_arg = concept.lineage.arguments[0]
29
30
  parent_concepts: List[Concept] = concept.lineage.concept_arguments
30
31
  logger.info(
31
32
  f"{padding(depth)}{LOGGER_PREFIX} group by node has required parents {[x.address for x in parent_concepts]}"
@@ -47,6 +48,13 @@ def gen_group_to_node(
47
48
  environment=environment,
48
49
  parents=parents,
49
50
  depth=depth,
51
+ preexisting_conditions=conditions.conditional if conditions else None,
52
+ hidden_concepts=set(
53
+ [group_arg.address]
54
+ if isinstance(group_arg, Concept)
55
+ and group_arg.address not in local_optional
56
+ else []
57
+ ),
50
58
  )
51
59
 
52
60
  # early exit if no optional
@@ -62,6 +70,7 @@ def gen_group_to_node(
62
70
  g=g,
63
71
  depth=depth + 1,
64
72
  history=history,
73
+ conditions=conditions,
65
74
  )
66
75
  if not enrich_node:
67
76
  logger.info(
@@ -83,4 +92,5 @@ def gen_group_to_node(
83
92
  ],
84
93
  whole_grain=True,
85
94
  depth=depth,
95
+ preexisting_conditions=conditions.conditional if conditions else None,
86
96
  )
@@ -69,6 +69,7 @@ def gen_multiselect_node(
69
69
  lineage: MultiSelectStatement = concept.lineage
70
70
 
71
71
  base_parents: List[StrategyNode] = []
72
+ partial = []
72
73
  for select in lineage.selects:
73
74
  snode: StrategyNode = source_concepts(
74
75
  mandatory_list=select.output_components,
@@ -103,6 +104,9 @@ def gen_multiselect_node(
103
104
  for mc in merge_concepts:
104
105
  assert mc in snode.resolve().output_concepts
105
106
  base_parents.append(snode)
107
+ if select.where_clause:
108
+ for item in select.output_components:
109
+ partial.append(item)
106
110
 
107
111
  node_joins = extra_align_joins(lineage, base_parents)
108
112
  node = MergeNode(
@@ -112,35 +116,28 @@ def gen_multiselect_node(
112
116
  depth=depth,
113
117
  parents=base_parents,
114
118
  node_joins=node_joins,
119
+ hidden_concepts=set([x for y in base_parents for x in y.hidden_concepts]),
115
120
  )
116
121
 
117
122
  enrichment = set([x.address for x in local_optional])
118
123
 
119
- rowset_relevant = [
124
+ multiselect_relevant = [
120
125
  x
121
126
  for x in lineage.derived_concepts
122
127
  if x.address == concept.address or x.address in enrichment
123
128
  ]
124
- additional_relevant = [
125
- x for x in select.output_components if x.address in enrichment
126
- ]
129
+ additional_relevant = [x for x in node.output_concepts if x.address in enrichment]
127
130
  # add in other other concepts
128
- for item in rowset_relevant:
129
- node.output_concepts.append(item)
130
- for item in additional_relevant:
131
- node.output_concepts.append(item)
132
- if select.where_clause:
133
- for item in additional_relevant:
134
- node.partial_concepts.append(item)
135
131
 
136
- # we need a better API for refreshing a nodes QDS
137
- node.resolution_cache = node._resolve()
132
+ node.set_output_concepts(multiselect_relevant + additional_relevant)
138
133
 
139
- # assume grain to be output of select
140
- # but don't include anything aggregate at this point
141
- node.resolution_cache.grain = Grain.from_concepts(
142
- node.output_concepts,
143
- )
134
+ # node.add_partial_concepts(partial)
135
+ # if select.where_clause:
136
+ # for item in additional_relevant:
137
+ # node.partial_concepts.append(item)
138
+ node.grain = Grain.from_concepts(node.output_concepts, environment=environment)
139
+ node.rebuild_cache()
140
+ # we need a better API for refreshing a nodes QDS
144
141
  possible_joins = concept_to_relevant_joins(additional_relevant)
145
142
  if not local_optional:
146
143
  logger.info(
@@ -159,6 +156,7 @@ def gen_multiselect_node(
159
156
  f"{padding(depth)}{LOGGER_PREFIX} all enriched concepts returned from base rowset node; exiting early"
160
157
  )
161
158
  return node
159
+
162
160
  enrich_node: MergeNode = source_concepts( # this fetches the parent + join keys
163
161
  # to then connect to the rest of the query
164
162
  mandatory_list=additional_relevant + local_optional,
@@ -47,7 +47,7 @@ def gen_rowset_node(
47
47
  return None
48
48
  enrichment = set([x.address for x in local_optional])
49
49
  rowset_relevant = [x for x in rowset.derived_concepts]
50
- select_hidden = set([x.address for x in select.hidden_components])
50
+ select_hidden = select.hidden_components
51
51
  rowset_hidden = [
52
52
  x
53
53
  for x in rowset.derived_concepts
@@ -80,7 +80,9 @@ def gen_rowset_node(
80
80
  for x in node.output_concepts
81
81
  if x.address
82
82
  not in [
83
- y for y in node.hidden_concepts if y.derivation != PurposeLineage.ROWSET
83
+ y
84
+ for y in node.hidden_concepts
85
+ if environment.concepts[y].derivation != PurposeLineage.ROWSET
84
86
  ]
85
87
  ],
86
88
  )
@@ -103,6 +105,7 @@ def gen_rowset_node(
103
105
  )
104
106
  return node
105
107
  if any(x.derivation == PurposeLineage.ROWSET for x in possible_joins):
108
+
106
109
  logger.info(
107
110
  f"{padding(depth)}{LOGGER_PREFIX} cannot enrich rowset node with rowset concepts; exiting early"
108
111
  )
@@ -240,10 +240,12 @@ def create_datasource_node(
240
240
  depth: int,
241
241
  conditions: WhereClause | None = None,
242
242
  ) -> tuple[StrategyNode, bool]:
243
- target_grain = Grain.from_concepts(all_concepts)
243
+ target_grain = Grain.from_concepts(all_concepts, environment=environment)
244
244
  force_group = False
245
245
  if not datasource.grain.issubset(target_grain):
246
246
  force_group = True
247
+ if not datasource.grain.components:
248
+ force_group = True
247
249
  partial_concepts = [
248
250
  c.concept
249
251
  for c in datasource.columns
@@ -350,6 +352,9 @@ def create_select_node(
350
352
 
351
353
  # we need to nest the group node one further
352
354
  if force_group is True:
355
+ logger.info(
356
+ f"{padding(depth)}{LOGGER_PREFIX} source requires group before consumption."
357
+ )
353
358
  candidate: StrategyNode = GroupNode(
354
359
  output_concepts=all_concepts,
355
360
  input_concepts=all_concepts,
@@ -359,8 +364,10 @@ def create_select_node(
359
364
  partial_concepts=bcandidate.partial_concepts,
360
365
  nullable_concepts=bcandidate.nullable_concepts,
361
366
  preexisting_conditions=bcandidate.preexisting_conditions,
367
+ force_group=force_group,
362
368
  )
363
369
  else:
370
+
364
371
  candidate = bcandidate
365
372
  return candidate
366
373
 
@@ -40,9 +40,10 @@ def resolve_concept_map(
40
40
  for concept in input.output_concepts:
41
41
  if concept.address not in input.non_partial_concept_addresses:
42
42
  continue
43
- if isinstance(input, QueryDatasource) and concept.address in [
44
- x.address for x in input.hidden_concepts
45
- ]:
43
+ if (
44
+ isinstance(input, QueryDatasource)
45
+ and concept.address in input.hidden_concepts
46
+ ):
46
47
  continue
47
48
  if concept.address in full_addresses:
48
49
  concept_map[concept.address].add(input)
@@ -138,7 +139,7 @@ class StrategyNode:
138
139
  preexisting_conditions: Conditional | Comparison | Parenthetical | None = None,
139
140
  force_group: bool | None = None,
140
141
  grain: Optional[Grain] = None,
141
- hidden_concepts: List[Concept] | None = None,
142
+ hidden_concepts: set[str] | None = None,
142
143
  existence_concepts: List[Concept] | None = None,
143
144
  virtual_output_concepts: List[Concept] | None = None,
144
145
  ):
@@ -165,7 +166,7 @@ class StrategyNode:
165
166
  self.grain = grain
166
167
  self.force_group = force_group
167
168
  self.tainted = False
168
- self.hidden_concepts = hidden_concepts or []
169
+ self.hidden_concepts = hidden_concepts or set()
169
170
  self.existence_concepts = existence_concepts or []
170
171
  self.virtual_output_concepts = virtual_output_concepts or []
171
172
  self.preexisting_conditions = preexisting_conditions
@@ -192,6 +193,8 @@ class StrategyNode:
192
193
  for x in self.parents:
193
194
  for z in x.usable_outputs:
194
195
  non_hidden.add(z.address)
196
+ for psd in z.pseudonyms:
197
+ non_hidden.add(psd)
195
198
  if not all([x.address in non_hidden for x in self.input_concepts]):
196
199
  missing = [x for x in self.input_concepts if x.address not in non_hidden]
197
200
  raise ValueError(
@@ -246,6 +249,15 @@ class StrategyNode:
246
249
  self.rebuild_cache()
247
250
  return self
248
251
 
252
+ def add_partial_concepts(self, concepts: List[Concept], rebuild: bool = True):
253
+ for concept in concepts:
254
+ if concept.address not in self.partial_lcl.addresses:
255
+ self.partial_concepts.append(concept)
256
+ self.partial_lcl = LooseConceptList(concepts=self.partial_concepts)
257
+ if rebuild:
258
+ self.rebuild_cache()
259
+ return self
260
+
249
261
  def add_existence_concepts(self, concepts: List[Concept], rebuild: bool = True):
250
262
  for concept in concepts:
251
263
  if concept.address not in self.output_concepts:
@@ -270,22 +282,20 @@ class StrategyNode:
270
282
 
271
283
  def hide_output_concepts(self, concepts: List[Concept], rebuild: bool = True):
272
284
  for x in concepts:
273
- self.hidden_concepts.append(x)
285
+ self.hidden_concepts.add(x.address)
274
286
  if rebuild:
275
287
  self.rebuild_cache()
276
288
  return self
277
289
 
278
290
  def unhide_output_concepts(self, concepts: List[Concept], rebuild: bool = True):
279
- self.hidden_concepts = [
280
- x for x in self.hidden_concepts if x.address not in concepts
281
- ]
291
+ self.hidden_concepts = set(x for x in self.hidden_concepts if x not in concepts)
282
292
  if rebuild:
283
293
  self.rebuild_cache()
284
294
  return self
285
295
 
286
296
  def remove_output_concepts(self, concepts: List[Concept], rebuild: bool = True):
287
297
  for x in concepts:
288
- self.hidden_concepts.append(x)
298
+ self.hidden_concepts.add(x.address)
289
299
  addresses = [x.address for x in concepts]
290
300
  self.output_concepts = [
291
301
  x for x in self.output_concepts if x.address not in addresses
@@ -377,7 +387,7 @@ class StrategyNode:
377
387
  preexisting_conditions=self.preexisting_conditions,
378
388
  force_group=self.force_group,
379
389
  grain=self.grain,
380
- hidden_concepts=list(self.hidden_concepts),
390
+ hidden_concepts=set(self.hidden_concepts),
381
391
  existence_concepts=list(self.existence_concepts),
382
392
  virtual_output_concepts=list(self.virtual_output_concepts),
383
393
  )