pytrilogy 0.0.3.113__py3-none-any.whl → 0.0.3.115__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.113
3
+ Version: 0.0.3.115
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Classifier: Programming Language :: Python
6
6
  Classifier: Programming Language :: Python :: 3
@@ -1,6 +1,6 @@
1
- pytrilogy-0.0.3.113.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=lAuWMqhwvK53mopDwiFjvqXcgCmF97vD3MVcY8Npwq4,304
3
- trilogy/constants.py,sha256=g_zkVCNjGop6coZ1kM8eXXAzCnUN22ldx3TYFz0E9sc,1747
1
+ pytrilogy-0.0.3.115.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=L11AizxIDnbCCjCvAE55VTsy0d4QmvAZfNp61uAPu00,304
3
+ trilogy/constants.py,sha256=2ERigKZBmULg3Qr43nWKgdwqiCnT8XPQVeH-O-k66wc,2640
4
4
  trilogy/engine.py,sha256=v4TpNktM4zZ9OX7jZH2nde4dpX5uAH2U23ELfULTCSg,2280
5
5
  trilogy/executor.py,sha256=q3EsAjzgxNxPn-yTHD_FTFzm7bJ2mlf9CrJEjyt6-pE,17884
6
6
  trilogy/parser.py,sha256=o4cfk3j3yhUFoiDKq9ZX_GjBF3dKhDjXEwb63rcBkBM,293
@@ -23,20 +23,20 @@ trilogy/ai/providers/utils.py,sha256=yttP6y2E_XzdytBCwhaKekfXfxM6gE6MRce4AtyLL60
23
23
  trilogy/authoring/__init__.py,sha256=TABMOETSMERrWuyDLR0nK4ISlqR0yaqeXrmuOdrSvAY,3060
24
24
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  trilogy/core/constants.py,sha256=nizWYDCJQ1bigQMtkNIEMNTcN0NoEAXiIHLzpelxQ24,201
26
- trilogy/core/enums.py,sha256=B0EbyjIBFl5SaP0J3lHIqudgy16fXCf9rlr16yxk6kk,8933
26
+ trilogy/core/enums.py,sha256=R_iNsK8j0uGJ5fzfFUwoe1vhIHTX28VJfDQd99WBQ4E,9064
27
27
  trilogy/core/env_processor.py,sha256=H-rr2ALj31l5oh3FqeI47Qju6OOfiXBacXNJGNZ92zQ,4521
28
28
  trilogy/core/environment_helpers.py,sha256=TRlqVctqIRBxzfjRBmpQsAVoiCcsEKBhG1B6PUE0l1M,12743
29
29
  trilogy/core/ergonomics.py,sha256=e-7gE29vPLFdg0_A1smQ7eOrUwKl5VYdxRSTddHweRA,1631
30
30
  trilogy/core/exceptions.py,sha256=axkVXYJYQXCCwMHwlyDA232g4tCOwdCZUt7eHeUMDMg,2829
31
- trilogy/core/functions.py,sha256=966xC6ypgzWwCs4BvORFwzWEMJoVlqvH2biipkIYl4E,34005
31
+ trilogy/core/functions.py,sha256=hURJBM5dNcHfXuPHaHM_tz_ahsEktnmL4At0zebfjMk,34668
32
32
  trilogy/core/graph_models.py,sha256=4EWFTHGfYd72zvS2HYoV6hm7nMC_VEd7vWr6txY-ig0,3400
33
33
  trilogy/core/internal.py,sha256=r9QagDB2GvpqlyD_I7VrsfbVfIk5mnok2znEbv72Aa4,2681
34
34
  trilogy/core/optimization.py,sha256=Km0ITEx9n6Iv5ReX6tm4uXO5uniSv_ooahycNNiET3g,9212
35
35
  trilogy/core/query_processor.py,sha256=rMrtLSQxVm7yeyh0nWjDNI9nnu4Xi0NgHvBJ14gvu4I,20384
36
36
  trilogy/core/utility.py,sha256=3VC13uSQWcZNghgt7Ot0ZTeEmNqs__cx122abVq9qhM,410
37
37
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- trilogy/core/models/author.py,sha256=YXom_XC-wdcZmJ2PexWCrJ2Slr3FnrxA__X4ayc_A8w,81792
39
- trilogy/core/models/build.py,sha256=iqk_-3plxX1BdxvUCTebqE9F3x62f40neKGf6Ld4VVU,70858
38
+ trilogy/core/models/author.py,sha256=DFpIGlIruz0JYIOLJ0qZQrsdT4BvQX0augow1G8pD54,81841
39
+ trilogy/core/models/build.py,sha256=LN8G1JRk8RDSFQwqS3CUszIIUgB5v9Nxy6EpxvAcJ1A,71514
40
40
  trilogy/core/models/build_environment.py,sha256=mpx7MKGc60fnZLVdeLi2YSREy7eQbQYycCrP4zF-rHU,5258
41
41
  trilogy/core/models/core.py,sha256=iT9WdZoiXeglmUHWn6bZyXCTBpkApTGPKtNm_Mhbu_g,12987
42
42
  trilogy/core/models/datasource.py,sha256=wogTevZ-9CyUW2a8gjzqMCieircxi-J5lkI7EOAZnck,9596
@@ -48,9 +48,9 @@ trilogy/core/optimizations/hide_unused_concept.py,sha256=DbsP8NqQOxmPv9omDOoFNPU
48
48
  trilogy/core/optimizations/inline_datasource.py,sha256=2sWNRpoRInnTgo9wExVT_r9RfLAQHI57reEV5cGHUcg,4329
49
49
  trilogy/core/optimizations/predicate_pushdown.py,sha256=5ubatgq1IwWQ4L2FDt4--y168YLuGP-vwqH0m8IeTIw,9786
50
50
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
- trilogy/core/processing/concept_strategies_v3.py,sha256=-iC2CLALmSrOglMKZM4TslVncyOrJbUH0V_COmbqHIw,22681
52
- trilogy/core/processing/discovery_node_factory.py,sha256=p23jiiHyhrW-Q8ndbnRlqMHJKT8ZqPOA89SzE4xaFFo,15445
53
- trilogy/core/processing/discovery_utility.py,sha256=BfzeliD0CQ3x3MRLpw3msibGc7rOPh8aH_gbfuchAIs,13352
51
+ trilogy/core/processing/concept_strategies_v3.py,sha256=Y6AG10NGU_mUiTfXmxkss7QSO5iLpOdzGN6-7Hc5XiQ,23294
52
+ trilogy/core/processing/discovery_node_factory.py,sha256=llnLxZo1NqBRIuuPz0GUohym6LZFhVkPT3xSiORi3k4,15446
53
+ trilogy/core/processing/discovery_utility.py,sha256=mO0npZMRlQSzxt3l4m8garKBAOrXFkzt3eiiUyUSoIU,13528
54
54
  trilogy/core/processing/discovery_validation.py,sha256=eZ4HfHMpqZLI8MGG2jez8arS8THs6ceuVrQFIY6gXrU,5364
55
55
  trilogy/core/processing/graph_utils.py,sha256=8QUVrkE9j-9C1AyrCb1nQEh8daCe0u1HuXl-Te85lag,1205
56
56
  trilogy/core/processing/utility.py,sha256=ESs6pKqVP2c9eMdfB2JNjw7D7YnoezVwbLFx1D6OUYA,26088
@@ -58,7 +58,7 @@ trilogy/core/processing/node_generators/__init__.py,sha256=iVJ-crowPxYeut-hFjyEj
58
58
  trilogy/core/processing/node_generators/basic_node.py,sha256=rv53il-W633by0plvbN5qaqznJfl60yPinLZGBAC_MI,5707
59
59
  trilogy/core/processing/node_generators/common.py,sha256=xF32Kf6B08dZgKs2SOow1HomptSiSC057GCUCHFlS5s,9464
60
60
  trilogy/core/processing/node_generators/constant_node.py,sha256=LfpDq2WrBRZ3tGsLxw77LuigKfhbteWWh9L8BGdMGwk,1146
61
- trilogy/core/processing/node_generators/filter_node.py,sha256=cJ5od1fAfvalaUDO2O4Y6Yrr2RukOCqey7f3zrKSBbI,10808
61
+ trilogy/core/processing/node_generators/filter_node.py,sha256=EiP_tafx-X0gM-BIVRCy2rnp1_apt2cbhVfv8cg9dVg,11259
62
62
  trilogy/core/processing/node_generators/group_node.py,sha256=sIm1QYrF4EY6sk56A48B6MieCZqvaJLSQebih_aiKnQ,8567
63
63
  trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZwaRU0sbUSr9H0moQbAxrGgX6iGA,3832
64
64
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=dHPDoSKU0FF6Ue_t_LkZxTd0Q-Sf-EpYdsMYdyUlFQc,7120
@@ -69,7 +69,7 @@ trilogy/core/processing/node_generators/select_merge_node.py,sha256=9MZdQJtkoe5N
69
69
  trilogy/core/processing/node_generators/select_node.py,sha256=Ta1G39V94gjX_AgyZDz9OqnwLz4BjY3D6Drx9YpziMQ,3555
70
70
  trilogy/core/processing/node_generators/synonym_node.py,sha256=AnAsa_Wj50NJ_IK0HSgab_7klYmKVrv0WI1uUe-GvEY,3766
71
71
  trilogy/core/processing/node_generators/union_node.py,sha256=NxQbnRRoYMI4WjMeph41yk4E6yipj53qdGuNt-Mozxw,2818
72
- trilogy/core/processing/node_generators/unnest_node.py,sha256=u_hVHFYMz-ZylDdHH9mhFSRpxuKcTGvrrOP0rxrY_Xg,3901
72
+ trilogy/core/processing/node_generators/unnest_node.py,sha256=Q9E6MF6uVLqP15tlx_Bwa6mIWSNW68LJpkSYyr_TArs,6229
73
73
  trilogy/core/processing/node_generators/window_node.py,sha256=wNvmumGO6AIQ7C9bDUYYZ6LJvDvPQPfFVX82pTxjV-k,6767
74
74
  trilogy/core/processing/node_generators/select_helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
75
  trilogy/core/processing/node_generators/select_helpers/datasource_injection.py,sha256=m2YQ4OmG0N2O61a7NEq1ZzbTa7JsCC00lxB2ymjcYRI,8224
@@ -84,7 +84,7 @@ trilogy/core/processing/nodes/union_node.py,sha256=hLAXXVWqEgMWi7dlgSHfCF59fon64
84
84
  trilogy/core/processing/nodes/unnest_node.py,sha256=oLKMMNMx6PLDPlt2V5neFMFrFWxET8r6XZElAhSNkO0,2181
85
85
  trilogy/core/processing/nodes/window_node.py,sha256=JXJ0iVRlSEM2IBr1TANym2RaUf_p5E_l2sNykRzXWDo,1710
86
86
  trilogy/core/statements/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
- trilogy/core/statements/author.py,sha256=w_d55It4zs8qBMyEGxRyUjgMX0U3AQojG_GJ587UgrM,16414
87
+ trilogy/core/statements/author.py,sha256=zLd-1IZKF5UE7oWQYqifn3b3JcW2wCbonlELjJiZhfo,16436
88
88
  trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
89
  trilogy/core/statements/common.py,sha256=VnVLULQg1TJLNUFzJaROT1tsf2ewk3IpuhvZaP36R6A,535
90
90
  trilogy/core/statements/execute.py,sha256=kiwJcVeMa4wZR-xLfM2oYOJ9DeyJkP8An38WFyJxktM,2413
@@ -95,7 +95,7 @@ trilogy/core/validation/datasource.py,sha256=nJeEFyb6iMBwlEVdYVy1vLzAbdRZwOsUjGx
95
95
  trilogy/core/validation/environment.py,sha256=ymvhQyt7jLK641JAAIQkqjQaAmr9C5022ILzYvDgPP0,2835
96
96
  trilogy/core/validation/fix.py,sha256=Z818UFNLxndMTLiyhB3doLxIfnOZ-16QGvVFWuD7UsA,3750
97
97
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
- trilogy/dialect/base.py,sha256=B_zjQ6HyOQEW0iRGgGBjhqJ1Xr-KODk1fUBZDAWfE54,50798
98
+ trilogy/dialect/base.py,sha256=Eo8n5fJiHOYkfDDDFgnrqMuIZTxRorWwVer_1-DRG4c,51178
99
99
  trilogy/dialect/bigquery.py,sha256=XS3hpybeowgfrOrkycAigAF3NX2YUzTzfgE6f__2fT4,4316
100
100
  trilogy/dialect/common.py,sha256=I5Ku_TR5MwJTB3ZhcQenrtvXhH2RvTQ8wQe9w5lfkfA,5708
101
101
  trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
@@ -113,13 +113,13 @@ trilogy/hooks/graph_hook.py,sha256=5BfR7Dt0bgEsCLgwjowgCsVkboGYfVJGOz8g9mqpnos,4
113
113
  trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
114
114
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
115
115
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
- trilogy/parsing/common.py,sha256=alayr5yy6MZOLTnw8CKLtxU2drtzbq4bKteQWfTw9QU,32281
116
+ trilogy/parsing/common.py,sha256=r9EmWzN2ozMKtWCY1HfjOREJnLKvaURT6h6edS5U4x8,32508
117
117
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
118
118
  trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
119
119
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
120
- trilogy/parsing/parse_engine.py,sha256=p758ukVifI_ygWp-1vJIy3X5NZVmwpNbbxVDfbkkTbU,82253
121
- trilogy/parsing/render.py,sha256=FhSU3-bMA0YM3oEn6nfpfjbM74nvH2r1TtFgbWNzOsM,24204
122
- trilogy/parsing/trilogy.lark,sha256=EN0Nrwz8cagzt69O85VSteW-k30lj8U5bRtXetM0JiU,16671
120
+ trilogy/parsing/parse_engine.py,sha256=lMo73QD37ih5kgvAOToMLCBbCkBzo_G7QMOaQaHprV8,84333
121
+ trilogy/parsing/render.py,sha256=k7MNp8EBTqVBSVqFlgTHSwIhfSKLyJfSeb2fSbt9dVA,24307
122
+ trilogy/parsing/trilogy.lark,sha256=kPOIrwa__kdXvxS3uM5GzdlgIa2cQxNW_0DNRqsRBeg,16939
123
123
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
124
124
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
125
125
  trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -132,8 +132,8 @@ trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
132
132
  trilogy/std/net.preql,sha256=WZCuvH87_rZntZiuGJMmBDMVKkdhTtxeHOkrXNwJ1EE,416
133
133
  trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
134
134
  trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
135
- pytrilogy-0.0.3.113.dist-info/METADATA,sha256=4Ix5np1ZL_PCT0B6i4e8mazXdFNvRkpHznssca1beeI,12911
136
- pytrilogy-0.0.3.113.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
137
- pytrilogy-0.0.3.113.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
138
- pytrilogy-0.0.3.113.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
139
- pytrilogy-0.0.3.113.dist-info/RECORD,,
135
+ pytrilogy-0.0.3.115.dist-info/METADATA,sha256=pB3OdY3t5s8GkX73rniQKQkMSy8JcXjzKZ0w1va3k5E,12911
136
+ pytrilogy-0.0.3.115.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
137
+ pytrilogy-0.0.3.115.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
138
+ pytrilogy-0.0.3.115.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
139
+ pytrilogy-0.0.3.115.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.113"
7
+ __version__ = "0.0.3.115"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/constants.py CHANGED
@@ -1,7 +1,9 @@
1
1
  import random
2
+ from contextlib import contextmanager
2
3
  from dataclasses import dataclass, field
3
4
  from enum import Enum
4
5
  from logging import getLogger
6
+ from typing import Any
5
7
 
6
8
  logger = getLogger("trilogy")
7
9
 
@@ -50,6 +52,32 @@ class Rendering:
50
52
  parameters: bool = True
51
53
  concise: bool = False
52
54
 
55
+ @contextmanager
56
+ def temporary(self, **kwargs: Any):
57
+ """
58
+ Context manager to temporarily set attributes and revert them afterwards.
59
+
60
+ Usage:
61
+ r = Rendering()
62
+ with r.temporary(parameters=False, concise=True):
63
+ # parameters is False, concise is True here
64
+ do_something()
65
+ # parameters and concise are back to their original values
66
+ """
67
+ # Store original values
68
+ original_values = {key: getattr(self, key) for key in kwargs}
69
+
70
+ # Set new values
71
+ for key, value in kwargs.items():
72
+ setattr(self, key, value)
73
+
74
+ try:
75
+ yield self
76
+ finally:
77
+ # Restore original values
78
+ for key, value in original_values.items():
79
+ setattr(self, key, value)
80
+
53
81
 
54
82
  @dataclass
55
83
  class Parsing:
trilogy/core/enums.py CHANGED
@@ -169,6 +169,7 @@ class FunctionType(Enum):
169
169
  ARRAY_SORT = "array_sort"
170
170
  ARRAY_TRANSFORM = "array_transform"
171
171
  ARRAY_TO_STRING = "array_to_string"
172
+ ARRAY_FILTER = "array_filter"
172
173
 
173
174
  # MAP
174
175
  MAP_KEYS = "map_keys"
@@ -204,6 +205,7 @@ class FunctionType(Enum):
204
205
  MIN = "min"
205
206
  AVG = "avg"
206
207
  ARRAY_AGG = "array_agg"
208
+ ANY = "any"
207
209
 
208
210
  # String
209
211
  LIKE = "like"
@@ -244,6 +246,7 @@ class FunctionType(Enum):
244
246
  DATE_ADD = "date_add"
245
247
  DATE_SUB = "date_sub"
246
248
  DATE_DIFF = "date_diff"
249
+ DATE_SPINE = "date_spine"
247
250
 
248
251
  # UNIX
249
252
  UNIX_TO_TIMESTAMP = "unix_to_timestamp"
@@ -263,6 +266,7 @@ class FunctionClass(Enum):
263
266
  FunctionType.ARRAY_AGG,
264
267
  FunctionType.COUNT,
265
268
  FunctionType.COUNT_DISTINCT,
269
+ FunctionType.ANY,
266
270
  ]
267
271
  SINGLE_ROW = [
268
272
  FunctionType.CONSTANT,
@@ -270,7 +274,7 @@ class FunctionClass(Enum):
270
274
  FunctionType.CURRENT_DATETIME,
271
275
  ]
272
276
 
273
- ONE_TO_MANY = [FunctionType.UNNEST]
277
+ ONE_TO_MANY = [FunctionType.UNNEST, FunctionType.DATE_SPINE]
274
278
 
275
279
  RECURSIVE = [FunctionType.RECURSE_EDGE]
276
280
 
trilogy/core/functions.py CHANGED
@@ -212,6 +212,14 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
212
212
  output_type_function=get_unnest_output_type,
213
213
  arg_count=1,
214
214
  ),
215
+ FunctionType.DATE_SPINE: FunctionConfig(
216
+ valid_inputs={
217
+ DataType.DATE,
218
+ },
219
+ output_purpose=Purpose.KEY,
220
+ output_type=DataType.DATE,
221
+ arg_count=2,
222
+ ),
215
223
  FunctionType.RECURSE_EDGE: FunctionConfig(
216
224
  arg_count=2,
217
225
  ),
@@ -318,6 +326,18 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
318
326
  output_type_function=get_transform_output_type,
319
327
  arg_count=3,
320
328
  ),
329
+ FunctionType.ARRAY_FILTER: FunctionConfig(
330
+ valid_inputs=[
331
+ {
332
+ DataType.ARRAY,
333
+ },
334
+ {*DataType},
335
+ {*DataType},
336
+ ],
337
+ output_purpose=Purpose.PROPERTY,
338
+ output_type_function=get_transform_output_type,
339
+ arg_count=3,
340
+ ),
321
341
  FunctionType.ARRAY_TO_STRING: FunctionConfig(
322
342
  valid_inputs={
323
343
  DataType.ARRAY,
@@ -887,6 +907,11 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
887
907
  ),
888
908
  arg_count=1,
889
909
  ),
910
+ FunctionType.ANY: FunctionConfig(
911
+ valid_inputs={*DataType},
912
+ output_purpose=Purpose.PROPERTY,
913
+ arg_count=1,
914
+ ),
890
915
  FunctionType.AVG: FunctionConfig(
891
916
  valid_inputs={
892
917
  DataType.INTEGER,
@@ -1234,7 +1234,7 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1234
1234
  elif (
1235
1235
  lineage
1236
1236
  and isinstance(lineage, (BuildFunction, Function))
1237
- and lineage.operator == FunctionType.UNNEST
1237
+ and lineage.operator in FunctionClass.ONE_TO_MANY.value
1238
1238
  ):
1239
1239
  return Derivation.UNNEST
1240
1240
  elif (
@@ -1286,7 +1286,8 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1286
1286
  elif (
1287
1287
  lineage
1288
1288
  and isinstance(lineage, (Function, BuildFunction))
1289
- and lineage.operator in (FunctionType.UNNEST, FunctionType.UNION)
1289
+ and lineage.operator
1290
+ in (FunctionType.UNNEST, FunctionType.UNION, FunctionType.DATE_SPINE)
1290
1291
  ):
1291
1292
  return Granularity.MULTI_ROW
1292
1293
  elif lineage and all(
@@ -134,8 +134,9 @@ def concept_is_relevant(
134
134
  if concept.purpose in (Purpose.METRIC,):
135
135
  if all([c in others for c in concept.grain.components]):
136
136
  return False
137
+ if concept.derivation in (Derivation.UNNEST,):
138
+ return True
137
139
  if concept.derivation in (Derivation.BASIC,):
138
-
139
140
  return any(concept_is_relevant(c, others) for c in concept.concept_arguments)
140
141
  if concept.granularity == Granularity.SINGLE_ROW:
141
142
  return False
@@ -1668,7 +1669,6 @@ class Factory:
1668
1669
  valid_inputs=base.valid_inputs,
1669
1670
  arg_count=base.arg_count,
1670
1671
  )
1671
-
1672
1672
  new = BuildFunction(
1673
1673
  operator=base.operator,
1674
1674
  arguments=[self.handle_constant(self.build(c)) for c in raw_args],
@@ -1724,6 +1724,14 @@ class Factory:
1724
1724
  return self._build_concept(base)
1725
1725
 
1726
1726
  def _build_concept(self, base: Concept) -> BuildConcept:
1727
+ try:
1728
+ return self.__build_concept(base)
1729
+ except RecursionError as e:
1730
+ raise RecursionError(
1731
+ f"Recursion error building concept {base.address}. This is likely due to a circular reference."
1732
+ ) from e
1733
+
1734
+ def __build_concept(self, base: Concept) -> BuildConcept:
1727
1735
  # TODO: if we are using parameters, wrap it in a new model and use that in rendering
1728
1736
  if base.address in self.local_concepts:
1729
1737
  return self.local_concepts[base.address]
@@ -2002,6 +2010,13 @@ class Factory:
2002
2010
  def _build_tuple_wrapper(self, base: TupleWrapper) -> TupleWrapper:
2003
2011
  return TupleWrapper(val=[self.build(x) for x in base.val], type=base.type)
2004
2012
 
2013
+ @build.register
2014
+ def _(self, base: ListWrapper) -> ListWrapper:
2015
+ return self._build_list_wrapper(base)
2016
+
2017
+ def _build_list_wrapper(self, base: ListWrapper) -> ListWrapper:
2018
+ return ListWrapper([self.build(x) for x in base], type=base.type)
2019
+
2005
2020
  @build.register
2006
2021
  def _(self, base: FilterItem) -> BuildFilterItem:
2007
2022
  return self._build_filter_item(base)
@@ -306,7 +306,12 @@ def evaluate_loop_conditions(
306
306
 
307
307
 
308
308
  def check_for_early_exit(
309
- complete, partial, missing, context: LoopContext, priority_concept: BuildConcept
309
+ complete: ValidationResult,
310
+ found: set[str],
311
+ partial: set[str],
312
+ missing: set[str],
313
+ context: LoopContext,
314
+ priority_concept: BuildConcept,
310
315
  ) -> bool:
311
316
  if complete == ValidationResult.INCOMPLETE_CONDITION:
312
317
  cond_dict = {str(node): node.preexisting_conditions for node in context.stack}
@@ -331,8 +336,18 @@ def check_for_early_exit(
331
336
  f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Breaking as we have attempted all nodes"
332
337
  )
333
338
  return True
339
+ elif all(
340
+ [
341
+ x.address in found and x.address not in partial
342
+ for x in context.mandatory_list
343
+ ]
344
+ ):
345
+ logger.info(
346
+ f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Breaking as we have found all mandatory nodes without partials"
347
+ )
348
+ return True
334
349
  logger.info(
335
- f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Found complete stack with partials {partial}, continuing search, attempted {context.attempted} all {len(context.mandatory_list)}"
350
+ f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Found complete stack with partials {partial}, continuing search, attempted {context.attempted} of total {len(context.mandatory_list)}."
336
351
  )
337
352
  else:
338
353
  logger.info(
@@ -436,6 +451,7 @@ def generate_loop_completion(context: LoopContext, virtual: set[str]) -> Strateg
436
451
  context.original_mandatory,
437
452
  context.environment,
438
453
  non_virtual_difference_values,
454
+ depth=context.depth,
439
455
  )
440
456
 
441
457
  return group_if_required_v2(
@@ -443,6 +459,7 @@ def generate_loop_completion(context: LoopContext, virtual: set[str]) -> Strateg
443
459
  context.original_mandatory,
444
460
  context.environment,
445
461
  non_virtual_difference_values,
462
+ depth=context.depth,
446
463
  )
447
464
 
448
465
 
@@ -466,6 +483,7 @@ def _search_concepts(
466
483
  conditions=conditions,
467
484
  )
468
485
 
486
+ # if we get a can
469
487
  if candidate:
470
488
  return candidate
471
489
  context = initialize_loop_context(
@@ -477,13 +495,14 @@ def _search_concepts(
477
495
  accept_partial=accept_partial,
478
496
  conditions=conditions,
479
497
  )
480
-
498
+ partial: set[str] = set()
481
499
  while context.incomplete:
482
500
 
483
501
  priority_concept = get_priority_concept(
484
502
  context.mandatory_list,
485
503
  context.attempted,
486
504
  found_concepts=context.found,
505
+ partial_concepts=partial,
487
506
  depth=depth,
488
507
  )
489
508
 
@@ -538,7 +557,7 @@ def _search_concepts(
538
557
  # assign
539
558
  context.found = found_c
540
559
  early_exit = check_for_early_exit(
541
- complete, partial, missing_c, context, priority_concept
560
+ complete, found_c, partial, missing_c, context, priority_concept
542
561
  )
543
562
  if early_exit:
544
563
  break
@@ -608,4 +627,4 @@ def source_query_concepts(
608
627
  logger.info(
609
628
  f"{depth_to_prefix(0)}{LOGGER_PREFIX} final concepts are {[x.address for x in final]}"
610
629
  )
611
- return group_if_required_v2(root, output_concepts, environment)
630
+ return group_if_required_v2(root, output_concepts, environment, depth=0)
@@ -187,7 +187,7 @@ def _generate_aggregate_node(ctx: NodeGenerationContext) -> StrategyNode | None:
187
187
 
188
188
  logger.info(
189
189
  f"{depth_to_prefix(ctx.depth)}{LOGGER_PREFIX} "
190
- f"for {ctx.concept.address}, generating aggregate node with {agg_optional}"
190
+ f"for {ctx.concept.address}, generating aggregate node with optional {agg_optional}"
191
191
  )
192
192
 
193
193
  return gen_group_node(
@@ -441,7 +441,7 @@ def generate_node(
441
441
  depth: int,
442
442
  source_concepts: SearchConceptsType,
443
443
  history: History,
444
- accept_partial: bool = False,
444
+ accept_partial: bool,
445
445
  conditions: BuildWhereClause | None = None,
446
446
  ) -> StrategyNode | None:
447
447
 
@@ -184,10 +184,14 @@ def group_if_required_v2(
184
184
  final: List[BuildConcept],
185
185
  environment: BuildEnvironment,
186
186
  where_injected: set[str] | None = None,
187
+ depth: int = 0,
187
188
  ):
188
189
  where_injected = where_injected or set()
189
190
  required = check_if_group_required(
190
- downstream_concepts=final, parents=[root.resolve()], environment=environment
191
+ downstream_concepts=final,
192
+ parents=[root.resolve()],
193
+ environment=environment,
194
+ depth=depth,
191
195
  )
192
196
  targets = [
193
197
  x
@@ -258,6 +262,7 @@ def get_priority_concept(
258
262
  all_concepts: List[BuildConcept],
259
263
  attempted_addresses: set[str],
260
264
  found_concepts: set[str],
265
+ partial_concepts: set[str],
261
266
  depth: int,
262
267
  ) -> BuildConcept:
263
268
  # optimized search for missing concepts
@@ -265,13 +270,15 @@ def get_priority_concept(
265
270
  [
266
271
  c
267
272
  for c in all_concepts
268
- if c.address not in attempted_addresses and c.address not in found_concepts
273
+ if c.address not in attempted_addresses
274
+ and (c.address not in found_concepts or c.address in partial_concepts)
269
275
  ],
270
276
  key=lambda x: x.address,
271
277
  )
272
278
  # sometimes we need to scan intermediate concepts to get merge keys or filter keys,
273
279
  # so do an exhaustive search
274
- # pass_two = [c for c in all_concepts+filter_only if c.address not in attempted_addresses]
280
+ # pass_two = [c for c in all_concepts if c.address not in attempted_addresses]
281
+
275
282
  for remaining_concept in (pass_one,):
276
283
  priority = (
277
284
  # then multiselects to remove them from scope
@@ -333,5 +340,5 @@ def get_priority_concept(
333
340
  if final:
334
341
  return final[0]
335
342
  raise ValueError(
336
- f"Cannot resolve query. No remaining priority concepts, have attempted {attempted_addresses}"
343
+ f"Cannot resolve query. No remaining priority concepts, have attempted {attempted_addresses} out of {all_concepts} with found {found_concepts}"
337
344
  )
@@ -96,6 +96,8 @@ def build_parent_concepts(
96
96
  continue
97
97
  elif global_filter_is_local_filter:
98
98
  same_filter_optional.append(x)
99
+ # also append it to the parent row concepts
100
+ parent_row_concepts.append(x)
99
101
 
100
102
  # sometimes, it's okay to include other local optional above the filter
101
103
  # in case it is, prep our list
@@ -204,11 +206,16 @@ def gen_filter_node(
204
206
  f"{padding(depth)}{LOGGER_PREFIX} filter node row parents {[x.address for x in parent_row_concepts]} could not be found"
205
207
  )
206
208
  return None
209
+ else:
210
+ logger.info(
211
+ f"{padding(depth)}{LOGGER_PREFIX} filter node has row parents {[x.address for x in parent_row_concepts]} from node with output [{[x.address for x in row_parent.output_concepts]}] partial {row_parent.partial_concepts}"
212
+ )
207
213
  if global_filter_is_local_filter:
208
214
  logger.info(
209
215
  f"{padding(depth)}{LOGGER_PREFIX} filter node conditions match global conditions adding row parent {row_parent.output_concepts} with condition {where.conditional}"
210
216
  )
211
217
  row_parent.add_parents(core_parent_nodes)
218
+ # all local optional will be in the parent already, so we can set outputs
212
219
  row_parent.set_output_concepts([concept] + local_optional)
213
220
  return row_parent
214
221
  if optimized_pushdown:
@@ -9,6 +9,7 @@ from trilogy.core.models.build import (
9
9
  from trilogy.core.models.build_environment import BuildEnvironment
10
10
  from trilogy.core.processing.nodes import (
11
11
  History,
12
+ MergeNode,
12
13
  StrategyNode,
13
14
  UnnestNode,
14
15
  WhereSafetyNode,
@@ -18,6 +19,32 @@ from trilogy.core.processing.utility import padding
18
19
  LOGGER_PREFIX = "[GEN_UNNEST_NODE]"
19
20
 
20
21
 
22
+ def get_pseudonym_parents(
23
+ concept: BuildConcept,
24
+ local_optional: List[BuildConcept],
25
+ source_concepts,
26
+ environment: BuildEnvironment,
27
+ g,
28
+ depth,
29
+ history,
30
+ conditions,
31
+ ) -> List[StrategyNode]:
32
+ for x in concept.pseudonyms:
33
+ attempt = source_concepts(
34
+ mandatory_list=[environment.alias_origin_lookup[x]] + local_optional,
35
+ environment=environment,
36
+ g=g,
37
+ depth=depth + 1,
38
+ history=history,
39
+ conditions=conditions,
40
+ accept_partial=True,
41
+ )
42
+ if not attempt:
43
+ continue
44
+ return [attempt]
45
+ return []
46
+
47
+
21
48
  def gen_unnest_node(
22
49
  concept: BuildConcept,
23
50
  local_optional: List[BuildConcept],
@@ -29,14 +56,34 @@ def gen_unnest_node(
29
56
  conditions: BuildWhereClause | None = None,
30
57
  ) -> StrategyNode | None:
31
58
  arguments = []
59
+ join_nodes: list[StrategyNode] = []
32
60
  depth_prefix = "\t" * depth
33
61
  if isinstance(concept.lineage, BuildFunction):
34
62
  arguments = concept.lineage.concept_arguments
63
+ search_optional = local_optional
64
+ if (not arguments) and (local_optional and concept.pseudonyms):
65
+ logger.info(
66
+ f"{padding(depth)}{LOGGER_PREFIX} unnest node for {concept} has no parents; creating solo unnest node"
67
+ )
68
+ join_nodes += get_pseudonym_parents(
69
+ concept,
70
+ local_optional,
71
+ source_concepts,
72
+ environment,
73
+ g,
74
+ depth,
75
+ history,
76
+ conditions,
77
+ )
78
+ logger.info(
79
+ f"{padding(depth)}{LOGGER_PREFIX} unnest node for {concept} got join nodes {join_nodes}"
80
+ )
81
+ search_optional = []
35
82
 
36
- equivalent_optional = [x for x in local_optional if x.lineage == concept.lineage]
83
+ equivalent_optional = [x for x in search_optional if x.lineage == concept.lineage]
37
84
 
38
85
  non_equivalent_optional = [
39
- x for x in local_optional if x not in equivalent_optional
86
+ x for x in search_optional if x not in equivalent_optional
40
87
  ]
41
88
  all_parents = arguments + non_equivalent_optional
42
89
  logger.info(
@@ -44,7 +91,8 @@ def gen_unnest_node(
44
91
  )
45
92
  local_conditions = False
46
93
  expected_outputs = [concept] + local_optional
47
- if arguments or local_optional:
94
+ parent: StrategyNode | None = None
95
+ if arguments or search_optional:
48
96
  parent = source_concepts(
49
97
  mandatory_list=all_parents,
50
98
  environment=environment,
@@ -86,14 +134,37 @@ def gen_unnest_node(
86
134
  base = UnnestNode(
87
135
  unnest_concepts=[concept] + equivalent_optional,
88
136
  input_concepts=arguments + non_equivalent_optional,
89
- output_concepts=[concept] + local_optional,
137
+ output_concepts=[concept] + search_optional,
90
138
  environment=environment,
91
139
  parents=([parent] if parent else []),
92
140
  )
141
+
142
+ conditional = conditions.conditional if conditions else None
143
+ if join_nodes:
144
+ logger.info(
145
+ f"{depth_prefix}{LOGGER_PREFIX} unnest node for {concept} needs to merge with join nodes {join_nodes}"
146
+ )
147
+ for x in join_nodes:
148
+ logger.info(
149
+ f"{depth_prefix}{LOGGER_PREFIX} join node {x} with partial {x.partial_concepts}"
150
+ )
151
+ pseudonyms = [
152
+ environment.alias_origin_lookup[p] for p in concept.pseudonyms
153
+ ]
154
+ x.add_partial_concepts(pseudonyms)
155
+ return MergeNode(
156
+ input_concepts=base.output_concepts
157
+ + [j for n in join_nodes for j in n.output_concepts],
158
+ output_concepts=[concept] + local_optional,
159
+ environment=environment,
160
+ parents=[base] + join_nodes,
161
+ conditions=conditional if local_conditions is True else None,
162
+ preexisting_conditions=(
163
+ conditional if conditional and local_conditions is False else None
164
+ ),
165
+ )
93
166
  # we need to sometimes nest an unnest node,
94
167
  # as unnest operations are not valid in all situations
95
- # TODO: inline this node when we can detect it's safe
96
- conditional = conditions.conditional if conditions else None
97
168
  new = WhereSafetyNode(
98
169
  input_concepts=base.output_concepts,
99
170
  output_concepts=base.output_concepts,
@@ -197,7 +197,7 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
197
197
  for x in self.where_clause.concept_arguments:
198
198
  if isinstance(x, UndefinedConcept):
199
199
  validate = environment.concepts.get(x.address)
200
- if validate:
200
+ if validate and self.where_clause:
201
201
  self.where_clause = (
202
202
  self.where_clause.with_reference_replacement(
203
203
  x.address, validate.reference
trilogy/dialect/base.py CHANGED
@@ -194,6 +194,13 @@ FUNCTION_MAP = {
194
194
  FunctionType.INDEX_ACCESS: lambda x: f"{x[0]}[{x[1]}]",
195
195
  FunctionType.MAP_ACCESS: lambda x: f"{x[0]}[{x[1]}]",
196
196
  FunctionType.UNNEST: lambda x: f"unnest({x[0]})",
197
+ FunctionType.DATE_SPINE: lambda x: f"""unnest(
198
+ generate_series(
199
+ {x[0]},
200
+ {x[1]},
201
+ INTERVAL '1 day'
202
+ )
203
+ )""",
197
204
  FunctionType.RECURSE_EDGE: lambda x: f"CASE WHEN {x[1]} IS NULL THEN {x[0]} ELSE {x[1]} END",
198
205
  FunctionType.ATTR_ACCESS: lambda x: f"""{x[0]}.{x[1].replace("'", "")}""",
199
206
  FunctionType.STRUCT: lambda x: f"{{{', '.join(struct_arg(x))}}}",
@@ -213,6 +220,9 @@ FUNCTION_MAP = {
213
220
  FunctionType.ARRAY_TO_STRING: lambda args: (
214
221
  f"array_to_string({args[0]}, {args[1]})"
215
222
  ),
223
+ FunctionType.ARRAY_FILTER: lambda args: (
224
+ f"array_filter({args[0]}, {args[1]} -> {args[2]})"
225
+ ),
216
226
  # math
217
227
  FunctionType.ADD: lambda x: " + ".join(x),
218
228
  FunctionType.ABS: lambda x: f"abs({x[0]})",
@@ -237,6 +247,7 @@ FUNCTION_MAP = {
237
247
  FunctionType.AVG: lambda x: f"avg({x[0]})",
238
248
  FunctionType.MAX: lambda x: f"max({x[0]})",
239
249
  FunctionType.MIN: lambda x: f"min({x[0]})",
250
+ FunctionType.ANY: lambda x: f"any_value({x[0]})",
240
251
  # string types
241
252
  FunctionType.LIKE: lambda x: f" {x[0]} like {x[1]} ",
242
253
  FunctionType.UPPER: lambda x: f"UPPER({x[0]}) ",
@@ -285,6 +296,7 @@ FUNCTION_GRAIN_MATCH_MAP = {
285
296
  FunctionType.AVG: lambda args: f"{args[0]}",
286
297
  FunctionType.MAX: lambda args: f"{args[0]}",
287
298
  FunctionType.MIN: lambda args: f"{args[0]}",
299
+ FunctionType.ANY: lambda args: f"{args[0]}",
288
300
  }
289
301
 
290
302
 
trilogy/parsing/common.py CHANGED
@@ -91,7 +91,7 @@ def process_function_arg(
91
91
  # to simplify anonymous function handling
92
92
  if (
93
93
  arg.operator not in FunctionClass.AGGREGATE_FUNCTIONS.value
94
- and arg.operator != FunctionType.UNNEST
94
+ and arg.operator not in FunctionClass.ONE_TO_MANY.value
95
95
  ):
96
96
  return arg
97
97
  id_hash = string_to_hash(str(arg))
@@ -311,13 +311,18 @@ def concept_is_relevant(
311
311
  if concept.purpose in (Purpose.METRIC,):
312
312
  if all([c in others for c in concept.grain.components]):
313
313
  return False
314
+ if (
315
+ concept.derivation in (Derivation.BASIC,)
316
+ and isinstance(concept.lineage, Function)
317
+ and concept.lineage.operator == FunctionType.DATE_SPINE
318
+ ):
319
+ return True
314
320
  if concept.derivation in (Derivation.BASIC,) and isinstance(
315
321
  concept.lineage, (Function, CaseWhen)
316
322
  ):
317
323
  relevant = False
318
324
  for arg in concept.lineage.arguments:
319
325
  relevant = atom_is_relevant(arg, others, environment) or relevant
320
-
321
326
  return relevant
322
327
  if concept.derivation in (Derivation.BASIC,) and isinstance(
323
328
  concept.lineage, Parenthetical
@@ -529,7 +534,7 @@ def function_to_concept(
529
534
  elif parent.operator == FunctionType.UNION:
530
535
  derivation = Derivation.UNION
531
536
  granularity = Granularity.MULTI_ROW
532
- elif parent.operator == FunctionType.UNNEST:
537
+ elif parent.operator in FunctionClass.ONE_TO_MANY.value:
533
538
  derivation = Derivation.UNNEST
534
539
  granularity = Granularity.MULTI_ROW
535
540
  elif parent.operator == FunctionType.RECURSE_EDGE:
@@ -1819,6 +1819,10 @@ class ParseToObjects(Transformer):
1819
1819
  def array_agg(self, meta, args):
1820
1820
  return self.function_factory.create_function(args, FunctionType.ARRAY_AGG, meta)
1821
1821
 
1822
+ @v_args(meta=True)
1823
+ def any(self, meta, args):
1824
+ return self.function_factory.create_function(args, FunctionType.ANY, meta)
1825
+
1822
1826
  @v_args(meta=True)
1823
1827
  def avg(self, meta, args):
1824
1828
  return self.function_factory.create_function(args, FunctionType.AVG, meta)
@@ -2022,6 +2026,12 @@ class ParseToObjects(Transformer):
2022
2026
  )
2023
2027
  return self.function_factory.create_function(args, FunctionType.CAST, meta)
2024
2028
 
2029
+ @v_args(meta=True)
2030
+ def fdate_spine(self, meta, args) -> Function:
2031
+ return self.function_factory.create_function(
2032
+ args, FunctionType.DATE_SPINE, meta
2033
+ )
2034
+
2025
2035
  # utility functions
2026
2036
  @v_args(meta=True)
2027
2037
  def fcast(self, meta, args) -> Function:
@@ -2191,6 +2201,33 @@ class ParseToObjects(Transformer):
2191
2201
  meta,
2192
2202
  )
2193
2203
 
2204
+ @v_args(meta=True)
2205
+ def farray_filter(self, meta, args) -> Function:
2206
+ factory: CustomFunctionFactory = args[1]
2207
+ if not len(factory.function_arguments) == 1:
2208
+ raise InvalidSyntaxException(
2209
+ "Array filter function must have exactly one argument;"
2210
+ )
2211
+ array_type = arg_to_datatype(args[0])
2212
+ if not isinstance(array_type, ArrayType):
2213
+ raise InvalidSyntaxException(
2214
+ f"Array filter function must be applied to an array, not {array_type}"
2215
+ )
2216
+ return self.function_factory.create_function(
2217
+ [
2218
+ args[0],
2219
+ factory.function_arguments[0],
2220
+ factory(
2221
+ ArgBinding(
2222
+ name=factory.function_arguments[0].name,
2223
+ datatype=array_type.value_data_type,
2224
+ )
2225
+ ),
2226
+ ],
2227
+ FunctionType.ARRAY_FILTER,
2228
+ meta,
2229
+ )
2230
+
2194
2231
 
2195
2232
  def unpack_visit_error(e: VisitError, text: str | None = None):
2196
2233
  """This is required to get exceptions from imports, which would
@@ -2227,6 +2264,7 @@ ERROR_CODES: dict[int, str] = {
2227
2264
  101: "Using FROM keyword? Trilogy does not have a FROM clause (Datasource resolution is automatic).",
2228
2265
  # 200 codes relate to required explicit syntax (we could loosen these?)
2229
2266
  201: 'Missing alias? Alias must be specified with "AS" - e.g. `SELECT x+1 AS y`',
2267
+ 202: "Missing closing semicolon? Statements must be terminated with a semicolon `;`.",
2230
2268
  210: "Missing order direction? Order by must be explicit about direction - specify `asc` or `desc`.",
2231
2269
  }
2232
2270
 
@@ -2307,7 +2345,7 @@ def parse_text(
2307
2345
  )
2308
2346
 
2309
2347
  def _handle_unexpected_token(e: UnexpectedToken, text: str) -> None:
2310
- """Handle UnexpectedToken errors with specific logic."""
2348
+ """Handle UnexpectedToken errors to make friendlier error messages."""
2311
2349
  # Handle ordering direction error
2312
2350
  pos = e.pos_in_stream or 0
2313
2351
  if e.expected == {"ORDERING_DIRECTION"}:
@@ -2319,12 +2357,27 @@ def parse_text(
2319
2357
  )
2320
2358
  if parsed_tokens == ["FROM"]:
2321
2359
  raise _create_syntax_error(101, pos, text)
2322
-
2323
- # Attempt recovery for aliasing
2360
+ # check if they are missing a semicolon
2361
+ try:
2362
+ e.interactive_parser.feed_token(Token("_TERMINATOR", ";"))
2363
+ state = e.interactive_parser.lexer_thread.state
2364
+ if state and state.last_token:
2365
+ new_pos = state.last_token.end_pos or pos
2366
+ else:
2367
+ new_pos = pos
2368
+ raise _create_syntax_error(202, new_pos, text)
2369
+ except UnexpectedToken:
2370
+ pass
2371
+ # check if they forgot an as
2324
2372
  try:
2325
2373
  e.interactive_parser.feed_token(Token("AS", "AS"))
2374
+ state = e.interactive_parser.lexer_thread.state
2375
+ if state and state.last_token:
2376
+ new_pos = state.last_token.end_pos or pos
2377
+ else:
2378
+ new_pos = pos
2326
2379
  e.interactive_parser.feed_token(Token("IDENTIFIER", e.token.value))
2327
- raise _create_syntax_error(201, pos, text)
2380
+ raise _create_syntax_error(201, new_pos, text)
2328
2381
  except UnexpectedToken:
2329
2382
  pass
2330
2383
 
trilogy/parsing/render.py CHANGED
@@ -662,7 +662,8 @@ class Renderer:
662
662
  pair_strings.append(self.indent_lines(pair_line))
663
663
  inputs = ",\n".join(pair_strings)
664
664
  return f"struct(\n{inputs}\n{self.indent_context.current_indent})"
665
-
665
+ if arg.operator == FunctionType.ALIAS:
666
+ return f"{self.to_string(arg.arguments[0])}"
666
667
  inputs = ",".join(args)
667
668
  return f"{arg.operator.value}({inputs})"
668
669
 
@@ -113,7 +113,7 @@
113
113
  filter_item: _filter_base | _filter_alt
114
114
 
115
115
  // rank/lag/lead
116
- WINDOW_TYPE: ("row_number"i|"rank"i|"lag"i|"lead"i | "sum"i | "avg"i | "max"i | "min"i ) /[\s]+/
116
+ WINDOW_TYPE: ("row_number"i|"rank"i|"lag"i|"lead"i | "sum"i | "avg"i | "max"i | "min"i | "count"i ) /[\s]+/
117
117
 
118
118
  window_item_over: ("OVER"i over_list)
119
119
 
@@ -323,8 +323,10 @@
323
323
  _ARRAY_TRANSFORM.1: "array_transform("i
324
324
  transform_lambda: "@" IDENTIFIER
325
325
  farray_transform: _ARRAY_TRANSFORM expr "," transform_lambda ")"
326
+ _ARRAY_FILTER.1: "array_filter("i
327
+ farray_filter: _ARRAY_FILTER expr "," transform_lambda ")"
326
328
 
327
- _array_functions: farray_sum | farray_distinct | farray_sort | farray_transform | farray_to_string
329
+ _array_functions: farray_sum | farray_distinct | farray_sort | farray_transform | farray_to_string | farray_filter
328
330
 
329
331
  //map_functions
330
332
  _MAP_KEYS.1: "map_keys("i
@@ -358,11 +360,13 @@
358
360
  min: _MIN expr ")"
359
361
  _ARRAY_AGG.1: "array_agg("i
360
362
  array_agg: _ARRAY_AGG expr ")"
363
+ _ANY.1: "any("i
364
+ any: _ANY expr ")"
361
365
 
362
366
  //aggregates can force a grain
363
367
  aggregate_all: "*"
364
368
  aggregate_over: ("BY"i (aggregate_all | over_list))
365
- aggregate_functions: (count | count_distinct | sum | avg | max | min | array_agg) aggregate_over?
369
+ aggregate_functions: (count | count_distinct | sum | avg | max | min | array_agg | any) aggregate_over?
366
370
 
367
371
  // date functions
368
372
  _DATE.1: "date("i
@@ -405,8 +409,10 @@
405
409
  fdate_sub: _DATE_SUB expr "," DATE_PART "," expr ")"
406
410
  _DATE_DIFF.1: "date_diff("i
407
411
  fdate_diff: _DATE_DIFF expr "," expr "," DATE_PART ")"
412
+ _DATE_SPINE.1: "date_spine("i
413
+ fdate_spine: _DATE_SPINE expr "," expr ")"
408
414
 
409
- _date_functions: fdate | fdate_add | fdate_sub | fdate_diff | fdatetime | ftimestamp | fsecond | fminute | fhour | fday |fday_name | fday_of_week | fweek | fmonth | fmonth_name | fquarter | fyear | fdate_part | fdate_trunc
415
+ _date_functions: fdate | fdate_add | fdate_sub | fdate_diff | fdatetime | ftimestamp | fsecond | fminute | fhour | fday |fday_name | fday_of_week | fweek | fmonth | fmonth_name | fquarter | fyear | fdate_part | fdate_trunc | fdate_spine
410
416
 
411
417
  _static_functions: _string_functions | _math_functions | _array_functions | _map_functions
412
418
 
@@ -434,7 +440,7 @@
434
440
 
435
441
  float_lit: /\-?[0-9]*\.[0-9]+/
436
442
 
437
- array_lit: "[" (literal ",")* literal ","? "]"()
443
+ array_lit: "[" (expr ",")* expr ","? "]"()
438
444
 
439
445
  tuple_lit: "(" (literal ",")* literal ","? ")"
440
446