pyoq-sql 1.0.2__py3-none-any.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 (267) hide show
  1. pyoq/__init__.py +10 -0
  2. pyoq/__main__.py +5 -0
  3. pyoq/_native.pyi +5 -0
  4. pyoq/cli/__init__.py +5 -0
  5. pyoq/cli/commands.py +270 -0
  6. pyoq/cli/defaults.py +98 -0
  7. pyoq/cli/services.py +97 -0
  8. pyoq/config/__init__.py +31 -0
  9. pyoq/config/connection.py +161 -0
  10. pyoq/config/loader.py +289 -0
  11. pyoq/config/models.py +245 -0
  12. pyoq/config/values.py +142 -0
  13. pyoq/descriptors.py +165 -0
  14. pyoq/diagnostics/__init__.py +68 -0
  15. pyoq/diagnostics/budget.py +136 -0
  16. pyoq/diagnostics/events.py +137 -0
  17. pyoq/diagnostics/fingerprint.py +267 -0
  18. pyoq/diagnostics/instrumented.py +237 -0
  19. pyoq/diagnostics/metrics.py +61 -0
  20. pyoq/diagnostics/observation.py +227 -0
  21. pyoq/diagnostics/scoped.py +103 -0
  22. pyoq/django/__init__.py +15 -0
  23. pyoq/django/apps.py +17 -0
  24. pyoq/django/execution.py +317 -0
  25. pyoq/django/generation.py +59 -0
  26. pyoq/django/management/__init__.py +0 -0
  27. pyoq/django/management/commands/__init__.py +0 -0
  28. pyoq/django/management/commands/makemigrations.py +53 -0
  29. pyoq/django/management/commands/pyoq_codegen.py +75 -0
  30. pyoq/django/parameters.py +101 -0
  31. pyoq/django/schema.py +379 -0
  32. pyoq/django/settings.py +87 -0
  33. pyoq/django/timeouts.py +105 -0
  34. pyoq/dsl/__init__.py +64 -0
  35. pyoq/dsl/aio/__init__.py +31 -0
  36. pyoq/dsl/aio/context.py +295 -0
  37. pyoq/dsl/aio/queries.py +335 -0
  38. pyoq/dsl/aio/writes.py +368 -0
  39. pyoq/dsl/context.py +326 -0
  40. pyoq/dsl/entry.py +37 -0
  41. pyoq/dsl/labels.py +36 -0
  42. pyoq/dsl/queries.py +339 -0
  43. pyoq/dsl/result.py +164 -0
  44. pyoq/dsl/writes.py +360 -0
  45. pyoq/errors.py +317 -0
  46. pyoq/fastapi/__init__.py +32 -0
  47. pyoq/fastapi/dependencies.py +167 -0
  48. pyoq/fastapi/lifespan.py +119 -0
  49. pyoq/fetching/__init__.py +55 -0
  50. pyoq/fetching/collections.py +136 -0
  51. pyoq/fetching/execution.py +587 -0
  52. pyoq/fetching/joined.py +79 -0
  53. pyoq/fetching/nesting.py +183 -0
  54. pyoq/fetching/plans.py +541 -0
  55. pyoq/fetching/select_in.py +149 -0
  56. pyoq/fetching/tables.py +110 -0
  57. pyoq/generation/__init__.py +54 -0
  58. pyoq/generation/cleanup.py +44 -0
  59. pyoq/generation/contracts.py +248 -0
  60. pyoq/generation/drift.py +169 -0
  61. pyoq/generation/lock.py +33 -0
  62. pyoq/generation/manifest.py +114 -0
  63. pyoq/generation/model.py +1001 -0
  64. pyoq/generation/pipeline.py +119 -0
  65. pyoq/generation/rendering/__init__.py +5 -0
  66. pyoq/generation/rendering/domains.py +51 -0
  67. pyoq/generation/rendering/enums.py +29 -0
  68. pyoq/generation/rendering/exports.py +70 -0
  69. pyoq/generation/rendering/imports.py +63 -0
  70. pyoq/generation/rendering/package.py +56 -0
  71. pyoq/generation/rendering/relations.py +133 -0
  72. pyoq/generation/rendering/routines.py +396 -0
  73. pyoq/generation/rendering/rows.py +79 -0
  74. pyoq/generation/rendering/source.py +121 -0
  75. pyoq/generation/rendering/tables.py +300 -0
  76. pyoq/generation/rendering/writes.py +514 -0
  77. pyoq/generation/validation.py +27 -0
  78. pyoq/generation/writer.py +184 -0
  79. pyoq/hydration/__init__.py +24 -0
  80. pyoq/hydration/engine.py +155 -0
  81. pyoq/hydration/identity.py +194 -0
  82. pyoq/hydration/plan.py +116 -0
  83. pyoq/migrations/__init__.py +9 -0
  84. pyoq/migrations/alembic.py +106 -0
  85. pyoq/migrations/hooks.py +75 -0
  86. pyoq/naming.py +261 -0
  87. pyoq/policies/__init__.py +47 -0
  88. pyoq/policies/bypass.py +122 -0
  89. pyoq/policies/governed.py +430 -0
  90. pyoq/policies/model.py +242 -0
  91. pyoq/policies/rewriting.py +263 -0
  92. pyoq/py.typed +1 -0
  93. pyoq/query/__init__.py +312 -0
  94. pyoq/query/aggregates.py +172 -0
  95. pyoq/query/arrays.py +65 -0
  96. pyoq/query/binding.py +52 -0
  97. pyoq/query/capabilities.py +317 -0
  98. pyoq/query/casts.py +73 -0
  99. pyoq/query/choices.py +185 -0
  100. pyoq/query/decoding.py +360 -0
  101. pyoq/query/documents.py +56 -0
  102. pyoq/query/execution/__init__.py +63 -0
  103. pyoq/query/execution/aio/__init__.py +31 -0
  104. pyoq/query/execution/aio/operations.py +228 -0
  105. pyoq/query/execution/aio/pooling.py +233 -0
  106. pyoq/query/execution/aio/streaming.py +161 -0
  107. pyoq/query/execution/aio/transactions.py +105 -0
  108. pyoq/query/execution/batch.py +96 -0
  109. pyoq/query/execution/binding_style.py +30 -0
  110. pyoq/query/execution/compilation.py +48 -0
  111. pyoq/query/execution/context.py +61 -0
  112. pyoq/query/execution/control.py +50 -0
  113. pyoq/query/execution/operations.py +224 -0
  114. pyoq/query/execution/planning.py +107 -0
  115. pyoq/query/execution/pooling.py +279 -0
  116. pyoq/query/execution/results.py +36 -0
  117. pyoq/query/execution/streaming.py +178 -0
  118. pyoq/query/execution/transactions.py +95 -0
  119. pyoq/query/expressions.py +1200 -0
  120. pyoq/query/fields.py +60 -0
  121. pyoq/query/mysql/__init__.py +59 -0
  122. pyoq/query/mysql/aio/__init__.py +38 -0
  123. pyoq/query/mysql/aio/commands.py +389 -0
  124. pyoq/query/mysql/aio/driver.py +196 -0
  125. pyoq/query/mysql/aio/executor.py +123 -0
  126. pyoq/query/mysql/aio/factory.py +26 -0
  127. pyoq/query/mysql/aio/operations.py +38 -0
  128. pyoq/query/mysql/aio/pool.py +53 -0
  129. pyoq/query/mysql/aio/transactions.py +313 -0
  130. pyoq/query/mysql/commands.py +354 -0
  131. pyoq/query/mysql/compiler.py +134 -0
  132. pyoq/query/mysql/context.py +20 -0
  133. pyoq/query/mysql/executor.py +126 -0
  134. pyoq/query/mysql/expressions.py +244 -0
  135. pyoq/query/mysql/factory.py +46 -0
  136. pyoq/query/mysql/health.py +66 -0
  137. pyoq/query/mysql/identifiers.py +9 -0
  138. pyoq/query/mysql/model.py +79 -0
  139. pyoq/query/mysql/operations.py +43 -0
  140. pyoq/query/mysql/parameters.py +69 -0
  141. pyoq/query/mysql/planning.py +20 -0
  142. pyoq/query/mysql/pool.py +67 -0
  143. pyoq/query/mysql/transactions.py +331 -0
  144. pyoq/query/mysql/writes.py +73 -0
  145. pyoq/query/nodes.py +750 -0
  146. pyoq/query/postgres/__init__.py +48 -0
  147. pyoq/query/postgres/aio/__init__.py +25 -0
  148. pyoq/query/postgres/aio/bulk.py +56 -0
  149. pyoq/query/postgres/aio/commands.py +264 -0
  150. pyoq/query/postgres/aio/executor.py +152 -0
  151. pyoq/query/postgres/aio/factory.py +26 -0
  152. pyoq/query/postgres/aio/operations.py +26 -0
  153. pyoq/query/postgres/aio/pool.py +40 -0
  154. pyoq/query/postgres/aio/transactions.py +295 -0
  155. pyoq/query/postgres/bulk.py +62 -0
  156. pyoq/query/postgres/commands.py +238 -0
  157. pyoq/query/postgres/compiler.py +114 -0
  158. pyoq/query/postgres/context.py +20 -0
  159. pyoq/query/postgres/executor.py +147 -0
  160. pyoq/query/postgres/expressions.py +311 -0
  161. pyoq/query/postgres/factory.py +24 -0
  162. pyoq/query/postgres/health.py +24 -0
  163. pyoq/query/postgres/identifiers.py +9 -0
  164. pyoq/query/postgres/model.py +81 -0
  165. pyoq/query/postgres/operations.py +25 -0
  166. pyoq/query/postgres/parameters.py +71 -0
  167. pyoq/query/postgres/planning.py +20 -0
  168. pyoq/query/postgres/pool.py +52 -0
  169. pyoq/query/postgres/transactions.py +295 -0
  170. pyoq/query/postgres/writes.py +37 -0
  171. pyoq/query/projections.py +105 -0
  172. pyoq/query/raw.py +90 -0
  173. pyoq/query/recursion.py +265 -0
  174. pyoq/query/rendering/__init__.py +1 -0
  175. pyoq/query/rendering/expressions.py +913 -0
  176. pyoq/query/rendering/identifiers.py +40 -0
  177. pyoq/query/rendering/projections.py +63 -0
  178. pyoq/query/rendering/queries.py +334 -0
  179. pyoq/query/rendering/sources.py +66 -0
  180. pyoq/query/rendering/writes.py +176 -0
  181. pyoq/query/results.py +459 -0
  182. pyoq/query/routines.py +196 -0
  183. pyoq/query/rows.py +156 -0
  184. pyoq/query/select.py +793 -0
  185. pyoq/query/select_nodes.py +277 -0
  186. pyoq/query/sources.py +236 -0
  187. pyoq/query/sqlite/__init__.py +43 -0
  188. pyoq/query/sqlite/commands.py +201 -0
  189. pyoq/query/sqlite/compiler.py +139 -0
  190. pyoq/query/sqlite/context.py +20 -0
  191. pyoq/query/sqlite/executor.py +119 -0
  192. pyoq/query/sqlite/expressions.py +224 -0
  193. pyoq/query/sqlite/factory.py +32 -0
  194. pyoq/query/sqlite/health.py +28 -0
  195. pyoq/query/sqlite/identifiers.py +9 -0
  196. pyoq/query/sqlite/model.py +73 -0
  197. pyoq/query/sqlite/operations.py +36 -0
  198. pyoq/query/sqlite/parameters.py +50 -0
  199. pyoq/query/sqlite/planning.py +20 -0
  200. pyoq/query/sqlite/pool.py +50 -0
  201. pyoq/query/sqlite/streaming.py +13 -0
  202. pyoq/query/sqlite/transactions.py +274 -0
  203. pyoq/query/sqlite/writes.py +35 -0
  204. pyoq/query/statements.py +27 -0
  205. pyoq/query/values.py +23 -0
  206. pyoq/query/vendor.py +162 -0
  207. pyoq/query/windows.py +424 -0
  208. pyoq/query/write_nodes.py +174 -0
  209. pyoq/query/writes.py +628 -0
  210. pyoq/relations/__init__.py +66 -0
  211. pyoq/relations/batching.py +219 -0
  212. pyoq/relations/derivation.py +111 -0
  213. pyoq/relations/fetching.py +355 -0
  214. pyoq/relations/graph.py +245 -0
  215. pyoq/relations/loading.py +74 -0
  216. pyoq/relations/model.py +75 -0
  217. pyoq/relations/planning.py +206 -0
  218. pyoq/runtime/__init__.py +9 -0
  219. pyoq/runtime/kernels.py +25 -0
  220. pyoq/runtime/python.py +43 -0
  221. pyoq/runtime/selection.py +73 -0
  222. pyoq/sanic/__init__.py +32 -0
  223. pyoq/sanic/scope.py +197 -0
  224. pyoq/sanic/workers.py +129 -0
  225. pyoq/schema/__init__.py +108 -0
  226. pyoq/schema/codec.py +711 -0
  227. pyoq/schema/models.py +604 -0
  228. pyoq/schema/mysql/__init__.py +16 -0
  229. pyoq/schema/mysql/connection.py +73 -0
  230. pyoq/schema/mysql/dsn.py +72 -0
  231. pyoq/schema/mysql/records.py +354 -0
  232. pyoq/schema/mysql/reflection.py +309 -0
  233. pyoq/schema/mysql/source.py +30 -0
  234. pyoq/schema/mysql/sql.py +128 -0
  235. pyoq/schema/mysql/types.py +105 -0
  236. pyoq/schema/postgres/__init__.py +13 -0
  237. pyoq/schema/postgres/connection.py +63 -0
  238. pyoq/schema/postgres/records.py +384 -0
  239. pyoq/schema/postgres/reflection.py +466 -0
  240. pyoq/schema/postgres/source.py +30 -0
  241. pyoq/schema/postgres/sql.py +246 -0
  242. pyoq/schema/postgres/types.py +98 -0
  243. pyoq/schema/registry.py +45 -0
  244. pyoq/schema/source.py +15 -0
  245. pyoq/schema/sqlite/__init__.py +6 -0
  246. pyoq/schema/sqlite/connection.py +54 -0
  247. pyoq/schema/sqlite/records.py +167 -0
  248. pyoq/schema/sqlite/reflection.py +393 -0
  249. pyoq/schema/sqlite/source.py +30 -0
  250. pyoq/schema/sqlite/sql.py +254 -0
  251. pyoq/schema/sqlite/types.py +74 -0
  252. pyoq/serving/__init__.py +23 -0
  253. pyoq/serving/databases.py +107 -0
  254. pyoq/serving/opening.py +331 -0
  255. pyoq/snapshots/__init__.py +20 -0
  256. pyoq/snapshots/drift.py +312 -0
  257. pyoq/snapshots/files.py +96 -0
  258. pyoq/snapshots/routing.py +40 -0
  259. pyoq/snapshots/source.py +33 -0
  260. pyoq/tracing/__init__.py +5 -0
  261. pyoq/tracing/spans.py +89 -0
  262. pyoq/unset.py +14 -0
  263. pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
  264. pyoq_sql-1.0.2.dist-info/RECORD +267 -0
  265. pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
  266. pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
  267. pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
@@ -0,0 +1,913 @@
1
+ """Shared scalar expression rendering with dialect hooks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from abc import ABC, abstractmethod
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass
9
+ from typing import Generic, TypeAlias, TypeVar
10
+
11
+ from pyoq.config import DatabaseDialect
12
+ from pyoq.errors import CompilationError, UnsupportedQueryError
13
+ from pyoq.query.casts import kind_for_target
14
+ from pyoq.query.execution import CompilationContext
15
+ from pyoq.query.nodes import (
16
+ AggregateName,
17
+ AggregateNode,
18
+ ArrayConstructNode,
19
+ ArrayDimensionsNode,
20
+ ArrayElementNode,
21
+ ArrayLengthNode,
22
+ ArrayNode,
23
+ BinaryNode,
24
+ BinaryOperator,
25
+ BoundValueNode,
26
+ CaseNode,
27
+ CastNode,
28
+ CollectionJoinNode,
29
+ CollectionNode,
30
+ ExistsNode,
31
+ ExpressionNode,
32
+ ExtractNode,
33
+ FieldNode,
34
+ FrameBound,
35
+ FrameBoundNode,
36
+ FrameExclusion,
37
+ FrameKind,
38
+ FrameNode,
39
+ FunctionName,
40
+ FunctionNode,
41
+ JsonBuild,
42
+ JsonBuildNode,
43
+ JsonContainsNode,
44
+ JsonKeysNode,
45
+ JsonNode,
46
+ JsonWriteNode,
47
+ RawNode,
48
+ RowValueNode,
49
+ ScalarFamily,
50
+ UnaryNode,
51
+ UnaryOperator,
52
+ VariadicNode,
53
+ VariadicOperator,
54
+ VendorFunctionNode,
55
+ WindowFunctionNode,
56
+ WindowNode,
57
+ WindowReferenceNode,
58
+ WindowSpecificationNode,
59
+ )
60
+ from pyoq.query.rendering.identifiers import IdentifierQuoting
61
+ from pyoq.query.rendering.sources import OrderTextRenderer, QueryTextRenderer
62
+ from pyoq.query.select_nodes import JsonElementsSourceNode, OrderNode
63
+ from pyoq.query.vendor import PLAIN_NAME
64
+ from pyoq.schema.models import TypeKind
65
+
66
+ ContextType = TypeVar("ContextType", bound=CompilationContext)
67
+
68
+ _UNSAFE_RAW_TEXT = re.compile(r"\?|;|--|/\*|\*/|(?<![A-Za-z0-9_])[:@$][A-Za-z_]")
69
+
70
+ _UNARY_SUFFIX = {
71
+ UnaryOperator.IS_NULL: "IS NULL",
72
+ UnaryOperator.IS_NOT_NULL: "IS NOT NULL",
73
+ UnaryOperator.IS_TRUE: "IS TRUE",
74
+ UnaryOperator.IS_FALSE: "IS FALSE",
75
+ }
76
+
77
+ _STANDARD_BINARY_SQL = {
78
+ BinaryOperator.EQUAL: "=",
79
+ BinaryOperator.NOT_EQUAL: "<>",
80
+ BinaryOperator.LESS_THAN: "<",
81
+ BinaryOperator.LESS_OR_EQUAL: "<=",
82
+ BinaryOperator.GREATER_THAN: ">",
83
+ BinaryOperator.GREATER_OR_EQUAL: ">=",
84
+ BinaryOperator.ADD: "+",
85
+ BinaryOperator.SUBTRACT: "-",
86
+ BinaryOperator.MULTIPLY: "*",
87
+ BinaryOperator.DIVIDE: "/",
88
+ BinaryOperator.MODULO: "%",
89
+ BinaryOperator.CONCAT: "||",
90
+ BinaryOperator.LIKE: "LIKE",
91
+ BinaryOperator.NOT_LIKE: "NOT LIKE",
92
+ }
93
+
94
+ _AGGREGATE_SQL = {
95
+ AggregateName.COUNT: "COUNT",
96
+ AggregateName.SUM: "SUM",
97
+ AggregateName.AVERAGE: "AVG",
98
+ AggregateName.MINIMUM: "MIN",
99
+ AggregateName.MAXIMUM: "MAX",
100
+ }
101
+
102
+ _FRAME_KINDS: Mapping[FrameKind, str] = {
103
+ FrameKind.ROWS: "ROWS",
104
+ FrameKind.RANGE: "RANGE",
105
+ FrameKind.GROUPS: "GROUPS",
106
+ }
107
+
108
+ _FRAME_BOUNDS: Mapping[FrameBound, str] = {
109
+ FrameBound.UNBOUNDED_PRECEDING: "UNBOUNDED PRECEDING",
110
+ FrameBound.PRECEDING: "PRECEDING",
111
+ FrameBound.CURRENT_ROW: "CURRENT ROW",
112
+ FrameBound.FOLLOWING: "FOLLOWING",
113
+ FrameBound.UNBOUNDED_FOLLOWING: "UNBOUNDED FOLLOWING",
114
+ }
115
+
116
+ _FRAME_EXCLUSIONS: Mapping[FrameExclusion, str] = {
117
+ FrameExclusion.CURRENT_ROW: "CURRENT ROW",
118
+ FrameExclusion.GROUP: "GROUP",
119
+ FrameExclusion.TIES: "TIES",
120
+ FrameExclusion.NO_OTHERS: "NO OTHERS",
121
+ }
122
+
123
+ _STANDARD_CAST_SQL: Mapping[TypeKind, str] = {
124
+ TypeKind.BOOLEAN: "BOOLEAN",
125
+ TypeKind.SMALL_INTEGER: "SMALLINT",
126
+ TypeKind.INTEGER: "INTEGER",
127
+ TypeKind.BIG_INTEGER: "BIGINT",
128
+ TypeKind.DECIMAL: "NUMERIC",
129
+ TypeKind.REAL: "REAL",
130
+ TypeKind.DOUBLE: "DOUBLE PRECISION",
131
+ TypeKind.STRING: "TEXT",
132
+ TypeKind.BINARY: "BYTEA",
133
+ TypeKind.DATE: "DATE",
134
+ TypeKind.TIME: "TIME",
135
+ TypeKind.DATETIME: "TIMESTAMP",
136
+ TypeKind.INTERVAL: "INTERVAL",
137
+ TypeKind.UUID: "UUID",
138
+ TypeKind.JSON: "JSONB",
139
+ }
140
+ """Verified against a running PostgreSQL server, whose names these are."""
141
+
142
+ _STANDARD_CHOOSING_SQL: Mapping[FunctionName, str] = {
143
+ FunctionName.COALESCE: "COALESCE",
144
+ FunctionName.NULLIF: "NULLIF",
145
+ FunctionName.GREATEST: "GREATEST",
146
+ FunctionName.LEAST: "LEAST",
147
+ }
148
+
149
+ _CHOOSING_FUNCTIONS = frozenset(
150
+ {
151
+ FunctionName.COALESCE,
152
+ FunctionName.NULLIF,
153
+ FunctionName.GREATEST,
154
+ FunctionName.LEAST,
155
+ }
156
+ )
157
+ """Functions that take a list and answer with one of it."""
158
+
159
+ _SCALAR_TRANSFORMS = frozenset(
160
+ {FunctionName.LOWER, FunctionName.UPPER, FunctionName.LENGTH}
161
+ )
162
+
163
+
164
+ @dataclass(frozen=True, slots=True)
165
+ class CollectionFunctions:
166
+ """How one dialect gathers rows into a single value, and builds one."""
167
+
168
+ array_function: str
169
+ object_function: str
170
+ empty_array: str
171
+ array_constructor: str
172
+ object_aggregate: str
173
+ ordered: bool = True
174
+
175
+
176
+ _Call: TypeAlias = (
177
+ CaseNode
178
+ | CastNode
179
+ | WindowNode
180
+ | WindowFunctionNode
181
+ | ExistsNode
182
+ | VendorFunctionNode
183
+ | ArrayNode
184
+ | ArrayElementNode
185
+ | ArrayLengthNode
186
+ | ArrayConstructNode
187
+ | ArrayDimensionsNode
188
+ | JsonContainsNode
189
+ | JsonWriteNode
190
+ | JsonNode
191
+ | JsonBuildNode
192
+ | JsonKeysNode
193
+ | RowValueNode
194
+ | FunctionNode
195
+ | ExtractNode
196
+ | AggregateNode
197
+ | CollectionNode
198
+ | RawNode
199
+ )
200
+ """Everything not a leaf and not an operator, which is everything named."""
201
+
202
+
203
+ _KEPT = "pyoq_kept"
204
+ """What a limited collection calls the rows its order chose."""
205
+
206
+
207
+ def _kept_entry(position: int) -> str:
208
+ """What one value a row carries is called inside that set."""
209
+ return f"pyoq_value_{position}"
210
+
211
+
212
+ def _kept_order(position: int) -> str:
213
+ """What one of the keys it is ordered by is called inside that set."""
214
+ return f"pyoq_order_{position}"
215
+
216
+
217
+ def _text_literal(value: str) -> str:
218
+ return "'" + value.replace("'", "''") + "'"
219
+
220
+
221
+ class ExpressionRenderer(ABC, Generic[ContextType]):
222
+ __slots__ = ("_context", "_declared_windows", "_render_order", "_render_query")
223
+ _context: ContextType
224
+ _declared_windows: frozenset[str]
225
+ _render_order: OrderTextRenderer | None
226
+ _render_query: QueryTextRenderer | None
227
+
228
+ def __init__(self, context: ContextType) -> None:
229
+ self._context = context
230
+ self._render_order = None
231
+ self._render_query = None
232
+ self._declared_windows = frozenset()
233
+
234
+ def declares_windows(self, names: frozenset[str], /) -> frozenset[str]:
235
+ """The windows named by the query being written, and the ones before it.
236
+
237
+ A subquery declares its own, so the caller puts back what it replaced
238
+ once that query has been written.
239
+ """
240
+ previous = self._declared_windows
241
+ self._declared_windows = names
242
+ return previous
243
+
244
+ def orders_rows_with(self, render_order: OrderTextRenderer, /) -> None:
245
+ """How to write one term of an ORDER BY, direction and nulls included.
246
+
247
+ A window orders rows the way a query does, and only one place knows
248
+ what a dialect accepts there, so it hands the answer back rather than
249
+ being reached for.
250
+ """
251
+ self._render_order = render_order
252
+
253
+ def reads_queries_with(self, render_query: QueryTextRenderer, /) -> None:
254
+ """How to write a query that appears inside an expression.
255
+
256
+ Only the query renderer knows how, and it is built from this one, so
257
+ it hands the answer back rather than being reached for.
258
+ """
259
+ self._render_query = render_query
260
+
261
+ @property
262
+ @abstractmethod
263
+ def dialect(self) -> str: ...
264
+
265
+ @abstractmethod
266
+ def qualified_field(self, node: FieldNode) -> str: ...
267
+
268
+ @abstractmethod
269
+ def render_extract(self, node: ExtractNode) -> str: ...
270
+
271
+ @abstractmethod
272
+ def render_string_predicate(self, node: FunctionNode) -> str: ...
273
+
274
+ @abstractmethod
275
+ def require_raw_expressions(self) -> None: ...
276
+
277
+ @property
278
+ @abstractmethod
279
+ def collection_functions(self) -> CollectionFunctions:
280
+ """How this dialect gathers rows into one value."""
281
+
282
+ def qualified_table(self, node: CollectionNode) -> str:
283
+ """Name the table a collection reads, and the name it answers to.
284
+
285
+ A collection may read the table its parent is already reading, which
286
+ is what a relation to its own table is, so the alias is what keeps the
287
+ correlation naming two different things.
288
+ """
289
+ written = self.quoting.qualified(
290
+ node.table, schema=node.schema, catalog=node.catalog
291
+ )
292
+ if node.alias is None:
293
+ return written
294
+ return f"{written} AS {self.quoting.quote(node.alias)}"
295
+
296
+ def qualified_join(self, join: CollectionJoinNode) -> str:
297
+ """Name a table a collection reads alongside its own.
298
+
299
+ Quoted the same way any other name is, and aliased where the join says
300
+ so, because a collection may read a table it has already read.
301
+ """
302
+ written = self.quoting.qualified(
303
+ join.table, schema=join.schema, catalog=join.catalog
304
+ )
305
+ if join.alias is None:
306
+ return written
307
+ return f"{written} AS {self.quoting.quote(join.alias)}"
308
+
309
+ @abstractmethod
310
+ def validate_binary(self, node: BinaryNode) -> None: ...
311
+
312
+ def binary_operator_sql(self, operator: BinaryOperator) -> str | None:
313
+ return _STANDARD_BINARY_SQL.get(operator)
314
+
315
+ @property
316
+ def cast_type_names(self) -> Mapping[TypeKind, str]:
317
+ """What this dialect calls each type a cast can ask for."""
318
+ return _STANDARD_CAST_SQL
319
+
320
+ def cast_type_sql(self, value_type: type[object]) -> str:
321
+ """How to name a cast target, or a refusal when there is no way to.
322
+
323
+ A dialect that has no such type must say so. Emitting a name it
324
+ merely tolerates would answer with a value of the wrong type and call
325
+ it the requested one.
326
+ """
327
+ kind = kind_for_target(value_type)
328
+ name = self.cast_type_names.get(kind)
329
+ if name is None:
330
+ message = (
331
+ f"{self.dialect} cannot cast to {kind.value}, "
332
+ f"because it has no such type"
333
+ )
334
+ raise UnsupportedQueryError(message)
335
+ return name
336
+
337
+ def require_window_functions(self) -> None:
338
+ """Whether this dialect can measure a row against its neighbours."""
339
+
340
+ def _compile_window_function(self, node: WindowFunctionNode) -> str:
341
+ arguments = ", ".join(self.compile(argument) for argument in node.arguments)
342
+ return f"{node.name.value.replace('-', '_').upper()}({arguments})"
343
+
344
+ def _compile_window(self, node: WindowNode) -> str:
345
+ self.require_window_functions()
346
+ looked_through = node.window
347
+ if isinstance(looked_through, WindowReferenceNode):
348
+ self._require_declared(looked_through.name)
349
+ return f"{self.compile(node.expression)} OVER {looked_through.name}"
350
+ specification = self.compile_window_specification(looked_through)
351
+ return f"{self.compile(node.expression)} OVER ({specification})"
352
+
353
+ def _require_declared(self, name: str) -> None:
354
+ if name not in self._declared_windows:
355
+ message = f"window {name} is read through but the query does not declare it"
356
+ raise CompilationError(message)
357
+
358
+ def compile_window_specification(self, node: WindowSpecificationNode) -> str:
359
+ """How a window divides and orders the rows it covers."""
360
+ clauses: list[str] = []
361
+ if node.partition_by:
362
+ partitions = ", ".join(
363
+ self.compile(expression) for expression in node.partition_by
364
+ )
365
+ clauses.append(f"PARTITION BY {partitions}")
366
+ if node.order_by:
367
+ clauses.append(f"ORDER BY {self._ordered_rows(node.order_by)}")
368
+ if node.frame is not None:
369
+ clauses.append(self._compile_frame(node.frame))
370
+ return " ".join(clauses)
371
+
372
+ def _compile_frame(self, node: FrameNode) -> str:
373
+ """The rows a window function reads, where this dialect can narrow them."""
374
+ self.require_frame(node)
375
+ start = self._frame_bound(node.start)
376
+ if node.end is None:
377
+ written = f"{_FRAME_KINDS[node.kind]} {start}"
378
+ else:
379
+ written = (
380
+ f"{_FRAME_KINDS[node.kind]} BETWEEN {start} "
381
+ f"AND {self._frame_bound(node.end)}"
382
+ )
383
+ if node.exclusion is None:
384
+ return written
385
+ return f"{written} EXCLUDE {_FRAME_EXCLUSIONS[node.exclusion]}"
386
+
387
+ def _frame_bound(self, node: FrameBoundNode) -> str:
388
+ written = _FRAME_BOUNDS[node.bound]
389
+ if node.offset is None:
390
+ return written
391
+ return f"{self.compile(node.offset)} {written}"
392
+
393
+ def require_frame(self, node: FrameNode) -> None:
394
+ """Whether this dialect can narrow a window the way it was asked to."""
395
+
396
+ def _ordered_rows(self, orders: tuple[OrderNode, ...]) -> str:
397
+ if self._render_order is None:
398
+ message = f"{self.dialect} cannot order the rows a window covers"
399
+ raise CompilationError(message)
400
+ render_order = self._render_order
401
+ return ", ".join(render_order(order) for order in orders)
402
+
403
+ def _compile_cast(self, node: CastNode) -> str:
404
+ target = self.cast_type_sql(node.value_type)
405
+ return f"CAST({self.compile(node.expression)} AS {target})"
406
+
407
+ def choosing_function_sql(self, name: FunctionName) -> str:
408
+ """What this dialect calls the function that picks one of its arguments."""
409
+ return _STANDARD_CHOOSING_SQL[name]
410
+
411
+ def compile(self, node: ExpressionNode) -> str:
412
+ """Whatever this node is, written the way this dialect writes it."""
413
+ if isinstance(node, (BoundValueNode, FieldNode)):
414
+ return self._compile_leaf(node)
415
+ if isinstance(node, (UnaryNode, BinaryNode, VariadicNode)):
416
+ return self._compile_operator(node)
417
+ return self._compile_call(node)
418
+
419
+ def _compile_leaf(self, node: BoundValueNode | FieldNode) -> str:
420
+ """A value or a column, which nothing inside has to be written first."""
421
+ if isinstance(node, BoundValueNode):
422
+ return self._context.bind(node)
423
+ return self.qualified_field(node)
424
+
425
+ def _compile_operator(
426
+ self,
427
+ node: UnaryNode | BinaryNode | VariadicNode,
428
+ ) -> str:
429
+ """The nodes written as operators between or before their operands."""
430
+ if isinstance(node, UnaryNode):
431
+ return self._compile_unary(node)
432
+ if isinstance(node, BinaryNode):
433
+ return self._compile_binary(node)
434
+ return self._compile_variadic(node)
435
+
436
+ def _compile_call(self, node: _Call) -> str:
437
+ """The nodes written as a name, a bracket, or a query of their own."""
438
+ if isinstance(node, CaseNode):
439
+ return self._compile_case(node)
440
+ if isinstance(node, CastNode):
441
+ return self._compile_cast(node)
442
+ if isinstance(node, (WindowNode, WindowFunctionNode)):
443
+ return self._compile_windowed(node)
444
+ if isinstance(node, ExistsNode):
445
+ return self._compile_exists(node)
446
+ if isinstance(node, RowValueNode):
447
+ return self._compile_row_value(node)
448
+ if isinstance(node, VendorFunctionNode):
449
+ return self._compile_vendor_function(node)
450
+ if isinstance(
451
+ node,
452
+ (
453
+ ArrayNode,
454
+ ArrayElementNode,
455
+ ArrayLengthNode,
456
+ ArrayConstructNode,
457
+ ArrayDimensionsNode,
458
+ ),
459
+ ):
460
+ return self._compile_array(node)
461
+ if isinstance(node, JsonBuildNode):
462
+ return self.render_json_build(node)
463
+ if isinstance(node, (JsonNode, JsonContainsNode, JsonWriteNode, JsonKeysNode)):
464
+ return self._compile_json(node)
465
+ if isinstance(node, FunctionNode):
466
+ return self._compile_function(node)
467
+ if isinstance(node, ExtractNode):
468
+ return self.render_extract(node)
469
+ if isinstance(node, AggregateNode):
470
+ return self._compile_aggregate(node)
471
+ if isinstance(node, CollectionNode):
472
+ return self._compile_collection(node)
473
+ return self._compile_raw(node)
474
+
475
+ @property
476
+ @abstractmethod
477
+ def quoting(self) -> IdentifierQuoting:
478
+ """How this dialect writes a name that has to be written out."""
479
+
480
+ @property
481
+ @abstractmethod
482
+ def database_dialect(self) -> DatabaseDialect:
483
+ """Which database this writes for, as the configuration names it."""
484
+
485
+ def routine_name(
486
+ self,
487
+ name: str,
488
+ schema: str | None = None,
489
+ catalog: str | None = None,
490
+ ) -> str:
491
+ """A routine's name as written, or quoted when it is not plain.
492
+
493
+ A name a database reads as written is left alone, so a call reads the
494
+ way it would be typed, and a built-in keeps the spelling the database
495
+ knows it by. Anything else is quoted, which is what makes a name with a
496
+ space usable and a name with a quote in it harmless.
497
+
498
+ Where the routine says which schema it is kept in, each part is quoted
499
+ as itself, because a schema may hold a dot and a flattened name cannot
500
+ say where one part ends.
501
+ """
502
+ if schema is None and catalog is None:
503
+ if PLAIN_NAME.match(name):
504
+ return name
505
+ return ".".join(self.quoting.quote(part) for part in name.split("."))
506
+ return self.quoting.qualified(name, schema=schema, catalog=catalog)
507
+
508
+ def _compile_vendor_function(self, node: VendorFunctionNode) -> str:
509
+ """A function this database has, refused where it has not got it.
510
+
511
+ A caller who said which databases a function exists on is told here
512
+ rather than by the server, because a query that cannot run is better
513
+ found while it is being written.
514
+ """
515
+ if node.dialects and self.database_dialect not in node.dialects:
516
+ named = ", ".join(sorted(node.dialects))
517
+ message = (
518
+ f"{node.name} was declared for {named}, and this is "
519
+ f"{self.database_dialect.value}"
520
+ )
521
+ raise UnsupportedQueryError(message)
522
+ arguments = ", ".join(self.compile(argument) for argument in node.arguments)
523
+ written = self.routine_name(node.name, node.schema, node.catalog)
524
+ return f"{written}({arguments})"
525
+
526
+ def _compile_json(
527
+ self,
528
+ node: JsonNode | JsonContainsNode | JsonWriteNode | JsonKeysNode,
529
+ ) -> str:
530
+ if isinstance(node, JsonNode):
531
+ return self.render_json(node)
532
+ if isinstance(node, JsonContainsNode):
533
+ return self.render_json_contains(node)
534
+ if isinstance(node, JsonKeysNode):
535
+ return self.render_json_keys(node)
536
+ return self.render_json_write(node)
537
+
538
+ def _compile_array(
539
+ self,
540
+ node: (
541
+ ArrayNode
542
+ | ArrayElementNode
543
+ | ArrayLengthNode
544
+ | ArrayConstructNode
545
+ | ArrayDimensionsNode
546
+ ),
547
+ ) -> str:
548
+ if isinstance(node, ArrayNode):
549
+ return self.render_array(node)
550
+ if isinstance(node, ArrayElementNode):
551
+ return self.render_array_element(node)
552
+ if isinstance(node, ArrayConstructNode):
553
+ return self.render_array_construct(node)
554
+ if isinstance(node, ArrayDimensionsNode):
555
+ return self.render_array_dimensions(node)
556
+ return self.render_array_length(node)
557
+
558
+ def render_array(self, node: ArrayNode) -> str:
559
+ """An array asked about, where this dialect has arrays."""
560
+ del node
561
+ raise UnsupportedQueryError(self._no_arrays())
562
+
563
+ def render_array_element(self, node: ArrayElementNode) -> str:
564
+ """One element of an array, where this dialect has arrays."""
565
+ del node
566
+ raise UnsupportedQueryError(self._no_arrays())
567
+
568
+ def render_array_length(self, node: ArrayLengthNode) -> str:
569
+ """How many elements an array holds, where this dialect has arrays."""
570
+ del node
571
+ raise UnsupportedQueryError(self._no_arrays())
572
+
573
+ def render_array_construct(self, node: ArrayConstructNode) -> str:
574
+ """An array built from values, where this dialect has arrays."""
575
+ del node
576
+ raise UnsupportedQueryError(self._no_arrays())
577
+
578
+ def render_array_dimensions(self, node: ArrayDimensionsNode) -> str:
579
+ """How many dimensions an array has, where this dialect has arrays."""
580
+ del node
581
+ raise UnsupportedQueryError(self._no_arrays())
582
+
583
+ def _no_arrays(self) -> str:
584
+ return (
585
+ f"{self.dialect} has no array type; hold a collection as JSON "
586
+ f"or in a table of its own"
587
+ )
588
+
589
+ @abstractmethod
590
+ def render_json_elements(self, node: JsonElementsSourceNode) -> str:
591
+ """A JSON array read as rows, which every dialect spells its own way."""
592
+
593
+ @abstractmethod
594
+ def render_json_keys(self, node: JsonKeysNode) -> str:
595
+ """The members an object has, as a JSON array of their names."""
596
+
597
+ @abstractmethod
598
+ def render_json(self, node: JsonNode) -> str:
599
+ """How this dialect reads inside a JSON value.
600
+
601
+ There is no shared spelling to fall back on: PostgreSQL takes a path
602
+ as a list of steps, while SQLite and MySQL take one written string,
603
+ and PostgreSQL reads that written string as a member name that is
604
+ simply not there. So every dialect answers this itself.
605
+ """
606
+
607
+ @abstractmethod
608
+ def render_json_write(self, node: JsonWriteNode) -> str:
609
+ """How this dialect writes a value into a JSON document."""
610
+
611
+ @abstractmethod
612
+ def render_json_contains(self, node: JsonContainsNode) -> str:
613
+ """Whether one JSON value holds another, where a dialect can ask."""
614
+
615
+ def json_path_text(self, path: tuple[str | int, ...]) -> str:
616
+ """A path as SQLite and MySQL write one, bound rather than written in."""
617
+ steps = "".join(
618
+ f"[{step}]" if isinstance(step, int) else f".{step}" for step in path
619
+ )
620
+ return self._context.bind(BoundValueNode(f"${steps}", ScalarFamily.STRING))
621
+
622
+ def _compile_row_value(self, node: RowValueNode) -> str:
623
+ values = ", ".join(self.compile(expression) for expression in node.expressions)
624
+ return f"({values})"
625
+
626
+ def _compile_windowed(self, node: WindowNode | WindowFunctionNode) -> str:
627
+ if isinstance(node, WindowNode):
628
+ return self._compile_window(node)
629
+ return self._compile_window_function(node)
630
+
631
+ def _compile_collection(self, node: CollectionNode) -> str:
632
+ """Render a correlated collection as one value beside its parent.
633
+
634
+ The empty case is coalesced rather than left null, because a relation
635
+ with no rows is an empty collection and a caller should not have to tell
636
+ the difference from a fetch that did not happen.
637
+ """
638
+ functions = self.collection_functions
639
+ self._require_collection_order(node, functions)
640
+ if node.limit is not None:
641
+ return self._compile_limited_collection(node, functions)
642
+ aggregate = (
643
+ f"{functions.array_function}"
644
+ f"({self._collection_row(node)}{self._collection_order(node)})"
645
+ )
646
+ return "".join(
647
+ (
648
+ "(SELECT COALESCE(",
649
+ aggregate,
650
+ ", ",
651
+ functions.empty_array,
652
+ ") FROM ",
653
+ self._collection_reading(node),
654
+ " WHERE ",
655
+ self.compile(node.condition),
656
+ ")",
657
+ )
658
+ )
659
+
660
+ def _compile_limited_collection(
661
+ self,
662
+ node: CollectionNode,
663
+ functions: CollectionFunctions,
664
+ ) -> str:
665
+ """A collection of only the rows its order put first.
666
+
667
+ The derived table answers with the values each row carries rather than
668
+ with the row already built, for two reasons. A join's columns are not
669
+ reachable through the one name a derived table answers under. And
670
+ SQLite loses the JSON subtype of a value that passes through one, so an
671
+ object built inside comes back quoted as text rather than as an object.
672
+ """
673
+ kept = self.quoting.quote(_KEPT)
674
+ carried = ", ".join(
675
+ (
676
+ *(
677
+ f"{self.compile(entry.expression)} AS "
678
+ f"{self.quoting.quote(_kept_entry(position))}"
679
+ for position, entry in enumerate(node.entries)
680
+ ),
681
+ *(
682
+ f"{self.compile(order.expression)} AS "
683
+ f"{self.quoting.quote(_kept_order(position))}"
684
+ for position, order in enumerate(node.orders)
685
+ ),
686
+ )
687
+ )
688
+ inner = "".join(
689
+ (
690
+ "SELECT ",
691
+ carried,
692
+ " FROM ",
693
+ self._collection_reading(node),
694
+ " WHERE ",
695
+ self.compile(node.condition),
696
+ self._collection_order(node),
697
+ f" LIMIT {node.limit:d}",
698
+ )
699
+ )
700
+ pairs = ", ".join(
701
+ f"{_text_literal(entry.key)}, "
702
+ f"{kept}.{self.quoting.quote(_kept_entry(position))}"
703
+ for position, entry in enumerate(node.entries)
704
+ )
705
+ ordering = ", ".join(
706
+ f"{kept}.{self.quoting.quote(_kept_order(position))}"
707
+ f"{' DESC' if order.descending else ''}"
708
+ for position, order in enumerate(node.orders)
709
+ )
710
+ row = f"{functions.object_function}({pairs})"
711
+ aggregate = f"{functions.array_function}({row} ORDER BY {ordering})"
712
+ return "".join(
713
+ (
714
+ "(SELECT COALESCE(",
715
+ aggregate,
716
+ ", ",
717
+ functions.empty_array,
718
+ ") FROM (",
719
+ inner,
720
+ f") AS {kept})",
721
+ )
722
+ )
723
+
724
+ def _collection_row(self, node: CollectionNode) -> str:
725
+ """One row of the collection, as the object it is gathered into."""
726
+ pairs = ", ".join(
727
+ f"{_text_literal(entry.key)}, {self.compile(entry.expression)}"
728
+ for entry in node.entries
729
+ )
730
+ return f"{self.collection_functions.object_function}({pairs})"
731
+
732
+ def _collection_reading(self, node: CollectionNode) -> str:
733
+ """The table a collection reads, and whatever it reads alongside it.
734
+
735
+ Every join is a left join. A join widens what each row of a collection
736
+ carries and does not decide which rows there are, and an inner one
737
+ would drop a row whose join matched nothing: a null foreign key, or a
738
+ row pointing at one that is not there. The row is the collection's
739
+ answer, so it stays and the value it could not reach reads as null.
740
+ """
741
+ joined = "".join(
742
+ f" LEFT JOIN {self.qualified_join(join)} ON {self.compile(join.condition)}"
743
+ for join in node.joins
744
+ )
745
+ return f"{self.qualified_table(node)}{joined}"
746
+
747
+ def _collection_order(self, node: CollectionNode) -> str:
748
+ if not node.orders:
749
+ return ""
750
+ terms = ", ".join(
751
+ f"{self.compile(order.expression)}{' DESC' if order.descending else ''}"
752
+ for order in node.orders
753
+ )
754
+ return f" ORDER BY {terms}"
755
+
756
+ def _require_collection_order(
757
+ self,
758
+ node: CollectionNode,
759
+ functions: CollectionFunctions,
760
+ ) -> None:
761
+ if not node.orders or functions.ordered:
762
+ return
763
+ message = (
764
+ f"{self.dialect} cannot order rows inside a collection; fetch this "
765
+ f"relation with select-in instead"
766
+ )
767
+ raise UnsupportedQueryError(message)
768
+
769
+ def _compile_exists(self, node: ExistsNode) -> str:
770
+ if self._render_query is None:
771
+ message = f"{self.dialect} cannot read a query inside an expression"
772
+ raise CompilationError(message)
773
+ prefix = "NOT EXISTS" if node.negated else "EXISTS"
774
+ return f"{prefix} ({self._render_query(node.query)})"
775
+
776
+ def _compile_unary(self, node: UnaryNode) -> str:
777
+ operand = self.compile(node.operand)
778
+ if node.operator is UnaryOperator.NOT:
779
+ return f"(NOT {operand})"
780
+ if node.operator is UnaryOperator.NEGATE:
781
+ return f"(-{operand})"
782
+ suffix = _UNARY_SUFFIX.get(node.operator)
783
+ if suffix is None:
784
+ message = f"unsupported {self.dialect} unary operator: {node.operator}"
785
+ raise UnsupportedQueryError(message)
786
+ return f"({operand} {suffix})"
787
+
788
+ def render_binary(self, node: BinaryNode, left: str, right: str) -> str:
789
+ operator = self.binary_operator_sql(node.operator)
790
+ if operator is None:
791
+ message = f"unsupported {self.dialect} binary operator: {node.operator}"
792
+ raise UnsupportedQueryError(message)
793
+ rendered = self._context.structural_text(operator)
794
+ return f"({left} {rendered} {right})"
795
+
796
+ def _compile_binary(self, node: BinaryNode) -> str:
797
+ self.validate_binary(node)
798
+ return self.render_binary(
799
+ node, self.compile(node.left), self.compile(node.right)
800
+ )
801
+
802
+ def _compile_variadic(self, node: VariadicNode) -> str:
803
+ if node.operator in {VariadicOperator.IN, VariadicOperator.NOT_IN}:
804
+ return self._compile_membership(node)
805
+ if node.operator in {VariadicOperator.BETWEEN, VariadicOperator.NOT_BETWEEN}:
806
+ return self._compile_range(node)
807
+ if node.operator is VariadicOperator.AND:
808
+ separator = " AND "
809
+ elif node.operator is VariadicOperator.OR:
810
+ separator = " OR "
811
+ else:
812
+ message = f"unsupported {self.dialect} variadic operator: {node.operator}"
813
+ raise UnsupportedQueryError(message)
814
+ return f"({separator.join(self.compile(item) for item in node.operands)})"
815
+
816
+ def _compile_membership(self, node: VariadicNode) -> str:
817
+ expression, *values = node.operands
818
+ if not values:
819
+ return "(1 = 1)" if node.operator is VariadicOperator.NOT_IN else "(0 = 1)"
820
+ operator = "NOT IN" if node.operator is VariadicOperator.NOT_IN else "IN"
821
+ rendered_values = ", ".join(self.compile(value) for value in values)
822
+ return f"({self.compile(expression)} {operator} ({rendered_values}))"
823
+
824
+ def _compile_range(self, node: VariadicNode) -> str:
825
+ if len(node.operands) != 3:
826
+ message = f"{self.dialect} range predicate requires exactly three operands"
827
+ raise CompilationError(message)
828
+ expression, lower, upper = node.operands
829
+ operator = (
830
+ "NOT BETWEEN"
831
+ if node.operator is VariadicOperator.NOT_BETWEEN
832
+ else "BETWEEN"
833
+ )
834
+ return (
835
+ f"({self.compile(expression)} {operator} {self.compile(lower)} "
836
+ f"AND {self.compile(upper)})"
837
+ )
838
+
839
+ def _compile_case(self, node: CaseNode) -> str:
840
+ branches = " ".join(
841
+ f"WHEN {self.compile(when)} THEN {self.compile(then)}"
842
+ for when, then in node.branches
843
+ )
844
+ if node.otherwise is None:
845
+ return f"CASE {branches} END"
846
+ return f"CASE {branches} ELSE {self.compile(node.otherwise)} END"
847
+
848
+ def _compile_function(self, node: FunctionNode) -> str:
849
+ if node.name in _CHOOSING_FUNCTIONS:
850
+ arguments = ", ".join(self.compile(argument) for argument in node.arguments)
851
+ return f"{self.choosing_function_sql(node.name)}({arguments})"
852
+ if node.name in _SCALAR_TRANSFORMS:
853
+ if len(node.arguments) != 1:
854
+ message = f"{self.dialect} scalar transform requires one argument"
855
+ raise CompilationError(message)
856
+ name = node.name.value.upper()
857
+ return f"{name}({self.compile(node.arguments[0])})"
858
+ if len(node.arguments) != 2:
859
+ message = f"{self.dialect} string predicate requires two arguments"
860
+ raise CompilationError(message)
861
+ return self.render_string_predicate(node)
862
+
863
+ def _compile_aggregate(self, node: AggregateNode) -> str:
864
+ name = _AGGREGATE_SQL.get(node.name) or self._json_aggregate_sql(node.name)
865
+ if name is None:
866
+ message = f"unsupported {self.dialect} aggregate: {node.name}"
867
+ raise UnsupportedQueryError(message)
868
+ if not node.arguments:
869
+ return f"{name}(*)"
870
+ distinct = "DISTINCT " if node.distinct else ""
871
+ arguments = ", ".join(self.compile(argument) for argument in node.arguments)
872
+ return f"{name}({distinct}{arguments})"
873
+
874
+ def _json_aggregate_sql(self, name: AggregateName) -> str | None:
875
+ """Gathering rows into JSON, which each dialect calls its own thing."""
876
+ functions = self.collection_functions
877
+ if name is AggregateName.JSON_ARRAY_AGG:
878
+ return functions.array_function
879
+ if name is AggregateName.JSON_OBJECT_AGG:
880
+ return functions.object_aggregate
881
+ return None
882
+
883
+ def render_json_build(self, node: JsonBuildNode) -> str:
884
+ """A JSON value built from the values around it, named per dialect."""
885
+ functions = self.collection_functions
886
+ if node.kind is JsonBuild.ARRAY:
887
+ built = functions.array_constructor
888
+ else:
889
+ built = functions.object_function
890
+ arguments = ", ".join(self.compile(argument) for argument in node.arguments)
891
+ return f"{built}({arguments})"
892
+
893
+ def _compile_raw(self, node: RawNode) -> str:
894
+ self.require_raw_expressions()
895
+ fragments: list[str] = []
896
+ for part in node.parts:
897
+ if isinstance(part, str):
898
+ self._validate_raw_text(part)
899
+ fragments.append(self._context.structural_text(part))
900
+ else:
901
+ fragments.append(self.compile(part))
902
+ return f"({''.join(fragments)})"
903
+
904
+ def _validate_raw_text(self, value: str) -> None:
905
+ if _UNSAFE_RAW_TEXT.search(value):
906
+ message = (
907
+ f"{self.dialect} raw text cannot contain bind markers "
908
+ "or statement syntax"
909
+ )
910
+ raise CompilationError(message)
911
+
912
+
913
+ __all__ = ("ExpressionRenderer",)