pytrilogy 0.3.138__cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.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 (182) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cpython-311-x86_64-linux-gnu.so +0 -0
  4. pytrilogy-0.3.138.dist-info/METADATA +525 -0
  5. pytrilogy-0.3.138.dist-info/RECORD +182 -0
  6. pytrilogy-0.3.138.dist-info/WHEEL +5 -0
  7. pytrilogy-0.3.138.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.138.dist-info/licenses/LICENSE.md +19 -0
  9. trilogy/__init__.py +9 -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 +87 -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 +143 -0
  26. trilogy/constants.py +113 -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 +443 -0
  31. trilogy/core/env_processor.py +120 -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 +1227 -0
  36. trilogy/core/graph_models.py +139 -0
  37. trilogy/core/internal.py +85 -0
  38. trilogy/core/models/__init__.py +0 -0
  39. trilogy/core/models/author.py +2672 -0
  40. trilogy/core/models/build.py +2521 -0
  41. trilogy/core/models/build_environment.py +180 -0
  42. trilogy/core/models/core.py +494 -0
  43. trilogy/core/models/datasource.py +322 -0
  44. trilogy/core/models/environment.py +748 -0
  45. trilogy/core/models/execute.py +1177 -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 +517 -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 +268 -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 +205 -0
  71. trilogy/core/processing/node_generators/node_merge_node.py +653 -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 +748 -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 +519 -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 +596 -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 +106 -0
  110. trilogy/dialect/__init__.py +32 -0
  111. trilogy/dialect/base.py +1359 -0
  112. trilogy/dialect/bigquery.py +256 -0
  113. trilogy/dialect/common.py +147 -0
  114. trilogy/dialect/config.py +144 -0
  115. trilogy/dialect/dataframe.py +50 -0
  116. trilogy/dialect/duckdb.py +177 -0
  117. trilogy/dialect/enums.py +147 -0
  118. trilogy/dialect/metadata.py +173 -0
  119. trilogy/dialect/mock.py +190 -0
  120. trilogy/dialect/postgres.py +91 -0
  121. trilogy/dialect/presto.py +104 -0
  122. trilogy/dialect/results.py +89 -0
  123. trilogy/dialect/snowflake.py +90 -0
  124. trilogy/dialect/sql_server.py +92 -0
  125. trilogy/engine.py +48 -0
  126. trilogy/execution/config.py +75 -0
  127. trilogy/executor.py +568 -0
  128. trilogy/hooks/__init__.py +4 -0
  129. trilogy/hooks/base_hook.py +40 -0
  130. trilogy/hooks/graph_hook.py +139 -0
  131. trilogy/hooks/query_debugger.py +166 -0
  132. trilogy/metadata/__init__.py +0 -0
  133. trilogy/parser.py +10 -0
  134. trilogy/parsing/README.md +21 -0
  135. trilogy/parsing/__init__.py +0 -0
  136. trilogy/parsing/common.py +1069 -0
  137. trilogy/parsing/config.py +5 -0
  138. trilogy/parsing/exceptions.py +8 -0
  139. trilogy/parsing/helpers.py +1 -0
  140. trilogy/parsing/parse_engine.py +2813 -0
  141. trilogy/parsing/render.py +750 -0
  142. trilogy/parsing/trilogy.lark +540 -0
  143. trilogy/py.typed +0 -0
  144. trilogy/render.py +42 -0
  145. trilogy/scripts/README.md +7 -0
  146. trilogy/scripts/__init__.py +0 -0
  147. trilogy/scripts/dependency/Cargo.lock +617 -0
  148. trilogy/scripts/dependency/Cargo.toml +39 -0
  149. trilogy/scripts/dependency/README.md +131 -0
  150. trilogy/scripts/dependency/build.sh +25 -0
  151. trilogy/scripts/dependency/src/directory_resolver.rs +162 -0
  152. trilogy/scripts/dependency/src/lib.rs +16 -0
  153. trilogy/scripts/dependency/src/main.rs +770 -0
  154. trilogy/scripts/dependency/src/parser.rs +435 -0
  155. trilogy/scripts/dependency/src/preql.pest +208 -0
  156. trilogy/scripts/dependency/src/python_bindings.rs +289 -0
  157. trilogy/scripts/dependency/src/resolver.rs +716 -0
  158. trilogy/scripts/dependency/tests/base.preql +3 -0
  159. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  160. trilogy/scripts/dependency/tests/customer.preql +6 -0
  161. trilogy/scripts/dependency/tests/main.preql +9 -0
  162. trilogy/scripts/dependency/tests/orders.preql +7 -0
  163. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  164. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  165. trilogy/scripts/dependency.py +323 -0
  166. trilogy/scripts/display.py +460 -0
  167. trilogy/scripts/environment.py +46 -0
  168. trilogy/scripts/parallel_execution.py +483 -0
  169. trilogy/scripts/single_execution.py +131 -0
  170. trilogy/scripts/trilogy.py +772 -0
  171. trilogy/std/__init__.py +0 -0
  172. trilogy/std/color.preql +3 -0
  173. trilogy/std/date.preql +13 -0
  174. trilogy/std/display.preql +18 -0
  175. trilogy/std/geography.preql +22 -0
  176. trilogy/std/metric.preql +15 -0
  177. trilogy/std/money.preql +67 -0
  178. trilogy/std/net.preql +14 -0
  179. trilogy/std/ranking.preql +7 -0
  180. trilogy/std/report.preql +5 -0
  181. trilogy/std/semantic.preql +6 -0
  182. trilogy/utility.py +34 -0
trilogy/executor.py ADDED
@@ -0,0 +1,568 @@
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
+ FunctionType,
10
+ Granularity,
11
+ IOType,
12
+ PersistMode,
13
+ ValidationScope,
14
+ )
15
+ from trilogy.core.models.author import Comment, Concept, Function
16
+ from trilogy.core.models.build import BuildFunction
17
+ from trilogy.core.models.core import ListWrapper, MapWrapper
18
+ from trilogy.core.models.datasource import Datasource
19
+ from trilogy.core.models.environment import Environment
20
+ from trilogy.core.statements.author import (
21
+ STATEMENT_TYPES,
22
+ ConceptDeclarationStatement,
23
+ CopyStatement,
24
+ CreateStatement,
25
+ ImportStatement,
26
+ MergeStatementV2,
27
+ MockStatement,
28
+ MultiSelectStatement,
29
+ PersistStatement,
30
+ PublishStatement,
31
+ RawSQLStatement,
32
+ SelectStatement,
33
+ ShowStatement,
34
+ ValidateStatement,
35
+ )
36
+ from trilogy.core.statements.execute import (
37
+ PROCESSED_STATEMENT_TYPES,
38
+ ProcessedCopyStatement,
39
+ ProcessedCreateStatement,
40
+ ProcessedMockStatement,
41
+ ProcessedPublishStatement,
42
+ ProcessedQuery,
43
+ ProcessedQueryPersist,
44
+ ProcessedRawSQLStatement,
45
+ ProcessedShowStatement,
46
+ ProcessedValidateStatement,
47
+ )
48
+ from trilogy.core.validation.common import (
49
+ ValidationTest,
50
+ )
51
+ from trilogy.dialect.base import BaseDialect
52
+ from trilogy.dialect.enums import Dialects
53
+ from trilogy.dialect.metadata import (
54
+ generate_result_set,
55
+ handle_concept_declaration,
56
+ handle_datasource,
57
+ handle_import_statement,
58
+ handle_merge_statement,
59
+ handle_processed_show_statement,
60
+ handle_processed_validate_statement,
61
+ handle_publish_statement,
62
+ handle_show_statement_outputs,
63
+ )
64
+ from trilogy.dialect.mock import handle_processed_mock_statement
65
+ from trilogy.engine import EngineConnection, ExecutionEngine, ResultProtocol
66
+ from trilogy.hooks.base_hook import BaseHook
67
+ from trilogy.parser import parse_text
68
+ from trilogy.render import get_dialect_generator
69
+
70
+
71
+ class Executor(object):
72
+ def __init__(
73
+ self,
74
+ dialect: Dialects,
75
+ engine: ExecutionEngine,
76
+ environment: Optional[Environment] = None,
77
+ rendering: Rendering | None = None,
78
+ hooks: List[BaseHook] | None = None,
79
+ ):
80
+ self.dialect: Dialects = dialect
81
+ self.engine = engine
82
+ self.environment = environment or Environment()
83
+ self.generator: BaseDialect
84
+ self.logger = logger
85
+ self.hooks = hooks
86
+ self.generator = get_dialect_generator(self.dialect, rendering)
87
+ self.connection = self.connect()
88
+ # TODO: make generic
89
+ if self.dialect == Dialects.DATAFRAME:
90
+ self.engine.setup(self.environment, self.connection)
91
+
92
+ def connect(self) -> EngineConnection:
93
+ self.connection = self.engine.connect()
94
+ self.connected = True
95
+ return self.connection
96
+
97
+ def close(self):
98
+ self.engine.dispose(close=True)
99
+ if self.dialect == Dialects.DUCK_DB:
100
+ import gc
101
+
102
+ gc.collect()
103
+ self.connected = False
104
+
105
+ def execute_statement(
106
+ self,
107
+ statement: PROCESSED_STATEMENT_TYPES,
108
+ ) -> Optional[ResultProtocol]:
109
+ if isinstance(statement, STATEMENT_TYPES):
110
+ generate = self.generator.generate_queries(
111
+ self.environment, [statement], hooks=self.hooks
112
+ )
113
+ if not generate:
114
+ return None
115
+ statement = generate[0]
116
+
117
+ if not isinstance(statement, PROCESSED_STATEMENT_TYPES):
118
+ return None
119
+
120
+ return self.execute_query(statement)
121
+
122
+ @singledispatchmethod
123
+ def execute_query(self, query) -> ResultProtocol | None:
124
+ raise NotImplementedError("Cannot execute type {}".format(type(query)))
125
+
126
+ @execute_query.register
127
+ def _(self, query: Comment) -> ResultProtocol | None:
128
+ return None
129
+
130
+ @execute_query.register
131
+ def _(self, query: ConceptDeclarationStatement) -> ResultProtocol | None:
132
+ return handle_concept_declaration(query)
133
+
134
+ @execute_query.register
135
+ def _(self, query: Datasource) -> ResultProtocol | None:
136
+ return handle_datasource(query)
137
+
138
+ @execute_query.register
139
+ def _(self, query: str) -> ResultProtocol | None:
140
+ results = self.execute_text(query)
141
+ if results:
142
+ return results[-1]
143
+ return None
144
+
145
+ @execute_query.register
146
+ def _(self, query: SelectStatement) -> ResultProtocol | None:
147
+ sql = self.generator.generate_queries(
148
+ self.environment, [query], hooks=self.hooks
149
+ )
150
+ return self.execute_query(sql[0])
151
+
152
+ @execute_query.register
153
+ def _(self, query: PersistStatement) -> ResultProtocol | None:
154
+ sql = self.generator.generate_queries(
155
+ self.environment, [query], hooks=self.hooks
156
+ )
157
+ return self.execute_query(sql[0])
158
+
159
+ @execute_query.register
160
+ def _(self, query: RawSQLStatement) -> ResultProtocol | None:
161
+ return self.execute_raw_sql(query.text)
162
+
163
+ @execute_query.register
164
+ def _(self, query: ShowStatement) -> ResultProtocol | None:
165
+ sql = self.generator.generate_queries(
166
+ self.environment, [query], hooks=self.hooks
167
+ )
168
+ return self.execute_query(sql[0])
169
+
170
+ @execute_query.register
171
+ def _(self, query: ProcessedShowStatement) -> ResultProtocol | None:
172
+ return handle_processed_show_statement(
173
+ query,
174
+ [
175
+ self.generator.compile_statement(x)
176
+ for x in query.output_values
177
+ if isinstance(x, (ProcessedQuery, ProcessedQueryPersist))
178
+ ],
179
+ )
180
+
181
+ @execute_query.register
182
+ def _(self, query: ProcessedValidateStatement) -> ResultProtocol | None:
183
+ return handle_processed_validate_statement(
184
+ query, self.generator, self.validate_environment
185
+ )
186
+
187
+ @execute_query.register
188
+ def _(self, query: ProcessedMockStatement) -> ResultProtocol | None:
189
+
190
+ return handle_processed_mock_statement(query, self.environment, self)
191
+
192
+ @execute_query.register
193
+ def _(self, query: ProcessedCreateStatement) -> ResultProtocol | None:
194
+ sql = self.generator.compile_statement(query)
195
+ output = self.execute_raw_sql(sql)
196
+ return output
197
+
198
+ @execute_query.register
199
+ def _(self, query: ProcessedPublishStatement) -> ResultProtocol | None:
200
+ return handle_publish_statement(query, self.environment)
201
+
202
+ @execute_query.register
203
+ def _(self, query: ImportStatement) -> ResultProtocol | None:
204
+ return handle_import_statement(query)
205
+
206
+ @execute_query.register
207
+ def _(self, query: MergeStatementV2) -> ResultProtocol | None:
208
+ return handle_merge_statement(query, self.environment)
209
+
210
+ @execute_query.register
211
+ def _(self, query: ProcessedRawSQLStatement) -> ResultProtocol | None:
212
+ return self.execute_raw_sql(query.text)
213
+
214
+ @execute_query.register
215
+ def _(self, query: ProcessedQuery) -> ResultProtocol | None:
216
+ sql = self.generator.compile_statement(query)
217
+ output = self.execute_raw_sql(sql, local_concepts=query.local_concepts)
218
+ return output
219
+
220
+ @execute_query.register
221
+ def _(self, query: ProcessedQueryPersist) -> ResultProtocol | None:
222
+ sql = self.generator.compile_statement(query)
223
+
224
+ output = self.execute_raw_sql(sql, local_concepts=query.local_concepts)
225
+ if query.persist_mode == PersistMode.OVERWRITE:
226
+ self.environment.add_datasource(query.datasource)
227
+ return output
228
+
229
+ @execute_query.register
230
+ def _(self, query: ProcessedCopyStatement) -> ResultProtocol | None:
231
+ sql = self.generator.compile_statement(query)
232
+ output: ResultProtocol = self.execute_raw_sql(
233
+ sql, local_concepts=query.local_concepts
234
+ )
235
+ if query.target_type == IOType.CSV:
236
+ import csv
237
+
238
+ with open(query.target, "w", newline="", encoding="utf-8") as f:
239
+ outcsv = csv.writer(f)
240
+ outcsv.writerow(output.keys())
241
+ outcsv.writerows(output)
242
+ else:
243
+ raise NotImplementedError(f"Unsupported IO Type {query.target_type}")
244
+ # now return the query we ran through IO
245
+ # TODO: instead return how many rows were written?
246
+ return generate_result_set(
247
+ query.output_columns,
248
+ [self.generator.compile_statement(query)],
249
+ )
250
+
251
+ @singledispatchmethod
252
+ def generate_sql(self, command) -> list[str]:
253
+ raise NotImplementedError(
254
+ "Cannot generate sql for type {}".format(type(command))
255
+ )
256
+
257
+ @generate_sql.register # type: ignore
258
+ def _(self, command: ProcessedQuery) -> list[str]:
259
+ output = []
260
+ compiled_sql = self.generator.compile_statement(command)
261
+ output.append(compiled_sql)
262
+ return output
263
+
264
+ @generate_sql.register
265
+ def _(self, command: ProcessedShowStatement) -> list[str]:
266
+ output = []
267
+ for statement in command.output_values:
268
+ if isinstance(statement, (ProcessedQuery, ProcessedQueryPersist)):
269
+ compiled_sql = self.generator.compile_statement(statement)
270
+ output.append(compiled_sql)
271
+ return output
272
+
273
+ @generate_sql.register # type: ignore
274
+ def _(self, command: MultiSelectStatement) -> list[str]:
275
+ output = []
276
+ sql = self.generator.generate_queries(
277
+ self.environment, [command], hooks=self.hooks
278
+ )
279
+ for statement in sql:
280
+ compiled_sql = self.generator.compile_statement(statement)
281
+ output.append(compiled_sql)
282
+ return output
283
+
284
+ @generate_sql.register
285
+ def _(self, command: SelectStatement) -> list[str]:
286
+ output = []
287
+ sql = self.generator.generate_queries(
288
+ self.environment, [command], hooks=self.hooks
289
+ )
290
+ for statement in sql:
291
+ compiled_sql = self.generator.compile_statement(statement)
292
+ output.append(compiled_sql)
293
+ return output
294
+
295
+ @generate_sql.register
296
+ def _(self, command: ProcessedCreateStatement) -> list[str]:
297
+ output = []
298
+ compiled_sql = self.generator.compile_statement(command)
299
+ output.append(compiled_sql)
300
+ return output
301
+
302
+ @generate_sql.register
303
+ def _(self, command: ProcessedPublishStatement) -> list[str]:
304
+ output = []
305
+ compiled_sql = self.generator.compile_statement(command)
306
+ output.append(compiled_sql)
307
+ return output
308
+
309
+ @generate_sql.register
310
+ def _(self, command: str) -> list[str]:
311
+ _, parsed = parse_text(command, self.environment)
312
+ generatable = [
313
+ x
314
+ for x in parsed
315
+ if isinstance(x, (SelectStatement, PersistStatement, MultiSelectStatement))
316
+ ]
317
+ sql = self.generator.generate_queries(
318
+ self.environment, generatable, hooks=self.hooks
319
+ )
320
+ output = []
321
+ for statement in sql:
322
+ if isinstance(statement, ProcessedShowStatement):
323
+ continue
324
+ compiled_sql = self.generator.compile_statement(statement)
325
+ output.append(compiled_sql)
326
+ return output
327
+
328
+ def parse_file(
329
+ self, file: str | Path, persist: bool = False
330
+ ) -> list[PROCESSED_STATEMENT_TYPES]:
331
+ return list(self.parse_file_generator(file, persist=persist))
332
+
333
+ def parse_file_generator(
334
+ self, file: str | Path, persist: bool = False
335
+ ) -> Generator[
336
+ PROCESSED_STATEMENT_TYPES,
337
+ None,
338
+ None,
339
+ ]:
340
+ file = Path(file)
341
+ candidates = [file, self.environment.working_path / file]
342
+ err = None
343
+ for file in candidates:
344
+ try:
345
+ with open(file, "r") as f:
346
+ command = f.read()
347
+ return self.parse_text_generator(
348
+ command, persist=persist, root=file
349
+ )
350
+ except FileNotFoundError as e:
351
+ if not err:
352
+ err = e
353
+ continue
354
+ if err:
355
+ raise err
356
+ raise FileNotFoundError(f"File {file} not found")
357
+
358
+ def parse_text(
359
+ self, command: str, persist: bool = False, root: Path | None = None
360
+ ) -> List[PROCESSED_STATEMENT_TYPES]:
361
+ return list(self.parse_text_generator(command, persist=persist, root=root))
362
+
363
+ def parse_text_generator(
364
+ self, command: str, persist: bool = False, root: Path | None = None
365
+ ) -> Generator[
366
+ PROCESSED_STATEMENT_TYPES,
367
+ None,
368
+ None,
369
+ ]:
370
+ """Process a preql text command"""
371
+ _, parsed = parse_text(command, self.environment, root=root)
372
+ generatable = [
373
+ x
374
+ for x in parsed
375
+ if isinstance(
376
+ x,
377
+ (
378
+ SelectStatement,
379
+ PersistStatement,
380
+ MultiSelectStatement,
381
+ ShowStatement,
382
+ RawSQLStatement,
383
+ CopyStatement,
384
+ ValidateStatement,
385
+ CreateStatement,
386
+ PublishStatement,
387
+ MockStatement,
388
+ ),
389
+ )
390
+ ]
391
+ while generatable:
392
+ t = generatable.pop(0)
393
+ x = self.generator.generate_queries(
394
+ self.environment, [t], hooks=self.hooks
395
+ )[0]
396
+
397
+ yield x
398
+
399
+ if persist and isinstance(x, ProcessedQueryPersist):
400
+ self.environment.add_datasource(x.datasource)
401
+
402
+ def _atom_to_value(self, val: Any) -> Any:
403
+ if val == MagicConstants.NULL:
404
+ return None
405
+ return val
406
+
407
+ def _concept_to_value(
408
+ self,
409
+ concept: Concept,
410
+ local_concepts: dict[str, Concept] | None = None,
411
+ ) -> Any:
412
+ if not concept.granularity == Granularity.SINGLE_ROW:
413
+ raise SyntaxError(
414
+ f"Cannot bind non-singleton concept {concept.address} ({concept.granularity}) to a parameter."
415
+ )
416
+ # TODO: to get rid of function here - need to figure out why it's getting passed in
417
+ if (
418
+ isinstance(concept.lineage, (BuildFunction, Function))
419
+ and concept.lineage.operator == FunctionType.CONSTANT
420
+ ):
421
+ rval = concept.lineage.arguments[0]
422
+ if isinstance(rval, ListWrapper):
423
+ return [self._atom_to_value(x) for x in rval]
424
+ if isinstance(rval, MapWrapper):
425
+ # duckdb expects maps in this format as variables
426
+ if self.dialect == Dialects.DUCK_DB:
427
+ return {
428
+ "key": [self._atom_to_value(x) for x in rval],
429
+ "value": [self._atom_to_value(rval[x]) for x in rval],
430
+ }
431
+ return {k: self._atom_to_value(v) for k, v in rval.items()}
432
+ # if isinstance(rval, ConceptRef):
433
+ # return self._concept_to_value(self.environment.concepts[rval.address], local_concepts=local_concepts)
434
+ return rval
435
+ else:
436
+ results = self.execute_query(f"select {concept.name} limit 1;")
437
+ if results:
438
+ fetcher = results.fetchone()
439
+ if fetcher:
440
+ return fetcher[0]
441
+ return None
442
+
443
+ def _hydrate_param(
444
+ self, param: str, local_concepts: dict[str, Concept] | None = None
445
+ ) -> Any:
446
+ matched = [
447
+ v
448
+ for v in self.environment.concepts.values()
449
+ if v.safe_address == param or v.address == param
450
+ ]
451
+ if local_concepts and not matched:
452
+ matched = [
453
+ v
454
+ for v in local_concepts.values()
455
+ if v.safe_address == param or v.address == param
456
+ ]
457
+ if not matched:
458
+ raise SyntaxError(f"No concept found for parameter {param};")
459
+
460
+ concept: Concept = matched.pop()
461
+ return self._concept_to_value(concept, local_concepts=local_concepts)
462
+
463
+ def execute_raw_sql(
464
+ self,
465
+ command: str | Path,
466
+ variables: dict | None = None,
467
+ local_concepts: dict[str, Concept] | None = None,
468
+ ) -> ResultProtocol:
469
+ """Run a command against the raw underlying
470
+ execution engine."""
471
+ final_params = None
472
+ if isinstance(command, Path):
473
+ with open(command, "r") as f:
474
+ command = f.read()
475
+ q = text(command)
476
+ if variables:
477
+ final_params = variables
478
+ else:
479
+ params = q.compile().params
480
+ if params:
481
+ final_params = {
482
+ x: self._hydrate_param(x, local_concepts=local_concepts)
483
+ for x in params
484
+ }
485
+
486
+ if final_params:
487
+ return self.connection.execute(text(command), final_params)
488
+ return self.connection.execute(
489
+ text(command),
490
+ )
491
+
492
+ def execute_text(
493
+ self, command: str, non_interactive: bool = False
494
+ ) -> List[ResultProtocol]:
495
+ if not self.connected:
496
+ self.connect()
497
+
498
+ """Run a trilogy query expressed as text."""
499
+ output: list[ResultProtocol] = []
500
+ # connection = self.engine.connect()
501
+ for statement in self.parse_text_generator(command):
502
+ if isinstance(statement, ProcessedShowStatement):
503
+ results = handle_show_statement_outputs(
504
+ statement,
505
+ [
506
+ self.generator.compile_statement(x)
507
+ for x in statement.output_values
508
+ if isinstance(x, (ProcessedQuery, ProcessedQueryPersist))
509
+ ],
510
+ self.environment,
511
+ self.generator,
512
+ )
513
+ output.extend(results)
514
+ continue
515
+ elif isinstance(statement, ProcessedValidateStatement):
516
+ validate_result = handle_processed_validate_statement(
517
+ statement, self.generator, self.validate_environment
518
+ )
519
+ if validate_result:
520
+ output.append(validate_result)
521
+ continue
522
+ if non_interactive:
523
+ if not isinstance(
524
+ statement,
525
+ (
526
+ ProcessedCopyStatement,
527
+ ProcessedQueryPersist,
528
+ ProcessedValidateStatement,
529
+ ProcessedRawSQLStatement,
530
+ ProcessedPublishStatement,
531
+ ),
532
+ ):
533
+ continue
534
+ result = self.execute_statement(statement)
535
+ if result:
536
+ output.append(result)
537
+ return output
538
+
539
+ def execute_file(
540
+ self, file: str | Path, non_interactive: bool = False
541
+ ) -> List[ResultProtocol]:
542
+ file = Path(file)
543
+ candidates = [file, self.environment.working_path / file]
544
+ err = None
545
+ for file in candidates:
546
+ if not file.exists():
547
+ continue
548
+ with open(file, "r") as f:
549
+ command = f.read()
550
+ if file.suffix == ".sql":
551
+ return [self.execute_raw_sql(command)]
552
+ else:
553
+ return self.execute_text(command, non_interactive=non_interactive)
554
+ if err:
555
+ raise err
556
+ raise FileNotFoundError(f"File {file} not found")
557
+
558
+ def validate_environment(
559
+ self,
560
+ scope: ValidationScope = ValidationScope.ALL,
561
+ targets: Optional[list[str]] = None,
562
+ generate_only: bool = False,
563
+ ) -> list[ValidationTest]:
564
+ from trilogy.core.validation.environment import validate_environment
565
+
566
+ return validate_environment(
567
+ self.environment, scope, targets, exec=None if generate_only else self
568
+ )
@@ -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