pytrilogy 0.3.149__cp313-cp313-win_amd64.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 (207) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cp313-win_amd64.pyd +0 -0
  4. pytrilogy-0.3.149.dist-info/METADATA +555 -0
  5. pytrilogy-0.3.149.dist-info/RECORD +207 -0
  6. pytrilogy-0.3.149.dist-info/WHEEL +4 -0
  7. pytrilogy-0.3.149.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.149.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 +2670 -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 +436 -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 +846 -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 +1432 -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 +397 -0
  117. trilogy/dialect/enums.py +151 -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/exceptions.py +26 -0
  130. trilogy/execution/state/file_state_store.py +0 -0
  131. trilogy/execution/state/sqllite_state_store.py +0 -0
  132. trilogy/execution/state/state_store.py +406 -0
  133. trilogy/executor.py +692 -0
  134. trilogy/hooks/__init__.py +4 -0
  135. trilogy/hooks/base_hook.py +40 -0
  136. trilogy/hooks/graph_hook.py +135 -0
  137. trilogy/hooks/query_debugger.py +166 -0
  138. trilogy/metadata/__init__.py +0 -0
  139. trilogy/parser.py +10 -0
  140. trilogy/parsing/README.md +21 -0
  141. trilogy/parsing/__init__.py +0 -0
  142. trilogy/parsing/common.py +1069 -0
  143. trilogy/parsing/config.py +5 -0
  144. trilogy/parsing/exceptions.py +8 -0
  145. trilogy/parsing/helpers.py +1 -0
  146. trilogy/parsing/parse_engine.py +2876 -0
  147. trilogy/parsing/render.py +775 -0
  148. trilogy/parsing/trilogy.lark +546 -0
  149. trilogy/py.typed +0 -0
  150. trilogy/render.py +45 -0
  151. trilogy/scripts/README.md +9 -0
  152. trilogy/scripts/__init__.py +0 -0
  153. trilogy/scripts/agent.py +41 -0
  154. trilogy/scripts/agent_info.py +306 -0
  155. trilogy/scripts/common.py +432 -0
  156. trilogy/scripts/dependency/Cargo.lock +617 -0
  157. trilogy/scripts/dependency/Cargo.toml +39 -0
  158. trilogy/scripts/dependency/README.md +131 -0
  159. trilogy/scripts/dependency/build.sh +25 -0
  160. trilogy/scripts/dependency/src/directory_resolver.rs +387 -0
  161. trilogy/scripts/dependency/src/lib.rs +16 -0
  162. trilogy/scripts/dependency/src/main.rs +770 -0
  163. trilogy/scripts/dependency/src/parser.rs +435 -0
  164. trilogy/scripts/dependency/src/preql.pest +208 -0
  165. trilogy/scripts/dependency/src/python_bindings.rs +311 -0
  166. trilogy/scripts/dependency/src/resolver.rs +716 -0
  167. trilogy/scripts/dependency/tests/base.preql +3 -0
  168. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  169. trilogy/scripts/dependency/tests/customer.preql +6 -0
  170. trilogy/scripts/dependency/tests/main.preql +9 -0
  171. trilogy/scripts/dependency/tests/orders.preql +7 -0
  172. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  173. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  174. trilogy/scripts/dependency.py +323 -0
  175. trilogy/scripts/display.py +555 -0
  176. trilogy/scripts/environment.py +59 -0
  177. trilogy/scripts/fmt.py +32 -0
  178. trilogy/scripts/ingest.py +487 -0
  179. trilogy/scripts/ingest_helpers/__init__.py +1 -0
  180. trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
  181. trilogy/scripts/ingest_helpers/formatting.py +93 -0
  182. trilogy/scripts/ingest_helpers/typing.py +161 -0
  183. trilogy/scripts/init.py +105 -0
  184. trilogy/scripts/parallel_execution.py +762 -0
  185. trilogy/scripts/plan.py +189 -0
  186. trilogy/scripts/refresh.py +161 -0
  187. trilogy/scripts/run.py +79 -0
  188. trilogy/scripts/serve.py +202 -0
  189. trilogy/scripts/serve_helpers/__init__.py +41 -0
  190. trilogy/scripts/serve_helpers/file_discovery.py +142 -0
  191. trilogy/scripts/serve_helpers/index_generation.py +206 -0
  192. trilogy/scripts/serve_helpers/models.py +38 -0
  193. trilogy/scripts/single_execution.py +131 -0
  194. trilogy/scripts/testing.py +143 -0
  195. trilogy/scripts/trilogy.py +75 -0
  196. trilogy/std/__init__.py +0 -0
  197. trilogy/std/color.preql +3 -0
  198. trilogy/std/date.preql +13 -0
  199. trilogy/std/display.preql +18 -0
  200. trilogy/std/geography.preql +22 -0
  201. trilogy/std/metric.preql +15 -0
  202. trilogy/std/money.preql +67 -0
  203. trilogy/std/net.preql +14 -0
  204. trilogy/std/ranking.preql +7 -0
  205. trilogy/std/report.preql +5 -0
  206. trilogy/std/semantic.preql +6 -0
  207. trilogy/utility.py +34 -0
@@ -0,0 +1,775 @@
1
+ from collections import defaultdict
2
+ from contextlib import contextmanager
3
+ from dataclasses import dataclass
4
+ from datetime import date, datetime
5
+ from functools import singledispatchmethod
6
+ from typing import Any
7
+
8
+ from jinja2 import Template
9
+
10
+ from trilogy.constants import DEFAULT_NAMESPACE, VIRTUAL_CONCEPT_PREFIX, MagicConstants
11
+ from trilogy.core.enums import (
12
+ ConceptSource,
13
+ DatasourceState,
14
+ DatePart,
15
+ FunctionType,
16
+ Modifier,
17
+ PersistMode,
18
+ Purpose,
19
+ ValidationScope,
20
+ )
21
+ from trilogy.core.models.author import (
22
+ AggregateWrapper,
23
+ AlignClause,
24
+ AlignItem,
25
+ CaseElse,
26
+ CaseWhen,
27
+ Comment,
28
+ Comparison,
29
+ Concept,
30
+ ConceptRef,
31
+ Conditional,
32
+ FilterItem,
33
+ Function,
34
+ FunctionCallWrapper,
35
+ Grain,
36
+ OrderBy,
37
+ Ordering,
38
+ OrderItem,
39
+ Parenthetical,
40
+ SubselectComparison,
41
+ WhereClause,
42
+ WindowItem,
43
+ )
44
+ from trilogy.core.models.core import (
45
+ ArrayType,
46
+ DataType,
47
+ ListWrapper,
48
+ MapWrapper,
49
+ NumericType,
50
+ TraitDataType,
51
+ TupleWrapper,
52
+ )
53
+ from trilogy.core.models.datasource import (
54
+ Address,
55
+ ColumnAssignment,
56
+ Datasource,
57
+ Query,
58
+ RawColumnExpr,
59
+ )
60
+ from trilogy.core.models.environment import Environment, Import
61
+ from trilogy.core.statements.author import (
62
+ ArgBinding,
63
+ ConceptDeclarationStatement,
64
+ ConceptDerivationStatement,
65
+ ConceptTransform,
66
+ CopyStatement,
67
+ FunctionDeclaration,
68
+ ImportStatement,
69
+ KeyMergeStatement,
70
+ MergeStatementV2,
71
+ MultiSelectStatement,
72
+ PersistStatement,
73
+ RawSQLStatement,
74
+ RowsetDerivationStatement,
75
+ SelectItem,
76
+ SelectStatement,
77
+ TypeDeclaration,
78
+ ValidateStatement,
79
+ )
80
+
81
+ QUERY_TEMPLATE = Template(
82
+ """{% if where %}WHERE
83
+ {{ where }}
84
+ {% endif %}SELECT{%- for select in select_columns %}
85
+ {{ select }},{% endfor %}{% if having %}
86
+ HAVING
87
+ {{ having }}
88
+ {% endif %}{%- if order_by %}
89
+ ORDER BY{% for order in order_by %}
90
+ {{ order }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %}{%- if limit is not none %}
91
+ LIMIT {{ limit }}{% endif %}
92
+ ;"""
93
+ )
94
+
95
+
96
+ @dataclass
97
+ class IndentationContext:
98
+ """Tracks indentation state during rendering"""
99
+
100
+ depth: int = 0
101
+ indent_string: str = " " # 4 spaces by default
102
+
103
+ @property
104
+ def current_indent(self) -> str:
105
+ return self.indent_string * self.depth
106
+
107
+ def increase_depth(self, extra_levels: int = 1) -> "IndentationContext":
108
+ return IndentationContext(
109
+ depth=self.depth + extra_levels, indent_string=self.indent_string
110
+ )
111
+
112
+
113
+ def safe_address(address: str) -> str:
114
+ if "." not in address:
115
+ address = f"{DEFAULT_NAMESPACE}.{address}"
116
+ return address
117
+
118
+
119
+ class Renderer:
120
+
121
+ def __init__(
122
+ self, environment: Environment | None = None, indent_string: str = " "
123
+ ):
124
+ self.environment = environment
125
+ self.indent_context = IndentationContext(indent_string=indent_string)
126
+
127
+ @contextmanager
128
+ def indented(self, levels: int = 1):
129
+ """Context manager for temporarily increasing indentation"""
130
+ old_context = self.indent_context
131
+ self.indent_context = self.indent_context.increase_depth(levels)
132
+ try:
133
+ yield
134
+ finally:
135
+ self.indent_context = old_context
136
+
137
+ def indent_lines(self, text: str, extra_levels: int = 0) -> str:
138
+ """Apply current indentation to all lines in text"""
139
+ if not text:
140
+ return text
141
+
142
+ indent = self.indent_context.indent_string * (
143
+ self.indent_context.depth + extra_levels
144
+ )
145
+ lines = text.split("\n")
146
+ indented_lines = []
147
+
148
+ for line in lines:
149
+ if line.strip(): # Only indent non-empty lines
150
+ indented_lines.append(indent + line)
151
+ else:
152
+ indented_lines.append(line) # Keep empty lines as-is
153
+
154
+ return "\n".join(indented_lines)
155
+
156
+ def render_statement_string(self, list_of_statements: list[Any]) -> str:
157
+ new = []
158
+ last_statement_type = None
159
+ for stmt in list_of_statements:
160
+ stmt_type = type(stmt)
161
+ if last_statement_type is None:
162
+ pass
163
+ elif last_statement_type == Comment:
164
+ new.append("\n")
165
+ elif stmt_type != last_statement_type:
166
+ new.append("\n\n")
167
+ else:
168
+ new.append("\n")
169
+ new.append(self.to_string(stmt))
170
+ last_statement_type = stmt_type
171
+ return "".join(new)
172
+
173
+ @singledispatchmethod
174
+ def to_string(self, arg):
175
+ raise NotImplementedError("Cannot render type {}".format(type(arg)))
176
+
177
+ @to_string.register
178
+ def _(self, arg: Environment):
179
+ output_concepts = []
180
+ constants: list[Concept] = []
181
+ keys: list[Concept] = []
182
+ properties = defaultdict(list)
183
+ metrics = []
184
+ # first, keys
185
+ for concept in arg.concepts.values():
186
+ if "__preql_internal" in concept.address:
187
+ continue
188
+
189
+ # don't render anything that came from an import
190
+ if concept.namespace in arg.imports:
191
+ continue
192
+ if (
193
+ concept.metadata
194
+ and concept.metadata.concept_source == ConceptSource.AUTO_DERIVED
195
+ ):
196
+ continue
197
+ elif not concept.lineage and concept.purpose == Purpose.CONSTANT:
198
+ constants.append(concept)
199
+ elif not concept.lineage and concept.purpose == Purpose.KEY:
200
+ keys.append(concept)
201
+
202
+ elif not concept.lineage and concept.purpose == Purpose.PROPERTY:
203
+ if concept.keys:
204
+ # avoid duplicate declarations
205
+ # but we need better composite key support
206
+ for key in sorted(list(concept.keys))[:1]:
207
+ properties[key].append(concept)
208
+ else:
209
+ keys.append(concept)
210
+ else:
211
+ metrics.append(concept)
212
+
213
+ output_concepts = constants
214
+ for key_concept in keys:
215
+ output_concepts += [key_concept]
216
+ output_concepts += properties.get(key_concept.name, [])
217
+ output_concepts += metrics
218
+
219
+ rendered_concepts = [
220
+ self.to_string(ConceptDeclarationStatement(concept=concept))
221
+ for concept in output_concepts
222
+ ]
223
+
224
+ rendered_datasources = [
225
+ # extra padding between datasources
226
+ # todo: make this more generic
227
+ self.to_string(datasource) + "\n"
228
+ for datasource in arg.datasources.values()
229
+ if datasource.namespace == DEFAULT_NAMESPACE
230
+ ]
231
+ rendered_imports = []
232
+ for _, imports in arg.imports.items():
233
+ for import_statement in imports:
234
+ rendered_imports.append(self.to_string(import_statement))
235
+ components = []
236
+ if rendered_imports:
237
+ components.append(rendered_imports)
238
+ if rendered_concepts:
239
+ components.append(rendered_concepts)
240
+ if rendered_datasources:
241
+ components.append(rendered_datasources)
242
+
243
+ final = "\n\n".join("\n".join(x) for x in components)
244
+ return final
245
+
246
+ @to_string.register
247
+ def _(self, arg: TypeDeclaration):
248
+ return f"type {arg.type.name} {self.to_string(arg.type.type)};"
249
+
250
+ @to_string.register
251
+ def _(self, arg: ArgBinding):
252
+ if arg.default:
253
+ return f"{arg.name}={self.to_string(arg.default)}"
254
+ return f"{arg.name}"
255
+
256
+ @to_string.register
257
+ def _(self, arg: FunctionDeclaration):
258
+ args = ", ".join([self.to_string(x) for x in arg.args])
259
+ return f"def {arg.name}({args}) -> {self.to_string(arg.expr)};"
260
+
261
+ @to_string.register
262
+ def _(self, arg: Datasource):
263
+ with self.indented():
264
+ assignments = ",\n".join(
265
+ [self.indent_lines(self.to_string(x)) for x in arg.columns]
266
+ )
267
+
268
+ if arg.non_partial_for:
269
+ non_partial = f"\ncomplete where {self.to_string(arg.non_partial_for)}"
270
+ else:
271
+ non_partial = ""
272
+ entry = "datasource"
273
+ if arg.is_root:
274
+ entry = "root datasource"
275
+ base = f"""{entry} {arg.name} (
276
+ {assignments}
277
+ )
278
+ {self.to_string(arg.grain) if arg.grain.components else ''}{non_partial}
279
+ {self.to_string(arg.address)}"""
280
+ if arg.where:
281
+ base += f"\nwhere {self.to_string(arg.where)}"
282
+
283
+ if arg.incremental_by:
284
+ base += f"\nincremental by {','.join(self.to_string(x) for x in arg.incremental_by)}"
285
+
286
+ if arg.partition_by:
287
+ base += f"\npartition by {','.join(self.to_string(x) for x in arg.partition_by)}"
288
+ if arg.freshness_by:
289
+ base += f"\nfreshness by {','.join(self.to_string(x) for x in arg.freshness_by)}"
290
+ if arg.status != DatasourceState.PUBLISHED:
291
+ base += f"\nstate {arg.status.value.lower()}"
292
+
293
+ base += ";"
294
+ return base
295
+
296
+ @to_string.register
297
+ def _(self, arg: "Grain"):
298
+ final = []
299
+ for comp in arg.components:
300
+ if comp.startswith(DEFAULT_NAMESPACE):
301
+ final.append(comp.split(".", 1)[1])
302
+ else:
303
+ final.append(comp)
304
+ final = sorted(final)
305
+ components = ",".join(x for x in final)
306
+ return f"grain ({components})"
307
+
308
+ @to_string.register
309
+ def _(self, arg: "Query"):
310
+ return f"""query {arg.text}"""
311
+
312
+ @to_string.register
313
+ def _(self, arg: RowsetDerivationStatement):
314
+ return f"""rowset {arg.name} <- {self.to_string(arg.select)}"""
315
+
316
+ @to_string.register
317
+ def _(self, arg: "CaseWhen"):
318
+ return (
319
+ f"""WHEN {self.to_string(arg.comparison)} THEN {self.to_string(arg.expr)}"""
320
+ )
321
+
322
+ @to_string.register
323
+ def _(self, arg: "CaseElse"):
324
+ return f"""ELSE {self.to_string(arg.expr)}"""
325
+
326
+ @to_string.register
327
+ def _(self, arg: "FunctionCallWrapper"):
328
+ args = [self.to_string(c) for c in arg.args]
329
+ arg_string = ", ".join(args)
330
+ return f"""@{arg.name}({arg_string})"""
331
+
332
+ @to_string.register
333
+ def _(self, arg: "Parenthetical"):
334
+ return f"""({self.to_string(arg.content)})"""
335
+
336
+ @to_string.register
337
+ def _(self, arg: DataType):
338
+ return arg.value
339
+
340
+ @to_string.register
341
+ def _(self, arg: "NumericType"):
342
+ return f"""Numeric({arg.precision},{arg.scale})"""
343
+
344
+ @to_string.register
345
+ def _(self, arg: TraitDataType):
346
+ traits = "::".join([x for x in arg.traits])
347
+ return f"{self.to_string(arg.data_type)}::{traits}"
348
+
349
+ @to_string.register
350
+ def _(self, arg: ListWrapper):
351
+ return "[" + ", ".join([self.to_string(x) for x in arg]) + "]"
352
+
353
+ @to_string.register
354
+ def _(self, arg: TupleWrapper):
355
+ return "(" + ", ".join([self.to_string(x) for x in arg]) + ")"
356
+
357
+ @to_string.register
358
+ def _(self, arg: MapWrapper):
359
+ def process_key_value(key, value):
360
+ return f"{self.to_string(key)}: {self.to_string(value)}"
361
+
362
+ return (
363
+ "{"
364
+ + ", ".join([process_key_value(key, value) for key, value in arg.items()])
365
+ + "}"
366
+ )
367
+
368
+ @to_string.register
369
+ def _(self, arg: DatePart):
370
+ return arg.value
371
+
372
+ @to_string.register
373
+ def _(self, arg: "Address"):
374
+ if arg.is_query:
375
+ if arg.location.startswith("("):
376
+ return f"query '''{arg.location[1:-1]}'''"
377
+ return f"query '''{arg.location}'''"
378
+ elif arg.is_file:
379
+ return f"file '''`{arg.location}`'''"
380
+ return f"address {arg.location}"
381
+
382
+ @to_string.register
383
+ def _(self, arg: "RawSQLStatement"):
384
+ return f"raw_sql('''{arg.text}''');"
385
+
386
+ @to_string.register
387
+ def _(self, arg: "MagicConstants"):
388
+ if arg == MagicConstants.NULL:
389
+ return "null"
390
+ return arg.value
391
+
392
+ @to_string.register
393
+ def _(self, arg: "ColumnAssignment"):
394
+ if arg.modifiers:
395
+ modifiers = "".join(
396
+ [self.to_string(modifier) for modifier in sorted(arg.modifiers)]
397
+ )
398
+ else:
399
+ modifiers = ""
400
+
401
+ # Get concept string representation
402
+ concept_str = self.to_string(arg.concept)
403
+
404
+ # Get alias string representation
405
+ if isinstance(arg.alias, str):
406
+ alias_str = arg.alias
407
+ else:
408
+ alias_str = self.to_string(arg.alias)
409
+
410
+ # If alias matches concept string and no modifiers, use shorthand
411
+ if alias_str == concept_str and not modifiers:
412
+ return alias_str
413
+
414
+ # Otherwise use full syntax
415
+ return f"{alias_str}: {modifiers}{concept_str}"
416
+
417
+ @to_string.register
418
+ def _(self, arg: "RawColumnExpr"):
419
+ return f"raw('''{arg.text}''')"
420
+
421
+ @to_string.register
422
+ def _(self, arg: "ConceptDeclarationStatement"):
423
+ concept = arg.concept
424
+ if concept.metadata and concept.metadata.description:
425
+ base_description = concept.metadata.description
426
+ else:
427
+ base_description = None
428
+ if concept.namespace and concept.namespace != DEFAULT_NAMESPACE:
429
+ namespace = f"{concept.namespace}."
430
+ else:
431
+ namespace = ""
432
+ if not concept.lineage:
433
+ if concept.purpose == Purpose.PROPERTY and concept.keys:
434
+ if len(concept.keys) == 1:
435
+ output = f"{concept.purpose.value} {self.to_string(ConceptRef(address=safe_address(list(concept.keys)[0])))}.{namespace}{concept.name} {self.to_string(concept.datatype)};"
436
+ else:
437
+ keys = ",".join(
438
+ sorted(
439
+ list(
440
+ self.to_string(ConceptRef(address=safe_address(x)))
441
+ for x in concept.keys
442
+ )
443
+ )
444
+ )
445
+ output = f"{concept.purpose.value} <{keys}>.{namespace}{concept.name} {self.to_string(concept.datatype)};"
446
+ else:
447
+ output = f"{concept.purpose.value} {namespace}{concept.name} {self.to_string(concept.datatype)};"
448
+ else:
449
+ output = f"{concept.purpose.value} {namespace}{concept.name} <- {self.to_string(concept.lineage)};"
450
+ if base_description:
451
+ lines = "\n#".join(base_description.split("\n"))
452
+ output += f" #{lines}"
453
+ return output
454
+
455
+ @to_string.register
456
+ def _(self, arg: ArrayType):
457
+ return f"list<{self.to_string(arg.value_data_type)}>"
458
+
459
+ @to_string.register
460
+ def _(self, arg: DataType):
461
+ return arg.value
462
+
463
+ @to_string.register
464
+ def _(self, arg: date):
465
+ return f"'{arg.isoformat()}'::date"
466
+
467
+ @to_string.register
468
+ def _(self, arg: datetime):
469
+ return f"'{arg.isoformat()}'::datetime"
470
+
471
+ @to_string.register
472
+ def _(self, arg: ConceptDerivationStatement):
473
+ # this is identical rendering;
474
+ return self.to_string(ConceptDeclarationStatement(concept=arg.concept))
475
+
476
+ @to_string.register
477
+ def _(self, arg: PersistStatement):
478
+ if arg.persist_mode == PersistMode.APPEND:
479
+ keyword = "APPEND"
480
+ else:
481
+ keyword = "OVERWRITE"
482
+ if arg.partition_by:
483
+ partition_by = (
484
+ f"BY {', '.join(self.to_string(x) for x in arg.partition_by)}"
485
+ )
486
+ return f"{keyword} {arg.identifier} INTO {arg.address.location} {partition_by} FROM {self.to_string(arg.select)}"
487
+ return f"{keyword} {arg.identifier} INTO {arg.address.location} FROM {self.to_string(arg.select)}"
488
+
489
+ @to_string.register
490
+ def _(self, arg: SelectItem):
491
+ prefixes = []
492
+ if Modifier.HIDDEN in arg.modifiers:
493
+ prefixes.append("--")
494
+ if Modifier.PARTIAL in arg.modifiers:
495
+ prefixes.append("~")
496
+ final = "".join(prefixes)
497
+ return f"{final}{self.to_string(arg.content)}"
498
+
499
+ @to_string.register
500
+ def _(self, arg: ValidateStatement):
501
+ targets = ",".join(arg.targets) if arg.targets else "*"
502
+ if arg.scope.value == ValidationScope.ALL:
503
+ return "validate all;"
504
+ return f"validate {arg.scope.value} {targets};"
505
+
506
+ @to_string.register
507
+ def _(self, arg: SelectStatement):
508
+ with self.indented():
509
+ select_columns = [
510
+ self.indent_lines(self.to_string(c)) for c in arg.selection
511
+ ]
512
+ where_clause = None
513
+ if arg.where_clause:
514
+ where_clause = self.indent_lines(self.to_string(arg.where_clause))
515
+ having_clause = None
516
+ if arg.having_clause:
517
+ having_clause = self.indent_lines(self.to_string(arg.having_clause))
518
+ order_by = None
519
+ if arg.order_by:
520
+ order_by = [
521
+ self.indent_lines(self.to_string(c)) for c in arg.order_by.items
522
+ ]
523
+
524
+ return QUERY_TEMPLATE.render(
525
+ select_columns=select_columns,
526
+ where=where_clause,
527
+ having=having_clause,
528
+ order_by=order_by,
529
+ limit=arg.limit,
530
+ )
531
+
532
+ @to_string.register
533
+ def _(self, arg: MultiSelectStatement):
534
+ # Each select gets its own indentation
535
+ select_parts = []
536
+ for select in arg.selects:
537
+ select_parts.append(
538
+ self.to_string(select)[:-2]
539
+ ) # Remove the trailing ";\n"
540
+
541
+ base = "\nMERGE\n".join(select_parts)
542
+ base += self.to_string(arg.align)
543
+ if arg.where_clause:
544
+ base += f"\nWHERE\n{self.to_string(arg.where_clause)}"
545
+ if arg.order_by:
546
+ base += f"\nORDER BY\n{self.to_string(arg.order_by)}"
547
+ if arg.limit:
548
+ base += f"\nLIMIT {arg.limit}"
549
+ base += "\n;"
550
+ return base
551
+
552
+ @to_string.register
553
+ def _(self, arg: CopyStatement):
554
+ return f"COPY INTO {arg.target_type.value.upper()} '{arg.target}' FROM {self.to_string(arg.select)}"
555
+
556
+ @to_string.register
557
+ def _(self, arg: AlignClause):
558
+ with self.indented():
559
+ align_items = [self.indent_lines(self.to_string(c)) for c in arg.items]
560
+ return "\nALIGN\n" + ",\n".join(align_items)
561
+
562
+ @to_string.register
563
+ def _(self, arg: AlignItem):
564
+ return f"{arg.alias}:{','.join([self.to_string(c) for c in arg.concepts])}"
565
+
566
+ @to_string.register
567
+ def _(self, arg: OrderBy):
568
+ with self.indented():
569
+ order_items = [self.indent_lines(self.to_string(c)) for c in arg.items]
570
+ return ",\n".join(order_items)
571
+
572
+ @to_string.register
573
+ def _(self, arg: Ordering):
574
+ return arg.value
575
+
576
+ @to_string.register
577
+ def _(self, arg: "WhereClause"):
578
+ base = f"{self.to_string(arg.conditional)}"
579
+ if base[0] == "(" and base[-1] == ")":
580
+ return base[1:-1]
581
+ return base
582
+
583
+ @to_string.register
584
+ def _(self, arg: "Conditional"):
585
+ return f"{self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)}"
586
+
587
+ @to_string.register
588
+ def _(self, arg: "SubselectComparison"):
589
+ return f"{self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)}"
590
+
591
+ @to_string.register
592
+ def _(self, arg: "Comparison"):
593
+ return f"{self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)}"
594
+
595
+ @to_string.register
596
+ def _(self, arg: "Comment"):
597
+ lines = "\n#".join(arg.text.split("\n"))
598
+ return f"{lines}"
599
+
600
+ @to_string.register
601
+ def _(self, arg: "WindowItem"):
602
+ over = ",".join(self.to_string(c) for c in arg.over)
603
+ order = ",".join(self.to_string(c) for c in arg.order_by)
604
+ if over and order:
605
+ return (
606
+ f"{arg.type.value} {self.to_string(arg.content)} by {order} over {over}"
607
+ )
608
+ elif over:
609
+ return f"{arg.type.value} {self.to_string(arg.content)} over {over}"
610
+ return f"{arg.type.value} {self.to_string(arg.content)} by {order}"
611
+
612
+ @to_string.register
613
+ def _(self, arg: "FilterItem"):
614
+ return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
615
+
616
+ @to_string.register
617
+ def _(self, arg: "ConceptRef"):
618
+ if arg.address == "__preql_internal.all_rows":
619
+ return "*"
620
+ if arg.name.startswith(VIRTUAL_CONCEPT_PREFIX) and self.environment:
621
+ return self.to_string(self.environment.concepts[arg.address])
622
+
623
+ ns, base = arg.address.rsplit(".", 1)
624
+ if ns == DEFAULT_NAMESPACE:
625
+ return base
626
+ return arg.address
627
+
628
+ @to_string.register
629
+ def _(self, arg: "ImportStatement"):
630
+ path: str = str(arg.path).replace("\\", ".")
631
+ path = path.replace("/", ".")
632
+ if path.endswith(".preql"):
633
+ path = path.rsplit(".", 1)[0]
634
+ if path.startswith("."):
635
+ path = path[1:]
636
+ if arg.alias == DEFAULT_NAMESPACE or not arg.alias:
637
+ return f"import {path};"
638
+ return f"import {path} as {arg.alias};"
639
+
640
+ @to_string.register
641
+ def _(self, arg: "Import"):
642
+ path: str = str(arg.path).replace("\\", ".")
643
+ path = path.replace("/", ".")
644
+ if path.endswith(".preql"):
645
+ path = path.rsplit(".", 1)[0]
646
+ if path.startswith("."):
647
+ path = path[1:]
648
+ if arg.alias == DEFAULT_NAMESPACE or not arg.alias:
649
+ return f"import {path};"
650
+ return f"import {path} as {arg.alias};"
651
+
652
+ @to_string.register
653
+ def _(self, arg: "Concept"):
654
+ if arg.name.startswith(VIRTUAL_CONCEPT_PREFIX):
655
+ return self.to_string(arg.lineage)
656
+ if arg.namespace == DEFAULT_NAMESPACE:
657
+ return arg.name
658
+ return arg.address
659
+
660
+ @to_string.register
661
+ def _(self, arg: "ConceptTransform"):
662
+ return f"{self.to_string(arg.function)} -> {arg.output.name}"
663
+
664
+ @to_string.register
665
+ def _(self, arg: "Function"):
666
+ args = [self.to_string(c) for c in arg.arguments]
667
+
668
+ if arg.operator == FunctionType.SUBTRACT:
669
+ return " - ".join(args)
670
+ if arg.operator == FunctionType.ADD:
671
+ return " + ".join(args)
672
+ if arg.operator == FunctionType.MULTIPLY:
673
+ return " * ".join(args)
674
+ if arg.operator == FunctionType.DIVIDE:
675
+ return " / ".join(args)
676
+ if arg.operator == FunctionType.MOD:
677
+ return f"{args[0]} % {args[1]}"
678
+ if arg.operator == FunctionType.PARENTHETICAL:
679
+ return f"({args[0]})"
680
+ if arg.operator == FunctionType.GROUP:
681
+ arg_string = ", ".join(args[1:])
682
+ if len(args) == 1:
683
+ return f"group({args[0]})"
684
+ return f"group({args[0]}) by {arg_string}"
685
+
686
+ if arg.operator == FunctionType.CONSTANT:
687
+ return f"{', '.join(args)}"
688
+ if arg.operator == FunctionType.CAST:
689
+ return f"{self.to_string(arg.arguments[0])}::{self.to_string(arg.arguments[1])}"
690
+ if arg.operator == FunctionType.INDEX_ACCESS:
691
+ return f"{self.to_string(arg.arguments[0])}[{self.to_string(arg.arguments[1])}]"
692
+
693
+ if arg.operator == FunctionType.CASE:
694
+ with self.indented():
695
+ indented_args = [
696
+ self.indent_lines(self.to_string(a)) for a in arg.arguments
697
+ ]
698
+ inputs = "\n".join(indented_args)
699
+ return f"CASE\n{inputs}\n{self.indent_context.current_indent}END"
700
+
701
+ if arg.operator == FunctionType.STRUCT:
702
+ # zip arguments to pairs
703
+ input_pairs = zip(arg.arguments[0::2], arg.arguments[1::2])
704
+ with self.indented():
705
+ pair_strings = []
706
+ for k, v in input_pairs:
707
+ pair_line = f"{self.to_string(k)}-> {v}"
708
+ pair_strings.append(self.indent_lines(pair_line))
709
+ inputs = ",\n".join(pair_strings)
710
+ return f"struct(\n{inputs}\n{self.indent_context.current_indent})"
711
+ if arg.operator == FunctionType.ALIAS:
712
+ return f"{self.to_string(arg.arguments[0])}"
713
+ inputs = ",".join(args)
714
+ return f"{arg.operator.value}({inputs})"
715
+
716
+ @to_string.register
717
+ def _(self, arg: "OrderItem"):
718
+ return f"{self.to_string(arg.expr)} {arg.order.value}"
719
+
720
+ @to_string.register
721
+ def _(self, arg: AggregateWrapper):
722
+ if arg.by:
723
+ by = ", ".join([self.to_string(x) for x in arg.by])
724
+ return f"{self.to_string(arg.function)} by {by}"
725
+ return f"{self.to_string(arg.function)}"
726
+
727
+ @to_string.register
728
+ def _(self, arg: MergeStatementV2):
729
+ if len(arg.sources) == 1:
730
+ return f"MERGE {self.to_string(arg.sources[0])} into {''.join([self.to_string(modifier) for modifier in arg.modifiers])}{self.to_string(arg.targets[arg.sources[0].address])};"
731
+ return f"MERGE {arg.source_wildcard}.* into {''.join([self.to_string(modifier) for modifier in arg.modifiers])}{arg.target_wildcard}.*;"
732
+
733
+ @to_string.register
734
+ def _(self, arg: KeyMergeStatement):
735
+ keys = ", ".join(sorted(list(arg.keys)))
736
+ return f"MERGE PROPERTY <{keys}> from {arg.target.address};"
737
+
738
+ @to_string.register
739
+ def _(self, arg: Modifier):
740
+ if arg == Modifier.PARTIAL:
741
+ return "~"
742
+ elif arg == Modifier.HIDDEN:
743
+ return "--"
744
+ elif arg == Modifier.NULLABLE:
745
+ return "?"
746
+ return arg.value
747
+
748
+ @to_string.register
749
+ def _(self, arg: int):
750
+ return f"{arg}"
751
+
752
+ @to_string.register
753
+ def _(self, arg: str):
754
+ return f"'{arg}'"
755
+
756
+ @to_string.register
757
+ def _(self, arg: float):
758
+ return f"{arg}"
759
+
760
+ @to_string.register
761
+ def _(self, arg: bool):
762
+ return f"{arg}"
763
+
764
+ @to_string.register
765
+ def _(self, arg: list):
766
+ base = ", ".join([self.to_string(x) for x in arg])
767
+ return f"[{base}]"
768
+
769
+
770
+ def render_query(query: "SelectStatement") -> str:
771
+ return Renderer().to_string(query)
772
+
773
+
774
+ def render_environment(environment: "Environment") -> str:
775
+ return Renderer().to_string(environment)