pytrilogy 0.0.3.101__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.101
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.101.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=NMiEE_jE99ZiREk8IPjfT2M-jxAwtmd2vyCWVD3kT28,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
@@ -22,8 +22,8 @@ trilogy/core/optimization.py,sha256=ojpn-p79lr03SSVQbbw74iPCyoYpDYBmj1dbZ3oXCjI,
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
@@ -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=E8-R0zO40QoeTeVX9OYdi5e9YgRYtuRrezDRj7VOgds,20614
108
- trilogy/parsing/trilogy.lark,sha256=2-jguxgJQnNLbODjTijqrXXzFZ_UlivTdiYhec2YWuc,16451
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.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,,
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.101"
7
+ __version__ = "0.0.3.102"
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:
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;