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.
- pyoq/__init__.py +10 -0
- pyoq/__main__.py +5 -0
- pyoq/_native.pyi +5 -0
- pyoq/cli/__init__.py +5 -0
- pyoq/cli/commands.py +270 -0
- pyoq/cli/defaults.py +98 -0
- pyoq/cli/services.py +97 -0
- pyoq/config/__init__.py +31 -0
- pyoq/config/connection.py +161 -0
- pyoq/config/loader.py +289 -0
- pyoq/config/models.py +245 -0
- pyoq/config/values.py +142 -0
- pyoq/descriptors.py +165 -0
- pyoq/diagnostics/__init__.py +68 -0
- pyoq/diagnostics/budget.py +136 -0
- pyoq/diagnostics/events.py +137 -0
- pyoq/diagnostics/fingerprint.py +267 -0
- pyoq/diagnostics/instrumented.py +237 -0
- pyoq/diagnostics/metrics.py +61 -0
- pyoq/diagnostics/observation.py +227 -0
- pyoq/diagnostics/scoped.py +103 -0
- pyoq/django/__init__.py +15 -0
- pyoq/django/apps.py +17 -0
- pyoq/django/execution.py +317 -0
- pyoq/django/generation.py +59 -0
- pyoq/django/management/__init__.py +0 -0
- pyoq/django/management/commands/__init__.py +0 -0
- pyoq/django/management/commands/makemigrations.py +53 -0
- pyoq/django/management/commands/pyoq_codegen.py +75 -0
- pyoq/django/parameters.py +101 -0
- pyoq/django/schema.py +379 -0
- pyoq/django/settings.py +87 -0
- pyoq/django/timeouts.py +105 -0
- pyoq/dsl/__init__.py +64 -0
- pyoq/dsl/aio/__init__.py +31 -0
- pyoq/dsl/aio/context.py +295 -0
- pyoq/dsl/aio/queries.py +335 -0
- pyoq/dsl/aio/writes.py +368 -0
- pyoq/dsl/context.py +326 -0
- pyoq/dsl/entry.py +37 -0
- pyoq/dsl/labels.py +36 -0
- pyoq/dsl/queries.py +339 -0
- pyoq/dsl/result.py +164 -0
- pyoq/dsl/writes.py +360 -0
- pyoq/errors.py +317 -0
- pyoq/fastapi/__init__.py +32 -0
- pyoq/fastapi/dependencies.py +167 -0
- pyoq/fastapi/lifespan.py +119 -0
- pyoq/fetching/__init__.py +55 -0
- pyoq/fetching/collections.py +136 -0
- pyoq/fetching/execution.py +587 -0
- pyoq/fetching/joined.py +79 -0
- pyoq/fetching/nesting.py +183 -0
- pyoq/fetching/plans.py +541 -0
- pyoq/fetching/select_in.py +149 -0
- pyoq/fetching/tables.py +110 -0
- pyoq/generation/__init__.py +54 -0
- pyoq/generation/cleanup.py +44 -0
- pyoq/generation/contracts.py +248 -0
- pyoq/generation/drift.py +169 -0
- pyoq/generation/lock.py +33 -0
- pyoq/generation/manifest.py +114 -0
- pyoq/generation/model.py +1001 -0
- pyoq/generation/pipeline.py +119 -0
- pyoq/generation/rendering/__init__.py +5 -0
- pyoq/generation/rendering/domains.py +51 -0
- pyoq/generation/rendering/enums.py +29 -0
- pyoq/generation/rendering/exports.py +70 -0
- pyoq/generation/rendering/imports.py +63 -0
- pyoq/generation/rendering/package.py +56 -0
- pyoq/generation/rendering/relations.py +133 -0
- pyoq/generation/rendering/routines.py +396 -0
- pyoq/generation/rendering/rows.py +79 -0
- pyoq/generation/rendering/source.py +121 -0
- pyoq/generation/rendering/tables.py +300 -0
- pyoq/generation/rendering/writes.py +514 -0
- pyoq/generation/validation.py +27 -0
- pyoq/generation/writer.py +184 -0
- pyoq/hydration/__init__.py +24 -0
- pyoq/hydration/engine.py +155 -0
- pyoq/hydration/identity.py +194 -0
- pyoq/hydration/plan.py +116 -0
- pyoq/migrations/__init__.py +9 -0
- pyoq/migrations/alembic.py +106 -0
- pyoq/migrations/hooks.py +75 -0
- pyoq/naming.py +261 -0
- pyoq/policies/__init__.py +47 -0
- pyoq/policies/bypass.py +122 -0
- pyoq/policies/governed.py +430 -0
- pyoq/policies/model.py +242 -0
- pyoq/policies/rewriting.py +263 -0
- pyoq/py.typed +1 -0
- pyoq/query/__init__.py +312 -0
- pyoq/query/aggregates.py +172 -0
- pyoq/query/arrays.py +65 -0
- pyoq/query/binding.py +52 -0
- pyoq/query/capabilities.py +317 -0
- pyoq/query/casts.py +73 -0
- pyoq/query/choices.py +185 -0
- pyoq/query/decoding.py +360 -0
- pyoq/query/documents.py +56 -0
- pyoq/query/execution/__init__.py +63 -0
- pyoq/query/execution/aio/__init__.py +31 -0
- pyoq/query/execution/aio/operations.py +228 -0
- pyoq/query/execution/aio/pooling.py +233 -0
- pyoq/query/execution/aio/streaming.py +161 -0
- pyoq/query/execution/aio/transactions.py +105 -0
- pyoq/query/execution/batch.py +96 -0
- pyoq/query/execution/binding_style.py +30 -0
- pyoq/query/execution/compilation.py +48 -0
- pyoq/query/execution/context.py +61 -0
- pyoq/query/execution/control.py +50 -0
- pyoq/query/execution/operations.py +224 -0
- pyoq/query/execution/planning.py +107 -0
- pyoq/query/execution/pooling.py +279 -0
- pyoq/query/execution/results.py +36 -0
- pyoq/query/execution/streaming.py +178 -0
- pyoq/query/execution/transactions.py +95 -0
- pyoq/query/expressions.py +1200 -0
- pyoq/query/fields.py +60 -0
- pyoq/query/mysql/__init__.py +59 -0
- pyoq/query/mysql/aio/__init__.py +38 -0
- pyoq/query/mysql/aio/commands.py +389 -0
- pyoq/query/mysql/aio/driver.py +196 -0
- pyoq/query/mysql/aio/executor.py +123 -0
- pyoq/query/mysql/aio/factory.py +26 -0
- pyoq/query/mysql/aio/operations.py +38 -0
- pyoq/query/mysql/aio/pool.py +53 -0
- pyoq/query/mysql/aio/transactions.py +313 -0
- pyoq/query/mysql/commands.py +354 -0
- pyoq/query/mysql/compiler.py +134 -0
- pyoq/query/mysql/context.py +20 -0
- pyoq/query/mysql/executor.py +126 -0
- pyoq/query/mysql/expressions.py +244 -0
- pyoq/query/mysql/factory.py +46 -0
- pyoq/query/mysql/health.py +66 -0
- pyoq/query/mysql/identifiers.py +9 -0
- pyoq/query/mysql/model.py +79 -0
- pyoq/query/mysql/operations.py +43 -0
- pyoq/query/mysql/parameters.py +69 -0
- pyoq/query/mysql/planning.py +20 -0
- pyoq/query/mysql/pool.py +67 -0
- pyoq/query/mysql/transactions.py +331 -0
- pyoq/query/mysql/writes.py +73 -0
- pyoq/query/nodes.py +750 -0
- pyoq/query/postgres/__init__.py +48 -0
- pyoq/query/postgres/aio/__init__.py +25 -0
- pyoq/query/postgres/aio/bulk.py +56 -0
- pyoq/query/postgres/aio/commands.py +264 -0
- pyoq/query/postgres/aio/executor.py +152 -0
- pyoq/query/postgres/aio/factory.py +26 -0
- pyoq/query/postgres/aio/operations.py +26 -0
- pyoq/query/postgres/aio/pool.py +40 -0
- pyoq/query/postgres/aio/transactions.py +295 -0
- pyoq/query/postgres/bulk.py +62 -0
- pyoq/query/postgres/commands.py +238 -0
- pyoq/query/postgres/compiler.py +114 -0
- pyoq/query/postgres/context.py +20 -0
- pyoq/query/postgres/executor.py +147 -0
- pyoq/query/postgres/expressions.py +311 -0
- pyoq/query/postgres/factory.py +24 -0
- pyoq/query/postgres/health.py +24 -0
- pyoq/query/postgres/identifiers.py +9 -0
- pyoq/query/postgres/model.py +81 -0
- pyoq/query/postgres/operations.py +25 -0
- pyoq/query/postgres/parameters.py +71 -0
- pyoq/query/postgres/planning.py +20 -0
- pyoq/query/postgres/pool.py +52 -0
- pyoq/query/postgres/transactions.py +295 -0
- pyoq/query/postgres/writes.py +37 -0
- pyoq/query/projections.py +105 -0
- pyoq/query/raw.py +90 -0
- pyoq/query/recursion.py +265 -0
- pyoq/query/rendering/__init__.py +1 -0
- pyoq/query/rendering/expressions.py +913 -0
- pyoq/query/rendering/identifiers.py +40 -0
- pyoq/query/rendering/projections.py +63 -0
- pyoq/query/rendering/queries.py +334 -0
- pyoq/query/rendering/sources.py +66 -0
- pyoq/query/rendering/writes.py +176 -0
- pyoq/query/results.py +459 -0
- pyoq/query/routines.py +196 -0
- pyoq/query/rows.py +156 -0
- pyoq/query/select.py +793 -0
- pyoq/query/select_nodes.py +277 -0
- pyoq/query/sources.py +236 -0
- pyoq/query/sqlite/__init__.py +43 -0
- pyoq/query/sqlite/commands.py +201 -0
- pyoq/query/sqlite/compiler.py +139 -0
- pyoq/query/sqlite/context.py +20 -0
- pyoq/query/sqlite/executor.py +119 -0
- pyoq/query/sqlite/expressions.py +224 -0
- pyoq/query/sqlite/factory.py +32 -0
- pyoq/query/sqlite/health.py +28 -0
- pyoq/query/sqlite/identifiers.py +9 -0
- pyoq/query/sqlite/model.py +73 -0
- pyoq/query/sqlite/operations.py +36 -0
- pyoq/query/sqlite/parameters.py +50 -0
- pyoq/query/sqlite/planning.py +20 -0
- pyoq/query/sqlite/pool.py +50 -0
- pyoq/query/sqlite/streaming.py +13 -0
- pyoq/query/sqlite/transactions.py +274 -0
- pyoq/query/sqlite/writes.py +35 -0
- pyoq/query/statements.py +27 -0
- pyoq/query/values.py +23 -0
- pyoq/query/vendor.py +162 -0
- pyoq/query/windows.py +424 -0
- pyoq/query/write_nodes.py +174 -0
- pyoq/query/writes.py +628 -0
- pyoq/relations/__init__.py +66 -0
- pyoq/relations/batching.py +219 -0
- pyoq/relations/derivation.py +111 -0
- pyoq/relations/fetching.py +355 -0
- pyoq/relations/graph.py +245 -0
- pyoq/relations/loading.py +74 -0
- pyoq/relations/model.py +75 -0
- pyoq/relations/planning.py +206 -0
- pyoq/runtime/__init__.py +9 -0
- pyoq/runtime/kernels.py +25 -0
- pyoq/runtime/python.py +43 -0
- pyoq/runtime/selection.py +73 -0
- pyoq/sanic/__init__.py +32 -0
- pyoq/sanic/scope.py +197 -0
- pyoq/sanic/workers.py +129 -0
- pyoq/schema/__init__.py +108 -0
- pyoq/schema/codec.py +711 -0
- pyoq/schema/models.py +604 -0
- pyoq/schema/mysql/__init__.py +16 -0
- pyoq/schema/mysql/connection.py +73 -0
- pyoq/schema/mysql/dsn.py +72 -0
- pyoq/schema/mysql/records.py +354 -0
- pyoq/schema/mysql/reflection.py +309 -0
- pyoq/schema/mysql/source.py +30 -0
- pyoq/schema/mysql/sql.py +128 -0
- pyoq/schema/mysql/types.py +105 -0
- pyoq/schema/postgres/__init__.py +13 -0
- pyoq/schema/postgres/connection.py +63 -0
- pyoq/schema/postgres/records.py +384 -0
- pyoq/schema/postgres/reflection.py +466 -0
- pyoq/schema/postgres/source.py +30 -0
- pyoq/schema/postgres/sql.py +246 -0
- pyoq/schema/postgres/types.py +98 -0
- pyoq/schema/registry.py +45 -0
- pyoq/schema/source.py +15 -0
- pyoq/schema/sqlite/__init__.py +6 -0
- pyoq/schema/sqlite/connection.py +54 -0
- pyoq/schema/sqlite/records.py +167 -0
- pyoq/schema/sqlite/reflection.py +393 -0
- pyoq/schema/sqlite/source.py +30 -0
- pyoq/schema/sqlite/sql.py +254 -0
- pyoq/schema/sqlite/types.py +74 -0
- pyoq/serving/__init__.py +23 -0
- pyoq/serving/databases.py +107 -0
- pyoq/serving/opening.py +331 -0
- pyoq/snapshots/__init__.py +20 -0
- pyoq/snapshots/drift.py +312 -0
- pyoq/snapshots/files.py +96 -0
- pyoq/snapshots/routing.py +40 -0
- pyoq/snapshots/source.py +33 -0
- pyoq/tracing/__init__.py +5 -0
- pyoq/tracing/spans.py +89 -0
- pyoq/unset.py +14 -0
- pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
- pyoq_sql-1.0.2.dist-info/RECORD +267 -0
- pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
- pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
- 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")
|