pytrilogy 0.0.3.109__py3-none-any.whl → 0.0.3.111__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,16 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.109
3
+ Version: 0.0.3.111
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
- Home-page:
6
- Author:
7
- Author-email: preql-community@gmail.com
8
5
  Classifier: Programming Language :: Python
9
6
  Classifier: Programming Language :: Python :: 3
10
7
  Classifier: Programming Language :: Python :: 3.9
11
8
  Classifier: Programming Language :: Python :: 3.10
12
9
  Classifier: Programming Language :: Python :: 3.11
13
10
  Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
14
12
  Description-Content-Type: text/markdown
15
13
  License-File: LICENSE.md
16
14
  Requires-Dist: lark
@@ -30,14 +28,7 @@ Provides-Extra: snowflake
30
28
  Requires-Dist: snowflake-sqlalchemy; extra == "snowflake"
31
29
  Provides-Extra: ai
32
30
  Requires-Dist: httpx; extra == "ai"
33
- Dynamic: author-email
34
- Dynamic: classifier
35
- Dynamic: description
36
- Dynamic: description-content-type
37
31
  Dynamic: license-file
38
- Dynamic: provides-extra
39
- Dynamic: requires-dist
40
- Dynamic: summary
41
32
 
42
33
  # Trilogy
43
34
  **SQL with superpowers for analytics**
@@ -1,8 +1,8 @@
1
- pytrilogy-0.0.3.109.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=KT6UoNoE4ZPq_JfCkzB6yZ8a543YyAWsBDGNZxY8LEg,304
1
+ pytrilogy-0.0.3.111.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=t3BBsjUY17XlIDGxHsMUFEuSMUyVnnWEzcU49iVEQNE,304
3
3
  trilogy/constants.py,sha256=g_zkVCNjGop6coZ1kM8eXXAzCnUN22ldx3TYFz0E9sc,1747
4
- trilogy/engine.py,sha256=3MiADf5MKcmxqiHBuRqiYdsXiLj7oitDfVvXvHrfjkA,2178
5
- trilogy/executor.py,sha256=-VeOV0bTGmchHRHpRwFJDyl8FElUxDpwUTUix7hhIFM,17429
4
+ trilogy/engine.py,sha256=v4TpNktM4zZ9OX7jZH2nde4dpX5uAH2U23ELfULTCSg,2280
5
+ trilogy/executor.py,sha256=f7ry_mtWMgwnPKV0MJszPqhNueOIrF4atfEigPlm9Dk,17959
6
6
  trilogy/parser.py,sha256=o4cfk3j3yhUFoiDKq9ZX_GjBF3dKhDjXEwb63rcBkBM,293
7
7
  trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  trilogy/render.py,sha256=qQWwduymauOlB517UtM-VGbVe8Cswa4UJub5aGbSO6c,1512
@@ -32,7 +32,7 @@ trilogy/core/functions.py,sha256=sdV6Z3NUVfwL1d18eNcaAXllVNqzLez23McsJ6xIp7M,331
32
32
  trilogy/core/graph_models.py,sha256=4EWFTHGfYd72zvS2HYoV6hm7nMC_VEd7vWr6txY-ig0,3400
33
33
  trilogy/core/internal.py,sha256=r9QagDB2GvpqlyD_I7VrsfbVfIk5mnok2znEbv72Aa4,2681
34
34
  trilogy/core/optimization.py,sha256=Km0ITEx9n6Iv5ReX6tm4uXO5uniSv_ooahycNNiET3g,9212
35
- trilogy/core/query_processor.py,sha256=uqygDJqkjIH4vLP-lbGRgTN7rRcYEkr3KGqNimNw_80,20345
35
+ trilogy/core/query_processor.py,sha256=rMrtLSQxVm7yeyh0nWjDNI9nnu4Xi0NgHvBJ14gvu4I,20384
36
36
  trilogy/core/utility.py,sha256=3VC13uSQWcZNghgt7Ot0ZTeEmNqs__cx122abVq9qhM,410
37
37
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  trilogy/core/models/author.py,sha256=3I7PFpJgoQT9RPOT3DfiqAjEtkcQPJnScs60I2UoyWo,81461
@@ -41,7 +41,7 @@ trilogy/core/models/build_environment.py,sha256=mpx7MKGc60fnZLVdeLi2YSREy7eQbQYy
41
41
  trilogy/core/models/core.py,sha256=iT9WdZoiXeglmUHWn6bZyXCTBpkApTGPKtNm_Mhbu_g,12987
42
42
  trilogy/core/models/datasource.py,sha256=wogTevZ-9CyUW2a8gjzqMCieircxi-J5lkI7EOAZnck,9596
43
43
  trilogy/core/models/environment.py,sha256=hwTIRnJgaHUdCYof7U5A9NPitGZ2s9yxqiW5O2SaJ9Y,28759
44
- trilogy/core/models/execute.py,sha256=pdL3voYB4dCQR_KMHwFaofP3ZpRbALRC2ELHueWyTko,42191
44
+ trilogy/core/models/execute.py,sha256=3fgEdho2e7S0outq91cCzb9jFwz6L1hTbsTrJwGvIFs,42311
45
45
  trilogy/core/optimizations/__init__.py,sha256=yspWc25M5SgAuvXYoSt5J8atyPbDlOfsKjIo5yGD9s4,368
46
46
  trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
47
47
  trilogy/core/optimizations/hide_unused_concept.py,sha256=DbsP8NqQOxmPv9omDOoFNPUGObUkqsRRNrr5d1xDxx4,1962
@@ -61,7 +61,7 @@ trilogy/core/processing/node_generators/constant_node.py,sha256=LfpDq2WrBRZ3tGsL
61
61
  trilogy/core/processing/node_generators/filter_node.py,sha256=cJ5od1fAfvalaUDO2O4Y6Yrr2RukOCqey7f3zrKSBbI,10808
62
62
  trilogy/core/processing/node_generators/group_node.py,sha256=sIm1QYrF4EY6sk56A48B6MieCZqvaJLSQebih_aiKnQ,8567
63
63
  trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZwaRU0sbUSr9H0moQbAxrGgX6iGA,3832
64
- trilogy/core/processing/node_generators/multiselect_node.py,sha256=a505AEixjsjp5jI8Ng3H5KF_AaehkS6HfRfTef64l_o,7063
64
+ trilogy/core/processing/node_generators/multiselect_node.py,sha256=dHPDoSKU0FF6Ue_t_LkZxTd0Q-Sf-EpYdsMYdyUlFQc,7120
65
65
  trilogy/core/processing/node_generators/node_merge_node.py,sha256=hNcZxnDLTZyYJWfojg769zH9HB9PfZfESmpN1lcHWXg,23172
66
66
  trilogy/core/processing/node_generators/recursive_node.py,sha256=l5zdh0dURKwmAy8kK4OpMtZfyUEQRk6N-PwSWIyBpSM,2468
67
67
  trilogy/core/processing/node_generators/rowset_node.py,sha256=MuVNIexXhqGONho_mewqMOwaYXNUnjjvyPvk_RDGNYE,5943
@@ -74,10 +74,10 @@ trilogy/core/processing/node_generators/window_node.py,sha256=wNvmumGO6AIQ7C9bDU
74
74
  trilogy/core/processing/node_generators/select_helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
75
  trilogy/core/processing/node_generators/select_helpers/datasource_injection.py,sha256=m2YQ4OmG0N2O61a7NEq1ZzbTa7JsCC00lxB2ymjcYRI,8224
76
76
  trilogy/core/processing/nodes/__init__.py,sha256=zTge1EzwzEydlcMliIFO_TT7h7lS8l37lyZuQDir1h0,5487
77
- trilogy/core/processing/nodes/base_node.py,sha256=6LPQ5zP_dZJ6-k_dmX9ZSLsHaQMHgqiR5DEylpHYGZA,18478
77
+ trilogy/core/processing/nodes/base_node.py,sha256=k4Z9qdL9BXxxHH66L5udQwSDcYeiaWEn3bRmy84SShs,18559
78
78
  trilogy/core/processing/nodes/filter_node.py,sha256=5VtRfKbCORx0dV-vQfgy3gOEkmmscL9f31ExvlODwvY,2461
79
79
  trilogy/core/processing/nodes/group_node.py,sha256=Ku8El9KQvRiTiHCZDS_jX0DjErSDNv7IIQMcd1Gsk7I,7449
80
- trilogy/core/processing/nodes/merge_node.py,sha256=uc0tlz30Yt9SnCwLhMcWuPVbXLzm3dzy0XqbyirqqTo,16521
80
+ trilogy/core/processing/nodes/merge_node.py,sha256=4y_itKoipHKjpCIQjK9SHga-Fq-HqyeQLwAoSIFQ1hM,16567
81
81
  trilogy/core/processing/nodes/recursive_node.py,sha256=k0rizxR8KE64ievfHx_GPfQmU8QAP118Laeyq5BLUOk,1526
82
82
  trilogy/core/processing/nodes/select_node_v2.py,sha256=IWyKyNgFlV8A2S1FUTPdIaogg6PzaHh-HmQo6v24sbg,8862
83
83
  trilogy/core/processing/nodes/union_node.py,sha256=hLAXXVWqEgMWi7dlgSHfCF59fon64av14-uPgJzoKzM,1870
@@ -95,12 +95,12 @@ trilogy/core/validation/datasource.py,sha256=nJeEFyb6iMBwlEVdYVy1vLzAbdRZwOsUjGx
95
95
  trilogy/core/validation/environment.py,sha256=ymvhQyt7jLK641JAAIQkqjQaAmr9C5022ILzYvDgPP0,2835
96
96
  trilogy/core/validation/fix.py,sha256=Z818UFNLxndMTLiyhB3doLxIfnOZ-16QGvVFWuD7UsA,3750
97
97
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
- trilogy/dialect/base.py,sha256=hFX0_3N-m3ZRTCyv1S650a8OPlx9qjp5Zh8wzTBx6E8,50338
98
+ trilogy/dialect/base.py,sha256=Qk4HkjKlnAnhcZwwLte9Arb_1pVnBmkgRlwRFX1A_GQ,50680
99
99
  trilogy/dialect/bigquery.py,sha256=XS3hpybeowgfrOrkycAigAF3NX2YUzTzfgE6f__2fT4,4316
100
- trilogy/dialect/common.py,sha256=cUI7JMmpG_A5KcaxRI-GoyqwLMD6jTf0JJhgcOdwQK4,5833
100
+ trilogy/dialect/common.py,sha256=I5Ku_TR5MwJTB3ZhcQenrtvXhH2RvTQ8wQe9w5lfkfA,5708
101
101
  trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
102
- trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
103
- trilogy/dialect/duckdb.py,sha256=JoUvQ19WvgxoaJkGLM7DPXOd1H0394k3vBiblksQzOI,5631
102
+ trilogy/dialect/dataframe.py,sha256=nDTHMSd7GiGjEhjAobrZND0w4zjr-vgOalM2Cdxjets,1596
103
+ trilogy/dialect/duckdb.py,sha256=cRPyqnuMgjhZVaW9BYA360p-5OXle_1Xt65Yy0Vzbr4,5901
104
104
  trilogy/dialect/enums.py,sha256=FRNYQ5-w-B6-X0yXKNU5g9GowsMlERFogTC5u2nxL_s,4740
105
105
  trilogy/dialect/metadata.py,sha256=p_V-MYPQ2iR6fcTjagnptCIWtsZe4fTfoS_iXpavPzY,7098
106
106
  trilogy/dialect/postgres.py,sha256=el2PKwfyvWGk5EZtLudqAH5ewLitY1sFHJiocBSyxyM,3393
@@ -132,8 +132,8 @@ trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
132
132
  trilogy/std/net.preql,sha256=WZCuvH87_rZntZiuGJMmBDMVKkdhTtxeHOkrXNwJ1EE,416
133
133
  trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
134
134
  trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
135
- pytrilogy-0.0.3.109.dist-info/METADATA,sha256=U_r100YWYUQoWKW48qdPDL2eZVxvDOia7fKkfOOiK3I,13460
136
- pytrilogy-0.0.3.109.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
137
- pytrilogy-0.0.3.109.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
138
- pytrilogy-0.0.3.109.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
139
- pytrilogy-0.0.3.109.dist-info/RECORD,,
135
+ pytrilogy-0.0.3.111.dist-info/METADATA,sha256=QagDSTPXJqU3Xa3OBJANdEauKslRIGUCczAVvV9HNcU,13289
136
+ pytrilogy-0.0.3.111.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
137
+ pytrilogy-0.0.3.111.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
138
+ pytrilogy-0.0.3.111.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
139
+ pytrilogy-0.0.3.111.dist-info/RECORD,,
trilogy/__init__.py CHANGED
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.3.109"
7
+ __version__ = "0.0.3.111"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -500,6 +500,7 @@ class BaseJoin(BaseModel):
500
500
  concepts: Optional[List[BuildConcept]] = None
501
501
  left_datasource: Optional[Union[BuildDatasource, "QueryDatasource"]] = None
502
502
  concept_pairs: list[ConceptPair] | None = None
503
+ modifiers: List[Modifier] = Field(default_factory=list)
503
504
 
504
505
  @model_validator(mode="after")
505
506
  def validate_join(self) -> "BaseJoin":
@@ -1103,6 +1104,7 @@ class Join(BaseModel):
1103
1104
  inlined_ctes: set[str] = Field(default_factory=set)
1104
1105
  quote: str | None = None
1105
1106
  condition: BuildConditional | BuildComparison | BuildParenthetical | None = None
1107
+ modifiers: List[Modifier] = Field(default_factory=list)
1106
1108
 
1107
1109
  def inline_cte(self, cte: CTE):
1108
1110
  self.inlined_ctes.add(cte.name)
@@ -3,7 +3,7 @@ from itertools import combinations
3
3
  from typing import List
4
4
 
5
5
  from trilogy.constants import logger
6
- from trilogy.core.enums import JoinType, Purpose
6
+ from trilogy.core.enums import JoinType, Modifier, Purpose
7
7
  from trilogy.core.models.build import (
8
8
  BuildConcept,
9
9
  BuildGrain,
@@ -47,6 +47,7 @@ def extra_align_joins(
47
47
  right_node=right,
48
48
  concepts=matched_concepts,
49
49
  join_type=JoinType.FULL,
50
+ modifiers=[Modifier.NULLABLE],
50
51
  )
51
52
  )
52
53
  return resolve_join_order(output)
@@ -1,11 +1,12 @@
1
1
  from collections import defaultdict
2
- from dataclasses import dataclass
2
+ from dataclasses import dataclass, field
3
3
  from typing import List, Optional
4
4
 
5
5
  from trilogy.core.enums import (
6
6
  BooleanOperator,
7
7
  Derivation,
8
8
  JoinType,
9
+ Modifier,
9
10
  SourceType,
10
11
  )
11
12
  from trilogy.core.models.build import (
@@ -436,6 +437,7 @@ class NodeJoin:
436
437
  join_type: JoinType
437
438
  filter_to_mutual: bool = False
438
439
  concept_pairs: list[ConceptPair] | None = None
440
+ modifiers: List[Modifier] = field(default_factory=list)
439
441
 
440
442
  def __post_init__(self):
441
443
  if self.left_node == self.right_node:
@@ -169,6 +169,7 @@ class MergeNode(StrategyNode):
169
169
  join_type=join.join_type,
170
170
  concepts=join.concepts,
171
171
  concept_pairs=join.concept_pairs,
172
+ modifiers=join.modifiers,
172
173
  )
173
174
  )
174
175
  return joins
@@ -122,6 +122,7 @@ def base_join_to_join(
122
122
  right_cte=right_cte,
123
123
  jointype=base_join.join_type,
124
124
  joinkey_pairs=final_pairs,
125
+ modifiers=base_join.modifiers,
125
126
  )
126
127
 
127
128
 
trilogy/dialect/base.py CHANGED
@@ -17,6 +17,7 @@ from trilogy.core.enums import (
17
17
  DatePart,
18
18
  FunctionType,
19
19
  GroupMode,
20
+ Modifier,
20
21
  Ordering,
21
22
  ShowCategory,
22
23
  UnnestMode,
@@ -89,6 +90,14 @@ from trilogy.core.utility import safe_quote
89
90
  from trilogy.dialect.common import render_join, render_unnest
90
91
  from trilogy.hooks.base_hook import BaseHook
91
92
 
93
+
94
+ def null_wrapper(lval: str, rval: str, modifiers: list[Modifier]) -> str:
95
+
96
+ if Modifier.NULLABLE in modifiers:
97
+ return f"({lval} = {rval} or ({lval} is null and {rval} is null))"
98
+ return f"{lval} = {rval}"
99
+
100
+
92
101
  LOGGER_PREFIX = "[RENDERING]"
93
102
 
94
103
  WINDOW_ITEMS = (BuildWindowItem,)
@@ -353,6 +362,7 @@ class BaseDialect:
353
362
  UNNEST_MODE = UnnestMode.CROSS_APPLY
354
363
  GROUP_MODE = GroupMode.AUTO
355
364
  EXPLAIN_KEYWORD = "EXPLAIN"
365
+ NULL_WRAPPER = staticmethod(null_wrapper)
356
366
 
357
367
  def __init__(self, rendering: Rendering | None = None):
358
368
  self.rendering = rendering or CONFIG.rendering
@@ -964,6 +974,7 @@ class BaseDialect:
964
974
  cte,
965
975
  use_map=self.used_map,
966
976
  unnest_mode=self.UNNEST_MODE,
977
+ null_wrapper=self.NULL_WRAPPER,
967
978
  )
968
979
  for join in final_joins
969
980
  ]
trilogy/dialect/common.py CHANGED
@@ -18,13 +18,6 @@ from trilogy.core.models.execute import (
18
18
  )
19
19
 
20
20
 
21
- def null_wrapper(lval: str, rval: str, modifiers: list[Modifier]) -> str:
22
-
23
- if Modifier.NULLABLE in modifiers:
24
- return f"({lval} = {rval} or ({lval} is null and {rval} is null))"
25
- return f"{lval} = {rval}"
26
-
27
-
28
21
  def render_unnest(
29
22
  unnest_mode: UnnestMode,
30
23
  quote_character: str,
@@ -88,6 +81,7 @@ def render_join(
88
81
  ],
89
82
  cte: CTE,
90
83
  use_map: dict[str, set[str]],
84
+ null_wrapper: Callable[[str, str, list[Modifier]], str],
91
85
  unnest_mode: UnnestMode = UnnestMode.CROSS_APPLY,
92
86
  ) -> str | None:
93
87
  # {% for key in join.joinkeys %}{{ key.inner }} = {{ key.outer}}{% endfor %}
@@ -135,9 +129,10 @@ def render_join(
135
129
  join.inlined_ctes,
136
130
  use_map=use_map,
137
131
  ),
138
- modifiers=pair.modifiers
132
+ pair.modifiers
139
133
  + (pair.left.modifiers or [])
140
- + (pair.right.modifiers or []),
134
+ + (pair.right.modifiers or [])
135
+ + (join.modifiers or []),
141
136
  )
142
137
  for pair in join.joinkey_pairs
143
138
  ]
@@ -45,3 +45,6 @@ class DataframeConnectionWrapper(ExecutionEngine):
45
45
 
46
46
  def connect(self) -> Any:
47
47
  return self.engine.connect()
48
+
49
+ def dispose(self, close=True):
50
+ return super().dispose(close)
trilogy/dialect/duckdb.py CHANGED
@@ -3,7 +3,7 @@ from typing import Any, Callable, Mapping
3
3
 
4
4
  from jinja2 import Template
5
5
 
6
- from trilogy.core.enums import FunctionType, UnnestMode, WindowType
6
+ from trilogy.core.enums import FunctionType, Modifier, UnnestMode, WindowType
7
7
  from trilogy.core.models.core import DataType
8
8
  from trilogy.dialect.base import BaseDialect
9
9
 
@@ -12,6 +12,17 @@ WINDOW_FUNCTION_MAP: Mapping[WindowType, Callable[[Any, Any, Any], str]] = {}
12
12
  SENTINAL_AUTO_CAPTURE_GROUP_VALUE = "-1"
13
13
 
14
14
 
15
+ def null_wrapper(
16
+ lval: str,
17
+ rval: str,
18
+ modifiers: list[Modifier],
19
+ ) -> str:
20
+
21
+ if Modifier.NULLABLE in modifiers:
22
+ return f"{lval} is not distinct from {rval}"
23
+ return f"{lval} = {rval}"
24
+
25
+
15
26
  def generate_regex_extract(x: list[str]) -> str:
16
27
  if str(x[2]) == SENTINAL_AUTO_CAPTURE_GROUP_VALUE:
17
28
  regex = re.compile(x[1])
@@ -151,3 +162,4 @@ class DuckDBDialect(BaseDialect):
151
162
  QUOTE_CHARACTER = '"'
152
163
  SQL_TEMPLATE = DUCKDB_TEMPLATE
153
164
  UNNEST_MODE = UnnestMode.DIRECT
165
+ NULL_WRAPPER = staticmethod(null_wrapper)
trilogy/engine.py CHANGED
@@ -33,6 +33,9 @@ class EngineConnection(Protocol):
33
33
  def rollback(self):
34
34
  raise NotImplementedError()
35
35
 
36
+ def close(self) -> None:
37
+ return
38
+
36
39
 
37
40
  class ExecutionEngine(Protocol):
38
41
  pass
@@ -43,6 +46,9 @@ class ExecutionEngine(Protocol):
43
46
  def setup(self, env: Environment, connection):
44
47
  pass
45
48
 
49
+ def dispose(self, close: bool = True):
50
+ pass
51
+
46
52
 
47
53
  ### Begin default SQLAlchemy implementation
48
54
  class SqlAlchemyResult:
trilogy/executor.py CHANGED
@@ -47,7 +47,7 @@ from trilogy.dialect.metadata import (
47
47
  handle_processed_validate_statement,
48
48
  handle_show_statement_outputs,
49
49
  )
50
- from trilogy.engine import ExecutionEngine, ResultProtocol
50
+ from trilogy.engine import EngineConnection, ExecutionEngine, ResultProtocol
51
51
  from trilogy.hooks.base_hook import BaseHook
52
52
  from trilogy.parser import parse_text
53
53
  from trilogy.render import get_dialect_generator
@@ -69,11 +69,27 @@ class Executor(object):
69
69
  self.logger = logger
70
70
  self.hooks = hooks
71
71
  self.generator = get_dialect_generator(self.dialect, rendering)
72
- self.connection = self.engine.connect()
72
+ self.connection = self.connect()
73
73
  # TODO: make generic
74
74
  if self.dialect == Dialects.DATAFRAME:
75
75
  self.engine.setup(self.environment, self.connection)
76
76
 
77
+ def connect(self) -> EngineConnection:
78
+ self.connection = self.engine.connect()
79
+ self.connected = True
80
+ return self.connection
81
+
82
+ def close(self):
83
+ self.engine.dispose(close=True)
84
+ if self.dialect == Dialects.DUCK_DB:
85
+ import duckdb
86
+
87
+ duckdb.default_connection().close()
88
+ import gc
89
+
90
+ gc.collect()
91
+ self.connected = False
92
+
77
93
  def execute_statement(
78
94
  self,
79
95
  statement: PROCESSED_STATEMENT_TYPES,
@@ -244,7 +260,9 @@ class Executor(object):
244
260
  """generate SQL for execution"""
245
261
  _, parsed = parse_text(command, self.environment)
246
262
  generatable = [
247
- x for x in parsed if isinstance(x, (SelectStatement, PersistStatement))
263
+ x
264
+ for x in parsed
265
+ if isinstance(x, (SelectStatement, PersistStatement, MultiSelectStatement))
248
266
  ]
249
267
  sql = self.generator.generate_queries(
250
268
  self.environment, generatable, hooks=self.hooks
@@ -419,6 +437,9 @@ class Executor(object):
419
437
  def execute_text(
420
438
  self, command: str, non_interactive: bool = False
421
439
  ) -> List[ResultProtocol]:
440
+ if not self.connected:
441
+ self.connect()
442
+
422
443
  """Run a trilogy query expressed as text."""
423
444
  output: list[ResultProtocol] = []
424
445
  # connection = self.engine.connect()