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,228 @@
1
+ """Dialect-neutral asynchronous typed result operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from collections.abc import Awaitable, Callable, Iterable, Sequence
7
+ from typing import Protocol, TypeVar, cast
8
+
9
+ from pyoq.errors import (
10
+ MultipleResultsError,
11
+ NoResultError,
12
+ QueryValidationError,
13
+ ResultCardinalityError,
14
+ )
15
+ from pyoq.query.decoding import decode_row, decode_rows
16
+ from pyoq.query.execution.batch import (
17
+ BulkPlan,
18
+ BulkResult,
19
+ OperationBudget,
20
+ WriteProvider,
21
+ )
22
+ from pyoq.query.execution.compilation import CompiledQuery
23
+ from pyoq.query.execution.control import ExecutionControl
24
+ from pyoq.query.execution.results import ExecuteResult, StatementKind
25
+ from pyoq.query.routines import Call
26
+ from pyoq.query.select import Query
27
+ from pyoq.query.statements import StatementCompiler
28
+ from pyoq.query.write_nodes import WriteNode
29
+ from pyoq.query.writes import ReturningStatement, WriteStatement
30
+
31
+
32
+ class AsyncDatabaseCursor(Protocol):
33
+ @property
34
+ def rowcount(self) -> int: ...
35
+
36
+ async def fetchall(self) -> Sequence[object]: ...
37
+
38
+ async def fetchmany(self, size: int, /) -> Sequence[object]: ...
39
+
40
+
41
+ ResultRow = TypeVar("ResultRow")
42
+ ScalarValue = TypeVar("ScalarValue")
43
+ OperationResult = TypeVar("OperationResult")
44
+
45
+
46
+ class AsyncQueryOperations(ABC):
47
+ __slots__ = ()
48
+
49
+ @property
50
+ @abstractmethod
51
+ def compiler(self) -> StatementCompiler: ...
52
+
53
+ @abstractmethod
54
+ def plan_bulk(self, statement: WriteProvider | WriteNode, /) -> BulkPlan: ...
55
+
56
+ @abstractmethod
57
+ async def _run(
58
+ self,
59
+ statement: CompiledQuery,
60
+ operation: Callable[[AsyncDatabaseCursor], Awaitable[OperationResult]],
61
+ control: ExecutionControl | None,
62
+ ) -> OperationResult: ...
63
+
64
+ def last_inserted_id(self, cursor: AsyncDatabaseCursor) -> int | None:
65
+ del cursor
66
+ return None
67
+
68
+ async def one(
69
+ self,
70
+ query: Query[ResultRow] | ReturningStatement[ResultRow] | Call[ResultRow],
71
+ /,
72
+ *,
73
+ control: ExecutionControl | None = None,
74
+ ) -> ResultRow:
75
+ compiled = self.compiler.compile(query)
76
+ rows = await self._limited_rows(compiled, control)
77
+ if not rows:
78
+ raise NoResultError("query returned no rows")
79
+ if len(rows) > 1:
80
+ raise MultipleResultsError("query returned more than one row")
81
+ return cast("ResultRow", decode_row(rows[0], compiled.row_types))
82
+
83
+ async def one_or_none(
84
+ self,
85
+ query: Query[ResultRow] | ReturningStatement[ResultRow] | Call[ResultRow],
86
+ /,
87
+ *,
88
+ control: ExecutionControl | None = None,
89
+ ) -> ResultRow | None:
90
+ compiled = self.compiler.compile(query)
91
+ rows = await self._limited_rows(compiled, control)
92
+ if len(rows) > 1:
93
+ raise MultipleResultsError("query returned more than one row")
94
+ if not rows:
95
+ return None
96
+ return cast("ResultRow", decode_row(rows[0], compiled.row_types))
97
+
98
+ async def many(
99
+ self,
100
+ query: Query[ResultRow] | ReturningStatement[ResultRow] | Call[ResultRow],
101
+ /,
102
+ *,
103
+ control: ExecutionControl | None = None,
104
+ ) -> list[ResultRow]:
105
+ compiled = self.compiler.compile(query)
106
+ rows = await self._run(compiled, fetch_all, control)
107
+ return cast("list[ResultRow]", decode_rows(rows, compiled.row_types))
108
+
109
+ async def scalar(
110
+ self,
111
+ query: Query[tuple[ScalarValue]] | ReturningStatement[tuple[ScalarValue]],
112
+ /,
113
+ *,
114
+ control: ExecutionControl | None = None,
115
+ ) -> ScalarValue:
116
+ row = cast("tuple[object, ...]", await self.one(query, control=control))
117
+ return cast("ScalarValue", _scalar_value(row))
118
+
119
+ async def execute(
120
+ self,
121
+ statement: CompiledQuery | WriteStatement[WriteNode] | Call[tuple[object, ...]],
122
+ /,
123
+ *,
124
+ control: ExecutionControl | None = None,
125
+ ) -> ExecuteResult:
126
+ compiled = (
127
+ statement
128
+ if isinstance(statement, CompiledQuery)
129
+ else self.compiler.compile(statement)
130
+ )
131
+
132
+ async def operation(cursor: AsyncDatabaseCursor) -> ExecuteResult:
133
+ return self._execution_result(cursor, compiled.statement_kind)
134
+
135
+ return await self._run(compiled, operation, control)
136
+
137
+ async def execute_bulk(
138
+ self,
139
+ statement: WriteProvider | WriteNode,
140
+ /,
141
+ *,
142
+ control: ExecutionControl | None = None,
143
+ ) -> BulkResult:
144
+ plan = self.plan_bulk(statement)
145
+ return BulkResult(await self.execute_chunks(plan, control), plan.row_counts)
146
+
147
+ async def execute_chunks(
148
+ self,
149
+ plan: BulkPlan,
150
+ control: ExecutionControl | None,
151
+ ) -> tuple[ExecuteResult, ...]:
152
+ results = [
153
+ await self.execute(chunk.statement, control=control)
154
+ for chunk in plan.chunks
155
+ ]
156
+ return tuple(results)
157
+
158
+ async def many_bulk(
159
+ self,
160
+ statement: ReturningStatement[ResultRow],
161
+ /,
162
+ *,
163
+ control: ExecutionControl | None = None,
164
+ ) -> list[ResultRow]:
165
+ plan = self.plan_bulk(statement)
166
+ rows: list[object] = []
167
+ for chunk in plan.chunks:
168
+ answered = await self._run(chunk.statement, fetch_all, control)
169
+ rows.extend(decode_rows(answered, chunk.statement.row_types))
170
+ return cast("list[ResultRow]", rows)
171
+
172
+ async def execute_all(
173
+ self,
174
+ statements: Iterable[CompiledQuery | WriteStatement[WriteNode]],
175
+ /,
176
+ *,
177
+ budget: OperationBudget | None = None,
178
+ control: ExecutionControl | None = None,
179
+ ) -> tuple[ExecuteResult, ...]:
180
+ operations: Sequence[CompiledQuery | WriteStatement[WriteNode]] = tuple(
181
+ statements
182
+ )
183
+ limit = (budget or OperationBudget()).maximum_operations
184
+ if len(operations) > limit:
185
+ message = f"operation batch exceeds its budget of {limit} operations"
186
+ raise QueryValidationError(message)
187
+ results = [
188
+ await self.execute(operation, control=control) for operation in operations
189
+ ]
190
+ return tuple(results)
191
+
192
+ def _execution_result(
193
+ self,
194
+ cursor: AsyncDatabaseCursor,
195
+ statement_kind: StatementKind,
196
+ ) -> ExecuteResult:
197
+ row_count = max(cursor.rowcount, 0)
198
+ inserted = (
199
+ self.last_inserted_id(cursor)
200
+ if statement_kind is StatementKind.INSERT
201
+ else None
202
+ )
203
+ return ExecuteResult(row_count, inserted)
204
+
205
+ async def _limited_rows(
206
+ self,
207
+ statement: CompiledQuery,
208
+ control: ExecutionControl | None,
209
+ ) -> list[object]:
210
+ return await self._run(statement, _fetch_up_to_two, control)
211
+
212
+
213
+ async def fetch_all(cursor: AsyncDatabaseCursor) -> list[object]:
214
+ return list(await cursor.fetchall())
215
+
216
+
217
+ async def _fetch_up_to_two(cursor: AsyncDatabaseCursor) -> list[object]:
218
+ return list(await cursor.fetchmany(2))
219
+
220
+
221
+ def _scalar_value(row: tuple[object, ...]) -> object:
222
+ if len(row) != 1:
223
+ message = "scalar query projection must contain exactly one value"
224
+ raise ResultCardinalityError(message)
225
+ return row[0]
226
+
227
+
228
+ __all__ = ("AsyncDatabaseCursor", "AsyncQueryOperations", "fetch_all")
@@ -0,0 +1,233 @@
1
+ """Exclusive asynchronous connection pooling shared by every dialect."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from asyncio import Condition, wait_for
7
+ from collections import deque
8
+ from collections.abc import Awaitable, Callable
9
+ from time import monotonic
10
+ from typing import Generic, Self, TypeVar
11
+
12
+ from pyoq.errors import (
13
+ ConnectionPoolClosedError,
14
+ ConnectionPoolError,
15
+ ConnectionPoolTimeoutError,
16
+ )
17
+ from pyoq.query.execution.pooling import PoolPolicy, PoolStats
18
+
19
+ ConnectionType = TypeVar("ConnectionType")
20
+ AsyncConnectionFactory = Callable[[], Awaitable[ConnectionType]]
21
+
22
+
23
+ class AsyncConnectionLease(Generic[ConnectionType]):
24
+ __slots__ = ("_connection", "_entered", "_invalid", "_pool")
25
+
26
+ def __init__(self, pool: AsyncConnectionPool[ConnectionType]) -> None:
27
+ self._pool = pool
28
+ self._connection: ConnectionType | None = None
29
+ self._entered = False
30
+ self._invalid = False
31
+
32
+ @property
33
+ def connection(self) -> ConnectionType:
34
+ if self._connection is None or not self._entered:
35
+ message = "connection lease is not active"
36
+ raise ConnectionPoolError(message)
37
+ return self._connection
38
+
39
+ def invalidate(self) -> None:
40
+ if not self._entered:
41
+ message = "inactive connection lease cannot be invalidated"
42
+ raise ConnectionPoolError(message)
43
+ self._invalid = True
44
+
45
+ async def __aenter__(self) -> ConnectionType:
46
+ if self._entered:
47
+ message = "connection lease cannot be entered twice"
48
+ raise ConnectionPoolError(message)
49
+ connection = await self._pool.acquire_for_lease()
50
+ self._connection = connection
51
+ self._entered = True
52
+ return connection
53
+
54
+ async def __aexit__(
55
+ self,
56
+ exception_type: type[BaseException] | None,
57
+ exception: BaseException | None,
58
+ traceback: object,
59
+ ) -> None:
60
+ connection = self._connection
61
+ if connection is None or not self._entered:
62
+ return
63
+ self._entered = False
64
+ self._connection = None
65
+ await self._pool.release_from_lease(connection, invalid=self._invalid)
66
+
67
+
68
+ class AsyncConnectionPool(ABC, Generic[ConnectionType]):
69
+ __slots__ = (
70
+ "_checked_out",
71
+ "_closed",
72
+ "_condition",
73
+ "_factory",
74
+ "_idle",
75
+ "_open_connections",
76
+ "_policy",
77
+ )
78
+
79
+ def __init__(
80
+ self,
81
+ factory: AsyncConnectionFactory[ConnectionType],
82
+ policy: PoolPolicy | None = None,
83
+ ) -> None:
84
+ self._factory = factory
85
+ self._policy = policy or PoolPolicy()
86
+ self._condition = Condition()
87
+ self._idle: deque[ConnectionType] = deque()
88
+ self._checked_out: set[int] = set()
89
+ self._open_connections = 0
90
+ self._closed = False
91
+
92
+ @abstractmethod
93
+ async def reset(self, connection: ConnectionType) -> bool: ...
94
+
95
+ @abstractmethod
96
+ def validate(self, value: object) -> ConnectionType: ...
97
+
98
+ @abstractmethod
99
+ async def dispose(self, connection: ConnectionType) -> None: ...
100
+
101
+ @property
102
+ def policy(self) -> PoolPolicy:
103
+ return self._policy
104
+
105
+ @property
106
+ def stats(self) -> PoolStats:
107
+ return PoolStats(
108
+ self._open_connections,
109
+ len(self._idle),
110
+ len(self._checked_out),
111
+ self._closed,
112
+ )
113
+
114
+ def checkout(self) -> AsyncConnectionLease[ConnectionType]:
115
+ return AsyncConnectionLease(self)
116
+
117
+ async def open(self) -> Self:
118
+ created: list[ConnectionType] = []
119
+ try:
120
+ for _ in range(self.policy.minimum_size):
121
+ created.append(await self._create_connection())
122
+ except Exception:
123
+ for connection in created:
124
+ await self.dispose(connection)
125
+ raise
126
+ self._idle.extend(created)
127
+ self._open_connections += len(created)
128
+ return self
129
+
130
+ async def close(self) -> None:
131
+ async with self._condition:
132
+ if self._closed:
133
+ return
134
+ self._closed = True
135
+ idle = tuple(self._idle)
136
+ self._idle.clear()
137
+ self._open_connections -= len(idle)
138
+ self._condition.notify_all()
139
+ for connection in idle:
140
+ await self.dispose(connection)
141
+
142
+ async def __aenter__(self) -> Self:
143
+ return await self.open()
144
+
145
+ async def __aexit__(
146
+ self,
147
+ exception_type: type[BaseException] | None,
148
+ exception: BaseException | None,
149
+ traceback: object,
150
+ ) -> None:
151
+ await self.close()
152
+
153
+ async def acquire_for_lease(self) -> ConnectionType:
154
+ deadline = _deadline(self.policy.checkout_timeout)
155
+ async with self._condition:
156
+ while True:
157
+ self._require_open()
158
+ if self._idle:
159
+ connection = self._idle.popleft()
160
+ self._checked_out.add(id(connection))
161
+ return connection
162
+ if self._open_connections < self.policy.maximum_size:
163
+ self._open_connections += 1
164
+ break
165
+ await self._wait_for_connection(deadline)
166
+ return await self._create_reserved_connection()
167
+
168
+ async def release_from_lease(
169
+ self,
170
+ connection: ConnectionType,
171
+ *,
172
+ invalid: bool,
173
+ ) -> None:
174
+ reusable = not invalid and await self.reset(connection)
175
+ async with self._condition:
176
+ self._checked_out.discard(id(connection))
177
+ if reusable and not self._closed:
178
+ self._idle.append(connection)
179
+ else:
180
+ self._open_connections -= 1
181
+ self._condition.notify()
182
+ if not reusable or self._closed:
183
+ await self.dispose(connection)
184
+
185
+ async def _create_reserved_connection(self) -> ConnectionType:
186
+ try:
187
+ connection = await self._create_connection()
188
+ except Exception:
189
+ async with self._condition:
190
+ self._open_connections -= 1
191
+ self._condition.notify()
192
+ raise
193
+ async with self._condition:
194
+ if self._closed:
195
+ self._open_connections -= 1
196
+ await self.dispose(connection)
197
+ raise ConnectionPoolClosedError("connection pool is closed")
198
+ self._checked_out.add(id(connection))
199
+ return connection
200
+
201
+ async def _create_connection(self) -> ConnectionType:
202
+ try:
203
+ connection_value: object = await self._factory()
204
+ except ConnectionPoolError:
205
+ raise
206
+ except Exception as error:
207
+ message = f"unable to create pooled connection: {error}"
208
+ raise ConnectionPoolError(message) from error
209
+ return self.validate(connection_value)
210
+
211
+ async def _wait_for_connection(self, deadline: float | None) -> None:
212
+ remaining = None if deadline is None else deadline - monotonic()
213
+ try:
214
+ await wait_for(self._condition.wait(), remaining)
215
+ except TimeoutError as error:
216
+ message = "connection checkout exceeded its timeout"
217
+ raise ConnectionPoolTimeoutError(message) from error
218
+
219
+ def _require_open(self) -> None:
220
+ if self._closed:
221
+ message = "connection pool is closed"
222
+ raise ConnectionPoolClosedError(message)
223
+
224
+
225
+ def _deadline(timeout: float | None) -> float | None:
226
+ return None if timeout is None else monotonic() + timeout
227
+
228
+
229
+ __all__ = (
230
+ "AsyncConnectionFactory",
231
+ "AsyncConnectionLease",
232
+ "AsyncConnectionPool",
233
+ )
@@ -0,0 +1,161 @@
1
+ """Bounded asynchronous row streaming shared by every dialect."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from asyncio import current_task
6
+ from collections import deque
7
+ from collections.abc import AsyncIterator, Awaitable, Callable
8
+ from typing import Generic, Protocol, Self, TypeVar, cast
9
+
10
+ from pyoq.errors import StreamStateError
11
+ from pyoq.query.decoding import decode_rows
12
+ from pyoq.query.execution.streaming import StreamPolicy
13
+
14
+ ResultRow = TypeVar("ResultRow")
15
+
16
+
17
+ class AsyncCursorSession(Protocol):
18
+ def checkpoint(self) -> None: ...
19
+
20
+ async def fetch_batch(self, size: int) -> list[tuple[object, ...]]: ...
21
+
22
+ async def close(self) -> None: ...
23
+
24
+
25
+ class AsyncStreamResource:
26
+ __slots__ = ("_closed", "_release", "session")
27
+
28
+ def __init__(
29
+ self,
30
+ session: AsyncCursorSession,
31
+ release: Callable[[], Awaitable[None]],
32
+ ) -> None:
33
+ self.session = session
34
+ self._release = release
35
+ self._closed = False
36
+
37
+ async def close(self) -> None:
38
+ if self._closed:
39
+ return
40
+ self._closed = True
41
+ try:
42
+ await self.session.close()
43
+ finally:
44
+ await self._release()
45
+
46
+
47
+ class AsyncRowStream(AsyncIterator[ResultRow], Generic[ResultRow]):
48
+ __slots__ = (
49
+ "_buffer",
50
+ "_closed",
51
+ "_opened",
52
+ "_opener",
53
+ "_owner_task",
54
+ "_policy",
55
+ "_resource",
56
+ "_row_types",
57
+ )
58
+
59
+ def __init__(
60
+ self,
61
+ opener: Callable[[], Awaitable[AsyncStreamResource]],
62
+ policy: StreamPolicy,
63
+ row_types: tuple[type[object] | None, ...] = (),
64
+ ) -> None:
65
+ self._opener = opener
66
+ self._policy = policy
67
+ self._row_types = row_types
68
+ self._buffer: deque[tuple[object, ...]] = deque()
69
+ self._resource: AsyncStreamResource | None = None
70
+ self._owner_task: object | None = None
71
+ self._opened = False
72
+ self._closed = False
73
+
74
+ @property
75
+ def closed(self) -> bool:
76
+ return self._closed
77
+
78
+ @property
79
+ def buffered_rows(self) -> int:
80
+ return len(self._buffer)
81
+
82
+ async def __aenter__(self) -> Self:
83
+ if self._closed:
84
+ raise StreamStateError("closed row stream cannot be entered")
85
+ await self._ensure_open()
86
+ return self
87
+
88
+ async def __aexit__(
89
+ self,
90
+ exception_type: type[BaseException] | None,
91
+ exception: BaseException | None,
92
+ traceback: object,
93
+ ) -> None:
94
+ await self.close()
95
+
96
+ def __aiter__(self) -> Self:
97
+ return self
98
+
99
+ async def __anext__(self) -> ResultRow:
100
+ if self._closed:
101
+ raise StopAsyncIteration
102
+ await self._ensure_open()
103
+ self._require_owner()
104
+ try:
105
+ resource = self._require_resource()
106
+ resource.session.checkpoint()
107
+ if not self._buffer:
108
+ await self._load_batch()
109
+ return cast("ResultRow", self._buffer.popleft())
110
+ except BaseException:
111
+ await self.close()
112
+ raise
113
+
114
+ async def close(self) -> None:
115
+ if self._closed:
116
+ return
117
+ if self._opened:
118
+ self._require_owner()
119
+ self._closed = True
120
+ self._buffer.clear()
121
+ resource = self._resource
122
+ self._resource = None
123
+ if resource is not None:
124
+ await resource.close()
125
+
126
+ async def _ensure_open(self) -> None:
127
+ if self._opened:
128
+ self._require_owner()
129
+ return
130
+ self._owner_task = current_task()
131
+ try:
132
+ self._resource = await self._opener()
133
+ except BaseException:
134
+ self._closed = True
135
+ raise
136
+ self._opened = True
137
+
138
+ async def _load_batch(self) -> None:
139
+ resource = self._require_resource()
140
+ rows = await resource.session.fetch_batch(self._policy.batch_size)
141
+ if not rows:
142
+ raise StopAsyncIteration
143
+ self._buffer.extend(
144
+ cast("list[tuple[object, ...]]", decode_rows(rows, self._row_types))
145
+ )
146
+
147
+ def _require_resource(self) -> AsyncStreamResource:
148
+ if self._resource is None:
149
+ raise StreamStateError("row stream has no active cursor")
150
+ return self._resource
151
+
152
+ def _require_owner(self) -> None:
153
+ if self._owner_task is not current_task():
154
+ raise StreamStateError("row stream used outside its owner task")
155
+
156
+
157
+ __all__ = (
158
+ "AsyncCursorSession",
159
+ "AsyncRowStream",
160
+ "AsyncStreamResource",
161
+ )