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
pyoq/query/results.py ADDED
@@ -0,0 +1,459 @@
1
+ """What an expression answers with, in one place.
2
+
3
+ Three facts, one home: the kind of value it is, the Python type it holds where
4
+ that can be known, and whether it may answer with nothing. They are derived
5
+ from the same tree by the same walk, so nothing holds two answers to one of
6
+ them, and nothing above this decides any of them again.
7
+
8
+ A family says what kind of value an expression is. This says the rest: which
9
+ Python type it is where that can be known, and whether it may answer with no
10
+ value at all. All three are derived from the same tree, so there is one answer
11
+ to what an expression results in rather than one per layer that wants to know.
12
+
13
+ Without it a caller is promised a Decimal by the type checker and handed a
14
+ float by the driver, which is the promise the decoder exists to keep.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Mapping
20
+ from dataclasses import dataclass
21
+ from datetime import date, time, timedelta
22
+ from decimal import Decimal
23
+ from typing import TypeAlias, get_args, get_origin
24
+
25
+ from pyoq.query.nodes import (
26
+ AggregateName,
27
+ AggregateNode,
28
+ ArrayConstructNode,
29
+ ArrayDimensionsNode,
30
+ ArrayElementNode,
31
+ ArrayLengthNode,
32
+ ArrayNode,
33
+ ArrayOperation,
34
+ BinaryNode,
35
+ BinaryOperator,
36
+ BoundValueNode,
37
+ CaseNode,
38
+ CastNode,
39
+ CollectionNode,
40
+ ExistsNode,
41
+ ExpressionNode,
42
+ ExtractNode,
43
+ FieldNode,
44
+ FunctionName,
45
+ FunctionNode,
46
+ JsonBuildNode,
47
+ JsonContainsNode,
48
+ JsonKeysNode,
49
+ JsonNode,
50
+ JsonOperation,
51
+ JsonWriteNode,
52
+ RawNode,
53
+ RowValueNode,
54
+ ScalarFamily,
55
+ UnaryNode,
56
+ UnaryOperator,
57
+ VariadicNode,
58
+ VendorFunctionNode,
59
+ WindowFunctionNode,
60
+ WindowName,
61
+ WindowNode,
62
+ )
63
+ from pyoq.schema.models import TypeKind
64
+
65
+ _NUMERIC_KINDS = frozenset(
66
+ {
67
+ TypeKind.BIG_INTEGER,
68
+ TypeKind.DECIMAL,
69
+ TypeKind.DOUBLE,
70
+ TypeKind.INTEGER,
71
+ TypeKind.REAL,
72
+ TypeKind.SMALL_INTEGER,
73
+ }
74
+ )
75
+ _TEMPORAL_KINDS = frozenset(
76
+ {TypeKind.DATE, TypeKind.DATETIME, TypeKind.INTERVAL, TypeKind.TIME}
77
+ )
78
+
79
+
80
+ def family_for_kind(kind: TypeKind, /) -> ScalarFamily:
81
+ """What kind of value a column declared with this SQL type holds.
82
+
83
+ The companion to `family_for_type`: one reads a Python type and this reads
84
+ a schema's own. Both answer the same question, so they live together and a
85
+ kind gains a family in one place rather than in each layer that asks.
86
+ """
87
+ if kind is TypeKind.BOOLEAN:
88
+ return ScalarFamily.BOOLEAN
89
+ if kind in _NUMERIC_KINDS:
90
+ return ScalarFamily.NUMERIC
91
+ if kind in {TypeKind.STRING, TypeKind.ENUM}:
92
+ return ScalarFamily.STRING
93
+ if kind is TypeKind.BINARY:
94
+ return ScalarFamily.BINARY
95
+ if kind in _TEMPORAL_KINDS:
96
+ return ScalarFamily.TEMPORAL
97
+ if kind is TypeKind.JSON:
98
+ return ScalarFamily.JSON
99
+ return ScalarFamily.OTHER
100
+
101
+
102
+ def family_for_type(value_type: type[object]) -> ScalarFamily:
103
+ """What kind of value a column of this type holds.
104
+
105
+ A column that holds a collection is written with what it holds, so the
106
+ type given here may be a parameterised one. What decides the family is
107
+ what it is a collection of, which is the type it is written from.
108
+ """
109
+ written = get_origin(value_type) or value_type
110
+ if not isinstance(written, type):
111
+ return ScalarFamily.OTHER
112
+ return _family_of(written)
113
+
114
+
115
+ def _family_of(value_type: type[object]) -> ScalarFamily:
116
+ if issubclass(value_type, bool):
117
+ return ScalarFamily.BOOLEAN
118
+ if issubclass(value_type, (int, float, Decimal)):
119
+ return ScalarFamily.NUMERIC
120
+ if issubclass(value_type, str):
121
+ return ScalarFamily.STRING
122
+ if issubclass(value_type, bytes):
123
+ return ScalarFamily.BINARY
124
+ if issubclass(value_type, (date, time, timedelta)):
125
+ return ScalarFamily.TEMPORAL
126
+ if issubclass(value_type, (list, dict)):
127
+ return ScalarFamily.JSON
128
+ return ScalarFamily.OTHER
129
+
130
+
131
+ def expression_family(node: ExpressionNode) -> ScalarFamily:
132
+ """What kind of value this node answers with."""
133
+ if isinstance(node, (BoundValueNode, FieldNode, RawNode, VendorFunctionNode)):
134
+ return node.family
135
+ if isinstance(node, UnaryNode):
136
+ if node.operator is UnaryOperator.NEGATE:
137
+ return expression_family(node.operand)
138
+ return ScalarFamily.BOOLEAN
139
+ if isinstance(node, BinaryNode):
140
+ return _binary_family(node)
141
+ if isinstance(node, VariadicNode):
142
+ return ScalarFamily.BOOLEAN
143
+ if isinstance(node, FunctionNode):
144
+ if node.name in _CHOOSING:
145
+ return expression_family(node.arguments[0])
146
+ return _function_family(node.name)
147
+ if isinstance(node, ExtractNode):
148
+ return ScalarFamily.NUMERIC
149
+ if isinstance(node, RowValueNode):
150
+ return ScalarFamily.OTHER
151
+ if isinstance(node, _Shaped):
152
+ return _shaped_family(node)
153
+ return _borrowed_family(node)
154
+
155
+
156
+ _ARRAY_ANSWERS_AN_ARRAY = frozenset(
157
+ {
158
+ ArrayOperation.APPEND,
159
+ ArrayOperation.PREPEND,
160
+ ArrayOperation.CONCAT,
161
+ ArrayOperation.REMOVE,
162
+ ArrayOperation.REPLACE,
163
+ }
164
+ )
165
+ """The ways of asking about an array that answer with another array."""
166
+
167
+
168
+ def _shaped_family(node: _Shaped) -> ScalarFamily:
169
+ """The kinds whose family their own shape decides, not what is inside."""
170
+ if isinstance(node, JsonNode):
171
+ return _JSON_FAMILIES[node.operation]
172
+ if isinstance(node, (JsonWriteNode, JsonBuildNode, JsonKeysNode)):
173
+ return ScalarFamily.JSON
174
+ if isinstance(node, JsonContainsNode):
175
+ return ScalarFamily.BOOLEAN
176
+ if isinstance(node, ArrayNode):
177
+ return (
178
+ ScalarFamily.OTHER
179
+ if node.operation in _ARRAY_ANSWERS_AN_ARRAY
180
+ else ScalarFamily.BOOLEAN
181
+ )
182
+ if isinstance(node, (ArrayLengthNode, ArrayDimensionsNode)):
183
+ return ScalarFamily.NUMERIC
184
+ if isinstance(node, ArrayConstructNode):
185
+ return ScalarFamily.OTHER
186
+ return ScalarFamily.OTHER
187
+
188
+
189
+ _JSON_AGGREGATES = frozenset(
190
+ {AggregateName.JSON_ARRAY_AGG, AggregateName.JSON_OBJECT_AGG}
191
+ )
192
+ """Aggregates that gather rows into a JSON value rather than a number."""
193
+
194
+
195
+ def _borrowed_family(node: _Borrowing) -> ScalarFamily:
196
+ """The kinds that take their family from something inside them."""
197
+ if isinstance(node, CollectionNode):
198
+ return ScalarFamily.JSON
199
+ if isinstance(node, ExistsNode):
200
+ return ScalarFamily.BOOLEAN
201
+ if isinstance(node, CaseNode):
202
+ return expression_family(node.branches[0][1])
203
+ if isinstance(node, CastNode):
204
+ return family_for_type(node.value_type)
205
+ if isinstance(node, WindowNode):
206
+ return expression_family(node.expression)
207
+ if isinstance(node, WindowFunctionNode):
208
+ return _window_family(node)
209
+ if node.name in {AggregateName.MINIMUM, AggregateName.MAXIMUM}:
210
+ return expression_family(node.arguments[0])
211
+ if node.name in _JSON_AGGREGATES:
212
+ return ScalarFamily.JSON
213
+ return ScalarFamily.NUMERIC
214
+
215
+
216
+ _Shaped: TypeAlias = (
217
+ JsonNode
218
+ | JsonWriteNode
219
+ | JsonBuildNode
220
+ | JsonKeysNode
221
+ | JsonContainsNode
222
+ | ArrayNode
223
+ | ArrayLengthNode
224
+ | ArrayElementNode
225
+ | ArrayConstructNode
226
+ | ArrayDimensionsNode
227
+ )
228
+
229
+ _Borrowing: TypeAlias = (
230
+ CollectionNode
231
+ | ExistsNode
232
+ | CaseNode
233
+ | CastNode
234
+ | WindowNode
235
+ | WindowFunctionNode
236
+ | AggregateNode
237
+ )
238
+
239
+
240
+ _JSON_FAMILIES: Mapping[JsonOperation, ScalarFamily] = {
241
+ JsonOperation.VALUE: ScalarFamily.JSON,
242
+ JsonOperation.TEXT: ScalarFamily.STRING,
243
+ JsonOperation.LENGTH: ScalarFamily.NUMERIC,
244
+ JsonOperation.EXISTS: ScalarFamily.BOOLEAN,
245
+ JsonOperation.REMOVE: ScalarFamily.JSON,
246
+ }
247
+ """What each question about a JSON value answers with."""
248
+
249
+
250
+ _NAVIGATING_WINDOWS = frozenset(
251
+ {
252
+ WindowName.LAG,
253
+ WindowName.LEAD,
254
+ WindowName.FIRST_VALUE,
255
+ WindowName.LAST_VALUE,
256
+ WindowName.NTH_VALUE,
257
+ }
258
+ )
259
+ """Windows that answer with a value already in the window."""
260
+
261
+
262
+ def _window_family(node: WindowFunctionNode) -> ScalarFamily:
263
+ """A position is a number; a neighbour's value is whatever that value is."""
264
+ if node.name in _NAVIGATING_WINDOWS:
265
+ return expression_family(node.arguments[0])
266
+ return ScalarFamily.NUMERIC
267
+
268
+
269
+ def _binary_family(node: BinaryNode) -> ScalarFamily:
270
+ boolean_operators = {
271
+ BinaryOperator.EQUAL,
272
+ BinaryOperator.NOT_EQUAL,
273
+ BinaryOperator.LESS_THAN,
274
+ BinaryOperator.LESS_OR_EQUAL,
275
+ BinaryOperator.GREATER_THAN,
276
+ BinaryOperator.GREATER_OR_EQUAL,
277
+ BinaryOperator.IS_DISTINCT_FROM,
278
+ BinaryOperator.IS_NOT_DISTINCT_FROM,
279
+ BinaryOperator.LIKE,
280
+ BinaryOperator.NOT_LIKE,
281
+ }
282
+ if node.operator in boolean_operators:
283
+ return ScalarFamily.BOOLEAN
284
+ if node.operator is BinaryOperator.CONCAT:
285
+ return ScalarFamily.STRING
286
+ families = {expression_family(node.left), expression_family(node.right)}
287
+ if ScalarFamily.TEMPORAL in families:
288
+ return ScalarFamily.TEMPORAL
289
+ return ScalarFamily.NUMERIC
290
+
291
+
292
+ _CHOOSING = frozenset(
293
+ {
294
+ FunctionName.COALESCE,
295
+ FunctionName.NULLIF,
296
+ FunctionName.GREATEST,
297
+ FunctionName.LEAST,
298
+ }
299
+ )
300
+
301
+
302
+ def _function_family(name: FunctionName) -> ScalarFamily:
303
+ if name in {FunctionName.LOWER, FunctionName.UPPER}:
304
+ return ScalarFamily.STRING
305
+ if name is FunctionName.LENGTH:
306
+ return ScalarFamily.NUMERIC
307
+ return ScalarFamily.BOOLEAN
308
+
309
+
310
+ _CARRYING_FUNCTIONS = frozenset(
311
+ {
312
+ FunctionName.COALESCE,
313
+ FunctionName.NULLIF,
314
+ FunctionName.GREATEST,
315
+ FunctionName.LEAST,
316
+ }
317
+ )
318
+ """Functions that answer with one of the values they were given."""
319
+
320
+ _CARRYING_AGGREGATES = frozenset({AggregateName.MINIMUM, AggregateName.MAXIMUM})
321
+
322
+ _CARRYING_WINDOWS = frozenset(
323
+ {
324
+ WindowName.LAG,
325
+ WindowName.LEAD,
326
+ WindowName.FIRST_VALUE,
327
+ WindowName.LAST_VALUE,
328
+ WindowName.NTH_VALUE,
329
+ }
330
+ )
331
+ """Windows that answer with a value one of their rows already held."""
332
+
333
+
334
+ def declared_type(node: ExpressionNode, /) -> type[object] | None:
335
+ """The Python type this answers with, where the tree knows one.
336
+
337
+ A node that carries a value through answers with whatever that value was
338
+ declared to be. A node that computes something new answers with nothing
339
+ here, and its family speaks for it instead.
340
+ """
341
+ if isinstance(node, (FieldNode, CastNode)):
342
+ return node.value_type
343
+ if isinstance(node, CaseNode):
344
+ return declared_type(node.branches[0][1])
345
+ if isinstance(node, WindowNode):
346
+ return declared_type(node.expression)
347
+ if isinstance(node, ArrayElementNode):
348
+ return _element_type(declared_type(node.expression))
349
+ if isinstance(node, ArrayConstructNode):
350
+ return node.element_type
351
+ if isinstance(node, VendorFunctionNode):
352
+ return node.value_type
353
+ if isinstance(node, ArrayNode) and node.operation in _ARRAY_ANSWERS_AN_ARRAY:
354
+ return declared_type(node.expression)
355
+ return _carried(node)
356
+
357
+
358
+ def _carried(node: ExpressionNode) -> type[object] | None:
359
+ """The kinds that answer with one of their own arguments."""
360
+ if isinstance(node, FunctionNode) and node.name in _CARRYING_FUNCTIONS:
361
+ return declared_type(node.arguments[0])
362
+ if isinstance(node, AggregateNode) and node.name in _CARRYING_AGGREGATES:
363
+ return declared_type(node.arguments[0])
364
+ if isinstance(node, WindowFunctionNode) and node.name in _CARRYING_WINDOWS:
365
+ return declared_type(node.arguments[0])
366
+ return None
367
+
368
+
369
+ def _element_type(array_type: type[object] | None) -> type[object] | None:
370
+ """What one element of an array was declared to hold.
371
+
372
+ An array column is declared as a tuple of its element type, so the element
373
+ type is inside it. A bare tuple says nothing about what it holds.
374
+ """
375
+ if array_type is None or get_origin(array_type) is not tuple:
376
+ return None
377
+ arguments = get_args(array_type)
378
+ return arguments[0] if arguments else None
379
+
380
+
381
+ _NEVER_NULL_AGGREGATES = frozenset({AggregateName.COUNT})
382
+ """Aggregates that answer with a value even where there are no rows."""
383
+
384
+
385
+ @dataclass(frozen=True, slots=True)
386
+ class ResultDescriptor:
387
+ """Everything one expression answers with, derived from the tree once.
388
+
389
+ ``family`` is the kind of value, which every expression has. ``declared``
390
+ is the Python type where the tree knows one, and is none where an
391
+ expression computes something the tree cannot name. ``nullable`` is
392
+ whether it may answer with no value.
393
+ """
394
+
395
+ family: ScalarFamily
396
+ declared: type[object] | None
397
+ nullable: bool
398
+
399
+
400
+ def result_descriptor(node: ExpressionNode, /) -> ResultDescriptor:
401
+ """What this expression answers with, which one place decides."""
402
+ return ResultDescriptor(
403
+ expression_family(node), declared_type(node), nullable(node)
404
+ )
405
+
406
+
407
+ def nullable(node: ExpressionNode, /) -> bool:
408
+ """Whether this may answer with no value at all.
409
+
410
+ A column says so itself. Everything else says so through what it reads:
411
+ a value carried through is as nullable as the value was, and a question
412
+ answered about rows that may not be there is nullable whatever it reads.
413
+ """
414
+ if isinstance(node, FieldNode):
415
+ return node.nullable
416
+ if isinstance(node, BoundValueNode):
417
+ return node.value is None
418
+ if isinstance(node, (ExistsNode, RowValueNode)):
419
+ return False
420
+ if isinstance(node, AggregateNode):
421
+ return node.name not in _NEVER_NULL_AGGREGATES
422
+ if isinstance(node, WindowFunctionNode):
423
+ return node.name in _CARRYING_WINDOWS
424
+ return _nullable_through(node)
425
+
426
+
427
+ def _nullable_through(node: ExpressionNode) -> bool:
428
+ """The kinds that are nullable because what they read is."""
429
+ if isinstance(node, (CastNode, WindowNode)):
430
+ return nullable(node.expression)
431
+ if isinstance(node, UnaryNode):
432
+ return nullable(node.operand)
433
+ if isinstance(node, BinaryNode):
434
+ return nullable(node.left) or nullable(node.right)
435
+ if isinstance(node, CaseNode):
436
+ return _nullable_case(node)
437
+ if isinstance(node, FunctionNode) and node.name is FunctionName.COALESCE:
438
+ return nullable(node.arguments[-1])
439
+ return True
440
+
441
+
442
+ def _nullable_case(node: CaseNode) -> bool:
443
+ """A case answers with null where no branch holds and none was given."""
444
+ if node.otherwise is None:
445
+ return True
446
+ return any(nullable(result) for _, result in node.branches) or nullable(
447
+ node.otherwise
448
+ )
449
+
450
+
451
+ __all__ = (
452
+ "ResultDescriptor",
453
+ "declared_type",
454
+ "expression_family",
455
+ "family_for_kind",
456
+ "family_for_type",
457
+ "nullable",
458
+ "result_descriptor",
459
+ )
pyoq/query/routines.py ADDED
@@ -0,0 +1,196 @@
1
+ """Running a stored procedure.
2
+
3
+ A procedure is invoked rather than selected from, and it may answer with rows
4
+ or with nothing at all, so it is a statement rather than an expression. A
5
+ stored function is a function and is declared with `vendor_function` instead.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import replace
11
+ from typing import TYPE_CHECKING, Generic, Literal, TypeVar, cast, overload
12
+
13
+ from pyoq.errors import QueryValidationError
14
+ from pyoq.query.expressions import ComputedExpression, Expression
15
+ from pyoq.query.nodes import FieldNode
16
+ from pyoq.query.results import family_for_type
17
+ from pyoq.query.select_nodes import ProjectionNode
18
+ from pyoq.query.values import bind
19
+ from pyoq.query.vendor import checked_routine_name, checked_routine_qualifier
20
+ from pyoq.query.write_nodes import CallNode
21
+
22
+ if TYPE_CHECKING:
23
+ from pyoq.query.nodes import ExpressionNode
24
+ from pyoq.query.raw import NodeProvider
25
+
26
+ ResultRow = TypeVar("ResultRow")
27
+ Value1 = TypeVar("Value1")
28
+ Value2 = TypeVar("Value2")
29
+ Value3 = TypeVar("Value3")
30
+ Value4 = TypeVar("Value4")
31
+ Value5 = TypeVar("Value5")
32
+ Value6 = TypeVar("Value6")
33
+
34
+
35
+ class Call(Generic[ResultRow]):
36
+ """A procedure asked to run, and whatever it answers with.
37
+
38
+ A procedure's rows are described by the expressions passed to returning,
39
+ so their tuple type and runtime decoder come from the same declarations.
40
+ """
41
+
42
+ __slots__ = ("_node",)
43
+
44
+ def __init__(self, node: CallNode) -> None:
45
+ self._node = node
46
+
47
+ @property
48
+ def node(self) -> CallNode:
49
+ return self._node
50
+
51
+ @overload
52
+ def returning(self, value1: Expression[Value1], /) -> Call[tuple[Value1]]: ...
53
+
54
+ @overload
55
+ def returning(
56
+ self,
57
+ value1: Expression[Value1],
58
+ value2: Expression[Value2],
59
+ /,
60
+ ) -> Call[tuple[Value1, Value2]]: ...
61
+
62
+ @overload
63
+ def returning(
64
+ self,
65
+ value1: Expression[Value1],
66
+ value2: Expression[Value2],
67
+ value3: Expression[Value3],
68
+ /,
69
+ ) -> Call[tuple[Value1, Value2, Value3]]: ...
70
+
71
+ @overload
72
+ def returning(
73
+ self,
74
+ value1: Expression[Value1],
75
+ value2: Expression[Value2],
76
+ value3: Expression[Value3],
77
+ value4: Expression[Value4],
78
+ /,
79
+ ) -> Call[tuple[Value1, Value2, Value3, Value4]]: ...
80
+
81
+ @overload
82
+ def returning(
83
+ self,
84
+ value1: Expression[Value1],
85
+ value2: Expression[Value2],
86
+ value3: Expression[Value3],
87
+ value4: Expression[Value4],
88
+ value5: Expression[Value5],
89
+ /,
90
+ ) -> Call[tuple[Value1, Value2, Value3, Value4, Value5]]: ...
91
+
92
+ @overload
93
+ def returning(
94
+ self,
95
+ value1: Expression[Value1],
96
+ value2: Expression[Value2],
97
+ value3: Expression[Value3],
98
+ value4: Expression[Value4],
99
+ value5: Expression[Value5],
100
+ value6: Expression[Value6],
101
+ /,
102
+ ) -> Call[tuple[Value1, Value2, Value3, Value4, Value5, Value6]]: ...
103
+
104
+ def returning( # type: ignore[misc]
105
+ self,
106
+ *columns: Expression[object],
107
+ ) -> Call[object]:
108
+ """Say what the rows this answers with hold.
109
+
110
+ Naming the columns supplies both the tuple type and runtime decoder.
111
+ """
112
+ if not columns:
113
+ message = "a procedure returning rows names at least one column"
114
+ raise QueryValidationError(message)
115
+ return Call[object](
116
+ replace(
117
+ self._node,
118
+ returning=tuple(ProjectionNode(column.node) for column in columns),
119
+ )
120
+ )
121
+
122
+
123
+ def argument(value: object, /) -> NodeProvider:
124
+ """A routine argument, whether it was given as an expression or a value.
125
+
126
+ Generated call surfaces take either, because a caller passing a literal
127
+ should not have to say so, and a value still travels bound.
128
+ """
129
+ if isinstance(value, Expression):
130
+ return cast("Expression[object]", value)
131
+ return bind(value)
132
+
133
+
134
+ @overload
135
+ def routine_output(
136
+ value_type: type[ResultRow],
137
+ name: str,
138
+ /,
139
+ *,
140
+ nullable: Literal[False],
141
+ ) -> ComputedExpression[ResultRow]: ...
142
+
143
+
144
+ @overload
145
+ def routine_output(
146
+ value_type: type[ResultRow],
147
+ name: str,
148
+ /,
149
+ *,
150
+ nullable: Literal[True],
151
+ ) -> ComputedExpression[ResultRow | None]: ...
152
+
153
+
154
+ def routine_output(
155
+ value_type: type[ResultRow],
156
+ name: str,
157
+ /,
158
+ *,
159
+ nullable: bool,
160
+ ) -> ComputedExpression[ResultRow] | ComputedExpression[ResultRow | None]:
161
+ """Describe a reflected routine output without losing its nullability."""
162
+ family = family_for_type(value_type)
163
+ expression: ComputedExpression[ResultRow] | ComputedExpression[ResultRow | None]
164
+ expression = ComputedExpression(
165
+ FieldNode(name, family=family, value_type=value_type, nullable=nullable),
166
+ family,
167
+ )
168
+ return expression
169
+
170
+
171
+ def call(
172
+ name: str,
173
+ /,
174
+ *arguments: NodeProvider,
175
+ schema: str | None = None,
176
+ catalog: str | None = None,
177
+ ) -> Call[tuple[object, ...]]:
178
+ """Run a stored procedure, with its arguments bound like any other value.
179
+
180
+ The name is written into the SQL rather than bound, because no database
181
+ takes a routine name as a parameter, so it is held to being a name. Saying
182
+ which schema keeps it is what makes the call reach that one rather than
183
+ whichever the search path finds first.
184
+ """
185
+ nodes: tuple[ExpressionNode, ...] = tuple(argument.node for argument in arguments)
186
+ return Call[tuple[object, ...]](
187
+ CallNode(
188
+ checked_routine_name(name),
189
+ checked_routine_qualifier(schema, "routine schema"),
190
+ checked_routine_qualifier(catalog, "routine catalog"),
191
+ nodes,
192
+ )
193
+ )
194
+
195
+
196
+ __all__ = ("Call", "argument", "call", "routine_output")