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,335 @@
1
+ """The asynchronous counterpart of a query bound to its database.
2
+
3
+ Only the ending differs. A chain is built the same way and describes the same
4
+ statement; the fetch at the end is awaited because the database is.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Callable
10
+ from typing import TYPE_CHECKING, Generic, TypeVar
11
+
12
+ from pyoq.dsl.labels import labels_of
13
+ from pyoq.dsl.result import Result, Row
14
+ from pyoq.query.select import JoinStep, Query, SelectQuery, SemiJoinStep
15
+ from pyoq.query.select_nodes import SelectNode, SetNode
16
+
17
+ if TYPE_CHECKING:
18
+ from pyoq.descriptors import RelationshipDescriptor
19
+ from pyoq.query.execution.aio import AsyncQueryOperations
20
+ from pyoq.query.expressions import Expression
21
+ from pyoq.query.sources import CommonTable, SourceBase, TableLike
22
+ from pyoq.query.writes import ColumnReference
23
+
24
+ ResultRow = TypeVar("ResultRow", bound=tuple[object, ...])
25
+ Mapped = TypeVar("Mapped")
26
+ ScalarValue = TypeVar("ScalarValue")
27
+
28
+
29
+ class AsyncBoundQuery(Generic[ResultRow]):
30
+ """A query and the database it will run against."""
31
+
32
+ __slots__ = ("_operations", "_query")
33
+ _operations: AsyncQueryOperations
34
+ _query: Query[ResultRow]
35
+
36
+ def __init__(
37
+ self,
38
+ operations: AsyncQueryOperations,
39
+ query: Query[ResultRow],
40
+ ) -> None:
41
+ object.__setattr__(self, "_operations", operations)
42
+ object.__setattr__(self, "_query", query)
43
+
44
+ def __setattr__(self, name: str, value: object) -> None:
45
+ message = f"{type(self).__name__} is immutable"
46
+ raise AttributeError(message, name, value)
47
+
48
+ @property
49
+ def query(self) -> Query[ResultRow]:
50
+ """The query on its own, which cannot reach a database."""
51
+ return self._query
52
+
53
+ def order_by(self, *terms: object) -> AsyncBoundQuery[ResultRow]:
54
+ return self._with(self._query.order_by(*terms))
55
+
56
+ def limit(self, value: int, /) -> AsyncBoundQuery[ResultRow]:
57
+ return self._with(self._query.limit(value))
58
+
59
+ def offset(self, value: int, /) -> AsyncBoundQuery[ResultRow]:
60
+ return self._with(self._query.offset(value))
61
+
62
+ def union(self, other: Query[ResultRow], /) -> AsyncBoundQuery[ResultRow]:
63
+ return self._with(self._query.union(other))
64
+
65
+ def union_all(self, other: Query[ResultRow], /) -> AsyncBoundQuery[ResultRow]:
66
+ return self._with(self._query.union_all(other))
67
+
68
+ def intersect(self, other: Query[ResultRow], /) -> AsyncBoundQuery[ResultRow]:
69
+ return self._with(self._query.intersect(other))
70
+
71
+ def except_(self, other: Query[ResultRow], /) -> AsyncBoundQuery[ResultRow]:
72
+ return self._with(self._query.except_(other))
73
+
74
+ async def fetch_all(self) -> Result[ResultRow]:
75
+ rows = tuple(await self._operations.many(self._query))
76
+ return Result(rows, _labels(self._query))
77
+
78
+ async def fetch_one(self) -> Row[ResultRow]:
79
+ """Exactly one row, because that is what the caller said to expect."""
80
+ return Row(await self._operations.one(self._query), _labels(self._query))
81
+
82
+ async def fetch_optional(self) -> Row[ResultRow] | None:
83
+ row = await self._operations.one_or_none(self._query)
84
+ return None if row is None else Row(row, _labels(self._query))
85
+
86
+ async def fetch_optional_into(
87
+ self,
88
+ factory: Callable[..., Mapped],
89
+ /,
90
+ ) -> Mapped | None:
91
+ """One row built by the caller's own type, or nothing.
92
+
93
+ The row and the many-row result both take a factory inline.
94
+ A row that may be missing had to be tested for first, which
95
+ is the one shape that could not be asked for in the chain.
96
+ """
97
+ row = await self.fetch_optional()
98
+ return None if row is None else row.into(factory)
99
+
100
+ async def fetch_value(
101
+ self: AsyncBoundQuery[tuple[ScalarValue]],
102
+ /,
103
+ ) -> ScalarValue:
104
+ """The single column of the single row.
105
+
106
+ Only a query that selected one column can answer this, which is why the
107
+ annotation says so rather than a check at run time.
108
+ """
109
+ return await self._operations.scalar(self._query)
110
+
111
+ def _with(self, query: Query[ResultRow]) -> AsyncBoundQuery[ResultRow]:
112
+ return AsyncBoundQuery(self._operations, query)
113
+
114
+
115
+ class AsyncBoundSelect(AsyncBoundQuery[ResultRow], Generic[ResultRow]):
116
+ """A SELECT that knows its database, with the builder's own shape kept."""
117
+
118
+ __slots__ = ()
119
+
120
+ @property
121
+ def query(self) -> SelectQuery[ResultRow]:
122
+ return _as_select(self._query)
123
+
124
+ def distinct(self, enabled: bool = True, /) -> AsyncBoundSelect[ResultRow]:
125
+ return self._select(self.query.distinct(enabled))
126
+
127
+ def from_(
128
+ self,
129
+ source: SourceBase | TableLike,
130
+ /,
131
+ ) -> AsyncBoundSelect[ResultRow]:
132
+ return self._select(self.query.from_(source))
133
+
134
+ def join(self, source: SourceBase | TableLike, /) -> AsyncBoundJoinStep[ResultRow]:
135
+ """An inner join, qualified by the clause that follows it."""
136
+ return AsyncBoundJoinStep(self._select, self.query.join(source))
137
+
138
+ def inner_join(
139
+ self,
140
+ source: SourceBase | TableLike,
141
+ /,
142
+ ) -> AsyncBoundJoinStep[ResultRow]:
143
+ return AsyncBoundJoinStep(self._select, self.query.inner_join(source))
144
+
145
+ def left_join(
146
+ self,
147
+ source: SourceBase | TableLike,
148
+ /,
149
+ ) -> AsyncBoundJoinStep[ResultRow]:
150
+ return AsyncBoundJoinStep(self._select, self.query.left_join(source))
151
+
152
+ def right_join(
153
+ self,
154
+ source: SourceBase | TableLike,
155
+ /,
156
+ ) -> AsyncBoundJoinStep[ResultRow]:
157
+ return AsyncBoundJoinStep(self._select, self.query.right_join(source))
158
+
159
+ def full_join(
160
+ self,
161
+ source: SourceBase | TableLike,
162
+ /,
163
+ ) -> AsyncBoundJoinStep[ResultRow]:
164
+ return AsyncBoundJoinStep(self._select, self.query.full_join(source))
165
+
166
+ def semi_join(
167
+ self,
168
+ source: SourceBase | TableLike,
169
+ /,
170
+ ) -> AsyncBoundSemiJoinStep[ResultRow]:
171
+ """Rows that have a match, without bringing the match back."""
172
+ return AsyncBoundSemiJoinStep(self._select, self.query.semi_join(source))
173
+
174
+ def anti_join(
175
+ self,
176
+ source: SourceBase | TableLike,
177
+ /,
178
+ ) -> AsyncBoundSemiJoinStep[ResultRow]:
179
+ """Rows that have no match."""
180
+ return AsyncBoundSemiJoinStep(self._select, self.query.anti_join(source))
181
+
182
+ def cross_join(
183
+ self,
184
+ source: SourceBase | TableLike,
185
+ /,
186
+ ) -> AsyncBoundSelect[ResultRow]:
187
+ return self._select(self.query.cross_join(source))
188
+
189
+ def natural_join(
190
+ self,
191
+ source: SourceBase | TableLike,
192
+ /,
193
+ ) -> AsyncBoundSelect[ResultRow]:
194
+ return self._select(self.query.natural_join(source))
195
+
196
+ def natural_left_join(
197
+ self,
198
+ source: SourceBase | TableLike,
199
+ /,
200
+ ) -> AsyncBoundSelect[ResultRow]:
201
+ return self._select(self.query.natural_left_join(source))
202
+
203
+ def natural_right_join(
204
+ self,
205
+ source: SourceBase | TableLike,
206
+ /,
207
+ ) -> AsyncBoundSelect[ResultRow]:
208
+ return self._select(self.query.natural_right_join(source))
209
+
210
+ def natural_full_join(
211
+ self,
212
+ source: SourceBase | TableLike,
213
+ /,
214
+ ) -> AsyncBoundSelect[ResultRow]:
215
+ return self._select(self.query.natural_full_join(source))
216
+
217
+ def where(
218
+ self,
219
+ condition: Expression[bool],
220
+ /,
221
+ ) -> AsyncBoundSelect[ResultRow]:
222
+ return self._select(self.query.where(condition))
223
+
224
+ def group_by(self, *expressions: object) -> AsyncBoundSelect[ResultRow]:
225
+ return self._select(self.query.group_by(*expressions))
226
+
227
+ def having(
228
+ self,
229
+ condition: Expression[bool],
230
+ /,
231
+ ) -> AsyncBoundSelect[ResultRow]:
232
+ return self._select(self.query.having(condition))
233
+
234
+ def with_(
235
+ self,
236
+ *common_tables: CommonTable[object],
237
+ ) -> AsyncBoundSelect[ResultRow]:
238
+ return self._select(self.query.with_(*common_tables))
239
+
240
+ def order_by(self, *terms: object) -> AsyncBoundSelect[ResultRow]:
241
+ return self._select(self.query.order_by(*terms))
242
+
243
+ def limit(self, value: int, /) -> AsyncBoundSelect[ResultRow]:
244
+ return self._select(self.query.limit(value))
245
+
246
+ def offset(self, value: int, /) -> AsyncBoundSelect[ResultRow]:
247
+ return self._select(self.query.offset(value))
248
+
249
+ def _select(
250
+ self,
251
+ query: SelectQuery[ResultRow],
252
+ ) -> AsyncBoundSelect[ResultRow]:
253
+ return AsyncBoundSelect(self._operations, query)
254
+
255
+
256
+ def _as_select(query: Query[ResultRow]) -> SelectQuery[ResultRow]:
257
+ if not isinstance(query, SelectQuery):
258
+ message = "the query is no longer a SELECT"
259
+ raise TypeError(message)
260
+ return query
261
+
262
+
263
+ def _labels(query: Query[ResultRow]) -> tuple[str | None, ...]:
264
+ """A set operation takes its names from the first query in it."""
265
+ select = _first_select(query.node)
266
+ return () if select is None else labels_of(select.projections)
267
+
268
+
269
+ def _first_select(node: object) -> SelectNode | None:
270
+ while isinstance(node, SetNode):
271
+ node = node.left
272
+ return node if isinstance(node, SelectNode) else None
273
+
274
+
275
+ class AsyncBoundJoinStep(Generic[ResultRow]):
276
+ """A bound join waiting to be told how the two tables line up."""
277
+
278
+ __slots__ = ("_bind", "_step")
279
+ _bind: Callable[[SelectQuery[ResultRow]], AsyncBoundSelect[ResultRow]]
280
+ _step: JoinStep[ResultRow]
281
+
282
+ def __init__(
283
+ self,
284
+ bind: Callable[[SelectQuery[ResultRow]], AsyncBoundSelect[ResultRow]],
285
+ step: JoinStep[ResultRow],
286
+ ) -> None:
287
+ object.__setattr__(self, "_bind", bind)
288
+ object.__setattr__(self, "_step", step)
289
+
290
+ def __setattr__(self, name: str, value: object) -> None:
291
+ message = "join steps are immutable"
292
+ raise AttributeError(message, name, value)
293
+
294
+ def on(self, condition: Expression[bool], /) -> AsyncBoundSelect[ResultRow]:
295
+ """The predicate the rows are matched by."""
296
+ return self._bind(self._step.on(condition))
297
+
298
+ def using(self, *columns: str | ColumnReference) -> AsyncBoundSelect[ResultRow]:
299
+ """Matched on columns both tables name the same way."""
300
+ return self._bind(self._step.using(*columns))
301
+
302
+ def on_key(
303
+ self,
304
+ relation: RelationshipDescriptor[object, object],
305
+ /,
306
+ ) -> AsyncBoundSelect[ResultRow]:
307
+ """Matched on the foreign key the schema already declares."""
308
+ return self._bind(self._step.on_key(relation))
309
+
310
+
311
+ class AsyncBoundSemiJoinStep(Generic[ResultRow]):
312
+ """A bound semi or anti join waiting for its predicate."""
313
+
314
+ __slots__ = ("_bind", "_step")
315
+ _bind: Callable[[SelectQuery[ResultRow]], AsyncBoundSelect[ResultRow]]
316
+ _step: SemiJoinStep[ResultRow]
317
+
318
+ def __init__(
319
+ self,
320
+ bind: Callable[[SelectQuery[ResultRow]], AsyncBoundSelect[ResultRow]],
321
+ step: SemiJoinStep[ResultRow],
322
+ ) -> None:
323
+ object.__setattr__(self, "_bind", bind)
324
+ object.__setattr__(self, "_step", step)
325
+
326
+ def __setattr__(self, name: str, value: object) -> None:
327
+ message = "join steps are immutable"
328
+ raise AttributeError(message, name, value)
329
+
330
+ def on(self, condition: Expression[bool], /) -> AsyncBoundSelect[ResultRow]:
331
+ """The predicate a row is matched by."""
332
+ return self._bind(self._step.on(condition))
333
+
334
+
335
+ __all__ = ("AsyncBoundQuery", "AsyncBoundSelect")
pyoq/dsl/aio/writes.py ADDED
@@ -0,0 +1,368 @@
1
+ """The asynchronous counterpart of a write bound to its database."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Generic, TypeVar, TypeVarTuple, cast, overload
6
+
7
+ from pyoq.dsl.labels import labels_of
8
+ from pyoq.dsl.result import Result, Row
9
+ from pyoq.query.write_nodes import WriteNode
10
+ from pyoq.query.writes import (
11
+ ConflictUpdate,
12
+ DeleteStatement,
13
+ InsertStatement,
14
+ MutationStatement,
15
+ ReturningStatement,
16
+ UpdateStatement,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Callable, Iterable
21
+
22
+ from pyoq.query.execution.aio import AsyncQueryOperations
23
+ from pyoq.query.expressions import Expression
24
+ from pyoq.query.writes import ColumnReference
25
+
26
+ Values = TypeVarTuple("Values")
27
+ Mutation = TypeVar("Mutation", bound=MutationStatement[WriteNode])
28
+ ResultRow = TypeVar("ResultRow", bound=tuple[object, ...])
29
+ Mapped = TypeVar("Mapped")
30
+ ScalarValue = TypeVar("ScalarValue")
31
+ Value1 = TypeVar("Value1")
32
+ Value2 = TypeVar("Value2")
33
+ Value3 = TypeVar("Value3")
34
+ Value4 = TypeVar("Value4")
35
+ Value5 = TypeVar("Value5")
36
+ Value6 = TypeVar("Value6")
37
+ Value7 = TypeVar("Value7")
38
+ Value8 = TypeVar("Value8")
39
+ AssignedValue = TypeVar("AssignedValue")
40
+
41
+
42
+ class AsyncBoundReturning(Generic[ResultRow]):
43
+ """Rows a write handed back, read the way a query's rows are read."""
44
+
45
+ __slots__ = ("_operations", "_statement")
46
+ _operations: AsyncQueryOperations
47
+ _statement: ReturningStatement[ResultRow]
48
+
49
+ def __init__(
50
+ self,
51
+ operations: AsyncQueryOperations,
52
+ statement: ReturningStatement[ResultRow],
53
+ ) -> None:
54
+ object.__setattr__(self, "_operations", operations)
55
+ object.__setattr__(self, "_statement", statement)
56
+
57
+ def __setattr__(self, name: str, value: object) -> None:
58
+ message = f"{type(self).__name__} is immutable"
59
+ raise AttributeError(message, name, value)
60
+
61
+ @property
62
+ def statement(self) -> ReturningStatement[ResultRow]:
63
+ return self._statement
64
+
65
+ async def fetch_all(self) -> Result[ResultRow]:
66
+ rows = tuple(await self._operations.many(self._statement))
67
+ return Result(rows, labels_of(self._statement.node.returning))
68
+
69
+ async def fetch_one(self) -> Row[ResultRow]:
70
+ """Exactly one row, because that is what the caller said to expect."""
71
+ return Row(
72
+ await self._operations.one(self._statement),
73
+ labels_of(self._statement.node.returning),
74
+ )
75
+
76
+ async def fetch_optional(self) -> Row[ResultRow] | None:
77
+ row = await self._operations.one_or_none(self._statement)
78
+ return (
79
+ None if row is None else Row(row, labels_of(self._statement.node.returning))
80
+ )
81
+
82
+ async def fetch_optional_into(
83
+ self,
84
+ factory: Callable[..., Mapped],
85
+ /,
86
+ ) -> Mapped | None:
87
+ """One row built by the caller's own type, or nothing.
88
+
89
+ The row and the many-row result both take a factory inline.
90
+ A row that may be missing had to be tested for first, which
91
+ is the one shape that could not be asked for in the chain.
92
+ """
93
+ row = await self.fetch_optional()
94
+ return None if row is None else row.into(factory)
95
+
96
+ async def fetch_value(
97
+ self: AsyncBoundReturning[tuple[ScalarValue]],
98
+ /,
99
+ ) -> ScalarValue:
100
+ return await self._operations.scalar(self._statement)
101
+
102
+
103
+ class AsyncBoundMutation(Generic[Mutation]):
104
+ """What every bound write can do, whatever it is writing."""
105
+
106
+ __slots__ = ("_operations", "_statement")
107
+ _operations: AsyncQueryOperations
108
+ _statement: Mutation
109
+
110
+ def __init__(
111
+ self,
112
+ operations: AsyncQueryOperations,
113
+ statement: Mutation,
114
+ ) -> None:
115
+ object.__setattr__(self, "_operations", operations)
116
+ object.__setattr__(self, "_statement", statement)
117
+
118
+ def __setattr__(self, name: str, value: object) -> None:
119
+ message = f"{type(self).__name__} is immutable"
120
+ raise AttributeError(message, name, value)
121
+
122
+ @property
123
+ def statement(self) -> Mutation:
124
+ """The statement on its own, which cannot reach a database."""
125
+ return self._statement
126
+
127
+ async def execute(self) -> int:
128
+ """How many rows the database says it wrote."""
129
+ written = await self._operations.execute(self._statement)
130
+ return written.rows_affected
131
+
132
+ def returning_all(self) -> AsyncBoundReturning[tuple[object, ...]]:
133
+ """Every column the table declares, without listing them."""
134
+ return AsyncBoundReturning(self._operations, self._statement.returning_all())
135
+
136
+ @overload
137
+ def returning(
138
+ self,
139
+ value1: Expression[Value1],
140
+ /,
141
+ ) -> AsyncBoundReturning[tuple[Value1]]: ...
142
+
143
+ @overload
144
+ def returning(
145
+ self,
146
+ value1: Expression[Value1],
147
+ value2: Expression[Value2],
148
+ /,
149
+ ) -> AsyncBoundReturning[tuple[Value1, Value2]]: ...
150
+
151
+ @overload
152
+ def returning(
153
+ self,
154
+ value1: Expression[Value1],
155
+ value2: Expression[Value2],
156
+ value3: Expression[Value3],
157
+ /,
158
+ ) -> AsyncBoundReturning[tuple[Value1, Value2, Value3]]: ...
159
+
160
+ @overload
161
+ def returning(
162
+ self,
163
+ value1: Expression[Value1],
164
+ value2: Expression[Value2],
165
+ value3: Expression[Value3],
166
+ value4: Expression[Value4],
167
+ /,
168
+ ) -> AsyncBoundReturning[tuple[Value1, Value2, Value3, Value4]]: ...
169
+
170
+ @overload
171
+ def returning(
172
+ self,
173
+ value1: Expression[Value1],
174
+ value2: Expression[Value2],
175
+ value3: Expression[Value3],
176
+ value4: Expression[Value4],
177
+ value5: Expression[Value5],
178
+ /,
179
+ ) -> AsyncBoundReturning[tuple[Value1, Value2, Value3, Value4, Value5]]: ...
180
+
181
+ @overload
182
+ def returning(
183
+ self,
184
+ value1: Expression[Value1],
185
+ value2: Expression[Value2],
186
+ value3: Expression[Value3],
187
+ value4: Expression[Value4],
188
+ value5: Expression[Value5],
189
+ value6: Expression[Value6],
190
+ /,
191
+ ) -> AsyncBoundReturning[tuple[Value1, Value2, Value3, Value4, Value5, Value6]]: ...
192
+
193
+ @overload
194
+ def returning(
195
+ self,
196
+ value1: Expression[Value1],
197
+ value2: Expression[Value2],
198
+ value3: Expression[Value3],
199
+ value4: Expression[Value4],
200
+ value5: Expression[Value5],
201
+ value6: Expression[Value6],
202
+ value7: Expression[Value7],
203
+ /,
204
+ ) -> AsyncBoundReturning[
205
+ tuple[Value1, Value2, Value3, Value4, Value5, Value6, Value7]
206
+ ]: ...
207
+
208
+ @overload
209
+ def returning(
210
+ self,
211
+ value1: Expression[Value1],
212
+ value2: Expression[Value2],
213
+ value3: Expression[Value3],
214
+ value4: Expression[Value4],
215
+ value5: Expression[Value5],
216
+ value6: Expression[Value6],
217
+ value7: Expression[Value7],
218
+ value8: Expression[Value8],
219
+ /,
220
+ ) -> AsyncBoundReturning[
221
+ tuple[Value1, Value2, Value3, Value4, Value5, Value6, Value7, Value8]
222
+ ]: ...
223
+
224
+ @overload
225
+ def returning(
226
+ self,
227
+ value1: object,
228
+ value2: object,
229
+ value3: object,
230
+ value4: object,
231
+ value5: object,
232
+ value6: object,
233
+ value7: object,
234
+ value8: object,
235
+ /,
236
+ *values: object,
237
+ ) -> AsyncBoundReturning[tuple[object, ...]]:
238
+ """Wider than the overloads above name, where the types stop.
239
+
240
+ A statement is still a statement past eight returned columns,
241
+ and matching no overload would take the whole chain down with
242
+ it rather than just the tuple.
243
+ """
244
+
245
+ def returning( # type: ignore[misc]
246
+ self,
247
+ *values: object,
248
+ ) -> AsyncBoundReturning[tuple[object, ...]]:
249
+ # The builder declares typed overloads for callers; this is the one
250
+ # implementation behind them, reached variadically.
251
+ build = cast(
252
+ "Callable[..., ReturningStatement[tuple[object, ...]]]",
253
+ self._statement.returning,
254
+ )
255
+ return AsyncBoundReturning(self._operations, build(*values))
256
+
257
+
258
+ class AsyncBoundInsert(
259
+ AsyncBoundMutation[InsertStatement[*Values]],
260
+ Generic[*Values],
261
+ ):
262
+ """An INSERT and the database it will run against."""
263
+
264
+ __slots__ = ()
265
+
266
+ def values(self, *values: *Values) -> AsyncBoundInsert[*Values]:
267
+ return AsyncBoundInsert(self._operations, self._statement.values(*values))
268
+
269
+ def values_many(self, rows: Iterable[object], /) -> AsyncBoundInsert[*Values]:
270
+ return AsyncBoundInsert(self._operations, self._statement.values_many(rows))
271
+
272
+ def on_conflict_do_nothing(
273
+ self,
274
+ *columns: ColumnReference,
275
+ ) -> AsyncBoundInsert[*Values]:
276
+ return AsyncBoundInsert(
277
+ self._operations,
278
+ self._statement.on_conflict_do_nothing(*columns),
279
+ )
280
+
281
+ def on_conflict_do_update(
282
+ self,
283
+ *columns: ColumnReference,
284
+ ) -> AsyncBoundConflictUpdate[*Values]:
285
+ return AsyncBoundConflictUpdate(
286
+ self._operations,
287
+ self._statement.on_conflict_do_update(*columns),
288
+ )
289
+
290
+
291
+ class AsyncBoundConflictUpdate(
292
+ AsyncBoundMutation[ConflictUpdate[*Values]],
293
+ Generic[*Values],
294
+ ):
295
+ """The SET and WHERE that belong to a conflict clause."""
296
+
297
+ __slots__ = ()
298
+
299
+ def set(
300
+ self,
301
+ column: Expression[AssignedValue],
302
+ value: AssignedValue | Expression[AssignedValue],
303
+ /,
304
+ ) -> AsyncBoundConflictUpdate[*Values]:
305
+ return AsyncBoundConflictUpdate(
306
+ self._operations, self._statement.set(column, value)
307
+ )
308
+
309
+ def where(
310
+ self,
311
+ condition: Expression[bool],
312
+ /,
313
+ ) -> AsyncBoundConflictUpdate[*Values]:
314
+ return AsyncBoundConflictUpdate(
315
+ self._operations, self._statement.where(condition)
316
+ )
317
+
318
+
319
+ class AsyncBoundUpdate(AsyncBoundMutation[UpdateStatement]):
320
+ """An UPDATE, which has to say which rows it means."""
321
+
322
+ __slots__ = ()
323
+
324
+ def set(
325
+ self,
326
+ column: Expression[AssignedValue],
327
+ value: AssignedValue | Expression[AssignedValue],
328
+ /,
329
+ ) -> AsyncBoundUpdate:
330
+ return AsyncBoundUpdate(self._operations, self._statement.set(column, value))
331
+
332
+ def set_values(
333
+ self,
334
+ values: object,
335
+ /,
336
+ *columns: ColumnReference,
337
+ ) -> AsyncBoundUpdate:
338
+ return AsyncBoundUpdate(
339
+ self._operations, self._statement.set_values(values, *columns)
340
+ )
341
+
342
+ def where(self, condition: Expression[bool], /) -> AsyncBoundUpdate:
343
+ return AsyncBoundUpdate(self._operations, self._statement.where(condition))
344
+
345
+ def all_rows(self) -> AsyncBoundUpdate:
346
+ return AsyncBoundUpdate(self._operations, self._statement.all_rows())
347
+
348
+
349
+ class AsyncBoundDelete(AsyncBoundMutation[DeleteStatement]):
350
+ """A DELETE, which has to say which rows it means."""
351
+
352
+ __slots__ = ()
353
+
354
+ def where(self, condition: Expression[bool], /) -> AsyncBoundDelete:
355
+ return AsyncBoundDelete(self._operations, self._statement.where(condition))
356
+
357
+ def all_rows(self) -> AsyncBoundDelete:
358
+ return AsyncBoundDelete(self._operations, self._statement.all_rows())
359
+
360
+
361
+ __all__ = (
362
+ "AsyncBoundConflictUpdate",
363
+ "AsyncBoundDelete",
364
+ "AsyncBoundInsert",
365
+ "AsyncBoundMutation",
366
+ "AsyncBoundReturning",
367
+ "AsyncBoundUpdate",
368
+ )