pytrilogy 0.0.2.56__py3-none-any.whl → 0.0.2.58__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.

@@ -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,11 +1,11 @@
1
- trilogy/__init__.py,sha256=imedw0kyeau1auD7e6CEEG8YhDBsWVzU4AQTRovN0Ms,291
1
+ trilogy/__init__.py,sha256=CTKkLw46Arh2GYkTbonpNlj9mv7xV7TLDpB2rTn_afU,291
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  trilogy/constants.py,sha256=qZ1d0hoKPPV2HHCoFwPYTVB7b6bXjpWvXd3lE-zEhy8,1494
4
4
  trilogy/engine.py,sha256=yOPnR7XCjWG82Gym_LLZBkYKKJdLCvqdCyt8zguNcnM,1103
5
5
  trilogy/executor.py,sha256=SbReI_xWd081WZeRt_YAyVTdMOGg2XPrsaOKgMS7YUY,15969
6
6
  trilogy/parser.py,sha256=UtuqSiGiCjpMAYgo1bvNq-b7NSzCA5hzbUW31RXaMII,281
7
7
  trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- trilogy/utility.py,sha256=eguES83XhmSOAQSBu5xq4aAXimiZFrxcUu81zDL22ug,707
8
+ trilogy/utility.py,sha256=euQccZLKoYBz0LNg5tzLlvv2YHvXh9HArnYp1V3uXsM,763
9
9
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  trilogy/core/constants.py,sha256=7XaCpZn5mQmjTobbeBn56SzPWq9eMNDfzfsRU-fP0VE,171
11
11
  trilogy/core/enums.py,sha256=6pGjEXNJPB1ngbDQRJjxRi4NmKM8NZQ5-iwnZhrdo5U,7281
@@ -13,31 +13,31 @@ trilogy/core/env_processor.py,sha256=Pt4lmJfbShBbeSe5M7_FrTk5krrOziiAA__Slnettvc
13
13
  trilogy/core/environment_helpers.py,sha256=ugKDnPYQNxKzc1Weq_kj9IVppNdgT8iS1RTS_f5hHxc,7905
14
14
  trilogy/core/ergonomics.py,sha256=ASLDd0RqKWrZiG3XcKHo8nyTjaB_8xfE9t4NZ1UvGpc,1639
15
15
  trilogy/core/exceptions.py,sha256=1c1lQCwSw4_5CQS3q7scOkXU8GQvullJXfPHubprl90,617
16
- trilogy/core/functions.py,sha256=hDlwLxQUskT9iRcIic1lfACQnxMLNM5ASdHRPi0ghyw,10835
16
+ trilogy/core/functions.py,sha256=8auZhInqnY28zg7Kil4LbvDT7jD4JggwS6HzK6ZIemE,10867
17
17
  trilogy/core/graph_models.py,sha256=mameUTiuCajtihDw_2-W218xyJlvTusOWrEKP1yAWgk,2003
18
18
  trilogy/core/internal.py,sha256=FQWbuETKPfzjALMmdXJwlOMlESfm2Z5gmErSsq3BX9c,1173
19
- trilogy/core/models.py,sha256=fsKfB8oL45KFw-xKKvi25ars92YckFJ_1HfVqn9TgmQ,165133
20
- trilogy/core/optimization.py,sha256=Jy3tVJNeqhpK6VSyTvgIWKCao6y-VCZ7mYA69MIF6L0,7989
21
- trilogy/core/query_processor.py,sha256=JUtsDh64mWwQHM3HFZMPtVCu-Yw7WsK3cx4NxiMACSM,18584
19
+ trilogy/core/models.py,sha256=VrbgVuyKPtMwt3wpQnFFYFpzqNX3B94rlw6PxvpazNQ,165392
20
+ trilogy/core/optimization.py,sha256=dIgpcletzEtcX0hAq74Y4cleAWkznR422geo00rZFKk,7900
21
+ trilogy/core/query_processor.py,sha256=-KjOKsRhvpdR_NKxoyLq83T97Aw3J-4De-hpslzhKfQ,18576
22
22
  trilogy/core/optimizations/__init__.py,sha256=EBanqTXEzf1ZEYjAneIWoIcxtMDite5-n2dQ5xcfUtg,356
23
23
  trilogy/core/optimizations/base_optimization.py,sha256=P4kF-eCXkBxO-5c6tLHhMZ4ODRH1A04hb_6ovkaVyLw,505
24
24
  trilogy/core/optimizations/inline_constant.py,sha256=c-YHOg6eAufL4EaCf4-0PbY_D4skBHW0ldR55_phsMA,1277
25
25
  trilogy/core/optimizations/inline_datasource.py,sha256=LsngRKBy-LYcx1sfo1-rnDym_ly73YV9WkEngSjpFx8,3943
26
26
  trilogy/core/optimizations/predicate_pushdown.py,sha256=XPWEBv8jXnc0OL2JDPNwFvJ5AtOE7dLzJK0LzdmdZMo,9252
27
27
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- trilogy/core/processing/concept_strategies_v3.py,sha256=8cY7b1lLkSsiC76_C4AvXRFZ6X32hdeRquRm5upiDeo,37547
28
+ trilogy/core/processing/concept_strategies_v3.py,sha256=Ty8JfK0KSCW-RBvw4v7RERe7xuSbzJlRSaqRfcSfOBE,37691
29
29
  trilogy/core/processing/graph_utils.py,sha256=stbYnDxnK-1kbo9L4XNU85FQhWCP-oZYO7LCXhAdC5M,1198
30
- trilogy/core/processing/utility.py,sha256=DEfp2F-AalS4wCRO9onhx79_-MmTH4wBfQznCq4diTY,19799
30
+ trilogy/core/processing/utility.py,sha256=N2RUGtUuktvy1VXoVpve8F2QtCBEFQf6RWpr0PXXhwk,19671
31
31
  trilogy/core/processing/node_generators/__init__.py,sha256=s_YV1OYc336DuS9591259qjI_K_CtOCuhkf4t2aOgYs,733
32
- trilogy/core/processing/node_generators/basic_node.py,sha256=dz7i0BSn4qRv6SBIS_JnVAm09-nkNizoAHrznmqnJXY,3074
33
- trilogy/core/processing/node_generators/common.py,sha256=dHycWu9iiRxH3WIkkyibsnYD5mJfXvdEOhsTvyaO8fg,9128
32
+ trilogy/core/processing/node_generators/basic_node.py,sha256=pExVmLDQK9okXNeC1-jQgDwpj8JWAgQfejd2lMt8L4U,3157
33
+ trilogy/core/processing/node_generators/common.py,sha256=Ft5giLHFn0t_A_4FPl6MJWsi0ghEen0bPFAmrdZ3S58,9107
34
34
  trilogy/core/processing/node_generators/filter_node.py,sha256=aWR82yAZOAnUrJejTj6yD4jpqH6cSPzyJMd1V-M0Kj0,7883
35
35
  trilogy/core/processing/node_generators/group_node.py,sha256=k57SVWHSVvTqCd47tyLUGCsSZaP7UQqMCJYTSz1S7oQ,5566
36
- trilogy/core/processing/node_generators/group_to_node.py,sha256=8ToptIWQoJttquEPrRTMvU33jCJQI-VJxVObN8W8QJk,2511
37
- trilogy/core/processing/node_generators/multiselect_node.py,sha256=se-cHRYRPskxq2Wq9bw5LkUFSCN1rhk8_05-OTezLz0,6421
36
+ trilogy/core/processing/node_generators/group_to_node.py,sha256=CZiPnD4rEkMOdyBQrDaH6HA3VSMKJ775BiJdQkM7dO8,2939
37
+ trilogy/core/processing/node_generators/multiselect_node.py,sha256=qmaufoSVwavNBNsdw0NGpNOTNWFPDKunA9COohEYW08,6469
38
38
  trilogy/core/processing/node_generators/node_merge_node.py,sha256=3GzuiTiorFVe9MyLhoz2PDyI0x9XL7bQ8ucEbV54le8,14627
39
- trilogy/core/processing/node_generators/rowset_node.py,sha256=ekrXWFu4ga3VR59Ux870w5gSmzFPC9WjIRuyB4yFqag,5138
40
- trilogy/core/processing/node_generators/select_merge_node.py,sha256=YW0H81IpE9B6f0SK75QH2DVSfr8d3oA9AbbqP44Jhnc,15746
39
+ trilogy/core/processing/node_generators/rowset_node.py,sha256=FDCJORLEcmpF9a071IFl-VbmKjq32rFOXi1SarjBVxY,5167
40
+ trilogy/core/processing/node_generators/select_merge_node.py,sha256=yfNeuc24Ejn7j07szwJif60qmU6OVk3wfa7C2_RJ39k,15996
41
41
  trilogy/core/processing/node_generators/select_node.py,sha256=bjTylBa-vYbmzpuSpphmIo_Oi78YZpI8ppHnN9KDYDk,1795
42
42
  trilogy/core/processing/node_generators/union_node.py,sha256=MfJjF2m0ARl0oUH9QT1awzPv0e3yA3mXK1XqAvUTgKw,2504
43
43
  trilogy/core/processing/node_generators/unnest_node.py,sha256=8El2B1mzC9vIUSk-m94xHvaJwAf5GtCAGfTxGDSiqmU,2229
@@ -45,16 +45,16 @@ trilogy/core/processing/node_generators/window_node.py,sha256=5htRRxaxw6EnS-2TVo
45
45
  trilogy/core/processing/node_generators/select_helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
46
  trilogy/core/processing/node_generators/select_helpers/datasource_injection.py,sha256=hJZS7GT0dl7sK0riweEwVAVRv5LCXOnMD1hF0XY9hpE,6548
47
47
  trilogy/core/processing/nodes/__init__.py,sha256=WNUmYmZF3uqF2qiJ1L7y0u9qiVD9YnluKds0wA5opJE,4813
48
- trilogy/core/processing/nodes/base_node.py,sha256=YQcohrk3VJHgasaHrz13bf5S9tRyGBX5ZAy3aB6yIVA,15637
48
+ trilogy/core/processing/nodes/base_node.py,sha256=YgZaiAlzvwa7AkHXNfMo5R0DJKWdvbDvMWEvTJR7fpM,16081
49
49
  trilogy/core/processing/nodes/filter_node.py,sha256=j7icDAXJ7oFPkHTOQVmm9QbZxrhhYEUGJj2lSiguXKA,2292
50
- trilogy/core/processing/nodes/group_node.py,sha256=C2U4kyfYnM0Gy1b_C5K-uh-s-9Kncr5olFxMuF0G7zQ,7852
51
- trilogy/core/processing/nodes/merge_node.py,sha256=Fnnmb86GGZGYD3tqrTwwkhvtsqmB4u5vL1TpDe7R1hY,14759
52
- trilogy/core/processing/nodes/select_node_v2.py,sha256=t3ln9Kxeml8mVTnLgtNPvavb5TLTRtfkJ0nyxh7UYUs,8212
50
+ trilogy/core/processing/nodes/group_node.py,sha256=7lX9SvOK_-oPeg4UydjYMcp4BB7IneJxT-bqEs-OSQ8,7272
51
+ trilogy/core/processing/nodes/merge_node.py,sha256=lHRL1CpiFUocqyMVzGt97VAH_aUeHAEjebGFL1YwHys,14754
52
+ trilogy/core/processing/nodes/select_node_v2.py,sha256=cwbVuiu81EsX86cHCmiZSydTNJ1V3ODuXQaWO1HU4xQ,8207
53
53
  trilogy/core/processing/nodes/union_node.py,sha256=1QgOWkjJ-ADFdanoRzi0EM5buhuzJbmlda9BAUGp4mM,1352
54
54
  trilogy/core/processing/nodes/unnest_node.py,sha256=0TFANwqVPaVpUR6SF5uweGTlXfEnagXRBBZU6dUwtcY,2101
55
55
  trilogy/core/processing/nodes/window_node.py,sha256=yYwWuOq1Uwm-xEl8lFH_urm-YXaAGAgNhE20MEoD5QQ,1163
56
56
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
- trilogy/dialect/base.py,sha256=DR7cHoL5rbRBnsj6PCq5wK8GHH-l5szpKXUaxMqx1Mw,38568
57
+ trilogy/dialect/base.py,sha256=TtQ3ATAaXKVsrhMA9EGkSS-vIC_jX0nHCzK5GBrgvrg,38511
58
58
  trilogy/dialect/bigquery.py,sha256=mKC3zoEU232h9RtIXJjqiZ72lWH8a6S28p6wAZKrAfg,2952
59
59
  trilogy/dialect/common.py,sha256=b0E6JqdKaaSzThLiFa9jwUg4YnXahf-3bqmzOn5z-6E,3827
60
60
  trilogy/dialect/config.py,sha256=UiBY2tBbNk9owx-zxP_3lN9lErEUXhXIU_bcXA18AvU,2992
@@ -79,9 +79,9 @@ trilogy/parsing/render.py,sha256=o4C12a407iZvlRGUJDiuJUezrLLo4QEaLtu60ZQX3gk,169
79
79
  trilogy/parsing/trilogy.lark,sha256=EazfEvYPuvkPkNjUnVzFi0uD9baavugbSI8CyfawShk,12573
80
80
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
81
  trilogy/scripts/trilogy.py,sha256=DQDW81E5mDMWFP8oPw8q-IyrR2JGxQSDWgUWe2VTSRQ,3731
82
- pytrilogy-0.0.2.56.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
83
- pytrilogy-0.0.2.56.dist-info/METADATA,sha256=K7l5WsgFmt55n7NLqvBsmeUfIXD3g9YIE8wN2v_SGv0,8823
84
- pytrilogy-0.0.2.56.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
85
- pytrilogy-0.0.2.56.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
86
- pytrilogy-0.0.2.56.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
87
- pytrilogy-0.0.2.56.dist-info/RECORD,,
82
+ pytrilogy-0.0.2.58.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
83
+ pytrilogy-0.0.2.58.dist-info/METADATA,sha256=PYvDIfNL0E2TRIBxawpJ_SyhNDfc2TWlFnL5uOeGO0E,8823
84
+ pytrilogy-0.0.2.58.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
85
+ pytrilogy-0.0.2.58.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
86
+ pytrilogy-0.0.2.58.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
87
+ pytrilogy-0.0.2.58.dist-info/RECORD,,
trilogy/__init__.py CHANGED
@@ -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"]
trilogy/core/functions.py CHANGED
@@ -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
  )
trilogy/core/models.py CHANGED
@@ -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
  )
@@ -1,3 +1,4 @@
1
+ from dataclasses import dataclass
1
2
  from typing import List, Optional
2
3
 
3
4
  from trilogy.constants import logger
@@ -8,7 +9,6 @@ from trilogy.core.models import (
8
9
  Datasource,
9
10
  Environment,
10
11
  Grain,
11
- LooseConceptList,
12
12
  Parenthetical,
13
13
  QueryDatasource,
14
14
  SourceType,
@@ -24,6 +24,13 @@ from trilogy.utility import unique
24
24
  LOGGER_PREFIX = "[CONCEPT DETAIL - GROUP NODE]"
25
25
 
26
26
 
27
+ @dataclass
28
+ class GroupRequiredResponse:
29
+ target: Grain
30
+ upstream: Grain
31
+ required: bool
32
+
33
+
27
34
  class GroupNode(StrategyNode):
28
35
  source_type = SourceType.GROUP
29
36
 
@@ -41,7 +48,7 @@ class GroupNode(StrategyNode):
41
48
  conditions: Conditional | Comparison | Parenthetical | None = None,
42
49
  preexisting_conditions: Conditional | Comparison | Parenthetical | None = None,
43
50
  existence_concepts: List[Concept] | None = None,
44
- hidden_concepts: List[Concept] | None = None,
51
+ hidden_concepts: set[str] | None = None,
45
52
  ):
46
53
  super().__init__(
47
54
  input_concepts=input_concepts,
@@ -59,45 +66,44 @@ class GroupNode(StrategyNode):
59
66
  hidden_concepts=hidden_concepts,
60
67
  )
61
68
 
62
- def _resolve(self) -> QueryDatasource:
63
- parent_sources: List[QueryDatasource | Datasource] = [
64
- p.resolve() for p in self.parents
65
- ]
66
-
67
- target_grain = self.grain or Grain.from_concepts(
69
+ @classmethod
70
+ def check_if_required(
71
+ cls,
72
+ downstream_concepts: List[Concept],
73
+ parents: list[QueryDatasource | Datasource],
74
+ environment: Environment,
75
+ ) -> GroupRequiredResponse:
76
+ target_grain = Grain.from_concepts(
68
77
  concepts_to_grain_concepts(
69
- self.output_concepts, environment=self.environment
78
+ downstream_concepts,
79
+ environment=environment,
70
80
  )
71
81
  )
72
82
  comp_grain = Grain()
73
- for source in parent_sources:
83
+ for source in parents:
74
84
  comp_grain += source.grain
75
85
  comp_grain = Grain.from_concepts(
76
- concepts_to_grain_concepts(
77
- comp_grain.components, environment=self.environment
78
- )
86
+ concepts_to_grain_concepts(comp_grain.components, environment=environment)
79
87
  )
80
88
  # dynamically select if we need to group
81
89
  # because sometimes, we are already at required grain
82
- if comp_grain == target_grain and self.force_group is not True:
83
- # if there is no group by, and inputs equal outputs
84
- # return the parent
85
- logger.info(
86
- f"{self.logging_prefix}{LOGGER_PREFIX} Grain of group by equals output"
87
- f" grains {comp_grain} and {target_grain}"
88
- )
89
- if (
90
- len(parent_sources) == 1
91
- and LooseConceptList(concepts=parent_sources[0].output_concepts)
92
- == self.output_lcl
93
- ) and isinstance(parent_sources[0], QueryDatasource):
94
- logger.info(
95
- f"{self.logging_prefix}{LOGGER_PREFIX} No group by required as inputs match outputs of parent; returning parent node"
96
- )
97
- will_return: QueryDatasource = parent_sources[0]
98
- if self.conditions:
99
- will_return.condition = self.conditions + will_return.condition
100
- return will_return
90
+ if comp_grain.issubset(target_grain):
91
+ return GroupRequiredResponse(target_grain, comp_grain, False)
92
+
93
+ return GroupRequiredResponse(target_grain, comp_grain, True)
94
+
95
+ def _resolve(self) -> QueryDatasource:
96
+ parent_sources: List[QueryDatasource | Datasource] = [
97
+ p.resolve() for p in self.parents
98
+ ]
99
+ grains = self.check_if_required(
100
+ self.output_concepts, parent_sources, self.environment
101
+ )
102
+ target_grain = grains.target
103
+ comp_grain = grains.upstream
104
+ # dynamically select if we need to group
105
+ # because sometimes, we are already at required grain
106
+ if not grains.required and self.force_group is not True:
101
107
  # otherwise if no group by, just treat it as a select
102
108
  source_type = SourceType.SELECT
103
109
  else:
@@ -108,15 +114,6 @@ class GroupNode(StrategyNode):
108
114
  f" target grain {target_grain}"
109
115
  f" delta: {comp_grain - target_grain}"
110
116
  )
111
- for parent in self.parents:
112
- logger.info(
113
- f"{self.logging_prefix}{LOGGER_PREFIX} Parent node"
114
- f" {[c.address for c in parent.output_concepts[:2]]}... has"
115
- " set node grain"
116
- f" {parent.grain}"
117
- f" and resolved grain {parent.resolve().grain}"
118
- f" {type(parent)}"
119
- )
120
117
  source_type = SourceType.GROUP
121
118
  source_map = resolve_concept_map(
122
119
  parent_sources,
@@ -157,9 +154,9 @@ class GroupNode(StrategyNode):
157
154
  base.output_concepts + self.conditions.row_arguments, "address"
158
155
  )
159
156
  # re-visible any hidden concepts
160
- base.hidden_concepts = [
161
- x for x in base.hidden_concepts if x not in base.output_concepts
162
- ]
157
+ base.hidden_concepts = set(
158
+ [x for x in base.hidden_concepts if x not in base.output_concepts]
159
+ )
163
160
  source_map = resolve_concept_map(
164
161
  [base],
165
162
  targets=self.output_concepts,
@@ -194,5 +191,5 @@ class GroupNode(StrategyNode):
194
191
  conditions=self.conditions,
195
192
  preexisting_conditions=self.preexisting_conditions,
196
193
  existence_concepts=list(self.existence_concepts),
197
- hidden_concepts=list(self.hidden_concepts),
194
+ hidden_concepts=set(self.hidden_concepts),
198
195
  )
@@ -115,7 +115,7 @@ class MergeNode(StrategyNode):
115
115
  grain: Grain | None = None,
116
116
  conditions: Conditional | Comparison | Parenthetical | None = None,
117
117
  preexisting_conditions: Conditional | Comparison | Parenthetical | None = None,
118
- hidden_concepts: List[Concept] | None = None,
118
+ hidden_concepts: set[str] | None = None,
119
119
  virtual_output_concepts: List[Concept] | None = None,
120
120
  existence_concepts: List[Concept] | None = None,
121
121
  ):
@@ -310,6 +310,7 @@ class MergeNode(StrategyNode):
310
310
  for join in joins:
311
311
  if isinstance(join, BaseJoin) and join.join_type == JoinType.FULL:
312
312
  full_join_concepts += join.input_concepts
313
+
313
314
  if self.whole_grain:
314
315
  force_group = False
315
316
  elif self.force_group is False:
@@ -368,7 +369,7 @@ class MergeNode(StrategyNode):
368
369
  conditions=self.conditions,
369
370
  preexisting_conditions=self.preexisting_conditions,
370
371
  nullable_concepts=list(self.nullable_concepts),
371
- hidden_concepts=list(self.hidden_concepts),
372
+ hidden_concepts=set(self.hidden_concepts),
372
373
  virtual_output_concepts=list(self.virtual_output_concepts),
373
374
  node_joins=list(self.node_joins) if self.node_joins else None,
374
375
  join_concepts=list(self.join_concepts) if self.join_concepts else None,
@@ -45,7 +45,7 @@ class SelectNode(StrategyNode):
45
45
  force_group: bool | None = False,
46
46
  conditions: Conditional | Comparison | Parenthetical | None = None,
47
47
  preexisting_conditions: Conditional | Comparison | Parenthetical | None = None,
48
- hidden_concepts: List[Concept] | None = None,
48
+ hidden_concepts: set[str] | None = None,
49
49
  ):
50
50
  super().__init__(
51
51
  input_concepts=input_concepts,
@@ -6,7 +6,6 @@ from typing import Any, Dict, List, Set, Tuple
6
6
 
7
7
  import networkx as nx
8
8
 
9
- from trilogy.constants import logger
10
9
  from trilogy.core.enums import BooleanOperator, FunctionClass, Granularity, Purpose
11
10
  from trilogy.core.models import (
12
11
  CTE,
@@ -161,8 +160,6 @@ def resolve_join_order_v2(
161
160
  final_join_type = JoinType.LEFT_OUTER
162
161
  elif any([x == JoinType.FULL for x in join_types]):
163
162
  final_join_type = JoinType.FULL
164
- logger.info("JOIN DEBUG")
165
- logger.info(joinkeys)
166
163
  output.append(
167
164
  JoinOrderOutput(
168
165
  # left=left_candidate,
@@ -350,6 +347,8 @@ def get_node_joins(
350
347
  graph.add_node(ds_node, type=NodeType.NODE)
351
348
  partials[ds_node] = [f"c~{c.address}" for c in datasource.partial_concepts]
352
349
  for concept in datasource.output_concepts:
350
+ if concept.address in datasource.hidden_concepts:
351
+ continue
353
352
  add_node_join_concept(
354
353
  graph=graph,
355
354
  concept=concept,
@@ -568,9 +567,8 @@ def find_nullable_concepts(
568
567
  def sort_select_output_processed(
569
568
  cte: CTE | UnionCTE, query: ProcessedQuery
570
569
  ) -> CTE | UnionCTE:
571
- hidden_addresses = [c.address for c in query.hidden_columns]
572
570
  output_addresses = [
573
- c.address for c in query.output_columns if c.address not in hidden_addresses
571
+ c.address for c in query.output_columns if c.address not in query.hidden_columns
574
572
  ]
575
573
 
576
574
  mapping = {x.address: x for x in cte.output_columns}
@@ -587,9 +585,10 @@ def sort_select_output(
587
585
  ) -> CTE | UnionCTE:
588
586
  if isinstance(query, ProcessedQuery):
589
587
  return sort_select_output_processed(cte, query)
590
- hidden_addresses = [c.address for c in query.hidden_components]
591
588
  output_addresses = [
592
- c.address for c in query.output_components if c.address not in hidden_addresses
589
+ c.address
590
+ for c in query.output_components
591
+ if c.address not in query.hidden_components
593
592
  ]
594
593
 
595
594
  mapping = {x.address: x for x in cte.output_columns}
@@ -500,7 +500,7 @@ def process_query(
500
500
  deduped_ctes: List[CTE | UnionCTE] = list(seen.values())
501
501
  root_cte.order_by = statement.order_by
502
502
  root_cte.limit = statement.limit
503
- root_cte.hidden_concepts = [x for x in statement.hidden_components]
503
+ root_cte.hidden_concepts = statement.hidden_components
504
504
 
505
505
  final_ctes = optimize_ctes(deduped_ctes, root_cte, statement)
506
506
  return ProcessedQuery(
@@ -514,6 +514,6 @@ def process_query(
514
514
  base=root_cte,
515
515
  # we no longer do any joins at final level, this should always happen in parent CTEs
516
516
  joins=[],
517
- hidden_columns=[x for x in statement.hidden_components],
517
+ hidden_columns=set([x for x in statement.hidden_components]),
518
518
  local_concepts=statement.local_concepts,
519
519
  )
trilogy/dialect/base.py CHANGED
@@ -249,7 +249,9 @@ def safe_get_cte_value(coalesce, cte: CTE | UnionCTE, c: Concept, quote_char: st
249
249
  if isinstance(raw, list) and len(raw) == 1:
250
250
  rendered = cte.get_alias(c, raw[0])
251
251
  return f"{raw[0]}.{safe_quote(rendered, quote_char)}"
252
- return coalesce([f"{x}.{safe_quote(cte.get_alias(c, x), quote_char)}" for x in raw])
252
+ return coalesce(
253
+ sorted([f"{x}.{safe_quote(cte.get_alias(c, x), quote_char)}" for x in raw])
254
+ )
253
255
 
254
256
 
255
257
  class BaseDialect:
@@ -658,7 +660,7 @@ class BaseDialect:
658
660
  self.render_concept_sql(c, cte)
659
661
  for c in cte.output_columns
660
662
  if c.address not in [y.address for y in cte.join_derived_concepts]
661
- and c.address not in [y.address for y in cte.hidden_concepts]
663
+ and c.address not in cte.hidden_concepts
662
664
  ] + [
663
665
  f"{self.QUOTE_CHARACTER}{c.safe_address}{self.QUOTE_CHARACTER}"
664
666
  for c in cte.join_derived_concepts
@@ -668,7 +670,7 @@ class BaseDialect:
668
670
  select_columns = [
669
671
  self.render_concept_sql(c, cte)
670
672
  for c in cte.output_columns
671
- if c.address not in [y.address for y in cte.hidden_concepts]
673
+ if c.address not in cte.hidden_concepts
672
674
  ]
673
675
  if auto_sort:
674
676
  select_columns = sorted(select_columns, key=lambda x: x)
@@ -884,9 +886,10 @@ class BaseDialect:
884
886
  select_columns: Dict[str, str] = {}
885
887
  cte_output_map = {}
886
888
  selected = set()
887
- hidden_addresses = [c.address for c in query.hidden_columns]
888
889
  output_addresses = [
889
- c.address for c in query.output_columns if c.address not in hidden_addresses
890
+ c.address
891
+ for c in query.output_columns
892
+ if c.address not in query.hidden_columns
890
893
  ]
891
894
 
892
895
  for c in query.base.output_columns:
@@ -895,7 +898,7 @@ class BaseDialect:
895
898
  f"{query.base.name}.{safe_quote(c.safe_address, self.QUOTE_CHARACTER)}"
896
899
  )
897
900
  cte_output_map[c.address] = query.base
898
- if c.address not in hidden_addresses:
901
+ if c.address not in query.hidden_columns:
899
902
  selected.add(c.address)
900
903
  if not all([x in selected for x in output_addresses]):
901
904
  missing = [x for x in output_addresses if x not in selected]
trilogy/utility.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import hashlib
2
- from typing import Any, Callable, List, Union
2
+ from typing import Callable, List, TypeVar, Union
3
3
 
4
4
  from trilogy.constants import DEFAULT_NAMESPACE
5
5
 
@@ -12,7 +12,10 @@ def string_to_hash(input: str) -> int:
12
12
  )
13
13
 
14
14
 
15
- def unique(inputs: List, property: Union[str, Callable]) -> List[Any]:
15
+ UniqueArg = TypeVar("UniqueArg")
16
+
17
+
18
+ def unique(inputs: List[UniqueArg], property: Union[str, Callable]) -> List[UniqueArg]:
16
19
  final = []
17
20
  dedupe = set()
18
21
  if isinstance(property, str):