pytrilogy 0.0.2.56__py3-none-any.whl → 0.0.2.57__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.57
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=YgiCOJgZfH7Ciz9GfXgtxr5_r5NkhAaRoFEru-cSoQs,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,10 +13,10 @@ 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
19
+ trilogy/core/models.py,sha256=FO1JUeUN8N3qqIjytLimayNLQWczq0aAYwephZyq7Ec,165389
20
20
  trilogy/core/optimization.py,sha256=Jy3tVJNeqhpK6VSyTvgIWKCao6y-VCZ7mYA69MIF6L0,7989
21
21
  trilogy/core/query_processor.py,sha256=JUtsDh64mWwQHM3HFZMPtVCu-Yw7WsK3cx4NxiMACSM,18584
22
22
  trilogy/core/optimizations/__init__.py,sha256=EBanqTXEzf1ZEYjAneIWoIcxtMDite5-n2dQ5xcfUtg,356
@@ -25,19 +25,19 @@ trilogy/core/optimizations/inline_constant.py,sha256=c-YHOg6eAufL4EaCf4-0PbY_D4s
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=hgxQ5nrlLYfx02yM--7GN8MoYYX06iLBFujjPoZaxGI,37683
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=JpyPScfD8i4CgsTZSR0siWdXVhQDHizBAiQc81d7lbw,19769
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
32
+ trilogy/core/processing/node_generators/basic_node.py,sha256=pExVmLDQK9okXNeC1-jQgDwpj8JWAgQfejd2lMt8L4U,3157
33
33
  trilogy/core/processing/node_generators/common.py,sha256=dHycWu9iiRxH3WIkkyibsnYD5mJfXvdEOhsTvyaO8fg,9128
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=Hz17vZ1EjKVa275CZPF12FkLdrc916PA5T6OsfgryRQ,2928
37
+ trilogy/core/processing/node_generators/multiselect_node.py,sha256=9bQPla367WT85iWGZxlZM4EWkWFGM0i6jgBuA4O0QvQ,6464
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=aSk1Ltv1S6aSRKHpWGjEjgrNTbJXuIXkFiGQVZOyb1o,5139
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=utIs_c5V7SIRDPrIVlHJ7zD4caRFmffhuJQSM4cVYoY,16104
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
50
+ trilogy/core/processing/nodes/group_node.py,sha256=-dx_g1b6j3zygLKWp8yPYtnFxwLtKT9wHv62-U7GBZQ,7273
51
+ trilogy/core/processing/nodes/merge_node.py,sha256=kU4JChblGEoule-qKyXAwlQ2UtLXZsvugL50iUVQvQQ,14760
52
52
  trilogy/core/processing/nodes/select_node_v2.py,sha256=t3ln9Kxeml8mVTnLgtNPvavb5TLTRtfkJ0nyxh7UYUs,8212
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=jRHr_LrI0M7Pak3HizuBcbeTnAJ2e0NoYLMpGHXJhUw,38590
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.57.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
83
+ pytrilogy-0.0.2.57.dist-info/METADATA,sha256=--PBPmro81sFziRia1QJYSp1zQBGRmEUo46DYa-xBgg,8823
84
+ pytrilogy-0.0.2.57.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
85
+ pytrilogy-0.0.2.57.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
86
+ pytrilogy-0.0.2.57.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
87
+ pytrilogy-0.0.2.57.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.57"
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
  )
@@ -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(
@@ -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 (
@@ -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)
@@ -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,
@@ -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
  )
@@ -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=(
53
+ [group_arg]
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=[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,
@@ -103,6 +103,7 @@ def gen_rowset_node(
103
103
  )
104
104
  return node
105
105
  if any(x.derivation == PurposeLineage.ROWSET for x in possible_joins):
106
+
106
107
  logger.info(
107
108
  f"{padding(depth)}{LOGGER_PREFIX} cannot enrich rowset node with rowset concepts; exiting early"
108
109
  )
@@ -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
 
@@ -192,6 +192,8 @@ class StrategyNode:
192
192
  for x in self.parents:
193
193
  for z in x.usable_outputs:
194
194
  non_hidden.add(z.address)
195
+ for psd in z.pseudonyms:
196
+ non_hidden.add(psd)
195
197
  if not all([x.address in non_hidden for x in self.input_concepts]):
196
198
  missing = [x for x in self.input_concepts if x.address not in non_hidden]
197
199
  raise ValueError(
@@ -246,6 +248,15 @@ class StrategyNode:
246
248
  self.rebuild_cache()
247
249
  return self
248
250
 
251
+ def add_partial_concepts(self, concepts: List[Concept], rebuild: bool = True):
252
+ for concept in concepts:
253
+ if concept.address not in self.partial_lcl.addresses:
254
+ self.partial_concepts.append(concept)
255
+ self.partial_lcl = LooseConceptList(concepts=self.partial_concepts)
256
+ if rebuild:
257
+ self.rebuild_cache()
258
+ return self
259
+
249
260
  def add_existence_concepts(self, concepts: List[Concept], rebuild: bool = True):
250
261
  for concept in concepts:
251
262
  if concept.address not in self.output_concepts:
@@ -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
 
@@ -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,
@@ -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:
@@ -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 in datasource.hidden_concepts:
351
+ continue
353
352
  add_node_join_concept(
354
353
  graph=graph,
355
354
  concept=concept,
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:
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):