pytrilogy 0.3.138__cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

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