pytrilogy 0.0.3.101__py3-none-any.whl → 0.0.3.103__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.101
3
+ Version: 0.0.3.103
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -19,6 +19,7 @@ Requires-Dist: sqlalchemy<2.0.0
19
19
  Requires-Dist: networkx
20
20
  Requires-Dist: pyodbc
21
21
  Requires-Dist: pydantic
22
+ Requires-Dist: duckdb<1.4.0
22
23
  Requires-Dist: duckdb-engine
23
24
  Requires-Dist: click
24
25
  Provides-Extra: postgres
@@ -1,5 +1,5 @@
1
- pytrilogy-0.0.3.101.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=NMiEE_jE99ZiREk8IPjfT2M-jxAwtmd2vyCWVD3kT28,304
1
+ pytrilogy-0.0.3.103.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=lBanLP2CsDaPdJvJ3K68ncf8sO3sMfgsapECGG5J4fk,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
@@ -18,24 +18,25 @@ trilogy/core/exceptions.py,sha256=axkVXYJYQXCCwMHwlyDA232g4tCOwdCZUt7eHeUMDMg,28
18
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
- trilogy/core/optimization.py,sha256=ojpn-p79lr03SSVQbbw74iPCyoYpDYBmj1dbZ3oXCjI,8860
21
+ trilogy/core/optimization.py,sha256=Km0ITEx9n6Iv5ReX6tm4uXO5uniSv_ooahycNNiET3g,9212
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
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
32
- trilogy/core/optimizations/__init__.py,sha256=YH2-mGXZnVDnBcWVi8vTbrdw7Qs5TivG4h38rH3js_I,290
32
+ trilogy/core/optimizations/__init__.py,sha256=yspWc25M5SgAuvXYoSt5J8atyPbDlOfsKjIo5yGD9s4,368
33
33
  trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
34
+ trilogy/core/optimizations/hide_unused_concept.py,sha256=DbsP8NqQOxmPv9omDOoFNPUGObUkqsRRNrr5d1xDxx4,1962
34
35
  trilogy/core/optimizations/inline_datasource.py,sha256=2sWNRpoRInnTgo9wExVT_r9RfLAQHI57reEV5cGHUcg,4329
35
36
  trilogy/core/optimizations/predicate_pushdown.py,sha256=g4AYE8Aw_iMlAh68TjNXGP754NTurrDduFECkUjoBnc,9399
36
37
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
38
  trilogy/core/processing/concept_strategies_v3.py,sha256=tvSN_aiqb1H7LkTl96vj7YK_DKcq_1nDdRJ69wZCLc8,22158
38
- trilogy/core/processing/discovery_node_factory.py,sha256=5QVYUsci_h6iYWhS0GCoDow2tSAipiBW1OyTRX-g_L8,15581
39
+ trilogy/core/processing/discovery_node_factory.py,sha256=r1JAnVhnB9YHEB1TW3racNH9mJvXjKRPZjzZrXsuiqg,15348
39
40
  trilogy/core/processing/discovery_utility.py,sha256=Xntgug6VnEF96uw5Zwen1qMEUwKjqrm_ZDUr4i4tc1U,5595
40
41
  trilogy/core/processing/discovery_validation.py,sha256=eZ4HfHMpqZLI8MGG2jez8arS8THs6ceuVrQFIY6gXrU,5364
41
42
  trilogy/core/processing/graph_utils.py,sha256=8QUVrkE9j-9C1AyrCb1nQEh8daCe0u1HuXl-Te85lag,1205
@@ -50,17 +51,17 @@ trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZ
50
51
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
51
52
  trilogy/core/processing/node_generators/node_merge_node.py,sha256=1joMV7XpQ9Gpe-d5y7JUMBHIqakV5wFJi3Mtvs4UcL4,23415
52
53
  trilogy/core/processing/node_generators/recursive_node.py,sha256=l5zdh0dURKwmAy8kK4OpMtZfyUEQRk6N-PwSWIyBpSM,2468
53
- trilogy/core/processing/node_generators/rowset_node.py,sha256=5L5u6xz1In8EaHQdcYgR2si-tz9WB9YLXURo4AkUT9A,6630
54
+ trilogy/core/processing/node_generators/rowset_node.py,sha256=T11Rqj-tsfubjFvBO0rzIVxtv9tOwwKXjGyut0r9xIY,5919
54
55
  trilogy/core/processing/node_generators/select_merge_node.py,sha256=KQvGoNT5ZBWQ_caEomRTtG1PKZC7OPT4PKfY0QmwMGE,22270
55
56
  trilogy/core/processing/node_generators/select_node.py,sha256=Ta1G39V94gjX_AgyZDz9OqnwLz4BjY3D6Drx9YpziMQ,3555
56
57
  trilogy/core/processing/node_generators/synonym_node.py,sha256=AnAsa_Wj50NJ_IK0HSgab_7klYmKVrv0WI1uUe-GvEY,3766
57
- trilogy/core/processing/node_generators/union_node.py,sha256=VNo6Oey4p8etU9xrOh2oTT2lIOTvY6PULUPRvVa2uxU,2877
58
- trilogy/core/processing/node_generators/unnest_node.py,sha256=w9vhPzASz53QPASLqFcLDdR9eY132tgVUcp3QolD5Jw,3726
58
+ trilogy/core/processing/node_generators/union_node.py,sha256=NxQbnRRoYMI4WjMeph41yk4E6yipj53qdGuNt-Mozxw,2818
59
+ trilogy/core/processing/node_generators/unnest_node.py,sha256=7uOZzBidEEKeZE0VW_XlgHGhEYf_snEHtV8UgJ_ZjyY,4048
59
60
  trilogy/core/processing/node_generators/window_node.py,sha256=A90linr4pkZtTNfn9k2YNLqrJ_SFII3lbHxB-BC6mI8,6688
60
61
  trilogy/core/processing/node_generators/select_helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
62
  trilogy/core/processing/node_generators/select_helpers/datasource_injection.py,sha256=m2YQ4OmG0N2O61a7NEq1ZzbTa7JsCC00lxB2ymjcYRI,8224
62
63
  trilogy/core/processing/nodes/__init__.py,sha256=zTge1EzwzEydlcMliIFO_TT7h7lS8l37lyZuQDir1h0,5487
63
- trilogy/core/processing/nodes/base_node.py,sha256=C_CjlOzlGMXckyV0b_PJZerpopNesRCKfambMq7Asvc,18221
64
+ trilogy/core/processing/nodes/base_node.py,sha256=TQZLEz_xfXpdVyFa9R5BwvikH1OqzJUioOPw8vTETWc,18144
64
65
  trilogy/core/processing/nodes/filter_node.py,sha256=5VtRfKbCORx0dV-vQfgy3gOEkmmscL9f31ExvlODwvY,2461
65
66
  trilogy/core/processing/nodes/group_node.py,sha256=njz-5T7OJ3-kaBC7EhdtPra3G77HnI7apjUwMGhUeXo,10569
66
67
  trilogy/core/processing/nodes/merge_node.py,sha256=daJywBxh44Gqk-7eTiXbYtY7xo6O6fNvqX-DagTOTmE,16231
@@ -81,9 +82,9 @@ trilogy/core/validation/datasource.py,sha256=nJeEFyb6iMBwlEVdYVy1vLzAbdRZwOsUjGx
81
82
  trilogy/core/validation/environment.py,sha256=ymvhQyt7jLK641JAAIQkqjQaAmr9C5022ILzYvDgPP0,2835
82
83
  trilogy/core/validation/fix.py,sha256=Z818UFNLxndMTLiyhB3doLxIfnOZ-16QGvVFWuD7UsA,3750
83
84
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
84
- trilogy/dialect/base.py,sha256=d2gXfa5Jh3uyN9H9MxG53JT-xQQgntq2X7EprobJYUc,49698
85
+ trilogy/dialect/base.py,sha256=mgARj-aldkFAqdwps_25da03NLIAxU6Xg9Jq_VcOtp0,50181
85
86
  trilogy/dialect/bigquery.py,sha256=XS3hpybeowgfrOrkycAigAF3NX2YUzTzfgE6f__2fT4,4316
86
- trilogy/dialect/common.py,sha256=_MarnMWRBn3VcNt3k5VUdFrwH6oHzGdNQquSpHNLq4o,5644
87
+ trilogy/dialect/common.py,sha256=cUI7JMmpG_A5KcaxRI-GoyqwLMD6jTf0JJhgcOdwQK4,5833
87
88
  trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
88
89
  trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
89
90
  trilogy/dialect/duckdb.py,sha256=JoUvQ19WvgxoaJkGLM7DPXOd1H0394k3vBiblksQzOI,5631
@@ -99,26 +100,26 @@ trilogy/hooks/graph_hook.py,sha256=5BfR7Dt0bgEsCLgwjowgCsVkboGYfVJGOz8g9mqpnos,4
99
100
  trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
100
101
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
101
102
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
102
- trilogy/parsing/common.py,sha256=550-L0444GUuBFdiDWkOg_DxnMXtcJFUMES2R5zlwik,31026
103
+ trilogy/parsing/common.py,sha256=NJLm31J3W9BLWq1ClhNvYE43jrF950698KJ3o0UfSCo,31340
103
104
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
104
105
  trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
105
106
  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=E8-R0zO40QoeTeVX9OYdi5e9YgRYtuRrezDRj7VOgds,20614
108
- trilogy/parsing/trilogy.lark,sha256=2-jguxgJQnNLbODjTijqrXXzFZ_UlivTdiYhec2YWuc,16451
107
+ trilogy/parsing/parse_engine.py,sha256=T-3Q4UH256IB6cfX85crScZwZ6gAwslgv0fy3WKBdjc,81930
108
+ trilogy/parsing/render.py,sha256=IklKMdXiqQEB6D28PrU1BewlDwD88Hnmqn1xjA9h720,23863
109
+ trilogy/parsing/trilogy.lark,sha256=6eBDD6d4D9N1Nnn4CtmaoB-NpOpjHrEn5oi0JykAlbE,16509
109
110
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
110
111
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
111
112
  trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
112
113
  trilogy/std/date.preql,sha256=HWZm4t4HWyxr5geWRsY05RnHBVDMci8z8YA2cu0-OOw,188
113
- trilogy/std/display.preql,sha256=nm7lox87Xf6lBvXCVCS6x2HskguMKzndEBucJ5pktzk,175
114
+ trilogy/std/display.preql,sha256=S20HW8qbShBc4OZPcHYiRlLdcaBp9dwruozWBoXKscs,293
114
115
  trilogy/std/geography.preql,sha256=1A9Sq5PPMBnEPPf7f-rPVYxJfsnWpQ8oV_k4Fm3H2dU,675
115
116
  trilogy/std/metric.preql,sha256=DRECGhkMyqfit5Fl4Ut9zbWrJuSMI1iO9HikuyoBpE0,421
116
117
  trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
117
118
  trilogy/std/net.preql,sha256=WZCuvH87_rZntZiuGJMmBDMVKkdhTtxeHOkrXNwJ1EE,416
118
119
  trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
119
120
  trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
120
- pytrilogy-0.0.3.101.dist-info/METADATA,sha256=dkvyYmeCXSZl2uHkPpoy-R7HdKb2w7pLGFrDu1tRGEU,11811
121
- pytrilogy-0.0.3.101.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
122
- pytrilogy-0.0.3.101.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
123
- pytrilogy-0.0.3.101.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
124
- pytrilogy-0.0.3.101.dist-info/RECORD,,
121
+ pytrilogy-0.0.3.103.dist-info/METADATA,sha256=RnMfz8EH2sCtqHEDAraYhAb_V7oPbovtuI3PsL2F3Ms,11839
122
+ pytrilogy-0.0.3.103.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
123
+ pytrilogy-0.0.3.103.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
124
+ pytrilogy-0.0.3.103.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
125
+ pytrilogy-0.0.3.103.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.101"
7
+ __version__ = "0.0.3.103"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -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:
@@ -5,6 +5,7 @@ from trilogy.core.models.build import (
5
5
  )
6
6
  from trilogy.core.models.execute import CTE, RecursiveCTE, UnionCTE
7
7
  from trilogy.core.optimizations import (
8
+ HideUnusedConcepts,
8
9
  InlineDatasource,
9
10
  OptimizationRule,
10
11
  PredicatePushdown,
@@ -84,11 +85,18 @@ def filter_irrelevant_ctes(
84
85
  # child.existence_source_map[x2].append(parent.name)
85
86
  # else:
86
87
  relevant_ctes.add(cte.name)
87
- for cte in cte.parent_ctes:
88
- recurse(cte, inverse_map)
88
+
89
+ for parent in cte.parent_ctes:
90
+ if parent.name in relevant_ctes:
91
+ logger.info(
92
+ f"[Optimization][Irrelevent CTE filtering] Already visited {parent.name} when visting {cte.name}, potential recursive dag"
93
+ )
94
+ continue
95
+
96
+ recurse(parent, inverse_map)
89
97
  if isinstance(cte, UnionCTE):
90
- for cte in cte.internal_ctes:
91
- recurse(cte, inverse_map)
98
+ for internal in cte.internal_ctes:
99
+ recurse(internal, inverse_map)
92
100
 
93
101
  inverse_map = gen_inverse_map(input)
94
102
  recurse(root_cte, inverse_map)
@@ -220,6 +228,7 @@ def optimize_ctes(
220
228
  REGISTERED_RULES.append(PredicatePushdown())
221
229
  if CONFIG.optimizations.predicate_pushdown:
222
230
  REGISTERED_RULES.append(PredicatePushdownRemove())
231
+ REGISTERED_RULES.append(HideUnusedConcepts())
223
232
  for rule in REGISTERED_RULES:
224
233
  loops = 0
225
234
  complete = False
@@ -1,4 +1,5 @@
1
1
  from .base_optimization import OptimizationRule
2
+ from .hide_unused_concept import HideUnusedConcepts
2
3
  from .inline_datasource import InlineDatasource
3
4
  from .predicate_pushdown import PredicatePushdown, PredicatePushdownRemove
4
5
 
@@ -7,4 +8,5 @@ __all__ = [
7
8
  "InlineDatasource",
8
9
  "PredicatePushdown",
9
10
  "PredicatePushdownRemove",
11
+ "HideUnusedConcepts",
10
12
  ]
@@ -0,0 +1,51 @@
1
+ from trilogy.core.models.build import (
2
+ BuildConcept,
3
+ )
4
+ from trilogy.core.models.execute import CTE, UnionCTE
5
+ from trilogy.core.optimizations.base_optimization import OptimizationRule
6
+
7
+
8
+ class HideUnusedConcepts(OptimizationRule):
9
+ def __init__(self, *args, **kwargs) -> None:
10
+ super().__init__(*args, **kwargs)
11
+
12
+ def optimize(
13
+ self, cte: CTE | UnionCTE, inverse_map: dict[str, list[CTE | UnionCTE]]
14
+ ) -> bool:
15
+ used = set()
16
+ from trilogy.dialect.base import BaseDialect
17
+
18
+ renderer = BaseDialect()
19
+ children = inverse_map.get(cte.name, [])
20
+ if not children:
21
+ return False
22
+ for v in children:
23
+ self.log(f"Analyzing usage of {cte.name} in {v.name}")
24
+ renderer.render_cte(v)
25
+ used = renderer.used_map.get(cte.name, set())
26
+ self.log(f"Used concepts for {cte.name}: {used} from {renderer.used_map}")
27
+ add_to_hidden: list[BuildConcept] = []
28
+ for concept in cte.output_columns:
29
+ if concept.address not in used:
30
+ add_to_hidden.append(concept)
31
+ newly_hidden = [
32
+ x.address for x in add_to_hidden if x.address not in cte.hidden_concepts
33
+ ]
34
+ non_hidden = [
35
+ x for x in cte.output_columns if x.address not in cte.hidden_concepts
36
+ ]
37
+ if not newly_hidden or len(non_hidden) <= 1:
38
+ return False
39
+ self.log(
40
+ f"Hiding unused concepts {[x.address for x in add_to_hidden]} from {cte.name} (used: {used}, all: {[x.address for x in cte.output_columns]})"
41
+ )
42
+ candidates = [
43
+ x.address
44
+ for x in cte.output_columns
45
+ if x.address not in used and x.address not in cte.hidden_concepts
46
+ ]
47
+ if len(candidates) == len(set([x.address for x in cte.output_columns])):
48
+ # pop one out
49
+ candidates.pop()
50
+ cte.hidden_concepts = set(candidates)
51
+ return True
@@ -376,11 +376,6 @@ class RootNodeHandler:
376
376
 
377
377
  if pseudonyms:
378
378
  expanded.add_output_concepts(pseudonyms)
379
- logger.info(
380
- f"{depth_to_prefix(self.ctx.depth)}{LOGGER_PREFIX} "
381
- f"Hiding pseudonyms {[c.address for c in pseudonyms]}"
382
- )
383
- expanded.hide_output_concepts(pseudonyms)
384
379
 
385
380
  logger.info(
386
381
  f"{depth_to_prefix(self.ctx.depth)}{LOGGER_PREFIX} "
@@ -64,14 +64,6 @@ def gen_rowset_node(
64
64
  v for v in concept_pool if v.address in rowset_outputs
65
65
  ]
66
66
 
67
- select_hidden = node.hidden_concepts
68
- rowset_hidden = [
69
- x
70
- for x in rowset_relevant
71
- if x.address in lineage.rowset.derived_concepts
72
- and isinstance(x.lineage, BuildRowsetItem)
73
- and x.lineage.content.address in select_hidden
74
- ]
75
67
  additional_relevant = [
76
68
  factory.build(x) for x in select.output_components if x.address in enrichment
77
69
  ]
@@ -84,18 +76,6 @@ def gen_rowset_node(
84
76
  )
85
77
  node.partial_concepts.append(item)
86
78
 
87
- final_hidden = rowset_hidden + [
88
- x
89
- for x in node.output_concepts
90
- if x.address not in local_optional + [concept]
91
- and x.derivation != Derivation.ROWSET
92
- and not any(z in lineage.rowset.derived_concepts for z in x.pseudonyms)
93
- ]
94
- logger.info(
95
- f"{padding(depth)}{LOGGER_PREFIX} hiding {final_hidden} local optional {local_optional}"
96
- )
97
- node.hide_output_concepts(final_hidden)
98
-
99
79
  node.grain = BuildGrain.from_concepts(
100
80
  [
101
81
  x
@@ -74,7 +74,7 @@ def gen_union_node(
74
74
  history=history,
75
75
  conditions=conditions,
76
76
  )
77
- parent.hide_output_concepts(parent.output_concepts)
77
+
78
78
  parent.add_output_concepts(resolved)
79
79
  parent_nodes.append(parent)
80
80
  if not parent:
@@ -1,9 +1,19 @@
1
1
  from typing import List
2
2
 
3
3
  from trilogy.constants import logger
4
- from trilogy.core.models.build import BuildConcept, BuildFunction, BuildWhereClause
4
+ from trilogy.core.models.build import (
5
+ BuildConcept,
6
+ BuildFunction,
7
+ BuildGrain,
8
+ BuildWhereClause,
9
+ )
5
10
  from trilogy.core.models.build_environment import BuildEnvironment
6
- from trilogy.core.processing.nodes import History, StrategyNode, UnnestNode
11
+ from trilogy.core.processing.nodes import (
12
+ History,
13
+ StrategyNode,
14
+ UnnestNode,
15
+ WhereSafetyNode,
16
+ )
7
17
  from trilogy.core.processing.utility import padding
8
18
 
9
19
  LOGGER_PREFIX = "[GEN_UNNEST_NODE]"
@@ -71,7 +81,9 @@ def gen_unnest_node(
71
81
  return None
72
82
  else:
73
83
  parent = None
74
-
84
+ logger.info(
85
+ f"{depth_prefix}{LOGGER_PREFIX} unnest node for {concept} got parent {parent}"
86
+ )
75
87
  base = UnnestNode(
76
88
  unnest_concepts=[concept] + equivalent_optional,
77
89
  input_concepts=arguments + non_equivalent_optional,
@@ -83,7 +95,7 @@ def gen_unnest_node(
83
95
  # as unnest operations are not valid in all situations
84
96
  # TODO: inline this node when we can detect it's safe
85
97
  conditional = conditions.conditional if conditions else None
86
- new = StrategyNode(
98
+ new = WhereSafetyNode(
87
99
  input_concepts=base.output_concepts,
88
100
  output_concepts=base.output_concepts,
89
101
  environment=environment,
@@ -92,9 +104,13 @@ def gen_unnest_node(
92
104
  preexisting_conditions=(
93
105
  conditional if conditional and local_conditions is False else None
94
106
  ),
107
+ grain=BuildGrain.from_concepts(
108
+ concepts=base.output_concepts,
109
+ environment=environment,
110
+ ),
95
111
  )
96
- qds = new.resolve()
97
- assert qds.source_map[concept.address] == {base.resolve()}
98
- for x in equivalent_optional:
99
- assert qds.source_map[x.address] == {base.resolve()}
112
+ # qds = new.resolve()
113
+ # assert qds.source_map[concept.address] == {base.resolve()}
114
+ # for x in equivalent_optional:
115
+ # assert qds.source_map[x.address] == {base.resolve()}
100
116
  return new
@@ -489,7 +489,6 @@ class WhereSafetyNode(StrategyNode):
489
489
  parent = parent.copy()
490
490
  # avoid performance hit by not rebuilding until end
491
491
  parent.set_output_concepts(self.output_concepts, rebuild=False)
492
- parent.hide_output_concepts(self.hidden_concepts, rebuild=False)
493
492
 
494
493
  # these conditions
495
494
  if self.preexisting_conditions:
trilogy/dialect/base.py CHANGED
@@ -1,3 +1,4 @@
1
+ from collections import defaultdict
1
2
  from datetime import date, datetime
2
3
  from typing import Any, Callable, Dict, List, Optional, Sequence, Union
3
4
 
@@ -311,6 +312,7 @@ def safe_get_cte_value(
311
312
  c: BuildConcept,
312
313
  quote_char: str,
313
314
  render_expr: Callable,
315
+ use_map: dict[str, set[str]],
314
316
  ) -> Optional[str]:
315
317
  address = c.address
316
318
  raw = cte.source_map.get(address, None)
@@ -319,13 +321,17 @@ def safe_get_cte_value(
319
321
  return None
320
322
  if isinstance(raw, str):
321
323
  rendered = cte.get_alias(c, raw)
324
+ use_map[raw].add(c.address)
322
325
  return f"{quote_char}{raw}{quote_char}.{safe_quote(rendered, quote_char)}"
323
326
  if isinstance(raw, list) and len(raw) == 1:
324
327
  rendered = cte.get_alias(c, raw[0])
325
328
  if isinstance(rendered, FUNCTION_ITEMS):
326
329
  # if it's a function, we need to render it as a function
327
330
  return f"{render_expr(rendered, cte=cte, raise_invalid=True)}"
331
+ use_map[raw[0]].add(c.address)
328
332
  return f"{quote_char}{raw[0]}{quote_char}.{safe_quote(rendered, quote_char)}"
333
+ for x in raw:
334
+ use_map[x].add(c.address)
329
335
  return coalesce(
330
336
  sorted(
331
337
  [
@@ -350,13 +356,12 @@ class BaseDialect:
350
356
 
351
357
  def __init__(self, rendering: Rendering | None = None):
352
358
  self.rendering = rendering or CONFIG.rendering
359
+ self.used_map: dict[str, set[str]] = defaultdict(set)
353
360
 
354
361
  def render_order_item(
355
362
  self,
356
363
  order_item: BuildOrderItem,
357
364
  cte: CTE | UnionCTE,
358
- final: bool = False,
359
- alias: bool = True,
360
365
  ) -> str:
361
366
  # if final:
362
367
  # if not alias:
@@ -527,6 +532,9 @@ class BaseDialect:
527
532
  )
528
533
 
529
534
  raw_content = cte.get_alias(c)
535
+ parent = cte.source_map.get(c.address, None)
536
+ if parent:
537
+ self.used_map[parent[0]].add(c.address)
530
538
  if isinstance(raw_content, RawColumnExpr):
531
539
  rval = raw_content.text
532
540
  elif isinstance(raw_content, FUNCTION_ITEMS):
@@ -540,6 +548,7 @@ class BaseDialect:
540
548
  c,
541
549
  self.QUOTE_CHARACTER,
542
550
  self.render_expr,
551
+ self.used_map,
543
552
  )
544
553
  if not rval:
545
554
  # unions won't have a specific source mapped; just use a generic column reference
@@ -615,6 +624,7 @@ class BaseDialect:
615
624
  lookup_cte = cte
616
625
  if cte_map and not lookup_cte:
617
626
  lookup_cte = cte_map.get(e.right.address)
627
+
618
628
  assert lookup_cte, "Subselects must be rendered with a CTE in context"
619
629
  if e.right.address not in lookup_cte.existence_source_map:
620
630
  lookup = lookup_cte.source_map.get(
@@ -634,6 +644,7 @@ class BaseDialect:
634
644
  f"Missing source CTE for {e.right.address}"
635
645
  )
636
646
  assert cte, "CTE must be provided for inlined CTEs"
647
+ self.used_map[target].add(e.right.address)
637
648
  if target in cte.inlined_ctes:
638
649
  info = cte.inlined_ctes[target]
639
650
  return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} (select {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} from {info.new_base} as {target} where {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} is not null)"
@@ -738,6 +749,7 @@ class BaseDialect:
738
749
  raise_invalid=raise_invalid,
739
750
  )
740
751
  elif cte_map:
752
+ self.used_map[cte_map[e.address].name].add(e.address)
741
753
  return f"{cte_map[e.address].name}.{self.QUOTE_CHARACTER}{e.safe_address}{self.QUOTE_CHARACTER}"
742
754
  return f"{self.QUOTE_CHARACTER}{e.safe_address}{self.QUOTE_CHARACTER}"
743
755
  elif isinstance(e, bool):
@@ -822,10 +834,7 @@ class BaseDialect:
822
834
  )
823
835
  if cte.order_by:
824
836
 
825
- ordering = [
826
- self.render_order_item(i, cte, final=True, alias=False)
827
- for i in cte.order_by.items
828
- ]
837
+ ordering = [self.render_order_item(i, cte) for i in cte.order_by.items]
829
838
  base_statement += "\nORDER BY " + ",".join(ordering)
830
839
  return CompiledCTE(name=cte.name, statement=base_statement)
831
840
  elif isinstance(cte, RecursiveCTE):
@@ -950,7 +959,8 @@ class BaseDialect:
950
959
  self.QUOTE_CHARACTER,
951
960
  self.render_expr,
952
961
  cte,
953
- self.UNNEST_MODE,
962
+ use_map=self.used_map,
963
+ unnest_mode=self.UNNEST_MODE,
954
964
  )
955
965
  for join in final_joins
956
966
  ]
trilogy/dialect/common.py CHANGED
@@ -62,10 +62,12 @@ def render_join_concept(
62
62
  concept: BuildConcept,
63
63
  render_expr,
64
64
  inlined_ctes: set[str],
65
+ use_map: dict[str, set[str]],
65
66
  ):
66
67
  if cte.name in inlined_ctes:
67
68
  base = render_expr(concept, cte)
68
69
  return base
70
+ use_map[name].add(concept.address)
69
71
  return f"{quote_character}{name}{quote_character}.{quote_character}{concept.safe_address}{quote_character}"
70
72
 
71
73
 
@@ -85,6 +87,7 @@ def render_join(
85
87
  str,
86
88
  ],
87
89
  cte: CTE,
90
+ use_map: dict[str, set[str]],
88
91
  unnest_mode: UnnestMode = UnnestMode.CROSS_APPLY,
89
92
  ) -> str | None:
90
93
  # {% for key in join.joinkeys %}{{ key.inner }} = {{ key.outer}}{% endfor %}
@@ -121,6 +124,7 @@ def render_join(
121
124
  pair.left,
122
125
  render_expr_func,
123
126
  join.inlined_ctes,
127
+ use_map=use_map,
124
128
  ),
125
129
  render_join_concept(
126
130
  right_name,
@@ -129,6 +133,7 @@ def render_join(
129
133
  pair.right,
130
134
  render_expr_func,
131
135
  join.inlined_ctes,
136
+ use_map=use_map,
132
137
  ),
133
138
  modifiers=pair.modifiers
134
139
  + (pair.left.modifiers or [])
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
 
@@ -390,26 +447,45 @@ class Renderer:
390
447
 
391
448
  @to_string.register
392
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
+
393
466
  return QUERY_TEMPLATE.render(
394
- select_columns=[self.to_string(c) for c in arg.selection],
395
- where=self.to_string(arg.where_clause) if arg.where_clause else None,
396
- having=self.to_string(arg.having_clause) if arg.having_clause else None,
397
- order_by=(
398
- [self.to_string(c) for c in arg.order_by.items]
399
- if arg.order_by
400
- else None
401
- ),
467
+ select_columns=select_columns,
468
+ where=where_clause,
469
+ having=having_clause,
470
+ order_by=order_by,
402
471
  limit=arg.limit,
403
472
  )
404
473
 
405
474
  @to_string.register
406
475
  def _(self, arg: MultiSelectStatement):
407
- 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)
408
484
  base += self.to_string(arg.align)
409
485
  if arg.where_clause:
410
486
  base += f"\nWHERE\n{self.to_string(arg.where_clause)}"
411
487
  if arg.order_by:
412
- base += f"\nORDER BY\n\t{self.to_string(arg.order_by)}"
488
+ base += f"\nORDER BY\n{self.to_string(arg.order_by)}"
413
489
  if arg.limit:
414
490
  base += f"\nLIMIT {arg.limit}"
415
491
  base += "\n;"
@@ -421,7 +497,9 @@ class Renderer:
421
497
 
422
498
  @to_string.register
423
499
  def _(self, arg: AlignClause):
424
- 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)
425
503
 
426
504
  @to_string.register
427
505
  def _(self, arg: AlignItem):
@@ -429,7 +507,13 @@ class Renderer:
429
507
 
430
508
  @to_string.register
431
509
  def _(self, arg: OrderBy):
432
- 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
433
517
 
434
518
  @to_string.register
435
519
  def _(self, arg: "WhereClause"):
@@ -469,7 +553,6 @@ class Renderer:
469
553
 
470
554
  @to_string.register
471
555
  def _(self, arg: "FilterItem"):
472
-
473
556
  return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
474
557
 
475
558
  @to_string.register
@@ -538,18 +621,34 @@ class Renderer:
538
621
  if len(args) == 1:
539
622
  return f"group({args[0]})"
540
623
  return f"group({args[0]}) by {arg_string}"
541
- inputs = ",".join(args)
542
624
 
543
625
  if arg.operator == FunctionType.CONSTANT:
544
- return f"{inputs}"
626
+ return f"{', '.join(args)}"
545
627
  if arg.operator == FunctionType.CAST:
546
628
  return f"CAST({self.to_string(arg.arguments[0])} AS {self.to_string(arg.arguments[1])})"
547
629
  if arg.operator == FunctionType.INDEX_ACCESS:
548
630
  return f"{self.to_string(arg.arguments[0])}[{self.to_string(arg.arguments[1])}]"
549
631
 
550
632
  if arg.operator == FunctionType.CASE:
551
- inputs = "\n\t".join(args)
552
- 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)
553
652
  return f"{arg.operator.value}({inputs})"
554
653
 
555
654
  @to_string.register
@@ -139,7 +139,9 @@
139
139
 
140
140
  order_list: _order_atom ("," _order_atom)* ","?
141
141
 
142
- over_list: (concept_lit ",")* concept_lit ","?
142
+ over_component: /,\s*[a-zA-Z\_][a-zA-Z0-9\_\.]*/ "END"?
143
+
144
+ over_list: concept_lit over_component*
143
145
 
144
146
  ORDERING_DIRECTION: /ASC|DESC/i
145
147
 
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;