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
@@ -0,0 +1,773 @@
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.status != DatasourceState.PUBLISHED:
289
+ base += f"\nstate {arg.status.value.lower()}"
290
+
291
+ base += ";"
292
+ return base
293
+
294
+ @to_string.register
295
+ def _(self, arg: "Grain"):
296
+ final = []
297
+ for comp in arg.components:
298
+ if comp.startswith(DEFAULT_NAMESPACE):
299
+ final.append(comp.split(".", 1)[1])
300
+ else:
301
+ final.append(comp)
302
+ final = sorted(final)
303
+ components = ",".join(x for x in final)
304
+ return f"grain ({components})"
305
+
306
+ @to_string.register
307
+ def _(self, arg: "Query"):
308
+ return f"""query {arg.text}"""
309
+
310
+ @to_string.register
311
+ def _(self, arg: RowsetDerivationStatement):
312
+ return f"""rowset {arg.name} <- {self.to_string(arg.select)}"""
313
+
314
+ @to_string.register
315
+ def _(self, arg: "CaseWhen"):
316
+ return (
317
+ f"""WHEN {self.to_string(arg.comparison)} THEN {self.to_string(arg.expr)}"""
318
+ )
319
+
320
+ @to_string.register
321
+ def _(self, arg: "CaseElse"):
322
+ return f"""ELSE {self.to_string(arg.expr)}"""
323
+
324
+ @to_string.register
325
+ def _(self, arg: "FunctionCallWrapper"):
326
+ args = [self.to_string(c) for c in arg.args]
327
+ arg_string = ", ".join(args)
328
+ return f"""@{arg.name}({arg_string})"""
329
+
330
+ @to_string.register
331
+ def _(self, arg: "Parenthetical"):
332
+ return f"""({self.to_string(arg.content)})"""
333
+
334
+ @to_string.register
335
+ def _(self, arg: DataType):
336
+ return arg.value
337
+
338
+ @to_string.register
339
+ def _(self, arg: "NumericType"):
340
+ return f"""Numeric({arg.precision},{arg.scale})"""
341
+
342
+ @to_string.register
343
+ def _(self, arg: TraitDataType):
344
+ traits = "::".join([x for x in arg.traits])
345
+ return f"{self.to_string(arg.data_type)}::{traits}"
346
+
347
+ @to_string.register
348
+ def _(self, arg: ListWrapper):
349
+ return "[" + ", ".join([self.to_string(x) for x in arg]) + "]"
350
+
351
+ @to_string.register
352
+ def _(self, arg: TupleWrapper):
353
+ return "(" + ", ".join([self.to_string(x) for x in arg]) + ")"
354
+
355
+ @to_string.register
356
+ def _(self, arg: MapWrapper):
357
+ def process_key_value(key, value):
358
+ return f"{self.to_string(key)}: {self.to_string(value)}"
359
+
360
+ return (
361
+ "{"
362
+ + ", ".join([process_key_value(key, value) for key, value in arg.items()])
363
+ + "}"
364
+ )
365
+
366
+ @to_string.register
367
+ def _(self, arg: DatePart):
368
+ return arg.value
369
+
370
+ @to_string.register
371
+ def _(self, arg: "Address"):
372
+ if arg.is_query:
373
+ if arg.location.startswith("("):
374
+ return f"query '''{arg.location[1:-1]}'''"
375
+ return f"query '''{arg.location}'''"
376
+ elif arg.is_file:
377
+ return f"file '''`{arg.location}`'''"
378
+ return f"address {arg.location}"
379
+
380
+ @to_string.register
381
+ def _(self, arg: "RawSQLStatement"):
382
+ return f"raw_sql('''{arg.text}''');"
383
+
384
+ @to_string.register
385
+ def _(self, arg: "MagicConstants"):
386
+ if arg == MagicConstants.NULL:
387
+ return "null"
388
+ return arg.value
389
+
390
+ @to_string.register
391
+ def _(self, arg: "ColumnAssignment"):
392
+ if arg.modifiers:
393
+ modifiers = "".join(
394
+ [self.to_string(modifier) for modifier in sorted(arg.modifiers)]
395
+ )
396
+ else:
397
+ modifiers = ""
398
+
399
+ # Get concept string representation
400
+ concept_str = self.to_string(arg.concept)
401
+
402
+ # Get alias string representation
403
+ if isinstance(arg.alias, str):
404
+ alias_str = arg.alias
405
+ else:
406
+ alias_str = self.to_string(arg.alias)
407
+
408
+ # If alias matches concept string and no modifiers, use shorthand
409
+ if alias_str == concept_str and not modifiers:
410
+ return alias_str
411
+
412
+ # Otherwise use full syntax
413
+ return f"{alias_str}: {modifiers}{concept_str}"
414
+
415
+ @to_string.register
416
+ def _(self, arg: "RawColumnExpr"):
417
+ return f"raw('''{arg.text}''')"
418
+
419
+ @to_string.register
420
+ def _(self, arg: "ConceptDeclarationStatement"):
421
+ concept = arg.concept
422
+ if concept.metadata and concept.metadata.description:
423
+ base_description = concept.metadata.description
424
+ else:
425
+ base_description = None
426
+ if concept.namespace and concept.namespace != DEFAULT_NAMESPACE:
427
+ namespace = f"{concept.namespace}."
428
+ else:
429
+ namespace = ""
430
+ if not concept.lineage:
431
+ if concept.purpose == Purpose.PROPERTY and concept.keys:
432
+ if len(concept.keys) == 1:
433
+ output = f"{concept.purpose.value} {self.to_string(ConceptRef(address=safe_address(list(concept.keys)[0])))}.{namespace}{concept.name} {self.to_string(concept.datatype)};"
434
+ else:
435
+ keys = ",".join(
436
+ sorted(
437
+ list(
438
+ self.to_string(ConceptRef(address=safe_address(x)))
439
+ for x in concept.keys
440
+ )
441
+ )
442
+ )
443
+ output = f"{concept.purpose.value} <{keys}>.{namespace}{concept.name} {self.to_string(concept.datatype)};"
444
+ else:
445
+ output = f"{concept.purpose.value} {namespace}{concept.name} {self.to_string(concept.datatype)};"
446
+ else:
447
+ output = f"{concept.purpose.value} {namespace}{concept.name} <- {self.to_string(concept.lineage)};"
448
+ if base_description:
449
+ lines = "\n#".join(base_description.split("\n"))
450
+ output += f" #{lines}"
451
+ return output
452
+
453
+ @to_string.register
454
+ def _(self, arg: ArrayType):
455
+ return f"list<{self.to_string(arg.value_data_type)}>"
456
+
457
+ @to_string.register
458
+ def _(self, arg: DataType):
459
+ return arg.value
460
+
461
+ @to_string.register
462
+ def _(self, arg: date):
463
+ return f"'{arg.isoformat()}'::date"
464
+
465
+ @to_string.register
466
+ def _(self, arg: datetime):
467
+ return f"'{arg.isoformat()}'::datetime"
468
+
469
+ @to_string.register
470
+ def _(self, arg: ConceptDerivationStatement):
471
+ # this is identical rendering;
472
+ return self.to_string(ConceptDeclarationStatement(concept=arg.concept))
473
+
474
+ @to_string.register
475
+ def _(self, arg: PersistStatement):
476
+ if arg.persist_mode == PersistMode.APPEND:
477
+ keyword = "APPEND"
478
+ else:
479
+ keyword = "OVERWRITE"
480
+ if arg.partition_by:
481
+ partition_by = (
482
+ f"BY {', '.join(self.to_string(x) for x in arg.partition_by)}"
483
+ )
484
+ return f"{keyword} {arg.identifier} INTO {arg.address.location} {partition_by} FROM {self.to_string(arg.select)}"
485
+ return f"{keyword} {arg.identifier} INTO {arg.address.location} FROM {self.to_string(arg.select)}"
486
+
487
+ @to_string.register
488
+ def _(self, arg: SelectItem):
489
+ prefixes = []
490
+ if Modifier.HIDDEN in arg.modifiers:
491
+ prefixes.append("--")
492
+ if Modifier.PARTIAL in arg.modifiers:
493
+ prefixes.append("~")
494
+ final = "".join(prefixes)
495
+ return f"{final}{self.to_string(arg.content)}"
496
+
497
+ @to_string.register
498
+ def _(self, arg: ValidateStatement):
499
+ targets = ",".join(arg.targets) if arg.targets else "*"
500
+ if arg.scope.value == ValidationScope.ALL:
501
+ return "validate all;"
502
+ return f"validate {arg.scope.value} {targets};"
503
+
504
+ @to_string.register
505
+ def _(self, arg: SelectStatement):
506
+ with self.indented():
507
+ select_columns = [
508
+ self.indent_lines(self.to_string(c)) for c in arg.selection
509
+ ]
510
+ where_clause = None
511
+ if arg.where_clause:
512
+ where_clause = self.indent_lines(self.to_string(arg.where_clause))
513
+ having_clause = None
514
+ if arg.having_clause:
515
+ having_clause = self.indent_lines(self.to_string(arg.having_clause))
516
+ order_by = None
517
+ if arg.order_by:
518
+ order_by = [
519
+ self.indent_lines(self.to_string(c)) for c in arg.order_by.items
520
+ ]
521
+
522
+ return QUERY_TEMPLATE.render(
523
+ select_columns=select_columns,
524
+ where=where_clause,
525
+ having=having_clause,
526
+ order_by=order_by,
527
+ limit=arg.limit,
528
+ )
529
+
530
+ @to_string.register
531
+ def _(self, arg: MultiSelectStatement):
532
+ # Each select gets its own indentation
533
+ select_parts = []
534
+ for select in arg.selects:
535
+ select_parts.append(
536
+ self.to_string(select)[:-2]
537
+ ) # Remove the trailing ";\n"
538
+
539
+ base = "\nMERGE\n".join(select_parts)
540
+ base += self.to_string(arg.align)
541
+ if arg.where_clause:
542
+ base += f"\nWHERE\n{self.to_string(arg.where_clause)}"
543
+ if arg.order_by:
544
+ base += f"\nORDER BY\n{self.to_string(arg.order_by)}"
545
+ if arg.limit:
546
+ base += f"\nLIMIT {arg.limit}"
547
+ base += "\n;"
548
+ return base
549
+
550
+ @to_string.register
551
+ def _(self, arg: CopyStatement):
552
+ return f"COPY INTO {arg.target_type.value.upper()} '{arg.target}' FROM {self.to_string(arg.select)}"
553
+
554
+ @to_string.register
555
+ def _(self, arg: AlignClause):
556
+ with self.indented():
557
+ align_items = [self.indent_lines(self.to_string(c)) for c in arg.items]
558
+ return "\nALIGN\n" + ",\n".join(align_items)
559
+
560
+ @to_string.register
561
+ def _(self, arg: AlignItem):
562
+ return f"{arg.alias}:{','.join([self.to_string(c) for c in arg.concepts])}"
563
+
564
+ @to_string.register
565
+ def _(self, arg: OrderBy):
566
+ with self.indented():
567
+ order_items = [self.indent_lines(self.to_string(c)) for c in arg.items]
568
+ return ",\n".join(order_items)
569
+
570
+ @to_string.register
571
+ def _(self, arg: Ordering):
572
+ return arg.value
573
+
574
+ @to_string.register
575
+ def _(self, arg: "WhereClause"):
576
+ base = f"{self.to_string(arg.conditional)}"
577
+ if base[0] == "(" and base[-1] == ")":
578
+ return base[1:-1]
579
+ return base
580
+
581
+ @to_string.register
582
+ def _(self, arg: "Conditional"):
583
+ return f"{self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)}"
584
+
585
+ @to_string.register
586
+ def _(self, arg: "SubselectComparison"):
587
+ return f"{self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)}"
588
+
589
+ @to_string.register
590
+ def _(self, arg: "Comparison"):
591
+ return f"{self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)}"
592
+
593
+ @to_string.register
594
+ def _(self, arg: "Comment"):
595
+ lines = "\n#".join(arg.text.split("\n"))
596
+ return f"{lines}"
597
+
598
+ @to_string.register
599
+ def _(self, arg: "WindowItem"):
600
+ over = ",".join(self.to_string(c) for c in arg.over)
601
+ order = ",".join(self.to_string(c) for c in arg.order_by)
602
+ if over and order:
603
+ return (
604
+ f"{arg.type.value} {self.to_string(arg.content)} by {order} over {over}"
605
+ )
606
+ elif over:
607
+ return f"{arg.type.value} {self.to_string(arg.content)} over {over}"
608
+ return f"{arg.type.value} {self.to_string(arg.content)} by {order}"
609
+
610
+ @to_string.register
611
+ def _(self, arg: "FilterItem"):
612
+ return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
613
+
614
+ @to_string.register
615
+ def _(self, arg: "ConceptRef"):
616
+ if arg.address == "__preql_internal.all_rows":
617
+ return "*"
618
+ if arg.name.startswith(VIRTUAL_CONCEPT_PREFIX) and self.environment:
619
+ return self.to_string(self.environment.concepts[arg.address])
620
+
621
+ ns, base = arg.address.rsplit(".", 1)
622
+ if ns == DEFAULT_NAMESPACE:
623
+ return base
624
+ return arg.address
625
+
626
+ @to_string.register
627
+ def _(self, arg: "ImportStatement"):
628
+ path: str = str(arg.path).replace("\\", ".")
629
+ path = path.replace("/", ".")
630
+ if path.endswith(".preql"):
631
+ path = path.rsplit(".", 1)[0]
632
+ if path.startswith("."):
633
+ path = path[1:]
634
+ if arg.alias == DEFAULT_NAMESPACE or not arg.alias:
635
+ return f"import {path};"
636
+ return f"import {path} as {arg.alias};"
637
+
638
+ @to_string.register
639
+ def _(self, arg: "Import"):
640
+ path: str = str(arg.path).replace("\\", ".")
641
+ path = path.replace("/", ".")
642
+ if path.endswith(".preql"):
643
+ path = path.rsplit(".", 1)[0]
644
+ if path.startswith("."):
645
+ path = path[1:]
646
+ if arg.alias == DEFAULT_NAMESPACE or not arg.alias:
647
+ return f"import {path};"
648
+ return f"import {path} as {arg.alias};"
649
+
650
+ @to_string.register
651
+ def _(self, arg: "Concept"):
652
+ if arg.name.startswith(VIRTUAL_CONCEPT_PREFIX):
653
+ return self.to_string(arg.lineage)
654
+ if arg.namespace == DEFAULT_NAMESPACE:
655
+ return arg.name
656
+ return arg.address
657
+
658
+ @to_string.register
659
+ def _(self, arg: "ConceptTransform"):
660
+ return f"{self.to_string(arg.function)} -> {arg.output.name}"
661
+
662
+ @to_string.register
663
+ def _(self, arg: "Function"):
664
+ args = [self.to_string(c) for c in arg.arguments]
665
+
666
+ if arg.operator == FunctionType.SUBTRACT:
667
+ return " - ".join(args)
668
+ if arg.operator == FunctionType.ADD:
669
+ return " + ".join(args)
670
+ if arg.operator == FunctionType.MULTIPLY:
671
+ return " * ".join(args)
672
+ if arg.operator == FunctionType.DIVIDE:
673
+ return " / ".join(args)
674
+ if arg.operator == FunctionType.MOD:
675
+ return f"{args[0]} % {args[1]}"
676
+ if arg.operator == FunctionType.PARENTHETICAL:
677
+ return f"({args[0]})"
678
+ if arg.operator == FunctionType.GROUP:
679
+ arg_string = ", ".join(args[1:])
680
+ if len(args) == 1:
681
+ return f"group({args[0]})"
682
+ return f"group({args[0]}) by {arg_string}"
683
+
684
+ if arg.operator == FunctionType.CONSTANT:
685
+ return f"{', '.join(args)}"
686
+ if arg.operator == FunctionType.CAST:
687
+ return f"{self.to_string(arg.arguments[0])}::{self.to_string(arg.arguments[1])}"
688
+ if arg.operator == FunctionType.INDEX_ACCESS:
689
+ return f"{self.to_string(arg.arguments[0])}[{self.to_string(arg.arguments[1])}]"
690
+
691
+ if arg.operator == FunctionType.CASE:
692
+ with self.indented():
693
+ indented_args = [
694
+ self.indent_lines(self.to_string(a)) for a in arg.arguments
695
+ ]
696
+ inputs = "\n".join(indented_args)
697
+ return f"CASE\n{inputs}\n{self.indent_context.current_indent}END"
698
+
699
+ if arg.operator == FunctionType.STRUCT:
700
+ # zip arguments to pairs
701
+ input_pairs = zip(arg.arguments[0::2], arg.arguments[1::2])
702
+ with self.indented():
703
+ pair_strings = []
704
+ for k, v in input_pairs:
705
+ pair_line = f"{self.to_string(k)}-> {v}"
706
+ pair_strings.append(self.indent_lines(pair_line))
707
+ inputs = ",\n".join(pair_strings)
708
+ return f"struct(\n{inputs}\n{self.indent_context.current_indent})"
709
+ if arg.operator == FunctionType.ALIAS:
710
+ return f"{self.to_string(arg.arguments[0])}"
711
+ inputs = ",".join(args)
712
+ return f"{arg.operator.value}({inputs})"
713
+
714
+ @to_string.register
715
+ def _(self, arg: "OrderItem"):
716
+ return f"{self.to_string(arg.expr)} {arg.order.value}"
717
+
718
+ @to_string.register
719
+ def _(self, arg: AggregateWrapper):
720
+ if arg.by:
721
+ by = ", ".join([self.to_string(x) for x in arg.by])
722
+ return f"{self.to_string(arg.function)} by {by}"
723
+ return f"{self.to_string(arg.function)}"
724
+
725
+ @to_string.register
726
+ def _(self, arg: MergeStatementV2):
727
+ if len(arg.sources) == 1:
728
+ 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])};"
729
+ return f"MERGE {arg.source_wildcard}.* into {''.join([self.to_string(modifier) for modifier in arg.modifiers])}{arg.target_wildcard}.*;"
730
+
731
+ @to_string.register
732
+ def _(self, arg: KeyMergeStatement):
733
+ keys = ", ".join(sorted(list(arg.keys)))
734
+ return f"MERGE PROPERTY <{keys}> from {arg.target.address};"
735
+
736
+ @to_string.register
737
+ def _(self, arg: Modifier):
738
+ if arg == Modifier.PARTIAL:
739
+ return "~"
740
+ elif arg == Modifier.HIDDEN:
741
+ return "--"
742
+ elif arg == Modifier.NULLABLE:
743
+ return "?"
744
+ return arg.value
745
+
746
+ @to_string.register
747
+ def _(self, arg: int):
748
+ return f"{arg}"
749
+
750
+ @to_string.register
751
+ def _(self, arg: str):
752
+ return f"'{arg}'"
753
+
754
+ @to_string.register
755
+ def _(self, arg: float):
756
+ return f"{arg}"
757
+
758
+ @to_string.register
759
+ def _(self, arg: bool):
760
+ return f"{arg}"
761
+
762
+ @to_string.register
763
+ def _(self, arg: list):
764
+ base = ", ".join([self.to_string(x) for x in arg])
765
+ return f"[{base}]"
766
+
767
+
768
+ def render_query(query: "SelectStatement") -> str:
769
+ return Renderer().to_string(query)
770
+
771
+
772
+ def render_environment(environment: "Environment") -> str:
773
+ return Renderer().to_string(environment)