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,79 @@
|
|
|
1
|
+
"""MySQL compilation capabilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from pyoq.errors import QueryValidationError
|
|
8
|
+
from pyoq.query.execution import ParameterStyle
|
|
9
|
+
from pyoq.relations.planning import FetchCapabilities
|
|
10
|
+
|
|
11
|
+
_PLACEHOLDER_LIMIT = 65535
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class MySQLCapabilities:
|
|
16
|
+
maximum_parameters: int = _PLACEHOLDER_LIMIT
|
|
17
|
+
parameter_style: ParameterStyle = ParameterStyle.FORMAT
|
|
18
|
+
right_join: bool = True
|
|
19
|
+
full_join: bool = False
|
|
20
|
+
lateral_sources: bool = True
|
|
21
|
+
recursive_common_tables: bool = True
|
|
22
|
+
raw_expressions: bool = True
|
|
23
|
+
# MySQL refuses ORDER BY inside JSON_ARRAYAGG, and ordering in a
|
|
24
|
+
# derived table is discarded rather than honoured.
|
|
25
|
+
nested_collections: bool = True
|
|
26
|
+
ordered_nested_collections: bool = False
|
|
27
|
+
returning: bool = False
|
|
28
|
+
upsert: bool = True
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def fetch(self) -> FetchCapabilities:
|
|
32
|
+
"""What this dialect can do when fetching a collection in one query."""
|
|
33
|
+
return FetchCapabilities(
|
|
34
|
+
self.nested_collections,
|
|
35
|
+
self.ordered_nested_collections,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def __post_init__(self) -> None:
|
|
39
|
+
if not _is_positive_integer(self.maximum_parameters):
|
|
40
|
+
message = "MySQL maximum parameters must be a positive integer"
|
|
41
|
+
raise QueryValidationError(message)
|
|
42
|
+
if self.maximum_parameters > _PLACEHOLDER_LIMIT:
|
|
43
|
+
message = (
|
|
44
|
+
"MySQL cannot bind more than "
|
|
45
|
+
f"{_PLACEHOLDER_LIMIT} placeholders in one statement"
|
|
46
|
+
)
|
|
47
|
+
raise QueryValidationError(message)
|
|
48
|
+
if not _is_parameter_style(self.parameter_style):
|
|
49
|
+
message = "MySQL parameter style must be a ParameterStyle value"
|
|
50
|
+
raise QueryValidationError(message)
|
|
51
|
+
feature_flags: tuple[object, ...] = (
|
|
52
|
+
self.right_join,
|
|
53
|
+
self.full_join,
|
|
54
|
+
self.lateral_sources,
|
|
55
|
+
self.recursive_common_tables,
|
|
56
|
+
self.raw_expressions,
|
|
57
|
+
self.nested_collections,
|
|
58
|
+
self.ordered_nested_collections,
|
|
59
|
+
self.returning,
|
|
60
|
+
self.upsert,
|
|
61
|
+
)
|
|
62
|
+
if not _all_boolean(feature_flags):
|
|
63
|
+
message = "MySQL capability flags must be boolean"
|
|
64
|
+
raise QueryValidationError(message)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _is_parameter_style(value: object) -> bool:
|
|
68
|
+
return isinstance(value, ParameterStyle)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _is_positive_integer(value: object) -> bool:
|
|
72
|
+
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _all_boolean(values: tuple[object, ...]) -> bool:
|
|
76
|
+
return all(isinstance(value, bool) for value in values)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ("MySQLCapabilities",)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""MySQL typed result operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from pymysql.cursors import Cursor
|
|
8
|
+
|
|
9
|
+
from pyoq.query.execution import (
|
|
10
|
+
BulkPlan,
|
|
11
|
+
DatabaseCursor,
|
|
12
|
+
QueryOperations,
|
|
13
|
+
WriteProvider,
|
|
14
|
+
)
|
|
15
|
+
from pyoq.query.mysql.compiler import MySQLCompiler
|
|
16
|
+
from pyoq.query.mysql.planning import MySQLBulkPlanner
|
|
17
|
+
from pyoq.query.write_nodes import WriteNode
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MySQLQueryOperations(QueryOperations):
|
|
21
|
+
__slots__ = ("_compiler",)
|
|
22
|
+
|
|
23
|
+
def __init__(self, compiler: MySQLCompiler) -> None:
|
|
24
|
+
self._compiler = compiler
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def compiler(self) -> MySQLCompiler:
|
|
28
|
+
return self._compiler
|
|
29
|
+
|
|
30
|
+
def plan_bulk(self, statement: WriteProvider | WriteNode, /) -> BulkPlan:
|
|
31
|
+
return MySQLBulkPlanner(self.compiler).plan(statement)
|
|
32
|
+
|
|
33
|
+
def last_inserted_id(self, cursor: DatabaseCursor) -> int | None:
|
|
34
|
+
"""Report the first identifier a multi-row insert generated.
|
|
35
|
+
|
|
36
|
+
MySQL reports the identifier of the first row an insert generated, not
|
|
37
|
+
the last, and reports zero when a statement generated none.
|
|
38
|
+
"""
|
|
39
|
+
identifier = cast("Cursor", cursor).lastrowid
|
|
40
|
+
return identifier if identifier > 0 else None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = ("MySQLQueryOperations",)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""MySQL driver parameter adaptation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import date, datetime, time, timedelta
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import cast
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from pyoq.errors import ParameterBindingError
|
|
13
|
+
|
|
14
|
+
MySQLParameter = (
|
|
15
|
+
str
|
|
16
|
+
| int
|
|
17
|
+
| float
|
|
18
|
+
| bool
|
|
19
|
+
| bytes
|
|
20
|
+
| Decimal
|
|
21
|
+
| date
|
|
22
|
+
| datetime
|
|
23
|
+
| time
|
|
24
|
+
| timedelta
|
|
25
|
+
| None
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_NATIVE_TYPES = (
|
|
29
|
+
str,
|
|
30
|
+
int,
|
|
31
|
+
float,
|
|
32
|
+
bool,
|
|
33
|
+
bytes,
|
|
34
|
+
Decimal,
|
|
35
|
+
datetime,
|
|
36
|
+
date,
|
|
37
|
+
time,
|
|
38
|
+
timedelta,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def adapt_parameters(values: tuple[object, ...]) -> tuple[MySQLParameter, ...]:
|
|
43
|
+
return tuple(adapt_parameter(value) for value in values)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def adapt_parameter(value: object) -> MySQLParameter:
|
|
47
|
+
if isinstance(value, Enum):
|
|
48
|
+
return adapt_parameter(value.value)
|
|
49
|
+
if value is None or isinstance(value, _NATIVE_TYPES):
|
|
50
|
+
return value
|
|
51
|
+
if isinstance(value, UUID):
|
|
52
|
+
return str(value)
|
|
53
|
+
if isinstance(value, memoryview):
|
|
54
|
+
return value.tobytes()
|
|
55
|
+
if isinstance(value, (list, tuple, dict)):
|
|
56
|
+
return _json_parameter(cast("object", value))
|
|
57
|
+
message = f"unsupported MySQL parameter type: {type(value).__name__}"
|
|
58
|
+
raise ParameterBindingError(message)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _json_parameter(value: object) -> str:
|
|
62
|
+
try:
|
|
63
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
64
|
+
except (TypeError, ValueError) as error:
|
|
65
|
+
message = "MySQL JSON parameter is not serializable"
|
|
66
|
+
raise ParameterBindingError(message) from error
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = ("MySQLParameter", "adapt_parameter", "adapt_parameters")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""MySQL bulk write planning."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pyoq.query.execution.planning import BulkPlanner
|
|
6
|
+
from pyoq.query.mysql.compiler import MySQLCompiler
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MySQLBulkPlanner(BulkPlanner):
|
|
10
|
+
__slots__ = ()
|
|
11
|
+
|
|
12
|
+
def __init__(self, compiler: MySQLCompiler) -> None:
|
|
13
|
+
super().__init__(
|
|
14
|
+
compiler,
|
|
15
|
+
compiler.capabilities.maximum_parameters,
|
|
16
|
+
"MySQL",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ("MySQLBulkPlanner",)
|
pyoq/query/mysql/pool.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Exclusive synchronous MySQL connection pool."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import TypeAlias, cast
|
|
7
|
+
|
|
8
|
+
import pymysql
|
|
9
|
+
import pymysql.connections
|
|
10
|
+
|
|
11
|
+
from pyoq.errors import ConnectionPoolError
|
|
12
|
+
from pyoq.query.execution.pooling import (
|
|
13
|
+
ConnectionLease,
|
|
14
|
+
ConnectionPool,
|
|
15
|
+
PoolPolicy,
|
|
16
|
+
PoolStats,
|
|
17
|
+
)
|
|
18
|
+
from pyoq.query.mysql.commands import Connection
|
|
19
|
+
|
|
20
|
+
MySQLConnectionFactoryProtocol: TypeAlias = "Callable[[], Connection]"
|
|
21
|
+
MySQLConnectionLease: TypeAlias = ConnectionLease[Connection]
|
|
22
|
+
MySQLPoolPolicy: TypeAlias = PoolPolicy
|
|
23
|
+
MySQLPoolStats: TypeAlias = PoolStats
|
|
24
|
+
|
|
25
|
+
_RESET_TIMEOUT = "SET @@max_execution_time = 0"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MySQLPool(ConnectionPool[Connection]):
|
|
29
|
+
__slots__ = ()
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def connection_factory(self) -> MySQLConnectionFactoryProtocol:
|
|
33
|
+
return self._factory
|
|
34
|
+
|
|
35
|
+
def reset(self, connection: Connection) -> bool:
|
|
36
|
+
try:
|
|
37
|
+
connection.rollback()
|
|
38
|
+
with connection.cursor() as cursor:
|
|
39
|
+
cursor.execute(_RESET_TIMEOUT, ())
|
|
40
|
+
except pymysql.Error:
|
|
41
|
+
return False
|
|
42
|
+
return connection.open
|
|
43
|
+
|
|
44
|
+
def validate(self, value: object) -> Connection:
|
|
45
|
+
if not isinstance(value, pymysql.connections.Connection):
|
|
46
|
+
message = "MySQL connection factory returned an invalid connection"
|
|
47
|
+
raise ConnectionPoolError(message)
|
|
48
|
+
return cast("Connection", value)
|
|
49
|
+
|
|
50
|
+
def dispose(self, connection: Connection) -> None:
|
|
51
|
+
"""Close a connection the driver has not already closed itself.
|
|
52
|
+
|
|
53
|
+
PyMySQL refuses a second close, and it force-closes the socket itself
|
|
54
|
+
after a fatal error, so an open socket is the only thing left to send a
|
|
55
|
+
quit message on.
|
|
56
|
+
"""
|
|
57
|
+
if connection.open:
|
|
58
|
+
connection.close()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = (
|
|
62
|
+
"MySQLConnectionFactoryProtocol",
|
|
63
|
+
"MySQLConnectionLease",
|
|
64
|
+
"MySQLPool",
|
|
65
|
+
"MySQLPoolPolicy",
|
|
66
|
+
"MySQLPoolStats",
|
|
67
|
+
)
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Owned MySQL transactions and nested savepoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import TypeVar, cast
|
|
8
|
+
|
|
9
|
+
import pymysql
|
|
10
|
+
from pymysql.cursors import Cursor
|
|
11
|
+
|
|
12
|
+
from pyoq.errors import ExecutionError, QueryValidationError, TransactionStateError
|
|
13
|
+
from pyoq.query.execution import (
|
|
14
|
+
CompiledQuery,
|
|
15
|
+
ExecutionControl,
|
|
16
|
+
RowStream,
|
|
17
|
+
StatementKind,
|
|
18
|
+
StreamPolicy,
|
|
19
|
+
StreamResource,
|
|
20
|
+
TransactionState,
|
|
21
|
+
)
|
|
22
|
+
from pyoq.query.mysql.commands import (
|
|
23
|
+
Connection,
|
|
24
|
+
ExecutionEnvironment,
|
|
25
|
+
FailureHandler,
|
|
26
|
+
open_cursor,
|
|
27
|
+
run_sql,
|
|
28
|
+
)
|
|
29
|
+
from pyoq.query.mysql.compiler import MySQLCompiler
|
|
30
|
+
from pyoq.query.mysql.operations import MySQLQueryOperations
|
|
31
|
+
from pyoq.query.mysql.parameters import adapt_parameters
|
|
32
|
+
from pyoq.query.mysql.pool import MySQLPool
|
|
33
|
+
from pyoq.query.select import Query
|
|
34
|
+
|
|
35
|
+
OperationResult = TypeVar("OperationResult")
|
|
36
|
+
ResultRow = TypeVar("ResultRow")
|
|
37
|
+
|
|
38
|
+
_ISOLATION_STATEMENTS = {
|
|
39
|
+
"READ_COMMITTED": "SET TRANSACTION ISOLATION LEVEL READ COMMITTED",
|
|
40
|
+
"REPEATABLE_READ": "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ",
|
|
41
|
+
"SERIALIZABLE": "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE",
|
|
42
|
+
"READ_ONLY": "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ",
|
|
43
|
+
}
|
|
44
|
+
_READ_ONLY_START = "START TRANSACTION READ ONLY"
|
|
45
|
+
_START = "START TRANSACTION"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TransactionMode(Enum):
|
|
49
|
+
"""MySQL sets isolation before starting, not as part of starting."""
|
|
50
|
+
|
|
51
|
+
READ_COMMITTED = "READ_COMMITTED"
|
|
52
|
+
REPEATABLE_READ = "REPEATABLE_READ"
|
|
53
|
+
SERIALIZABLE = "SERIALIZABLE"
|
|
54
|
+
READ_ONLY = "READ_ONLY"
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def isolation_sql(self) -> str:
|
|
58
|
+
return _ISOLATION_STATEMENTS[self.value]
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def start_sql(self) -> str:
|
|
62
|
+
return _READ_ONLY_START if self is TransactionMode.READ_ONLY else _START
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MySQLTransactionState(TransactionState[Connection, "MySQLTransaction"]):
|
|
66
|
+
"""Adds the one transaction fact MySQL reports that other dialects do not.
|
|
67
|
+
|
|
68
|
+
A deadlock rolls the whole transaction back on the server while leaving the
|
|
69
|
+
connection healthy, and a later ``COMMIT`` then succeeds having committed
|
|
70
|
+
nothing.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
__slots__ = ("server_rolled_back",)
|
|
74
|
+
|
|
75
|
+
def __init__(self) -> None:
|
|
76
|
+
super().__init__()
|
|
77
|
+
self.server_rolled_back = False
|
|
78
|
+
|
|
79
|
+
def mark_server_rollback(self) -> None:
|
|
80
|
+
self.server_rolled_back = True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class MySQLTransaction(MySQLQueryOperations):
|
|
84
|
+
__slots__ = (
|
|
85
|
+
"_control",
|
|
86
|
+
"_entered",
|
|
87
|
+
"_finished",
|
|
88
|
+
"_mode",
|
|
89
|
+
"_parent",
|
|
90
|
+
"_pool",
|
|
91
|
+
"_savepoint_name",
|
|
92
|
+
"_state",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
pool: MySQLPool,
|
|
98
|
+
compiler: MySQLCompiler,
|
|
99
|
+
mode: TransactionMode = TransactionMode.REPEATABLE_READ,
|
|
100
|
+
control: ExecutionControl | None = None,
|
|
101
|
+
*,
|
|
102
|
+
parent: MySQLTransaction | None = None,
|
|
103
|
+
state: MySQLTransactionState | None = None,
|
|
104
|
+
) -> None:
|
|
105
|
+
super().__init__(compiler)
|
|
106
|
+
if not _is_transaction_mode(mode):
|
|
107
|
+
raise QueryValidationError("MySQL transaction mode is invalid")
|
|
108
|
+
self._pool = pool
|
|
109
|
+
self._mode = mode
|
|
110
|
+
self._control = control
|
|
111
|
+
self._parent = parent
|
|
112
|
+
self._state = state or MySQLTransactionState()
|
|
113
|
+
self._savepoint_name: str | None = None
|
|
114
|
+
self._entered = False
|
|
115
|
+
self._finished = False
|
|
116
|
+
|
|
117
|
+
def __enter__(self) -> MySQLTransaction:
|
|
118
|
+
if self._entered or self._finished:
|
|
119
|
+
raise TransactionStateError("transaction cannot be entered twice")
|
|
120
|
+
if self._parent is None:
|
|
121
|
+
self._enter_outer()
|
|
122
|
+
else:
|
|
123
|
+
self._enter_savepoint()
|
|
124
|
+
self._entered = True
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def __exit__(
|
|
128
|
+
self,
|
|
129
|
+
exception_type: type[BaseException] | None,
|
|
130
|
+
exception: BaseException | None,
|
|
131
|
+
traceback: object,
|
|
132
|
+
) -> None:
|
|
133
|
+
if not self._entered or self._finished:
|
|
134
|
+
return
|
|
135
|
+
if self._parent is None:
|
|
136
|
+
self._exit_outer(exception_type)
|
|
137
|
+
else:
|
|
138
|
+
self._exit_savepoint(exception_type)
|
|
139
|
+
|
|
140
|
+
def savepoint(
|
|
141
|
+
self,
|
|
142
|
+
*,
|
|
143
|
+
control: ExecutionControl | None = None,
|
|
144
|
+
) -> MySQLTransaction:
|
|
145
|
+
self._state.require_current(self)
|
|
146
|
+
return MySQLTransaction(
|
|
147
|
+
self._pool,
|
|
148
|
+
self.compiler,
|
|
149
|
+
self._mode,
|
|
150
|
+
control or self._control,
|
|
151
|
+
parent=self,
|
|
152
|
+
state=self._state,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def stream(
|
|
156
|
+
self,
|
|
157
|
+
query: Query[ResultRow],
|
|
158
|
+
/,
|
|
159
|
+
*,
|
|
160
|
+
policy: StreamPolicy | None = None,
|
|
161
|
+
) -> RowStream[ResultRow]:
|
|
162
|
+
self._state.require_current(self)
|
|
163
|
+
selected = policy or StreamPolicy(control=self._control)
|
|
164
|
+
if selected.control is None and self._control is not None:
|
|
165
|
+
selected = StreamPolicy(selected.batch_size, self._control)
|
|
166
|
+
statement = self.compiler.compile(query)
|
|
167
|
+
stream: RowStream[ResultRow]
|
|
168
|
+
stream = RowStream(
|
|
169
|
+
lambda: self._open_stream(stream, statement, selected),
|
|
170
|
+
selected,
|
|
171
|
+
statement.row_types,
|
|
172
|
+
)
|
|
173
|
+
return stream
|
|
174
|
+
|
|
175
|
+
def _environment(self, connection: Connection) -> ExecutionEnvironment:
|
|
176
|
+
return ExecutionEnvironment(
|
|
177
|
+
connection,
|
|
178
|
+
self._pool.connection_factory,
|
|
179
|
+
FailureHandler(self._state.invalidate, self._state.mark_server_rollback),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def _run(
|
|
183
|
+
self,
|
|
184
|
+
statement: CompiledQuery,
|
|
185
|
+
operation: Callable[[Cursor], OperationResult],
|
|
186
|
+
control: ExecutionControl | None,
|
|
187
|
+
) -> OperationResult:
|
|
188
|
+
parameters = adapt_parameters(statement.parameters)
|
|
189
|
+
connection = self._state.require_current(self)
|
|
190
|
+
return run_sql(
|
|
191
|
+
self._environment(connection),
|
|
192
|
+
statement.sql,
|
|
193
|
+
parameters,
|
|
194
|
+
operation,
|
|
195
|
+
control or self._control,
|
|
196
|
+
one_result_set=statement.statement_kind is StatementKind.CALL,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def _enter_outer(self) -> None:
|
|
200
|
+
self._state.adopt(self._pool.checkout(), self)
|
|
201
|
+
try:
|
|
202
|
+
self._run_transaction_sql(self._mode.isolation_sql)
|
|
203
|
+
self._run_transaction_sql(self._mode.start_sql)
|
|
204
|
+
except BaseException:
|
|
205
|
+
self._release_outer()
|
|
206
|
+
raise
|
|
207
|
+
|
|
208
|
+
def _enter_savepoint(self) -> None:
|
|
209
|
+
self._state.require_current(cast("MySQLTransaction", self._parent))
|
|
210
|
+
name = self._state.next_savepoint()
|
|
211
|
+
self._run_transaction_sql(f"SAVEPOINT `{name}`", scope=self._parent)
|
|
212
|
+
self._savepoint_name = name
|
|
213
|
+
self._state.current_scope = self
|
|
214
|
+
|
|
215
|
+
def _exit_outer(self, exception_type: type[BaseException] | None) -> None:
|
|
216
|
+
try:
|
|
217
|
+
self._state.require_scope(self)
|
|
218
|
+
except TransactionStateError:
|
|
219
|
+
self._abort_misnested_outer(exception_type)
|
|
220
|
+
return
|
|
221
|
+
self._state.close_stream()
|
|
222
|
+
try:
|
|
223
|
+
self._finish_outer(exception_type)
|
|
224
|
+
except ExecutionError:
|
|
225
|
+
self._state.invalidate()
|
|
226
|
+
if exception_type is None:
|
|
227
|
+
raise
|
|
228
|
+
finally:
|
|
229
|
+
self._release_outer()
|
|
230
|
+
|
|
231
|
+
def _finish_outer(self, exception_type: type[BaseException] | None) -> None:
|
|
232
|
+
if self._state.server_rolled_back:
|
|
233
|
+
if exception_type is None:
|
|
234
|
+
message = (
|
|
235
|
+
"MySQL rolled this transaction back on the server; its work "
|
|
236
|
+
"was discarded and it cannot be committed"
|
|
237
|
+
)
|
|
238
|
+
raise TransactionStateError(message)
|
|
239
|
+
return
|
|
240
|
+
if self._state.broken:
|
|
241
|
+
return
|
|
242
|
+
self._run_transaction_sql("ROLLBACK" if exception_type else "COMMIT")
|
|
243
|
+
|
|
244
|
+
def _exit_savepoint(self, exception_type: type[BaseException] | None) -> None:
|
|
245
|
+
self._state.require_scope(self)
|
|
246
|
+
self._state.close_stream()
|
|
247
|
+
name = cast("str", self._savepoint_name)
|
|
248
|
+
try:
|
|
249
|
+
self._finish_savepoint(name, exception_type)
|
|
250
|
+
except ExecutionError:
|
|
251
|
+
self._state.invalidate()
|
|
252
|
+
if exception_type is None:
|
|
253
|
+
raise
|
|
254
|
+
finally:
|
|
255
|
+
self._state.current_scope = self._parent
|
|
256
|
+
self._finished = True
|
|
257
|
+
|
|
258
|
+
def _finish_savepoint(
|
|
259
|
+
self,
|
|
260
|
+
name: str,
|
|
261
|
+
exception_type: type[BaseException] | None,
|
|
262
|
+
) -> None:
|
|
263
|
+
if self._state.broken or self._state.server_rolled_back:
|
|
264
|
+
return
|
|
265
|
+
if exception_type is not None:
|
|
266
|
+
self._run_transaction_sql(f"ROLLBACK TO SAVEPOINT `{name}`")
|
|
267
|
+
self._run_transaction_sql(f"RELEASE SAVEPOINT `{name}`")
|
|
268
|
+
|
|
269
|
+
def _run_transaction_sql(
|
|
270
|
+
self,
|
|
271
|
+
sql: str,
|
|
272
|
+
*,
|
|
273
|
+
scope: MySQLTransaction | None = None,
|
|
274
|
+
) -> None:
|
|
275
|
+
connection = self._state.require_current(scope or self, allow_stream=True)
|
|
276
|
+
run_sql(
|
|
277
|
+
self._environment(connection),
|
|
278
|
+
sql,
|
|
279
|
+
(),
|
|
280
|
+
_discard_cursor,
|
|
281
|
+
self._control,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def _open_stream(
|
|
285
|
+
self,
|
|
286
|
+
stream: RowStream[ResultRow],
|
|
287
|
+
statement: CompiledQuery,
|
|
288
|
+
policy: StreamPolicy,
|
|
289
|
+
) -> StreamResource:
|
|
290
|
+
parameters = adapt_parameters(statement.parameters)
|
|
291
|
+
connection = self._state.require_current(self)
|
|
292
|
+
session = open_cursor(
|
|
293
|
+
self._environment(connection),
|
|
294
|
+
statement.sql,
|
|
295
|
+
parameters,
|
|
296
|
+
policy.control,
|
|
297
|
+
unbuffered=True,
|
|
298
|
+
)
|
|
299
|
+
self._state.active_stream = cast("RowStream[object]", stream)
|
|
300
|
+
return StreamResource(session, self._state.release_stream)
|
|
301
|
+
|
|
302
|
+
def _release_outer(self) -> None:
|
|
303
|
+
self._finished = True
|
|
304
|
+
self._state.release()
|
|
305
|
+
|
|
306
|
+
def _abort_misnested_outer(
|
|
307
|
+
self,
|
|
308
|
+
exception_type: type[BaseException] | None,
|
|
309
|
+
) -> None:
|
|
310
|
+
connection = self._state.require_owner()
|
|
311
|
+
self._state.close_stream()
|
|
312
|
+
try:
|
|
313
|
+
connection.rollback()
|
|
314
|
+
except pymysql.Error:
|
|
315
|
+
self._state.invalidate()
|
|
316
|
+
finally:
|
|
317
|
+
self._release_outer()
|
|
318
|
+
if exception_type is None:
|
|
319
|
+
message = "transaction exited with an active nested savepoint"
|
|
320
|
+
raise TransactionStateError(message)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _is_transaction_mode(value: object) -> bool:
|
|
324
|
+
return isinstance(value, TransactionMode)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _discard_cursor(cursor: Cursor) -> None:
|
|
328
|
+
del cursor
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
__all__ = ("MySQLTransaction", "MySQLTransactionState", "TransactionMode")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""MySQL write statement compilation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pyoq.errors import CompilationError, UnsupportedQueryError
|
|
6
|
+
from pyoq.query.mysql.context import MySQLCompilationContext
|
|
7
|
+
from pyoq.query.mysql.expressions import MySQLExpressionCompiler
|
|
8
|
+
from pyoq.query.mysql.identifiers import QUOTING
|
|
9
|
+
from pyoq.query.rendering.identifiers import IdentifierQuoting
|
|
10
|
+
from pyoq.query.rendering.writes import WriteRenderer
|
|
11
|
+
from pyoq.query.write_nodes import ConflictAction, ConflictNode, InsertNode
|
|
12
|
+
|
|
13
|
+
ROW_ALIAS = "new"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MySQLWriteCompilation(WriteRenderer):
|
|
17
|
+
__slots__ = ("_context",)
|
|
18
|
+
|
|
19
|
+
def __init__(self, context: MySQLCompilationContext) -> None:
|
|
20
|
+
self._context = context
|
|
21
|
+
super().__init__(MySQLExpressionCompiler(context))
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def quoting(self) -> IdentifierQuoting:
|
|
25
|
+
return QUOTING
|
|
26
|
+
|
|
27
|
+
def insert_row_alias(self, node: InsertNode) -> str | None:
|
|
28
|
+
if node.conflict is None:
|
|
29
|
+
return None
|
|
30
|
+
return ROW_ALIAS
|
|
31
|
+
|
|
32
|
+
def render_conflict(self, node: ConflictNode) -> str:
|
|
33
|
+
self._require_conflict_support(node)
|
|
34
|
+
assignments = node.assignments if node.action is ConflictAction.UPDATE else ()
|
|
35
|
+
if not assignments:
|
|
36
|
+
message = "MySQL conflict resolution requires an assignment"
|
|
37
|
+
raise CompilationError(message)
|
|
38
|
+
rendered = ", ".join(
|
|
39
|
+
self.compile_assignment(assignment) for assignment in assignments
|
|
40
|
+
)
|
|
41
|
+
return f"ON DUPLICATE KEY UPDATE {rendered}"
|
|
42
|
+
|
|
43
|
+
def require_returning(self) -> None:
|
|
44
|
+
message = (
|
|
45
|
+
"MySQL does not support RETURNING; read the written rows with a "
|
|
46
|
+
"separate query"
|
|
47
|
+
)
|
|
48
|
+
raise UnsupportedQueryError(message)
|
|
49
|
+
|
|
50
|
+
def require_conflict_resolution(self) -> None:
|
|
51
|
+
if not self._context.capabilities.upsert:
|
|
52
|
+
message = "MySQL conflict resolution is disabled by compiler capabilities"
|
|
53
|
+
raise UnsupportedQueryError(message)
|
|
54
|
+
|
|
55
|
+
def _require_conflict_support(self, node: ConflictNode) -> None:
|
|
56
|
+
self.require_conflict_resolution()
|
|
57
|
+
if node.targets:
|
|
58
|
+
message = (
|
|
59
|
+
"MySQL infers the conflicting key and cannot accept a conflict target"
|
|
60
|
+
)
|
|
61
|
+
raise UnsupportedQueryError(message)
|
|
62
|
+
if node.condition is not None:
|
|
63
|
+
message = "MySQL conflict resolution cannot filter with a condition"
|
|
64
|
+
raise UnsupportedQueryError(message)
|
|
65
|
+
if node.action is ConflictAction.NOTHING:
|
|
66
|
+
message = (
|
|
67
|
+
"MySQL has no safe do-nothing conflict resolution; assign the "
|
|
68
|
+
"conflicting columns explicitly"
|
|
69
|
+
)
|
|
70
|
+
raise UnsupportedQueryError(message)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = ("ROW_ALIAS", "MySQLWriteCompilation")
|