pytrilogy 0.3.148__cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.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.
Files changed (206) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cpython-312-aarch64-linux-gnu.so +0 -0
  4. pytrilogy-0.3.148.dist-info/METADATA +555 -0
  5. pytrilogy-0.3.148.dist-info/RECORD +206 -0
  6. pytrilogy-0.3.148.dist-info/WHEEL +5 -0
  7. pytrilogy-0.3.148.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.148.dist-info/licenses/LICENSE.md +19 -0
  9. trilogy/__init__.py +27 -0
  10. trilogy/ai/README.md +10 -0
  11. trilogy/ai/__init__.py +19 -0
  12. trilogy/ai/constants.py +92 -0
  13. trilogy/ai/conversation.py +107 -0
  14. trilogy/ai/enums.py +7 -0
  15. trilogy/ai/execute.py +50 -0
  16. trilogy/ai/models.py +34 -0
  17. trilogy/ai/prompts.py +100 -0
  18. trilogy/ai/providers/__init__.py +0 -0
  19. trilogy/ai/providers/anthropic.py +106 -0
  20. trilogy/ai/providers/base.py +24 -0
  21. trilogy/ai/providers/google.py +146 -0
  22. trilogy/ai/providers/openai.py +89 -0
  23. trilogy/ai/providers/utils.py +68 -0
  24. trilogy/authoring/README.md +3 -0
  25. trilogy/authoring/__init__.py +148 -0
  26. trilogy/constants.py +119 -0
  27. trilogy/core/README.md +52 -0
  28. trilogy/core/__init__.py +0 -0
  29. trilogy/core/constants.py +6 -0
  30. trilogy/core/enums.py +454 -0
  31. trilogy/core/env_processor.py +239 -0
  32. trilogy/core/environment_helpers.py +320 -0
  33. trilogy/core/ergonomics.py +193 -0
  34. trilogy/core/exceptions.py +123 -0
  35. trilogy/core/functions.py +1240 -0
  36. trilogy/core/graph_models.py +142 -0
  37. trilogy/core/internal.py +85 -0
  38. trilogy/core/models/__init__.py +0 -0
  39. trilogy/core/models/author.py +2662 -0
  40. trilogy/core/models/build.py +2603 -0
  41. trilogy/core/models/build_environment.py +165 -0
  42. trilogy/core/models/core.py +506 -0
  43. trilogy/core/models/datasource.py +434 -0
  44. trilogy/core/models/environment.py +756 -0
  45. trilogy/core/models/execute.py +1213 -0
  46. trilogy/core/optimization.py +251 -0
  47. trilogy/core/optimizations/__init__.py +12 -0
  48. trilogy/core/optimizations/base_optimization.py +17 -0
  49. trilogy/core/optimizations/hide_unused_concept.py +47 -0
  50. trilogy/core/optimizations/inline_datasource.py +102 -0
  51. trilogy/core/optimizations/predicate_pushdown.py +245 -0
  52. trilogy/core/processing/README.md +94 -0
  53. trilogy/core/processing/READMEv2.md +121 -0
  54. trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
  55. trilogy/core/processing/__init__.py +0 -0
  56. trilogy/core/processing/concept_strategies_v3.py +508 -0
  57. trilogy/core/processing/constants.py +15 -0
  58. trilogy/core/processing/discovery_node_factory.py +451 -0
  59. trilogy/core/processing/discovery_utility.py +548 -0
  60. trilogy/core/processing/discovery_validation.py +167 -0
  61. trilogy/core/processing/graph_utils.py +43 -0
  62. trilogy/core/processing/node_generators/README.md +9 -0
  63. trilogy/core/processing/node_generators/__init__.py +31 -0
  64. trilogy/core/processing/node_generators/basic_node.py +160 -0
  65. trilogy/core/processing/node_generators/common.py +270 -0
  66. trilogy/core/processing/node_generators/constant_node.py +38 -0
  67. trilogy/core/processing/node_generators/filter_node.py +315 -0
  68. trilogy/core/processing/node_generators/group_node.py +213 -0
  69. trilogy/core/processing/node_generators/group_to_node.py +117 -0
  70. trilogy/core/processing/node_generators/multiselect_node.py +207 -0
  71. trilogy/core/processing/node_generators/node_merge_node.py +695 -0
  72. trilogy/core/processing/node_generators/recursive_node.py +88 -0
  73. trilogy/core/processing/node_generators/rowset_node.py +165 -0
  74. trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  75. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
  76. trilogy/core/processing/node_generators/select_merge_node.py +786 -0
  77. trilogy/core/processing/node_generators/select_node.py +95 -0
  78. trilogy/core/processing/node_generators/synonym_node.py +98 -0
  79. trilogy/core/processing/node_generators/union_node.py +91 -0
  80. trilogy/core/processing/node_generators/unnest_node.py +182 -0
  81. trilogy/core/processing/node_generators/window_node.py +201 -0
  82. trilogy/core/processing/nodes/README.md +28 -0
  83. trilogy/core/processing/nodes/__init__.py +179 -0
  84. trilogy/core/processing/nodes/base_node.py +522 -0
  85. trilogy/core/processing/nodes/filter_node.py +75 -0
  86. trilogy/core/processing/nodes/group_node.py +194 -0
  87. trilogy/core/processing/nodes/merge_node.py +420 -0
  88. trilogy/core/processing/nodes/recursive_node.py +46 -0
  89. trilogy/core/processing/nodes/select_node_v2.py +242 -0
  90. trilogy/core/processing/nodes/union_node.py +53 -0
  91. trilogy/core/processing/nodes/unnest_node.py +62 -0
  92. trilogy/core/processing/nodes/window_node.py +56 -0
  93. trilogy/core/processing/utility.py +823 -0
  94. trilogy/core/query_processor.py +604 -0
  95. trilogy/core/statements/README.md +35 -0
  96. trilogy/core/statements/__init__.py +0 -0
  97. trilogy/core/statements/author.py +536 -0
  98. trilogy/core/statements/build.py +0 -0
  99. trilogy/core/statements/common.py +20 -0
  100. trilogy/core/statements/execute.py +155 -0
  101. trilogy/core/table_processor.py +66 -0
  102. trilogy/core/utility.py +8 -0
  103. trilogy/core/validation/README.md +46 -0
  104. trilogy/core/validation/__init__.py +0 -0
  105. trilogy/core/validation/common.py +161 -0
  106. trilogy/core/validation/concept.py +146 -0
  107. trilogy/core/validation/datasource.py +227 -0
  108. trilogy/core/validation/environment.py +73 -0
  109. trilogy/core/validation/fix.py +256 -0
  110. trilogy/dialect/__init__.py +32 -0
  111. trilogy/dialect/base.py +1431 -0
  112. trilogy/dialect/bigquery.py +314 -0
  113. trilogy/dialect/common.py +147 -0
  114. trilogy/dialect/config.py +159 -0
  115. trilogy/dialect/dataframe.py +50 -0
  116. trilogy/dialect/duckdb.py +376 -0
  117. trilogy/dialect/enums.py +149 -0
  118. trilogy/dialect/metadata.py +173 -0
  119. trilogy/dialect/mock.py +190 -0
  120. trilogy/dialect/postgres.py +117 -0
  121. trilogy/dialect/presto.py +110 -0
  122. trilogy/dialect/results.py +89 -0
  123. trilogy/dialect/snowflake.py +129 -0
  124. trilogy/dialect/sql_server.py +137 -0
  125. trilogy/engine.py +48 -0
  126. trilogy/execution/__init__.py +17 -0
  127. trilogy/execution/config.py +119 -0
  128. trilogy/execution/state/__init__.py +0 -0
  129. trilogy/execution/state/file_state_store.py +0 -0
  130. trilogy/execution/state/sqllite_state_store.py +0 -0
  131. trilogy/execution/state/state_store.py +301 -0
  132. trilogy/executor.py +656 -0
  133. trilogy/hooks/__init__.py +4 -0
  134. trilogy/hooks/base_hook.py +40 -0
  135. trilogy/hooks/graph_hook.py +135 -0
  136. trilogy/hooks/query_debugger.py +166 -0
  137. trilogy/metadata/__init__.py +0 -0
  138. trilogy/parser.py +10 -0
  139. trilogy/parsing/README.md +21 -0
  140. trilogy/parsing/__init__.py +0 -0
  141. trilogy/parsing/common.py +1069 -0
  142. trilogy/parsing/config.py +5 -0
  143. trilogy/parsing/exceptions.py +8 -0
  144. trilogy/parsing/helpers.py +1 -0
  145. trilogy/parsing/parse_engine.py +2863 -0
  146. trilogy/parsing/render.py +773 -0
  147. trilogy/parsing/trilogy.lark +544 -0
  148. trilogy/py.typed +0 -0
  149. trilogy/render.py +45 -0
  150. trilogy/scripts/README.md +9 -0
  151. trilogy/scripts/__init__.py +0 -0
  152. trilogy/scripts/agent.py +41 -0
  153. trilogy/scripts/agent_info.py +306 -0
  154. trilogy/scripts/common.py +430 -0
  155. trilogy/scripts/dependency/Cargo.lock +617 -0
  156. trilogy/scripts/dependency/Cargo.toml +39 -0
  157. trilogy/scripts/dependency/README.md +131 -0
  158. trilogy/scripts/dependency/build.sh +25 -0
  159. trilogy/scripts/dependency/src/directory_resolver.rs +387 -0
  160. trilogy/scripts/dependency/src/lib.rs +16 -0
  161. trilogy/scripts/dependency/src/main.rs +770 -0
  162. trilogy/scripts/dependency/src/parser.rs +435 -0
  163. trilogy/scripts/dependency/src/preql.pest +208 -0
  164. trilogy/scripts/dependency/src/python_bindings.rs +311 -0
  165. trilogy/scripts/dependency/src/resolver.rs +716 -0
  166. trilogy/scripts/dependency/tests/base.preql +3 -0
  167. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  168. trilogy/scripts/dependency/tests/customer.preql +6 -0
  169. trilogy/scripts/dependency/tests/main.preql +9 -0
  170. trilogy/scripts/dependency/tests/orders.preql +7 -0
  171. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  172. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  173. trilogy/scripts/dependency.py +323 -0
  174. trilogy/scripts/display.py +555 -0
  175. trilogy/scripts/environment.py +59 -0
  176. trilogy/scripts/fmt.py +32 -0
  177. trilogy/scripts/ingest.py +472 -0
  178. trilogy/scripts/ingest_helpers/__init__.py +1 -0
  179. trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
  180. trilogy/scripts/ingest_helpers/formatting.py +93 -0
  181. trilogy/scripts/ingest_helpers/typing.py +161 -0
  182. trilogy/scripts/init.py +105 -0
  183. trilogy/scripts/parallel_execution.py +748 -0
  184. trilogy/scripts/plan.py +189 -0
  185. trilogy/scripts/refresh.py +106 -0
  186. trilogy/scripts/run.py +79 -0
  187. trilogy/scripts/serve.py +202 -0
  188. trilogy/scripts/serve_helpers/__init__.py +41 -0
  189. trilogy/scripts/serve_helpers/file_discovery.py +142 -0
  190. trilogy/scripts/serve_helpers/index_generation.py +206 -0
  191. trilogy/scripts/serve_helpers/models.py +38 -0
  192. trilogy/scripts/single_execution.py +131 -0
  193. trilogy/scripts/testing.py +129 -0
  194. trilogy/scripts/trilogy.py +75 -0
  195. trilogy/std/__init__.py +0 -0
  196. trilogy/std/color.preql +3 -0
  197. trilogy/std/date.preql +13 -0
  198. trilogy/std/display.preql +18 -0
  199. trilogy/std/geography.preql +22 -0
  200. trilogy/std/metric.preql +15 -0
  201. trilogy/std/money.preql +67 -0
  202. trilogy/std/net.preql +14 -0
  203. trilogy/std/ranking.preql +7 -0
  204. trilogy/std/report.preql +5 -0
  205. trilogy/std/semantic.preql +6 -0
  206. trilogy/utility.py +34 -0
trilogy/executor.py ADDED
@@ -0,0 +1,656 @@
1
+ from functools import singledispatchmethod
2
+ from pathlib import Path
3
+ from typing import Any, Generator, List, Optional
4
+
5
+ from sqlalchemy import text
6
+
7
+ from trilogy.constants import MagicConstants, Rendering, logger
8
+ from trilogy.core.enums import (
9
+ AddressType,
10
+ CreateMode,
11
+ FunctionType,
12
+ Granularity,
13
+ IOType,
14
+ PersistMode,
15
+ ValidationScope,
16
+ )
17
+ from trilogy.core.models.author import Comment, Concept, Function
18
+ from trilogy.core.models.build import BuildFunction
19
+ from trilogy.core.models.core import ListWrapper, MapWrapper
20
+ from trilogy.core.models.datasource import Datasource, UpdateKeys
21
+ from trilogy.core.models.environment import Environment
22
+ from trilogy.core.statements.author import (
23
+ STATEMENT_TYPES,
24
+ ConceptDeclarationStatement,
25
+ CopyStatement,
26
+ CreateStatement,
27
+ ImportStatement,
28
+ MergeStatementV2,
29
+ MockStatement,
30
+ MultiSelectStatement,
31
+ PersistStatement,
32
+ PublishStatement,
33
+ RawSQLStatement,
34
+ SelectStatement,
35
+ ShowStatement,
36
+ ValidateStatement,
37
+ )
38
+ from trilogy.core.statements.execute import (
39
+ PROCESSED_STATEMENT_TYPES,
40
+ ProcessedCopyStatement,
41
+ ProcessedCreateStatement,
42
+ ProcessedMockStatement,
43
+ ProcessedPublishStatement,
44
+ ProcessedQuery,
45
+ ProcessedQueryPersist,
46
+ ProcessedRawSQLStatement,
47
+ ProcessedShowStatement,
48
+ ProcessedValidateStatement,
49
+ )
50
+ from trilogy.core.validation.common import (
51
+ ValidationTest,
52
+ )
53
+ from trilogy.dialect.base import BaseDialect
54
+ from trilogy.dialect.config import DialectConfig
55
+ from trilogy.dialect.enums import Dialects
56
+ from trilogy.dialect.metadata import (
57
+ generate_result_set,
58
+ handle_concept_declaration,
59
+ handle_datasource,
60
+ handle_import_statement,
61
+ handle_merge_statement,
62
+ handle_processed_show_statement,
63
+ handle_processed_validate_statement,
64
+ handle_publish_statement,
65
+ handle_show_statement_outputs,
66
+ )
67
+ from trilogy.dialect.mock import handle_processed_mock_statement
68
+ from trilogy.engine import EngineConnection, ExecutionEngine, ResultProtocol
69
+ from trilogy.hooks.base_hook import BaseHook
70
+ from trilogy.parser import parse_text
71
+ from trilogy.render import get_dialect_generator
72
+
73
+
74
+ class Executor(object):
75
+ def __init__(
76
+ self,
77
+ dialect: Dialects,
78
+ engine: ExecutionEngine,
79
+ environment: Optional[Environment] = None,
80
+ rendering: Rendering | None = None,
81
+ hooks: List[BaseHook] | None = None,
82
+ config: DialectConfig | None = None,
83
+ ):
84
+ self.dialect: Dialects = dialect
85
+ self.engine = engine
86
+ self.environment = environment or Environment()
87
+ self.generator: BaseDialect
88
+ self.logger = logger
89
+ self.hooks = hooks
90
+ self.config = config
91
+ self.generator = get_dialect_generator(self.dialect, rendering, config)
92
+ self.connection = self.connect()
93
+ # TODO: make generic
94
+ if self.dialect == Dialects.DATAFRAME:
95
+ self.engine.setup(self.environment, self.connection)
96
+ # Setup DuckDB extensions
97
+ if self.dialect == Dialects.DUCK_DB:
98
+ self._setup_duckdb_python_datasources()
99
+ self._setup_duckdb_gcs()
100
+
101
+ def connect(self) -> EngineConnection:
102
+ self.connection = self.engine.connect()
103
+ self.connected = True
104
+ return self.connection
105
+
106
+ def _setup_duckdb_python_datasources(self) -> None:
107
+ """Setup DuckDB macro for Python script datasources."""
108
+ import sys
109
+
110
+ from trilogy.dialect.config import DuckDBConfig
111
+ from trilogy.dialect.duckdb import get_python_datasource_setup_sql
112
+
113
+ enabled = (
114
+ isinstance(self.config, DuckDBConfig)
115
+ and self.config.enable_python_datasources
116
+ )
117
+ is_windows = sys.platform == "win32"
118
+ self.execute_raw_sql(get_python_datasource_setup_sql(enabled, is_windows))
119
+ self.connection.commit()
120
+
121
+ def _setup_duckdb_gcs(self) -> None:
122
+ """Setup DuckDB GCS extension with application default credentials."""
123
+ from trilogy.dialect.config import DuckDBConfig
124
+ from trilogy.dialect.duckdb import get_gcs_setup_sql
125
+
126
+ enabled = isinstance(self.config, DuckDBConfig) and self.config.enable_gcs
127
+ if not enabled:
128
+ return
129
+ sql = get_gcs_setup_sql(enabled)
130
+ if sql:
131
+ self.execute_raw_sql(sql)
132
+ self.connection.commit()
133
+
134
+ def close(self):
135
+ self.engine.dispose(close=True)
136
+ if self.dialect == Dialects.DUCK_DB:
137
+ import gc
138
+
139
+ gc.collect()
140
+ self.connected = False
141
+
142
+ def update_datasource(
143
+ self, datasource: Datasource, keys: UpdateKeys | None = None
144
+ ) -> None:
145
+ """Update a datasource with optional filtering based on update keys.
146
+
147
+ Args:
148
+ datasource: The datasource to update
149
+ keys: Optional UpdateKeys specifying incremental filters
150
+ """
151
+ where = keys.to_where_clause(self.environment) if keys else None
152
+ create_stmt = CreateStatement(
153
+ scope=ValidationScope.DATASOURCES,
154
+ create_mode=CreateMode.CREATE_IF_NOT_EXISTS,
155
+ targets=[datasource.name],
156
+ )
157
+ self.execute_statement(create_stmt)
158
+ select_stmt = datasource.create_update_statement(
159
+ self.environment, where, line_no=None
160
+ )
161
+ statement = PersistStatement(
162
+ datasource=datasource,
163
+ select=select_stmt,
164
+ )
165
+ self.execute_statement(statement)
166
+
167
+ def execute_statement(
168
+ self,
169
+ statement: PROCESSED_STATEMENT_TYPES | STATEMENT_TYPES,
170
+ ) -> Optional[ResultProtocol]:
171
+ if isinstance(statement, STATEMENT_TYPES):
172
+ generate = self.generator.generate_queries(
173
+ self.environment, [statement], hooks=self.hooks # type: ignore[list-item]
174
+ )
175
+ if not generate:
176
+ return None
177
+ statement = generate[0]
178
+
179
+ if not isinstance(statement, PROCESSED_STATEMENT_TYPES):
180
+ return None
181
+
182
+ return self.execute_query(statement)
183
+
184
+ @singledispatchmethod
185
+ def execute_query(self, query) -> ResultProtocol | None:
186
+ raise NotImplementedError("Cannot execute type {}".format(type(query)))
187
+
188
+ @execute_query.register
189
+ def _(self, query: Comment) -> ResultProtocol | None:
190
+ return None
191
+
192
+ @execute_query.register
193
+ def _(self, query: ConceptDeclarationStatement) -> ResultProtocol | None:
194
+ return handle_concept_declaration(query)
195
+
196
+ @execute_query.register
197
+ def _(self, query: Datasource) -> ResultProtocol | None:
198
+ return handle_datasource(query)
199
+
200
+ @execute_query.register
201
+ def _(self, query: str) -> ResultProtocol | None:
202
+ results = self.execute_text(query)
203
+ if results:
204
+ return results[-1]
205
+ return None
206
+
207
+ @execute_query.register
208
+ def _(self, query: SelectStatement) -> ResultProtocol | None:
209
+ sql = self.generator.generate_queries(
210
+ self.environment, [query], hooks=self.hooks
211
+ )
212
+ return self.execute_query(sql[0])
213
+
214
+ @execute_query.register
215
+ def _(self, query: PersistStatement) -> ResultProtocol | None:
216
+ sql = self.generator.generate_queries(
217
+ self.environment, [query], hooks=self.hooks
218
+ )
219
+ return self.execute_query(sql[0])
220
+
221
+ @execute_query.register
222
+ def _(self, query: RawSQLStatement) -> ResultProtocol | None:
223
+ return self.execute_raw_sql(query.text)
224
+
225
+ @execute_query.register
226
+ def _(self, query: ShowStatement) -> ResultProtocol | None:
227
+ sql = self.generator.generate_queries(
228
+ self.environment, [query], hooks=self.hooks
229
+ )
230
+ return self.execute_query(sql[0])
231
+
232
+ @execute_query.register
233
+ def _(self, query: ProcessedShowStatement) -> ResultProtocol | None:
234
+ return handle_processed_show_statement(
235
+ query,
236
+ [
237
+ self.generator.compile_statement(x)
238
+ for x in query.output_values
239
+ if isinstance(x, (ProcessedQuery, ProcessedQueryPersist))
240
+ ],
241
+ )
242
+
243
+ @execute_query.register
244
+ def _(self, query: ProcessedValidateStatement) -> ResultProtocol | None:
245
+ return handle_processed_validate_statement(
246
+ query, self.generator, self.validate_environment
247
+ )
248
+
249
+ @execute_query.register
250
+ def _(self, query: ProcessedMockStatement) -> ResultProtocol | None:
251
+
252
+ return handle_processed_mock_statement(query, self.environment, self)
253
+
254
+ @execute_query.register
255
+ def _(self, query: ProcessedCreateStatement) -> ResultProtocol | None:
256
+ sql = self.generator.compile_statement(query)
257
+ output = self.execute_raw_sql(sql)
258
+ return output
259
+
260
+ @execute_query.register
261
+ def _(self, query: ProcessedPublishStatement) -> ResultProtocol | None:
262
+ return handle_publish_statement(query, self.environment)
263
+
264
+ @execute_query.register
265
+ def _(self, query: ImportStatement) -> ResultProtocol | None:
266
+ return handle_import_statement(query)
267
+
268
+ @execute_query.register
269
+ def _(self, query: MergeStatementV2) -> ResultProtocol | None:
270
+ return handle_merge_statement(query, self.environment)
271
+
272
+ @execute_query.register
273
+ def _(self, query: ProcessedRawSQLStatement) -> ResultProtocol | None:
274
+ return self.execute_raw_sql(query.text)
275
+
276
+ @execute_query.register
277
+ def _(self, query: ProcessedQuery) -> ResultProtocol | None:
278
+ sql = self.generator.compile_statement(query)
279
+ output = self.execute_raw_sql(sql, local_concepts=query.local_concepts)
280
+ return output
281
+
282
+ def _address_type_to_io_type(self, addr_type: AddressType) -> IOType:
283
+ if addr_type == AddressType.PARQUET:
284
+ return IOType.PARQUET
285
+ elif addr_type == AddressType.CSV:
286
+ return IOType.CSV
287
+ raise NotImplementedError(f"File persist not supported for type {addr_type}")
288
+
289
+ @execute_query.register
290
+ def _(self, query: ProcessedQueryPersist) -> ResultProtocol | None:
291
+ # Check if target is a file - convert to CopyStatement
292
+ addr = query.output_to.address
293
+ if addr.is_file:
294
+ io_type = self._address_type_to_io_type(addr.type)
295
+ copy_statement = ProcessedCopyStatement(
296
+ output_columns=query.output_columns,
297
+ ctes=query.ctes,
298
+ base=query.base,
299
+ hidden_columns=query.hidden_columns,
300
+ limit=query.limit,
301
+ order_by=query.order_by,
302
+ local_concepts=query.local_concepts,
303
+ locally_derived=query.locally_derived,
304
+ target=addr.location,
305
+ target_type=io_type,
306
+ )
307
+ self.execute_query(copy_statement)
308
+ if query.persist_mode == PersistMode.OVERWRITE:
309
+ self.environment.add_datasource(query.datasource)
310
+ return None
311
+
312
+ sql = self.generator.compile_statement(query)
313
+ output = self.execute_raw_sql(sql, local_concepts=query.local_concepts)
314
+
315
+ if query.persist_mode == PersistMode.OVERWRITE:
316
+ self.environment.add_datasource(query.datasource)
317
+ return output
318
+
319
+ @execute_query.register
320
+ def _(self, query: ProcessedCopyStatement) -> ResultProtocol | None:
321
+ sql = self.generator.compile_statement(query)
322
+ if self.dialect == Dialects.DUCK_DB:
323
+ if query.target_type == IOType.PARQUET:
324
+ copy_sql = f"COPY ({sql}) TO '{query.target}' (FORMAT PARQUET)"
325
+ elif query.target_type == IOType.CSV:
326
+ copy_sql = f"COPY ({sql}) TO '{query.target}' (FORMAT CSV, HEADER)"
327
+ else:
328
+ raise NotImplementedError(f"Unsupported IO Type {query.target_type}")
329
+ self.execute_raw_sql(copy_sql, local_concepts=query.local_concepts)
330
+ else:
331
+ raise NotImplementedError(
332
+ f"COPY statement not supported for dialect {self.dialect}"
333
+ )
334
+ return generate_result_set(
335
+ query.output_columns,
336
+ [self.generator.compile_statement(query)],
337
+ )
338
+
339
+ @singledispatchmethod
340
+ def generate_sql(self, command) -> list[str]:
341
+ raise NotImplementedError(
342
+ "Cannot generate sql for type {}".format(type(command))
343
+ )
344
+
345
+ @generate_sql.register # type: ignore
346
+ def _(self, command: ProcessedQuery) -> list[str]:
347
+ output = []
348
+ compiled_sql = self.generator.compile_statement(command)
349
+ output.append(compiled_sql)
350
+ return output
351
+
352
+ @generate_sql.register
353
+ def _(self, command: ProcessedShowStatement) -> list[str]:
354
+ output = []
355
+ for statement in command.output_values:
356
+ if isinstance(statement, (ProcessedQuery, ProcessedQueryPersist)):
357
+ compiled_sql = self.generator.compile_statement(statement)
358
+ output.append(compiled_sql)
359
+ return output
360
+
361
+ @generate_sql.register # type: ignore
362
+ def _(self, command: MultiSelectStatement) -> list[str]:
363
+ output = []
364
+ sql = self.generator.generate_queries(
365
+ self.environment, [command], hooks=self.hooks
366
+ )
367
+ for statement in sql:
368
+ compiled_sql = self.generator.compile_statement(statement)
369
+ output.append(compiled_sql)
370
+ return output
371
+
372
+ @generate_sql.register
373
+ def _(self, command: SelectStatement) -> list[str]:
374
+ output = []
375
+ sql = self.generator.generate_queries(
376
+ self.environment, [command], hooks=self.hooks
377
+ )
378
+ for statement in sql:
379
+ compiled_sql = self.generator.compile_statement(statement)
380
+ output.append(compiled_sql)
381
+ return output
382
+
383
+ @generate_sql.register
384
+ def _(self, command: ProcessedCreateStatement) -> list[str]:
385
+ output = []
386
+ compiled_sql = self.generator.compile_statement(command)
387
+ output.append(compiled_sql)
388
+ return output
389
+
390
+ @generate_sql.register
391
+ def _(self, command: ProcessedPublishStatement) -> list[str]:
392
+ output = []
393
+ compiled_sql = self.generator.compile_statement(command)
394
+ output.append(compiled_sql)
395
+ return output
396
+
397
+ @generate_sql.register
398
+ def _(self, command: str) -> list[str]:
399
+ _, parsed = parse_text(command, self.environment)
400
+ generatable = [
401
+ x
402
+ for x in parsed
403
+ if isinstance(x, (SelectStatement, PersistStatement, MultiSelectStatement))
404
+ ]
405
+ sql = self.generator.generate_queries(
406
+ self.environment, generatable, hooks=self.hooks
407
+ )
408
+ output = []
409
+ for statement in sql:
410
+ if isinstance(statement, ProcessedShowStatement):
411
+ continue
412
+ compiled_sql = self.generator.compile_statement(statement)
413
+ output.append(compiled_sql)
414
+ return output
415
+
416
+ def parse_file(
417
+ self, file: str | Path, persist: bool = False
418
+ ) -> list[PROCESSED_STATEMENT_TYPES]:
419
+ return list(self.parse_file_generator(file, persist=persist))
420
+
421
+ def parse_file_generator(
422
+ self, file: str | Path, persist: bool = False
423
+ ) -> Generator[
424
+ PROCESSED_STATEMENT_TYPES,
425
+ None,
426
+ None,
427
+ ]:
428
+ file = Path(file)
429
+ candidates = [file, self.environment.working_path / file]
430
+ err = None
431
+ for file in candidates:
432
+ try:
433
+ with open(file, "r") as f:
434
+ command = f.read()
435
+ return self.parse_text_generator(
436
+ command, persist=persist, root=file
437
+ )
438
+ except FileNotFoundError as e:
439
+ if not err:
440
+ err = e
441
+ continue
442
+ if err:
443
+ raise err
444
+ raise FileNotFoundError(f"File {file} not found")
445
+
446
+ def parse_text(
447
+ self, command: str, persist: bool = False, root: Path | None = None
448
+ ) -> List[PROCESSED_STATEMENT_TYPES]:
449
+ return list(self.parse_text_generator(command, persist=persist, root=root))
450
+
451
+ def parse_text_generator(
452
+ self, command: str, persist: bool = False, root: Path | None = None
453
+ ) -> Generator[
454
+ PROCESSED_STATEMENT_TYPES,
455
+ None,
456
+ None,
457
+ ]:
458
+ """Process a preql text command"""
459
+ _, parsed = parse_text(command, self.environment, root=root)
460
+ generatable = [
461
+ x
462
+ for x in parsed
463
+ if isinstance(
464
+ x,
465
+ (
466
+ SelectStatement,
467
+ PersistStatement,
468
+ MultiSelectStatement,
469
+ ShowStatement,
470
+ RawSQLStatement,
471
+ CopyStatement,
472
+ ValidateStatement,
473
+ CreateStatement,
474
+ PublishStatement,
475
+ MockStatement,
476
+ ),
477
+ )
478
+ ]
479
+ while generatable:
480
+ t = generatable.pop(0)
481
+ x = self.generator.generate_queries(
482
+ self.environment, [t], hooks=self.hooks
483
+ )[0]
484
+
485
+ yield x
486
+
487
+ if persist and isinstance(x, ProcessedQueryPersist):
488
+ self.environment.add_datasource(x.datasource)
489
+
490
+ def _atom_to_value(self, val: Any) -> Any:
491
+ if val == MagicConstants.NULL:
492
+ return None
493
+ return val
494
+
495
+ def _concept_to_value(
496
+ self,
497
+ concept: Concept,
498
+ local_concepts: dict[str, Concept] | None = None,
499
+ ) -> Any:
500
+ if not concept.granularity == Granularity.SINGLE_ROW:
501
+ raise SyntaxError(
502
+ f"Cannot bind non-singleton concept {concept.address} ({concept.granularity}) to a parameter."
503
+ )
504
+ # TODO: to get rid of function here - need to figure out why it's getting passed in
505
+ if (
506
+ isinstance(concept.lineage, (BuildFunction, Function))
507
+ and concept.lineage.operator == FunctionType.CONSTANT
508
+ ):
509
+ rval = concept.lineage.arguments[0]
510
+ if isinstance(rval, ListWrapper):
511
+ return [self._atom_to_value(x) for x in rval]
512
+ if isinstance(rval, MapWrapper):
513
+ # duckdb expects maps in this format as variables
514
+ if self.dialect == Dialects.DUCK_DB:
515
+ return {
516
+ "key": [self._atom_to_value(x) for x in rval],
517
+ "value": [self._atom_to_value(rval[x]) for x in rval],
518
+ }
519
+ return {k: self._atom_to_value(v) for k, v in rval.items()}
520
+ # if isinstance(rval, ConceptRef):
521
+ # return self._concept_to_value(self.environment.concepts[rval.address], local_concepts=local_concepts)
522
+ return rval
523
+ else:
524
+ results = self.execute_query(f"select {concept.name} limit 1;")
525
+ if results:
526
+ fetcher = results.fetchone()
527
+ if fetcher:
528
+ return fetcher[0]
529
+ return None
530
+
531
+ def _hydrate_param(
532
+ self, param: str, local_concepts: dict[str, Concept] | None = None
533
+ ) -> Any:
534
+ matched = [
535
+ v
536
+ for v in self.environment.concepts.values()
537
+ if v.safe_address == param or v.address == param
538
+ ]
539
+ if local_concepts and not matched:
540
+ matched = [
541
+ v
542
+ for v in local_concepts.values()
543
+ if v.safe_address == param or v.address == param
544
+ ]
545
+ if not matched:
546
+ raise SyntaxError(f"No concept found for parameter {param};")
547
+
548
+ concept: Concept = matched.pop()
549
+ return self._concept_to_value(concept, local_concepts=local_concepts)
550
+
551
+ def execute_raw_sql(
552
+ self,
553
+ command: str | Path,
554
+ variables: dict | None = None,
555
+ local_concepts: dict[str, Concept] | None = None,
556
+ ) -> ResultProtocol:
557
+ """Run a command against the raw underlying
558
+ execution engine."""
559
+ final_params = None
560
+ if isinstance(command, Path):
561
+ with open(command, "r") as f:
562
+ command = f.read()
563
+ q = text(command)
564
+ if variables:
565
+ final_params = variables
566
+ else:
567
+ params = q.compile().params
568
+ if params:
569
+ final_params = {
570
+ x: self._hydrate_param(x, local_concepts=local_concepts)
571
+ for x in params
572
+ }
573
+
574
+ if final_params:
575
+ return self.connection.execute(text(command), final_params)
576
+ return self.connection.execute(
577
+ text(command),
578
+ )
579
+
580
+ def execute_text(
581
+ self, command: str, non_interactive: bool = False
582
+ ) -> List[ResultProtocol]:
583
+ if not self.connected:
584
+ self.connect()
585
+
586
+ """Run a trilogy query expressed as text."""
587
+ output: list[ResultProtocol] = []
588
+ # connection = self.engine.connect()
589
+ for statement in self.parse_text_generator(command):
590
+ if isinstance(statement, ProcessedShowStatement):
591
+ results = handle_show_statement_outputs(
592
+ statement,
593
+ [
594
+ self.generator.compile_statement(x)
595
+ for x in statement.output_values
596
+ if isinstance(x, (ProcessedQuery, ProcessedQueryPersist))
597
+ ],
598
+ self.environment,
599
+ self.generator,
600
+ )
601
+ output.extend(results)
602
+ continue
603
+ elif isinstance(statement, ProcessedValidateStatement):
604
+ validate_result = handle_processed_validate_statement(
605
+ statement, self.generator, self.validate_environment
606
+ )
607
+ if validate_result:
608
+ output.append(validate_result)
609
+ continue
610
+ if non_interactive:
611
+ if not isinstance(
612
+ statement,
613
+ (
614
+ ProcessedCopyStatement,
615
+ ProcessedQueryPersist,
616
+ ProcessedValidateStatement,
617
+ ProcessedRawSQLStatement,
618
+ ProcessedPublishStatement,
619
+ ),
620
+ ):
621
+ continue
622
+ result = self.execute_statement(statement)
623
+ if result:
624
+ output.append(result)
625
+ return output
626
+
627
+ def execute_file(
628
+ self, file: str | Path, non_interactive: bool = False
629
+ ) -> List[ResultProtocol]:
630
+ file = Path(file)
631
+ candidates = [file, self.environment.working_path / file]
632
+ err = None
633
+ for file in candidates:
634
+ if not file.exists():
635
+ continue
636
+ with open(file, "r") as f:
637
+ command = f.read()
638
+ if file.suffix == ".sql":
639
+ return [self.execute_raw_sql(command)]
640
+ else:
641
+ return self.execute_text(command, non_interactive=non_interactive)
642
+ if err:
643
+ raise err
644
+ raise FileNotFoundError(f"File {file} not found")
645
+
646
+ def validate_environment(
647
+ self,
648
+ scope: ValidationScope = ValidationScope.ALL,
649
+ targets: Optional[list[str]] = None,
650
+ generate_only: bool = False,
651
+ ) -> list[ValidationTest]:
652
+ from trilogy.core.validation.environment import validate_environment
653
+
654
+ return validate_environment(
655
+ self.environment, scope, targets, exec=None if generate_only else self
656
+ )
@@ -0,0 +1,4 @@
1
+ from trilogy.hooks.graph_hook import GraphHook
2
+ from trilogy.hooks.query_debugger import DebuggingHook
3
+
4
+ __all__ = ["DebuggingHook", "GraphHook"]
@@ -0,0 +1,40 @@
1
+ from trilogy.core.models.execute import (
2
+ CTE,
3
+ QueryDatasource,
4
+ UnionCTE,
5
+ )
6
+ from trilogy.core.processing.nodes import StrategyNode
7
+ from trilogy.core.statements.author import (
8
+ MultiSelectStatement,
9
+ PersistStatement,
10
+ RowsetDerivationStatement,
11
+ SelectStatement,
12
+ )
13
+
14
+
15
+ class BaseHook:
16
+ pass
17
+
18
+ def process_multiselect_info(self, select: MultiSelectStatement):
19
+ print("Multiselect with components:")
20
+ for x in select.selects:
21
+ self.process_select_info(x)
22
+
23
+ def process_select_info(self, select: SelectStatement):
24
+ print(f"Select statement grain: {str(select.grain)}")
25
+
26
+ def process_persist_info(self, persist: PersistStatement):
27
+ print(f"Persist statement persisting to {persist.address}")
28
+ self.process_select_info(persist.select)
29
+
30
+ def process_rowset_info(self, rowset: RowsetDerivationStatement):
31
+ print(f"Rowset statement with grain {str(rowset.select.grain)}")
32
+
33
+ def process_root_datasource(self, datasource: QueryDatasource):
34
+ pass
35
+
36
+ def process_root_cte(self, cte: CTE | UnionCTE):
37
+ pass
38
+
39
+ def process_root_strategy_node(self, node: StrategyNode):
40
+ pass