pytrilogy 0.0.3.100__py3-none-any.whl → 0.0.3.102__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.100
3
+ Version: 0.0.3.102
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,5 +1,5 @@
1
- pytrilogy-0.0.3.100.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=g9zY8214LmrIQyjxs_q4DVvbSXccQFQwFe0o0_PZcD8,304
1
+ pytrilogy-0.0.3.102.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=WZdbHlLqyuYo0xjcYkV5QDokunZLDlhGeibgoay48uc,304
3
3
  trilogy/constants.py,sha256=ohmro6so7PPNp2ruWQKVc0ijjXYPOyRrxB9LI8dr3TU,1746
4
4
  trilogy/engine.py,sha256=3MiADf5MKcmxqiHBuRqiYdsXiLj7oitDfVvXvHrfjkA,2178
5
5
  trilogy/executor.py,sha256=KgCAQhHPT-j0rPkBbALX0f84W9-Q-bkjHayGuavg99w,16490
@@ -14,18 +14,18 @@ trilogy/core/enums.py,sha256=H8I2Dz4POHZ4ixYCGzNs4c3KDqxLQklGLVfmje1DSMo,8877
14
14
  trilogy/core/env_processor.py,sha256=H-rr2ALj31l5oh3FqeI47Qju6OOfiXBacXNJGNZ92zQ,4521
15
15
  trilogy/core/environment_helpers.py,sha256=TRlqVctqIRBxzfjRBmpQsAVoiCcsEKBhG1B6PUE0l1M,12743
16
16
  trilogy/core/ergonomics.py,sha256=e-7gE29vPLFdg0_A1smQ7eOrUwKl5VYdxRSTddHweRA,1631
17
- trilogy/core/exceptions.py,sha256=fI16oTNCVMMAJFSn2AFzZVapzsF5M9WbdN5e5UixwXc,2807
18
- trilogy/core/functions.py,sha256=oY-F0hsA9vp1ZipGTyx4QVtz_x83Ekk-lkHv6mMkHVQ,33095
17
+ trilogy/core/exceptions.py,sha256=axkVXYJYQXCCwMHwlyDA232g4tCOwdCZUt7eHeUMDMg,2829
18
+ trilogy/core/functions.py,sha256=sdV6Z3NUVfwL1d18eNcaAXllVNqzLez23McsJ6xIp7M,33182
19
19
  trilogy/core/graph_models.py,sha256=4EWFTHGfYd72zvS2HYoV6hm7nMC_VEd7vWr6txY-ig0,3400
20
20
  trilogy/core/internal.py,sha256=r9QagDB2GvpqlyD_I7VrsfbVfIk5mnok2znEbv72Aa4,2681
21
21
  trilogy/core/optimization.py,sha256=ojpn-p79lr03SSVQbbw74iPCyoYpDYBmj1dbZ3oXCjI,8860
22
22
  trilogy/core/query_processor.py,sha256=uqygDJqkjIH4vLP-lbGRgTN7rRcYEkr3KGqNimNw_80,20345
23
23
  trilogy/core/utility.py,sha256=3VC13uSQWcZNghgt7Ot0ZTeEmNqs__cx122abVq9qhM,410
24
24
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- trilogy/core/models/author.py,sha256=ZSKEJ6Vg4otpI_m7_JuGyrFM8dZV1HaxBwprvDSwUzo,81149
26
- trilogy/core/models/build.py,sha256=ZwJJGyp4rVsISvL8Er_AxQdVJrafYc4fesSj4MNgoxU,70615
25
+ trilogy/core/models/author.py,sha256=3I7PFpJgoQT9RPOT3DfiqAjEtkcQPJnScs60I2UoyWo,81461
26
+ trilogy/core/models/build.py,sha256=iqk_-3plxX1BdxvUCTebqE9F3x62f40neKGf6Ld4VVU,70858
27
27
  trilogy/core/models/build_environment.py,sha256=mpx7MKGc60fnZLVdeLi2YSREy7eQbQYycCrP4zF-rHU,5258
28
- trilogy/core/models/core.py,sha256=EofJ8-kltNr_7oFhyCPqauVX1bSJzJI5xOp0eMP_vlA,12892
28
+ trilogy/core/models/core.py,sha256=iT9WdZoiXeglmUHWn6bZyXCTBpkApTGPKtNm_Mhbu_g,12987
29
29
  trilogy/core/models/datasource.py,sha256=wogTevZ-9CyUW2a8gjzqMCieircxi-J5lkI7EOAZnck,9596
30
30
  trilogy/core/models/environment.py,sha256=hwTIRnJgaHUdCYof7U5A9NPitGZ2s9yxqiW5O2SaJ9Y,28759
31
31
  trilogy/core/models/execute.py,sha256=lsNzNjS3nZvoW5CHjYwxDTwBe502NZyytpK1eq8CwW4,42357
@@ -45,10 +45,10 @@ trilogy/core/processing/node_generators/basic_node.py,sha256=0Uhnf07056SBbRkt-wY
45
45
  trilogy/core/processing/node_generators/common.py,sha256=PdysdroW9DUADP7f5Wv_GKPUyCTROZV1g3L45fawxi8,9443
46
46
  trilogy/core/processing/node_generators/constant_node.py,sha256=LfpDq2WrBRZ3tGsLxw77LuigKfhbteWWh9L8BGdMGwk,1146
47
47
  trilogy/core/processing/node_generators/filter_node.py,sha256=ArBsQJl-4fWBJWCE28CRQ7UT7ErnFfbcseoQQZrBodY,11220
48
- trilogy/core/processing/node_generators/group_node.py,sha256=pq8aqKe4hCtkzFtpHvE15BJoYvpveoe50_2IM1pqjIQ,6732
48
+ trilogy/core/processing/node_generators/group_node.py,sha256=yqOWl5TCV4PrdJua4OJkPUIHkljaLoSW2Y8eRAmVddQ,6733
49
49
  trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZwaRU0sbUSr9H0moQbAxrGgX6iGA,3832
50
50
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
51
- trilogy/core/processing/node_generators/node_merge_node.py,sha256=531ptEAZIczc7PR-kfuNe_KBaDToyIMUMKq4bkoZkgw,23561
51
+ trilogy/core/processing/node_generators/node_merge_node.py,sha256=1joMV7XpQ9Gpe-d5y7JUMBHIqakV5wFJi3Mtvs4UcL4,23415
52
52
  trilogy/core/processing/node_generators/recursive_node.py,sha256=l5zdh0dURKwmAy8kK4OpMtZfyUEQRk6N-PwSWIyBpSM,2468
53
53
  trilogy/core/processing/node_generators/rowset_node.py,sha256=5L5u6xz1In8EaHQdcYgR2si-tz9WB9YLXURo4AkUT9A,6630
54
54
  trilogy/core/processing/node_generators/select_merge_node.py,sha256=KQvGoNT5ZBWQ_caEomRTtG1PKZC7OPT4PKfY0QmwMGE,22270
@@ -77,11 +77,11 @@ trilogy/core/statements/execute.py,sha256=kiwJcVeMa4wZR-xLfM2oYOJ9DeyJkP8An38WFy
77
77
  trilogy/core/validation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
78
  trilogy/core/validation/common.py,sha256=Sd-towAX1uSDe3dK51FcVtIwVrMhayEwdHqhzeJHro0,4776
79
79
  trilogy/core/validation/concept.py,sha256=PM2BxBxLvuBScSWZMPsDZVcOblDil5pNT0pHLcLhdPA,5242
80
- trilogy/core/validation/datasource.py,sha256=d9AQNcukIRgN2spItPsXFiNtlZva-lDnfei3i06yQCE,6489
80
+ trilogy/core/validation/datasource.py,sha256=nJeEFyb6iMBwlEVdYVy1vLzAbdRZwOsUjGxgWKgY8oM,7636
81
81
  trilogy/core/validation/environment.py,sha256=ymvhQyt7jLK641JAAIQkqjQaAmr9C5022ILzYvDgPP0,2835
82
82
  trilogy/core/validation/fix.py,sha256=Z818UFNLxndMTLiyhB3doLxIfnOZ-16QGvVFWuD7UsA,3750
83
83
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
84
- trilogy/dialect/base.py,sha256=0QVHv4F0t3_gRQrZ0woFoUNKu7vaXGo-BG1l47CZUKc,49698
84
+ trilogy/dialect/base.py,sha256=d2gXfa5Jh3uyN9H9MxG53JT-xQQgntq2X7EprobJYUc,49698
85
85
  trilogy/dialect/bigquery.py,sha256=XS3hpybeowgfrOrkycAigAF3NX2YUzTzfgE6f__2fT4,4316
86
86
  trilogy/dialect/common.py,sha256=_MarnMWRBn3VcNt3k5VUdFrwH6oHzGdNQquSpHNLq4o,5644
87
87
  trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
@@ -99,26 +99,26 @@ trilogy/hooks/graph_hook.py,sha256=5BfR7Dt0bgEsCLgwjowgCsVkboGYfVJGOz8g9mqpnos,4
99
99
  trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
100
100
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
101
101
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
102
- trilogy/parsing/common.py,sha256=550-L0444GUuBFdiDWkOg_DxnMXtcJFUMES2R5zlwik,31026
102
+ trilogy/parsing/common.py,sha256=NJLm31J3W9BLWq1ClhNvYE43jrF950698KJ3o0UfSCo,31340
103
103
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
104
104
  trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
105
105
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
106
- trilogy/parsing/parse_engine.py,sha256=2k1TvnBYE_CW5zCmNfVbf1aWBuMDm5Wz4QfKKgGnE5k,81824
107
- trilogy/parsing/render.py,sha256=tqB3GlGk3bX6AbkJjvADad2QH6n63nw1kgrpjzLX2tI,20520
108
- trilogy/parsing/trilogy.lark,sha256=rM4WleeyGhoRgU-FOGcaeHOzZcYVxN4f13e_3B4OeLQ,16389
106
+ trilogy/parsing/parse_engine.py,sha256=T-3Q4UH256IB6cfX85crScZwZ6gAwslgv0fy3WKBdjc,81930
107
+ trilogy/parsing/render.py,sha256=IklKMdXiqQEB6D28PrU1BewlDwD88Hnmqn1xjA9h720,23863
108
+ trilogy/parsing/trilogy.lark,sha256=6eBDD6d4D9N1Nnn4CtmaoB-NpOpjHrEn5oi0JykAlbE,16509
109
109
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
110
110
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
111
111
  trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
112
112
  trilogy/std/date.preql,sha256=HWZm4t4HWyxr5geWRsY05RnHBVDMci8z8YA2cu0-OOw,188
113
- trilogy/std/display.preql,sha256=nm7lox87Xf6lBvXCVCS6x2HskguMKzndEBucJ5pktzk,175
113
+ trilogy/std/display.preql,sha256=S20HW8qbShBc4OZPcHYiRlLdcaBp9dwruozWBoXKscs,293
114
114
  trilogy/std/geography.preql,sha256=1A9Sq5PPMBnEPPf7f-rPVYxJfsnWpQ8oV_k4Fm3H2dU,675
115
115
  trilogy/std/metric.preql,sha256=DRECGhkMyqfit5Fl4Ut9zbWrJuSMI1iO9HikuyoBpE0,421
116
116
  trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
117
117
  trilogy/std/net.preql,sha256=WZCuvH87_rZntZiuGJMmBDMVKkdhTtxeHOkrXNwJ1EE,416
118
118
  trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
119
119
  trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
120
- pytrilogy-0.0.3.100.dist-info/METADATA,sha256=T9p4b_yjL4_HtEwChltpxh5mlP8pCoRQPn28Ucu_1gI,11811
121
- pytrilogy-0.0.3.100.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
122
- pytrilogy-0.0.3.100.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
123
- pytrilogy-0.0.3.100.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
124
- pytrilogy-0.0.3.100.dist-info/RECORD,,
120
+ pytrilogy-0.0.3.102.dist-info/METADATA,sha256=fQKKWHDkY9Nhofow6RO22oMSXp91H-vOD5d3kk3S-V8,11811
121
+ pytrilogy-0.0.3.102.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
122
+ pytrilogy-0.0.3.102.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
123
+ pytrilogy-0.0.3.102.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
124
+ pytrilogy-0.0.3.102.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.100"
7
+ __version__ = "0.0.3.102"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -69,7 +69,7 @@ class DatasourceColumnBindingData:
69
69
  actual_modifiers: List[Modifier]
70
70
 
71
71
  def format_failure(self):
72
- return f"Concept {self.address} value '{self.value}' with type {self.value_modifiers} does not conform to expected type {str(self.actual_type)} with modifiers {self.actual_modifiers}"
72
+ return f"Concept {self.address} value '{self.value}' with type {self.value_type} and {self.value_modifiers} does not conform to expected type {str(self.actual_type)} with modifiers {self.actual_modifiers}"
73
73
 
74
74
  def is_modifier_issue(self) -> bool:
75
75
  return len(self.value_modifiers) > 0 and any(
trilogy/core/functions.py CHANGED
@@ -18,6 +18,7 @@ from trilogy.core.models.author import (
18
18
  AggregateWrapper,
19
19
  Concept,
20
20
  ConceptRef,
21
+ Conditional,
21
22
  Function,
22
23
  Parenthetical,
23
24
  UndefinedConcept,
@@ -129,8 +130,8 @@ def validate_case_output(
129
130
  def create_struct_output(
130
131
  args: list[Any],
131
132
  ) -> StructType:
132
- zipped = dict(zip(args[::2], args[1::2]))
133
- types = [arg_to_datatype(x) for x in args[1::2]]
133
+ zipped = dict(zip(args[1::2], args[::2]))
134
+ types = [arg_to_datatype(x) for x in args[::2]]
134
135
  return StructType(fields=types, fields_map=zipped)
135
136
 
136
137
 
@@ -997,6 +998,8 @@ def argument_to_purpose(arg) -> Purpose:
997
998
  return argument_to_purpose(arg.content)
998
999
  elif isinstance(arg, WindowItem):
999
1000
  return Purpose.PROPERTY
1001
+ elif isinstance(arg, Conditional):
1002
+ return Purpose.PROPERTY
1000
1003
  elif isinstance(arg, Concept):
1001
1004
  base = arg.purpose
1002
1005
  if (
@@ -259,6 +259,15 @@ class Parenthetical(
259
259
  )
260
260
  )
261
261
 
262
+ def with_reference_replacement(self, source, target):
263
+ return Parenthetical.model_construct(
264
+ content=(
265
+ self.content.with_reference_replacement(source, target)
266
+ if isinstance(self.content, Mergeable)
267
+ else self.content
268
+ )
269
+ )
270
+
262
271
  @property
263
272
  def concept_arguments(self) -> Sequence[ConceptRef]:
264
273
  base: List[ConceptRef] = []
@@ -1511,7 +1511,10 @@ def requires_concept_nesting(
1511
1511
  ) -> AggregateWrapper | WindowItem | FilterItem | Function | None:
1512
1512
  if isinstance(expr, (AggregateWrapper, WindowItem, FilterItem)):
1513
1513
  return expr
1514
- if isinstance(expr, Function) and expr.operator == FunctionType.GROUP:
1514
+ if isinstance(expr, Function) and expr.operator in (
1515
+ FunctionType.GROUP,
1516
+ FunctionType.PARENTHETICAL,
1517
+ ):
1515
1518
  # group by requires nesting
1516
1519
  return expr
1517
1520
  return None
@@ -1696,13 +1699,12 @@ class Factory:
1696
1699
  return self._build_case_when(base)
1697
1700
 
1698
1701
  def _build_case_when(self, base: CaseWhen) -> BuildCaseWhen:
1699
- comparison = base.comparison
1700
1702
  expr: Concept | FuncArgs = base.expr
1701
1703
  validation = requires_concept_nesting(expr)
1702
1704
  if validation:
1703
1705
  expr, _ = self.instantiate_concept(validation)
1704
1706
  return BuildCaseWhen(
1705
- comparison=self.build(comparison),
1707
+ comparison=self.build(base.comparison),
1706
1708
  expr=self.build(expr),
1707
1709
  )
1708
1710
 
@@ -2019,7 +2021,12 @@ class Factory:
2019
2021
  return self._build_parenthetical(base)
2020
2022
 
2021
2023
  def _build_parenthetical(self, base: Parenthetical) -> BuildParenthetical:
2022
- return BuildParenthetical(content=(self.build(base.content)))
2024
+ validate = requires_concept_nesting(base.content)
2025
+ if validate:
2026
+ content, _ = self.instantiate_concept(validate)
2027
+ return BuildParenthetical(content=self.build(content))
2028
+ else:
2029
+ return BuildParenthetical(content=self.build(base.content))
2023
2030
 
2024
2031
  @build.register
2025
2032
  def _(self, base: SelectLineage) -> BuildSelectLineage:
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from abc import ABC
4
4
  from collections import UserDict, UserList
5
5
  from datetime import date, datetime
6
+ from decimal import Decimal
6
7
  from enum import Enum
7
8
  from typing import (
8
9
  Any,
@@ -448,6 +449,8 @@ def arg_to_datatype(arg) -> CONCRETE_TYPES:
448
449
  return DataType.STRING
449
450
  elif isinstance(arg, float):
450
451
  return DataType.FLOAT
452
+ elif isinstance(arg, Decimal):
453
+ return DataType.NUMERIC
451
454
  elif isinstance(arg, DataType):
452
455
  return arg
453
456
  elif isinstance(arg, NumericType):
@@ -28,6 +28,7 @@ def get_aggregate_grain(
28
28
  parent_concepts: List[BuildConcept] = unique(
29
29
  resolve_function_parent_concepts(concept, environment=environment), "address"
30
30
  )
31
+
31
32
  if (
32
33
  concept.grain
33
34
  and len(concept.grain.components) > 0
@@ -164,9 +164,6 @@ def reinject_common_join_keys_v2(
164
164
  reduced = BuildGrain.from_concepts(concrete_concepts).components
165
165
  existing_addresses = set()
166
166
  for concrete in concrete_concepts:
167
- logger.debug(
168
- f"looking at column {concrete.address} with pseudonyms {concrete.pseudonyms}"
169
- )
170
167
  cnode = concept_to_node(concrete.with_default_grain())
171
168
  if cnode in final.nodes:
172
169
  existing_addresses.add(concrete.address)
@@ -36,31 +36,51 @@ def type_check(
36
36
  ) -> bool:
37
37
  if input is None and nullable:
38
38
  return True
39
+
39
40
  target_type = expected_type
40
41
  while isinstance(target_type, TraitDataType):
41
42
  return type_check(input, target_type.data_type, nullable)
43
+
42
44
  if target_type == DataType.STRING:
43
45
  return isinstance(input, str)
44
46
  if target_type == DataType.INTEGER:
45
47
  return isinstance(input, int)
48
+ if target_type == DataType.BIGINT:
49
+ return isinstance(input, int) # or check for larger int if needed
46
50
  if target_type == DataType.FLOAT or isinstance(target_type, NumericType):
47
51
  return (
48
52
  isinstance(input, float)
49
53
  or isinstance(input, int)
50
54
  or isinstance(input, Decimal)
51
55
  )
56
+ if target_type == DataType.NUMBER:
57
+ return isinstance(input, (int, float, Decimal))
58
+ if target_type == DataType.NUMERIC:
59
+ return isinstance(input, (int, float, Decimal))
52
60
  if target_type == DataType.BOOL:
53
61
  return isinstance(input, bool)
54
62
  if target_type == DataType.DATE:
55
- return isinstance(input, date)
63
+ return isinstance(input, date) and not isinstance(input, datetime)
56
64
  if target_type == DataType.DATETIME:
57
65
  return isinstance(input, datetime)
66
+ if target_type == DataType.TIMESTAMP:
67
+ return isinstance(input, datetime) # or timestamp type if you have one
68
+ if target_type == DataType.UNIX_SECONDS:
69
+ return isinstance(input, (int, float)) # Unix timestamps are numeric
70
+ if target_type == DataType.DATE_PART:
71
+ return isinstance(
72
+ input, str
73
+ ) # assuming date parts are strings like "year", "month"
58
74
  if target_type == DataType.ARRAY or isinstance(target_type, ArrayType):
59
75
  return isinstance(input, list)
60
76
  if target_type == DataType.MAP or isinstance(target_type, MapType):
61
77
  return isinstance(input, dict)
62
78
  if target_type == DataType.STRUCT or isinstance(target_type, StructType):
63
79
  return isinstance(input, dict)
80
+ if target_type == DataType.NULL:
81
+ return input is None
82
+ if target_type == DataType.UNKNOWN:
83
+ return True
64
84
  return False
65
85
 
66
86
 
@@ -125,15 +145,19 @@ def validate_datasource(
125
145
  rval = row[actual_address]
126
146
  passed = type_check(rval, col.concept.datatype, col.is_nullable)
127
147
  if not passed:
148
+ value_type = (
149
+ arg_to_datatype(rval) if rval is not None else col.concept.datatype
150
+ )
151
+ traits = None
152
+ if isinstance(col.concept.datatype, TraitDataType):
153
+ traits = col.concept.datatype.traits
154
+ if traits and not isinstance(value_type, TraitDataType):
155
+ value_type = TraitDataType(type=value_type, traits=traits)
128
156
  failures.append(
129
157
  DatasourceColumnBindingData(
130
158
  address=col.concept.address,
131
159
  value=rval,
132
- value_type=(
133
- arg_to_datatype(rval)
134
- if rval is not None
135
- else col.concept.datatype
136
- ),
160
+ value_type=value_type,
137
161
  value_modifiers=[Modifier.NULLABLE] if rval is None else [],
138
162
  actual_type=col.concept.datatype,
139
163
  actual_modifiers=col.concept.modifiers,
trilogy/dialect/base.py CHANGED
@@ -163,7 +163,7 @@ def render_case(args):
163
163
 
164
164
 
165
165
  def struct_arg(args):
166
- return [f"{x[0]}: {x[1]}" for x in zip(args[::2], args[1::2])]
166
+ return [f"{x[1]}: {x[0]}" for x in zip(args[::2], args[1::2])]
167
167
 
168
168
 
169
169
  FUNCTION_MAP = {
trilogy/parsing/common.py CHANGED
@@ -249,11 +249,14 @@ def atom_is_relevant(
249
249
  return atom_is_relevant(atom.left, others, environment) or atom_is_relevant(
250
250
  atom.right, others, environment
251
251
  )
252
+ elif isinstance(atom, Parenthetical):
253
+ return atom_is_relevant(atom.content, others, environment)
252
254
  elif isinstance(atom, ConceptArgs):
253
255
  # use atom is relevant here to trigger the early exit behavior for concepts in set
254
256
  return any(
255
257
  [atom_is_relevant(x, others, environment) for x in atom.concept_arguments]
256
258
  )
259
+
257
260
  return False
258
261
 
259
262
 
@@ -294,12 +297,18 @@ def concept_is_relevant(
294
297
  if all([c in others for c in concept.grain.components]):
295
298
  return False
296
299
  if concept.derivation in (Derivation.BASIC,) and isinstance(
297
- concept.lineage, Function
300
+ concept.lineage, (Function, CaseWhen)
298
301
  ):
299
302
  relevant = False
300
303
  for arg in concept.lineage.arguments:
301
304
  relevant = atom_is_relevant(arg, others, environment) or relevant
305
+
302
306
  return relevant
307
+ if concept.derivation in (Derivation.BASIC,) and isinstance(
308
+ concept.lineage, Parenthetical
309
+ ):
310
+ return atom_is_relevant(concept.lineage.content, others, environment)
311
+
303
312
  if concept.granularity == Granularity.SINGLE_ROW:
304
313
  return False
305
314
  return True
@@ -346,6 +355,7 @@ def concepts_to_grain_concepts(
346
355
  if sub.address in seen:
347
356
  continue
348
357
  if not concept_is_relevant(sub, pconcepts, environment): # type: ignore
358
+
349
359
  continue
350
360
  seen.add(sub.address)
351
361
 
@@ -992,6 +992,9 @@ class ParseToObjects(Transformer):
992
992
  def order_by(self, args):
993
993
  return OrderBy(items=args[0])
994
994
 
995
+ def over_component(self, args):
996
+ return ConceptRef(address=args[0].value.lstrip(",").strip())
997
+
995
998
  def over_list(self, args):
996
999
  return [x for x in args]
997
1000
 
trilogy/parsing/render.py CHANGED
@@ -1,4 +1,6 @@
1
1
  from collections import defaultdict
2
+ from contextlib import contextmanager
3
+ from dataclasses import dataclass
2
4
  from datetime import date, datetime
3
5
  from functools import singledispatchmethod
4
6
  from typing import Any
@@ -23,6 +25,7 @@ from trilogy.core.models.author import (
23
25
  FunctionCallWrapper,
24
26
  Grain,
25
27
  OrderBy,
28
+ Ordering,
26
29
  OrderItem,
27
30
  Parenthetical,
28
31
  SubselectComparison,
@@ -67,23 +70,72 @@ from trilogy.core.statements.author import (
67
70
 
68
71
  QUERY_TEMPLATE = Template(
69
72
  """{% if where %}WHERE
70
- {{ where }}
73
+ {{ where }}
71
74
  {% endif %}SELECT{%- for select in select_columns %}
72
- {{ select }},{% endfor %}{% if having %}
75
+ {{ select }},{% endfor %}{% if having %}
73
76
  HAVING
74
- {{ having }}
77
+ {{ having }}
75
78
  {% endif %}{%- if order_by %}
76
79
  ORDER BY{% for order in order_by %}
77
- {{ order }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}{%- if limit is not none %}
80
+ {{ order }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}{%- if limit is not none %}
78
81
  LIMIT {{ limit }}{% endif %}
79
82
  ;"""
80
83
  )
81
84
 
82
85
 
86
+ @dataclass
87
+ class IndentationContext:
88
+ """Tracks indentation state during rendering"""
89
+
90
+ depth: int = 0
91
+ indent_string: str = " " # 4 spaces by default
92
+
93
+ @property
94
+ def current_indent(self) -> str:
95
+ return self.indent_string * self.depth
96
+
97
+ def increase_depth(self, extra_levels: int = 1) -> "IndentationContext":
98
+ return IndentationContext(
99
+ depth=self.depth + extra_levels, indent_string=self.indent_string
100
+ )
101
+
102
+
83
103
  class Renderer:
84
104
 
85
- def __init__(self, environment: Environment | None = None):
105
+ def __init__(
106
+ self, environment: Environment | None = None, indent_string: str = " "
107
+ ):
86
108
  self.environment = environment
109
+ self.indent_context = IndentationContext(indent_string=indent_string)
110
+
111
+ @contextmanager
112
+ def indented(self, levels: int = 1):
113
+ """Context manager for temporarily increasing indentation"""
114
+ old_context = self.indent_context
115
+ self.indent_context = self.indent_context.increase_depth(levels)
116
+ try:
117
+ yield
118
+ finally:
119
+ self.indent_context = old_context
120
+
121
+ def indent_lines(self, text: str, extra_levels: int = 0) -> str:
122
+ """Apply current indentation to all lines in text"""
123
+ if not text:
124
+ return text
125
+
126
+ indent = self.indent_context.indent_string * (
127
+ self.indent_context.depth + extra_levels
128
+ )
129
+ lines = text.split("\n")
130
+ indented_lines = []
131
+
132
+ for line in lines:
133
+ if line.strip(): # Only indent non-empty lines
134
+ indented_lines.append(indent + line)
135
+ else:
136
+ indented_lines.append(line) # Keep empty lines as-is
137
+
138
+ return "\n".join(indented_lines)
87
139
 
88
140
  def render_statement_string(self, list_of_statements: list[Any]) -> str:
89
141
  new = []
@@ -98,7 +150,7 @@ class Renderer:
98
150
  new.append("\n\n")
99
151
  else:
100
152
  new.append("\n")
101
- new.append(Renderer().to_string(stmt))
153
+ new.append(self.to_string(stmt))
102
154
  last_statement_type = stmt_type
103
155
  return "".join(new)
104
156
 
@@ -192,14 +244,19 @@ class Renderer:
192
244
 
193
245
  @to_string.register
194
246
  def _(self, arg: Datasource):
195
- assignments = ",\n ".join([self.to_string(x) for x in arg.columns])
247
+ with self.indented():
248
+ assignments = ",\n".join(
249
+ [self.indent_lines(self.to_string(x)) for x in arg.columns]
250
+ )
251
+
196
252
  if arg.non_partial_for:
197
253
  non_partial = f"\ncomplete where {self.to_string(arg.non_partial_for)}"
198
254
  else:
199
255
  non_partial = ""
256
+
200
257
  base = f"""datasource {arg.name} (
201
- {assignments}
202
- )
258
+ {assignments}
259
+ )
203
260
  {self.to_string(arg.grain) if arg.grain.components else ''}{non_partial}
204
261
  {self.to_string(arg.address)}"""
205
262
 
@@ -349,7 +406,8 @@ class Renderer:
349
406
  else:
350
407
  output = f"{concept.purpose.value} {namespace}{concept.name} <- {self.to_string(concept.lineage)};"
351
408
  if base_description:
352
- output += f" #{base_description}"
409
+ lines = "\n#".join(base_description.split("\n"))
410
+ output += f" #{lines}"
353
411
  return output
354
412
 
355
413
  @to_string.register
@@ -389,26 +447,45 @@ class Renderer:
389
447
 
390
448
  @to_string.register
391
449
  def _(self, arg: SelectStatement):
450
+ with self.indented():
451
+ select_columns = [
452
+ self.indent_lines(self.to_string(c)) for c in arg.selection
453
+ ]
454
+ where_clause = None
455
+ if arg.where_clause:
456
+ where_clause = self.indent_lines(self.to_string(arg.where_clause))
457
+ having_clause = None
458
+ if arg.having_clause:
459
+ having_clause = self.indent_lines(self.to_string(arg.having_clause))
460
+ order_by = None
461
+ if arg.order_by:
462
+ order_by = [
463
+ self.indent_lines(self.to_string(c)) for c in arg.order_by.items
464
+ ]
465
+
392
466
  return QUERY_TEMPLATE.render(
393
- select_columns=[self.to_string(c) for c in arg.selection],
394
- where=self.to_string(arg.where_clause) if arg.where_clause else None,
395
- having=self.to_string(arg.having_clause) if arg.having_clause else None,
396
- order_by=(
397
- [self.to_string(c) for c in arg.order_by.items]
398
- if arg.order_by
399
- else None
400
- ),
467
+ select_columns=select_columns,
468
+ where=where_clause,
469
+ having=having_clause,
470
+ order_by=order_by,
401
471
  limit=arg.limit,
402
472
  )
403
473
 
404
474
  @to_string.register
405
475
  def _(self, arg: MultiSelectStatement):
406
- base = "\nMERGE\n".join([self.to_string(select)[:-2] for select in arg.selects])
476
+ # Each select gets its own indentation
477
+ select_parts = []
478
+ for select in arg.selects:
479
+ select_parts.append(
480
+ self.to_string(select)[:-2]
481
+ ) # Remove the trailing ";\n"
482
+
483
+ base = "\nMERGE\n".join(select_parts)
407
484
  base += self.to_string(arg.align)
408
485
  if arg.where_clause:
409
486
  base += f"\nWHERE\n{self.to_string(arg.where_clause)}"
410
487
  if arg.order_by:
411
- base += f"\nORDER BY\n\t{self.to_string(arg.order_by)}"
488
+ base += f"\nORDER BY\n{self.to_string(arg.order_by)}"
412
489
  if arg.limit:
413
490
  base += f"\nLIMIT {arg.limit}"
414
491
  base += "\n;"
@@ -420,7 +497,9 @@ class Renderer:
420
497
 
421
498
  @to_string.register
422
499
  def _(self, arg: AlignClause):
423
- return "\nALIGN\n\t" + ",\n\t".join([self.to_string(c) for c in arg.items])
500
+ with self.indented():
501
+ align_items = [self.indent_lines(self.to_string(c)) for c in arg.items]
502
+ return "\nALIGN\n" + ",\n".join(align_items)
424
503
 
425
504
  @to_string.register
426
505
  def _(self, arg: AlignItem):
@@ -428,7 +507,13 @@ class Renderer:
428
507
 
429
508
  @to_string.register
430
509
  def _(self, arg: OrderBy):
431
- return ",\n".join([self.to_string(c) for c in arg.items])
510
+ with self.indented():
511
+ order_items = [self.indent_lines(self.to_string(c)) for c in arg.items]
512
+ return ",\n".join(order_items)
513
+
514
+ @to_string.register
515
+ def _(self, arg: Ordering):
516
+ return arg.value
432
517
 
433
518
  @to_string.register
434
519
  def _(self, arg: "WhereClause"):
@@ -439,7 +524,7 @@ class Renderer:
439
524
 
440
525
  @to_string.register
441
526
  def _(self, arg: "Conditional"):
442
- return f"({self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)})"
527
+ return f"{self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)}"
443
528
 
444
529
  @to_string.register
445
530
  def _(self, arg: "SubselectComparison"):
@@ -451,7 +536,8 @@ class Renderer:
451
536
 
452
537
  @to_string.register
453
538
  def _(self, arg: "Comment"):
454
- return f"{arg.text}"
539
+ lines = "\n#".join(arg.text.split("\n"))
540
+ return f"{lines}"
455
541
 
456
542
  @to_string.register
457
543
  def _(self, arg: "WindowItem"):
@@ -467,7 +553,6 @@ class Renderer:
467
553
 
468
554
  @to_string.register
469
555
  def _(self, arg: "FilterItem"):
470
-
471
556
  return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
472
557
 
473
558
  @to_string.register
@@ -536,18 +621,34 @@ class Renderer:
536
621
  if len(args) == 1:
537
622
  return f"group({args[0]})"
538
623
  return f"group({args[0]}) by {arg_string}"
539
- inputs = ",".join(args)
540
624
 
541
625
  if arg.operator == FunctionType.CONSTANT:
542
- return f"{inputs}"
626
+ return f"{', '.join(args)}"
543
627
  if arg.operator == FunctionType.CAST:
544
628
  return f"CAST({self.to_string(arg.arguments[0])} AS {self.to_string(arg.arguments[1])})"
545
629
  if arg.operator == FunctionType.INDEX_ACCESS:
546
630
  return f"{self.to_string(arg.arguments[0])}[{self.to_string(arg.arguments[1])}]"
547
631
 
548
632
  if arg.operator == FunctionType.CASE:
549
- inputs = "\n\t".join(args)
550
- return f"CASE\n\t{inputs}\nEND"
633
+ with self.indented():
634
+ indented_args = [
635
+ self.indent_lines(self.to_string(a)) for a in arg.arguments
636
+ ]
637
+ inputs = "\n".join(indented_args)
638
+ return f"CASE\n{inputs}\n{self.indent_context.current_indent}END"
639
+
640
+ if arg.operator == FunctionType.STRUCT:
641
+ # zip arguments to pairs
642
+ input_pairs = zip(arg.arguments[0::2], arg.arguments[1::2])
643
+ with self.indented():
644
+ pair_strings = []
645
+ for k, v in input_pairs:
646
+ pair_line = f"{self.to_string(k)}-> {v}"
647
+ pair_strings.append(self.indent_lines(pair_line))
648
+ inputs = ",\n".join(pair_strings)
649
+ return f"struct(\n{inputs}\n{self.indent_context.current_indent})"
650
+
651
+ inputs = ",".join(args)
551
652
  return f"{arg.operator.value}({inputs})"
552
653
 
553
654
  @to_string.register
@@ -134,10 +134,14 @@
134
134
  metadata: "metadata" "(" IDENTIFIER "=" string_lit ")"
135
135
 
136
136
  limit: "LIMIT"i /[0-9]+/
137
+
138
+ _order_atom: expr ordering
139
+
140
+ order_list: _order_atom ("," _order_atom)* ","?
137
141
 
138
- order_list: expr ordering ("," expr ordering)* ","?
142
+ over_component: /,\s*[a-zA-Z\_][a-zA-Z0-9\_\.]*/ "END"?
139
143
 
140
- over_list: concept_lit ("," concept_lit )* ","?
144
+ over_list: concept_lit over_component*
141
145
 
142
146
  ORDERING_DIRECTION: /ASC|DESC/i
143
147
 
@@ -433,7 +437,8 @@
433
437
  map_lit: "{" (literal ":" literal ",")* literal ":" literal ","? "}"
434
438
 
435
439
  _STRUCT.1: "struct("i
436
- struct_lit: _STRUCT (IDENTIFIER "->" expr ",")* IDENTIFIER "->" expr ","? ")"
440
+ _BINDING.1: "->"
441
+ struct_lit: _STRUCT expr _BINDING IDENTIFIER ( "," expr _BINDING IDENTIFIER )* ","? ")"
437
442
 
438
443
  !bool_lit: "True"i | "False"i
439
444
 
trilogy/std/display.preql CHANGED
@@ -2,5 +2,8 @@
2
2
 
3
3
  type percent float; # Percentage value
4
4
 
5
- def calc_percent(a, b, digits=-1) -> case when digits =-1 then (a/b):: float::percent
6
- else round((a/b):: float::percent, digits) end;
5
+ def calc_percent(a, b, digits=-1) -> case when digits =-1 then
6
+ case when b = 0 then 0.0::float::percent else
7
+ (a/b)::float::percent end
8
+ else round((case when b = 0 then 0.0::float::percent else
9
+ (a/b)::float::percent end):: float::percent, digits) end;