pytrilogy 0.0.2.3__py3-none-any.whl → 0.0.2.4__py3-none-any.whl

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

Potentially problematic release.


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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.3
3
+ Version: 0.0.2.4
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,4 +1,4 @@
1
- trilogy/__init__.py,sha256=P6eUqLnB3qFOCWULAL337Zc9648DuD0E0pGbTqzHChw,290
1
+ trilogy/__init__.py,sha256=sspl5Npt0npzVRC6wCwiYXa0nK0E7a_biwiL2T4kbjM,290
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  trilogy/constants.py,sha256=KIvi-cgU4R9urNgDdGiCsRkCrzjAfM4xGHhZb1SVy2w,881
4
4
  trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
@@ -8,30 +8,30 @@ trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  trilogy/utility.py,sha256=zM__8r29EsyDW7K9VOHz8yvZC2bXFzh7xKy3cL7GKsk,707
9
9
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  trilogy/core/constants.py,sha256=LL8NLvxb3HRnAjvofyLRXqQJijLcYiXAQYQzGarVD-g,128
11
- trilogy/core/enums.py,sha256=zf5VvxkXB9wz-cIMwGB4d00KXOTYfBhkbIp6Ff45YVs,5880
11
+ trilogy/core/enums.py,sha256=ozgw8G0s4CdbL6IUsX5nNT_ZGd4mMkoo8_EGLZXeQYY,5902
12
12
  trilogy/core/env_processor.py,sha256=l7TAB0LalxjTYJdTlcmFIkLXuyxa9lrenWLeZfa9qw0,2276
13
13
  trilogy/core/environment_helpers.py,sha256=mzBDHhdF9ssZ_-LY8CcaM_ddfJavkpRYrFImUd3cjXI,5972
14
14
  trilogy/core/ergonomics.py,sha256=w3gwXdgrxNHCuaRdyKg73t6F36tj-wIjQf47WZkHmJk,1465
15
15
  trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,561
16
- trilogy/core/functions.py,sha256=MSNe-OLAEvj72qn5mif3ce9ncRh3ZATpX3V300yAm98,10125
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=eZKTOD4mXGCH78YE0xNlHex15t1Ur4d6CGAZ1QNcBCQ,136597
19
+ trilogy/core/models.py,sha256=KVPUfupZVhQxJZCViMP7PELzvNR7WOwkjmqqgEM4oeM,137120
20
20
  trilogy/core/optimization.py,sha256=A8S9C9H5RcQcFSQLYtEEBnm-r1CW_e9GEWlLK7q3MqA,4930
21
- trilogy/core/query_processor.py,sha256=BrP4x96Nbzfq6MfxYzxnOL6zYCLIoHLEii65MBT-Daw,17731
21
+ trilogy/core/query_processor.py,sha256=6JiX6YqsTgnsEkIxcmWFxJuM6kRJbXbPGRCQgi8g-B8,17941
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
25
  trilogy/core/optimizations/inline_datasource.py,sha256=KiwZ4fnRnSOVK8zzdwMAkafszo3fNn5LY8xgb0X1CbI,3194
26
- trilogy/core/optimizations/predicate_pushdown.py,sha256=HWHHG0hejBoIv985hVlsh3INPTrx8EGwCRWeeiPEHsc,7374
26
+ trilogy/core/optimizations/predicate_pushdown.py,sha256=iVZV_BVaL3I6wlELPOuJAiuF0heOM7bCUnNqyCxxRVw,6370
27
27
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- trilogy/core/processing/concept_strategies_v3.py,sha256=Ssekd9sA7W-JgAif-Bp7ID1pZujaKJzlcP_KuoH2h4I,23938
28
+ trilogy/core/processing/concept_strategies_v3.py,sha256=xCPfg1ySHP6C-u777kqnV8eqIK1X68hqwvRcw61q5X0,23968
29
29
  trilogy/core/processing/graph_utils.py,sha256=aq-kqk4Iado2HywDxWEejWc-7PGO6Oa-ZQLAM6XWPHw,1199
30
- trilogy/core/processing/utility.py,sha256=L92nMZQqVSb_aOtYGi2rkvDD10e8WWg5-oQ7ApJBlI4,12374
30
+ trilogy/core/processing/utility.py,sha256=rhnkn3KgB1MW17mZYSEZ8XFEbC__B7PPmZgpDql4Myg,13614
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=liZDth7mvhkF_sUFXK7JitJsiaKD132w3ySLbF7l-nE,8956
34
- trilogy/core/processing/node_generators/filter_node.py,sha256=5B7UCK84A9lGgJ7EjCiC7YGWWM1xPyQGHkBwZgYypFM,4585
33
+ trilogy/core/processing/node_generators/common.py,sha256=uLrqbEsuYREswiTfuTQuxldO9SsS6ylZEQT_q9iB2uY,8948
34
+ trilogy/core/processing/node_generators/filter_node.py,sha256=kVsK9O9QWUgknui3ESHJJCnFY9dVMCHlGpHXn1QtXkU,4824
35
35
  trilogy/core/processing/node_generators/group_node.py,sha256=xWI1xNIXEOj6jlRGD9hcv2_vVNvY6lpzJl6pQ8HuFBE,2988
36
36
  trilogy/core/processing/node_generators/group_to_node.py,sha256=BzPdYwzoo8gRMH7BDffTTXq4z-mjfCEzvfB5I-P0_nw,2941
37
37
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=vP84dnLQy6dtypi6mUbt9sMAcmmrTgQ1Oz4GI6X1IEo,6421
@@ -43,40 +43,40 @@ trilogy/core/processing/node_generators/window_node.py,sha256=9nXUXUgQrNczU1gaOq
43
43
  trilogy/core/processing/nodes/__init__.py,sha256=-8ONG7Nqa6sNLpst5wNNyMigyaHRfUKGdE3xce8E3mM,3998
44
44
  trilogy/core/processing/nodes/base_node.py,sha256=q1G4FO3Q4OuboWL8EaJgJ4jcnnqM9QsothMIcF2ot1g,11161
45
45
  trilogy/core/processing/nodes/filter_node.py,sha256=ImUHYj5G5eHbOtki-ZMR31Op38wKOjR1xu_ETiE_D6I,1932
46
- trilogy/core/processing/nodes/group_node.py,sha256=5dqGlonXG5Ou7HRSsTStbM1KyxeDSZROz9qKTjOUMEc,5166
46
+ trilogy/core/processing/nodes/group_node.py,sha256=j3vtFQIn_t4EfCke8dGULlwVkG2KKPYfgGwo2rvgcww,6322
47
47
  trilogy/core/processing/nodes/merge_node.py,sha256=W7ryL9rceylCQGNgcuGY-dSiNoIz8qrbpJXKzg_Niwo,14056
48
48
  trilogy/core/processing/nodes/select_node_v2.py,sha256=COu-WPuyabGAc3HTkJB-_7eNzR4OgxIL4YDuZIRrST0,7179
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=bjoDwVAARpCVLjJvnfNpr1JtvoamkAAYZxw-G5QRV_A,27845
53
- trilogy/dialect/bigquery.py,sha256=BAN2o0SOuLvzLYLV7Sc11woapS5L7Cc36aMBeaphW5k,2905
52
+ trilogy/dialect/base.py,sha256=aE8iJJhkGtZE04mYm_wQx3ebQKYbcRsKISUd9mbrZ2A,28782
53
+ trilogy/dialect/bigquery.py,sha256=15KJ-cOpBlk9O7FPviPgmg8xIydJeKx7WfmL3SSsPE8,2953
54
54
  trilogy/dialect/common.py,sha256=5jdOHWIj3Xv8F8y5mnyWHLjxD_we2fncM-ZnNetJP7U,2781
55
55
  trilogy/dialect/config.py,sha256=tLVEMctaTDhUgARKXUNfHUcIolGaALkQ0RavUvXAY4w,2994
56
- trilogy/dialect/duckdb.py,sha256=JQPnR5F39iDQXfTccKhbq7xWYGzYc0dPpn5vo0Qd_Vk,3076
56
+ trilogy/dialect/duckdb.py,sha256=JEdhctDxZr-FEodot5V4De6JnrQDJT02zbjX48j6ay8,3125
57
57
  trilogy/dialect/enums.py,sha256=4NdpsydBpDn6jnh0JzFz5VvQEtnShErWtWHVyT6TNpw,3948
58
- trilogy/dialect/postgres.py,sha256=r47xbCA7nfEYENofiVfLZ-SnReNfDmUmW4OSHVkkP4E,3206
59
- trilogy/dialect/presto.py,sha256=evb7Tq77l4iBrZnBeSPfEdmdObgrca-p5H72CIrEips,3221
60
- trilogy/dialect/snowflake.py,sha256=N3HknYgN-fjD7BLX1Ucj-ss_ku2Ox8DgLsF3BIHutHo,2941
61
- trilogy/dialect/sql_server.py,sha256=HX68vNTrcDaTnOxe6Zbx_PBgrO42e2VuThxO6CYQ2cY,3026
58
+ trilogy/dialect/postgres.py,sha256=ev1RJZsC8BB3vJSxJ4q-TTYqZ4Hk1NXUtuRkLrQEBX0,3254
59
+ trilogy/dialect/presto.py,sha256=InvHDXsGLDG3E3ffl7hGZ-9y3vH8RO6UhNE0OSra01g,3269
60
+ trilogy/dialect/snowflake.py,sha256=_Bf4XO7-nImMv9XCSsTfVM3g2f_KHdO17VTa9J-HgSM,2989
61
+ trilogy/dialect/sql_server.py,sha256=OtXbm1v6NIGyXeC5i18ojUvLeqescc_Pbv3EASUBB94,3074
62
62
  trilogy/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
63
  trilogy/hooks/base_hook.py,sha256=Xkb-A2qCHozYjum0A36zOy5PwTVwrP3NLDF0U2GpgHo,1100
64
64
  trilogy/hooks/graph_hook.py,sha256=onHvMQPwj_KOS3HOTpRFiy7QLLKAiycq2MzJ_Q0Oh5Y,2467
65
65
  trilogy/hooks/query_debugger.py,sha256=NDChfkPmmW-KINa4TaQmDe_adGiwsKFdGLDSYpbodeU,4282
66
66
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
67
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
- trilogy/parsing/common.py,sha256=UZdG51pHNMGokvHjUxv7Zt5Xrh2snxfZuVOZVyg9yZ8,6091
68
+ trilogy/parsing/common.py,sha256=aYmLtsEk8VJj5yYASzUcTf0i0lXVsZBbZJwphwokxK4,6092
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=-dXf-W6cOj-l2hDvgb7TWJKLZZEHAj0kwaw-DT0YKNU,58336
72
+ trilogy/parsing/parse_engine.py,sha256=_Cpq7bq6QyuDcnmYX_fDLGNFNSQPj173cqYsh3V6jH8,60969
73
73
  trilogy/parsing/render.py,sha256=Gy_6wVYPwYLf35Iota08sbqveuWILtUhI8MYStcvtJM,12174
74
- trilogy/parsing/trilogy.lark,sha256=DiJe_XQZ9lXVUW5AF9eZl531lPAw7mm9MMeHCGkw7kc,11148
74
+ trilogy/parsing/trilogy.lark,sha256=uQlyb4RQ6Tpp8lRlXLK_Qb8sD1qzYVYErRI97v22Zak,11317
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.3.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
78
- pytrilogy-0.0.2.3.dist-info/METADATA,sha256=9Q2murSlwgsOWkr6A3FkEWWtlPKoI-VTmkONLbcnNPU,7906
79
- pytrilogy-0.0.2.3.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
80
- pytrilogy-0.0.2.3.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
81
- pytrilogy-0.0.2.3.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
82
- pytrilogy-0.0.2.3.dist-info/RECORD,,
77
+ pytrilogy-0.0.2.4.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
78
+ pytrilogy-0.0.2.4.dist-info/METADATA,sha256=R3D4ULv2Y2yJRO0sToJ5FlPPiqkU8H02X035M-vaG9s,7906
79
+ pytrilogy-0.0.2.4.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
80
+ pytrilogy-0.0.2.4.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
81
+ pytrilogy-0.0.2.4.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
82
+ pytrilogy-0.0.2.4.dist-info/RECORD,,
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.3"
7
+ __version__ = "0.0.2.4"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/core/enums.py CHANGED
@@ -121,6 +121,7 @@ class FunctionType(Enum):
121
121
  INDEX_ACCESS = "index_access"
122
122
  MAP_ACCESS = "map_access"
123
123
  ATTR_ACCESS = "attr_access"
124
+ STRUCT = "struct"
124
125
 
125
126
  # TEXT AND MAYBE MORE
126
127
  SPLIT = "split"
trilogy/core/functions.py CHANGED
@@ -251,11 +251,19 @@ def MapAccess(args: list[Concept]):
251
251
  )
252
252
 
253
253
 
254
+ def get_attr_datatype(
255
+ arg: Concept, lookup
256
+ ) -> DataType | ListType | StructType | MapType | NumericType:
257
+ if isinstance(arg.datatype, StructType):
258
+ return arg_to_datatype(arg.datatype.fields_map[lookup])
259
+ return arg.datatype
260
+
261
+
254
262
  def AttrAccess(args: list[Concept]):
255
263
  return Function(
256
264
  operator=FunctionType.ATTR_ACCESS,
257
265
  arguments=args,
258
- output_datatype=args[0].field_map[args[1]].datatype, # type: ignore
266
+ output_datatype=get_attr_datatype(args[0], args[1]), # type: ignore
259
267
  output_purpose=Purpose.PROPERTY,
260
268
  valid_inputs=[
261
269
  {DataType.STRUCT},
trilogy/core/models.py CHANGED
@@ -296,7 +296,7 @@ class MapType(BaseModel):
296
296
 
297
297
  class StructType(BaseModel):
298
298
  fields: List[ALL_TYPES]
299
- fields_map: Dict[str, Concept] = Field(default_factory=dict)
299
+ fields_map: Dict[str, Concept | int | float | str] = Field(default_factory=dict)
300
300
 
301
301
  @property
302
302
  def data_type(self):
@@ -419,6 +419,23 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
419
419
  def __hash__(self):
420
420
  return hash(str(self))
421
421
 
422
+ @property
423
+ def is_aggregate(self):
424
+ if (
425
+ self.lineage
426
+ and isinstance(self.lineage, Function)
427
+ and self.lineage.operator in FunctionClass.AGGREGATE_FUNCTIONS.value
428
+ ):
429
+ return True
430
+ if (
431
+ self.lineage
432
+ and isinstance(self.lineage, AggregateWrapper)
433
+ and self.lineage.function.operator
434
+ in FunctionClass.AGGREGATE_FUNCTIONS.value
435
+ ):
436
+ return True
437
+ return False
438
+
422
439
  def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
423
440
  if self.address == source.address:
424
441
  new = target.with_grain(self.grain.with_merge(source, target, modifiers))
@@ -6,16 +6,9 @@ from trilogy.core.models import (
6
6
  SubselectComparison,
7
7
  Comparison,
8
8
  Parenthetical,
9
- Function,
10
- FilterItem,
11
- MagicConstants,
12
- Concept,
13
- WindowItem,
14
- AggregateWrapper,
15
- DataType,
16
9
  )
17
10
  from trilogy.core.optimizations.base_optimization import OptimizationRule
18
- from trilogy.core.enums import FunctionClass
11
+ from trilogy.core.processing.utility import is_scalar_condition
19
12
 
20
13
 
21
14
  def decompose_condition(
@@ -56,40 +49,6 @@ def is_child_of(a, comparison):
56
49
  return base
57
50
 
58
51
 
59
- def is_basic(
60
- element: (
61
- int
62
- | str
63
- | float
64
- | list
65
- | WindowItem
66
- | FilterItem
67
- | Concept
68
- | Comparison
69
- | Conditional
70
- | Parenthetical
71
- | Function
72
- | AggregateWrapper
73
- | MagicConstants
74
- | DataType
75
- ),
76
- ) -> bool:
77
- if isinstance(element, Parenthetical):
78
- return is_basic(element.content)
79
- elif isinstance(element, SubselectComparison):
80
- return True
81
- elif isinstance(element, Comparison):
82
- return is_basic(element.left) and is_basic(element.right)
83
- elif isinstance(element, Function):
84
- if element.operator in FunctionClass.AGGREGATE_FUNCTIONS.value:
85
- return False
86
- elif isinstance(element, AggregateWrapper):
87
- return is_basic(element.function)
88
- elif isinstance(element, Conditional):
89
- return is_basic(element.left) and is_basic(element.right)
90
- return True
91
-
92
-
93
52
  class PredicatePushdown(OptimizationRule):
94
53
 
95
54
  def __init__(self, *args, **kwargs) -> None:
@@ -187,13 +146,13 @@ class PredicatePushdown(OptimizationRule):
187
146
  )
188
147
  optimized = False
189
148
  for candidate in candidates:
190
- if not is_basic(candidate):
149
+ if not is_scalar_condition(candidate):
191
150
  self.debug(
192
151
  f"Skipping {candidate} as not a basic [no aggregate, etc] condition"
193
152
  )
194
153
  continue
195
154
  self.log(
196
- f"Checking candidate {candidate}, {type(candidate)}, {is_basic(candidate)}"
155
+ f"Checking candidate {candidate}, {type(candidate)}, scalar: {is_scalar_condition(candidate)}"
197
156
  )
198
157
  for parent_cte in cte.parent_ctes:
199
158
  local_pushdown = self._check_parent(
@@ -489,6 +489,7 @@ def _search_concepts(
489
489
 
490
490
  found: set[str] = set()
491
491
  skip: set[str] = set()
492
+ virtual: set[str] = set()
492
493
  stack: List[StrategyNode] = []
493
494
  complete = ValidationResult.INCOMPLETE
494
495
 
@@ -47,7 +47,7 @@ def resolve_function_parent_concepts(concept: Concept) -> List[Concept]:
47
47
 
48
48
  def resolve_filter_parent_concepts(
49
49
  concept: Concept,
50
- ) -> Tuple[Concept, List[Concept], List[Concept]]:
50
+ ) -> Tuple[Concept, List[Concept], List[Tuple[Concept, ...]]]:
51
51
  if not isinstance(concept.lineage, FilterItem):
52
52
  raise ValueError(
53
53
  f"Concept {concept} lineage is not filter item, is {type(concept.lineage)}"
@@ -58,7 +58,7 @@ def resolve_filter_parent_concepts(
58
58
  base_rows += concept.lineage.where.row_arguments
59
59
  # TODO: pass tuple groups through
60
60
  for ctuple in concept.lineage.where.existence_arguments:
61
- base_existence += list(ctuple)
61
+ base_existence.append(ctuple)
62
62
  if direct_parent.grain:
63
63
  base_rows += direct_parent.grain.components_copy
64
64
  if (
@@ -71,7 +71,7 @@ def resolve_filter_parent_concepts(
71
71
  return (
72
72
  concept.lineage.content,
73
73
  unique(base_rows, "address"),
74
- unique(base_existence, "address"),
74
+ base_existence,
75
75
  )
76
76
  return concept.lineage.content, unique(base_rows, "address"), []
77
77
 
@@ -74,27 +74,31 @@ def gen_filter_node(
74
74
  return parent
75
75
 
76
76
  core_parents.append(parent)
77
+
77
78
  if parent_existence_concepts:
78
- logger.info(
79
- f"{padding(depth)}{LOGGER_PREFIX} fetching filter node existence parents {[x.address for x in parent_existence_concepts]}"
80
- )
81
- parent_existence = source_concepts(
82
- mandatory_list=parent_existence_concepts,
83
- environment=environment,
84
- g=g,
85
- depth=depth + 1,
86
- history=history,
87
- )
88
- if not parent_existence:
79
+ for existence_tuple in parent_existence_concepts:
80
+ if not existence_tuple:
81
+ continue
89
82
  logger.info(
90
- f"{padding(depth)}{LOGGER_PREFIX} filter existence node parents could not be found"
83
+ f"{padding(depth)}{LOGGER_PREFIX} fetching filter node existence parents {[x.address for x in existence_tuple]}"
91
84
  )
92
- return None
93
- core_parents.append(parent_existence)
94
-
85
+ parent_existence = source_concepts(
86
+ mandatory_list=list(existence_tuple),
87
+ environment=environment,
88
+ g=g,
89
+ depth=depth + 1,
90
+ history=history,
91
+ )
92
+ if not parent_existence:
93
+ logger.info(
94
+ f"{padding(depth)}{LOGGER_PREFIX} filter existence node parents could not be found"
95
+ )
96
+ return None
97
+ core_parents.append(parent_existence)
98
+ flattened_existence = [x for y in parent_existence_concepts for x in y]
95
99
  filter_node = FilterNode(
96
100
  input_concepts=unique(
97
- [immediate_parent] + parent_row_concepts + parent_existence_concepts,
101
+ [immediate_parent] + parent_row_concepts + flattened_existence,
98
102
  "address",
99
103
  ),
100
104
  output_concepts=[concept, immediate_parent] + parent_row_concepts,
@@ -19,7 +19,7 @@ from trilogy.core.processing.nodes.base_node import (
19
19
  concept_list_to_grain,
20
20
  )
21
21
  from trilogy.utility import unique
22
-
22
+ from trilogy.core.processing.utility import is_scalar_condition
23
23
 
24
24
  LOGGER_PREFIX = "[CONCEPT DETAIL - GROUP NODE]"
25
25
 
@@ -111,7 +111,8 @@ class GroupNode(StrategyNode):
111
111
  f" {parent.grain}"
112
112
  )
113
113
  source_type = SourceType.GROUP
114
- return QueryDatasource(
114
+
115
+ base = QueryDatasource(
115
116
  input_concepts=self.input_concepts,
116
117
  output_concepts=self.output_concepts,
117
118
  datasources=parent_sources,
@@ -134,6 +135,31 @@ class GroupNode(StrategyNode):
134
135
  partial_concepts=self.partial_concepts,
135
136
  condition=self.conditions,
136
137
  )
138
+ # if there is a condition on a group node and it's not scalar
139
+ # inject an additional CTE
140
+ if self.conditions:
141
+ logger.info("CONDITIONS")
142
+ logger.info(str(self.conditions))
143
+ logger.info(is_scalar_condition(self.conditions))
144
+ if self.conditions and not is_scalar_condition(self.conditions):
145
+ base.condition = None
146
+ base.output_concepts = self.output_concepts + self.conditions.row_arguments
147
+ return QueryDatasource(
148
+ input_concepts=base.output_concepts,
149
+ output_concepts=self.output_concepts,
150
+ datasources=[base],
151
+ source_type=SourceType.SELECT,
152
+ source_map=resolve_concept_map(
153
+ [base],
154
+ targets=self.output_concepts,
155
+ inherited_inputs=base.output_concepts,
156
+ ),
157
+ joins=[],
158
+ grain=grain,
159
+ partial_concepts=self.partial_concepts,
160
+ condition=self.conditions,
161
+ )
162
+ return base
137
163
 
138
164
  def copy(self) -> "GroupNode":
139
165
  return GroupNode(
@@ -8,6 +8,16 @@ from trilogy.core.models import (
8
8
  QueryDatasource,
9
9
  LooseConceptList,
10
10
  Environment,
11
+ Conditional,
12
+ SubselectComparison,
13
+ Comparison,
14
+ Parenthetical,
15
+ Function,
16
+ FilterItem,
17
+ MagicConstants,
18
+ WindowItem,
19
+ AggregateWrapper,
20
+ DataType,
11
21
  )
12
22
 
13
23
  from trilogy.core.enums import Purpose, Granularity
@@ -18,6 +28,8 @@ from collections import defaultdict
18
28
  from logging import Logger
19
29
  from pydantic import BaseModel
20
30
 
31
+ from trilogy.core.enums import FunctionClass
32
+
21
33
 
22
34
  class NodeType(Enum):
23
35
  CONCEPT = 1
@@ -332,3 +344,37 @@ def get_disconnected_components(
332
344
  x for x in sub_graphs if calculate_graph_relevance(graph, x, all_concepts) > 0
333
345
  ]
334
346
  return len(sub_graphs), sub_graphs
347
+
348
+
349
+ def is_scalar_condition(
350
+ element: (
351
+ int
352
+ | str
353
+ | float
354
+ | list
355
+ | WindowItem
356
+ | FilterItem
357
+ | Concept
358
+ | Comparison
359
+ | Conditional
360
+ | Parenthetical
361
+ | Function
362
+ | AggregateWrapper
363
+ | MagicConstants
364
+ | DataType
365
+ ),
366
+ ) -> bool:
367
+ if isinstance(element, Parenthetical):
368
+ return is_scalar_condition(element.content)
369
+ elif isinstance(element, SubselectComparison):
370
+ return True
371
+ elif isinstance(element, Comparison):
372
+ return is_scalar_condition(element.left) and is_scalar_condition(element.right)
373
+ elif isinstance(element, Function):
374
+ if element.operator in FunctionClass.AGGREGATE_FUNCTIONS.value:
375
+ return False
376
+ elif isinstance(element, AggregateWrapper):
377
+ return is_scalar_condition(element.function)
378
+ elif isinstance(element, Conditional):
379
+ return is_scalar_condition(element.left) and is_scalar_condition(element.right)
380
+ return True
@@ -350,7 +350,11 @@ def get_query_datasources(
350
350
 
351
351
  search_concepts: list[Concept] = statement.output_components
352
352
  nest_where = statement.where_clause_category == SelectFiltering.IMPLICIT
353
- if nest_where and statement.where_clause:
353
+
354
+ # if all are aggregates, we've pushed the filtering inside the aggregates anyway
355
+ all_aggregate = all([x.is_aggregate for x in search_concepts])
356
+
357
+ if nest_where and statement.where_clause and not all_aggregate:
354
358
  search_concepts = unique(
355
359
  statement.where_clause.row_arguments + search_concepts, "address"
356
360
  )
@@ -363,7 +367,8 @@ def get_query_datasources(
363
367
  )
364
368
  ds: GroupNode | SelectNode
365
369
  if nest_where and statement.where_clause:
366
- ods.conditions = statement.where_clause.conditional
370
+ if not all_aggregate:
371
+ ods.conditions = statement.where_clause.conditional
367
372
  ods.output_concepts = search_concepts
368
373
  # ods.hidden_concepts = where_delta
369
374
  ods.rebuild_cache()
trilogy/dialect/base.py CHANGED
@@ -2,6 +2,7 @@ from typing import List, Union, Optional, Dict, Any, Sequence, Callable
2
2
 
3
3
  from jinja2 import Template
4
4
 
5
+ from trilogy.core.processing.utility import is_scalar_condition
5
6
  from trilogy.constants import CONFIG, logger, MagicConstants
6
7
  from trilogy.core.internal import DEFAULT_CONCEPTS
7
8
  from trilogy.core.enums import (
@@ -46,6 +47,7 @@ from trilogy.core.models import (
46
47
  ProcessedRawSQLStatement,
47
48
  NumericType,
48
49
  MapType,
50
+ StructType,
49
51
  MergeStatementV2,
50
52
  )
51
53
  from trilogy.core.query_processor import process_query, process_persist
@@ -106,6 +108,10 @@ def render_case(args):
106
108
  return "CASE\n\t" + "\n\t".join(args) + "\n\tEND"
107
109
 
108
110
 
111
+ def struct_arg(args):
112
+ return [f"{x[0]}: {x[1]}" for x in zip(args[::2], args[1::2])]
113
+
114
+
109
115
  FUNCTION_MAP = {
110
116
  # generic types
111
117
  FunctionType.ALIAS: lambda x: f"{x[0]}",
@@ -120,6 +126,8 @@ FUNCTION_MAP = {
120
126
  FunctionType.INDEX_ACCESS: lambda x: f"{x[0]}[{x[1]}]",
121
127
  FunctionType.MAP_ACCESS: lambda x: f"{x[0]}[{x[1]}][1]",
122
128
  FunctionType.UNNEST: lambda x: f"unnest({x[0]})",
129
+ FunctionType.ATTR_ACCESS: lambda x: f"""{x[0]}.{x[1].replace("'", "")}""",
130
+ FunctionType.STRUCT: lambda x: f"{{{', '.join(struct_arg(x))}}}",
123
131
  # math
124
132
  FunctionType.ADD: lambda x: f"{x[0]} + {x[1]}",
125
133
  FunctionType.SUBTRACT: lambda x: f"{x[0]} - {x[1]}",
@@ -164,7 +172,6 @@ FUNCTION_MAP = {
164
172
  # constant types
165
173
  FunctionType.CURRENT_DATE: lambda x: "current_date()",
166
174
  FunctionType.CURRENT_DATETIME: lambda x: "current_datetime()",
167
- FunctionType.ATTR_ACCESS: lambda x: f"""{x[0]}.{x[1].replace("'", "")}""",
168
175
  }
169
176
 
170
177
  FUNCTION_GRAIN_MATCH_MAP = {
@@ -196,7 +203,9 @@ TOP {{ limit }}{% endif %}
196
203
  WHERE
197
204
  \t{{ where }}{% endif %}{%- if group_by %}
198
205
  GROUP BY {% for group in group_by %}
199
- \t{{group}}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}{%- if order_by %}
206
+ \t{{group}}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}{% if having %}
207
+ HAVING
208
+ \t{{ having }}{% endif %}{%- if order_by %}
200
209
  ORDER BY{% for order in order_by %}
201
210
  \t{{ order }}{% if not loop.last %},{% endif %}{% endfor %}
202
211
  {% endif %}{% endif %}
@@ -347,6 +356,7 @@ class BaseDialect:
347
356
  MapWrapper[Any, Any],
348
357
  MapType,
349
358
  NumericType,
359
+ StructType,
350
360
  ListType,
351
361
  ListWrapper[Any],
352
362
  DatePart,
@@ -370,11 +380,18 @@ class BaseDialect:
370
380
  lookup_cte = cte_map.get(e.right.address)
371
381
  assert lookup_cte, "Subselects must be rendered with a CTE in context"
372
382
  if e.right.address not in lookup_cte.existence_source_map:
373
- lookup = lookup_cte.source_map[e.right.address]
383
+ lookup = lookup_cte.source_map.get(
384
+ e.right.address,
385
+ [
386
+ INVALID_REFERENCE_STRING(
387
+ f"Missing source reference to {e.right.name}"
388
+ )
389
+ ],
390
+ )
374
391
  else:
375
392
  lookup = lookup_cte.existence_source_map[e.right.address]
376
393
 
377
- return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map)} {e.operator.value} (select {lookup[0]}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} from {lookup[0]})"
394
+ return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map)} {e.operator.value} (select {lookup[0]}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} from {lookup[0]} where {lookup[0]}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} is not null)"
378
395
  elif isinstance(e.right, (ListWrapper, Parenthetical, list)):
379
396
  return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map)}"
380
397
 
@@ -418,6 +435,7 @@ class BaseDialect:
418
435
  elif isinstance(e, CaseElse):
419
436
  return f"ELSE {self.render_expr(e.expr, cte=cte, cte_map=cte_map) }"
420
437
  elif isinstance(e, Function):
438
+
421
439
  if cte and cte.group_to_grain:
422
440
  return self.FUNCTION_MAP[e.operator](
423
441
  [self.render_expr(z, cte=cte, cte_map=cte_map) for z in e.arguments]
@@ -507,8 +525,15 @@ class BaseDialect:
507
525
  if j
508
526
  ],
509
527
  where=(
510
- self.render_expr(cte.condition, cte) if cte.condition else None
511
- ), # source_map=cte_output_map)
528
+ self.render_expr(cte.condition, cte)
529
+ if cte.condition and is_scalar_condition(cte.condition)
530
+ else None
531
+ ),
532
+ having=(
533
+ self.render_expr(cte.condition, cte)
534
+ if cte.condition and not is_scalar_condition(cte.condition)
535
+ else None
536
+ ),
512
537
  order_by=(
513
538
  [self.render_order_item(i, cte) for i in cte.order_by.items]
514
539
  if cte.order_by
@@ -59,7 +59,9 @@ SELECT
59
59
  {{ where }}
60
60
  {% endif %}
61
61
  {%- if group_by %}GROUP BY {% for group in group_by %}
62
- {{group}}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}
62
+ {{group}}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}{% if having %}
63
+ HAVING
64
+ \t{{ having }}{% endif %}
63
65
  {%- if order_by %}
64
66
  ORDER BY {% for order in order_by %}
65
67
  {{ order }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}
trilogy/dialect/duckdb.py CHANGED
@@ -59,11 +59,12 @@ SELECT
59
59
  {{ join }}{% endfor %}{% endif %}
60
60
  {% if where %}WHERE
61
61
  {{ where }}
62
- {% endif -%}
63
- {%- if group_by %}
62
+ {% endif -%}{%- if group_by %}
64
63
  GROUP BY {% for group in group_by %}
65
- {{group}}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}
66
- {%- if order_by %}
64
+ {{group}}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}{% if having %}
65
+ HAVING
66
+ {{ having }}
67
+ {% endif %}{%- if order_by %}
67
68
  ORDER BY {% for order in order_by %}
68
69
  {{ order }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}
69
70
  {%- if limit is not none %}
@@ -64,7 +64,9 @@ SELECT
64
64
  {{ where }}
65
65
  {% endif %}
66
66
  {%- if group_by %}GROUP BY {% for group in group_by %}
67
- {{group}}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}
67
+ {{group}}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}{% if having %}
68
+ HAVING
69
+ \t{{ having }}{% endif %}
68
70
  {%- if order_by %}
69
71
  ORDER BY {% for order in order_by %}
70
72
  {{ order }}{% if not loop.last %},{% endif %}
trilogy/dialect/presto.py CHANGED
@@ -63,7 +63,9 @@ SELECT
63
63
  {{ where }}
64
64
  {% endif %}
65
65
  {%- if group_by %}GROUP BY {% for group in group_by %}
66
- {{group}}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}
66
+ {{group}}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}{% if having %}
67
+ HAVING
68
+ \t{{ having }}{% endif %}
67
69
  {%- if order_by %}
68
70
  ORDER BY {% for order in order_by %}
69
71
  {{ order }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}
@@ -61,7 +61,9 @@ SELECT
61
61
  {{ where }}
62
62
  {% endif %}
63
63
  {%- if group_by %}GROUP BY {% for group in group_by %}
64
- {{group}}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}
64
+ {{group}}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}{% if having %}
65
+ HAVING
66
+ \t{{ having }}{% endif %}
65
67
  {%- if order_by %}
66
68
  ORDER BY {% for order in order_by %}
67
69
  {{ order }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}
@@ -60,7 +60,9 @@ TOP {{ limit }}{% endif %}
60
60
  {%- if group_by %}
61
61
  GROUP BY {% for group in group_by %}
62
62
  {{group}}{% if not loop.last %},{% endif %}
63
- {% endfor %}{% endif %}
63
+ {% endfor %}{% endif %}{% if having %}
64
+ HAVING
65
+ \t{{ having }}{% endif %}
64
66
  {%- if order_by %}
65
67
  ORDER BY {% for order in order_by %}
66
68
  {{ order }}{% if not loop.last %},{% endif %}
trilogy/parsing/common.py CHANGED
@@ -198,6 +198,7 @@ def arbitrary_to_concept(
198
198
  metadata: Metadata | None = None,
199
199
  purpose: Purpose | None = None,
200
200
  ) -> Concept:
201
+
201
202
  if isinstance(parent, AggregateWrapper):
202
203
  return agg_wrapper_to_concept(parent, namespace, name, metadata, purpose)
203
204
  elif isinstance(parent, WindowItem):
@@ -500,7 +500,53 @@ class ParseToObjects(Transformer):
500
500
  # we need to strip off every parenthetical to see what is being assigned.
501
501
  while isinstance(source_value, Parenthetical):
502
502
  source_value = source_value.content
503
- if isinstance(
503
+
504
+ if (
505
+ isinstance(source_value, Function)
506
+ and source_value.operator == FunctionType.STRUCT
507
+ ):
508
+ concept = arbitrary_to_concept(
509
+ source_value,
510
+ name=name,
511
+ namespace=namespace,
512
+ purpose=purpose,
513
+ metadata=metadata,
514
+ )
515
+
516
+ if concept.metadata:
517
+ concept.metadata.line_number = meta.line
518
+ self.environment.add_concept(concept, meta=meta)
519
+ assert isinstance(concept.datatype, StructType)
520
+ for key, value in concept.datatype.fields_map.items():
521
+ args = self.process_function_args([concept, key], meta=meta)
522
+ self.environment.add_concept(
523
+ Concept(
524
+ name=key,
525
+ datatype=arg_to_datatype(value),
526
+ purpose=Purpose.PROPERTY,
527
+ namespace=self.environment.namespace + "." + name,
528
+ lineage=AttrAccess(args),
529
+ )
530
+ )
531
+ return ConceptDerivation(concept=concept)
532
+ elif (
533
+ isinstance(source_value, Function)
534
+ and source_value.operator == FunctionType.ALIAS
535
+ ):
536
+ concept = arbitrary_to_concept(
537
+ source_value,
538
+ name=name,
539
+ namespace=namespace,
540
+ purpose=purpose,
541
+ metadata=metadata,
542
+ )
543
+
544
+ if concept.metadata:
545
+ concept.metadata.line_number = meta.line
546
+ self.environment.add_concept(concept, meta=meta)
547
+ return ConceptDerivation(concept=concept)
548
+
549
+ elif isinstance(
504
550
  source_value, (FilterItem, WindowItem, AggregateWrapper, Function)
505
551
  ):
506
552
  concept = arbitrary_to_concept(
@@ -515,6 +561,7 @@ class ParseToObjects(Transformer):
515
561
  concept.metadata.line_number = meta.line
516
562
  self.environment.add_concept(concept, meta=meta)
517
563
  return ConceptDerivation(concept=concept)
564
+
518
565
  elif isinstance(source_value, CONSTANT_TYPES):
519
566
  concept = constant_to_concept(
520
567
  source_value,
@@ -950,7 +997,15 @@ class ParseToObjects(Transformer):
950
997
  isinstance(orderitem.expr, Concept)
951
998
  and orderitem.expr.purpose == Purpose.METRIC
952
999
  ):
953
- orderitem.expr = orderitem.expr.with_grain(output.grain)
1000
+ orderitem.expr = orderitem.expr.with_select_context(
1001
+ output.grain,
1002
+ conditional=(
1003
+ output.where_clause.conditional
1004
+ if output.where_clause
1005
+ and output.where_clause_category == SelectFiltering.IMPLICIT
1006
+ else None
1007
+ ),
1008
+ )
954
1009
  return output
955
1010
 
956
1011
  @v_args(meta=True)
@@ -1016,6 +1071,18 @@ class ParseToObjects(Transformer):
1016
1071
  def array_lit(self, args):
1017
1072
  return list_to_wrapper(args)
1018
1073
 
1074
+ def struct_lit(self, args):
1075
+
1076
+ zipped = dict(zip(args[::2], args[1::2]))
1077
+ types = [arg_to_datatype(x) for x in args[1::2]]
1078
+ return Function(
1079
+ operator=FunctionType.STRUCT,
1080
+ output_datatype=StructType(fields=types, fields_map=zipped),
1081
+ output_purpose=function_args_to_output_purpose(args),
1082
+ arguments=args,
1083
+ arg_count=-1,
1084
+ )
1085
+
1019
1086
  def map_lit(self, args):
1020
1087
  parsed = dict(zip(args[::2], args[1::2]))
1021
1088
  wrapped = dict_to_map_wrapper(parsed)
@@ -1162,6 +1229,11 @@ class ParseToObjects(Transformer):
1162
1229
  return MapAccess(args)
1163
1230
  return IndexAccess(args)
1164
1231
 
1232
+ @v_args(meta=True)
1233
+ def map_key_access(self, meta, args):
1234
+ args = self.process_function_args(args, meta=meta)
1235
+ return MapAccess(args)
1236
+
1165
1237
  @v_args(meta=True)
1166
1238
  def attr_access(self, meta, args):
1167
1239
  args = self.process_function_args(args, meta=meta)
@@ -153,9 +153,10 @@
153
153
  unnest: _UNNEST expr ")"
154
154
  //indexing into an expression is a function
155
155
  index_access: expr "[" int_lit "]"
156
- attr_access: expr "[" _string_lit "]"
156
+ map_key_access: expr "[" _string_lit "]"
157
+ attr_access: expr "." _string_lit
157
158
 
158
- expr: _constant_functions | window_item | filter_item | subselect_comparison | between_comparison | fgroup | aggregate_functions | unnest | _static_functions | literal | concept_lit | index_access | attr_access | parenthetical | expr_tuple | comparison | alt_like
159
+ expr: _constant_functions | window_item | filter_item | subselect_comparison | between_comparison | fgroup | aggregate_functions | unnest | _static_functions | literal | concept_lit | index_access | map_key_access | attr_access | parenthetical | expr_tuple | comparison | alt_like
159
160
 
160
161
  // functions
161
162
 
@@ -276,12 +277,15 @@
276
277
  array_lit: "[" (literal ",")* literal ","? "]"()
277
278
 
278
279
  map_lit: "{" (literal ":" literal ",")* literal ":" literal ","? "}"
280
+
281
+
282
+ struct_lit: "struct" "(" (IDENTIFIER "=" literal ",")* IDENTIFIER "=" literal ","? ")"
279
283
 
280
284
  !bool_lit: "True"i | "False"i
281
285
 
282
286
  !null_lit.1: "null"i
283
287
 
284
- literal: null_lit | _string_lit | int_lit | float_lit | bool_lit | array_lit | map_lit
288
+ literal: null_lit | _string_lit | int_lit | float_lit | bool_lit | array_lit | map_lit | struct_lit
285
289
 
286
290
  MODIFIER: "Optional"i | "Partial"i | "Nullable"i
287
291