pytrilogy 0.0.2.22__tar.gz → 0.0.2.23__tar.gz

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.

Files changed (108) hide show
  1. {pytrilogy-0.0.2.22/pytrilogy.egg-info → pytrilogy-0.0.2.23}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. pytrilogy-0.0.2.23/tests/test_imports.py +42 -0
  4. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_models.py +9 -0
  5. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_parsing.py +31 -13
  6. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_show.py +0 -1
  7. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_statements.py +21 -0
  8. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/__init__.py +1 -1
  9. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/enums.py +10 -0
  10. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/functions.py +2 -0
  11. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/models.py +63 -2
  12. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/utility.py +2 -0
  13. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/query_processor.py +20 -0
  14. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/dialect/base.py +16 -2
  15. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/executor.py +41 -36
  16. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/parsing/parse_engine.py +42 -11
  17. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/parsing/render.py +23 -3
  18. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/parsing/trilogy.lark +11 -3
  19. pytrilogy-0.0.2.22/tests/test_imports.py +0 -23
  20. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/LICENSE.md +0 -0
  21. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/README.md +0 -0
  22. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/pyproject.toml +0 -0
  23. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/pytrilogy.egg-info/SOURCES.txt +0 -0
  24. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/pytrilogy.egg-info/dependency_links.txt +0 -0
  25. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/pytrilogy.egg-info/entry_points.txt +0 -0
  26. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/pytrilogy.egg-info/requires.txt +0 -0
  27. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/pytrilogy.egg-info/top_level.txt +0 -0
  28. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/setup.cfg +0 -0
  29. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/setup.py +0 -0
  30. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_datatypes.py +0 -0
  31. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_declarations.py +0 -0
  32. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_derived_concepts.py +0 -0
  33. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_discovery_nodes.py +0 -0
  34. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_environment.py +0 -0
  35. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_functions.py +0 -0
  36. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_metadata.py +0 -0
  37. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_multi_join_assignments.py +0 -0
  38. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_partial_handling.py +0 -0
  39. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_query_processing.py +0 -0
  40. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_select.py +0 -0
  41. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_undefined_concept.py +0 -0
  42. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/tests/test_where_clause.py +0 -0
  43. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/compiler.py +0 -0
  44. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/constants.py +0 -0
  45. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/__init__.py +0 -0
  46. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/constants.py +0 -0
  47. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/env_processor.py +0 -0
  48. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/environment_helpers.py +0 -0
  49. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/ergonomics.py +0 -0
  50. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/exceptions.py +0 -0
  51. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/graph_models.py +0 -0
  52. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/internal.py +0 -0
  53. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/optimization.py +0 -0
  54. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/optimizations/__init__.py +0 -0
  55. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/optimizations/base_optimization.py +0 -0
  56. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/optimizations/inline_constant.py +0 -0
  57. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/optimizations/inline_datasource.py +0 -0
  58. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  59. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/__init__.py +0 -0
  60. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/concept_strategies_v3.py +0 -0
  61. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/graph_utils.py +0 -0
  62. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/node_generators/__init__.py +0 -0
  63. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  64. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/node_generators/common.py +0 -0
  65. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  66. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/node_generators/group_node.py +0 -0
  67. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  68. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  69. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  70. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  71. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  72. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/node_generators/select_node.py +0 -0
  73. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  74. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/node_generators/window_node.py +0 -0
  75. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/nodes/__init__.py +0 -0
  76. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/nodes/base_node.py +0 -0
  77. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/nodes/filter_node.py +0 -0
  78. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/nodes/group_node.py +0 -0
  79. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/nodes/merge_node.py +0 -0
  80. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  81. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  82. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/core/processing/nodes/window_node.py +0 -0
  83. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/dialect/__init__.py +0 -0
  84. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/dialect/bigquery.py +0 -0
  85. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/dialect/common.py +0 -0
  86. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/dialect/config.py +0 -0
  87. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/dialect/duckdb.py +0 -0
  88. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/dialect/enums.py +0 -0
  89. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/dialect/postgres.py +0 -0
  90. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/dialect/presto.py +0 -0
  91. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/dialect/snowflake.py +0 -0
  92. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/dialect/sql_server.py +0 -0
  93. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/engine.py +0 -0
  94. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/hooks/__init__.py +0 -0
  95. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/hooks/base_hook.py +0 -0
  96. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/hooks/graph_hook.py +0 -0
  97. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/hooks/query_debugger.py +0 -0
  98. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/metadata/__init__.py +0 -0
  99. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/parser.py +0 -0
  100. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/parsing/__init__.py +0 -0
  101. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/parsing/common.py +0 -0
  102. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/parsing/config.py +0 -0
  103. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/parsing/exceptions.py +0 -0
  104. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/parsing/helpers.py +0 -0
  105. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/py.typed +0 -0
  106. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/scripts/__init__.py +0 -0
  107. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/scripts/trilogy.py +0 -0
  108. {pytrilogy-0.0.2.22 → pytrilogy-0.0.2.23}/trilogy/utility.py +0 -0
@@ -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,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:
@@ -0,0 +1,42 @@
1
+ from trilogy import Environment
2
+ from pathlib import Path
3
+
4
+
5
+ def test_multi_environment():
6
+ basic = Environment()
7
+
8
+ basic.parse(
9
+ """
10
+ const pi <- 3.14;
11
+
12
+
13
+ """,
14
+ namespace="math",
15
+ )
16
+
17
+ basic.parse(
18
+ """
19
+
20
+ select math.pi;
21
+ """
22
+ )
23
+
24
+ assert basic.concepts["math.pi"].name == "pi"
25
+
26
+
27
+ def test_test_alias_free_import():
28
+ basic = Environment(working_path=Path(__file__).parent)
29
+
30
+ basic.parse(
31
+ """
32
+ import test_env;
33
+
34
+ key id2 int;
35
+
36
+
37
+ """,
38
+ )
39
+
40
+ assert basic.concepts["id"].name == "id"
41
+ assert basic.concepts["id2"].name == "id2"
42
+ assert basic.concepts["id"].namespace == basic.concepts["id2"].namespace
@@ -15,8 +15,11 @@ from trilogy.core.models import (
15
15
  Concept,
16
16
  AggregateWrapper,
17
17
  RowsetItem,
18
+ TupleWrapper,
19
+ DataType,
18
20
  )
19
21
  from trilogy import parse
22
+ from copy import deepcopy
20
23
 
21
24
 
22
25
  def test_cte_merge(test_environment, test_environment_graph):
@@ -312,3 +315,9 @@ select avg_greater_ten;
312
315
  lineage = env.concepts["avg_greater_ten"].lineage
313
316
  assert isinstance(lineage, AggregateWrapper)
314
317
  assert isinstance(lineage.function.concept_arguments[0].lineage, RowsetItem)
318
+
319
+
320
+ def test_tuple_clone():
321
+ x = TupleWrapper([1, 2, 3], type=DataType.INTEGER)
322
+ y = deepcopy(x)
323
+ assert y == x
@@ -1,12 +1,12 @@
1
1
  from trilogy.core.enums import Purpose, ComparisonOperator
2
2
  from trilogy.core.models import (
3
3
  DataType,
4
- Parenthetical,
5
4
  ProcessedQuery,
6
5
  ShowStatement,
7
6
  SelectStatement,
8
7
  Environment,
9
8
  Comparison,
9
+ TupleWrapper,
10
10
  )
11
11
  from trilogy.core.functions import argument_to_purpose, function_args_to_output_purpose
12
12
  from trilogy.parsing.parse_engine import (
@@ -27,11 +27,11 @@ def test_in():
27
27
  right = query.where_clause.conditional.right
28
28
  assert isinstance(
29
29
  right,
30
- Parenthetical,
30
+ TupleWrapper,
31
31
  ), type(right)
32
- assert right.content[0] == 1
32
+ assert right[0] == 1
33
33
  rendered = BaseDialect().render_expr(right)
34
- assert rendered.strip() == "( 1,2,3 )".strip()
34
+ assert rendered.strip() == "(1,2,3)".strip()
35
35
 
36
36
  _, parsed = parse_text(
37
37
  "const order_id <- 3; SELECT order_id WHERE order_id IN (1,);"
@@ -40,11 +40,11 @@ def test_in():
40
40
  right = query.where_clause.conditional.right
41
41
  assert isinstance(
42
42
  right,
43
- Parenthetical,
43
+ TupleWrapper,
44
44
  ), type(right)
45
- assert right.content[0] == 1
45
+ assert right[0] == 1
46
46
  rendered = BaseDialect().render_expr(right)
47
- assert rendered.strip() == "( 1 )".strip()
47
+ assert rendered.strip() == "(1)".strip()
48
48
 
49
49
 
50
50
  def test_not_in():
@@ -53,13 +53,10 @@ def test_not_in():
53
53
  )
54
54
  query: ProcessedQuery = parsed[-1]
55
55
  right = query.where_clause.conditional.right
56
- assert isinstance(
57
- right,
58
- Parenthetical,
59
- ), type(right)
60
- assert right.content[0] == 1
56
+ assert isinstance(right, TupleWrapper), type(right)
57
+ assert right[0] == 1
61
58
  rendered = BaseDialect().render_expr(right)
62
- assert rendered.strip() == "( 1,2,3 )".strip()
59
+ assert rendered.strip() == "(1,2,3)".strip()
63
60
 
64
61
 
65
62
  def test_is_not_null():
@@ -516,3 +513,24 @@ select filtered_test;
516
513
  results = Dialects.DUCK_DB.default_executor().generate_sql(text)[0]
517
514
 
518
515
  assert "filtered_test" in results, results
516
+
517
+
518
+ def test_unnest_parsing():
519
+ x = """
520
+ key scalar int;
521
+ property scalar.int_array list<int>;
522
+
523
+ key split <- unnest(int_array);
524
+
525
+ datasource avalues (
526
+ int_array: int_array,
527
+ scalar: scalar
528
+ )
529
+ grain (scalar)
530
+ query '''(
531
+ select [1,2,3,4] as int_array, 2 as scalar
532
+ )''';
533
+ """
534
+
535
+ env, parsed = parse_text(x)
536
+ assert env.concepts["split"].datatype == DataType.INTEGER
@@ -1,5 +1,4 @@
1
1
  from trilogy.core.models import ShowStatement
2
- from trilogy.core.query_processor import process_query
3
2
  from trilogy import Dialects
4
3
  from trilogy.parser import parse
5
4
 
@@ -1,4 +1,7 @@
1
1
  from trilogy.parser import parse
2
+ from trilogy import Dialects
3
+ from trilogy.core.models import ProcessedCopyStatement
4
+ from pathlib import Path
2
5
 
3
6
  # from trilogy.compiler import compile
4
7
 
@@ -43,6 +46,24 @@ datasource posts (
43
46
  parse(text)
44
47
 
45
48
 
49
+ def test_io_statement():
50
+ target = Path(__file__).parent / "test_io_statement.csv"
51
+ if target.exists():
52
+ target.unlink()
53
+ text = f"""const array <- [1,2,3,4];
54
+
55
+ auto x <- unnest(array);
56
+
57
+ copy into csv '{target}' from select x -> test;
58
+ """
59
+ exec = Dialects.DUCK_DB.default_executor()
60
+ results = exec.parse_text(text)
61
+ assert isinstance(results[-1], ProcessedCopyStatement)
62
+ for z in results:
63
+ exec.execute_query(z)
64
+ assert target.exists(), "csv file was not created"
65
+
66
+
46
67
  def test_datasource_where():
47
68
  text = """key user_id int metadata(description="the description");
48
69
  property user_id.display_name string metadata(description="The display name ");
@@ -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"]
@@ -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)
@@ -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,
@@ -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,
@@ -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:
@@ -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
 
@@ -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
 
@@ -1,23 +0,0 @@
1
- from trilogy import Environment
2
-
3
-
4
- def test_multi_environment():
5
- basic = Environment()
6
-
7
- basic.parse(
8
- """
9
- const pi <- 3.14;
10
-
11
-
12
- """,
13
- namespace="math",
14
- )
15
-
16
- basic.parse(
17
- """
18
-
19
- select math.pi;
20
- """
21
- )
22
-
23
- assert basic.concepts["math.pi"].name == "pi"
File without changes
File without changes
File without changes
File without changes