pytrilogy 0.0.2.34__py3-none-any.whl → 0.0.2.36__py3-none-any.whl

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

Potentially problematic release.


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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.34
3
+ Version: 0.0.2.36
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -20,6 +20,7 @@ Requires-Dist: networkx
20
20
  Requires-Dist: pyodbc
21
21
  Requires-Dist: pydantic
22
22
  Requires-Dist: duckdb-engine
23
+ Requires-Dist: click
23
24
  Provides-Extra: bigquery
24
25
  Requires-Dist: sqlalchemy-bigquery; extra == "bigquery"
25
26
  Provides-Extra: postgres
@@ -1,14 +1,14 @@
1
- trilogy/__init__.py,sha256=KM64JjCIbIm0t7no7TfLDsHkXU-HC5d2by7YePMKmq8,291
1
+ trilogy/__init__.py,sha256=nmEtedZ0VhRnMAaMT0xsUW46MvwSivUw-Vq9Y6h2M70,291
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  trilogy/constants.py,sha256=HQAnGUqJ5uMri7TWtqXHhz8iVWBzi2LCfRG8vKnBIB8,1269
4
4
  trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
5
- trilogy/executor.py,sha256=VcZ2U3RUU2al_VJ75AKVwmCJQLltYouxlgTjq4oxPB0,12577
5
+ trilogy/executor.py,sha256=0b0iEd660aR3_rJlHp73azRu5iQK6si5qEszoSwz8FU,13472
6
6
  trilogy/parser.py,sha256=UtuqSiGiCjpMAYgo1bvNq-b7NSzCA5hzbUW31RXaMII,281
7
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=7XaCpZn5mQmjTobbeBn56SzPWq9eMNDfzfsRU-fP0VE,171
11
- trilogy/core/enums.py,sha256=GG51K-bP3HTVuvJQFCjbmVt7iBOGpmF_qtnRVf0_obI,6396
11
+ trilogy/core/enums.py,sha256=z-qBLh0YW52tkRtp6VLn6afxLfgI45r7_QfDtGH9CSw,6858
12
12
  trilogy/core/env_processor.py,sha256=SHVB3nkidIlFc5dz-sofRMKXx66stpLQNuVdQSjC-So,2586
13
13
  trilogy/core/environment_helpers.py,sha256=DIsoo-GcXmXVPB1JbNh8Oku25Nyef9mexPIdy2ur_sk,7159
14
14
  trilogy/core/ergonomics.py,sha256=ASLDd0RqKWrZiG3XcKHo8nyTjaB_8xfE9t4NZ1UvGpc,1639
@@ -16,7 +16,7 @@ trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,56
16
16
  trilogy/core/functions.py,sha256=IhVpt3n6wEanKHnGu3oA2w6-hKIlxWpEyz7fHN66mpo,10720
17
17
  trilogy/core/graph_models.py,sha256=mameUTiuCajtihDw_2-W218xyJlvTusOWrEKP1yAWgk,2003
18
18
  trilogy/core/internal.py,sha256=jNGFHKENnbMiMCtAgsnLZYVSENDK4b5ALecXFZpTDzQ,1075
19
- trilogy/core/models.py,sha256=xZHi9IYo_vSG35rF4fp7bLrGIu8J5QXqDObpZgN0_cI,160541
19
+ trilogy/core/models.py,sha256=iP1ioJmKDS1_43TE0mIGWZGDtFPD3m8kKvXYA9lwCkg,162860
20
20
  trilogy/core/optimization.py,sha256=VFSvJLNoCCOraip-PZUKeE4qrlxtXARjQUzJZiW-yRk,7325
21
21
  trilogy/core/query_processor.py,sha256=mbcZlgjChrRjDHkdmMbKe-T70UpbBkJhS09MyU5a6UY,17785
22
22
  trilogy/core/optimizations/__init__.py,sha256=bWQecbeiwiDx9LJnLsa7dkWxdbl2wcnkcTN69JyP8iI,356
@@ -70,14 +70,14 @@ trilogy/parsing/common.py,sha256=_GW9LU6_4RuUgcdcr8EE1ybCRd-7cz3idZtjHZ66pYA,101
70
70
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
71
71
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
72
72
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
73
- trilogy/parsing/parse_engine.py,sha256=mMMP0TPLAHx0m4c7scXxosDIgfPYyUYmlgwAFtVeRe0,66699
74
- trilogy/parsing/render.py,sha256=VKyo8wEOuiOzUtJ6w9EoGGmkhlqDQyy8wFj_Q_h6EfE,15263
75
- trilogy/parsing/trilogy.lark,sha256=Tuqw5oGMwOYt3TYOEx_hZqGpsAp-PiAKiMW8S3EFRcg,12236
73
+ trilogy/parsing/parse_engine.py,sha256=Mv2GrE-7IteyK67wZBPm0iEXY2Wbnb4CPNNSD3kXn-E,63915
74
+ trilogy/parsing/render.py,sha256=dhHkwmZ5HNNJr-z81JDpN2TBe4Ktqarus5vtMn2-_B8,15502
75
+ trilogy/parsing/trilogy.lark,sha256=o3gZiqPE3FNEJjJZDeSZ-ETD3HUQDIFZaYWrVDmxSB4,12319
76
76
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
77
  trilogy/scripts/trilogy.py,sha256=PHxvv6f2ODv0esyyhWxlARgra8dVhqQhYl0lTrSyVNo,3729
78
- pytrilogy-0.0.2.34.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
79
- pytrilogy-0.0.2.34.dist-info/METADATA,sha256=jvasI3x4y_xwYh4TToePk_EQIwEr0Kfw2zqESs65bQk,8403
80
- pytrilogy-0.0.2.34.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
81
- pytrilogy-0.0.2.34.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
82
- pytrilogy-0.0.2.34.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
83
- pytrilogy-0.0.2.34.dist-info/RECORD,,
78
+ pytrilogy-0.0.2.36.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
79
+ pytrilogy-0.0.2.36.dist-info/METADATA,sha256=Zl-ztc3tMnv-LbxCvmAItYOrmJ-p1pIDYW0uOmRFwYI,8424
80
+ pytrilogy-0.0.2.36.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
81
+ pytrilogy-0.0.2.36.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
82
+ pytrilogy-0.0.2.36.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
83
+ pytrilogy-0.0.2.36.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.0)
2
+ Generator: setuptools (75.5.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.2.34"
7
+ __version__ = "0.0.2.36"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/core/enums.py CHANGED
@@ -215,11 +215,28 @@ class Boolean(Enum):
215
215
  TRUE = "true"
216
216
  FALSE = "false"
217
217
 
218
+ @classmethod
219
+ def _missing_(cls, value):
220
+ if value is True:
221
+ return Boolean.TRUE
222
+ elif value is False:
223
+ return Boolean.FALSE
224
+ strval = str(value)
225
+ if strval.lower() != strval:
226
+ return Boolean(strval.lower())
227
+
218
228
 
219
229
  class BooleanOperator(Enum):
220
230
  AND = "and"
221
231
  OR = "or"
222
232
 
233
+ @classmethod
234
+ def _missing_(cls, value):
235
+ strval = str(value)
236
+ if strval.lower() != strval:
237
+ return BooleanOperator(strval.lower())
238
+ return None
239
+
223
240
 
224
241
  class ComparisonOperator(Enum):
225
242
  LT = "<"
trilogy/core/models.py CHANGED
@@ -1629,6 +1629,45 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1629
1629
  self.grain
1630
1630
  )
1631
1631
 
1632
+ def validate_syntax(self):
1633
+ all_in_output = [x.address for x in self.output_components]
1634
+ if self.where_clause:
1635
+ for concept in self.where_clause.concept_arguments:
1636
+
1637
+ if (
1638
+ concept.lineage
1639
+ and isinstance(concept.lineage, Function)
1640
+ and concept.lineage.operator
1641
+ in FunctionClass.AGGREGATE_FUNCTIONS.value
1642
+ ):
1643
+ if concept.address in self.locally_derived:
1644
+ raise SyntaxError(
1645
+ f"Cannot reference an aggregate derived in the select ({concept.address}) in the same statement where clause; move to the HAVING clause instead; Line: {self.meta.line_number}"
1646
+ )
1647
+
1648
+ if (
1649
+ concept.lineage
1650
+ and isinstance(concept.lineage, AggregateWrapper)
1651
+ and concept.lineage.function.operator
1652
+ in FunctionClass.AGGREGATE_FUNCTIONS.value
1653
+ ):
1654
+ if concept.address in self.locally_derived:
1655
+ raise SyntaxError(
1656
+ f"Cannot reference an aggregate derived in the select ({concept.address}) in the same statement where clause; move to the HAVING clause instead; Line: {self.meta.line_number}"
1657
+ )
1658
+ if self.having_clause:
1659
+ for concept in self.having_clause.concept_arguments:
1660
+ if concept.address not in [x.address for x in self.output_components]:
1661
+ raise SyntaxError(
1662
+ f"Cannot reference a column ({concept.address}) that is not in the select projection in the HAVING clause, move to WHERE; Line: {self.meta.line_number}"
1663
+ )
1664
+ if self.order_by:
1665
+ for concept in self.order_by.concept_arguments:
1666
+ if concept.address not in all_in_output:
1667
+ raise SyntaxError(
1668
+ f"Cannot order by a column that is not in the output projection; {self.meta.line_number}"
1669
+ )
1670
+
1632
1671
  def __str__(self):
1633
1672
  from trilogy.parsing.render import render_query
1634
1673
 
@@ -3237,7 +3276,7 @@ class EnvironmentConceptDict(dict):
3237
3276
  return default
3238
3277
 
3239
3278
  def __getitem__(
3240
- self, key, line_no: int | None = None
3279
+ self, key, line_no: int | None = None, file: Path | None = None
3241
3280
  ) -> Concept | UndefinedConcept:
3242
3281
  try:
3243
3282
  return super(EnvironmentConceptDict, self).__getitem__(key)
@@ -3265,6 +3304,10 @@ class EnvironmentConceptDict(dict):
3265
3304
  message += f" Suggestions: {matches}"
3266
3305
 
3267
3306
  if line_no:
3307
+ if file:
3308
+ raise UndefinedConceptException(
3309
+ f"{file}: {line_no}: " + message, matches
3310
+ )
3268
3311
  raise UndefinedConceptException(f"line: {line_no}: " + message, matches)
3269
3312
  raise UndefinedConceptException(message, matches)
3270
3313
 
trilogy/executor.py CHANGED
@@ -20,6 +20,8 @@ from trilogy.core.models import (
20
20
  ConceptDeclarationStatement,
21
21
  Datasource,
22
22
  CopyStatement,
23
+ ImportStatement,
24
+ MergeStatementV2,
23
25
  )
24
26
  from trilogy.dialect.base import BaseDialect
25
27
  from trilogy.dialect.enums import Dialects
@@ -104,6 +106,7 @@ class Executor(object):
104
106
  ProcessedShowStatement,
105
107
  ProcessedQueryPersist,
106
108
  ProcessedCopyStatement,
109
+ ProcessedRawSQLStatement,
107
110
  ),
108
111
  ):
109
112
  return None
@@ -142,7 +145,6 @@ class Executor(object):
142
145
 
143
146
  @execute_query.register
144
147
  def _(self, query: str) -> CursorResult:
145
-
146
148
  return self.execute_text(query)[-1]
147
149
 
148
150
  @execute_query.register
@@ -181,6 +183,35 @@ class Executor(object):
181
183
  ],
182
184
  )
183
185
 
186
+ @execute_query.register
187
+ def _(self, query: ImportStatement) -> CursorResult:
188
+ self.environment.add_file_import(query.path, query.alias)
189
+ return MockResult(
190
+ [
191
+ {
192
+ "path": query.path,
193
+ "alias": query.alias,
194
+ }
195
+ ],
196
+ ["path", "alias"],
197
+ )
198
+
199
+ @execute_query.register
200
+ def _(self, query: MergeStatementV2) -> CursorResult:
201
+
202
+ self.environment.merge_concept(
203
+ query.source, query.target, modifiers=query.modifiers
204
+ )
205
+ return MockResult(
206
+ [
207
+ {
208
+ "source": query.source.address,
209
+ "target": query.target.address,
210
+ }
211
+ ],
212
+ ["source", "target"],
213
+ )
214
+
184
215
  @execute_query.register
185
216
  def _(self, query: ProcessedRawSQLStatement) -> CursorResult:
186
217
  return self.execute_raw_sql(query.text)
@@ -30,7 +30,6 @@ from trilogy.core.enums import (
30
30
  WindowType,
31
31
  DatePart,
32
32
  ShowCategory,
33
- FunctionClass,
34
33
  IOType,
35
34
  ConceptSource,
36
35
  )
@@ -264,7 +263,6 @@ class ParseToObjects(Transformer):
264
263
  for k, v in self.parsed.items():
265
264
  if v.pass_count == 2:
266
265
  continue
267
- print(f"Hydrating {k}")
268
266
  v.hydrate_missing()
269
267
  self.environment.concepts.fail_on_missing = True
270
268
  reparsed = self.transform(self.tokens[self.token_address])
@@ -304,6 +302,9 @@ class ParseToObjects(Transformer):
304
302
  def IDENTIFIER(self, args) -> str:
305
303
  return args.value
306
304
 
305
+ def QUOTED_IDENTIFIER(self, args) -> str:
306
+ return args.value[1:-1]
307
+
307
308
  @v_args(meta=True)
308
309
  def concept_lit(self, meta: Meta, args) -> Concept:
309
310
  return self.environment.concepts.__getitem__(args[0], meta.line)
@@ -388,7 +389,7 @@ class ParseToObjects(Transformer):
388
389
  modifiers += concept_list[:-1]
389
390
  concept = concept_list[-1]
390
391
  resolved = self.environment.concepts.__getitem__( # type: ignore
391
- key=concept, line_no=meta.line
392
+ key=concept, line_no=meta.line, file=self.token_address
392
393
  )
393
394
  return ColumnAssignment(alias=alias, modifiers=modifiers, concept=resolved)
394
395
 
@@ -988,8 +989,6 @@ class ParseToObjects(Transformer):
988
989
  order_by=order_by,
989
990
  meta=Metadata(line_number=meta.line),
990
991
  )
991
- locally_derived: set[str] = set()
992
- all_in_output: set[str] = set()
993
992
  for item in select_items:
994
993
  # we don't know the grain of an aggregate at assignment time
995
994
  # so rebuild at this point in the tree
@@ -998,25 +997,16 @@ class ParseToObjects(Transformer):
998
997
  new_concept = item.content.output.with_select_context(
999
998
  output.grain,
1000
999
  conditional=None,
1001
- # conditional=(
1002
- # output.where_clause.conditional
1003
- # if output.where_clause
1004
- # and output.where_clause_category == SelectFiltering.IMPLICIT
1005
- # else None
1006
- # ),
1007
1000
  environment=self.environment,
1008
1001
  )
1009
1002
  self.environment.add_concept(new_concept, meta=meta)
1010
1003
  item.content.output = new_concept
1011
- locally_derived.add(new_concept.address)
1012
- all_in_output.add(new_concept.address)
1013
1004
  elif isinstance(item.content, Concept):
1014
1005
  # Sometimes cached values here don't have the latest info
1015
1006
  # but we can't just use environment, as it might not have the right grain.
1016
1007
  item.content = self.environment.concepts[
1017
1008
  item.content.address
1018
1009
  ].with_grain(item.content.grain)
1019
- all_in_output.add(item.content.address)
1020
1010
  if order_by:
1021
1011
  for orderitem in order_by.items:
1022
1012
  if isinstance(orderitem.expr, Concept):
@@ -1024,52 +1014,9 @@ class ParseToObjects(Transformer):
1024
1014
  orderitem.expr = orderitem.expr.with_select_context(
1025
1015
  output.grain,
1026
1016
  conditional=None,
1027
- # conditional=(
1028
- # output.where_clause.conditional
1029
- # if output.where_clause
1030
- # and output.where_clause_category
1031
- # == SelectFiltering.IMPLICIT
1032
- # else None
1033
- # ),
1034
1017
  environment=self.environment,
1035
1018
  )
1036
- if output.where_clause:
1037
- for concept in output.where_clause.concept_arguments:
1038
-
1039
- if (
1040
- concept.lineage
1041
- and isinstance(concept.lineage, Function)
1042
- and concept.lineage.operator
1043
- in FunctionClass.AGGREGATE_FUNCTIONS.value
1044
- ):
1045
- if concept.address in locally_derived:
1046
- raise SyntaxError(
1047
- f"Cannot reference an aggregate derived in the select ({concept.address}) in the same statement where clause; move to the HAVING clause instead; Line: {meta.line}"
1048
- )
1049
-
1050
- if (
1051
- concept.lineage
1052
- and isinstance(concept.lineage, AggregateWrapper)
1053
- and concept.lineage.function.operator
1054
- in FunctionClass.AGGREGATE_FUNCTIONS.value
1055
- ):
1056
- if concept.address in locally_derived:
1057
- raise SyntaxError(
1058
- f"Cannot reference an aggregate derived in the select ({concept.address}) in the same statement where clause; move to the HAVING clause instead; Line: {meta.line}"
1059
- )
1060
- if output.having_clause:
1061
- for concept in output.having_clause.concept_arguments:
1062
- if concept.address not in all_in_output:
1063
- raise SyntaxError(
1064
- f"Cannot reference a column ({concept.address}) that is not in the select projection in the HAVING clause, move to WHERE; Line: {meta.line}"
1065
- )
1066
- if output.order_by:
1067
- for concept in output.order_by.concept_arguments:
1068
- if concept.address not in all_in_output:
1069
- raise SyntaxError(
1070
- f"Cannot order by a column that is not in the output projection; {meta.line}"
1071
- )
1072
-
1019
+ output.validate_syntax()
1073
1020
  return output
1074
1021
 
1075
1022
  @v_args(meta=True)
trilogy/parsing/render.py CHANGED
@@ -368,9 +368,15 @@ class Renderer:
368
368
 
369
369
  @to_string.register
370
370
  def _(self, arg: "ImportStatement"):
371
- if arg.alias == DEFAULT_NAMESPACE:
372
- return f"import {arg.path};"
373
- return f"import {arg.path} as {arg.alias};"
371
+ path: str = str(arg.path).replace("\\", ".")
372
+ path = path.replace("/", ".")
373
+ if path.endswith(".preql"):
374
+ path = path.rsplit(".", 1)[0]
375
+ if path.startswith("."):
376
+ path = path[1:]
377
+ if arg.alias == DEFAULT_NAMESPACE or not arg.alias:
378
+ return f"import {path};"
379
+ return f"import {path} as {arg.alias};"
374
380
 
375
381
  @to_string.register
376
382
  def _(self, arg: "Concept"):
@@ -49,7 +49,7 @@
49
49
 
50
50
  //column_assignment
51
51
  //figure out if we want static
52
- column_assignment: (raw_column_assignment | IDENTIFIER | _static_functions ) ":" concept_assignment
52
+ column_assignment: (raw_column_assignment | IDENTIFIER | QUOTED_IDENTIFIER | _static_functions ) ":" concept_assignment
53
53
 
54
54
  RAW_ENTRY.1: /raw\s*\(/s
55
55
 
@@ -292,6 +292,7 @@
292
292
  // base language constructs
293
293
  concept_lit: IDENTIFIER
294
294
  IDENTIFIER: /[a-zA-Z\_][a-zA-Z0-9\_\-\.]*/
295
+ QUOTED_IDENTIFIER: /`[a-zA-Z\_][a-zA-Z0-9\_\.\-\*\:\s]*`/
295
296
  QUOTED_ADDRESS: /`[a-zA-Z\_][a-zA-Z0-9\_\.\-\*\:]*`/
296
297
  ADDRESS: IDENTIFIER
297
298