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,196 @@
1
+ """The typed boundary PyOQ holds the asynchronous MySQL driver to.
2
+
3
+ The driver ships only generated type stubs that leave most of its surface
4
+ unknown, so the shape PyOQ depends on is declared here and every reference to
5
+ the driver is confined to this module. Everything above works against these
6
+ protocols.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ssl
12
+ from collections.abc import Sequence
13
+ from types import ModuleType
14
+ from typing import Protocol, cast
15
+
16
+ from pyoq.errors import ConnectionPoolError, OperationUnavailableError
17
+ from pyoq.query.mysql.parameters import MySQLParameter
18
+ from pyoq.schema.mysql.dsn import MySQLConnectionSettings
19
+
20
+ _CHARSET = "utf8mb4"
21
+ _FOUND_ROWS = 2
22
+ _MISSING_DRIVER = (
23
+ "asynchronous MySQL support requires the asyncmy driver; "
24
+ "install pyoq-sql[mysql-async]"
25
+ )
26
+
27
+
28
+ class AsyncMySQLCursor(Protocol):
29
+ @property
30
+ def rowcount(self) -> int: ...
31
+
32
+ @property
33
+ def lastrowid(self) -> int: ...
34
+
35
+ async def execute(
36
+ self,
37
+ query: str,
38
+ args: tuple[MySQLParameter, ...],
39
+ /,
40
+ ) -> int: ...
41
+
42
+ async def fetchone(self) -> tuple[object, ...] | None: ...
43
+
44
+ async def fetchall(self) -> Sequence[object]: ...
45
+
46
+ async def fetchmany(self, size: int, /) -> Sequence[object]: ...
47
+
48
+ async def nextset(self) -> bool | None: ...
49
+
50
+ @property
51
+ def description(self) -> Sequence[object] | None: ...
52
+
53
+ async def close(self) -> None: ...
54
+
55
+
56
+ class AsyncMySQLConnection(Protocol):
57
+ @property
58
+ def closed(self) -> bool: ...
59
+
60
+ async def cursor(self, cursor_class: object = ..., /) -> AsyncMySQLCursor: ...
61
+
62
+ def close(self) -> None: ...
63
+
64
+ async def ensure_closed(self) -> None: ...
65
+
66
+
67
+ class _DriverConnection(Protocol):
68
+ """Only what the driver itself is asked for."""
69
+
70
+ async def cursor(self, cursor_class: object = ..., /) -> AsyncMySQLCursor: ...
71
+
72
+ def close(self) -> None: ...
73
+
74
+ async def ensure_closed(self) -> None: ...
75
+
76
+
77
+ class OwnedConnection:
78
+ """A driver connection that knows whether it has been closed.
79
+
80
+ The driver keeps reporting a closed connection as connected, and reading
81
+ through one fails inside the driver with an attribute error rather than a
82
+ driver error that could be classified. Closure is therefore tracked here,
83
+ where it is the one fact the layers above rely on.
84
+ """
85
+
86
+ __slots__ = ("_closed", "_connection")
87
+
88
+ def __init__(self, connection: _DriverConnection) -> None:
89
+ self._connection = connection
90
+ self._closed = False
91
+
92
+ @property
93
+ def closed(self) -> bool:
94
+ return self._closed
95
+
96
+ async def cursor(self, cursor_class: object = None, /) -> AsyncMySQLCursor:
97
+ if cursor_class is None:
98
+ return await self._connection.cursor()
99
+ return await self._connection.cursor(cursor_class)
100
+
101
+ def close(self) -> None:
102
+ if self._closed:
103
+ return
104
+ self._closed = True
105
+ self._connection.close()
106
+
107
+ async def ensure_closed(self) -> None:
108
+ if self._closed:
109
+ return
110
+ self._closed = True
111
+ await self._connection.ensure_closed()
112
+
113
+
114
+ def preferred_tls_context() -> ssl.SSLContext:
115
+ """Encrypt the connection without demanding a verifiable certificate.
116
+
117
+ This is the mode MySQL clients and the synchronous driver already use. A
118
+ MySQL server generates its own certificate, which no public authority
119
+ signs, so requiring verification would refuse the ordinary case. Encrypting
120
+ also keeps the password off the wire in clear text and avoids the RSA
121
+ exchange that would otherwise need a separate cryptography library.
122
+ """
123
+ context = ssl.create_default_context()
124
+ context.verify_flags &= ~ssl.VERIFY_X509_STRICT
125
+ context.check_hostname = False
126
+ context.verify_mode = ssl.CERT_NONE
127
+ return context
128
+
129
+
130
+ def load_driver() -> ModuleType:
131
+ try:
132
+ import asyncmy
133
+ except ImportError as error:
134
+ raise OperationUnavailableError(_MISSING_DRIVER) from error
135
+ return asyncmy
136
+
137
+
138
+ def driver_errors() -> type[BaseException]:
139
+ """The exception hierarchy this driver raises.
140
+
141
+ The codes match every other MySQL driver, but the classes do not, so the
142
+ layers above catch what this returns rather than naming a driver.
143
+ """
144
+ return cast("type[BaseException]", load_driver().errors.Error)
145
+
146
+
147
+ def operational_errors() -> type[BaseException]:
148
+ """What the driver raises for a server-reported operational failure."""
149
+ return cast("type[BaseException]", load_driver().errors.OperationalError)
150
+
151
+
152
+ def interface_errors() -> type[BaseException]:
153
+ """What the driver raises when its own connection is unusable."""
154
+ return cast("type[BaseException]", load_driver().errors.InterfaceError)
155
+
156
+
157
+ def unbuffered_cursor_class() -> object:
158
+ """The driver's unbuffered cursor, which keeps streaming memory bounded."""
159
+ return cast("object", load_driver().cursors.SSCursor)
160
+
161
+
162
+ async def open_connection(
163
+ settings: MySQLConnectionSettings,
164
+ ) -> AsyncMySQLConnection:
165
+ """Open one driver connection and hand it back as a typed protocol."""
166
+ driver = load_driver()
167
+ try:
168
+ connection = await driver.connect(
169
+ host=settings.host,
170
+ port=settings.port,
171
+ user=settings.user,
172
+ password=settings.password,
173
+ database=settings.database,
174
+ charset=_CHARSET,
175
+ autocommit=True,
176
+ client_flag=_FOUND_ROWS,
177
+ ssl=preferred_tls_context(),
178
+ )
179
+ except Exception as error:
180
+ message = f"unable to open MySQL connection: {error}"
181
+ raise ConnectionPoolError(message) from error
182
+ return OwnedConnection(cast("_DriverConnection", connection))
183
+
184
+
185
+ __all__ = (
186
+ "AsyncMySQLConnection",
187
+ "AsyncMySQLCursor",
188
+ "OwnedConnection",
189
+ "driver_errors",
190
+ "interface_errors",
191
+ "load_driver",
192
+ "open_connection",
193
+ "operational_errors",
194
+ "preferred_tls_context",
195
+ "unbuffered_cursor_class",
196
+ )
@@ -0,0 +1,123 @@
1
+ """Asynchronous typed MySQL query execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from typing import TypeVar
7
+
8
+ from pyoq.query.execution import (
9
+ CompiledQuery,
10
+ ExecutionControl,
11
+ StatementKind,
12
+ StreamPolicy,
13
+ )
14
+ from pyoq.query.execution.aio import AsyncRowStream, AsyncStreamResource
15
+ from pyoq.query.execution.aio.pooling import AsyncConnectionLease
16
+ from pyoq.query.mysql.aio.commands import (
17
+ AsyncExecutionEnvironment,
18
+ AsyncFailureHandler,
19
+ open_cursor,
20
+ run_sql,
21
+ )
22
+ from pyoq.query.mysql.aio.driver import AsyncMySQLConnection, AsyncMySQLCursor
23
+ from pyoq.query.mysql.aio.operations import MySQLAsyncQueryOperations
24
+ from pyoq.query.mysql.aio.pool import MySQLAsyncPool
25
+ from pyoq.query.mysql.aio.transactions import MySQLAsyncTransaction
26
+ from pyoq.query.mysql.compiler import MySQLCompiler
27
+ from pyoq.query.mysql.parameters import adapt_parameters
28
+ from pyoq.query.mysql.transactions import TransactionMode
29
+ from pyoq.query.select import Query
30
+
31
+ OperationResult = TypeVar("OperationResult")
32
+ ResultRow = TypeVar("ResultRow")
33
+
34
+
35
+ class MySQLAsyncExecutor(MySQLAsyncQueryOperations):
36
+ __slots__ = ("_pool",)
37
+
38
+ def __init__(
39
+ self,
40
+ pool: MySQLAsyncPool,
41
+ compiler: MySQLCompiler | None = None,
42
+ ) -> None:
43
+ self._pool = pool
44
+ super().__init__(compiler or MySQLCompiler())
45
+
46
+ @property
47
+ def pool(self) -> MySQLAsyncPool:
48
+ return self._pool
49
+
50
+ def stream(
51
+ self,
52
+ query: Query[ResultRow],
53
+ /,
54
+ *,
55
+ policy: StreamPolicy | None = None,
56
+ ) -> AsyncRowStream[ResultRow]:
57
+ selected = policy or StreamPolicy()
58
+ statement = self.compiler.compile(query)
59
+ return AsyncRowStream(lambda: self._open_stream(statement, selected), selected)
60
+
61
+ def transaction(
62
+ self,
63
+ mode: TransactionMode = TransactionMode.REPEATABLE_READ,
64
+ *,
65
+ control: ExecutionControl | None = None,
66
+ ) -> MySQLAsyncTransaction:
67
+ return MySQLAsyncTransaction(self.pool, self.compiler, mode, control)
68
+
69
+ def _environment(
70
+ self,
71
+ connection: AsyncMySQLConnection,
72
+ lease: AsyncConnectionLease[AsyncMySQLConnection],
73
+ ) -> AsyncExecutionEnvironment:
74
+ return AsyncExecutionEnvironment(
75
+ connection,
76
+ self.pool.connection_factory,
77
+ AsyncFailureHandler(lease.invalidate),
78
+ )
79
+
80
+ async def _run(
81
+ self,
82
+ statement: CompiledQuery,
83
+ operation: Callable[[AsyncMySQLCursor], Awaitable[OperationResult]],
84
+ control: ExecutionControl | None,
85
+ ) -> OperationResult:
86
+ parameters = adapt_parameters(statement.parameters)
87
+ lease = self.pool.checkout()
88
+ async with lease as connection:
89
+ return await run_sql(
90
+ self._environment(connection, lease),
91
+ statement.sql,
92
+ parameters,
93
+ operation,
94
+ control,
95
+ one_result_set=statement.statement_kind is StatementKind.CALL,
96
+ )
97
+
98
+ async def _open_stream(
99
+ self,
100
+ statement: CompiledQuery,
101
+ policy: StreamPolicy,
102
+ ) -> AsyncStreamResource:
103
+ parameters = adapt_parameters(statement.parameters)
104
+ lease = self.pool.checkout()
105
+ connection = await lease.__aenter__()
106
+ try:
107
+ session = await open_cursor(
108
+ self._environment(connection, lease),
109
+ statement.sql,
110
+ parameters,
111
+ policy.control,
112
+ unbuffered=True,
113
+ )
114
+ except BaseException:
115
+ await lease.__aexit__(None, None, None)
116
+ raise
117
+ return AsyncStreamResource(
118
+ session,
119
+ lambda: lease.__aexit__(None, None, None),
120
+ )
121
+
122
+
123
+ __all__ = ("MySQLAsyncExecutor",)
@@ -0,0 +1,26 @@
1
+ """Asynchronous MySQL connection creation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from pyoq.query.mysql.aio.driver import AsyncMySQLConnection, open_connection
8
+ from pyoq.schema.mysql.dsn import parse_data_source_name
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class MySQLAsyncConnectionFactory:
13
+ """Opens encrypted MySQL connections that count matched rows.
14
+
15
+ The transport is encrypted for the same reason the synchronous driver
16
+ encrypts it, and matched-row counting keeps an affected row count meaning
17
+ what it means on every other dialect.
18
+ """
19
+
20
+ dsn: str
21
+
22
+ async def __call__(self) -> AsyncMySQLConnection:
23
+ return await open_connection(parse_data_source_name(self.dsn))
24
+
25
+
26
+ __all__ = ("MySQLAsyncConnectionFactory",)
@@ -0,0 +1,38 @@
1
+ """Asynchronous MySQL typed result operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import cast
6
+
7
+ from pyoq.query.execution import BulkPlan, WriteProvider
8
+ from pyoq.query.execution.aio import AsyncDatabaseCursor, AsyncQueryOperations
9
+ from pyoq.query.mysql.aio.driver import AsyncMySQLCursor
10
+ from pyoq.query.mysql.compiler import MySQLCompiler
11
+ from pyoq.query.mysql.planning import MySQLBulkPlanner
12
+ from pyoq.query.write_nodes import WriteNode
13
+
14
+
15
+ class MySQLAsyncQueryOperations(AsyncQueryOperations):
16
+ __slots__ = ("_compiler",)
17
+
18
+ def __init__(self, compiler: MySQLCompiler) -> None:
19
+ self._compiler = compiler
20
+
21
+ @property
22
+ def compiler(self) -> MySQLCompiler:
23
+ return self._compiler
24
+
25
+ def plan_bulk(self, statement: WriteProvider | WriteNode, /) -> BulkPlan:
26
+ return MySQLBulkPlanner(self.compiler).plan(statement)
27
+
28
+ def last_inserted_id(self, cursor: AsyncDatabaseCursor) -> int | None:
29
+ """Report the first identifier a multi-row insert generated.
30
+
31
+ MySQL reports the identifier of the first row an insert generated, not
32
+ the last, and reports zero when a statement generated none.
33
+ """
34
+ identifier = cast("AsyncMySQLCursor", cursor).lastrowid
35
+ return identifier if identifier > 0 else None
36
+
37
+
38
+ __all__ = ("MySQLAsyncQueryOperations",)
@@ -0,0 +1,53 @@
1
+ """Exclusive asynchronous MySQL connection pool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TypeAlias, cast
6
+
7
+ from pyoq.errors import ConnectionPoolError
8
+ from pyoq.query.execution.aio.pooling import AsyncConnectionLease, AsyncConnectionPool
9
+ from pyoq.query.mysql.aio.commands import AsyncConnectionOpener
10
+ from pyoq.query.mysql.aio.driver import (
11
+ AsyncMySQLConnection,
12
+ OwnedConnection,
13
+ driver_errors,
14
+ )
15
+
16
+ MySQLAsyncConnectionLease: TypeAlias = AsyncConnectionLease[AsyncMySQLConnection]
17
+
18
+ _RESET_TIMEOUT = "SET @@max_execution_time = 0"
19
+ _ROLLBACK = "ROLLBACK"
20
+
21
+
22
+ class MySQLAsyncPool(AsyncConnectionPool[AsyncMySQLConnection]):
23
+ __slots__ = ()
24
+
25
+ @property
26
+ def connection_factory(self) -> AsyncConnectionOpener:
27
+ return self._factory
28
+
29
+ async def reset(self, connection: AsyncMySQLConnection) -> bool:
30
+ if connection.closed:
31
+ return False
32
+ try:
33
+ cursor = await connection.cursor()
34
+ await cursor.execute(_ROLLBACK, ())
35
+ await cursor.execute(_RESET_TIMEOUT, ())
36
+ await cursor.close()
37
+ except driver_errors():
38
+ return False
39
+ return not connection.closed
40
+
41
+ def validate(self, value: object) -> AsyncMySQLConnection:
42
+ if not isinstance(value, OwnedConnection):
43
+ message = "MySQL connection factory returned an invalid connection"
44
+ raise ConnectionPoolError(message)
45
+ return cast("AsyncMySQLConnection", value)
46
+
47
+ async def dispose(self, connection: AsyncMySQLConnection) -> None:
48
+ """Close a connection the driver has not already closed itself."""
49
+ if not connection.closed:
50
+ await connection.ensure_closed()
51
+
52
+
53
+ __all__ = ("MySQLAsyncConnectionLease", "MySQLAsyncPool")