pytrilogy 0.0.2.22__py3-none-any.whl → 0.0.2.23__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.22
3
+ Version: 0.0.2.23
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,24 +1,24 @@
1
- trilogy/__init__.py,sha256=C9onmFcqcrpb9znQMhvQ84guVv4plpSoR0x6qC1SDs4,291
1
+ trilogy/__init__.py,sha256=ubM_nAcusmpKBFKEh_KcrJJbLAvprPMo6sNjwVkytbQ,291
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  trilogy/constants.py,sha256=rHCe0Pe3LuB-VwCr2765QhzkUrTqZKEYPJ7rS0ykxYw,1273
4
4
  trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
5
- trilogy/executor.py,sha256=Sv623APcNOKScYTmiiSvDcSy_ZZiKa04Wtav6dO-TFs,11760
5
+ trilogy/executor.py,sha256=b2pUL_Ha1H7pyhqssc2-hTd0OUO2KIcS0x6BLMPckZw,11822
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=W4ojA8xWRNh8frrTEYyJNLSm1rDA_O0uBL447USmF6c,6144
11
+ trilogy/core/enums.py,sha256=y0Z0m-xtcVw1ktkQ5yD3fJYWfOa4ncN_MzCTpREAxy0,6374
12
12
  trilogy/core/env_processor.py,sha256=z8pYgl5XpprA4ZzRvn7CVIG0hbMu04BlNkugKlT6i3o,2333
13
13
  trilogy/core/environment_helpers.py,sha256=1miP4is4FEoci01KSAy2VZVYmlmT5TOCOALBekd2muQ,7211
14
14
  trilogy/core/ergonomics.py,sha256=ASLDd0RqKWrZiG3XcKHo8nyTjaB_8xfE9t4NZ1UvGpc,1639
15
15
  trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,561
16
- trilogy/core/functions.py,sha256=ShFTStIKbgI-3EZIU0xTumI78AC5QlvARwnBM53P2O0,10677
16
+ trilogy/core/functions.py,sha256=IhVpt3n6wEanKHnGu3oA2w6-hKIlxWpEyz7fHN66mpo,10720
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=IzB_IYcNmEWLYdqgG6fbplM3tNQOOxhW9oBkLP4XYs4,153920
19
+ trilogy/core/models.py,sha256=Q3lhch_w1YNrBx-jl6ch5tJKFBYop42En5bghdCGWXw,155612
20
20
  trilogy/core/optimization.py,sha256=od_60A9F8J8Nj24MHgrxl4vwRwmBFH13TMdoMQvgVKs,7717
21
- trilogy/core/query_processor.py,sha256=jTYYC0LjrC0ZSFNXSa26QUGsaImwdvWx2yHFFgWQZRU,16607
21
+ trilogy/core/query_processor.py,sha256=sdG0XcHNBS0kuqUPztDZ1i-kpDV5LJLrO55Og2Y8hSg,17140
22
22
  trilogy/core/optimizations/__init__.py,sha256=bWQecbeiwiDx9LJnLsa7dkWxdbl2wcnkcTN69JyP8iI,356
23
23
  trilogy/core/optimizations/base_optimization.py,sha256=tWWT-xnTbnEU-mNi_isMNbywm8B9WTRsNFwGpeh3rqE,468
24
24
  trilogy/core/optimizations/inline_constant.py,sha256=kHNyc2UoaPVdYfVAPAFwnWuk4sJ_IF5faRtVcDOrBtw,1110
@@ -27,7 +27,7 @@ trilogy/core/optimizations/predicate_pushdown.py,sha256=1l9WnFOSv79e341typG3tTdk
27
27
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  trilogy/core/processing/concept_strategies_v3.py,sha256=J4efhZCSGSo_CXVRObn7p3Lxr5Ry_G01265amsr2iIU,35294
29
29
  trilogy/core/processing/graph_utils.py,sha256=aq-kqk4Iado2HywDxWEejWc-7PGO6Oa-ZQLAM6XWPHw,1199
30
- trilogy/core/processing/utility.py,sha256=v06sqXpnuYct_MMZXxEaiP0WwkeblWpO81QG1Ns3yGc,19420
30
+ trilogy/core/processing/utility.py,sha256=KkbyMyDucbvK6YuLfUNVlDZ-1Adl7hthHsZAXeIbWm8,19466
31
31
  trilogy/core/processing/node_generators/__init__.py,sha256=-mzYkRsaRNa_dfTckYkKVFSR8h8a3ihEiPJDU_tAmDo,672
32
32
  trilogy/core/processing/node_generators/basic_node.py,sha256=WQNgJ1MwrMS_BQ-b3XwGGB6eToDykelAVj_fesJuqe0,2069
33
33
  trilogy/core/processing/node_generators/common.py,sha256=LwDgPlhWeuw0t07f3kX9IE5LXBdZhXfh-aY0XGk50ak,8946
@@ -50,7 +50,7 @@ trilogy/core/processing/nodes/select_node_v2.py,sha256=gS9OQgS2TSEK59BQ9R0i83pTH
50
50
  trilogy/core/processing/nodes/unnest_node.py,sha256=mAmFluzm2yeeiQ6NfIB7BU_8atRGh-UJfPf9ROwbhr8,2152
51
51
  trilogy/core/processing/nodes/window_node.py,sha256=ro0QfMFi4ZmIn5Q4D0M_vJWfnHH_C0MN7XkVkx8Gygg,1214
52
52
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
- trilogy/dialect/base.py,sha256=9hT4adhR4NG98AnrSYnJ9wGN0xJvth53fd-xLuyw3nI,33151
53
+ trilogy/dialect/base.py,sha256=BDxL4eFEmkcT8Nj8W9P4anCYkAAvfsl9G01-NpI7r6w,33802
54
54
  trilogy/dialect/bigquery.py,sha256=15KJ-cOpBlk9O7FPviPgmg8xIydJeKx7WfmL3SSsPE8,2953
55
55
  trilogy/dialect/common.py,sha256=Hr0mxcNxjSvhpBM5Wvb_Q7aklAuYj5aBDrW433py0Zs,4403
56
56
  trilogy/dialect/config.py,sha256=tLVEMctaTDhUgARKXUNfHUcIolGaALkQ0RavUvXAY4w,2994
@@ -70,14 +70,14 @@ trilogy/parsing/common.py,sha256=kbqWy30nnVc7ID-sdSDwxYomnxd3guyuIJF3yvlpQwg,996
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=X1EFrHrc22dOsWHbk_5VVhViZZnF_SdmYlz_xksxplk,63751
74
- trilogy/parsing/render.py,sha256=FRC42ZV2Xg3P4pZ7dBMZCgRdFYf_QbN_CCOgnVMON_g,12395
75
- trilogy/parsing/trilogy.lark,sha256=3ElzcGWx8exOv9zJwxd_Vs_lau7g97QulwdfWldOLkA,11971
73
+ trilogy/parsing/parse_engine.py,sha256=tcBgjfew0kAfSEt1aFo9Pu3yacEBB1KFm7v_Iobz52g,64467
74
+ trilogy/parsing/render.py,sha256=7mEEe5DWVAafaGl__oQE7FPn_4QhcsGT2VVp-nk1Lr8,13078
75
+ trilogy/parsing/trilogy.lark,sha256=ZP9USPgD8-Fq5UzIl4iGpAeGuh2JLGzSoYJhvEGOi2c,12188
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.22.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
79
- pytrilogy-0.0.2.22.dist-info/METADATA,sha256=KjHfUWqKMsWICew6drUKziQGGQINSUsrXIeggDK4CIw,8403
80
- pytrilogy-0.0.2.22.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
81
- pytrilogy-0.0.2.22.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
82
- pytrilogy-0.0.2.22.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
83
- pytrilogy-0.0.2.22.dist-info/RECORD,,
78
+ pytrilogy-0.0.2.23.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
79
+ pytrilogy-0.0.2.23.dist-info/METADATA,sha256=w2tvs68fbIBkngB-SzrUElriNjj5eXa8PfsPniRU2vY,8403
80
+ pytrilogy-0.0.2.23.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
81
+ pytrilogy-0.0.2.23.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
82
+ pytrilogy-0.0.2.23.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
83
+ pytrilogy-0.0.2.23.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.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.22"
7
+ __version__ = "0.0.2.23"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/core/enums.py CHANGED
@@ -292,3 +292,13 @@ class SelectFiltering(Enum):
292
292
  NONE = "none"
293
293
  EXPLICIT = "explicit" # the filtering contains only selected values
294
294
  IMPLICIT = "implicit" # the filtering contains unselected values
295
+
296
+
297
+ class IOType(Enum):
298
+ CSV = "csv"
299
+
300
+ @classmethod
301
+ def _missing_(cls, value):
302
+ if isinstance(value, str) and value.lower() != value:
303
+ return IOType(value.lower())
304
+ return super()._missing_(value)
trilogy/core/functions.py CHANGED
@@ -104,6 +104,8 @@ def Unnest(args: list[Concept]) -> Function:
104
104
  output = arg_to_datatype(args[0])
105
105
  if isinstance(output, (ListType)):
106
106
  output = output.value_data_type
107
+ else:
108
+ output = DataType.STRING
107
109
  return Function(
108
110
  operator=FunctionType.UNNEST,
109
111
  arguments=args,
trilogy/core/models.py CHANGED
@@ -65,6 +65,7 @@ from trilogy.core.enums import (
65
65
  ShowCategory,
66
66
  Granularity,
67
67
  SelectFiltering,
68
+ IOType,
68
69
  )
69
70
  from trilogy.core.exceptions import UndefinedConceptException, InvalidSyntaxException
70
71
  from trilogy.utility import unique
@@ -81,6 +82,9 @@ LT = TypeVar("LT")
81
82
 
82
83
 
83
84
  def is_compatible_datatype(left, right):
85
+ # for unknown types, we can't make any assumptions
86
+ if right == DataType.UNKNOWN or left == DataType.UNKNOWN:
87
+ return True
84
88
  if left == right:
85
89
  return True
86
90
  if {left, right} == {DataType.NUMERIC, DataType.FLOAT}:
@@ -1580,6 +1584,13 @@ class RawSQLStatement(BaseModel):
1580
1584
  meta: Optional[Metadata] = Field(default_factory=lambda: Metadata())
1581
1585
 
1582
1586
 
1587
+ class CopyStatement(BaseModel):
1588
+ target: str
1589
+ target_type: IOType
1590
+ meta: Optional[Metadata] = Field(default_factory=lambda: Metadata())
1591
+ select: SelectStatement
1592
+
1593
+
1583
1594
  class SelectStatement(Mergeable, Namespaced, SelectTypeMixin, BaseModel):
1584
1595
  selection: List[SelectItem]
1585
1596
  order_by: Optional[OrderBy] = None
@@ -3599,6 +3610,7 @@ class Comparison(
3599
3610
  MagicConstants,
3600
3611
  WindowItem,
3601
3612
  AggregateWrapper,
3613
+ TupleWrapper,
3602
3614
  ]
3603
3615
  operator: ComparisonOperator
3604
3616
 
@@ -4258,13 +4270,23 @@ class ProcessedQuery(BaseModel):
4258
4270
  order_by: Optional[OrderBy] = None
4259
4271
 
4260
4272
 
4261
- class ProcessedQueryMixin(BaseModel):
4273
+ class PersistQueryMixin(BaseModel):
4262
4274
  output_to: MaterializedDataset
4263
4275
  datasource: Datasource
4264
4276
  # base:Dataset
4265
4277
 
4266
4278
 
4267
- class ProcessedQueryPersist(ProcessedQuery, ProcessedQueryMixin):
4279
+ class ProcessedQueryPersist(ProcessedQuery, PersistQueryMixin):
4280
+ pass
4281
+
4282
+
4283
+ class CopyQueryMixin(BaseModel):
4284
+ target: str
4285
+ target_type: IOType
4286
+ # base:Dataset
4287
+
4288
+
4289
+ class ProcessedCopyStatement(ProcessedQuery, CopyQueryMixin):
4268
4290
  pass
4269
4291
 
4270
4292
 
@@ -4523,6 +4545,37 @@ class Parenthetical(
4523
4545
  return base
4524
4546
 
4525
4547
 
4548
+ class TupleWrapper(Generic[VT], tuple):
4549
+ """Used to distinguish parsed tuple objects from other tuples"""
4550
+
4551
+ def __init__(self, val, type: DataType, **kwargs):
4552
+ super().__init__()
4553
+ self.type = type
4554
+ self.val = val
4555
+
4556
+ def __getnewargs__(self):
4557
+ return (self.val, self.type)
4558
+
4559
+ def __new__(cls, val, type: DataType, **kwargs):
4560
+ return super().__new__(cls, tuple(val))
4561
+ # self.type = type
4562
+
4563
+ @classmethod
4564
+ def __get_pydantic_core_schema__(
4565
+ cls, source_type: Any, handler: Callable[[Any], core_schema.CoreSchema]
4566
+ ) -> core_schema.CoreSchema:
4567
+ args = get_args(source_type)
4568
+ if args:
4569
+ schema = handler(Tuple[args]) # type: ignore
4570
+ else:
4571
+ schema = handler(Tuple)
4572
+ return core_schema.no_info_after_validator_function(cls.validate, schema)
4573
+
4574
+ @classmethod
4575
+ def validate(cls, v):
4576
+ return cls(v, type=arg_to_datatype(v[0]))
4577
+
4578
+
4526
4579
  class PersistStatement(BaseModel):
4527
4580
  datasource: Datasource
4528
4581
  select: SelectStatement
@@ -4589,6 +4642,12 @@ def list_to_wrapper(args):
4589
4642
  return ListWrapper(args, type=types[0])
4590
4643
 
4591
4644
 
4645
+ def tuple_to_wrapper(args):
4646
+ types = [arg_to_datatype(arg) for arg in args]
4647
+ assert len(set(types)) == 1
4648
+ return TupleWrapper(args, type=types[0])
4649
+
4650
+
4592
4651
  def dict_to_map_wrapper(arg):
4593
4652
  key_types = [arg_to_datatype(arg) for arg in arg.keys()]
4594
4653
 
@@ -4644,6 +4703,8 @@ def arg_to_datatype(arg) -> DataType | ListType | StructType | MapType | Numeric
4644
4703
  return arg.function.output_datatype
4645
4704
  elif isinstance(arg, Parenthetical):
4646
4705
  return arg_to_datatype(arg.content)
4706
+ elif isinstance(arg, TupleWrapper):
4707
+ return ListType(type=arg.type)
4647
4708
  elif isinstance(arg, WindowItem):
4648
4709
  if arg.type in (WindowType.RANK, WindowType.ROW_NUMBER):
4649
4710
  return DataType.INTEGER
@@ -28,6 +28,7 @@ from trilogy.core.models import (
28
28
  DatePart,
29
29
  NumericType,
30
30
  ListType,
31
+ TupleWrapper,
31
32
  )
32
33
 
33
34
  from trilogy.core.enums import Purpose, Granularity, BooleanOperator, Modifier
@@ -422,6 +423,7 @@ def is_scalar_condition(
422
423
  | NumericType
423
424
  | DatePart
424
425
  | ListWrapper[Any]
426
+ | TupleWrapper[Any]
425
427
  ),
426
428
  materialized: set[str] | None = None,
427
429
  ) -> bool:
@@ -26,6 +26,8 @@ from trilogy.core.models import (
26
26
  BaseJoin,
27
27
  InstantiatedUnnestJoin,
28
28
  Conditional,
29
+ ProcessedCopyStatement,
30
+ CopyStatement,
29
31
  )
30
32
 
31
33
  from trilogy.utility import unique
@@ -418,6 +420,24 @@ def process_persist(
418
420
  )
419
421
 
420
422
 
423
+ def process_copy(
424
+ environment: Environment,
425
+ statement: CopyStatement,
426
+ hooks: List[BaseHook] | None = None,
427
+ ) -> ProcessedCopyStatement:
428
+ select = process_query(
429
+ environment=environment, statement=statement.select, hooks=hooks
430
+ )
431
+
432
+ # build our object to return
433
+ arg_dict = {k: v for k, v in select.__dict__.items()}
434
+ return ProcessedCopyStatement(
435
+ **arg_dict,
436
+ target=statement.target,
437
+ target_type=statement.target_type,
438
+ )
439
+
440
+
421
441
  def process_query(
422
442
  environment: Environment,
423
443
  statement: SelectStatement | MultiSelectStatement,
trilogy/dialect/base.py CHANGED
@@ -35,6 +35,7 @@ from trilogy.core.models import (
35
35
  Environment,
36
36
  RawColumnExpr,
37
37
  ListWrapper,
38
+ TupleWrapper,
38
39
  MapWrapper,
39
40
  ShowStatement,
40
41
  RowsetItem,
@@ -49,8 +50,10 @@ from trilogy.core.models import (
49
50
  StructType,
50
51
  MergeStatementV2,
51
52
  Datasource,
53
+ CopyStatement,
54
+ ProcessedCopyStatement,
52
55
  )
53
- from trilogy.core.query_processor import process_query, process_persist
56
+ from trilogy.core.query_processor import process_query, process_persist, process_copy
54
57
  from trilogy.dialect.common import render_join, render_unnest
55
58
  from trilogy.hooks.base_hook import BaseHook
56
59
  from trilogy.core.enums import UnnestMode
@@ -391,6 +394,7 @@ class BaseDialect:
391
394
  StructType,
392
395
  ListType,
393
396
  ListWrapper[Any],
397
+ TupleWrapper[Any],
394
398
  DatePart,
395
399
  CaseWhen,
396
400
  CaseElse,
@@ -430,7 +434,7 @@ class BaseDialect:
430
434
  f"Missing source CTE for {e.right.address}"
431
435
  )
432
436
  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 {target} where {target}.{self.QUOTE_CHARACTER}{e.right.safe_address}{self.QUOTE_CHARACTER} is not null)"
433
- elif isinstance(e.right, (ListWrapper, Parenthetical, list)):
437
+ elif isinstance(e.right, (ListWrapper, TupleWrapper, Parenthetical, list)):
434
438
  return f"{self.render_expr(e.left, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)} {e.operator.value} {self.render_expr(e.right, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}"
435
439
 
436
440
  elif isinstance(
@@ -511,6 +515,8 @@ class BaseDialect:
511
515
  return str(e)
512
516
  elif isinstance(e, ListWrapper):
513
517
  return f"[{','.join([self.render_expr(x, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) for x in e])}]"
518
+ elif isinstance(e, TupleWrapper):
519
+ return f"({','.join([self.render_expr(x, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) for x in e])})"
514
520
  elif isinstance(e, MapWrapper):
515
521
  return f"MAP {{{','.join([f'{self.render_expr(k, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}:{self.render_expr(v, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid)}' for k, v in e.items()])}}}"
516
522
  elif isinstance(e, list):
@@ -662,6 +668,7 @@ class BaseDialect:
662
668
  | ImportStatement
663
669
  | RawSQLStatement
664
670
  | MergeStatementV2
671
+ | CopyStatement
665
672
  ],
666
673
  hooks: Optional[List[BaseHook]] = None,
667
674
  ) -> List[
@@ -675,6 +682,7 @@ class BaseDialect:
675
682
  | ProcessedQueryPersist
676
683
  | ProcessedShowStatement
677
684
  | ProcessedRawSQLStatement
685
+ | ProcessedCopyStatement
678
686
  ] = []
679
687
  for statement in statements:
680
688
  if isinstance(statement, PersistStatement):
@@ -683,6 +691,12 @@ class BaseDialect:
683
691
  hook.process_persist_info(statement)
684
692
  persist = process_persist(environment, statement, hooks=hooks)
685
693
  output.append(persist)
694
+ elif isinstance(statement, CopyStatement):
695
+ if hooks:
696
+ for hook in hooks:
697
+ hook.process_select_info(statement.select)
698
+ copy = process_copy(environment, statement, hooks=hooks)
699
+ output.append(copy)
686
700
  elif isinstance(statement, SelectStatement):
687
701
  if hooks:
688
702
  for hook in hooks:
trilogy/executor.py CHANGED
@@ -10,6 +10,7 @@ from trilogy.core.models import (
10
10
  ProcessedShowStatement,
11
11
  ProcessedQueryPersist,
12
12
  ProcessedRawSQLStatement,
13
+ ProcessedCopyStatement,
13
14
  RawSQLStatement,
14
15
  MultiSelectStatement,
15
16
  SelectStatement,
@@ -18,9 +19,11 @@ from trilogy.core.models import (
18
19
  Concept,
19
20
  ConceptDeclarationStatement,
20
21
  Datasource,
22
+ CopyStatement,
21
23
  )
22
24
  from trilogy.dialect.base import BaseDialect
23
25
  from trilogy.dialect.enums import Dialects
26
+ from trilogy.core.enums import IOType
24
27
  from trilogy.parser import parse_text
25
28
  from trilogy.hooks.base_hook import BaseHook
26
29
  from pathlib import Path
@@ -94,7 +97,15 @@ class Executor(object):
94
97
  self.connection = self.engine.connect()
95
98
 
96
99
  def execute_statement(self, statement) -> Optional[CursorResult]:
97
- if not isinstance(statement, (ProcessedQuery, ProcessedQueryPersist)):
100
+ if not isinstance(
101
+ statement,
102
+ (
103
+ ProcessedQuery,
104
+ ProcessedShowStatement,
105
+ ProcessedQueryPersist,
106
+ ProcessedCopyStatement,
107
+ ),
108
+ ):
98
109
  return None
99
110
  return self.execute_query(statement)
100
111
 
@@ -183,12 +194,33 @@ class Executor(object):
183
194
 
184
195
  @execute_query.register
185
196
  def _(self, query: ProcessedQueryPersist) -> CursorResult:
197
+
186
198
  sql = self.generator.compile_statement(query)
187
- # connection = self.engine.connect()
199
+
188
200
  output = self.connection.execute(text(sql))
189
201
  self.environment.add_datasource(query.datasource)
190
202
  return output
191
203
 
204
+ @execute_query.register
205
+ def _(self, query: ProcessedCopyStatement) -> CursorResult:
206
+ sql = self.generator.compile_statement(query)
207
+ output: CursorResult = self.connection.execute(text(sql))
208
+ if query.target_type == IOType.CSV:
209
+ import csv
210
+
211
+ with open(query.target, "w", newline="", encoding="utf-8") as f:
212
+ outcsv = csv.writer(f)
213
+ outcsv.writerow(output.keys())
214
+ outcsv.writerows(output)
215
+ else:
216
+ raise NotImplementedError(f"Unsupported IOType {query.target_type}")
217
+ # now return the query we ran through IO
218
+ # TODO: instead return how many rows were written?
219
+ return generate_result_set(
220
+ query.output_columns,
221
+ [self.generator.compile_statement(query)],
222
+ )
223
+
192
224
  @singledispatchmethod
193
225
  def generate_sql(self, command) -> list[str]:
194
226
  raise NotImplementedError(
@@ -251,39 +283,17 @@ class Executor(object):
251
283
  | ProcessedQueryPersist
252
284
  | ProcessedShowStatement
253
285
  | ProcessedRawSQLStatement
286
+ | ProcessedCopyStatement
254
287
  ]:
255
- """Process a preql text command"""
256
- _, parsed = parse_text(command, self.environment)
257
- generatable = [
258
- x
259
- for x in parsed
260
- if isinstance(
261
- x,
262
- (
263
- SelectStatement,
264
- PersistStatement,
265
- MultiSelectStatement,
266
- ShowStatement,
267
- RawSQLStatement,
268
- ),
269
- )
270
- ]
271
- sql = []
272
- while generatable:
273
- t = generatable.pop(0)
274
- x = self.generator.generate_queries(
275
- self.environment, [t], hooks=self.hooks
276
- )[0]
277
- if persist and isinstance(x, ProcessedQueryPersist):
278
- self.environment.add_datasource(x.datasource)
279
- sql.append(x)
280
- return sql
288
+
289
+ return list(self.parse_text_generator(command, persist=persist))
281
290
 
282
291
  def parse_text_generator(self, command: str, persist: bool = False) -> Generator[
283
292
  ProcessedQuery
284
293
  | ProcessedQueryPersist
285
294
  | ProcessedShowStatement
286
- | ProcessedRawSQLStatement,
295
+ | ProcessedRawSQLStatement
296
+ | ProcessedCopyStatement,
287
297
  None,
288
298
  None,
289
299
  ]:
@@ -300,6 +310,7 @@ class Executor(object):
300
310
  MultiSelectStatement,
301
311
  ShowStatement,
302
312
  RawSQLStatement,
313
+ CopyStatement,
303
314
  ),
304
315
  )
305
316
  ]
@@ -340,13 +351,7 @@ class Executor(object):
340
351
  )
341
352
  )
342
353
  continue
343
- compiled_sql = self.generator.compile_statement(statement)
344
- logger.debug(compiled_sql)
345
-
346
- output.append(self.connection.execute(text(compiled_sql)))
347
- # generalize post-run success hooks
348
- if isinstance(statement, ProcessedQueryPersist):
349
- self.environment.add_datasource(statement.datasource)
354
+ output.append(self.execute_query(statement))
350
355
  return output
351
356
 
352
357
  def execute_file(self, file: str | Path) -> List[CursorResult]:
@@ -1,7 +1,7 @@
1
1
  from os.path import dirname, join
2
2
  from typing import List, Optional, Tuple, Union
3
3
  from re import IGNORECASE
4
- from lark import Lark, Transformer, v_args
4
+ from lark import Lark, Transformer, v_args, Tree
5
5
  from lark.exceptions import (
6
6
  UnexpectedCharacters,
7
7
  UnexpectedEOF,
@@ -31,6 +31,7 @@ from trilogy.core.enums import (
31
31
  DatePart,
32
32
  ShowCategory,
33
33
  FunctionClass,
34
+ IOType,
34
35
  )
35
36
  from trilogy.core.exceptions import InvalidSyntaxException, UndefinedConceptException
36
37
  from trilogy.core.functions import (
@@ -84,6 +85,7 @@ from trilogy.core.models import (
84
85
  PersistStatement,
85
86
  Query,
86
87
  RawSQLStatement,
88
+ CopyStatement,
87
89
  SelectStatement,
88
90
  SelectItem,
89
91
  WhereClause,
@@ -105,9 +107,11 @@ from trilogy.core.models import (
105
107
  ConceptDerivation,
106
108
  RowsetDerivationStatement,
107
109
  list_to_wrapper,
110
+ tuple_to_wrapper,
108
111
  dict_to_map_wrapper,
109
112
  NumericType,
110
113
  HavingClause,
114
+ TupleWrapper,
111
115
  )
112
116
  from trilogy.parsing.exceptions import ParseError
113
117
  from trilogy.parsing.common import (
@@ -748,13 +752,29 @@ class ParseToObjects(Transformer):
748
752
  def rawsql_statement(self, meta: Meta, args) -> RawSQLStatement:
749
753
  return RawSQLStatement(meta=Metadata(line_number=meta.line), text=args[0])
750
754
 
755
+ def COPY_TYPE(self, args) -> IOType:
756
+ return IOType(args.value)
757
+
758
+ @v_args(meta=True)
759
+ def copy_statement(self, meta: Meta, args) -> CopyStatement:
760
+
761
+ return CopyStatement(
762
+ target=args[1],
763
+ target_type=args[0],
764
+ meta=Metadata(line_number=meta.line),
765
+ select=args[-1],
766
+ )
767
+
751
768
  def resolve_import_address(self, address) -> str:
752
769
  with open(address, "r", encoding="utf-8") as f:
753
770
  text = f.read()
754
771
  return text
755
772
 
756
773
  def import_statement(self, args: list[str]) -> ImportStatement:
757
- alias = args[-1]
774
+ if len(args) == 2:
775
+ alias = args[-1]
776
+ else:
777
+ alias = self.environment.namespace
758
778
  path = args[0].split(".")
759
779
 
760
780
  target = join(self.environment.working_path, *path) + ".preql"
@@ -1064,6 +1084,9 @@ class ParseToObjects(Transformer):
1064
1084
  def array_lit(self, args):
1065
1085
  return list_to_wrapper(args)
1066
1086
 
1087
+ def tuple_lit(self, args):
1088
+ return tuple_to_wrapper(args)
1089
+
1067
1090
  def struct_lit(self, args):
1068
1091
 
1069
1092
  zipped = dict(zip(args[::2], args[1::2]))
@@ -1124,12 +1147,18 @@ class ParseToObjects(Transformer):
1124
1147
 
1125
1148
  while isinstance(right, Parenthetical) and isinstance(
1126
1149
  right.content,
1127
- (Concept, Function, FilterItem, WindowItem, AggregateWrapper, ListWrapper),
1150
+ (
1151
+ Concept,
1152
+ Function,
1153
+ FilterItem,
1154
+ WindowItem,
1155
+ AggregateWrapper,
1156
+ ListWrapper,
1157
+ TupleWrapper,
1158
+ ),
1128
1159
  ):
1129
1160
  right = right.content
1130
- if isinstance(
1131
- right, (Function, FilterItem, WindowItem, AggregateWrapper, ListWrapper)
1132
- ):
1161
+ if isinstance(right, (Function, FilterItem, WindowItem, AggregateWrapper)):
1133
1162
  right = arbitrary_to_concept(
1134
1163
  right,
1135
1164
  namespace=self.environment.namespace,
@@ -1142,7 +1171,7 @@ class ParseToObjects(Transformer):
1142
1171
  )
1143
1172
 
1144
1173
  def expr_tuple(self, args):
1145
- return Parenthetical(content=args)
1174
+ return TupleWrapper(content=tuple(args))
1146
1175
 
1147
1176
  def parenthetical(self, args):
1148
1177
  return Parenthetical(content=args[0])
@@ -1840,10 +1869,12 @@ def unpack_visit_error(e: VisitError):
1840
1869
  unpack_visit_error(e.orig_exc)
1841
1870
  elif isinstance(e.orig_exc, (UndefinedConceptException, ImportError)):
1842
1871
  raise e.orig_exc
1843
- elif isinstance(e.orig_exc, SyntaxError):
1844
- raise InvalidSyntaxException(str(e.orig_exc) + str(e.rule) + str(e.obj))
1845
- elif isinstance(e.orig_exc, (ValidationError, TypeError)):
1846
- raise InvalidSyntaxException(str(e.orig_exc) + str(e.rule) + str(e.obj))
1872
+ elif isinstance(e.orig_exc, (SyntaxError, TypeError)):
1873
+ if isinstance(e.obj, Tree):
1874
+ raise InvalidSyntaxException(
1875
+ str(e.orig_exc) + " in " + str(e.rule) + f" Line: {e.obj.meta.line}"
1876
+ )
1877
+ raise InvalidSyntaxException(str(e.orig_exc))
1847
1878
  raise e
1848
1879
 
1849
1880
 
trilogy/parsing/render.py CHANGED
@@ -32,6 +32,8 @@ from trilogy.core.models import (
32
32
  AggregateWrapper,
33
33
  PersistStatement,
34
34
  ListWrapper,
35
+ ListType,
36
+ TupleWrapper,
35
37
  RowsetDerivationStatement,
36
38
  MultiSelectStatement,
37
39
  OrderBy,
@@ -40,6 +42,7 @@ from trilogy.core.models import (
40
42
  RawSQLStatement,
41
43
  NumericType,
42
44
  MergeStatementV2,
45
+ CopyStatement,
43
46
  )
44
47
  from trilogy.core.enums import Modifier
45
48
 
@@ -180,6 +183,10 @@ class Renderer:
180
183
  def _(self, arg: ListWrapper):
181
184
  return "[" + ", ".join([self.to_string(x) for x in arg]) + "]"
182
185
 
186
+ @to_string.register
187
+ def _(self, arg: TupleWrapper):
188
+ return "(" + ", ".join([self.to_string(x) for x in arg]) + ")"
189
+
183
190
  @to_string.register
184
191
  def _(self, arg: DatePart):
185
192
  return arg.value
@@ -211,21 +218,30 @@ class Renderer:
211
218
  base_description = concept.metadata.description
212
219
  else:
213
220
  base_description = None
214
- if concept.namespace:
221
+ if concept.namespace and concept.namespace != DEFAULT_NAMESPACE:
215
222
  namespace = f"{concept.namespace}."
216
223
  else:
217
224
  namespace = ""
218
225
  if not concept.lineage:
219
226
  if concept.purpose == Purpose.PROPERTY and concept.keys:
220
- output = f"{concept.purpose.value} {namespace}{concept.keys[0].name}.{concept.name} {concept.datatype.value};"
227
+ keys = ",".join([self.to_string(key) for key in concept.keys])
228
+ output = f"{concept.purpose.value} <{keys}>.{namespace}{concept.name} {self.to_string(concept.datatype)};"
221
229
  else:
222
- output = f"{concept.purpose.value} {namespace}{concept.name} {concept.datatype.value};"
230
+ output = f"{concept.purpose.value} {namespace}{concept.name} {self.to_string(concept.datatype)};"
223
231
  else:
224
232
  output = f"{concept.purpose.value} {namespace}{concept.name} <- {self.to_string(concept.lineage)};"
225
233
  if base_description:
226
234
  output += f" # {base_description}"
227
235
  return output
228
236
 
237
+ @to_string.register
238
+ def _(self, arg: ListType):
239
+ return f"list<{self.to_string(arg.value_data_type)}>"
240
+
241
+ @to_string.register
242
+ def _(self, arg: DataType):
243
+ return arg.value
244
+
229
245
  @to_string.register
230
246
  def _(self, arg: ConceptDerivation):
231
247
  # this is identical rendering;
@@ -271,6 +287,10 @@ class Renderer:
271
287
  base += "\n;"
272
288
  return base
273
289
 
290
+ @to_string.register
291
+ def _(self, arg: CopyStatement):
292
+ return f"COPY INTO {arg.target_type.value.upper()} '{arg.target}' FROM {self.to_string(arg.select)}"
293
+
274
294
  @to_string.register
275
295
  def _(self, arg: AlignClause):
276
296
  return "\nALIGN\n\t" + ",\n\t".join([self.to_string(c) for c in arg.items])
@@ -8,7 +8,7 @@
8
8
  | persist_statement
9
9
  | rowset_derivation_statement
10
10
  | import_statement
11
-
11
+ | copy_statement
12
12
  | merge_statement_v2
13
13
  | rawsql_statement
14
14
 
@@ -57,7 +57,7 @@
57
57
 
58
58
  column_list : (IDENTIFIER "," )* IDENTIFIER ","?
59
59
 
60
- import_statement: "import" (IDENTIFIER ".") * IDENTIFIER "as" IDENTIFIER
60
+ import_statement: "import" IDENTIFIER ("." IDENTIFIER)* ("as" IDENTIFIER)?
61
61
 
62
62
  // persist_statement
63
63
  persist_statement: "persist"i IDENTIFIER "into"i IDENTIFIER "from"i select_statement grain_clause?
@@ -78,6 +78,12 @@
78
78
  // raw sql statement
79
79
  rawsql_statement: "raw_sql"i "(" MULTILINE_STRING ")"
80
80
 
81
+ // copy statement
82
+
83
+ COPY_TYPE: "csv"i
84
+
85
+ copy_statement: "copy"i "into"i COPY_TYPE _string_lit "from"i select_statement
86
+
81
87
  // FUNCTION blocks
82
88
  function: raw_function
83
89
  function_binding_item: IDENTIFIER ":" data_type
@@ -303,6 +309,8 @@
303
309
 
304
310
  array_lit: "[" (literal ",")* literal ","? "]"()
305
311
 
312
+ tuple_lit: "(" (literal ",")* literal ","? ")"
313
+
306
314
  map_lit: "{" (literal ":" literal ",")* literal ":" literal ","? "}"
307
315
 
308
316
  _STRUCT.1: "struct("i
@@ -312,7 +320,7 @@
312
320
 
313
321
  !null_lit.1: "null"i
314
322
 
315
- literal: null_lit | _string_lit | int_lit | float_lit | bool_lit | array_lit | map_lit | struct_lit
323
+ literal: null_lit | _string_lit | int_lit | float_lit | bool_lit | array_lit | map_lit | struct_lit | tuple_lit
316
324
 
317
325
  MODIFIER: "Optional"i | "Partial"i | "Nullable"i
318
326