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
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
|
+
)
|