pytrilogy 0.3.142__cp313-cp313-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 (200) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cpython-313-x86_64-linux-gnu.so +0 -0
  4. pytrilogy-0.3.142.dist-info/METADATA +555 -0
  5. pytrilogy-0.3.142.dist-info/RECORD +200 -0
  6. pytrilogy-0.3.142.dist-info/WHEEL +5 -0
  7. pytrilogy-0.3.142.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.142.dist-info/licenses/LICENSE.md +19 -0
  9. trilogy/__init__.py +16 -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 +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 +2669 -0
  40. trilogy/core/models/build.py +2521 -0
  41. trilogy/core/models/build_environment.py +180 -0
  42. trilogy/core/models/core.py +501 -0
  43. trilogy/core/models/datasource.py +322 -0
  44. trilogy/core/models/environment.py +751 -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 +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 +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 +256 -0
  110. trilogy/dialect/__init__.py +32 -0
  111. trilogy/dialect/base.py +1392 -0
  112. trilogy/dialect/bigquery.py +308 -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 +231 -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 +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/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 +769 -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 +9 -0
  146. trilogy/scripts/__init__.py +0 -0
  147. trilogy/scripts/agent.py +41 -0
  148. trilogy/scripts/agent_info.py +303 -0
  149. trilogy/scripts/common.py +355 -0
  150. trilogy/scripts/dependency/Cargo.lock +617 -0
  151. trilogy/scripts/dependency/Cargo.toml +39 -0
  152. trilogy/scripts/dependency/README.md +131 -0
  153. trilogy/scripts/dependency/build.sh +25 -0
  154. trilogy/scripts/dependency/src/directory_resolver.rs +177 -0
  155. trilogy/scripts/dependency/src/lib.rs +16 -0
  156. trilogy/scripts/dependency/src/main.rs +770 -0
  157. trilogy/scripts/dependency/src/parser.rs +435 -0
  158. trilogy/scripts/dependency/src/preql.pest +208 -0
  159. trilogy/scripts/dependency/src/python_bindings.rs +303 -0
  160. trilogy/scripts/dependency/src/resolver.rs +716 -0
  161. trilogy/scripts/dependency/tests/base.preql +3 -0
  162. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  163. trilogy/scripts/dependency/tests/customer.preql +6 -0
  164. trilogy/scripts/dependency/tests/main.preql +9 -0
  165. trilogy/scripts/dependency/tests/orders.preql +7 -0
  166. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  167. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  168. trilogy/scripts/dependency.py +323 -0
  169. trilogy/scripts/display.py +512 -0
  170. trilogy/scripts/environment.py +46 -0
  171. trilogy/scripts/fmt.py +32 -0
  172. trilogy/scripts/ingest.py +471 -0
  173. trilogy/scripts/ingest_helpers/__init__.py +1 -0
  174. trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
  175. trilogy/scripts/ingest_helpers/formatting.py +93 -0
  176. trilogy/scripts/ingest_helpers/typing.py +161 -0
  177. trilogy/scripts/init.py +105 -0
  178. trilogy/scripts/parallel_execution.py +713 -0
  179. trilogy/scripts/plan.py +189 -0
  180. trilogy/scripts/run.py +63 -0
  181. trilogy/scripts/serve.py +140 -0
  182. trilogy/scripts/serve_helpers/__init__.py +41 -0
  183. trilogy/scripts/serve_helpers/file_discovery.py +142 -0
  184. trilogy/scripts/serve_helpers/index_generation.py +206 -0
  185. trilogy/scripts/serve_helpers/models.py +38 -0
  186. trilogy/scripts/single_execution.py +131 -0
  187. trilogy/scripts/testing.py +119 -0
  188. trilogy/scripts/trilogy.py +68 -0
  189. trilogy/std/__init__.py +0 -0
  190. trilogy/std/color.preql +3 -0
  191. trilogy/std/date.preql +13 -0
  192. trilogy/std/display.preql +18 -0
  193. trilogy/std/geography.preql +22 -0
  194. trilogy/std/metric.preql +15 -0
  195. trilogy/std/money.preql +67 -0
  196. trilogy/std/net.preql +14 -0
  197. trilogy/std/ranking.preql +7 -0
  198. trilogy/std/report.preql +5 -0
  199. trilogy/std/semantic.preql +6 -0
  200. trilogy/utility.py +34 -0
@@ -0,0 +1,769 @@
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
+
273
+ base = f"""datasource {arg.name} (
274
+ {assignments}
275
+ )
276
+ {self.to_string(arg.grain) if arg.grain.components else ''}{non_partial}
277
+ {self.to_string(arg.address)}"""
278
+ if arg.where:
279
+ base += f"\nwhere {self.to_string(arg.where)}"
280
+
281
+ if arg.incremental_by:
282
+ base += f"\nincremental by {','.join(self.to_string(x) for x in arg.incremental_by)}"
283
+
284
+ if arg.partition_by:
285
+ base += f"\npartition by {','.join(self.to_string(x) for x in arg.partition_by)}"
286
+ if arg.status != DatasourceState.PUBLISHED:
287
+ base += f"\nstate {arg.status.value.lower()}"
288
+
289
+ base += ";"
290
+ return base
291
+
292
+ @to_string.register
293
+ def _(self, arg: "Grain"):
294
+ final = []
295
+ for comp in arg.components:
296
+ if comp.startswith(DEFAULT_NAMESPACE):
297
+ final.append(comp.split(".", 1)[1])
298
+ else:
299
+ final.append(comp)
300
+ final = sorted(final)
301
+ components = ",".join(x for x in final)
302
+ return f"grain ({components})"
303
+
304
+ @to_string.register
305
+ def _(self, arg: "Query"):
306
+ return f"""query {arg.text}"""
307
+
308
+ @to_string.register
309
+ def _(self, arg: RowsetDerivationStatement):
310
+ return f"""rowset {arg.name} <- {self.to_string(arg.select)}"""
311
+
312
+ @to_string.register
313
+ def _(self, arg: "CaseWhen"):
314
+ return (
315
+ f"""WHEN {self.to_string(arg.comparison)} THEN {self.to_string(arg.expr)}"""
316
+ )
317
+
318
+ @to_string.register
319
+ def _(self, arg: "CaseElse"):
320
+ return f"""ELSE {self.to_string(arg.expr)}"""
321
+
322
+ @to_string.register
323
+ def _(self, arg: "FunctionCallWrapper"):
324
+ args = [self.to_string(c) for c in arg.args]
325
+ arg_string = ", ".join(args)
326
+ return f"""@{arg.name}({arg_string})"""
327
+
328
+ @to_string.register
329
+ def _(self, arg: "Parenthetical"):
330
+ return f"""({self.to_string(arg.content)})"""
331
+
332
+ @to_string.register
333
+ def _(self, arg: DataType):
334
+ return arg.value
335
+
336
+ @to_string.register
337
+ def _(self, arg: "NumericType"):
338
+ return f"""Numeric({arg.precision},{arg.scale})"""
339
+
340
+ @to_string.register
341
+ def _(self, arg: TraitDataType):
342
+ traits = "::".join([x for x in arg.traits])
343
+ return f"{self.to_string(arg.data_type)}::{traits}"
344
+
345
+ @to_string.register
346
+ def _(self, arg: ListWrapper):
347
+ return "[" + ", ".join([self.to_string(x) for x in arg]) + "]"
348
+
349
+ @to_string.register
350
+ def _(self, arg: TupleWrapper):
351
+ return "(" + ", ".join([self.to_string(x) for x in arg]) + ")"
352
+
353
+ @to_string.register
354
+ def _(self, arg: MapWrapper):
355
+ def process_key_value(key, value):
356
+ return f"{self.to_string(key)}: {self.to_string(value)}"
357
+
358
+ return (
359
+ "{"
360
+ + ", ".join([process_key_value(key, value) for key, value in arg.items()])
361
+ + "}"
362
+ )
363
+
364
+ @to_string.register
365
+ def _(self, arg: DatePart):
366
+ return arg.value
367
+
368
+ @to_string.register
369
+ def _(self, arg: "Address"):
370
+ if arg.is_query:
371
+ if arg.location.startswith("("):
372
+ return f"query '''{arg.location[1:-1]}'''"
373
+ return f"query '''{arg.location}'''"
374
+ return f"address {arg.location}"
375
+
376
+ @to_string.register
377
+ def _(self, arg: "RawSQLStatement"):
378
+ return f"raw_sql('''{arg.text}''');"
379
+
380
+ @to_string.register
381
+ def _(self, arg: "MagicConstants"):
382
+ if arg == MagicConstants.NULL:
383
+ return "null"
384
+ return arg.value
385
+
386
+ @to_string.register
387
+ def _(self, arg: "ColumnAssignment"):
388
+ if arg.modifiers:
389
+ modifiers = "".join(
390
+ [self.to_string(modifier) for modifier in sorted(arg.modifiers)]
391
+ )
392
+ else:
393
+ modifiers = ""
394
+
395
+ # Get concept string representation
396
+ concept_str = self.to_string(arg.concept)
397
+
398
+ # Get alias string representation
399
+ if isinstance(arg.alias, str):
400
+ alias_str = arg.alias
401
+ else:
402
+ alias_str = self.to_string(arg.alias)
403
+
404
+ # If alias matches concept string and no modifiers, use shorthand
405
+ if alias_str == concept_str and not modifiers:
406
+ return alias_str
407
+
408
+ # Otherwise use full syntax
409
+ return f"{alias_str}: {modifiers}{concept_str}"
410
+
411
+ @to_string.register
412
+ def _(self, arg: "RawColumnExpr"):
413
+ return f"raw('''{arg.text}''')"
414
+
415
+ @to_string.register
416
+ def _(self, arg: "ConceptDeclarationStatement"):
417
+ concept = arg.concept
418
+ if concept.metadata and concept.metadata.description:
419
+ base_description = concept.metadata.description
420
+ else:
421
+ base_description = None
422
+ if concept.namespace and concept.namespace != DEFAULT_NAMESPACE:
423
+ namespace = f"{concept.namespace}."
424
+ else:
425
+ namespace = ""
426
+ if not concept.lineage:
427
+ if concept.purpose == Purpose.PROPERTY and concept.keys:
428
+ if len(concept.keys) == 1:
429
+ output = f"{concept.purpose.value} {self.to_string(ConceptRef(address=safe_address(list(concept.keys)[0])))}.{namespace}{concept.name} {self.to_string(concept.datatype)};"
430
+ else:
431
+ keys = ",".join(
432
+ sorted(
433
+ list(
434
+ self.to_string(ConceptRef(address=safe_address(x)))
435
+ for x in concept.keys
436
+ )
437
+ )
438
+ )
439
+ output = f"{concept.purpose.value} <{keys}>.{namespace}{concept.name} {self.to_string(concept.datatype)};"
440
+ else:
441
+ output = f"{concept.purpose.value} {namespace}{concept.name} {self.to_string(concept.datatype)};"
442
+ else:
443
+ output = f"{concept.purpose.value} {namespace}{concept.name} <- {self.to_string(concept.lineage)};"
444
+ if base_description:
445
+ lines = "\n#".join(base_description.split("\n"))
446
+ output += f" #{lines}"
447
+ return output
448
+
449
+ @to_string.register
450
+ def _(self, arg: ArrayType):
451
+ return f"list<{self.to_string(arg.value_data_type)}>"
452
+
453
+ @to_string.register
454
+ def _(self, arg: DataType):
455
+ return arg.value
456
+
457
+ @to_string.register
458
+ def _(self, arg: date):
459
+ return f"'{arg.isoformat()}'::date"
460
+
461
+ @to_string.register
462
+ def _(self, arg: datetime):
463
+ return f"'{arg.isoformat()}'::datetime"
464
+
465
+ @to_string.register
466
+ def _(self, arg: ConceptDerivationStatement):
467
+ # this is identical rendering;
468
+ return self.to_string(ConceptDeclarationStatement(concept=arg.concept))
469
+
470
+ @to_string.register
471
+ def _(self, arg: PersistStatement):
472
+ if arg.persist_mode == PersistMode.APPEND:
473
+ keyword = "APPEND"
474
+ else:
475
+ keyword = "OVERWRITE"
476
+ if arg.partition_by:
477
+ partition_by = (
478
+ f"BY {', '.join(self.to_string(x) for x in arg.partition_by)}"
479
+ )
480
+ return f"{keyword} {arg.identifier} INTO {arg.address.location} {partition_by} FROM {self.to_string(arg.select)}"
481
+ return f"{keyword} {arg.identifier} INTO {arg.address.location} FROM {self.to_string(arg.select)}"
482
+
483
+ @to_string.register
484
+ def _(self, arg: SelectItem):
485
+ prefixes = []
486
+ if Modifier.HIDDEN in arg.modifiers:
487
+ prefixes.append("--")
488
+ if Modifier.PARTIAL in arg.modifiers:
489
+ prefixes.append("~")
490
+ final = "".join(prefixes)
491
+ return f"{final}{self.to_string(arg.content)}"
492
+
493
+ @to_string.register
494
+ def _(self, arg: ValidateStatement):
495
+ targets = ",".join(arg.targets) if arg.targets else "*"
496
+ if arg.scope.value == ValidationScope.ALL:
497
+ return "validate all;"
498
+ return f"validate {arg.scope.value} {targets};"
499
+
500
+ @to_string.register
501
+ def _(self, arg: SelectStatement):
502
+ with self.indented():
503
+ select_columns = [
504
+ self.indent_lines(self.to_string(c)) for c in arg.selection
505
+ ]
506
+ where_clause = None
507
+ if arg.where_clause:
508
+ where_clause = self.indent_lines(self.to_string(arg.where_clause))
509
+ having_clause = None
510
+ if arg.having_clause:
511
+ having_clause = self.indent_lines(self.to_string(arg.having_clause))
512
+ order_by = None
513
+ if arg.order_by:
514
+ order_by = [
515
+ self.indent_lines(self.to_string(c)) for c in arg.order_by.items
516
+ ]
517
+
518
+ return QUERY_TEMPLATE.render(
519
+ select_columns=select_columns,
520
+ where=where_clause,
521
+ having=having_clause,
522
+ order_by=order_by,
523
+ limit=arg.limit,
524
+ )
525
+
526
+ @to_string.register
527
+ def _(self, arg: MultiSelectStatement):
528
+ # Each select gets its own indentation
529
+ select_parts = []
530
+ for select in arg.selects:
531
+ select_parts.append(
532
+ self.to_string(select)[:-2]
533
+ ) # Remove the trailing ";\n"
534
+
535
+ base = "\nMERGE\n".join(select_parts)
536
+ base += self.to_string(arg.align)
537
+ if arg.where_clause:
538
+ base += f"\nWHERE\n{self.to_string(arg.where_clause)}"
539
+ if arg.order_by:
540
+ base += f"\nORDER BY\n{self.to_string(arg.order_by)}"
541
+ if arg.limit:
542
+ base += f"\nLIMIT {arg.limit}"
543
+ base += "\n;"
544
+ return base
545
+
546
+ @to_string.register
547
+ def _(self, arg: CopyStatement):
548
+ return f"COPY INTO {arg.target_type.value.upper()} '{arg.target}' FROM {self.to_string(arg.select)}"
549
+
550
+ @to_string.register
551
+ def _(self, arg: AlignClause):
552
+ with self.indented():
553
+ align_items = [self.indent_lines(self.to_string(c)) for c in arg.items]
554
+ return "\nALIGN\n" + ",\n".join(align_items)
555
+
556
+ @to_string.register
557
+ def _(self, arg: AlignItem):
558
+ return f"{arg.alias}:{','.join([self.to_string(c) for c in arg.concepts])}"
559
+
560
+ @to_string.register
561
+ def _(self, arg: OrderBy):
562
+ with self.indented():
563
+ order_items = [self.indent_lines(self.to_string(c)) for c in arg.items]
564
+ return ",\n".join(order_items)
565
+
566
+ @to_string.register
567
+ def _(self, arg: Ordering):
568
+ return arg.value
569
+
570
+ @to_string.register
571
+ def _(self, arg: "WhereClause"):
572
+ base = f"{self.to_string(arg.conditional)}"
573
+ if base[0] == "(" and base[-1] == ")":
574
+ return base[1:-1]
575
+ return base
576
+
577
+ @to_string.register
578
+ def _(self, arg: "Conditional"):
579
+ return f"{self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)}"
580
+
581
+ @to_string.register
582
+ def _(self, arg: "SubselectComparison"):
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: "Comparison"):
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: "Comment"):
591
+ lines = "\n#".join(arg.text.split("\n"))
592
+ return f"{lines}"
593
+
594
+ @to_string.register
595
+ def _(self, arg: "WindowItem"):
596
+ over = ",".join(self.to_string(c) for c in arg.over)
597
+ order = ",".join(self.to_string(c) for c in arg.order_by)
598
+ if over and order:
599
+ return (
600
+ f"{arg.type.value} {self.to_string(arg.content)} by {order} over {over}"
601
+ )
602
+ elif over:
603
+ return f"{arg.type.value} {self.to_string(arg.content)} over {over}"
604
+ return f"{arg.type.value} {self.to_string(arg.content)} by {order}"
605
+
606
+ @to_string.register
607
+ def _(self, arg: "FilterItem"):
608
+ return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
609
+
610
+ @to_string.register
611
+ def _(self, arg: "ConceptRef"):
612
+ if arg.address == "__preql_internal.all_rows":
613
+ return "*"
614
+ if arg.name.startswith(VIRTUAL_CONCEPT_PREFIX) and self.environment:
615
+ return self.to_string(self.environment.concepts[arg.address])
616
+
617
+ ns, base = arg.address.rsplit(".", 1)
618
+ if ns == DEFAULT_NAMESPACE:
619
+ return base
620
+ return arg.address
621
+
622
+ @to_string.register
623
+ def _(self, arg: "ImportStatement"):
624
+ path: str = str(arg.path).replace("\\", ".")
625
+ path = path.replace("/", ".")
626
+ if path.endswith(".preql"):
627
+ path = path.rsplit(".", 1)[0]
628
+ if path.startswith("."):
629
+ path = path[1:]
630
+ if arg.alias == DEFAULT_NAMESPACE or not arg.alias:
631
+ return f"import {path};"
632
+ return f"import {path} as {arg.alias};"
633
+
634
+ @to_string.register
635
+ def _(self, arg: "Import"):
636
+ path: str = str(arg.path).replace("\\", ".")
637
+ path = path.replace("/", ".")
638
+ if path.endswith(".preql"):
639
+ path = path.rsplit(".", 1)[0]
640
+ if path.startswith("."):
641
+ path = path[1:]
642
+ if arg.alias == DEFAULT_NAMESPACE or not arg.alias:
643
+ return f"import {path};"
644
+ return f"import {path} as {arg.alias};"
645
+
646
+ @to_string.register
647
+ def _(self, arg: "Concept"):
648
+ if arg.name.startswith(VIRTUAL_CONCEPT_PREFIX):
649
+ return self.to_string(arg.lineage)
650
+ if arg.namespace == DEFAULT_NAMESPACE:
651
+ return arg.name
652
+ return arg.address
653
+
654
+ @to_string.register
655
+ def _(self, arg: "ConceptTransform"):
656
+ return f"{self.to_string(arg.function)} -> {arg.output.name}"
657
+
658
+ @to_string.register
659
+ def _(self, arg: "Function"):
660
+ args = [self.to_string(c) for c in arg.arguments]
661
+
662
+ if arg.operator == FunctionType.SUBTRACT:
663
+ return " - ".join(args)
664
+ if arg.operator == FunctionType.ADD:
665
+ return " + ".join(args)
666
+ if arg.operator == FunctionType.MULTIPLY:
667
+ return " * ".join(args)
668
+ if arg.operator == FunctionType.DIVIDE:
669
+ return " / ".join(args)
670
+ if arg.operator == FunctionType.MOD:
671
+ return f"{args[0]} % {args[1]}"
672
+ if arg.operator == FunctionType.PARENTHETICAL:
673
+ return f"({args[0]})"
674
+ if arg.operator == FunctionType.GROUP:
675
+ arg_string = ", ".join(args[1:])
676
+ if len(args) == 1:
677
+ return f"group({args[0]})"
678
+ return f"group({args[0]}) by {arg_string}"
679
+
680
+ if arg.operator == FunctionType.CONSTANT:
681
+ return f"{', '.join(args)}"
682
+ if arg.operator == FunctionType.CAST:
683
+ return f"{self.to_string(arg.arguments[0])}::{self.to_string(arg.arguments[1])}"
684
+ if arg.operator == FunctionType.INDEX_ACCESS:
685
+ return f"{self.to_string(arg.arguments[0])}[{self.to_string(arg.arguments[1])}]"
686
+
687
+ if arg.operator == FunctionType.CASE:
688
+ with self.indented():
689
+ indented_args = [
690
+ self.indent_lines(self.to_string(a)) for a in arg.arguments
691
+ ]
692
+ inputs = "\n".join(indented_args)
693
+ return f"CASE\n{inputs}\n{self.indent_context.current_indent}END"
694
+
695
+ if arg.operator == FunctionType.STRUCT:
696
+ # zip arguments to pairs
697
+ input_pairs = zip(arg.arguments[0::2], arg.arguments[1::2])
698
+ with self.indented():
699
+ pair_strings = []
700
+ for k, v in input_pairs:
701
+ pair_line = f"{self.to_string(k)}-> {v}"
702
+ pair_strings.append(self.indent_lines(pair_line))
703
+ inputs = ",\n".join(pair_strings)
704
+ return f"struct(\n{inputs}\n{self.indent_context.current_indent})"
705
+ if arg.operator == FunctionType.ALIAS:
706
+ return f"{self.to_string(arg.arguments[0])}"
707
+ inputs = ",".join(args)
708
+ return f"{arg.operator.value}({inputs})"
709
+
710
+ @to_string.register
711
+ def _(self, arg: "OrderItem"):
712
+ return f"{self.to_string(arg.expr)} {arg.order.value}"
713
+
714
+ @to_string.register
715
+ def _(self, arg: AggregateWrapper):
716
+ if arg.by:
717
+ by = ", ".join([self.to_string(x) for x in arg.by])
718
+ return f"{self.to_string(arg.function)} by {by}"
719
+ return f"{self.to_string(arg.function)}"
720
+
721
+ @to_string.register
722
+ def _(self, arg: MergeStatementV2):
723
+ if len(arg.sources) == 1:
724
+ 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])};"
725
+ return f"MERGE {arg.source_wildcard}.* into {''.join([self.to_string(modifier) for modifier in arg.modifiers])}{arg.target_wildcard}.*;"
726
+
727
+ @to_string.register
728
+ def _(self, arg: KeyMergeStatement):
729
+ keys = ", ".join(sorted(list(arg.keys)))
730
+ return f"MERGE PROPERTY <{keys}> from {arg.target.address};"
731
+
732
+ @to_string.register
733
+ def _(self, arg: Modifier):
734
+ if arg == Modifier.PARTIAL:
735
+ return "~"
736
+ elif arg == Modifier.HIDDEN:
737
+ return "--"
738
+ elif arg == Modifier.NULLABLE:
739
+ return "?"
740
+ return arg.value
741
+
742
+ @to_string.register
743
+ def _(self, arg: int):
744
+ return f"{arg}"
745
+
746
+ @to_string.register
747
+ def _(self, arg: str):
748
+ return f"'{arg}'"
749
+
750
+ @to_string.register
751
+ def _(self, arg: float):
752
+ return f"{arg}"
753
+
754
+ @to_string.register
755
+ def _(self, arg: bool):
756
+ return f"{arg}"
757
+
758
+ @to_string.register
759
+ def _(self, arg: list):
760
+ base = ", ".join([self.to_string(x) for x in arg])
761
+ return f"[{base}]"
762
+
763
+
764
+ def render_query(query: "SelectStatement") -> str:
765
+ return Renderer().to_string(query)
766
+
767
+
768
+ def render_environment(environment: "Environment") -> str:
769
+ return Renderer().to_string(environment)