pytrilogy 0.0.3.32__py3-none-any.whl → 0.0.3.33__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.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.32
3
+ Version: 0.0.3.33
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,5 +1,5 @@
1
- pytrilogy-0.0.3.32.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=LEjwWz5ASSeELMnWJRDTgcuJHGstMRNS9gvzU8PvHfQ,303
1
+ pytrilogy-0.0.3.33.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=d8QFTlSiA9H5vsTbRyWcF6hZW9Dih9-6ZorFRaWkb_Q,303
3
3
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  trilogy/constants.py,sha256=5eQxk1A0pv-TQk3CCvgZCFA9_K-6nxrOm7E5Lxd7KIY,1652
5
5
  trilogy/engine.py,sha256=OK2RuqCIUId6yZ5hfF8J1nxGP0AJqHRZiafcowmW0xc,1728
@@ -11,7 +11,7 @@ trilogy/utility.py,sha256=euQccZLKoYBz0LNg5tzLlvv2YHvXh9HArnYp1V3uXsM,763
11
11
  trilogy/authoring/__init__.py,sha256=ohkYA3_LGYZh3fwzEYKTN6ofACDI5GYl3VCbGxVvlzY,2233
12
12
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  trilogy/core/constants.py,sha256=7XaCpZn5mQmjTobbeBn56SzPWq9eMNDfzfsRU-fP0VE,171
14
- trilogy/core/enums.py,sha256=7SW4V0PpceeAUREyxDKPBe2JAtDMOM5IOFhn9fYKObM,7318
14
+ trilogy/core/enums.py,sha256=fWexUZtssfvP5TiD7eQ66Q_tPUCNCCTGNSzLbVXrnqQ,7358
15
15
  trilogy/core/env_processor.py,sha256=pFsxnluKIusGKx1z7tTnfsd_xZcPy9pZDungkjkyvI0,3170
16
16
  trilogy/core/environment_helpers.py,sha256=5ayyhf4CGBlg_LssPu3DbS_H9H1Kq6Qog5TgR8pwyMk,8518
17
17
  trilogy/core/ergonomics.py,sha256=e-7gE29vPLFdg0_A1smQ7eOrUwKl5VYdxRSTddHweRA,1631
@@ -19,34 +19,34 @@ trilogy/core/exceptions.py,sha256=JPYyBcit3T_pRtlHdtKSeVJkIyWUTozW2aaut25A2xI,67
19
19
  trilogy/core/functions.py,sha256=z5uyC5sAS_vnFBJcky7TjA0QAy-xI4Z9uD_9vK_XP1s,27134
20
20
  trilogy/core/graph_models.py,sha256=z17EoO8oky2QOuO6E2aMWoVNKEVJFhLdsQZOhC4fNLU,2079
21
21
  trilogy/core/internal.py,sha256=iicDBlC6nM8d7e7jqzf_ZOmpUsW8yrr2AA8AqEiLx-s,1577
22
- trilogy/core/optimization.py,sha256=xGO8piVsLrpqrx-Aid_Y56_5slSv4eZmlP64hCHRiEc,7957
22
+ trilogy/core/optimization.py,sha256=aihzx4-2-mSjx5td1TDTYGvc7e9Zvy-_xEyhPqLS-Ig,8314
23
23
  trilogy/core/query_processor.py,sha256=Do8YpdPBdsbKtl9n37hobzk8SORMGqH-e_zNNxd-BE4,19456
24
24
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- trilogy/core/models/author.py,sha256=FtECoNRevZ8sD9JI-D8a9ngxxVoKNtfrdEjbnofYcg8,76131
26
- trilogy/core/models/build.py,sha256=3_n6qxoMuzX3Ahu9HLfoyPbtH0Wb--6crZNwCnZhu7k,58104
25
+ trilogy/core/models/author.py,sha256=qEN8dJCnOMtB2Uu58GcbiYz6t5LiluCAdDUCUfl6BMk,76572
26
+ trilogy/core/models/build.py,sha256=N3Zr47iN4y9gFWS72uYjecmiQfcm8Fl9wT3QS8xKBAA,58185
27
27
  trilogy/core/models/build_environment.py,sha256=8UggvlPU708GZWYPJMc_ou2r7M3TY2g69eqGvz03YX0,5528
28
28
  trilogy/core/models/core.py,sha256=wx6hJcFECMG-Ij972ADNkr-3nFXkYESr82ObPiC46_U,10875
29
29
  trilogy/core/models/datasource.py,sha256=6RjJUd2u4nYmEwFBpJlM9LbHVYDv8iHJxqiBMZqUrwI,9422
30
30
  trilogy/core/models/environment.py,sha256=yd19vDdC-IiKPV5YbNZcDUOEr1BC_ZxE_5fQDIipa3s,26910
31
- trilogy/core/models/execute.py,sha256=KZHiovlSr_3ZjyzKD1mdBlAqnPCqFCChQkO4_4WlGtg,34224
31
+ trilogy/core/models/execute.py,sha256=cmDPkVng3LHw87Q4WiOxJvDQJCoPhEiVMeMqxSxRj7Q,34261
32
32
  trilogy/core/optimizations/__init__.py,sha256=EBanqTXEzf1ZEYjAneIWoIcxtMDite5-n2dQ5xcfUtg,356
33
33
  trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
34
34
  trilogy/core/optimizations/inline_constant.py,sha256=lvNTIXaLNkw3HseJyXyDNk5R52doLU9sIg3pmU2_S08,1332
35
35
  trilogy/core/optimizations/inline_datasource.py,sha256=AHuTGh2x0GQ8usOe0NiFncfTFQ_KogdgDl4uucmhIbI,4241
36
36
  trilogy/core/optimizations/predicate_pushdown.py,sha256=g4AYE8Aw_iMlAh68TjNXGP754NTurrDduFECkUjoBnc,9399
37
37
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- trilogy/core/processing/concept_strategies_v3.py,sha256=O-O08cGLl4XX3Faf59UlkFjsjzTU-QkzlDQ0mzY-LRE,40515
38
+ trilogy/core/processing/concept_strategies_v3.py,sha256=MhSCS1M6etHaonfrPNwe6Gk-QuDTVGHkPjt1_9_gqko,42536
39
39
  trilogy/core/processing/graph_utils.py,sha256=8QUVrkE9j-9C1AyrCb1nQEh8daCe0u1HuXl-Te85lag,1205
40
40
  trilogy/core/processing/utility.py,sha256=mnN_pewdpDgRou4QJ1JLcqYHyZdp8DrcsGsqW3QmA3o,20591
41
41
  trilogy/core/processing/node_generators/__init__.py,sha256=o8rOFHPSo-s_59hREwXMW6gjUJCsiXumdbJNozHUf-Y,800
42
42
  trilogy/core/processing/node_generators/basic_node.py,sha256=UVsXMn6jTjm_ofVFt218jAS11s4RV4zD781vP4im-GI,3371
43
43
  trilogy/core/processing/node_generators/common.py,sha256=ZsDzThjm_mAtdQpKAg8QIJiPVZ4KuUkKyilj4eOhSDs,9439
44
44
  trilogy/core/processing/node_generators/filter_node.py,sha256=rlY7TbgjJlGhahYgdCIJpJbaSREAGVJEsyUIGaA38O0,8271
45
- trilogy/core/processing/node_generators/group_node.py,sha256=iB5kq-EaueoaSQkgegULwo4g-fe8PKtRtSDiphNO5_k,5939
45
+ trilogy/core/processing/node_generators/group_node.py,sha256=kO-ersxIL04rZwX5-vFIFQQnp357PFo_7ZKXoGq3wyc,5989
46
46
  trilogy/core/processing/node_generators/group_to_node.py,sha256=E5bEjovSx422d_MlAUCDFdY4P2WJVp61BmWwltkhzA8,3095
47
47
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
48
48
  trilogy/core/processing/node_generators/node_merge_node.py,sha256=sv55oynfqgpHEpo1OEtVDri-5fywzPhDlR85qaWikvY,16195
49
- trilogy/core/processing/node_generators/rowset_node.py,sha256=tQog2YbG7h2TSVZGdXCFbwjqpq-CKqNewFOOfpZNqh4,6751
49
+ trilogy/core/processing/node_generators/rowset_node.py,sha256=YmBs6ZQ7azLXRFEmeoecpGjK4pMHsUCovuBxfb3UKZI,6848
50
50
  trilogy/core/processing/node_generators/select_merge_node.py,sha256=pIsHfXWA30RkKSMBDKPtDmCriJtHoNKRMJC0THSDxpI,19951
51
51
  trilogy/core/processing/node_generators/select_node.py,sha256=Y-zO0AFkTrpi2LyebjpyHU7WWANr7nKZSS9rY7DH4Wo,1888
52
52
  trilogy/core/processing/node_generators/synonym_node.py,sha256=9LHK2XHDjbyTLjmDQieskG8fqbiSpRnFOkfrutDnOTE,2258
@@ -58,8 +58,8 @@ trilogy/core/processing/node_generators/select_helpers/datasource_injection.py,s
58
58
  trilogy/core/processing/nodes/__init__.py,sha256=DqPG3Y8vl5-UTeox6hn1EE6iwPIJpsM-XeZALHSgLZQ,5058
59
59
  trilogy/core/processing/nodes/base_node.py,sha256=FHrY8GsTKPuMJklOjILbhGqCt5s1nmlj62Z-molARDA,16835
60
60
  trilogy/core/processing/nodes/filter_node.py,sha256=5VtRfKbCORx0dV-vQfgy3gOEkmmscL9f31ExvlODwvY,2461
61
- trilogy/core/processing/nodes/group_node.py,sha256=1caU36nHknnXS-dM6xmL9Mc7kK0wCq-IS3kL7_XYNlA,8024
62
- trilogy/core/processing/nodes/merge_node.py,sha256=bEz1QU2o-fl_O-VotE5dN1GmlZPClufMvUOvL2-2Uo8,15262
61
+ trilogy/core/processing/nodes/group_node.py,sha256=MUvcOg9U5J6TnWBel8eht9PdI9BfAKjUxmfjP_ZXx9o,10484
62
+ trilogy/core/processing/nodes/merge_node.py,sha256=djFBJ8Sq1_crP0g7REGCbMFl4so7zPzabkPgAA1TUl4,15720
63
63
  trilogy/core/processing/nodes/select_node_v2.py,sha256=Xyfq8lU7rP7JTAd8VV0ATDNal64n4xIBgWQsOuMe_Ak,8824
64
64
  trilogy/core/processing/nodes/union_node.py,sha256=fDFzLAUh5876X6_NM7nkhoMvHEdGJ_LpvPokpZKOhx4,1425
65
65
  trilogy/core/processing/nodes/unnest_node.py,sha256=oLKMMNMx6PLDPlt2V5neFMFrFWxET8r6XZElAhSNkO0,2181
@@ -87,13 +87,13 @@ trilogy/hooks/graph_hook.py,sha256=c-vC-IXoJ_jDmKQjxQyIxyXPOuUcLIURB573gCsAfzQ,2
87
87
  trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
88
88
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
89
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
90
- trilogy/parsing/common.py,sha256=_61lUd6DONwteY_D6lanZZgi2NoEWivAxJtrOYrmw34,21915
90
+ trilogy/parsing/common.py,sha256=bhZ_f520D2diRcXC3YeEToPSXBbUBBNq42zII_smdYc,23518
91
91
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
92
92
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
93
93
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
94
- trilogy/parsing/parse_engine.py,sha256=HBvJewa5zP4y8FasH6nYiqKP9WiIsdl7udfaY_GWxHU,62233
94
+ trilogy/parsing/parse_engine.py,sha256=2BeNdsYoyn5UktTdafEKTZ8H7-ELEahDJeqcwMvPtO8,62567
95
95
  trilogy/parsing/render.py,sha256=ElpmCWUhGs8h489S7cdlbI8bilJlnBgHZ8KMR8y1FrM,18840
96
- trilogy/parsing/trilogy.lark,sha256=h7mJiad7GgTTXnrjntE6OF7xpND5TQlvHqZFfx0nkyk,12993
96
+ trilogy/parsing/trilogy.lark,sha256=SeuOZrpIUSqQwCj5mTSmTSkeSLTPwBgtm2AdpcGAy9A,13023
97
97
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
98
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
99
99
  trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -102,8 +102,8 @@ trilogy/std/date.preql,sha256=0MHeGLp2mG4QBKtmozcBZ7qVjOAdWOtrliIKn6hz1Pc,95
102
102
  trilogy/std/display.preql,sha256=2BbhvqR4rcltyAbOXAUo7SZ_yGFYZgFnurglHMbjW2g,40
103
103
  trilogy/std/geography.preql,sha256=-fqAGnBL6tR-UtT8DbSek3iMFg66ECR_B_41pODxv-k,504
104
104
  trilogy/std/money.preql,sha256=ZHW-csTX-kYbOLmKSO-TcGGgQ-_DMrUXy0BjfuJSFxM,80
105
- pytrilogy-0.0.3.32.dist-info/METADATA,sha256=xnPeyXro94RAWPJBvFkrPPA219KCrQe0sAUhb0kcFgA,9100
106
- pytrilogy-0.0.3.32.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
107
- pytrilogy-0.0.3.32.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
108
- pytrilogy-0.0.3.32.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
109
- pytrilogy-0.0.3.32.dist-info/RECORD,,
105
+ pytrilogy-0.0.3.33.dist-info/METADATA,sha256=nCv6QIF6oA0nSwlsLa_znFnm6RetCf4zyfBdBszleNY,9100
106
+ pytrilogy-0.0.3.33.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
107
+ pytrilogy-0.0.3.33.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
108
+ pytrilogy-0.0.3.33.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
109
+ pytrilogy-0.0.3.33.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.3.32"
7
+ __version__ = "0.0.3.33"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/core/enums.py CHANGED
@@ -27,6 +27,7 @@ class Purpose(Enum):
27
27
  CONSTANT = "const"
28
28
  KEY = "key"
29
29
  PROPERTY = "property"
30
+ UNIQUE_PROPERTY = "unique_property"
30
31
  METRIC = "metric"
31
32
  ROWSET = "rowset"
32
33
  AUTO = "auto"
@@ -1326,7 +1326,7 @@ class OrderItem(Mergeable, ConceptArgs, Namespaced, BaseModel):
1326
1326
 
1327
1327
  class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1328
1328
  type: WindowType
1329
- content: ConceptRef
1329
+ content: FuncArgs
1330
1330
  order_by: List["OrderItem"]
1331
1331
  over: List["ConceptRef"] = Field(default_factory=list)
1332
1332
  index: Optional[int] = None
@@ -1335,7 +1335,7 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1335
1335
  return self.__repr__()
1336
1336
 
1337
1337
  def __repr__(self):
1338
- return f"{self.type}({self.content} {self.index}, {self.over}, {self.order_by})"
1338
+ return f"{self.type.value} {self.content} by {self.index} over {self.over} order {self.order_by}"
1339
1339
 
1340
1340
  @field_validator("content", mode="before")
1341
1341
  def enforce_concept_ref(cls, v):
@@ -1358,7 +1358,11 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1358
1358
  ) -> "WindowItem":
1359
1359
  output = WindowItem.model_construct(
1360
1360
  type=self.type,
1361
- content=self.content.with_merge(source, target, modifiers),
1361
+ content=(
1362
+ self.content.with_merge(source, target, modifiers)
1363
+ if isinstance(self.content, Mergeable)
1364
+ else self.content
1365
+ ),
1362
1366
  over=[x.with_merge(source, target, modifiers) for x in self.over],
1363
1367
  order_by=[x.with_merge(source, target, modifiers) for x in self.order_by],
1364
1368
  index=self.index,
@@ -1379,7 +1383,11 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1379
1383
  def with_namespace(self, namespace: str) -> "WindowItem":
1380
1384
  return WindowItem.model_construct(
1381
1385
  type=self.type,
1382
- content=self.content.with_namespace(namespace),
1386
+ content=(
1387
+ self.content.with_namespace(namespace)
1388
+ if isinstance(self.content, Namespaced)
1389
+ else self.content
1390
+ ),
1383
1391
  over=[x.with_namespace(namespace) for x in self.over],
1384
1392
  order_by=[x.with_namespace(namespace) for x in self.order_by],
1385
1393
  index=self.index,
@@ -1387,7 +1395,8 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1387
1395
 
1388
1396
  @property
1389
1397
  def concept_arguments(self) -> List[ConceptRef]:
1390
- output = [self.content]
1398
+ output = []
1399
+ output += get_concept_arguments(self.content)
1391
1400
  for order in self.order_by:
1392
1401
  output += get_concept_arguments(order)
1393
1402
  for item in self.over:
@@ -1787,6 +1796,10 @@ class FunctionCallWrapper(
1787
1796
  ],
1788
1797
  )
1789
1798
 
1799
+ def with_reference_replacement(self, source, target):
1800
+ raise NotImplementedError("Cannot reference replace")
1801
+ return self
1802
+
1790
1803
  def with_merge(
1791
1804
  self, source: Concept, target: Concept, modifiers: List[Modifier]
1792
1805
  ) -> "FunctionCallWrapper":
@@ -1646,7 +1646,7 @@ class Factory:
1646
1646
 
1647
1647
  @build.register
1648
1648
  def _(self, base: WindowItem) -> BuildWindowItem:
1649
-
1649
+ # to do proper discovery, we need to inject virtual intermediate ocncepts
1650
1650
  return BuildWindowItem.model_construct(
1651
1651
  type=base.type,
1652
1652
  content=self.build(base.content),
@@ -637,7 +637,7 @@ class QueryDatasource(BaseModel):
637
637
  and CONFIG.validate_missing
638
638
  ):
639
639
  raise SyntaxError(
640
- f"On query datasource missing source map for {concept.address} on {key}, have {v}"
640
+ f"On query datasource missing source map for {concept.address} on {key} with pseudonyms {concept.pseudonyms}, have {v}"
641
641
  )
642
642
  return v
643
643
 
@@ -94,6 +94,11 @@ def filter_irrelevant_ctes(
94
94
  inverse_map = gen_inverse_map(input)
95
95
  recurse(root_cte, inverse_map)
96
96
  final = [cte for cte in input if cte.name in relevant_ctes]
97
+ filtered = [cte for cte in input if cte.name not in relevant_ctes]
98
+ if filtered:
99
+ logger.info(
100
+ f"[Optimization][Irrelevent CTE filtering] Removing redundant CTEs {[x.name for x in filtered]}"
101
+ )
97
102
  if len(final) == len(input):
98
103
  return input
99
104
  return filter_irrelevant_ctes(final, root_cte)
@@ -130,6 +135,9 @@ def is_direct_return_eligible(cte: CTE | UnionCTE) -> CTE | UnionCTE | None:
130
135
  if not output_addresses.issubset(parent_output_addresses):
131
136
  return None
132
137
  if not direct_parent.grain == cte.grain:
138
+ logger.info("grain mismatch, cannot early exit")
139
+ logger.info(direct_parent.grain)
140
+ logger.info(cte.grain)
133
141
  return None
134
142
 
135
143
  assert isinstance(cte, CTE)
@@ -625,6 +625,7 @@ def validate_stack(
625
625
  for concept in resolved.output_concepts:
626
626
  if concept.address in resolved.hidden_concepts:
627
627
  continue
628
+
628
629
  validate_concept(
629
630
  concept,
630
631
  node,
@@ -773,6 +774,8 @@ def _search_concepts(
773
774
  ) -> StrategyNode | None:
774
775
  # these are the concepts we need in the output projection
775
776
  mandatory_list = unique(mandatory_list, "address")
777
+ # cache our values before an filter injection
778
+ original_mandatory = [*mandatory_list]
776
779
  for x in mandatory_list:
777
780
  if isinstance(x, UndefinedConcept):
778
781
  raise SyntaxError(f"Undefined concept {x.address}")
@@ -921,7 +924,7 @@ def _search_concepts(
921
924
  mandatory_completion = [c.address for c in completion_mandatory]
922
925
  logger.info(
923
926
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} finished concept loop for {priority_concept} {priority_concept.derivation} condition {conditions} flag for accepting partial addresses is"
924
- f" {accept_partial} (complete: {complete}), have {found} from {[n for n in stack]} (missing {missing} partial {partial} virtual {virtual}), attempted {attempted}, mandatory w/ filter {mandatory_completion}"
927
+ f" {accept_partial} (complete: {complete}), have {found} from {[n for n in stack]} (missing {missing} synonyms partial {partial} virtual {virtual}), attempted {attempted}, mandatory w/ filter {mandatory_completion}"
925
928
  )
926
929
  if complete == ValidationResult.INCOMPLETE_CONDITION:
927
930
  cond_dict = {str(node): node.preexisting_conditions for node in stack}
@@ -948,6 +951,11 @@ def _search_concepts(
948
951
  if complete == ValidationResult.COMPLETE:
949
952
  condition_required = True
950
953
  non_virtual = [c for c in completion_mandatory if c.address not in virtual]
954
+ non_virtual_output = [c for c in original_mandatory if c.address not in virtual]
955
+ non_virtual_different = len(completion_mandatory) != len(original_mandatory)
956
+ non_virtual_difference_values = set(
957
+ [x.address for x in completion_mandatory]
958
+ ).difference(set([x.address for x in original_mandatory]))
951
959
  if not conditions:
952
960
  condition_required = False
953
961
  non_virtual = [c for c in mandatory_list if c.address not in virtual]
@@ -966,7 +974,19 @@ def _search_concepts(
966
974
  )
967
975
  if len(stack) == 1:
968
976
  output: StrategyNode = stack[0]
969
- # _ = restrict_node_outputs_targets(output, mandatory_list, depth)
977
+ if non_virtual_different:
978
+ logger.info(
979
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Found different non-virtual output concepts ({non_virtual_difference_values}), removing condition injected values"
980
+ )
981
+ output.set_output_concepts(
982
+ [
983
+ x
984
+ for x in output.output_concepts
985
+ if x.address in non_virtual_output
986
+ ],
987
+ rebuild=False,
988
+ )
989
+
970
990
  logger.info(
971
991
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Source stack has single node, returning that {type(output)}"
972
992
  )
@@ -995,6 +1015,30 @@ def _search_concepts(
995
1015
  logger.info(
996
1016
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Graph is connected, returning {type(output)} node partial {[c.address for c in output.partial_concepts]}"
997
1017
  )
1018
+ if condition_required and conditions and non_virtual_different:
1019
+ logger.info(
1020
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Conditions {conditions} were injected, checking if we need a group to restore grain"
1021
+ )
1022
+ result = GroupNode.check_if_required(
1023
+ downstream_concepts=original_mandatory,
1024
+ parents=[output.resolve()],
1025
+ environment=environment,
1026
+ depth=depth,
1027
+ )
1028
+ logger.info(f"gcheck result is {result}")
1029
+ if result.required:
1030
+ logger.info(
1031
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Adding group node"
1032
+ )
1033
+ return GroupNode(
1034
+ output_concepts=original_mandatory,
1035
+ input_concepts=original_mandatory,
1036
+ environment=environment,
1037
+ parents=[output],
1038
+ partial_concepts=output.partial_concepts,
1039
+ preexisting_conditions=conditions.conditional,
1040
+ depth=depth,
1041
+ )
998
1042
  return output
999
1043
 
1000
1044
  # if we can't find it after expanding to a merge, then
@@ -149,7 +149,9 @@ def gen_group_node(
149
149
  g=g,
150
150
  depth=depth,
151
151
  source_concepts=source_concepts,
152
- log_lambda=create_log_lambda(LOGGER_PREFIX, depth, logger),
152
+ log_lambda=create_log_lambda(
153
+ LOGGER_PREFIX + f" for {concept.address}", depth, logger
154
+ ),
153
155
  history=history,
154
156
  conditions=conditions,
155
157
  )
@@ -117,7 +117,11 @@ def gen_rowset_node(
117
117
  f"{padding(depth)}{LOGGER_PREFIX} final output is {[x.address for x in node.output_concepts]}"
118
118
  )
119
119
  if not local_optional or all(
120
- x.address in node.output_concepts and x.address not in node.partial_concepts
120
+ (
121
+ x.address in node.output_concepts
122
+ or (z in x.pseudonyms for z in node.output_concepts)
123
+ )
124
+ and x.address not in node.partial_concepts
121
125
  for x in local_optional
122
126
  ):
123
127
  logger.info(
@@ -2,7 +2,7 @@ from dataclasses import dataclass
2
2
  from typing import List, Optional
3
3
 
4
4
  from trilogy.constants import logger
5
- from trilogy.core.enums import SourceType
5
+ from trilogy.core.enums import Purpose, SourceType
6
6
  from trilogy.core.models.build import (
7
7
  BuildComparison,
8
8
  BuildConcept,
@@ -78,7 +78,9 @@ class GroupNode(StrategyNode):
78
78
  downstream_concepts: List[BuildConcept],
79
79
  parents: list[QueryDatasource | BuildDatasource],
80
80
  environment: BuildEnvironment,
81
+ depth: int = 0,
81
82
  ) -> GroupRequiredResponse:
83
+ padding = "\t" * depth
82
84
  target_grain = BuildGrain.from_concepts(
83
85
  downstream_concepts,
84
86
  environment=environment,
@@ -95,12 +97,67 @@ class GroupNode(StrategyNode):
95
97
  lookups: list[BuildConcept | str] = [
96
98
  concept_map[x] if x in concept_map else x for x in comp_grain.components
97
99
  ]
100
+
98
101
  comp_grain = BuildGrain.from_concepts(lookups, environment=environment)
102
+
99
103
  # dynamically select if we need to group
100
104
  # because sometimes, we are already at required grain
101
105
  if comp_grain.issubset(target_grain):
106
+
107
+ logger.info(
108
+ f"{padding}{LOGGER_PREFIX} Group requirement check: {comp_grain}, {target_grain}, is subset, no grain required"
109
+ )
102
110
  return GroupRequiredResponse(target_grain, comp_grain, False)
111
+ # find out what extra is in the comp grain vs target grain
112
+ difference = [
113
+ environment.concepts[c] for c in (comp_grain - target_grain).components
114
+ ]
115
+ logger.info(
116
+ f"{padding}{LOGGER_PREFIX} Group requirement check: {comp_grain}, {target_grain}, difference {difference}"
117
+ )
118
+
119
+ # if the difference is all unique properties whose keys are in the source grain
120
+ # we can also suppress the group
121
+ if all(
122
+ [
123
+ x.purpose == Purpose.UNIQUE_PROPERTY
124
+ and x.keys
125
+ and all(z in comp_grain.components for z in x.keys)
126
+ for x in difference
127
+ ]
128
+ ):
129
+ logger.info(
130
+ f"{padding}{LOGGER_PREFIX} Group requirement check: skipped due to unique property validation"
131
+ )
132
+ return GroupRequiredResponse(target_grain, comp_grain, False)
133
+ if all([x.purpose == Purpose.KEY for x in difference]):
134
+ logger.info(
135
+ f"{padding}{LOGGER_PREFIX} checking if downstream is unique properties of key"
136
+ )
137
+ replaced_grain_raw: list[set[str]] = [
138
+ (
139
+ x.keys or set()
140
+ if x.purpose == Purpose.UNIQUE_PROPERTY
141
+ else set([x.address])
142
+ )
143
+ for x in downstream_concepts
144
+ if x.address in target_grain.components
145
+ ]
146
+ # flatten the list of lists
147
+ replaced_grain = [
148
+ item for sublist in replaced_grain_raw for item in sublist
149
+ ]
150
+ # if the replaced grain is a subset of the comp grain, we can skip the group
151
+ unique_grain_comp = BuildGrain.from_concepts(
152
+ replaced_grain, environment=environment
153
+ )
154
+ if comp_grain.issubset(unique_grain_comp):
155
+ logger.info(
156
+ f"{padding}{LOGGER_PREFIX} Group requirement check: skipped due to unique property validation"
157
+ )
158
+ return GroupRequiredResponse(target_grain, comp_grain, False)
103
159
 
160
+ logger.info(f"{padding}{LOGGER_PREFIX} Group requirement check: group required")
104
161
  return GroupRequiredResponse(target_grain, comp_grain, True)
105
162
 
106
163
  def _resolve(self) -> QueryDatasource:
@@ -296,9 +296,21 @@ class MergeNode(StrategyNode):
296
296
  return dataset
297
297
 
298
298
  pregrain = BuildGrain()
299
+
299
300
  for source in final_datasets:
301
+ if all(
302
+ [x.address in self.existence_concepts for x in source.output_concepts]
303
+ ):
304
+ logger.info(
305
+ f"{self.logging_prefix}{LOGGER_PREFIX} skipping existence only source with {source.output_concepts} from grain accumulation"
306
+ )
307
+ continue
300
308
  pregrain += source.grain
301
309
 
310
+ pregrain = BuildGrain.from_concepts(
311
+ pregrain.components, environment=self.environment
312
+ )
313
+
302
314
  grain = self.grain if self.grain else pregrain
303
315
  logger.info(
304
316
  f"{self.logging_prefix}{LOGGER_PREFIX} has pre grain {pregrain} and final merge node grain {grain}"
@@ -310,6 +322,7 @@ class MergeNode(StrategyNode):
310
322
  )
311
323
  else:
312
324
  joins = []
325
+
313
326
  logger.info(
314
327
  f"{self.logging_prefix}{LOGGER_PREFIX} Final join count for CTE parent count {len(join_candidates)} is {len(joins)}"
315
328
  )
@@ -343,7 +356,6 @@ class MergeNode(StrategyNode):
343
356
  nullable_concepts = find_nullable_concepts(
344
357
  source_map=source_map, joins=joins, datasources=final_datasets
345
358
  )
346
-
347
359
  qds = QueryDatasource(
348
360
  input_concepts=unique(self.input_concepts, "address"),
349
361
  output_concepts=unique(self.output_concepts, "address"),
trilogy/parsing/common.py CHANGED
@@ -40,6 +40,7 @@ from trilogy.core.models.author import (
40
40
  UndefinedConcept,
41
41
  WhereClause,
42
42
  WindowItem,
43
+ address_with_namespace,
43
44
  )
44
45
  from trilogy.core.models.core import DataType, arg_to_datatype
45
46
  from trilogy.core.models.environment import Environment
@@ -255,7 +256,7 @@ def concepts_to_grain_concepts(
255
256
  return v2
256
257
 
257
258
 
258
- def get_relevant_parent_concepts(arg) -> tuple[list[ConceptRef], bool]:
259
+ def _get_relevant_parent_concepts(arg) -> tuple[list[ConceptRef], bool]:
259
260
  from trilogy.core.models.author import get_concept_arguments
260
261
 
261
262
  is_metric = False
@@ -270,9 +271,16 @@ def get_relevant_parent_concepts(arg) -> tuple[list[ConceptRef], bool]:
270
271
  return [], True
271
272
  elif isinstance(arg, AggregateWrapper) and arg.by:
272
273
  return arg.by, True
274
+ elif isinstance(arg, FunctionCallWrapper):
275
+ return get_relevant_parent_concepts(arg.content)
273
276
  return get_concept_arguments(arg), False
274
277
 
275
278
 
279
+ def get_relevant_parent_concepts(arg):
280
+ results = _get_relevant_parent_concepts(arg)
281
+ return results
282
+
283
+
276
284
  def function_to_concept(
277
285
  parent: Function,
278
286
  name: str,
@@ -415,10 +423,31 @@ def window_item_to_concept(
415
423
  metadata: Metadata | None = None,
416
424
  ) -> Concept:
417
425
  fmetadata = metadata or Metadata()
426
+ # if isinstance(
427
+ # parent.content,
428
+ # (
429
+ # AggregateWrapper
430
+ # | FunctionCallWrapper
431
+ # | WindowItem
432
+ # | FilterItem
433
+ # | Function
434
+ # | ListWrapper
435
+ # | MapWrapper
436
+ # ),
437
+ # ):
438
+ # new_parent = arbitrary_to_concept(
439
+ # parent.content, environment=environment, namespace=namespace
440
+ # )
441
+ # environment.add_concept(new_parent)
442
+ # parent = parent.model_copy(update={"content": new_parent.reference})
443
+
444
+ if not isinstance(parent.content, ConceptRef):
445
+ raise NotImplementedError(
446
+ f"Window function wiht non ref content {parent.content} not yet supported"
447
+ )
418
448
  bcontent = environment.concepts[parent.content.address]
419
449
  if isinstance(bcontent, UndefinedConcept):
420
450
  return UndefinedConcept(address=f"{namespace}.{name}", metadata=fmetadata)
421
-
422
451
  if bcontent.purpose == Purpose.METRIC:
423
452
  local_purpose, keys = get_purpose_and_keys(None, (bcontent,), environment)
424
453
  else:
@@ -534,38 +563,58 @@ def align_item_to_concept(
534
563
  return new
535
564
 
536
565
 
566
+ def rowset_concept(
567
+ orig_address: ConceptRef,
568
+ environment: Environment,
569
+ rowset: RowsetDerivationStatement,
570
+ pre_output: list[Concept],
571
+ orig: dict[str, Concept],
572
+ orig_map: dict[str, Concept],
573
+ ):
574
+ orig_concept = environment.concepts[orig_address.address]
575
+ name = orig_concept.name
576
+ if isinstance(orig_concept.lineage, FilterItem):
577
+ if orig_concept.lineage.where == rowset.select.where_clause:
578
+ name = environment.concepts[orig_concept.lineage.content.address].name
579
+ base_namespace = (
580
+ f"{rowset.name}.{orig_concept.namespace}"
581
+ if orig_concept.namespace != rowset.namespace
582
+ else rowset.name
583
+ )
584
+
585
+ new_concept = Concept(
586
+ name=name,
587
+ datatype=orig_concept.datatype,
588
+ purpose=orig_concept.purpose,
589
+ lineage=None,
590
+ grain=orig_concept.grain,
591
+ metadata=Metadata(concept_source=ConceptSource.CTE),
592
+ namespace=base_namespace,
593
+ keys=orig_concept.keys,
594
+ derivation=Derivation.ROWSET,
595
+ granularity=orig_concept.granularity,
596
+ pseudonyms={
597
+ address_with_namespace(x, rowset.name) for x in orig_concept.pseudonyms
598
+ },
599
+ )
600
+ for x in orig_concept.pseudonyms:
601
+ new_address = address_with_namespace(x, rowset.name)
602
+ origa = environment.alias_origin_lookup[x]
603
+ environment.concepts[new_address] = new_concept
604
+ environment.alias_origin_lookup[new_address] = origa.model_copy(
605
+ update={"namespace": f"{rowset.name}.{origa.namespace}"}
606
+ )
607
+ orig[orig_concept.address] = new_concept
608
+ orig_map[new_concept.address] = orig_concept
609
+ pre_output.append(new_concept)
610
+
611
+
537
612
  def rowset_to_concepts(rowset: RowsetDerivationStatement, environment: Environment):
538
613
  pre_output: list[Concept] = []
539
614
  orig: dict[str, Concept] = {}
540
615
  orig_map: dict[str, Concept] = {}
541
616
  for orig_address in rowset.select.output_components:
542
- orig_concept = environment.concepts[orig_address.address]
543
- name = orig_concept.name
544
- if isinstance(orig_concept.lineage, FilterItem):
545
- if orig_concept.lineage.where == rowset.select.where_clause:
546
- name = environment.concepts[orig_concept.lineage.content.address].name
547
- base_namespace = (
548
- f"{rowset.name}.{orig_concept.namespace}"
549
- if orig_concept.namespace != rowset.namespace
550
- else rowset.name
551
- )
552
-
553
- new_concept = Concept(
554
- name=name,
555
- datatype=orig_concept.datatype,
556
- purpose=orig_concept.purpose,
557
- lineage=None,
558
- grain=orig_concept.grain,
559
- # TODO: add proper metadata
560
- metadata=Metadata(concept_source=ConceptSource.CTE),
561
- namespace=base_namespace,
562
- keys=orig_concept.keys,
563
- derivation=Derivation.ROWSET,
564
- granularity=orig_concept.granularity,
565
- )
566
- orig[orig_concept.address] = new_concept
567
- orig_map[new_concept.address] = orig_concept
568
- pre_output.append(new_concept)
617
+ rowset_concept(orig_address, environment, rowset, pre_output, orig, orig_map)
569
618
  select_lineage = rowset.select.as_lineage(environment)
570
619
  for x in pre_output:
571
620
  x.lineage = RowsetItem(
@@ -252,6 +252,8 @@ def rehydrate_lineage(
252
252
  )
253
253
  return lineage
254
254
  elif isinstance(lineage, WindowItem):
255
+ # this is temporarily guaranteed until we do some upstream work
256
+ assert isinstance(lineage.content, ConceptRef)
255
257
  lineage.content.datatype = environment.concepts[
256
258
  lineage.content.address
257
259
  ].datatype
@@ -542,6 +544,10 @@ class ParseToObjects(Transformer):
542
544
 
543
545
  @v_args(meta=True)
544
546
  def concept_property_declaration(self, meta: Meta, args) -> Concept:
547
+ unique = False
548
+ if not args[0] == Purpose.PROPERTY:
549
+ unique = True
550
+ args = args[1:]
545
551
  metadata = Metadata()
546
552
  modifiers = []
547
553
  for arg in args:
@@ -569,7 +575,7 @@ class ParseToObjects(Transformer):
569
575
  concept = Concept(
570
576
  name=name,
571
577
  datatype=args[2],
572
- purpose=args[0],
578
+ purpose=Purpose.PROPERTY if not unique else Purpose.UNIQUE_PROPERTY,
573
579
  metadata=metadata,
574
580
  grain=Grain(components={x.address for x in parents}),
575
581
  namespace=namespace,
@@ -633,7 +639,8 @@ class ParseToObjects(Transformer):
633
639
  source_value = source_value.content
634
640
 
635
641
  if isinstance(
636
- source_value, (FilterItem, WindowItem, AggregateWrapper, Function)
642
+ source_value,
643
+ (FilterItem, WindowItem, AggregateWrapper, Function, FunctionCallWrapper),
637
644
  ):
638
645
  concept = arbitrary_to_concept(
639
646
  source_value,
@@ -21,7 +21,8 @@
21
21
  concept_declaration: PURPOSE IDENTIFIER data_type concept_nullable_modifier? metadata?
22
22
  //customer_id.property first_name STRING;
23
23
  //<customer_id,country>.property local_alias STRING
24
- concept_property_declaration: PROPERTY (prop_ident | IDENTIFIER) data_type concept_nullable_modifier? metadata?
24
+ UNIQUE: "UNIQUE"i
25
+ concept_property_declaration: UNIQUE? PROPERTY (prop_ident | IDENTIFIER) data_type concept_nullable_modifier? metadata?
25
26
  //metric post_length <- len(post_text);
26
27
  concept_derivation: (PURPOSE | AUTO | PROPERTY ) (prop_ident | IDENTIFIER) "<-" expr
27
28