pytrilogy 0.0.2.8__py3-none-any.whl → 0.0.2.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pytrilogy might be problematic. Click here for more details.

Files changed (32) hide show
  1. {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.10.dist-info}/METADATA +1 -1
  2. {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.10.dist-info}/RECORD +32 -32
  3. {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.10.dist-info}/WHEEL +1 -1
  4. trilogy/__init__.py +1 -1
  5. trilogy/constants.py +1 -0
  6. trilogy/core/models.py +161 -59
  7. trilogy/core/optimization.py +44 -5
  8. trilogy/core/optimizations/inline_datasource.py +14 -8
  9. trilogy/core/optimizations/predicate_pushdown.py +73 -44
  10. trilogy/core/processing/concept_strategies_v3.py +69 -28
  11. trilogy/core/processing/node_generators/common.py +42 -16
  12. trilogy/core/processing/node_generators/filter_node.py +94 -48
  13. trilogy/core/processing/node_generators/group_node.py +3 -1
  14. trilogy/core/processing/node_generators/rowset_node.py +13 -54
  15. trilogy/core/processing/node_generators/select_node.py +10 -13
  16. trilogy/core/processing/node_generators/unnest_node.py +5 -3
  17. trilogy/core/processing/node_generators/window_node.py +23 -2
  18. trilogy/core/processing/nodes/__init__.py +34 -6
  19. trilogy/core/processing/nodes/base_node.py +67 -13
  20. trilogy/core/processing/nodes/filter_node.py +3 -0
  21. trilogy/core/processing/nodes/group_node.py +4 -5
  22. trilogy/core/processing/nodes/merge_node.py +1 -11
  23. trilogy/core/processing/nodes/select_node_v2.py +1 -0
  24. trilogy/core/processing/utility.py +46 -14
  25. trilogy/core/query_processor.py +48 -21
  26. trilogy/dialect/base.py +28 -15
  27. trilogy/dialect/duckdb.py +1 -1
  28. trilogy/parsing/parse_engine.py +39 -2
  29. trilogy/parsing/trilogy.lark +3 -1
  30. {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.10.dist-info}/LICENSE.md +0 -0
  31. {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.10.dist-info}/entry_points.txt +0 -0
  32. {pytrilogy-0.0.2.8.dist-info → pytrilogy-0.0.2.10.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.8
3
+ Version: 0.0.2.10
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,6 +1,6 @@
1
- trilogy/__init__.py,sha256=zJsrRvfQaVItXzGc-5Fgqgq2jhQrGMrKaI1adR7HE84,290
1
+ trilogy/__init__.py,sha256=-fwTLWZDBATA0SPkOXcsRS3py_W_F1oq7wt2cVysbNg,291
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- trilogy/constants.py,sha256=3YKp-xvS7yB5z9t9YDcGofkrdZ6v13iePDnn4O5mwTE,886
3
+ trilogy/constants.py,sha256=HRQq4i3cpSEJCywt61QKEzRO1jd4tEPZNSBuxUA_7yg,922
4
4
  trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
5
5
  trilogy/executor.py,sha256=5cRbU4Rj7p1pNV76rfp1pz704Hx_0q8_O8HFURjgXxQ,11016
6
6
  trilogy/parser.py,sha256=UtuqSiGiCjpMAYgo1bvNq-b7NSzCA5hzbUW31RXaMII,281
@@ -16,44 +16,44 @@ trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,56
16
16
  trilogy/core/functions.py,sha256=ARJAyBjeS415-54k3G_bx807rkPZonEulMaLRxSP7vU,10371
17
17
  trilogy/core/graph_models.py,sha256=oJUMSpmYhqXlavckHLpR07GJxuQ8dZ1VbB1fB0KaS8c,2036
18
18
  trilogy/core/internal.py,sha256=jNGFHKENnbMiMCtAgsnLZYVSENDK4b5ALecXFZpTDzQ,1075
19
- trilogy/core/models.py,sha256=ubtmdVsm_7QkigbvlAXEe7SXbZB502GLean0jFu8VLU,139884
20
- trilogy/core/optimization.py,sha256=A8S9C9H5RcQcFSQLYtEEBnm-r1CW_e9GEWlLK7q3MqA,4930
21
- trilogy/core/query_processor.py,sha256=fIRJWLBfMeKZ_o1-QB2rsQ8jZwsd9zxYkZ5QQzkGBus,17678
19
+ trilogy/core/models.py,sha256=mjORKd_RFvNc_c7QqCHCM4GrJSb5VA50tMqo7fX5-Bw,142855
20
+ trilogy/core/optimization.py,sha256=RJmDr2f9hyFePF-B6LpmHkj69-AzUm-nNtvO59899O8,6601
21
+ trilogy/core/query_processor.py,sha256=N5Jdbb8D8_mdeJufIch0uCbfuf2psSSkLxGXwC5CqCg,18663
22
22
  trilogy/core/optimizations/__init__.py,sha256=pxRzNzd2g8oRMy4f_ub5va6bNS2pd4hnyp9JBzTKc1E,300
23
23
  trilogy/core/optimizations/base_optimization.py,sha256=tWWT-xnTbnEU-mNi_isMNbywm8B9WTRsNFwGpeh3rqE,468
24
24
  trilogy/core/optimizations/inline_constant.py,sha256=kHNyc2UoaPVdYfVAPAFwnWuk4sJ_IF5faRtVcDOrBtw,1110
25
- trilogy/core/optimizations/inline_datasource.py,sha256=PyThSsQeZ6fi5sM2mUprC8kXXKo1sP8qLVwaGqpFDJI,3428
26
- trilogy/core/optimizations/predicate_pushdown.py,sha256=iVZV_BVaL3I6wlELPOuJAiuF0heOM7bCUnNqyCxxRVw,6370
25
+ trilogy/core/optimizations/inline_datasource.py,sha256=AATzQ6YrtW_1-aQFjQyTYqEYKBoMFhek7ADfBr4uUdQ,3634
26
+ trilogy/core/optimizations/predicate_pushdown.py,sha256=4Y6zfJN3VVexkD6p9IYWN4BTL8RwW6hyNb0VQZ8ETdI,8020
27
27
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- trilogy/core/processing/concept_strategies_v3.py,sha256=A0-RVwoqD06gUapGRD4SANj3g9hniKPWs7LvqyeHtbg,24445
28
+ trilogy/core/processing/concept_strategies_v3.py,sha256=yjCiGKzghjAc0E0QAcdJ1Xr5RXHAQL2zMPOz-dMhEzM,25431
29
29
  trilogy/core/processing/graph_utils.py,sha256=aq-kqk4Iado2HywDxWEejWc-7PGO6Oa-ZQLAM6XWPHw,1199
30
- trilogy/core/processing/utility.py,sha256=rhnkn3KgB1MW17mZYSEZ8XFEbC__B7PPmZgpDql4Myg,13614
30
+ trilogy/core/processing/utility.py,sha256=15Qi68ktmnQmMt5jxrpueQ34oG5fSSgst3kIEejwF8A,14519
31
31
  trilogy/core/processing/node_generators/__init__.py,sha256=-mzYkRsaRNa_dfTckYkKVFSR8h8a3ihEiPJDU_tAmDo,672
32
32
  trilogy/core/processing/node_generators/basic_node.py,sha256=4242PNGTCm2tklqMIkqVu5Iv4m_IeTnOYXxDveuCDZM,2856
33
- trilogy/core/processing/node_generators/common.py,sha256=Z3R5Ucb0s09pzs3lkz-t9sI2A5R-SIum1UdwGvqKpXA,8975
34
- trilogy/core/processing/node_generators/filter_node.py,sha256=0zey11fmXhI-ihkv-cK43tUKHlwG10cHYogByOnGbiw,5433
35
- trilogy/core/processing/node_generators/group_node.py,sha256=u9X0gAEY7AAWwYsK4g3jPgxcTwSSlI1uDQqNIY6vfG8,2988
33
+ trilogy/core/processing/node_generators/common.py,sha256=lDBRq9X6dQ_xSwXxLLNDq2pW8D-XwAY-ylTJLMugkLw,9525
34
+ trilogy/core/processing/node_generators/filter_node.py,sha256=N8PoPvj-_ZQbucTyNqpZ1EyPTDAy6oB7DuEYCQxQBOs,7411
35
+ trilogy/core/processing/node_generators/group_node.py,sha256=JwT8qYeGbozrZNGAlpndc9aaIN7K9ROYkkc2rAS8Qz8,3079
36
36
  trilogy/core/processing/node_generators/group_to_node.py,sha256=nzITnhaALIT7FMonyo16nNo-kSrLfefa9sZBYecrvkU,2887
37
37
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=vP84dnLQy6dtypi6mUbt9sMAcmmrTgQ1Oz4GI6X1IEo,6421
38
38
  trilogy/core/processing/node_generators/node_merge_node.py,sha256=yZ75lsl8RtM8kr-XQxqZEU6fm-JN5DTJxNbneayu36c,12319
39
- trilogy/core/processing/node_generators/rowset_node.py,sha256=wFkzQtXfudvAGifySvobgc9Qo1usTEEKpSbjkzchNy0,6282
40
- trilogy/core/processing/node_generators/select_node.py,sha256=XGcz74XsWv5POWhV2gGC_jbi7T9g7sY-VtP3sjlppFc,19057
41
- trilogy/core/processing/node_generators/unnest_node.py,sha256=6CH66eGwpadNX7TzUhWZ8aqIisOtQeHINbLV6X3QBUk,1779
42
- trilogy/core/processing/node_generators/window_node.py,sha256=9nXUXUgQrNczU1gaOqhOZPNzCUxw-lkxt0R7HORI6ss,2582
43
- trilogy/core/processing/nodes/__init__.py,sha256=-8ONG7Nqa6sNLpst5wNNyMigyaHRfUKGdE3xce8E3mM,3998
44
- trilogy/core/processing/nodes/base_node.py,sha256=7AeDm9WK3uiDGUL6QefeZ8Ok9OcWhbh5h1f9K0hr8IQ,11191
45
- trilogy/core/processing/nodes/filter_node.py,sha256=stY0tkppSdfCD43ZKRfCxfX9XxdLJ9gEtDA2_wPDcGE,1962
46
- trilogy/core/processing/nodes/group_node.py,sha256=j3vtFQIn_t4EfCke8dGULlwVkG2KKPYfgGwo2rvgcww,6322
47
- trilogy/core/processing/nodes/merge_node.py,sha256=RAuBPHPIQcGHqyU_R4HT6Kq9E40fWjQoOUCIngLEGQI,14629
48
- trilogy/core/processing/nodes/select_node_v2.py,sha256=COu-WPuyabGAc3HTkJB-_7eNzR4OgxIL4YDuZIRrST0,7179
39
+ trilogy/core/processing/node_generators/rowset_node.py,sha256=6KVnuk75mRzWJ-jIk7e8azN8BIPPuCn-VxPlxDqfPVE,4616
40
+ trilogy/core/processing/node_generators/select_node.py,sha256=E8bKOAUpwLwZy1iiaFVD5sM4XK-eFpHgijdyIWLMyH4,18904
41
+ trilogy/core/processing/node_generators/unnest_node.py,sha256=gHjurhr86JFkbq6vxTHDQGDKt95EdotSFHVKgrR_Z3Q,1860
42
+ trilogy/core/processing/node_generators/window_node.py,sha256=lFfmEjX_mLB7MuOM6CuKNnks1CabokGImpwhbQzjnkE,3283
43
+ trilogy/core/processing/nodes/__init__.py,sha256=jyduHk96j5fpju72sc8swOiBjR3Md866kt8JZGkp3ZU,4866
44
+ trilogy/core/processing/nodes/base_node.py,sha256=KVLAPLlo5QQq9Cm6lTLsqLN7l8EyXGxQkKrZFHdyRp8,12871
45
+ trilogy/core/processing/nodes/filter_node.py,sha256=DBOSGFfkiILrZa1BlLv2uxUSkgWtSIKiZplqyKXPjg8,2132
46
+ trilogy/core/processing/nodes/group_node.py,sha256=RKqODYxbY8wmClvI8-k3UgXWL7qQv5Hxhz5AB-RVUu0,6344
47
+ trilogy/core/processing/nodes/merge_node.py,sha256=ZUcFIm5v4hip_Ml_oJXqVMrevUkE62wvPrvCrO9FWAc,14272
48
+ trilogy/core/processing/nodes/select_node_v2.py,sha256=QuXNcwgjTRYamOoIooGrp4ie6INcqA9whtC5LZWjD8s,7180
49
49
  trilogy/core/processing/nodes/unnest_node.py,sha256=JFtm90IVM-46aCYkTNIaJah6v9ApAfonjVhcVM1HmDE,1903
50
50
  trilogy/core/processing/nodes/window_node.py,sha256=X7qxLUKd3tekjUUsmH_4vz5b-U89gMnGd04VBxuu2Ns,1280
51
51
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
- trilogy/dialect/base.py,sha256=kamdgq1zTO2bF3bMAfvNZBNO3LKP-n2kYXPJKM6fMzw,30102
52
+ trilogy/dialect/base.py,sha256=_WKy2YJyDZdelHVX4mcnwKzAp2J-56UjoBpS5hG2yrg,30819
53
53
  trilogy/dialect/bigquery.py,sha256=15KJ-cOpBlk9O7FPviPgmg8xIydJeKx7WfmL3SSsPE8,2953
54
54
  trilogy/dialect/common.py,sha256=HVNPL8dGyQjT2REruV5C2YPpBbVR7KlD0akVM7GcuPI,3329
55
55
  trilogy/dialect/config.py,sha256=tLVEMctaTDhUgARKXUNfHUcIolGaALkQ0RavUvXAY4w,2994
56
- trilogy/dialect/duckdb.py,sha256=2ERqiG2owb3uvmU90Bdo1e2fq4ICWC3we2RUQpHVPwg,3173
56
+ trilogy/dialect/duckdb.py,sha256=u_gpL35kouWxoBLas1h0ABYY2QzlVtEh22hm5h0lCOM,3182
57
57
  trilogy/dialect/enums.py,sha256=4NdpsydBpDn6jnh0JzFz5VvQEtnShErWtWHVyT6TNpw,3948
58
58
  trilogy/dialect/postgres.py,sha256=ev1RJZsC8BB3vJSxJ4q-TTYqZ4Hk1NXUtuRkLrQEBX0,3254
59
59
  trilogy/dialect/presto.py,sha256=2Rs53UfPxKU0rJTcEbiS-Lxm-CDiqUGojh7yRpQgyRE,3416
@@ -69,14 +69,14 @@ trilogy/parsing/common.py,sha256=zNd5buKxK4z9WSszOk4zOI0GexDFukzZtdNfQxg3kVw,621
69
69
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
70
70
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
71
71
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
72
- trilogy/parsing/parse_engine.py,sha256=cIoQuDGaCx_HYgNjOvG6GDVGOu5pcLBylZsK-wy5r6Q,61098
72
+ trilogy/parsing/parse_engine.py,sha256=TD7dr9f4ffRcNB0BpzgaACqc-gD3Hy4_pOyLA7rmK7A,62563
73
73
  trilogy/parsing/render.py,sha256=Gy_6wVYPwYLf35Iota08sbqveuWILtUhI8MYStcvtJM,12174
74
- trilogy/parsing/trilogy.lark,sha256=cUcwxUTlxU7jKFzYEXYARLTsPHG5cVLk-Xhltw6m2lY,11357
74
+ trilogy/parsing/trilogy.lark,sha256=kj1DXkqRFAb20PXN4gGDtXtQI61nUv1U8nfPWSWCr1Y,11407
75
75
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
76
76
  trilogy/scripts/trilogy.py,sha256=PHxvv6f2ODv0esyyhWxlARgra8dVhqQhYl0lTrSyVNo,3729
77
- pytrilogy-0.0.2.8.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
78
- pytrilogy-0.0.2.8.dist-info/METADATA,sha256=bKl-Q_qHdoiH1rJZDDBXD425zCYaNShnuPioYKxBTAA,7906
79
- pytrilogy-0.0.2.8.dist-info/WHEEL,sha256=uCRv0ZEik_232NlR4YDw4Pv3Ajt5bKvMH13NUU7hFuI,91
80
- pytrilogy-0.0.2.8.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
81
- pytrilogy-0.0.2.8.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
82
- pytrilogy-0.0.2.8.dist-info/RECORD,,
77
+ pytrilogy-0.0.2.10.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
78
+ pytrilogy-0.0.2.10.dist-info/METADATA,sha256=sH_c8ZeuuG6nuSMIrKQvhLoHi67-4qWu3DZ5KM27FJQ,7907
79
+ pytrilogy-0.0.2.10.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
80
+ pytrilogy-0.0.2.10.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
81
+ pytrilogy-0.0.2.10.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
82
+ pytrilogy-0.0.2.10.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.1.1)
2
+ Generator: setuptools (74.1.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
trilogy/__init__.py CHANGED
@@ -4,6 +4,6 @@ from trilogy.executor import Executor
4
4
  from trilogy.parser import parse
5
5
  from trilogy.constants import CONFIG
6
6
 
7
- __version__ = "0.0.2.8"
7
+ __version__ = "0.0.2.10"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/constants.py CHANGED
@@ -24,6 +24,7 @@ class Optimizations:
24
24
  predicate_pushdown: bool = True
25
25
  datasource_inlining: bool = True
26
26
  constant_inlining: bool = True
27
+ constant_inline_cutoff: int = 3
27
28
  direct_return: bool = True
28
29
 
29
30
 
trilogy/core/models.py CHANGED
@@ -70,7 +70,7 @@ from trilogy.utility import unique
70
70
  from collections import UserList, UserDict
71
71
  from functools import cached_property
72
72
  from abc import ABC
73
-
73
+ from collections import defaultdict
74
74
 
75
75
  LOGGER_PREFIX = "[MODELS]"
76
76
 
@@ -154,7 +154,10 @@ class ConceptArgs(ABC):
154
154
  class SelectContext(ABC):
155
155
 
156
156
  def with_select_context(
157
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
157
+ self,
158
+ grain: Grain,
159
+ conditional: Conditional | Comparison | Parenthetical | None,
160
+ environment: Environment | None = None,
158
161
  ):
159
162
  raise NotImplementedError
160
163
 
@@ -166,6 +169,7 @@ class ConstantInlineable(ABC):
166
169
 
167
170
  class SelectTypeMixin(BaseModel):
168
171
  where_clause: Union["WhereClause", None] = Field(default=None)
172
+ having_clause: Union["HavingClause", None] = Field(default=None)
169
173
 
170
174
  @property
171
175
  def output_components(self) -> List[Concept]:
@@ -595,13 +599,16 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
595
599
  self,
596
600
  grain: Optional["Grain"] = None,
597
601
  conditional: Conditional | Comparison | Parenthetical | None = None,
602
+ environment: Environment | None = None,
598
603
  ) -> "Concept":
599
604
  if not all([isinstance(x, Concept) for x in self.keys or []]):
600
605
  raise ValueError(f"Invalid keys {self.keys} for concept {self.address}")
601
606
  new_grain = grain or self.grain
602
607
  new_lineage = self.lineage
603
608
  if isinstance(self.lineage, SelectContext):
604
- new_lineage = self.lineage.with_select_context(new_grain, conditional)
609
+ new_lineage = self.lineage.with_select_context(
610
+ new_grain, conditional, environment=environment
611
+ )
605
612
  return self.__class__(
606
613
  name=self.name,
607
614
  datatype=self.datatype,
@@ -788,7 +795,9 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
788
795
  return Granularity.MULTI_ROW
789
796
 
790
797
  def with_filter(
791
- self, condition: "Conditional | Comparison | Parenthetical"
798
+ self,
799
+ condition: "Conditional | Comparison | Parenthetical",
800
+ environment: Environment | None = None,
792
801
  ) -> "Concept":
793
802
  from trilogy.utility import string_to_hash
794
803
 
@@ -805,12 +814,15 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
805
814
  modifiers=self.modifiers,
806
815
  pseudonyms=self.pseudonyms,
807
816
  )
817
+ if environment:
818
+ environment.add_concept(new)
808
819
  return new
809
820
 
810
821
 
811
822
  class Grain(Mergeable, BaseModel):
812
823
  nested: bool = False
813
824
  components: List[Concept] = Field(default_factory=list, validate_default=True)
825
+ where_clause: Optional[WhereClause] = Field(default=None)
814
826
 
815
827
  @field_validator("components")
816
828
  def component_validator(cls, v, info: ValidationInfo):
@@ -836,10 +848,12 @@ class Grain(Mergeable, BaseModel):
836
848
 
837
849
  def __str__(self):
838
850
  if self.abstract:
839
- return (
840
- "Grain<Abstract" + ",".join([c.address for c in self.components]) + ">"
841
- )
842
- return "Grain<" + ",".join([c.address for c in self.components]) + ">"
851
+ base = "Grain<Abstract>"
852
+ else:
853
+ base = "Grain<" + ",".join([c.address for c in self.components]) + ">"
854
+ if self.where_clause:
855
+ base += f"|{str(self.where_clause)}"
856
+ return base
843
857
 
844
858
  def with_namespace(self, namespace: str) -> "Grain":
845
859
  return Grain(
@@ -1046,12 +1060,15 @@ class Function(Mergeable, Namespaced, SelectContext, BaseModel):
1046
1060
  return self.output_datatype
1047
1061
 
1048
1062
  def with_select_context(
1049
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
1063
+ self,
1064
+ grain: Grain,
1065
+ conditional: Conditional | Comparison | Parenthetical | None,
1066
+ environment: Environment | None = None,
1050
1067
  ) -> Function:
1051
1068
  if self.operator in FunctionClass.AGGREGATE_FUNCTIONS.value and conditional:
1052
1069
  base = [
1053
1070
  (
1054
- c.with_select_context(grain, conditional)
1071
+ c.with_select_context(grain, conditional, environment)
1055
1072
  if isinstance(
1056
1073
  c,
1057
1074
  SelectContext,
@@ -1061,7 +1078,7 @@ class Function(Mergeable, Namespaced, SelectContext, BaseModel):
1061
1078
  for c in self.arguments
1062
1079
  ]
1063
1080
  final = [
1064
- c.with_filter(conditional) if isinstance(c, Concept) else c
1081
+ c.with_filter(conditional, environment) if isinstance(c, Concept) else c
1065
1082
  for c in base
1066
1083
  ]
1067
1084
  return Function(
@@ -1077,7 +1094,7 @@ class Function(Mergeable, Namespaced, SelectContext, BaseModel):
1077
1094
  operator=self.operator,
1078
1095
  arguments=[
1079
1096
  (
1080
- c.with_select_context(grain, conditional)
1097
+ c.with_select_context(grain, conditional, environment)
1081
1098
  if isinstance(
1082
1099
  c,
1083
1100
  SelectContext,
@@ -1293,13 +1310,22 @@ class WindowItem(Mergeable, Namespaced, SelectContext, BaseModel):
1293
1310
  )
1294
1311
 
1295
1312
  def with_select_context(
1296
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
1313
+ self,
1314
+ grain: Grain,
1315
+ conditional: Conditional | Comparison | Parenthetical | None,
1316
+ environment: Environment | None = None,
1297
1317
  ) -> "WindowItem":
1298
1318
  return WindowItem(
1299
1319
  type=self.type,
1300
- content=self.content.with_select_context(grain, conditional),
1301
- over=[x.with_select_context(grain, conditional) for x in self.over],
1302
- order_by=[x.with_select_context(grain, conditional) for x in self.order_by],
1320
+ content=self.content.with_select_context(grain, conditional, environment),
1321
+ over=[
1322
+ x.with_select_context(grain, conditional, environment)
1323
+ for x in self.over
1324
+ ],
1325
+ order_by=[
1326
+ x.with_select_context(grain, conditional, environment)
1327
+ for x in self.order_by
1328
+ ],
1303
1329
  )
1304
1330
 
1305
1331
  @property
@@ -1368,11 +1394,14 @@ class FilterItem(Namespaced, SelectContext, BaseModel):
1368
1394
  )
1369
1395
 
1370
1396
  def with_select_context(
1371
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
1397
+ self,
1398
+ grain: Grain,
1399
+ conditional: Conditional | Comparison | Parenthetical | None,
1400
+ environment: Environment | None = None,
1372
1401
  ) -> FilterItem:
1373
1402
  return FilterItem(
1374
- content=self.content.with_select_context(grain, conditional),
1375
- where=self.where.with_select_context(grain, conditional),
1403
+ content=self.content.with_select_context(grain, conditional, environment),
1404
+ where=self.where.with_select_context(grain, conditional, environment),
1376
1405
  )
1377
1406
 
1378
1407
  @property
@@ -1452,9 +1481,17 @@ class OrderItem(Mergeable, SelectContext, Namespaced, BaseModel):
1452
1481
  return OrderItem(expr=self.expr.with_namespace(namespace), order=self.order)
1453
1482
 
1454
1483
  def with_select_context(
1455
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
1484
+ self,
1485
+ grain: Grain,
1486
+ conditional: Conditional | Comparison | Parenthetical | None,
1487
+ environment: Environment | None = None,
1456
1488
  ) -> "OrderItem":
1457
- return OrderItem(expr=self.expr.with_grain(grain), order=self.order)
1489
+ return OrderItem(
1490
+ expr=self.expr.with_select_context(
1491
+ grain, conditional=conditional, environment=environment
1492
+ ),
1493
+ order=self.order,
1494
+ )
1458
1495
 
1459
1496
  def with_merge(
1460
1497
  self, source: Concept, target: Concept, modifiers: List[Modifier]
@@ -1643,7 +1680,9 @@ class SelectStatement(Mergeable, Namespaced, SelectTypeMixin, BaseModel):
1643
1680
  )
1644
1681
  ):
1645
1682
  output.append(item)
1646
- return Grain(components=unique(output, "address"))
1683
+ return Grain(
1684
+ components=unique(output, "address"), where_clause=self.where_clause
1685
+ )
1647
1686
 
1648
1687
  def with_namespace(self, namespace: str) -> "SelectStatement":
1649
1688
  return SelectStatement(
@@ -1988,7 +2027,7 @@ class Datasource(Namespaced, BaseModel):
1988
2027
  return self.__repr__()
1989
2028
 
1990
2029
  def __hash__(self):
1991
- return (self.namespace + self.identifier).__hash__()
2030
+ return self.full_name.__hash__()
1992
2031
 
1993
2032
  def with_namespace(self, namespace: str):
1994
2033
  new_namespace = (
@@ -2173,9 +2212,9 @@ class BaseJoin(BaseModel):
2173
2212
  class QueryDatasource(BaseModel):
2174
2213
  input_concepts: List[Concept]
2175
2214
  output_concepts: List[Concept]
2215
+ datasources: List[Union[Datasource, "QueryDatasource"]]
2176
2216
  source_map: Dict[str, Set[Union[Datasource, "QueryDatasource", "UnnestJoin"]]]
2177
2217
 
2178
- datasources: List[Union[Datasource, "QueryDatasource"]]
2179
2218
  grain: Grain
2180
2219
  joins: List[BaseJoin | UnnestJoin]
2181
2220
  limit: Optional[int] = None
@@ -2227,19 +2266,16 @@ class QueryDatasource(BaseModel):
2227
2266
 
2228
2267
  @field_validator("source_map")
2229
2268
  @classmethod
2230
- def validate_source_map(cls, v, info: ValidationInfo):
2269
+ def validate_source_map(cls, v: dict, info: ValidationInfo):
2231
2270
  values = info.data
2232
- expected = {c.address for c in values["output_concepts"]}.union(
2233
- c.address for c in values["input_concepts"]
2234
- )
2235
- seen = set()
2236
- for k, _ in v.items():
2237
- seen.add(k)
2238
- for x in expected:
2239
- if x not in seen and CONFIG.validate_missing:
2240
- raise SyntaxError(
2241
- f"source map missing {x} on (expected {expected}, have {seen})"
2242
- )
2271
+ for key in ("input_concepts", "output_concepts"):
2272
+ if not values.get(key):
2273
+ continue
2274
+ for concept in values[key]:
2275
+ if concept.address not in v and CONFIG.validate_missing:
2276
+ raise SyntaxError(
2277
+ f"Missing source map for {concept.address} on {key}, have {v}"
2278
+ )
2243
2279
  return v
2244
2280
 
2245
2281
  def __str__(self):
@@ -2308,11 +2344,23 @@ class QueryDatasource(BaseModel):
2308
2344
  )
2309
2345
 
2310
2346
  merged_datasources = {}
2347
+
2311
2348
  for ds in [*self.datasources, *other.datasources]:
2312
2349
  if ds.full_name in merged_datasources:
2313
2350
  merged_datasources[ds.full_name] = merged_datasources[ds.full_name] + ds
2314
2351
  else:
2315
2352
  merged_datasources[ds.full_name] = ds
2353
+
2354
+ final_source_map = defaultdict(set)
2355
+ for key in self.source_map:
2356
+ final_source_map[key] = self.source_map[key].union(
2357
+ other.source_map.get(key, set())
2358
+ )
2359
+ for key in other.source_map:
2360
+ if key not in final_source_map:
2361
+ final_source_map[key] = other.source_map[key]
2362
+ for k, v in final_source_map.items():
2363
+ final_source_map[k] = set(merged_datasources[x.full_name] for x in list(v))
2316
2364
  qds = QueryDatasource(
2317
2365
  input_concepts=unique(
2318
2366
  self.input_concepts + other.input_concepts, "address"
@@ -2320,7 +2368,7 @@ class QueryDatasource(BaseModel):
2320
2368
  output_concepts=unique(
2321
2369
  self.output_concepts + other.output_concepts, "address"
2322
2370
  ),
2323
- source_map={**self.source_map, **other.source_map},
2371
+ source_map=final_source_map,
2324
2372
  datasources=list(merged_datasources.values()),
2325
2373
  grain=self.grain,
2326
2374
  joins=unique(self.joins + other.joins, "unique_id"),
@@ -2849,6 +2897,7 @@ class UndefinedConcept(Concept, Mergeable, Namespaced):
2849
2897
  self,
2850
2898
  grain: Optional["Grain"] = None,
2851
2899
  conditional: Conditional | Comparison | Parenthetical | None = None,
2900
+ environment: Environment | None = None,
2852
2901
  ) -> "UndefinedConcept":
2853
2902
  if not all([isinstance(x, Concept) for x in self.keys or []]):
2854
2903
  raise ValueError(f"Invalid keys {self.keys} for concept {self.address}")
@@ -2856,7 +2905,9 @@ class UndefinedConcept(Concept, Mergeable, Namespaced):
2856
2905
  if self.lineage:
2857
2906
  new_lineage = self.lineage
2858
2907
  if isinstance(self.lineage, SelectContext):
2859
- new_lineage = self.lineage.with_select_context(new_grain, conditional)
2908
+ new_lineage = self.lineage.with_select_context(
2909
+ new_grain, conditional, environment
2910
+ )
2860
2911
  else:
2861
2912
  new_lineage = None
2862
2913
  return self.__class__(
@@ -3299,7 +3350,9 @@ class LazyEnvironment(Environment):
3299
3350
  ) or name.startswith("_"):
3300
3351
  return super().__getattribute__(name)
3301
3352
  if not self.loaded:
3302
- print(f"lazily evaluating load path {self.load_path} to access {name}")
3353
+ logger.info(
3354
+ f"lazily evaluating load path {self.load_path} to access {name}"
3355
+ )
3303
3356
  from trilogy import parse
3304
3357
 
3305
3358
  env = Environment(working_path=str(self.working_path))
@@ -3441,16 +3494,23 @@ class Comparison(
3441
3494
  )
3442
3495
 
3443
3496
  def with_select_context(
3444
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
3497
+ self,
3498
+ grain: Grain,
3499
+ conditional: Conditional | Comparison | Parenthetical | None,
3500
+ environment: Environment | None = None,
3445
3501
  ):
3446
3502
  return self.__class__(
3447
3503
  left=(
3448
- self.left.with_select_context(grain, conditional)
3504
+ self.left.with_select_context(grain, conditional, environment)
3449
3505
  if isinstance(self.left, SelectContext)
3450
3506
  else self.left
3451
3507
  ),
3452
3508
  # the right side does NOT need to inherit select grain
3453
- right=self.right,
3509
+ right=(
3510
+ self.right.with_select_context(grain, conditional, environment)
3511
+ if isinstance(self.right, SelectContext)
3512
+ else self.right
3513
+ ),
3454
3514
  operator=self.operator,
3455
3515
  )
3456
3516
 
@@ -3534,12 +3594,15 @@ class SubselectComparison(Comparison):
3534
3594
  return [tuple(get_concept_arguments(self.right))]
3535
3595
 
3536
3596
  def with_select_context(
3537
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
3597
+ self,
3598
+ grain: Grain,
3599
+ conditional: Conditional | Comparison | Parenthetical | None,
3600
+ environment: Environment | None = None,
3538
3601
  ):
3539
- # there's no need to pass the select grain through to a subselect comparison
3602
+ # there's no need to pass the select grain through to a subselect comparison on the right
3540
3603
  return self.__class__(
3541
3604
  left=(
3542
- self.left.with_select_context(grain, conditional)
3605
+ self.left.with_select_context(grain, conditional, environment)
3543
3606
  if isinstance(self.left, SelectContext)
3544
3607
  else self.left
3545
3608
  ),
@@ -3570,12 +3633,17 @@ class CaseWhen(Namespaced, SelectContext, BaseModel):
3570
3633
  )
3571
3634
 
3572
3635
  def with_select_context(
3573
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
3636
+ self,
3637
+ grain: Grain,
3638
+ conditional: Conditional | Comparison | Parenthetical | None,
3639
+ environment: Environment | None = None,
3574
3640
  ) -> CaseWhen:
3575
3641
  return CaseWhen(
3576
- comparison=self.comparison.with_select_context(grain, conditional),
3642
+ comparison=self.comparison.with_select_context(
3643
+ grain, conditional, environment
3644
+ ),
3577
3645
  expr=(
3578
- (self.expr.with_select_context(grain, conditional))
3646
+ (self.expr.with_select_context(grain, conditional, environment))
3579
3647
  if isinstance(self.expr, SelectContext)
3580
3648
  else self.expr
3581
3649
  ),
@@ -3592,12 +3660,15 @@ class CaseElse(Namespaced, SelectContext, BaseModel):
3592
3660
  return get_concept_arguments(self.expr)
3593
3661
 
3594
3662
  def with_select_context(
3595
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
3663
+ self,
3664
+ grain: Grain,
3665
+ conditional: Conditional | Comparison | Parenthetical | None,
3666
+ environment: Environment | None = None,
3596
3667
  ) -> CaseElse:
3597
3668
  return CaseElse(
3598
3669
  discriminant=self.discriminant,
3599
3670
  expr=(
3600
- self.expr.with_select_context(grain, conditional)
3671
+ self.expr.with_select_context(grain, conditional, environment)
3601
3672
  if isinstance(
3602
3673
  self.expr,
3603
3674
  SelectContext,
@@ -3737,16 +3808,19 @@ class Conditional(
3737
3808
  )
3738
3809
 
3739
3810
  def with_select_context(
3740
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
3811
+ self,
3812
+ grain: Grain,
3813
+ conditional: Conditional | Comparison | Parenthetical | None,
3814
+ environment: Environment | None = None,
3741
3815
  ):
3742
3816
  return Conditional(
3743
3817
  left=(
3744
- self.left.with_select_context(grain, conditional)
3818
+ self.left.with_select_context(grain, conditional, environment)
3745
3819
  if isinstance(self.left, SelectContext)
3746
3820
  else self.left
3747
3821
  ),
3748
3822
  right=(
3749
- self.right.with_select_context(grain, conditional)
3823
+ self.right.with_select_context(grain, conditional, environment)
3750
3824
  if isinstance(self.right, SelectContext)
3751
3825
  else self.right
3752
3826
  ),
@@ -3855,13 +3929,16 @@ class AggregateWrapper(Mergeable, Namespaced, SelectContext, BaseModel):
3855
3929
  )
3856
3930
 
3857
3931
  def with_select_context(
3858
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
3932
+ self,
3933
+ grain: Grain,
3934
+ conditional: Conditional | Comparison | Parenthetical | None,
3935
+ environment: Environment | None = None,
3859
3936
  ) -> AggregateWrapper:
3860
3937
  if not self.by:
3861
3938
  by = grain.components_copy
3862
3939
  else:
3863
3940
  by = self.by
3864
- parent = self.function.with_select_context(grain, conditional)
3941
+ parent = self.function.with_select_context(grain, conditional, environment)
3865
3942
  return AggregateWrapper(function=parent, by=by)
3866
3943
 
3867
3944
 
@@ -3893,10 +3970,15 @@ class WhereClause(Mergeable, ConceptArgs, Namespaced, SelectContext, BaseModel):
3893
3970
  return WhereClause(conditional=self.conditional.with_namespace(namespace))
3894
3971
 
3895
3972
  def with_select_context(
3896
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
3973
+ self,
3974
+ grain: Grain,
3975
+ conditional: Conditional | Comparison | Parenthetical | None,
3976
+ environment: Environment | None = None,
3897
3977
  ) -> WhereClause:
3898
3978
  return WhereClause(
3899
- conditional=self.conditional.with_select_context(grain, conditional)
3979
+ conditional=self.conditional.with_select_context(
3980
+ grain, conditional, environment
3981
+ )
3900
3982
  )
3901
3983
 
3902
3984
  @property
@@ -3909,6 +3991,22 @@ class WhereClause(Mergeable, ConceptArgs, Namespaced, SelectContext, BaseModel):
3909
3991
  output += item.grain.components if item.grain else []
3910
3992
  return Grain(components=list(set(output)))
3911
3993
 
3994
+ @property
3995
+ def components(self):
3996
+ from trilogy.core.processing.utility import decompose_condition
3997
+
3998
+ return decompose_condition(self.conditional)
3999
+
4000
+ @property
4001
+ def is_scalar(self):
4002
+ from trilogy.core.processing.utility import is_scalar_condition
4003
+
4004
+ return is_scalar_condition(self.conditional)
4005
+
4006
+
4007
+ class HavingClause(WhereClause):
4008
+ pass
4009
+
3912
4010
 
3913
4011
  class MaterializedDataset(BaseModel):
3914
4012
  address: Address
@@ -3928,6 +4026,7 @@ class ProcessedQuery(BaseModel):
3928
4026
  hidden_columns: List[Concept] = Field(default_factory=list)
3929
4027
  limit: Optional[int] = None
3930
4028
  where_clause: Optional[WhereClause] = None
4029
+ having_clause: Optional[HavingClause] = None
3931
4030
  order_by: Optional[OrderBy] = None
3932
4031
 
3933
4032
 
@@ -4130,11 +4229,14 @@ class Parenthetical(
4130
4229
  )
4131
4230
 
4132
4231
  def with_select_context(
4133
- self, grain: Grain, conditional: Conditional | Comparison | Parenthetical | None
4232
+ self,
4233
+ grain: Grain,
4234
+ conditional: Conditional | Comparison | Parenthetical | None,
4235
+ environment: Environment | None = None,
4134
4236
  ):
4135
4237
  return Parenthetical(
4136
4238
  content=(
4137
- self.content.with_select_context(grain, conditional)
4239
+ self.content.with_select_context(grain, conditional, environment)
4138
4240
  if isinstance(self.content, SelectContext)
4139
4241
  else self.content
4140
4242
  )