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/choices.py ADDED
@@ -0,0 +1,185 @@
1
+ """Choosing one value out of several.
2
+
3
+ Every construct here answers the same question with different rules for what
4
+ counts as an answer: the first branch whose condition holds, the first
5
+ argument that is not null, the widest, the narrowest. The type of the answer
6
+ is the type of what was offered, so a chosen value stays as usable as the
7
+ column it came from.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Generic, TypeVar, overload
13
+
14
+ from pyoq.query.expressions import (
15
+ ComputedExpression,
16
+ Expression,
17
+ family_for_value,
18
+ operand,
19
+ require_boolean,
20
+ )
21
+ from pyoq.query.nodes import CaseNode, FunctionName, FunctionNode, ScalarFamily
22
+
23
+ if TYPE_CHECKING:
24
+ from pyoq.query.nodes import ExpressionNode
25
+
26
+ Value = TypeVar("Value")
27
+
28
+
29
+ class CaseBuilder:
30
+ """A case with no branches yet, which is not a value until it has one."""
31
+
32
+ __slots__ = ()
33
+
34
+ @overload
35
+ def when(
36
+ self,
37
+ condition: Expression[bool],
38
+ result: Expression[Value],
39
+ /,
40
+ ) -> CaseBranches[Value]: ...
41
+
42
+ @overload
43
+ def when(
44
+ self,
45
+ condition: Expression[bool],
46
+ result: Value,
47
+ /,
48
+ ) -> CaseBranches[Value]: ...
49
+
50
+ def when(
51
+ self,
52
+ condition: Expression[bool],
53
+ result: Expression[Value] | Value,
54
+ /,
55
+ ) -> CaseBranches[Value]:
56
+ require_boolean(condition, "case branch")
57
+ family = _family_of(result)
58
+ return CaseBranches(((condition.node, operand(result, family)),), family)
59
+
60
+
61
+ class CaseBranches(Generic[Value]):
62
+ """Branches gathered so far, with the type the whole case will answer."""
63
+
64
+ __slots__ = ("_branches", "_family")
65
+
66
+ def __init__(
67
+ self,
68
+ branches: tuple[tuple[ExpressionNode, ExpressionNode], ...],
69
+ family: ScalarFamily,
70
+ ) -> None:
71
+ self._branches = branches
72
+ self._family = family
73
+
74
+ def when(
75
+ self,
76
+ condition: Expression[bool],
77
+ result: Expression[Value] | Value,
78
+ /,
79
+ ) -> CaseBranches[Value]:
80
+ require_boolean(condition, "case branch")
81
+ branch = (condition.node, operand(result, self._family))
82
+ return CaseBranches((*self._branches, branch), self._family)
83
+
84
+ def otherwise(
85
+ self,
86
+ result: Expression[Value] | Value,
87
+ /,
88
+ ) -> ComputedExpression[Value]:
89
+ """The value for rows no branch claimed, which makes the case total."""
90
+ node = CaseNode(self._branches, operand(result, self._family))
91
+ return ComputedExpression[Value](node, self._family)
92
+
93
+ def end(self) -> ComputedExpression[Value | None]:
94
+ """Close without an else, so unclaimed rows answer null."""
95
+ return ComputedExpression[Value | None](CaseNode(self._branches), self._family)
96
+
97
+
98
+ def case() -> CaseBuilder:
99
+ """Start a case, whose type is fixed by the result of its first branch."""
100
+ return CaseBuilder()
101
+
102
+
103
+ @overload
104
+ def coalesce(
105
+ first: Expression[Value | None],
106
+ fallback: Value,
107
+ /,
108
+ ) -> ComputedExpression[Value]: ...
109
+
110
+
111
+ @overload
112
+ def coalesce(
113
+ first: Expression[Value],
114
+ /,
115
+ *rest: Expression[Value] | Value,
116
+ ) -> ComputedExpression[Value]: ...
117
+
118
+
119
+ def coalesce(
120
+ first: Expression[Value] | Expression[Value | None],
121
+ /,
122
+ *rest: object,
123
+ ) -> ComputedExpression[Value]:
124
+ """The first argument that is not null."""
125
+ return _choosing(FunctionName.COALESCE, first, rest, least_arguments=2)
126
+
127
+
128
+ def nullif(
129
+ first: Expression[Value],
130
+ other: Expression[Value] | Value,
131
+ /,
132
+ ) -> ComputedExpression[Value | None]:
133
+ """The first argument, unless it equals the second, in which case null."""
134
+ chosen = _choosing(FunctionName.NULLIF, first, (other,), least_arguments=2)
135
+ return ComputedExpression[Value | None](chosen.node, chosen.family)
136
+
137
+
138
+ def greatest(
139
+ first: Expression[Value],
140
+ /,
141
+ *rest: Expression[Value] | Value,
142
+ ) -> ComputedExpression[Value]:
143
+ """The widest of its arguments."""
144
+ return _choosing(FunctionName.GREATEST, first, rest, least_arguments=2)
145
+
146
+
147
+ def least(
148
+ first: Expression[Value],
149
+ /,
150
+ *rest: Expression[Value] | Value,
151
+ ) -> ComputedExpression[Value]:
152
+ """The narrowest of its arguments."""
153
+ return _choosing(FunctionName.LEAST, first, rest, least_arguments=2)
154
+
155
+
156
+ def _choosing(
157
+ name: FunctionName,
158
+ first: Expression[Value] | Expression[Value | None],
159
+ rest: tuple[object, ...],
160
+ *,
161
+ least_arguments: int,
162
+ ) -> ComputedExpression[Value]:
163
+ if len(rest) + 1 < least_arguments:
164
+ message = f"{name.value} needs at least {least_arguments} arguments"
165
+ raise TypeError(message)
166
+ family = first.family
167
+ arguments = (first.node, *(operand(value, family) for value in rest))
168
+ return ComputedExpression[Value](FunctionNode(name, arguments), family)
169
+
170
+
171
+ def _family_of(value: object) -> ScalarFamily:
172
+ if isinstance(value, Expression):
173
+ return value.family
174
+ return family_for_value(value)
175
+
176
+
177
+ __all__ = (
178
+ "CaseBranches",
179
+ "CaseBuilder",
180
+ "case",
181
+ "coalesce",
182
+ "greatest",
183
+ "least",
184
+ "nullif",
185
+ )
pyoq/query/decoding.py ADDED
@@ -0,0 +1,360 @@
1
+ """Giving a column back as the type it was declared to hold.
2
+
3
+ A driver answers with what its own protocol carries, which is not always what a
4
+ column was declared to be: SQLite hands back a float for a decimal and a string
5
+ for a date. A descriptor that says `Decimal` and yields a float is a promise the
6
+ runtime does not keep, and money is the usual casualty.
7
+
8
+ The declared type travels with the column, so the row can be given back as the
9
+ row was described. A column with no declared type is left exactly as the driver
10
+ answered.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from collections.abc import Callable
17
+ from datetime import date, datetime, time, timedelta
18
+ from decimal import Decimal, InvalidOperation
19
+ from enum import Enum
20
+ from typing import TYPE_CHECKING, cast, get_origin
21
+ from uuid import UUID
22
+
23
+ from pyoq.errors import QueryExecutionError
24
+ from pyoq.query.nodes import FieldNode, ScalarFamily
25
+ from pyoq.query.results import result_descriptor
26
+ from pyoq.query.select_nodes import SelectNode, SetNode
27
+
28
+ if TYPE_CHECKING:
29
+ from collections.abc import Sequence
30
+
31
+ from pyoq.query.select_nodes import ProjectionNode
32
+
33
+ _SECONDS = "seconds"
34
+ _ONE_DAY = timedelta(days=1)
35
+ _MILLION = 1_000_000
36
+ _MINUTE = 60
37
+
38
+
39
+ def projected_types(node: object, /) -> tuple[type[object] | None, ...]:
40
+ """The type each projected column was declared to hold, in order.
41
+
42
+ A set operation takes its columns from the first query in it, which is where
43
+ a database takes their names from too.
44
+ """
45
+ projections = _projections(node)
46
+ return tuple(_declared(projection) for projection in projections)
47
+
48
+
49
+ def decode_rows(
50
+ rows: Sequence[object],
51
+ types: tuple[type[object] | None, ...],
52
+ /,
53
+ ) -> list[object]:
54
+ """Every row given back as it was described.
55
+
56
+ A statement whose columns declared nothing is handed straight back, so a
57
+ caller that never said what it wanted pays nothing for the question.
58
+ """
59
+ if not types or not any(types):
60
+ return list(rows)
61
+ return [_decode_row(row, types) for row in rows]
62
+
63
+
64
+ def decode_row(row: object, types: tuple[type[object] | None, ...], /) -> object:
65
+ """One row given back as it was described."""
66
+ if not types or not any(types):
67
+ return row
68
+ return _decode_row(row, types)
69
+
70
+
71
+ def decode_value(value: object, target: type[object] | None, /) -> object:
72
+ """One value, as the type it was declared to hold."""
73
+ if target is None or value is None or type(value) is target:
74
+ return value
75
+ decoder = _decoder(target)
76
+ if decoder is None:
77
+ return value
78
+ try:
79
+ return decoder(value)
80
+ except (ArithmeticError, TypeError, ValueError, InvalidOperation) as error:
81
+ message = (
82
+ f"a {type(value).__name__} came back for a column declared "
83
+ f"{target.__name__}, and could not be read as one"
84
+ )
85
+ raise QueryExecutionError(message) from error
86
+
87
+
88
+ def _decode_row(
89
+ row: object,
90
+ types: tuple[type[object] | None, ...],
91
+ ) -> object:
92
+ """A row of the width it was described as, decoded column by column.
93
+
94
+ Both callers above have already turned away a statement that named no
95
+ types. A statement that projected a different number of columns than the
96
+ database answered with is
97
+ refused, because a star widens with the table and reading the old columns
98
+ out of a wider row would answer with values from the wrong ones.
99
+ """
100
+ if not isinstance(row, tuple):
101
+ return row
102
+ # A driver answers with a tuple of driver values, and what those are is the
103
+ # question this is here to settle, so there is nothing narrower to say.
104
+ values = cast("tuple[object, ...]", row)
105
+ if len(values) != len(types):
106
+ message = (
107
+ f"the database answered with {len(values)} columns where the "
108
+ f"statement projected {len(types)}; a generated package that no "
109
+ f"longer matches its schema reads a row by the wrong columns, so "
110
+ f"regenerate it"
111
+ )
112
+ raise QueryExecutionError(message)
113
+ return _decoded(values, types)
114
+
115
+
116
+ def _decoded(
117
+ values: tuple[object, ...],
118
+ types: tuple[type[object] | None, ...],
119
+ ) -> tuple[object, ...]:
120
+ decoded: list[object] = []
121
+ for index, target in enumerate(types):
122
+ decoded.append(decode_value(values[index], target))
123
+ return tuple(decoded)
124
+
125
+
126
+ def _projections(node: object) -> tuple[ProjectionNode, ...]:
127
+ while isinstance(node, SetNode):
128
+ node = node.left
129
+ if isinstance(node, SelectNode):
130
+ return node.projections
131
+ returning: tuple[ProjectionNode, ...] | None = getattr(node, "returning", None)
132
+ return returning if isinstance(returning, tuple) else ()
133
+
134
+
135
+ def _declared(projection: ProjectionNode) -> type[object] | None:
136
+ """What reads this projection back, for the two that say what they hold.
137
+
138
+ A column says what it holds, and an expression that carries a value
139
+ through says whatever that value was declared to be. An expression that
140
+ computes something new says nothing, so it is read back as its family
141
+ where that family names one Python type: a predicate is a boolean whether
142
+ a driver answers with one or with a one, and text is text whether a driver
143
+ kept it as text.
144
+ """
145
+ expression = projection.expression
146
+ answers = result_descriptor(expression)
147
+ if isinstance(expression, FieldNode):
148
+ return _decodable(answers.declared, answers.family)
149
+ if answers.declared is not None and _decoder(answers.declared) is not None:
150
+ return answers.declared
151
+ return _COMPUTED_TYPES.get(answers.family)
152
+
153
+
154
+ def _decodable(
155
+ declared: type[object] | None,
156
+ family: ScalarFamily,
157
+ ) -> type[object] | None:
158
+ """What reads a value back, which its declared type may not name.
159
+
160
+ A column holding any JSON is declared as a union of everything JSON can
161
+ be, and a union is not a type a decoder can be found under. The family
162
+ says JSON on its own, so it answers where the declared type cannot.
163
+ """
164
+ if declared is not None and _decoder(declared) is not None:
165
+ return declared
166
+ return _FAMILY_TYPES.get(family)
167
+
168
+
169
+ def _decoder(target: type[object]) -> Callable[[object], object] | None:
170
+ """What reads a value back as this type, if anything does.
171
+
172
+ A column can be declared as a parameterised type, and what decodes one is
173
+ decided by what it is, not by what it holds. A generated enumeration is
174
+ its own decoder, because calling it is what turns a stored value into a
175
+ member of it.
176
+ """
177
+ origin = get_origin(target) or target
178
+ decoder = _DECODERS.get(origin)
179
+ if decoder is not None:
180
+ return decoder
181
+ if isinstance(origin, type) and issubclass(origin, Enum):
182
+ return origin
183
+ return None
184
+
185
+
186
+ def _to_decimal(value: object) -> Decimal:
187
+ if isinstance(value, float):
188
+ return Decimal(str(value))
189
+ if isinstance(value, (int, str)):
190
+ return Decimal(value)
191
+ message = f"cannot read {type(value).__name__} as a decimal"
192
+ raise TypeError(message)
193
+
194
+
195
+ def _to_datetime(value: object) -> datetime:
196
+ if isinstance(value, datetime):
197
+ return value
198
+ if isinstance(value, str):
199
+ return datetime.fromisoformat(value)
200
+ message = f"cannot read {type(value).__name__} as a datetime"
201
+ raise TypeError(message)
202
+
203
+
204
+ def _to_date(value: object) -> date:
205
+ if isinstance(value, datetime):
206
+ return value.date()
207
+ if isinstance(value, date):
208
+ return value
209
+ if isinstance(value, str):
210
+ return date.fromisoformat(value)
211
+ message = f"cannot read {type(value).__name__} as a date"
212
+ raise TypeError(message)
213
+
214
+
215
+ def _to_time(value: object) -> time:
216
+ """MySQL's `TIME` is a signed duration, and its driver answers with one.
217
+
218
+ A duration inside a single forward day names a clock time and is read as
219
+ one. Anything outside that does not, so it is refused rather than folded
220
+ into a time it does not mean.
221
+ """
222
+ if isinstance(value, time):
223
+ return value
224
+ if isinstance(value, timedelta):
225
+ return _clock_time(value)
226
+ if isinstance(value, str):
227
+ return time.fromisoformat(value)
228
+ message = f"cannot read {type(value).__name__} as a time"
229
+ raise TypeError(message)
230
+
231
+
232
+ def _clock_time(value: timedelta) -> time:
233
+ if not timedelta(0) <= value < _ONE_DAY:
234
+ message = f"{value} is not a time of day"
235
+ raise ValueError(message)
236
+ seconds, microseconds = divmod(value // timedelta(microseconds=1), _MILLION)
237
+ minutes, second = divmod(seconds, _MINUTE)
238
+ hour, minute = divmod(minutes, _MINUTE)
239
+ return time(hour, minute, second, microseconds)
240
+
241
+
242
+ def _to_timedelta(value: object) -> timedelta:
243
+ if isinstance(value, timedelta):
244
+ return value
245
+ if isinstance(value, (int, float)):
246
+ return timedelta(**{_SECONDS: value})
247
+ message = f"cannot read {type(value).__name__} as an interval"
248
+ raise TypeError(message)
249
+
250
+
251
+ def _to_uuid(value: object) -> UUID:
252
+ if isinstance(value, UUID):
253
+ return value
254
+ if isinstance(value, str):
255
+ return UUID(value)
256
+ if isinstance(value, bytes):
257
+ return UUID(bytes=value)
258
+ message = f"cannot read {type(value).__name__} as a uuid"
259
+ raise TypeError(message)
260
+
261
+
262
+ def _to_bytes(value: object) -> bytes:
263
+ if isinstance(value, (bytes, bytearray)):
264
+ return bytes(value)
265
+ if isinstance(value, memoryview):
266
+ return value.tobytes()
267
+ if isinstance(value, str):
268
+ return value.encode()
269
+ message = f"cannot read {type(value).__name__} as bytes"
270
+ raise TypeError(message)
271
+
272
+
273
+ def _to_bool(value: object) -> bool:
274
+ if isinstance(value, (bool, int)):
275
+ return bool(value)
276
+ message = f"cannot read {type(value).__name__} as a boolean"
277
+ raise TypeError(message)
278
+
279
+
280
+ def _to_int(value: object) -> int:
281
+ if isinstance(value, (int, float, str)):
282
+ return int(value)
283
+ message = f"cannot read {type(value).__name__} as an integer"
284
+ raise TypeError(message)
285
+
286
+
287
+ def _to_float(value: object) -> float:
288
+ if isinstance(value, (int, float, str, Decimal)):
289
+ return float(value)
290
+ message = f"cannot read {type(value).__name__} as a float"
291
+ raise TypeError(message)
292
+
293
+
294
+ def _to_text(value: object) -> str:
295
+ """A driver that answers with bytes for a text column says which encoding."""
296
+ if isinstance(value, (bytes, bytearray)):
297
+ return bytes(value).decode()
298
+ if isinstance(value, memoryview):
299
+ return value.tobytes().decode()
300
+ return str(value)
301
+
302
+
303
+ def _to_tuple(value: object) -> tuple[object, ...]:
304
+ """An array column is declared as a tuple, and a driver answers with a list."""
305
+ if isinstance(value, tuple):
306
+ return cast("tuple[object, ...]", value)
307
+ if isinstance(value, list):
308
+ return tuple(cast("list[object]", value))
309
+ message = f"cannot read {type(value).__name__} as an array"
310
+ raise TypeError(message)
311
+
312
+
313
+ def _to_json(value: object) -> object:
314
+ """A driver that has no JSON of its own answers with the text of it."""
315
+ if isinstance(value, (str, bytes, bytearray)):
316
+ return json.loads(value)
317
+ return value
318
+
319
+
320
+ _FAMILY_TYPES: dict[ScalarFamily, type[object]] = {ScalarFamily.JSON: dict}
321
+ """The type that reads a column back, where no declared type can name one.
322
+
323
+ A column that declared nothing is left as it came, because a caller that
324
+ never said what it wanted pays nothing for the question. Only JSON is here,
325
+ because a column holding any JSON is declared as a union no decoder is found
326
+ under.
327
+ """
328
+
329
+ _COMPUTED_TYPES: dict[ScalarFamily, type[object]] = {
330
+ ScalarFamily.BOOLEAN: bool,
331
+ ScalarFamily.STRING: str,
332
+ ScalarFamily.JSON: dict,
333
+ }
334
+ """What reads an expression back, which has no declared type to consult.
335
+
336
+ A predicate is a boolean whether a driver answers with one or with a one, and
337
+ text is text whether a driver kept it as text or handed back the number it
338
+ looked like. A numeric family names three Python types, so it is absent: what
339
+ a driver answered with is the only thing that says which.
340
+ """
341
+
342
+ _DECODERS: dict[type[object], Callable[[object], object]] = {
343
+ Decimal: _to_decimal,
344
+ datetime: _to_datetime,
345
+ date: _to_date,
346
+ time: _to_time,
347
+ timedelta: _to_timedelta,
348
+ UUID: _to_uuid,
349
+ bytes: _to_bytes,
350
+ bool: _to_bool,
351
+ int: _to_int,
352
+ float: _to_float,
353
+ str: _to_text,
354
+ list: _to_json,
355
+ dict: _to_json,
356
+ tuple: _to_tuple,
357
+ }
358
+
359
+
360
+ __all__ = ("decode_row", "decode_rows", "decode_value", "projected_types")
@@ -0,0 +1,56 @@
1
+ """Building a JSON value out of the values around it.
2
+
3
+ Reading inside a document is asked of the document, so those questions are
4
+ methods on the value itself. Building one starts from values that are not
5
+ documents yet, so it starts here.
6
+
7
+ Every dialect writes these under its own name, and each one carries the name
8
+ it uses, so a document built here is the same document on all of them.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ from pyoq.query.expressions import (
16
+ ComputedExpression,
17
+ family_for_value,
18
+ operand,
19
+ )
20
+ from pyoq.query.nodes import JsonBuild, JsonBuildNode, ScalarFamily
21
+ from pyoq.schema.models import JsonValue
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Mapping
25
+
26
+ from pyoq.query.nodes import ExpressionNode
27
+
28
+
29
+ def json_object(members: Mapping[str, object], /) -> ComputedExpression[JsonValue]:
30
+ """A JSON object with these members, in the order they are written.
31
+
32
+ A mapping rather than alternating arguments, because a member and its
33
+ value belong together and nothing else keeps them paired.
34
+ """
35
+ arguments: list[ExpressionNode] = []
36
+ for name, value in members.items():
37
+ arguments.append(operand(name, ScalarFamily.STRING))
38
+ arguments.append(_value(value))
39
+ return ComputedExpression[JsonValue](
40
+ JsonBuildNode(JsonBuild.OBJECT, tuple(arguments)), ScalarFamily.JSON
41
+ )
42
+
43
+
44
+ def json_array(*values: object) -> ComputedExpression[JsonValue]:
45
+ """A JSON array holding these values, in the order they are written."""
46
+ return ComputedExpression[JsonValue](
47
+ JsonBuildNode(JsonBuild.ARRAY, tuple(_value(value) for value in values)),
48
+ ScalarFamily.JSON,
49
+ )
50
+
51
+
52
+ def _value(value: object) -> ExpressionNode:
53
+ return operand(value, family_for_value(value))
54
+
55
+
56
+ __all__ = ("json_array", "json_object")
@@ -0,0 +1,63 @@
1
+ """Database-neutral execution results."""
2
+
3
+ from pyoq.query.execution.batch import (
4
+ BulkChunk,
5
+ BulkPlan,
6
+ BulkResult,
7
+ OperationBudget,
8
+ WriteProvider,
9
+ )
10
+ from pyoq.query.execution.binding_style import ParameterStyle
11
+ from pyoq.query.execution.compilation import CompiledQuery
12
+ from pyoq.query.execution.context import CompilationContext
13
+ from pyoq.query.execution.control import CancellationToken, ExecutionControl
14
+ from pyoq.query.execution.operations import (
15
+ DatabaseCursor,
16
+ QueryOperations,
17
+ fetch_all,
18
+ )
19
+ from pyoq.query.execution.planning import BulkPlanner
20
+ from pyoq.query.execution.pooling import (
21
+ ConnectionFactory,
22
+ ConnectionLease,
23
+ ConnectionPool,
24
+ PoolPolicy,
25
+ PoolStats,
26
+ )
27
+ from pyoq.query.execution.results import ExecuteResult, StatementKind
28
+ from pyoq.query.execution.streaming import (
29
+ CursorSession,
30
+ RowStream,
31
+ StreamPolicy,
32
+ StreamResource,
33
+ )
34
+ from pyoq.query.execution.transactions import TransactionState
35
+
36
+ __all__ = (
37
+ "BulkChunk",
38
+ "BulkPlan",
39
+ "BulkPlanner",
40
+ "BulkResult",
41
+ "CancellationToken",
42
+ "CompilationContext",
43
+ "CompiledQuery",
44
+ "ConnectionFactory",
45
+ "ConnectionLease",
46
+ "ConnectionPool",
47
+ "CursorSession",
48
+ "DatabaseCursor",
49
+ "ExecuteResult",
50
+ "ExecutionControl",
51
+ "OperationBudget",
52
+ "ParameterStyle",
53
+ "PoolPolicy",
54
+ "PoolStats",
55
+ "QueryOperations",
56
+ "RowStream",
57
+ "StatementKind",
58
+ "StreamPolicy",
59
+ "StreamResource",
60
+ "TransactionState",
61
+ "WriteProvider",
62
+ "fetch_all",
63
+ )
@@ -0,0 +1,31 @@
1
+ """Dialect-neutral asynchronous execution."""
2
+
3
+ from pyoq.query.execution.aio.operations import (
4
+ AsyncDatabaseCursor,
5
+ AsyncQueryOperations,
6
+ fetch_all,
7
+ )
8
+ from pyoq.query.execution.aio.pooling import (
9
+ AsyncConnectionFactory,
10
+ AsyncConnectionLease,
11
+ AsyncConnectionPool,
12
+ )
13
+ from pyoq.query.execution.aio.streaming import (
14
+ AsyncCursorSession,
15
+ AsyncRowStream,
16
+ AsyncStreamResource,
17
+ )
18
+ from pyoq.query.execution.aio.transactions import AsyncTransactionState
19
+
20
+ __all__ = (
21
+ "AsyncConnectionFactory",
22
+ "AsyncConnectionLease",
23
+ "AsyncConnectionPool",
24
+ "AsyncCursorSession",
25
+ "AsyncDatabaseCursor",
26
+ "AsyncQueryOperations",
27
+ "AsyncRowStream",
28
+ "AsyncStreamResource",
29
+ "AsyncTransactionState",
30
+ "fetch_all",
31
+ )