pytrilogy 0.0.1.111__py3-none-any.whl → 0.0.1.113__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.1.111
3
+ Version: 0.0.1.113
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=PNtNelxhMDftdgkjjOKNn49l5DhtOeAgkI93YI77r64,292
1
+ trilogy/__init__.py,sha256=GrjKB68K39dNQdfawYh4NFxVquqj3q7CeFeviCNNfHA,292
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  trilogy/constants.py,sha256=DJi3ESttmvqgy6fPRXiaQzqJVye6jYwf6XM89NHv0_M,735
4
4
  trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
@@ -8,7 +8,7 @@ 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=XSvq2yPxn9oJ18nhn7UERgIV1IXZDRiSWaGpvtU34eE,5416
11
+ trilogy/core/enums.py,sha256=9IzCv0Hzzr3OTJQWzg9yHV_b46Jvk47RNoZjdBi2Its,5678
12
12
  trilogy/core/env_processor.py,sha256=SU-jpaGfoWLe9sGTeQYG1qjVnwGQ7TwctmnJRlfzluc,1459
13
13
  trilogy/core/environment_helpers.py,sha256=mzBDHhdF9ssZ_-LY8CcaM_ddfJavkpRYrFImUd3cjXI,5972
14
14
  trilogy/core/ergonomics.py,sha256=w3gwXdgrxNHCuaRdyKg73t6F36tj-wIjQf47WZkHmJk,1465
@@ -16,8 +16,8 @@ trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,56
16
16
  trilogy/core/functions.py,sha256=zkRReytiotOBAW-a3Ri5eoejZDYTt2-7Op80ZxZxUmw,9129
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=EDtmcDKNBUBc--jIwWtk2vkQM2Q7heuZ0VH7JF_M32s,109985
20
- trilogy/core/optimization.py,sha256=B_EuAqHmJbuJiGyBfrC66FB_YPsGg-nbfnV8FjqfP6Q,9097
19
+ trilogy/core/models.py,sha256=bVwdsGCfTybJC85kZ8H6a5fjFb7JLCkveAfUMuIAmos,111494
20
+ trilogy/core/optimization.py,sha256=942MnGRzscAHcG9LsfMslIRRQBslbIiPHnAvJ3w8YRg,9157
21
21
  trilogy/core/query_processor.py,sha256=clIRJ6IcsqIVBPKFsxt8bqCLsLyajvAu02MUIcKQhTo,15713
22
22
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  trilogy/core/processing/concept_strategies_v3.py,sha256=MYrpNMidqvPOg123RekOcqVTjcj03i_538gBo0MzoWE,23432
@@ -45,7 +45,7 @@ trilogy/core/processing/nodes/select_node_v2.py,sha256=ERCflBFzKpD5SzweMevnJLyQn
45
45
  trilogy/core/processing/nodes/unnest_node.py,sha256=JFtm90IVM-46aCYkTNIaJah6v9ApAfonjVhcVM1HmDE,1903
46
46
  trilogy/core/processing/nodes/window_node.py,sha256=X7qxLUKd3tekjUUsmH_4vz5b-U89gMnGd04VBxuu2Ns,1280
47
47
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
- trilogy/dialect/base.py,sha256=ii9P_OO8BhKsQVAr9A13rhx_dzRZd4wxnkL-Ul5OS74,30398
48
+ trilogy/dialect/base.py,sha256=iy2jb43CvJ0xSozyNVLuJSCaZ4Uzb5o--2p2-ZKNzSM,30991
49
49
  trilogy/dialect/bigquery.py,sha256=9vxQn2BMv_oTGQSWQpoN5ho_OgqMWaHH9e-5vQVf44c,2906
50
50
  trilogy/dialect/common.py,sha256=zWrYmvevlXznocw9uGHmY5Ws1rp_kICm9zA_ulTe4eg,2165
51
51
  trilogy/dialect/config.py,sha256=tLVEMctaTDhUgARKXUNfHUcIolGaALkQ0RavUvXAY4w,2994
@@ -61,17 +61,17 @@ trilogy/hooks/graph_hook.py,sha256=i-Tv9sxZU0sMc-God8bLLz-nAg4-wYafogZtHaU8LXw,8
61
61
  trilogy/hooks/query_debugger.py,sha256=D2VJUcyvQrVJ8sT6FCMKR3NKTfVrgZQ7gly91avHHpw,4222
62
62
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
63
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
- trilogy/parsing/common.py,sha256=lz0IyVA8v-u-DGFgzkmdb4_00I--Kegmo9HNF7CrajI,5797
64
+ trilogy/parsing/common.py,sha256=iR3fiiZ7w8VJuUGrQ0v06XGDXov81f4z1ZlFnj6y40E,5804
65
65
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
66
66
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
67
67
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
68
- trilogy/parsing/parse_engine.py,sha256=Xxqyx0MLRWIcjU55jRao1XHEZ5SunhbZIPhJD9-urlE,65008
68
+ trilogy/parsing/parse_engine.py,sha256=A8sQVfVr5sGIgWvz_tPF0EQjxiQhF3UqOTkBDcOaSKg,56115
69
69
  trilogy/parsing/render.py,sha256=fxjpq2FZLgllw_d4cru-t_IXNPAz2DmYkT7v9ED0XRI,11540
70
70
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
71
  trilogy/scripts/trilogy.py,sha256=PHxvv6f2ODv0esyyhWxlARgra8dVhqQhYl0lTrSyVNo,3729
72
- pytrilogy-0.0.1.111.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
73
- pytrilogy-0.0.1.111.dist-info/METADATA,sha256=OoRPUSENsnE0Qd1-nOtrbHz2T-izBPb_hJV4jMvlIDw,7882
74
- pytrilogy-0.0.1.111.dist-info/WHEEL,sha256=rWxmBtp7hEUqVLOnTaDOPpR-cZpCDkzhhcBce-Zyd5k,91
75
- pytrilogy-0.0.1.111.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
76
- pytrilogy-0.0.1.111.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
77
- pytrilogy-0.0.1.111.dist-info/RECORD,,
72
+ pytrilogy-0.0.1.113.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
73
+ pytrilogy-0.0.1.113.dist-info/METADATA,sha256=DYWhK3uHuFanDIgs8TbEIO7IhpPVQqSyv4muG0vluWU,7882
74
+ pytrilogy-0.0.1.113.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
75
+ pytrilogy-0.0.1.113.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
76
+ pytrilogy-0.0.1.113.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
77
+ pytrilogy-0.0.1.113.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (71.0.4)
2
+ Generator: setuptools (71.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
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.1.111"
7
+ __version__ = "0.0.1.113"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/core/enums.py CHANGED
@@ -76,6 +76,12 @@ class JoinType(Enum):
76
76
  class Ordering(Enum):
77
77
  ASCENDING = "asc"
78
78
  DESCENDING = "desc"
79
+ ASC_NULLS_AUTO = "asc nulls auto"
80
+ ASC_NULLS_FIRST = "asc nulls first"
81
+ ASC_NULLS_LAST = "asc nulls last"
82
+ DESC_NULLS_FIRST = "desc nulls first"
83
+ DESC_NULLS_LAST = "desc nulls last"
84
+ DESC_NULLS_AUTO = "desc nulls auto"
79
85
 
80
86
 
81
87
  class WindowType(Enum):
@@ -220,6 +226,7 @@ class ComparisonOperator(Enum):
220
226
  ILIKE = "ilike"
221
227
  CONTAINS = "contains"
222
228
  ELSE = "else"
229
+ BETWEEN = "between"
223
230
 
224
231
  @classmethod
225
232
  def _missing_(cls, value):
trilogy/core/models.py CHANGED
@@ -812,6 +812,7 @@ class Function(Namespaced, SelectGrain, BaseModel):
812
812
  "Parenthetical",
813
813
  CaseWhen,
814
814
  "CaseElse",
815
+ list,
815
816
  ListWrapper[int],
816
817
  ListWrapper[str],
817
818
  ListWrapper[float],
@@ -996,6 +997,7 @@ class WindowItem(Namespaced, SelectGrain, BaseModel):
996
997
  content: Concept
997
998
  order_by: List["OrderItem"]
998
999
  over: List["Concept"] = Field(default_factory=list)
1000
+ index: Optional[int] = None
999
1001
 
1000
1002
  def with_namespace(self, namespace: str) -> "WindowItem":
1001
1003
  return WindowItem(
@@ -2041,6 +2043,14 @@ class CTE(BaseModel):
2041
2043
  ds_being_inlined = qds_being_inlined.datasources[0]
2042
2044
  if not isinstance(ds_being_inlined, Datasource):
2043
2045
  return False
2046
+ if any(
2047
+ [
2048
+ x.identifier == ds_being_inlined.identifier
2049
+ for x in self.source.datasources
2050
+ ]
2051
+ ):
2052
+ return False
2053
+
2044
2054
  self.source.datasources = [
2045
2055
  ds_being_inlined,
2046
2056
  *[
@@ -2727,6 +2737,14 @@ class Comparison(ConceptArgs, Namespaced, SelectGrain, BaseModel):
2727
2737
  raise SyntaxError(
2728
2738
  f"Cannot compare {self.left} and {self.right} of different types"
2729
2739
  )
2740
+ if self.operator == ComparisonOperator.BETWEEN:
2741
+ if (
2742
+ not isinstance(self.right, ComparisonOperator)
2743
+ and self.right.operator == BooleanOperator.AND
2744
+ ):
2745
+ raise SyntaxError(
2746
+ f"Between operator must have two operands with and, not {self.right}"
2747
+ )
2730
2748
 
2731
2749
  def __add__(self, other):
2732
2750
  if not isinstance(other, (Comparison, Conditional, Parenthetical)):
@@ -2802,6 +2820,29 @@ class Comparison(ConceptArgs, Namespaced, SelectGrain, BaseModel):
2802
2820
  output += get_concept_arguments(self.right)
2803
2821
  return output
2804
2822
 
2823
+ @property
2824
+ def row_arguments(self) -> List[Concept]:
2825
+ output = []
2826
+ if isinstance(self.left, ConceptArgs):
2827
+ output += self.left.row_arguments
2828
+ else:
2829
+ output += get_concept_arguments(self.left)
2830
+ if isinstance(self.right, ConceptArgs):
2831
+ output += self.right.row_arguments
2832
+ else:
2833
+ output += get_concept_arguments(self.right)
2834
+ return output
2835
+
2836
+ @property
2837
+ def existence_arguments(self) -> List[Tuple[Concept, ...]]:
2838
+ """Return concepts directly referenced in where clause"""
2839
+ output: List[Tuple[Concept, ...]] = []
2840
+ if isinstance(self.left, ConceptArgs):
2841
+ output += self.left.existence_arguments
2842
+ if isinstance(self.right, ConceptArgs):
2843
+ output += self.right.existence_arguments
2844
+ return output
2845
+
2805
2846
 
2806
2847
  class SubselectComparison(Comparison):
2807
2848
 
@@ -2901,6 +2942,7 @@ class Conditional(ConceptArgs, Namespaced, SelectGrain, BaseModel):
2901
2942
  float,
2902
2943
  list,
2903
2944
  bool,
2945
+ MagicConstants,
2904
2946
  Concept,
2905
2947
  Comparison,
2906
2948
  "Conditional",
@@ -2914,11 +2956,13 @@ class Conditional(ConceptArgs, Namespaced, SelectGrain, BaseModel):
2914
2956
  float,
2915
2957
  list,
2916
2958
  bool,
2959
+ MagicConstants,
2917
2960
  Concept,
2918
2961
  Comparison,
2919
2962
  "Conditional",
2920
2963
  "Parenthetical",
2921
2964
  Function,
2965
+ FilterItem,
2922
2966
  ]
2923
2967
  operator: BooleanOperator
2924
2968
 
@@ -12,9 +12,6 @@ from trilogy.constants import logger, CONFIG
12
12
  from abc import ABC
13
13
 
14
14
 
15
- REGISTERED_RULES: list["OptimizationRule"] = []
16
-
17
-
18
15
  class OptimizationRule(ABC):
19
16
 
20
17
  def optimize(self, cte: CTE, inverse_map: dict[str, list[CTE]]) -> bool:
@@ -66,13 +63,15 @@ class InlineDatasource(OptimizationRule):
66
63
  to_inline.append(parent_cte)
67
64
 
68
65
  for replaceable in to_inline:
69
- self.log(f"Inlining parent {replaceable.name}")
70
- cte.inline_parent_datasource(replaceable, force_group=force_group)
71
66
 
67
+ result = cte.inline_parent_datasource(replaceable, force_group=force_group)
68
+ if result:
69
+ self.log(f"Inlined parent {replaceable.name}")
70
+ else:
71
+ self.log(f"Failed to inline {replaceable.name}")
72
72
  return optimized
73
73
 
74
74
 
75
- # This will be used in the future for more complex condition decomposition
76
75
  def decompose_condition(conditional: Conditional):
77
76
  chunks = []
78
77
  if conditional.operator == BooleanOperator.AND:
@@ -154,12 +153,6 @@ class PredicatePushdown(OptimizationRule):
154
153
  return optimized
155
154
 
156
155
 
157
- if CONFIG.optimizations.datasource_inlining:
158
- REGISTERED_RULES.append(InlineDatasource())
159
- if CONFIG.optimizations.predicate_pushdown:
160
- REGISTERED_RULES.append(PredicatePushdown())
161
-
162
-
163
156
  def filter_irrelevant_ctes(
164
157
  input: list[CTE],
165
158
  root_cte: CTE,
@@ -233,6 +226,12 @@ def optimize_ctes(
233
226
  input: list[CTE], root_cte: CTE, select: SelectStatement | MultiSelectStatement
234
227
  ):
235
228
  complete = False
229
+ REGISTERED_RULES: list["OptimizationRule"] = []
230
+
231
+ if CONFIG.optimizations.datasource_inlining:
232
+ REGISTERED_RULES.append(InlineDatasource())
233
+ if CONFIG.optimizations.predicate_pushdown:
234
+ REGISTERED_RULES.append(PredicatePushdown())
236
235
 
237
236
  while not complete:
238
237
  actions_taken = False
trilogy/dialect/base.py CHANGED
@@ -10,6 +10,7 @@ from trilogy.core.enums import (
10
10
  WindowType,
11
11
  DatePart,
12
12
  PurposeLineage,
13
+ ComparisonOperator,
13
14
  )
14
15
  from trilogy.core.models import (
15
16
  ListType,
@@ -58,17 +59,23 @@ def INVALID_REFERENCE_STRING(x: Any, callsite: str = ""):
58
59
 
59
60
 
60
61
  def window_factory(string: str, include_concept: bool = False) -> Callable:
61
- def render_window(concept: str, window: str, sort: str) -> str:
62
+ def render_window(
63
+ concept: str, window: str, sort: str, offset: int | None = None
64
+ ) -> str:
62
65
  if not include_concept:
63
66
  concept = ""
67
+ if offset:
68
+ base = f"{string}({concept}, {offset})"
69
+ else:
70
+ base = f"{string}({concept})"
64
71
  if window and sort:
65
- return f"{string}({concept}) over (partition by {window} order by {sort} )"
72
+ return f"{base} over (partition by {window} order by {sort} )"
66
73
  elif window:
67
- return f"{string}({concept}) over (partition by {window})"
74
+ return f"{base} over (partition by {window})"
68
75
  elif sort:
69
- return f"{string}({concept}) over (order by {sort} )"
76
+ return f"{base} over (order by {sort} )"
70
77
  else:
71
- return f"{string}({concept}) over ()"
78
+ return f"{base} over ()"
72
79
 
73
80
  return render_window
74
81
 
@@ -109,10 +116,10 @@ FUNCTION_MAP = {
109
116
  FunctionType.INDEX_ACCESS: lambda x: f"{x[0]}[{x[1]}]",
110
117
  FunctionType.UNNEST: lambda x: f"unnest({x[0]})",
111
118
  # math
112
- FunctionType.ADD: lambda x: f"({x[0]} + {x[1]})",
113
- FunctionType.SUBTRACT: lambda x: f"({x[0]} - {x[1]})",
114
- FunctionType.DIVIDE: lambda x: f"({x[0]} / {x[1]})",
115
- FunctionType.MULTIPLY: lambda x: f"({x[0]} * {x[1]})",
119
+ FunctionType.ADD: lambda x: f"{x[0]} + {x[1]}",
120
+ FunctionType.SUBTRACT: lambda x: f"{x[0]} - {x[1]}",
121
+ FunctionType.DIVIDE: lambda x: f"{x[0]} / {x[1]}",
122
+ FunctionType.MULTIPLY: lambda x: f"{x[0]} * {x[1]}",
116
123
  FunctionType.ROUND: lambda x: f"round({x[0]},{x[1]})",
117
124
  FunctionType.MOD: lambda x: f"({x[0]} % {x[1]})",
118
125
  # aggregate types
@@ -355,17 +362,30 @@ class BaseDialect:
355
362
  lookup = lookup_cte.existence_source_map[e.right.address]
356
363
 
357
364
  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]})"
358
- elif isinstance(e.right, (ListWrapper, Parenthetical)):
365
+ elif isinstance(e.right, (ListWrapper, Parenthetical, list)):
359
366
  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)}"
360
- elif isinstance(e.right, (str, int, bool, float, list)):
367
+
368
+ elif isinstance(
369
+ e.right,
370
+ (
371
+ str,
372
+ int,
373
+ bool,
374
+ float,
375
+ ),
376
+ ):
361
377
  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)})"
362
378
  else:
363
- 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)})"
379
+ 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)}"
364
380
  elif isinstance(e, Comparison):
381
+ if e.operator == ComparisonOperator.BETWEEN:
382
+ right_comp = e.right
383
+ assert isinstance(right_comp, Conditional)
384
+ return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map)} {e.operator.value} {self.render_expr(right_comp.left, cte=cte, cte_map=cte_map) and self.render_expr(right_comp.right, cte=cte, cte_map=cte_map)}"
365
385
  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)}"
366
386
  elif isinstance(e, Conditional):
367
387
  # conditions need to be nested in parentheses
368
- 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)} ) "
388
+ 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)}"
369
389
  elif isinstance(e, WindowItem):
370
390
  rendered_order_components = [
371
391
  f"{self.render_expr(x.expr, cte, cte_map=cte_map)} {x.order.value}"
@@ -375,11 +395,11 @@ class BaseDialect:
375
395
  self.render_expr(x, cte, cte_map=cte_map) for x in e.over
376
396
  ]
377
397
  return f"{self.WINDOW_FUNCTION_MAP[e.type](concept = self.render_expr(e.content, cte=cte, cte_map=cte_map), window=','.join(rendered_over_components), sort=','.join(rendered_order_components))}" # noqa: E501
378
- elif isinstance(e, FilterItem):
379
- return f"CASE WHEN {self.render_expr(e.where.conditional, cte=cte, cte_map=cte_map)} THEN {self.render_expr(e.content, cte=cte, cte_map=cte_map)} ELSE 0 END"
380
398
  elif isinstance(e, Parenthetical):
381
399
  # conditions need to be nested in parentheses
382
- return f"( {self.render_expr(e.content, cte=cte, cte_map=cte_map)} ) "
400
+ if isinstance(e.content, list):
401
+ return f"( {','.join([self.render_expr(x, cte=cte, cte_map=cte_map) for x in e.content])} )"
402
+ return f"( {self.render_expr(e.content, cte=cte, cte_map=cte_map)} )"
383
403
  elif isinstance(e, CaseWhen):
384
404
  return f"WHEN {self.render_expr(e.comparison, cte=cte, cte_map=cte_map) } THEN {self.render_expr(e.expr, cte=cte, cte_map=cte_map) }"
385
405
  elif isinstance(e, CaseElse):
@@ -412,7 +432,7 @@ class BaseDialect:
412
432
  elif isinstance(e, ListWrapper):
413
433
  return f"[{','.join([self.render_expr(x, cte=cte, cte_map=cte_map) for x in e])}]"
414
434
  elif isinstance(e, list):
415
- return f"{','.join([self.render_expr(x, cte=cte, cte_map=cte_map) for x in e])}"
435
+ return f"[{','.join([self.render_expr(x, cte=cte, cte_map=cte_map) for x in e])}]"
416
436
  elif isinstance(e, DataType):
417
437
  return str(e.value)
418
438
  elif isinstance(e, DatePart):
trilogy/parsing/common.py CHANGED
@@ -41,7 +41,7 @@ def concept_list_to_keys(concepts: Tuple[Concept, ...]) -> Tuple[Concept, ...]:
41
41
 
42
42
 
43
43
  def constant_to_concept(
44
- parent: ListWrapper | int | float | str,
44
+ parent: ListWrapper | list | int | float | str,
45
45
  name: str,
46
46
  namespace: str,
47
47
  purpose: Purpose | None = None,
@@ -114,296 +114,17 @@ from trilogy.parsing.common import (
114
114
  arbitrary_to_concept,
115
115
  )
116
116
 
117
+ CONSTANT_TYPES = (int, float, str, bool, list, ListWrapper)
117
118
 
118
- CONSTANT_TYPES = (int, float, str, bool, ListWrapper)
119
-
120
- grammar = r"""
121
- !start: ( block | show_statement | comment )*
122
- block: statement _TERMINATOR comment?
123
- ?statement: concept
124
- | datasource
125
- | function
126
- | multi_select_statement
127
- | select_statement
128
- | persist_statement
129
- | rowset_derivation_statement
130
- | import_statement
131
- | merge_statement
132
-
133
- _TERMINATOR: ";"i /\s*/
134
-
135
- comment: /#.*(\n|$)/ | /\/\/.*\n/
136
-
137
- // property display_name string
138
- concept_declaration: PURPOSE IDENTIFIER data_type concept_nullable_modifier? metadata?
139
- //customer_id.property first_name STRING;
140
- //<customer_id,country>.property local_alias STRING
141
- concept_property_declaration: PROPERTY (prop_ident | IDENTIFIER) data_type concept_nullable_modifier? metadata?
142
- //metric post_length <- len(post_text);
143
- concept_derivation: (PURPOSE | AUTO | PROPERTY ) (prop_ident | IDENTIFIER) "<" "-" expr
144
-
145
- rowset_derivation_statement: ("rowset"i IDENTIFIER "<" "-" (multi_select_statement | select_statement)) | ("with"i IDENTIFIER "as"i (multi_select_statement | select_statement))
146
-
147
- constant_derivation: CONST IDENTIFIER "<" "-" literal
148
- concept_nullable_modifier: "?"
149
- concept: (concept_declaration | concept_derivation | concept_property_declaration | constant_derivation)
150
-
151
- //concept property
152
- prop_ident: "<" (IDENTIFIER ",")* IDENTIFIER ","? ">" "." IDENTIFIER
153
-
154
- // datasource concepts
155
- datasource: "datasource" IDENTIFIER "(" column_assignment_list ")" grain_clause? (address | query)
156
-
157
- grain_clause: "grain" "(" column_list ")"
158
-
159
- address: "address" ADDRESS
160
-
161
- query: "query" MULTILINE_STRING
162
-
163
- concept_assignment: IDENTIFIER | (MODIFIER "[" concept_assignment "]" ) | (SHORTHAND_MODIFIER concept_assignment )
164
-
165
- column_assignment: ((IDENTIFIER | raw_column_assignment | _static_functions ) ":" concept_assignment)
166
-
167
- raw_column_assignment: "raw" "(" MULTILINE_STRING ")"
168
-
169
- column_assignment_list : (column_assignment "," )* column_assignment ","?
170
-
171
- column_list : (IDENTIFIER "," )* IDENTIFIER ","?
172
-
173
- import_statement: "import" (IDENTIFIER ".") * IDENTIFIER "as" IDENTIFIER
174
-
175
- // persist_statement
176
- persist_statement: "persist"i IDENTIFIER "into"i IDENTIFIER "from"i select_statement grain_clause?
177
-
178
- // select statement
179
- select_statement: "select"i select_list where? comment* order_by? comment* limit? comment*
180
-
181
- // multiple_selects
182
- multi_select_statement: select_statement ("merge" select_statement)+ "align"i align_clause where? comment* order_by? comment* limit? comment*
183
-
184
- align_item: IDENTIFIER ":" IDENTIFIER ("," IDENTIFIER)* ","?
185
-
186
- align_clause: align_item ("," align_item)* ","?
187
-
188
- // merge statemment
189
- merge_statement: "merge" IDENTIFIER ("," IDENTIFIER)* ","? comment*
190
-
191
- // FUNCTION blocks
192
- function: raw_function
193
- function_binding_item: IDENTIFIER ":" data_type
194
- function_binding_list: (function_binding_item ",")* function_binding_item ","?
195
- raw_function: "bind" "sql" IDENTIFIER "(" function_binding_list ")" "-" ">" data_type "as"i MULTILINE_STRING
196
-
197
- // user_id where state = Mexico
198
- filter_item: "filter"i IDENTIFIER where
199
-
200
- // rank/lag/lead
201
- WINDOW_TYPE: ("row_number"i|"rank"i|"lag"i|"lead"i | "sum"i) /[\s]+/
202
-
203
- window_item: WINDOW_TYPE (IDENTIFIER | select_transform | comment+ ) window_item_over? window_item_order?
204
-
205
- window_item_over: ("OVER"i over_list)
206
-
207
- window_item_order: ("ORDER"i? "BY"i order_list)
208
-
209
- select_hide_modifier: "--"
210
- select_partial_modifier: "~"
211
- select_item: (select_hide_modifier | select_partial_modifier)? (IDENTIFIER | select_transform | comment+ )
212
-
213
- select_list: ( select_item "," )* select_item ","?
214
-
215
- // count(post_id) -> post_count
216
- _assignment: ("-" ">") | "as"
217
- select_transform : expr _assignment IDENTIFIER metadata?
218
-
219
- metadata: "metadata" "(" IDENTIFIER "=" _string_lit ")"
220
-
221
- limit: "LIMIT"i /[0-9]+/
222
-
223
- !window_order: ("TOP"i | "BOTTOM"i)
224
-
225
- window: window_order /[0-9]+/
226
-
227
- window_order_by: "BY"i column_list
228
-
229
- order_list: (expr ORDERING "," )* expr ORDERING ","?
230
-
231
- over_list: (IDENTIFIER "," )* IDENTIFIER ","?
232
-
233
- ORDERING: ("ASC"i | "DESC"i)
234
-
235
- order_by: "ORDER"i "BY"i order_list
236
-
237
- //WHERE STATEMENT
238
-
239
- LOGICAL_OPERATOR: "AND"i | "OR"i
240
-
241
- conditional: expr LOGICAL_OPERATOR (conditional | expr)
242
-
243
- where: "WHERE"i (expr | conditional)
244
-
245
- expr_reference: IDENTIFIER
246
-
247
- !array_comparison: ( ("NOT"i "IN"i) | "IN"i)
248
-
249
- COMPARISON_OPERATOR: (/is[\s]+not/ | "is" |"=" | ">" | "<" | ">=" | "<=" | "!=" )
250
-
251
- comparison: (expr COMPARISON_OPERATOR expr)
252
-
253
- subselect_comparison: expr array_comparison expr | (expr array_comparison expr_tuple)
254
-
255
- expr_tuple: "(" (expr ",")* expr ","? ")"
256
-
257
- //unnesting is a function
258
- unnest: "UNNEST"i "(" expr ")"
259
- //indexing into an expression is a function
260
- index_access: expr "[" int_lit "]"
261
- attr_access: expr "[" _string_lit "]"
262
-
263
- parenthetical: "(" (conditional | expr) ")"
264
-
265
- expr: window_item | filter_item | comparison | subselect_comparison | fgroup | aggregate_functions | unnest | _string_functions | _math_functions | _generic_functions | _constant_functions| _date_functions | literal | expr_reference | index_access | attr_access | parenthetical
266
-
267
- // functions
268
-
269
- fadd: ("add"i "(" expr "," expr ")" ) | ( expr "+" expr )
270
- fsub: ("subtract"i "(" expr "," expr ")" ) | ( expr "-" expr )
271
- fmul: ("multiply"i "(" expr "," expr ")" ) | ( expr "*" expr )
272
- fdiv: ( "divide"i "(" expr "," expr ")") | ( expr "/" expr )
273
- fmod: ( "mod"i "(" expr "," expr ")") | ( expr "%" expr )
274
- fround: "round"i "(" expr "," expr ")"
275
- fabs: "abs"i "(" expr ")"
276
-
277
- _math_functions: fadd | fsub | fmul | fdiv | fround | fmod | fabs
278
-
279
- //generic
280
- fcast: "cast"i "(" expr "AS"i data_type ")"
281
- concat: ("concat"i "(" (expr ",")* expr ")") | (expr "||" expr)
282
- fcoalesce: "coalesce"i "(" (expr ",")* expr ")"
283
- fcase_when: "WHEN"i (expr | conditional) "THEN"i expr
284
- fcase_else: "ELSE"i expr
285
- fcase: "CASE"i (fcase_when)* (fcase_else)? "END"i
286
- len: "len"i "(" expr ")"
287
- fnot: "NOT"i expr
288
-
289
- _generic_functions: fcast | concat | fcoalesce | fcase | len | fnot
290
-
291
- //constant
292
- fcurrent_date: "current_date"i "(" ")"
293
- fcurrent_datetime: "current_datetime"i "(" ")"
294
-
295
- _constant_functions: fcurrent_date | fcurrent_datetime
296
-
297
- //string
298
- like: "like"i "(" expr "," _string_lit ")"
299
- ilike: "ilike"i "(" expr "," _string_lit ")"
300
- alt_like: expr "like"i expr
301
- upper: "upper"i "(" expr ")"
302
- lower: "lower"i "(" expr ")"
303
- fsplit: "split"i "(" expr "," _string_lit ")"
304
- fstrpos: "strpos"i "(" expr "," expr ")"
305
- fsubstring: "substring"i "(" expr "," expr "," expr ")"
306
-
307
- _string_functions: like | ilike | upper | lower | fsplit | fstrpos | fsubstring | alt_like
308
-
309
- // special aggregate
310
- fgroup: "group"i "(" expr ")" aggregate_over?
311
- //aggregates
312
- count: "count"i "(" expr ")"
313
- count_distinct: "count_distinct"i "(" expr ")"
314
- sum: "sum"i "(" expr ")"
315
- avg: "avg"i "(" expr ")"
316
- max: "max"i "(" expr ")"
317
- min: "min"i "(" expr ")"
318
-
319
- //aggregates can force a grain
320
- aggregate_all: "*"
321
- aggregate_over: ("BY"i (aggregate_all | over_list))
322
- aggregate_functions: (count | count_distinct | sum | avg | max | min) aggregate_over?
323
-
324
- // date functions
325
- fdate: "date"i "(" expr ")"
326
- fdatetime: "datetime"i "(" expr ")"
327
- ftimestamp: "timestamp"i "(" expr ")"
328
-
329
- fsecond: "second"i "(" expr ")"
330
- fminute: "minute"i "(" expr ")"
331
- fhour: "hour"i "(" expr ")"
332
- fday: "day"i "(" expr ")"
333
- fday_of_week: "day_of_week"i "(" expr ")"
334
- fweek: "week"i "(" expr ")"
335
- fmonth: "month"i "(" expr ")"
336
- fquarter: "quarter"i "(" expr ")"
337
- fyear: "year"i "(" expr ")"
338
-
339
- DATE_PART: "DAY"i | "WEEK"i | "MONTH"i | "QUARTER"i | "YEAR"i | "MINUTE"i | "HOUR"i | "SECOND"i
340
- fdate_trunc: "date_trunc"i "(" expr "," DATE_PART ")"
341
- fdate_part: "date_part"i "(" expr "," DATE_PART ")"
342
- fdate_add: "date_add"i "(" expr "," DATE_PART "," int_lit ")"
343
- fdate_diff: "date_diff"i "(" expr "," expr "," DATE_PART ")"
344
-
345
- _date_functions: fdate | fdate_add | fdate_diff | fdatetime | ftimestamp | fsecond | fminute | fhour | fday | fday_of_week | fweek | fmonth | fquarter | fyear | fdate_part | fdate_trunc
346
-
347
- _static_functions: _string_functions | _math_functions | _generic_functions | _constant_functions| _date_functions
348
-
349
- // base language constructs
350
- IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_\\-\\.\-]*/
351
- ADDRESS: /[a-zA-Z_][a-zA-Z0-9_\\-\\.\-\*]*/ | /`[a-zA-Z_][a-zA-Z0-9_\\-\\.\-\*]*`/
352
-
353
- MULTILINE_STRING: /\'{3}(.*?)\'{3}/s
354
-
355
- DOUBLE_STRING_CHARS: /(?:(?!\${)([^"\\]|\\.))+/+ // any character except "
356
- SINGLE_STRING_CHARS: /(?:(?!\${)([^'\\]|\\.))+/+ // any character except '
357
- _single_quote: "'" ( SINGLE_STRING_CHARS )* "'"
358
- _double_quote: "\"" ( DOUBLE_STRING_CHARS )* "\""
359
- _string_lit: _single_quote | _double_quote
360
-
361
- MINUS: "-"
362
-
363
- int_lit: MINUS? /[0-9]+/
364
-
365
- float_lit: /[0-9]*\.[0-9]+/
366
-
367
- array_lit: "[" (literal ",")* literal ","? "]"()
368
-
369
- !bool_lit: "True"i | "False"i
370
-
371
- !null_lit: "null"i
372
-
373
- literal: _string_lit | int_lit | float_lit | bool_lit | null_lit | array_lit
374
-
375
- MODIFIER: "Optional"i | "Partial"i | "Nullable"i
376
-
377
- SHORTHAND_MODIFIER: "~"
378
-
379
- struct_type: "struct" "<" ((data_type | IDENTIFIER) ",")* (data_type | IDENTIFIER) ","? ">"
380
-
381
- list_type: "list" "<" data_type ">"
382
-
383
-
384
- !data_type: "string"i | "number"i | "numeric"i | "map"i | "list"i | "array"i | "any"i | "int"i | "bigint" | "date"i | "datetime"i | "timestamp"i | "float"i | "bool"i | struct_type | list_type
385
-
386
- PURPOSE: "key"i | "metric"i | "const"i | "constant"i
387
- PROPERTY: "property"i
388
- CONST: "const"i | "constant"i
389
- AUTO: "AUTO"i
390
-
391
- // meta functions
392
- CONCEPTS: "CONCEPTS"i
393
- DATASOURCES: "DATASOURCES"i
394
-
395
- show_category: CONCEPTS | DATASOURCES
396
-
397
- show_statement: "show"i ( show_category | select_statement | persist_statement) _TERMINATOR
398
-
399
- %import common.WS_INLINE -> _WHITESPACE
400
- %import common.WS
401
- %ignore WS
402
- """ # noqa: E501
403
-
404
- PARSER = Lark(
405
- grammar, start="start", propagate_positions=True, g_regex_flags=IGNORECASE
406
- )
119
+ with open(join(dirname(__file__), "trilogy.lark"), "r") as f:
120
+ PARSER = Lark(
121
+ f.read(),
122
+ start="start",
123
+ propagate_positions=True,
124
+ g_regex_flags=IGNORECASE,
125
+ parser="lalr",
126
+ cache=True,
127
+ )
407
128
 
408
129
 
409
130
  def parse_concept_reference(
@@ -567,6 +288,9 @@ class ParseToObjects(Transformer):
567
288
  def IDENTIFIER(self, args) -> str:
568
289
  return args.value
569
290
 
291
+ def concept_lit(self, args) -> Concept:
292
+ return self.environment.concepts.__getitem__(args[0])
293
+
570
294
  def ADDRESS(self, args) -> str:
571
295
  return args.value
572
296
 
@@ -622,19 +346,23 @@ class ParseToObjects(Transformer):
622
346
  def column_assignment(self, meta: Meta, args):
623
347
  # TODO -> deal with conceptual modifiers
624
348
  modifiers = []
625
- concept = args[1]
349
+ alias = args[0]
350
+ concept_list = args[1]
626
351
  # recursively collect modifiers
627
- while len(concept) > 1:
628
- modifiers.append(concept[0])
629
- concept = concept[1]
352
+ if len(concept_list) > 1:
353
+ modifiers += concept_list[:-1]
354
+ concept = concept_list[-1]
630
355
  resolved = self.environment.concepts.__getitem__( # type: ignore
631
- key=concept[0], line_no=meta.line
356
+ key=concept, line_no=meta.line
632
357
  )
633
- return ColumnAssignment(alias=args[0], modifiers=modifiers, concept=resolved)
358
+ return ColumnAssignment(alias=alias, modifiers=modifiers, concept=resolved)
634
359
 
635
360
  def _TERMINATOR(self, args):
636
361
  return None
637
362
 
363
+ def _static_functions(self, args):
364
+ return args[0]
365
+
638
366
  def MODIFIER(self, args) -> Modifier:
639
367
  return Modifier(args.value)
640
368
 
@@ -893,6 +621,9 @@ class ParseToObjects(Transformer):
893
621
  assert len(args) == 1
894
622
  return Comment(text=args[0].value)
895
623
 
624
+ def PARSE_COMMENT(self, args):
625
+ return Comment(text=args.value)
626
+
896
627
  @v_args(meta=True)
897
628
  def select_transform(self, meta, args) -> ConceptTransform:
898
629
 
@@ -962,7 +693,7 @@ class ParseToObjects(Transformer):
962
693
  if isinstance(content, ConceptTransform):
963
694
  return SelectItem(content=content, modifiers=modifiers)
964
695
  return SelectItem(
965
- content=self.environment.concepts.__getitem__(content, meta.line),
696
+ content=content,
966
697
  modifiers=modifiers,
967
698
  )
968
699
 
@@ -972,8 +703,12 @@ class ParseToObjects(Transformer):
972
703
  def limit(self, args):
973
704
  return Limit(count=int(args[0].value))
974
705
 
975
- def ORDERING(self, args):
976
- return Ordering(args.lower())
706
+ def ordering(self, args: list[str]):
707
+ base = args[0].lower()
708
+ if len(args) > 1:
709
+ null_sort = args[-1]
710
+ return Ordering(" ".join([base, "nulls", null_sort.lower()]))
711
+ return Ordering(base)
977
712
 
978
713
  def order_list(self, args):
979
714
 
@@ -1001,7 +736,7 @@ class ParseToObjects(Transformer):
1001
736
  return OrderBy(items=args[0])
1002
737
 
1003
738
  def over_list(self, args):
1004
- return [self.environment.concepts[x] for x in args]
739
+ return [x for x in args]
1005
740
 
1006
741
  @v_args(meta=True)
1007
742
  def merge_statement(self, meta: Meta, args) -> MergeStatement:
@@ -1044,9 +779,7 @@ class ParseToObjects(Transformer):
1044
779
  # add the parsed objects of the import in
1045
780
  self.parsed = {**self.parsed, **nparser.parsed}
1046
781
  except Exception as e:
1047
- raise ImportError(
1048
- f"Unable to import file {dirname(target)}, parsing error: {e}"
1049
- )
782
+ raise ImportError(f"Unable to import file {target}, parsing error: {e}")
1050
783
 
1051
784
  for _, concept in nparser.environment.concepts.items():
1052
785
  self.environment.add_concept(concept.with_namespace(alias))
@@ -1241,6 +974,19 @@ class ParseToObjects(Transformer):
1241
974
  raise SyntaxError
1242
975
  return Comparison(left=args[0], right=args[2], operator=args[1])
1243
976
 
977
+ def between_comparison(self, args) -> Conditional:
978
+ left_bound = args[1]
979
+ right_bound = args[2]
980
+ return Conditional(
981
+ left=Comparison(
982
+ left=args[0], right=left_bound, operator=ComparisonOperator.GTE
983
+ ),
984
+ right=Comparison(
985
+ left=args[0], right=right_bound, operator=ComparisonOperator.LTE
986
+ ),
987
+ operator=BooleanOperator.AND,
988
+ )
989
+
1244
990
  @v_args(meta=True)
1245
991
  def subselect_comparison(self, meta: Meta, args) -> SubselectComparison:
1246
992
  right = args[2]
@@ -1297,13 +1043,23 @@ class ParseToObjects(Transformer):
1297
1043
  type = args[0]
1298
1044
  order_by = []
1299
1045
  over = []
1300
- for item in args[2:]:
1301
- if isinstance(item, WindowItemOrder):
1046
+ index = None
1047
+ concept: Concept | None = None
1048
+ for item in args:
1049
+ if isinstance(item, int):
1050
+ index = item
1051
+ elif isinstance(item, WindowItemOrder):
1302
1052
  order_by = item.contents
1303
1053
  elif isinstance(item, WindowItemOver):
1304
1054
  over = item.contents
1305
- concept = self.environment.concepts[args[1]]
1306
- return WindowItem(type=type, content=concept, over=over, order_by=order_by)
1055
+ elif isinstance(item, str):
1056
+ concept = self.environment.concepts[item]
1057
+ elif isinstance(item, Concept):
1058
+ concept = item
1059
+ assert concept
1060
+ return WindowItem(
1061
+ type=type, content=concept, over=over, order_by=order_by, index=index
1062
+ )
1307
1063
 
1308
1064
  def filter_item(self, args) -> FilterItem:
1309
1065
  where: WhereClause
@@ -1501,6 +1257,9 @@ class ParseToObjects(Transformer):
1501
1257
  )
1502
1258
  return SubString(args)
1503
1259
 
1260
+ def logical_operator(self, args):
1261
+ return BooleanOperator(args[0].value.lower())
1262
+
1504
1263
  @v_args(meta=True)
1505
1264
  def lower(self, meta, args):
1506
1265
  args = self.process_function_args(args, meta=meta)
@@ -1893,12 +1652,12 @@ class ParseToObjects(Transformer):
1893
1652
  @v_args(meta=True)
1894
1653
  def fcurrent_date(self, meta, args):
1895
1654
  args = self.process_function_args(args, meta=meta)
1896
- return CurrentDate(args)
1655
+ return CurrentDate([])
1897
1656
 
1898
1657
  @v_args(meta=True)
1899
1658
  def fcurrent_datetime(self, meta, args):
1900
1659
  args = self.process_function_args(args, meta=meta)
1901
- return CurrentDatetime(args)
1660
+ return CurrentDatetime([])
1902
1661
 
1903
1662
  @v_args(meta=True)
1904
1663
  def fnot(self, meta, args):
@@ -1914,10 +1673,14 @@ def unpack_visit_error(e: VisitError):
1914
1673
  elif isinstance(e.orig_exc, (UndefinedConceptException, ImportError)):
1915
1674
  raise e.orig_exc
1916
1675
  elif isinstance(e.orig_exc, (ValidationError, TypeError)):
1917
- raise InvalidSyntaxException(str(e.orig_exc))
1676
+ raise InvalidSyntaxException(str(e.orig_exc) + str(e.rule) + str(e.obj))
1918
1677
  raise e
1919
1678
 
1920
1679
 
1680
+ def parse_text_raw(text: str, environment: Optional[Environment] = None):
1681
+ PARSER.parse(text)
1682
+
1683
+
1921
1684
  def parse_text(text: str, environment: Optional[Environment] = None) -> Tuple[
1922
1685
  Environment,
1923
1686
  List[