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/fields.py ADDED
@@ -0,0 +1,60 @@
1
+ """Typed structural field expressions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Generic, TypeVar
6
+
7
+ from pyoq.query.expressions import Expression
8
+ from pyoq.query.nodes import FieldNode
9
+ from pyoq.query.results import family_for_type
10
+
11
+ Value = TypeVar("Value")
12
+ FieldValue = TypeVar("FieldValue")
13
+
14
+
15
+ class Field(Expression[Value], Generic[Value]):
16
+ __slots__ = ()
17
+
18
+ def __init__(
19
+ self,
20
+ database_name: str,
21
+ /,
22
+ *,
23
+ value_type: type[object],
24
+ table_name: str | None = None,
25
+ schema_name: str | None = None,
26
+ catalog_name: str | None = None,
27
+ ) -> None:
28
+ family = family_for_type(value_type)
29
+ super().__init__(
30
+ FieldNode(
31
+ database_name,
32
+ table_name,
33
+ schema_name,
34
+ catalog_name,
35
+ family,
36
+ value_type,
37
+ ),
38
+ family,
39
+ )
40
+
41
+
42
+ def field(
43
+ value_type: type[FieldValue],
44
+ database_name: str,
45
+ /,
46
+ *,
47
+ table_name: str | None = None,
48
+ schema_name: str | None = None,
49
+ catalog_name: str | None = None,
50
+ ) -> Field[FieldValue]:
51
+ return Field(
52
+ database_name,
53
+ value_type=value_type,
54
+ table_name=table_name,
55
+ schema_name=schema_name,
56
+ catalog_name=catalog_name,
57
+ )
58
+
59
+
60
+ __all__ = ("Field", "family_for_type", "field")
@@ -0,0 +1,59 @@
1
+ """MySQL query compilation and execution."""
2
+
3
+ from pyoq.query.mysql.commands import (
4
+ ExecutionEnvironment,
5
+ FailureHandler,
6
+ MySQLCursorSession,
7
+ )
8
+ from pyoq.query.mysql.compiler import MySQLCompiler
9
+ from pyoq.query.mysql.context import MySQLCompilationContext
10
+ from pyoq.query.mysql.executor import MySQLExecutor
11
+ from pyoq.query.mysql.expressions import MySQLExpressionCompiler
12
+ from pyoq.query.mysql.factory import MySQLConnectionFactory
13
+ from pyoq.query.mysql.model import MySQLCapabilities
14
+ from pyoq.query.mysql.operations import MySQLQueryOperations
15
+ from pyoq.query.mysql.parameters import (
16
+ MySQLParameter,
17
+ adapt_parameter,
18
+ adapt_parameters,
19
+ )
20
+ from pyoq.query.mysql.planning import MySQLBulkPlanner
21
+ from pyoq.query.mysql.pool import (
22
+ MySQLConnectionFactoryProtocol,
23
+ MySQLConnectionLease,
24
+ MySQLPool,
25
+ MySQLPoolPolicy,
26
+ MySQLPoolStats,
27
+ )
28
+ from pyoq.query.mysql.transactions import (
29
+ MySQLTransaction,
30
+ MySQLTransactionState,
31
+ TransactionMode,
32
+ )
33
+ from pyoq.query.mysql.writes import MySQLWriteCompilation
34
+
35
+ __all__ = (
36
+ "ExecutionEnvironment",
37
+ "FailureHandler",
38
+ "MySQLBulkPlanner",
39
+ "MySQLCapabilities",
40
+ "MySQLCompilationContext",
41
+ "MySQLCompiler",
42
+ "MySQLConnectionFactory",
43
+ "MySQLConnectionFactoryProtocol",
44
+ "MySQLConnectionLease",
45
+ "MySQLCursorSession",
46
+ "MySQLExecutor",
47
+ "MySQLExpressionCompiler",
48
+ "MySQLParameter",
49
+ "MySQLPool",
50
+ "MySQLPoolPolicy",
51
+ "MySQLPoolStats",
52
+ "MySQLQueryOperations",
53
+ "MySQLTransaction",
54
+ "MySQLTransactionState",
55
+ "MySQLWriteCompilation",
56
+ "TransactionMode",
57
+ "adapt_parameter",
58
+ "adapt_parameters",
59
+ )
@@ -0,0 +1,38 @@
1
+ """Asynchronous MySQL query execution."""
2
+
3
+ from pyoq.query.mysql.aio.commands import (
4
+ AsyncExecutionEnvironment,
5
+ AsyncFailureHandler,
6
+ MySQLAsyncCursorSession,
7
+ )
8
+ from pyoq.query.mysql.aio.driver import (
9
+ AsyncMySQLConnection,
10
+ AsyncMySQLCursor,
11
+ preferred_tls_context,
12
+ )
13
+ from pyoq.query.mysql.aio.executor import MySQLAsyncExecutor
14
+ from pyoq.query.mysql.aio.factory import MySQLAsyncConnectionFactory
15
+ from pyoq.query.mysql.aio.operations import MySQLAsyncQueryOperations
16
+ from pyoq.query.mysql.aio.pool import MySQLAsyncConnectionLease, MySQLAsyncPool
17
+ from pyoq.query.mysql.aio.transactions import (
18
+ MySQLAsyncTransaction,
19
+ MySQLAsyncTransactionState,
20
+ TransactionMode,
21
+ )
22
+
23
+ __all__ = (
24
+ "AsyncExecutionEnvironment",
25
+ "AsyncFailureHandler",
26
+ "AsyncMySQLConnection",
27
+ "AsyncMySQLCursor",
28
+ "MySQLAsyncConnectionFactory",
29
+ "MySQLAsyncConnectionLease",
30
+ "MySQLAsyncCursorSession",
31
+ "MySQLAsyncExecutor",
32
+ "MySQLAsyncPool",
33
+ "MySQLAsyncQueryOperations",
34
+ "MySQLAsyncTransaction",
35
+ "MySQLAsyncTransactionState",
36
+ "TransactionMode",
37
+ "preferred_tls_context",
38
+ )
@@ -0,0 +1,389 @@
1
+ """Asynchronous MySQL cursor execution, timeouts, and cancellation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from asyncio import CancelledError, Event, Task, create_task, shield, wait_for
6
+ from collections.abc import Awaitable, Callable
7
+ from dataclasses import dataclass, field
8
+ from time import monotonic
9
+ from typing import NoReturn, TypeAlias, TypeVar, cast
10
+
11
+ from pyoq.errors import QueryCancelledError, QueryExecutionError, QueryTimeoutError
12
+ from pyoq.query.execution import ExecutionControl
13
+ from pyoq.query.mysql.aio.driver import (
14
+ AsyncMySQLConnection,
15
+ AsyncMySQLCursor,
16
+ driver_errors,
17
+ interface_errors,
18
+ unbuffered_cursor_class,
19
+ )
20
+ from pyoq.query.mysql.commands import MANY_RESULT_SETS
21
+ from pyoq.query.mysql.health import (
22
+ is_cancellation,
23
+ is_connection_failure,
24
+ is_timeout,
25
+ is_transaction_abort,
26
+ )
27
+ from pyoq.query.mysql.parameters import MySQLParameter
28
+
29
+ OperationResult = TypeVar("OperationResult")
30
+
31
+ AsyncConnectionOpener: TypeAlias = "Callable[[], Awaitable[AsyncMySQLConnection]]"
32
+
33
+ _WATCHDOG_INTERVAL = 0.025
34
+ _MILLISECONDS = 1000
35
+ _CONNECTION_ID = "SELECT CONNECTION_ID()"
36
+ _CLEAR_TIMEOUT = "SET @@max_execution_time = 0"
37
+
38
+
39
+ def _is_connection_failure(error: BaseException) -> bool:
40
+ """The driver reports a locally closed connection as an interface error."""
41
+ return isinstance(error, interface_errors()) or is_connection_failure(error)
42
+
43
+
44
+ async def await_watchdog(watchdog: Task[None]) -> None:
45
+ """Wait for a watchdog this monitor owns, without inheriting its fate.
46
+
47
+ Cleanup runs while an error is already in flight, so a watchdog that some
48
+ outer shutdown cancelled must not turn a release into a cancellation. Being
49
+ cancelled ourselves is a different matter and still propagates, which is
50
+ what the shield distinguishes: awaiting a task directly would cancel it
51
+ along with us and leave the two cases indistinguishable.
52
+ """
53
+ try:
54
+ await shield(watchdog)
55
+ except CancelledError:
56
+ if not watchdog.cancelled():
57
+ raise
58
+
59
+
60
+ def _ignore_transaction_abort() -> None:
61
+ return
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class AsyncFailureHandler:
66
+ """Where a driver failure is reported once it has been classified."""
67
+
68
+ invalidate: Callable[[], None]
69
+ abort_transaction: Callable[[], None] = field(default=_ignore_transaction_abort)
70
+
71
+
72
+ @dataclass(frozen=True, slots=True)
73
+ class AsyncExecutionEnvironment:
74
+ connection: AsyncMySQLConnection
75
+ opener: AsyncConnectionOpener
76
+ handler: AsyncFailureHandler
77
+
78
+
79
+ async def kill_query(opener: AsyncConnectionOpener, victim: int) -> None:
80
+ """Interrupt a running statement from a second connection.
81
+
82
+ MySQL has no client-side cancel, so the only way to stop a statement is to
83
+ ask the server to kill it from somewhere else. A kill that cannot be
84
+ delivered leaves the statement running, which the caller detects at its own
85
+ checkpoint.
86
+ """
87
+ try:
88
+ killer = await opener()
89
+ except Exception:
90
+ return
91
+ try:
92
+ cursor = await killer.cursor()
93
+ await cursor.execute(f"KILL QUERY {victim:d}", ())
94
+ except driver_errors():
95
+ return
96
+ finally:
97
+ killer.close()
98
+
99
+
100
+ class MySQLAsyncExecutionMonitor:
101
+ """Server-side SELECT timeout plus a watchdog that kills the running query.
102
+
103
+ MySQL applies ``max_execution_time`` to read-only SELECT statements only, so
104
+ the watchdog enforces both cancellation and the deadline for every other
105
+ statement.
106
+ """
107
+
108
+ __slots__ = ("_control", "_started", "_stop", "_timeout_applied", "_watchdog")
109
+
110
+ def __init__(self, control: ExecutionControl | None) -> None:
111
+ self._control = control or ExecutionControl()
112
+ self._started = monotonic() if self._control.timeout is not None else 0.0
113
+ self._stop = Event()
114
+ self._watchdog: Task[None] | None = None
115
+ self._timeout_applied = False
116
+
117
+ async def install(self, environment: AsyncExecutionEnvironment) -> None:
118
+ self.checkpoint()
119
+ await self._apply_timeout(environment)
120
+ if not self._watchdog_required():
121
+ return
122
+ victim = await read_connection_id(environment.connection)
123
+ self._watchdog = create_task(self._watch(environment, victim))
124
+
125
+ def checkpoint(self) -> None:
126
+ if self._cancel_requested():
127
+ raise QueryCancelledError("MySQL query execution was cancelled")
128
+ if self._expired():
129
+ raise QueryTimeoutError("MySQL query execution exceeded its timeout")
130
+
131
+ def translate_driver_error(
132
+ self,
133
+ error: BaseException,
134
+ environment: AsyncExecutionEnvironment,
135
+ ) -> NoReturn:
136
+ if _is_connection_failure(error):
137
+ environment.handler.invalidate()
138
+ elif is_transaction_abort(error):
139
+ environment.handler.abort_transaction()
140
+ self.checkpoint()
141
+ if is_timeout(error):
142
+ raise QueryTimeoutError(
143
+ "MySQL query execution exceeded its timeout"
144
+ ) from error
145
+ if is_cancellation(error):
146
+ raise QueryCancelledError("MySQL query execution was cancelled") from error
147
+ message = f"MySQL query execution failed: {error}"
148
+ raise QueryExecutionError(message) from error
149
+
150
+ async def close(self, environment: AsyncExecutionEnvironment) -> None:
151
+ self._stop.set()
152
+ watchdog = self._watchdog
153
+ self._watchdog = None
154
+ if watchdog is not None:
155
+ await await_watchdog(watchdog)
156
+ await self._clear_timeout(environment)
157
+
158
+ async def _watch(
159
+ self,
160
+ environment: AsyncExecutionEnvironment,
161
+ victim: int,
162
+ ) -> None:
163
+ while True:
164
+ try:
165
+ await wait_for(self._stop.wait(), _WATCHDOG_INTERVAL)
166
+ except (TimeoutError, CancelledError):
167
+ if self._cancel_requested() or self._expired():
168
+ await kill_query(environment.opener, victim)
169
+ return
170
+ continue
171
+ return
172
+
173
+ def _watchdog_required(self) -> bool:
174
+ return (
175
+ self._control.cancellation_token is not None
176
+ or self._control.timeout is not None
177
+ )
178
+
179
+ async def _apply_timeout(self, environment: AsyncExecutionEnvironment) -> None:
180
+ timeout = self._control.timeout
181
+ if timeout is None:
182
+ return
183
+ milliseconds = max(int(timeout * _MILLISECONDS), 1)
184
+ cursor = await environment.connection.cursor()
185
+ await cursor.execute(f"SET @@max_execution_time = {milliseconds:d}", ())
186
+ await cursor.close()
187
+ self._timeout_applied = True
188
+
189
+ async def _clear_timeout(self, environment: AsyncExecutionEnvironment) -> None:
190
+ if not self._timeout_applied:
191
+ return
192
+ self._timeout_applied = False
193
+ if environment.connection.closed:
194
+ environment.handler.invalidate()
195
+ return
196
+ try:
197
+ cursor = await environment.connection.cursor()
198
+ await cursor.execute(_CLEAR_TIMEOUT, ())
199
+ await cursor.close()
200
+ except driver_errors():
201
+ environment.handler.invalidate()
202
+
203
+ def _cancel_requested(self) -> bool:
204
+ token = self._control.cancellation_token
205
+ return token is not None and token.cancelled
206
+
207
+ def _expired(self) -> bool:
208
+ timeout = self._control.timeout
209
+ return timeout is not None and monotonic() - self._started >= timeout
210
+
211
+
212
+ class MySQLAsyncCursorSession:
213
+ __slots__ = ("_closed", "_cursor", "_environment", "_monitor")
214
+
215
+ def __init__(
216
+ self,
217
+ environment: AsyncExecutionEnvironment,
218
+ cursor: AsyncMySQLCursor,
219
+ monitor: MySQLAsyncExecutionMonitor,
220
+ ) -> None:
221
+ self._environment = environment
222
+ self._cursor = cursor
223
+ self._monitor = monitor
224
+ self._closed = False
225
+
226
+ @property
227
+ def cursor(self) -> AsyncMySQLCursor:
228
+ return self._cursor
229
+
230
+ def checkpoint(self) -> None:
231
+ self._monitor.checkpoint()
232
+
233
+ def translate_driver_error(self, error: BaseException) -> NoReturn:
234
+ self._monitor.translate_driver_error(error, self._environment)
235
+
236
+ async def fetch_batch(self, size: int) -> list[tuple[object, ...]]:
237
+ require_live_connection(self._environment)
238
+ try:
239
+ self._monitor.checkpoint()
240
+ rows = await self._cursor.fetchmany(size)
241
+ self._monitor.checkpoint()
242
+ except driver_errors() as error:
243
+ self._monitor.translate_driver_error(error, self._environment)
244
+ return cast("list[tuple[object, ...]]", list(rows))
245
+
246
+ async def answered_with_more_than_one_result_set(self) -> bool:
247
+ """Whether the procedure left result sets behind that nothing would read.
248
+
249
+ MySQL follows the sets of a procedure with a packet that carries no
250
+ columns, so a set that has columns is one that holds rows.
251
+ """
252
+ try:
253
+ while await self._cursor.nextset():
254
+ if self._cursor.description:
255
+ return True
256
+ except driver_errors() as error:
257
+ self._monitor.translate_driver_error(error, self._environment)
258
+ return False
259
+
260
+ async def close(self) -> None:
261
+ if self._closed:
262
+ return
263
+ self._closed = True
264
+ await _close_cursor(self._cursor, self._environment)
265
+ await self._monitor.close(self._environment)
266
+
267
+
268
+ async def run_sql(
269
+ environment: AsyncExecutionEnvironment,
270
+ sql: str,
271
+ parameters: tuple[MySQLParameter, ...],
272
+ operation: Callable[[AsyncMySQLCursor], Awaitable[OperationResult]],
273
+ control: ExecutionControl | None,
274
+ *,
275
+ one_result_set: bool = False,
276
+ ) -> OperationResult:
277
+ session = await open_cursor(environment, sql, parameters, control)
278
+ try:
279
+ result = await operation(session.cursor)
280
+ if one_result_set and await session.answered_with_more_than_one_result_set():
281
+ raise QueryExecutionError(MANY_RESULT_SETS)
282
+ session.checkpoint()
283
+ return result
284
+ except driver_errors() as error:
285
+ session.translate_driver_error(error)
286
+ finally:
287
+ await session.close()
288
+
289
+
290
+ async def open_cursor(
291
+ environment: AsyncExecutionEnvironment,
292
+ sql: str,
293
+ parameters: tuple[MySQLParameter, ...],
294
+ control: ExecutionControl | None,
295
+ *,
296
+ unbuffered: bool = False,
297
+ ) -> MySQLAsyncCursorSession:
298
+ monitor = MySQLAsyncExecutionMonitor(control)
299
+ cursor: AsyncMySQLCursor | None = None
300
+ try:
301
+ await monitor.install(environment)
302
+ cursor = await _open_driver_cursor(
303
+ environment.connection,
304
+ unbuffered=unbuffered,
305
+ )
306
+ await cursor.execute(sql, parameters)
307
+ monitor.checkpoint()
308
+ return MySQLAsyncCursorSession(environment, cursor, monitor)
309
+ except driver_errors() as error:
310
+ await _release(cursor, monitor, environment)
311
+ monitor.translate_driver_error(error, environment)
312
+ except BaseException:
313
+ await _release(cursor, monitor, environment)
314
+ raise
315
+
316
+
317
+ async def read_connection_id(connection: AsyncMySQLConnection) -> int:
318
+ """Read the identifier a kill has to name.
319
+
320
+ ``SELECT CONNECTION_ID()`` always answers with one integer row, so a reply
321
+ of any other shape means the peer is not a MySQL server and the resulting
322
+ error belongs to the caller.
323
+ """
324
+ cursor = await connection.cursor()
325
+ try:
326
+ await cursor.execute(_CONNECTION_ID, ())
327
+ row = cast("tuple[int, ...]", await cursor.fetchone())
328
+ finally:
329
+ await cursor.close()
330
+ return row[0]
331
+
332
+
333
+ def require_live_connection(environment: AsyncExecutionEnvironment) -> None:
334
+ """Refuse to read through a connection whose socket is already gone.
335
+
336
+ The driver reads an unbuffered result straight from the socket, so a closed
337
+ connection fails inside it rather than raising a driver error that could be
338
+ classified.
339
+ """
340
+ if not environment.connection.closed:
341
+ return
342
+ environment.handler.invalidate()
343
+ message = "MySQL connection was closed while a result was open"
344
+ raise QueryExecutionError(message)
345
+
346
+
347
+ async def _open_driver_cursor(
348
+ connection: AsyncMySQLConnection,
349
+ *,
350
+ unbuffered: bool,
351
+ ) -> AsyncMySQLCursor:
352
+ if unbuffered:
353
+ return await connection.cursor(unbuffered_cursor_class())
354
+ return await connection.cursor()
355
+
356
+
357
+ async def _release(
358
+ cursor: AsyncMySQLCursor | None,
359
+ monitor: MySQLAsyncExecutionMonitor,
360
+ environment: AsyncExecutionEnvironment,
361
+ ) -> None:
362
+ if cursor is not None:
363
+ await _close_cursor(cursor, environment)
364
+ await monitor.close(environment)
365
+
366
+
367
+ async def _close_cursor(
368
+ cursor: AsyncMySQLCursor,
369
+ environment: AsyncExecutionEnvironment,
370
+ ) -> None:
371
+ if environment.connection.closed:
372
+ environment.handler.invalidate()
373
+ return
374
+ await cursor.close()
375
+
376
+
377
+ __all__ = (
378
+ "AsyncConnectionOpener",
379
+ "AsyncExecutionEnvironment",
380
+ "AsyncFailureHandler",
381
+ "MySQLAsyncCursorSession",
382
+ "MySQLAsyncExecutionMonitor",
383
+ "await_watchdog",
384
+ "kill_query",
385
+ "open_cursor",
386
+ "read_connection_id",
387
+ "require_live_connection",
388
+ "run_sql",
389
+ )