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,134 @@
|
|
|
1
|
+
"""MySQL query and statement compiler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pyoq.errors import UnsupportedQueryError
|
|
6
|
+
from pyoq.query.execution import CompiledQuery, StatementKind
|
|
7
|
+
from pyoq.query.mysql.context import MySQLCompilationContext
|
|
8
|
+
from pyoq.query.mysql.expressions import MySQLExpressionCompiler
|
|
9
|
+
from pyoq.query.mysql.identifiers import QUOTING
|
|
10
|
+
from pyoq.query.mysql.model import MySQLCapabilities
|
|
11
|
+
from pyoq.query.mysql.writes import MySQLWriteCompilation
|
|
12
|
+
from pyoq.query.rendering.identifiers import IdentifierQuoting
|
|
13
|
+
from pyoq.query.rendering.queries import QueryRenderer
|
|
14
|
+
from pyoq.query.select_nodes import (
|
|
15
|
+
JoinKind,
|
|
16
|
+
LockStrength,
|
|
17
|
+
NullOrder,
|
|
18
|
+
SelectNode,
|
|
19
|
+
SetNode,
|
|
20
|
+
)
|
|
21
|
+
from pyoq.query.statements import StatementNode, StatementProvider
|
|
22
|
+
from pyoq.query.write_nodes import CallNode, DeleteNode, InsertNode, UpdateNode
|
|
23
|
+
|
|
24
|
+
_STATEMENT_NODES = (
|
|
25
|
+
SelectNode,
|
|
26
|
+
SetNode,
|
|
27
|
+
InsertNode,
|
|
28
|
+
UpdateNode,
|
|
29
|
+
DeleteNode,
|
|
30
|
+
CallNode,
|
|
31
|
+
)
|
|
32
|
+
_UNBOUNDED_ROWS = 18446744073709551615
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MySQLCompiler:
|
|
36
|
+
__slots__ = ("_capabilities",)
|
|
37
|
+
|
|
38
|
+
def __init__(self, capabilities: MySQLCapabilities | None = None) -> None:
|
|
39
|
+
self._capabilities = capabilities or MySQLCapabilities()
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def capabilities(self) -> MySQLCapabilities:
|
|
43
|
+
return self._capabilities
|
|
44
|
+
|
|
45
|
+
def compile(
|
|
46
|
+
self,
|
|
47
|
+
statement: StatementProvider | StatementNode,
|
|
48
|
+
/,
|
|
49
|
+
) -> CompiledQuery:
|
|
50
|
+
node = statement if isinstance(statement, _STATEMENT_NODES) else statement.node
|
|
51
|
+
context = MySQLCompilationContext(self.capabilities)
|
|
52
|
+
if isinstance(node, CallNode):
|
|
53
|
+
sql = MySQLWriteCompilation(context).compile_call(node)
|
|
54
|
+
return context.result(sql, StatementKind.CALL, node)
|
|
55
|
+
if isinstance(node, (InsertNode, UpdateNode, DeleteNode)):
|
|
56
|
+
sql, statement_kind = MySQLWriteCompilation(context).compile(node)
|
|
57
|
+
return context.result(sql, statement_kind, node)
|
|
58
|
+
sql = _MySQLQueryCompilation(context).compile(node)
|
|
59
|
+
return context.result(sql, StatementKind.SELECT, node)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _MySQLQueryCompilation(QueryRenderer):
|
|
63
|
+
__slots__ = ("_context",)
|
|
64
|
+
|
|
65
|
+
def __init__(self, context: MySQLCompilationContext) -> None:
|
|
66
|
+
self._context = context
|
|
67
|
+
super().__init__(MySQLExpressionCompiler(context))
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def dialect(self) -> str:
|
|
71
|
+
return "MySQL"
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def quoting(self) -> IdentifierQuoting:
|
|
75
|
+
return QUOTING
|
|
76
|
+
|
|
77
|
+
def require_join_capability(self, kind: JoinKind) -> None:
|
|
78
|
+
if kind is JoinKind.RIGHT and not self._context.capabilities.right_join:
|
|
79
|
+
message = "MySQL RIGHT JOIN is disabled by compiler capabilities"
|
|
80
|
+
raise UnsupportedQueryError(message)
|
|
81
|
+
if kind is JoinKind.FULL and not self._context.capabilities.full_join:
|
|
82
|
+
message = "MySQL does not support FULL JOIN"
|
|
83
|
+
raise UnsupportedQueryError(message)
|
|
84
|
+
|
|
85
|
+
def lock_strength_sql(self, strength: LockStrength) -> str:
|
|
86
|
+
"""MySQL holds a row or shares it, and has no lock weaker than either."""
|
|
87
|
+
if strength in {LockStrength.NO_KEY_UPDATE, LockStrength.KEY_SHARE}:
|
|
88
|
+
message = (
|
|
89
|
+
f"MySQL has no {strength.value.replace('-', ' ')} lock; "
|
|
90
|
+
f"lock for update or for share instead"
|
|
91
|
+
)
|
|
92
|
+
raise UnsupportedQueryError(message)
|
|
93
|
+
return super().lock_strength_sql(strength)
|
|
94
|
+
|
|
95
|
+
def null_ordered(
|
|
96
|
+
self,
|
|
97
|
+
expression: str,
|
|
98
|
+
direction: str,
|
|
99
|
+
nulls: NullOrder,
|
|
100
|
+
) -> str:
|
|
101
|
+
"""MySQL has no NULLS clause, so it is told where nulls go by order.
|
|
102
|
+
|
|
103
|
+
It sorts nulls first in either direction, so ordering on whether a
|
|
104
|
+
value is null at all, before ordering on the value, puts them where
|
|
105
|
+
they were asked to go.
|
|
106
|
+
"""
|
|
107
|
+
nulls_first = "DESC" if nulls is NullOrder.FIRST else "ASC"
|
|
108
|
+
return f"{expression} IS NULL {nulls_first}, {expression} {direction}"
|
|
109
|
+
|
|
110
|
+
def require_recursive_common_tables(self) -> None:
|
|
111
|
+
if not self._context.capabilities.recursive_common_tables:
|
|
112
|
+
message = "MySQL recursive common tables are disabled"
|
|
113
|
+
raise UnsupportedQueryError(message)
|
|
114
|
+
|
|
115
|
+
def require_lateral_sources(self) -> None:
|
|
116
|
+
if not self._context.capabilities.lateral_sources:
|
|
117
|
+
message = "MySQL has no lateral sources"
|
|
118
|
+
raise UnsupportedQueryError(message)
|
|
119
|
+
|
|
120
|
+
def append_pagination(
|
|
121
|
+
self,
|
|
122
|
+
fragments: list[str],
|
|
123
|
+
limit: int | None,
|
|
124
|
+
offset: int | None,
|
|
125
|
+
) -> None:
|
|
126
|
+
if limit is not None:
|
|
127
|
+
fragments.append(f"LIMIT {self._context.bind_page_value(limit)}")
|
|
128
|
+
elif offset is not None:
|
|
129
|
+
fragments.append(f"LIMIT {_UNBOUNDED_ROWS}")
|
|
130
|
+
if offset is not None:
|
|
131
|
+
fragments.append(f"OFFSET {self._context.bind_page_value(offset)}")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
__all__ = ("MySQLCompiler",)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""MySQL compilation context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pyoq.query.execution import CompilationContext
|
|
6
|
+
from pyoq.query.mysql.model import MySQLCapabilities
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MySQLCompilationContext(CompilationContext):
|
|
10
|
+
__slots__ = ("capabilities",)
|
|
11
|
+
|
|
12
|
+
def __init__(self, capabilities: MySQLCapabilities) -> None:
|
|
13
|
+
super().__init__(
|
|
14
|
+
capabilities.maximum_parameters,
|
|
15
|
+
capabilities.parameter_style,
|
|
16
|
+
)
|
|
17
|
+
self.capabilities = capabilities
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ("MySQLCompilationContext",)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Synchronous typed MySQL query execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import TypeVar
|
|
7
|
+
|
|
8
|
+
from pymysql.cursors import Cursor
|
|
9
|
+
|
|
10
|
+
from pyoq.query.execution import (
|
|
11
|
+
CompiledQuery,
|
|
12
|
+
ExecutionControl,
|
|
13
|
+
RowStream,
|
|
14
|
+
StatementKind,
|
|
15
|
+
StreamPolicy,
|
|
16
|
+
StreamResource,
|
|
17
|
+
)
|
|
18
|
+
from pyoq.query.execution.pooling import ConnectionLease
|
|
19
|
+
from pyoq.query.mysql.commands import (
|
|
20
|
+
Connection,
|
|
21
|
+
ExecutionEnvironment,
|
|
22
|
+
FailureHandler,
|
|
23
|
+
open_cursor,
|
|
24
|
+
run_sql,
|
|
25
|
+
)
|
|
26
|
+
from pyoq.query.mysql.compiler import MySQLCompiler
|
|
27
|
+
from pyoq.query.mysql.operations import MySQLQueryOperations
|
|
28
|
+
from pyoq.query.mysql.parameters import adapt_parameters
|
|
29
|
+
from pyoq.query.mysql.pool import MySQLPool
|
|
30
|
+
from pyoq.query.mysql.transactions import MySQLTransaction, TransactionMode
|
|
31
|
+
from pyoq.query.select import Query
|
|
32
|
+
|
|
33
|
+
OperationResult = TypeVar("OperationResult")
|
|
34
|
+
ResultRow = TypeVar("ResultRow")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MySQLExecutor(MySQLQueryOperations):
|
|
38
|
+
__slots__ = ("_pool",)
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
pool: MySQLPool,
|
|
43
|
+
compiler: MySQLCompiler | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
self._pool = pool
|
|
46
|
+
super().__init__(compiler or MySQLCompiler())
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def pool(self) -> MySQLPool:
|
|
50
|
+
return self._pool
|
|
51
|
+
|
|
52
|
+
def stream(
|
|
53
|
+
self,
|
|
54
|
+
query: Query[ResultRow],
|
|
55
|
+
/,
|
|
56
|
+
*,
|
|
57
|
+
policy: StreamPolicy | None = None,
|
|
58
|
+
) -> RowStream[ResultRow]:
|
|
59
|
+
selected = policy or StreamPolicy()
|
|
60
|
+
statement = self.compiler.compile(query)
|
|
61
|
+
return RowStream(
|
|
62
|
+
lambda: self._open_stream(statement, selected),
|
|
63
|
+
selected,
|
|
64
|
+
statement.row_types,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def transaction(
|
|
68
|
+
self,
|
|
69
|
+
mode: TransactionMode = TransactionMode.REPEATABLE_READ,
|
|
70
|
+
*,
|
|
71
|
+
control: ExecutionControl | None = None,
|
|
72
|
+
) -> MySQLTransaction:
|
|
73
|
+
return MySQLTransaction(self.pool, self.compiler, mode, control)
|
|
74
|
+
|
|
75
|
+
def _environment(
|
|
76
|
+
self,
|
|
77
|
+
connection: Connection,
|
|
78
|
+
lease: ConnectionLease[Connection],
|
|
79
|
+
) -> ExecutionEnvironment:
|
|
80
|
+
return ExecutionEnvironment(
|
|
81
|
+
connection,
|
|
82
|
+
self.pool.connection_factory,
|
|
83
|
+
FailureHandler(lease.invalidate),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def _run(
|
|
87
|
+
self,
|
|
88
|
+
statement: CompiledQuery,
|
|
89
|
+
operation: Callable[[Cursor], OperationResult],
|
|
90
|
+
control: ExecutionControl | None,
|
|
91
|
+
) -> OperationResult:
|
|
92
|
+
parameters = adapt_parameters(statement.parameters)
|
|
93
|
+
lease = self.pool.checkout()
|
|
94
|
+
with lease as connection:
|
|
95
|
+
return run_sql(
|
|
96
|
+
self._environment(connection, lease),
|
|
97
|
+
statement.sql,
|
|
98
|
+
parameters,
|
|
99
|
+
operation,
|
|
100
|
+
control,
|
|
101
|
+
one_result_set=statement.statement_kind is StatementKind.CALL,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _open_stream(
|
|
105
|
+
self,
|
|
106
|
+
statement: CompiledQuery,
|
|
107
|
+
policy: StreamPolicy,
|
|
108
|
+
) -> StreamResource:
|
|
109
|
+
parameters = adapt_parameters(statement.parameters)
|
|
110
|
+
lease = self.pool.checkout()
|
|
111
|
+
connection = lease.__enter__()
|
|
112
|
+
try:
|
|
113
|
+
session = open_cursor(
|
|
114
|
+
self._environment(connection, lease),
|
|
115
|
+
statement.sql,
|
|
116
|
+
parameters,
|
|
117
|
+
policy.control,
|
|
118
|
+
unbuffered=True,
|
|
119
|
+
)
|
|
120
|
+
except BaseException:
|
|
121
|
+
lease.__exit__(None, None, None)
|
|
122
|
+
raise
|
|
123
|
+
return StreamResource(session, lambda: lease.__exit__(None, None, None))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
__all__ = ("MySQLExecutor",)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""MySQL scalar expression compilation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
7
|
+
from pyoq.config import DatabaseDialect
|
|
8
|
+
from pyoq.errors import UnsupportedQueryError
|
|
9
|
+
from pyoq.query.mysql.context import MySQLCompilationContext
|
|
10
|
+
from pyoq.query.mysql.identifiers import QUOTING
|
|
11
|
+
from pyoq.query.nodes import (
|
|
12
|
+
BinaryNode,
|
|
13
|
+
BinaryOperator,
|
|
14
|
+
DatePart,
|
|
15
|
+
ExtractNode,
|
|
16
|
+
FieldNode,
|
|
17
|
+
FrameKind,
|
|
18
|
+
FrameNode,
|
|
19
|
+
FunctionName,
|
|
20
|
+
FunctionNode,
|
|
21
|
+
JsonContainsNode,
|
|
22
|
+
JsonKeysNode,
|
|
23
|
+
JsonNode,
|
|
24
|
+
JsonOperation,
|
|
25
|
+
JsonWrite,
|
|
26
|
+
JsonWriteNode,
|
|
27
|
+
)
|
|
28
|
+
from pyoq.query.rendering.expressions import (
|
|
29
|
+
CollectionFunctions,
|
|
30
|
+
ExpressionRenderer,
|
|
31
|
+
)
|
|
32
|
+
from pyoq.query.rendering.identifiers import IdentifierQuoting
|
|
33
|
+
from pyoq.query.select_nodes import ELEMENT_COLUMN, JsonElementsSourceNode
|
|
34
|
+
from pyoq.schema.models import TypeKind
|
|
35
|
+
|
|
36
|
+
_EXCLUDED_ROW = "excluded"
|
|
37
|
+
_ROW_ALIAS = "new"
|
|
38
|
+
|
|
39
|
+
_DATE_FIELD = {
|
|
40
|
+
DatePart.YEAR: "YEAR",
|
|
41
|
+
DatePart.MONTH: "MONTH",
|
|
42
|
+
DatePart.DAY: "DAY",
|
|
43
|
+
DatePart.HOUR: "HOUR",
|
|
44
|
+
DatePart.MINUTE: "MINUTE",
|
|
45
|
+
DatePart.SECOND: "SECOND",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_MYSQL_CAST_SQL: Mapping[TypeKind, str] = {
|
|
50
|
+
TypeKind.SMALL_INTEGER: "SIGNED",
|
|
51
|
+
TypeKind.INTEGER: "SIGNED",
|
|
52
|
+
TypeKind.BIG_INTEGER: "SIGNED",
|
|
53
|
+
TypeKind.DECIMAL: "DECIMAL(65, 30)",
|
|
54
|
+
TypeKind.REAL: "FLOAT",
|
|
55
|
+
TypeKind.DOUBLE: "DOUBLE",
|
|
56
|
+
TypeKind.STRING: "CHAR",
|
|
57
|
+
TypeKind.BINARY: "BINARY",
|
|
58
|
+
TypeKind.DATE: "DATE",
|
|
59
|
+
TypeKind.TIME: "TIME",
|
|
60
|
+
TypeKind.DATETIME: "DATETIME",
|
|
61
|
+
TypeKind.JSON: "JSON",
|
|
62
|
+
}
|
|
63
|
+
"""MySQL casts to its own short list, and refuses the ANSI names outright.
|
|
64
|
+
|
|
65
|
+
Verified against a running server: `INTEGER`, `TEXT`, `VARCHAR`, `NUMERIC`,
|
|
66
|
+
`TIMESTAMP`, `INTERVAL`, and `UUID` are all syntax errors there, so the names
|
|
67
|
+
here are the ones it accepts rather than the ones it stores.
|
|
68
|
+
|
|
69
|
+
A bare `DECIMAL` means `DECIMAL(10, 0)` to MySQL, so casting `123.45` to one
|
|
70
|
+
answers `123`. The largest precision it has is used instead, because a cast
|
|
71
|
+
that quietly drops the fractional part of money is worse than no cast.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_MYSQL_JSON_WRITES: Mapping[JsonWrite, str] = {
|
|
76
|
+
JsonWrite.SET: "JSON_SET",
|
|
77
|
+
JsonWrite.INSERT: "JSON_INSERT",
|
|
78
|
+
JsonWrite.REPLACE: "JSON_REPLACE",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class MySQLExpressionCompiler(ExpressionRenderer[MySQLCompilationContext]):
|
|
83
|
+
__slots__ = ()
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def collection_functions(self) -> CollectionFunctions:
|
|
87
|
+
# Measured: MySQL rejects ORDER BY inside JSON_ARRAYAGG.
|
|
88
|
+
return CollectionFunctions(
|
|
89
|
+
"JSON_ARRAYAGG",
|
|
90
|
+
"JSON_OBJECT",
|
|
91
|
+
"JSON_ARRAY()",
|
|
92
|
+
"JSON_ARRAY",
|
|
93
|
+
"JSON_OBJECTAGG",
|
|
94
|
+
ordered=False,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def dialect(self) -> str:
|
|
99
|
+
return "MySQL"
|
|
100
|
+
|
|
101
|
+
def qualified_field(self, node: FieldNode) -> str:
|
|
102
|
+
table = _ROW_ALIAS if node.table == _EXCLUDED_ROW else node.table
|
|
103
|
+
schema = None if table == _ROW_ALIAS else node.schema
|
|
104
|
+
catalog = None if table == _ROW_ALIAS else node.catalog
|
|
105
|
+
return QUOTING.qualified(
|
|
106
|
+
node.name,
|
|
107
|
+
table=table,
|
|
108
|
+
schema=schema,
|
|
109
|
+
catalog=catalog,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def render_binary(self, node: BinaryNode, left: str, right: str) -> str:
|
|
113
|
+
if node.operator is BinaryOperator.CONCAT:
|
|
114
|
+
return f"CONCAT({left}, {right})"
|
|
115
|
+
if node.operator is BinaryOperator.IS_NOT_DISTINCT_FROM:
|
|
116
|
+
return f"({left} <=> {right})"
|
|
117
|
+
if node.operator is BinaryOperator.IS_DISTINCT_FROM:
|
|
118
|
+
return f"(NOT ({left} <=> {right}))"
|
|
119
|
+
return super().render_binary(node, left, right)
|
|
120
|
+
|
|
121
|
+
def validate_binary(self, node: BinaryNode) -> None:
|
|
122
|
+
del node
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def cast_type_names(self) -> Mapping[TypeKind, str]:
|
|
126
|
+
return _MYSQL_CAST_SQL
|
|
127
|
+
|
|
128
|
+
def render_json_elements(self, node: JsonElementsSourceNode) -> str:
|
|
129
|
+
"""MySQL expands an array only through a table whose columns it states."""
|
|
130
|
+
subject = self.compile(node.expression)
|
|
131
|
+
alias = QUOTING.quote(node.alias)
|
|
132
|
+
return (
|
|
133
|
+
f"JSON_TABLE({subject}, '$[*]' "
|
|
134
|
+
f"COLUMNS ({ELEMENT_COLUMN} JSON PATH '$')) AS {alias}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def render_json_keys(self, node: JsonKeysNode) -> str:
|
|
138
|
+
"""MySQL names the members of an object itself."""
|
|
139
|
+
return f"JSON_KEYS({self.compile(node.expression)})"
|
|
140
|
+
|
|
141
|
+
def render_json(self, node: JsonNode) -> str:
|
|
142
|
+
"""MySQL walks a written path, and refuses one written as a bare key."""
|
|
143
|
+
subject = self.compile(node.expression)
|
|
144
|
+
path = self.json_path_text(node.path)
|
|
145
|
+
if node.operation is JsonOperation.REMOVE:
|
|
146
|
+
return f"JSON_REMOVE({subject}, {path})"
|
|
147
|
+
if node.operation is JsonOperation.EXISTS:
|
|
148
|
+
return f"JSON_CONTAINS_PATH({subject}, 'one', {path})"
|
|
149
|
+
if node.operation is JsonOperation.LENGTH:
|
|
150
|
+
return f"JSON_LENGTH({subject}, {path})"
|
|
151
|
+
extracted = f"JSON_EXTRACT({subject}, {path})"
|
|
152
|
+
if node.operation is JsonOperation.TEXT:
|
|
153
|
+
return self._json_text(node, extracted)
|
|
154
|
+
return extracted
|
|
155
|
+
|
|
156
|
+
def _json_text(self, node: JsonNode, extracted: str) -> str:
|
|
157
|
+
"""A JSON null read as text is no text, the way the others read it.
|
|
158
|
+
|
|
159
|
+
MySQL unquotes a JSON null into the four letters of its name, so a
|
|
160
|
+
member holding null and a member holding "null" answer alike and both
|
|
161
|
+
differ from what PostgreSQL and SQLite answer. The value is asked for
|
|
162
|
+
a second time because each asking binds the path again.
|
|
163
|
+
"""
|
|
164
|
+
again = (
|
|
165
|
+
f"JSON_EXTRACT({self.compile(node.expression)}, "
|
|
166
|
+
f"{self.json_path_text(node.path)})"
|
|
167
|
+
)
|
|
168
|
+
return (
|
|
169
|
+
f"CASE WHEN JSON_TYPE({extracted}) = 'NULL' THEN NULL "
|
|
170
|
+
f"ELSE JSON_UNQUOTE({again}) END"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def render_json_write(self, node: JsonWriteNode) -> str:
|
|
174
|
+
"""Compiled in the order it is written, because binding follows that.
|
|
175
|
+
|
|
176
|
+
A value compiled before the path it is written at would take the
|
|
177
|
+
earlier placeholder, and the two would swap.
|
|
178
|
+
"""
|
|
179
|
+
subject = self.compile(node.expression)
|
|
180
|
+
if node.operation is JsonWrite.MERGE:
|
|
181
|
+
return f"JSON_MERGE_PATCH({subject}, {self.compile(node.value)})"
|
|
182
|
+
if node.operation is JsonWrite.CONCAT:
|
|
183
|
+
raise UnsupportedQueryError(_NO_CONCAT)
|
|
184
|
+
path = self.json_path_text(node.path)
|
|
185
|
+
value = self.compile(node.value)
|
|
186
|
+
return f"{_MYSQL_JSON_WRITES[node.operation]}({subject}, {path}, {value})"
|
|
187
|
+
|
|
188
|
+
def render_json_contains(self, node: JsonContainsNode) -> str:
|
|
189
|
+
subject = self.compile(node.expression)
|
|
190
|
+
return f"JSON_CONTAINS({subject}, {self.compile(node.value)})"
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def quoting(self) -> IdentifierQuoting:
|
|
194
|
+
return QUOTING
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def database_dialect(self) -> DatabaseDialect:
|
|
198
|
+
return DatabaseDialect.MYSQL
|
|
199
|
+
|
|
200
|
+
def require_frame(self, node: FrameNode) -> None:
|
|
201
|
+
"""MySQL counts rows and values, and has no peer groups to count."""
|
|
202
|
+
if node.kind is FrameKind.GROUPS:
|
|
203
|
+
message = "MySQL has no GROUPS frame; count rows or values instead"
|
|
204
|
+
raise UnsupportedQueryError(message)
|
|
205
|
+
if node.exclusion is not None:
|
|
206
|
+
message = "MySQL cannot exclude rows from a frame"
|
|
207
|
+
raise UnsupportedQueryError(message)
|
|
208
|
+
|
|
209
|
+
def render_extract(self, node: ExtractNode) -> str:
|
|
210
|
+
field = _DATE_FIELD.get(node.part)
|
|
211
|
+
if field is None:
|
|
212
|
+
message = f"unsupported MySQL date part: {node.part}"
|
|
213
|
+
raise UnsupportedQueryError(message)
|
|
214
|
+
return f"CAST(EXTRACT({field} FROM {self.compile(node.expression)}) AS SIGNED)"
|
|
215
|
+
|
|
216
|
+
def render_string_predicate(self, node: FunctionNode) -> str:
|
|
217
|
+
left_node, right_node = node.arguments
|
|
218
|
+
left = self.compile(left_node)
|
|
219
|
+
right = self.compile(right_node)
|
|
220
|
+
if node.name is FunctionName.CONTAINS:
|
|
221
|
+
return f"(INSTR({left}, {right}) > 0)"
|
|
222
|
+
if node.name is FunctionName.STARTS_WITH:
|
|
223
|
+
comparison = self.compile(right_node)
|
|
224
|
+
return f"(LEFT({left}, CHAR_LENGTH({right})) = {comparison})"
|
|
225
|
+
if node.name is FunctionName.ENDS_WITH:
|
|
226
|
+
comparison = self.compile(right_node)
|
|
227
|
+
return f"(RIGHT({left}, CHAR_LENGTH({right})) = {comparison})"
|
|
228
|
+
message = f"unsupported MySQL function: {node.name}"
|
|
229
|
+
raise UnsupportedQueryError(message)
|
|
230
|
+
|
|
231
|
+
def require_raw_expressions(self) -> None:
|
|
232
|
+
if not self._context.capabilities.raw_expressions:
|
|
233
|
+
message = "MySQL raw expressions are disabled by compiler capabilities"
|
|
234
|
+
raise UnsupportedQueryError(message)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
__all__ = ("MySQLExpressionCompiler",)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
_NO_CONCAT = (
|
|
241
|
+
"MySQL merges a document all the way down and has no one-level "
|
|
242
|
+
"concatenation; json_merge says what it does"
|
|
243
|
+
)
|
|
244
|
+
"""Refused rather than emulated, because all the way down is not one level."""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""MySQL runtime connection creation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import pymysql
|
|
8
|
+
from pymysql.constants import CLIENT
|
|
9
|
+
|
|
10
|
+
from pyoq.errors import ConnectionPoolError
|
|
11
|
+
from pyoq.query.mysql.commands import Connection
|
|
12
|
+
from pyoq.schema.mysql.dsn import parse_data_source_name
|
|
13
|
+
|
|
14
|
+
_CHARSET = "utf8mb4"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class MySQLConnectionFactory:
|
|
19
|
+
"""Opens MySQL connections that count matched rows, not changed rows.
|
|
20
|
+
|
|
21
|
+
``CLIENT.FOUND_ROWS`` makes an affected row count mean the same thing it
|
|
22
|
+
means for every other dialect, so an update that writes a column's existing
|
|
23
|
+
value still reports the row it matched.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
dsn: str
|
|
27
|
+
|
|
28
|
+
def __call__(self) -> Connection:
|
|
29
|
+
settings = parse_data_source_name(self.dsn)
|
|
30
|
+
try:
|
|
31
|
+
return pymysql.connect(
|
|
32
|
+
host=settings.host,
|
|
33
|
+
port=settings.port,
|
|
34
|
+
user=settings.user,
|
|
35
|
+
password=settings.password,
|
|
36
|
+
database=settings.database,
|
|
37
|
+
charset=_CHARSET,
|
|
38
|
+
autocommit=True,
|
|
39
|
+
client_flag=CLIENT.FOUND_ROWS,
|
|
40
|
+
)
|
|
41
|
+
except pymysql.Error as error:
|
|
42
|
+
message = f"unable to open MySQL connection: {error}"
|
|
43
|
+
raise ConnectionPoolError(message) from error
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = ("MySQLConnectionFactory",)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""MySQL failure classification, by server error code.
|
|
2
|
+
|
|
3
|
+
Every MySQL driver reports the same numeric codes while raising exceptions from
|
|
4
|
+
its own hierarchy, so classification lives here and each driver contributes only
|
|
5
|
+
the exception types it raises.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
_CANCELLED_CODE = 1317
|
|
11
|
+
_TIMEOUT_CODE = 3024
|
|
12
|
+
_CONNECTION_FAILURE_CODES = frozenset(
|
|
13
|
+
{
|
|
14
|
+
1053, # server shutdown in progress
|
|
15
|
+
1152, # aborted connection
|
|
16
|
+
1927, # connection killed
|
|
17
|
+
2002, # client cannot reach the server socket
|
|
18
|
+
2003, # client cannot reach the server host
|
|
19
|
+
2006, # server has gone away
|
|
20
|
+
2013, # connection lost during a query
|
|
21
|
+
2055, # connection lost with a system error
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
_TRANSACTION_ABORT_CODES = frozenset(
|
|
25
|
+
{
|
|
26
|
+
1213, # deadlock; the server rolled the whole transaction back
|
|
27
|
+
1614, # distributed transaction rolled back on deadlock
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def error_code(error: BaseException) -> int | None:
|
|
33
|
+
arguments = error.args
|
|
34
|
+
if not arguments or not isinstance(arguments[0], int):
|
|
35
|
+
return None
|
|
36
|
+
return arguments[0]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_cancellation(error: BaseException) -> bool:
|
|
40
|
+
return error_code(error) == _CANCELLED_CODE
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_timeout(error: BaseException) -> bool:
|
|
44
|
+
return error_code(error) == _TIMEOUT_CODE
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_connection_failure(error: BaseException) -> bool:
|
|
48
|
+
"""Report whether the server said the connection itself is gone.
|
|
49
|
+
|
|
50
|
+
A driver also signals a locally closed connection through its own interface
|
|
51
|
+
error, which carries no server code, so each driver checks for that itself.
|
|
52
|
+
"""
|
|
53
|
+
return error_code(error) in _CONNECTION_FAILURE_CODES
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_transaction_abort(error: BaseException) -> bool:
|
|
57
|
+
return error_code(error) in _TRANSACTION_ABORT_CODES
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__all__ = (
|
|
61
|
+
"error_code",
|
|
62
|
+
"is_cancellation",
|
|
63
|
+
"is_connection_failure",
|
|
64
|
+
"is_timeout",
|
|
65
|
+
"is_transaction_abort",
|
|
66
|
+
)
|