Загрузить файлы в «venv/Lib/site-packages/sqlalchemy/dialects/mysql»
This commit is contained in:
@@ -0,0 +1,323 @@
|
|||||||
|
# dialects/mysql/mariadbconnector.py
|
||||||
|
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
||||||
|
# <see AUTHORS file>
|
||||||
|
#
|
||||||
|
# This module is part of SQLAlchemy and is released under
|
||||||
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
.. dialect:: mysql+mariadbconnector
|
||||||
|
:name: MariaDB Connector/Python
|
||||||
|
:dbapi: mariadb
|
||||||
|
:connectstring: mariadb+mariadbconnector://<user>:<password>@<host>[:<port>]/<dbname>
|
||||||
|
:url: https://pypi.org/project/mariadb/
|
||||||
|
|
||||||
|
Driver Status
|
||||||
|
-------------
|
||||||
|
|
||||||
|
MariaDB Connector/Python enables Python programs to access MariaDB and MySQL
|
||||||
|
databases using an API which is compliant with the Python DB API 2.0 (PEP-249).
|
||||||
|
It is written in C and uses MariaDB Connector/C client library for client server
|
||||||
|
communication.
|
||||||
|
|
||||||
|
Note that the default driver for a ``mariadb://`` connection URI continues to
|
||||||
|
be ``mysqldb``. ``mariadb+mariadbconnector://`` is required to use this driver.
|
||||||
|
|
||||||
|
.. mariadb: https://github.com/mariadb-corporation/mariadb-connector-python
|
||||||
|
|
||||||
|
""" # noqa
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Sequence
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from typing import Union
|
||||||
|
from uuid import UUID as _python_UUID
|
||||||
|
|
||||||
|
from .base import MySQLCompiler
|
||||||
|
from .base import MySQLDialect
|
||||||
|
from .base import MySQLExecutionContext
|
||||||
|
from ... import sql
|
||||||
|
from ... import util
|
||||||
|
from ...sql import sqltypes
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ...engine.base import Connection
|
||||||
|
from ...engine.interfaces import ConnectArgsType
|
||||||
|
from ...engine.interfaces import DBAPIConnection
|
||||||
|
from ...engine.interfaces import DBAPICursor
|
||||||
|
from ...engine.interfaces import DBAPIModule
|
||||||
|
from ...engine.interfaces import Dialect
|
||||||
|
from ...engine.interfaces import IsolationLevel
|
||||||
|
from ...engine.interfaces import PoolProxiedConnection
|
||||||
|
from ...engine.url import URL
|
||||||
|
from ...sql.compiler import SQLCompiler
|
||||||
|
from ...sql.type_api import _ResultProcessorType
|
||||||
|
|
||||||
|
|
||||||
|
mariadb_cpy_minimum_version = (1, 0, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class _MariaDBUUID(sqltypes.UUID[sqltypes._UUID_RETURN]):
|
||||||
|
# work around JIRA issue
|
||||||
|
# https://jira.mariadb.org/browse/CONPY-270. When that issue is fixed,
|
||||||
|
# this type can be removed.
|
||||||
|
def result_processor(
|
||||||
|
self, dialect: Dialect, coltype: object
|
||||||
|
) -> Optional[_ResultProcessorType[Any]]:
|
||||||
|
if self.as_uuid:
|
||||||
|
|
||||||
|
def process(value: Any) -> Any:
|
||||||
|
if value is not None:
|
||||||
|
if hasattr(value, "decode"):
|
||||||
|
value = value.decode("ascii")
|
||||||
|
value = _python_UUID(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
return process
|
||||||
|
else:
|
||||||
|
|
||||||
|
def process(value: Any) -> Any:
|
||||||
|
if value is not None:
|
||||||
|
if hasattr(value, "decode"):
|
||||||
|
value = value.decode("ascii")
|
||||||
|
value = str(_python_UUID(value))
|
||||||
|
return value
|
||||||
|
|
||||||
|
return process
|
||||||
|
|
||||||
|
|
||||||
|
class MySQLExecutionContext_mariadbconnector(MySQLExecutionContext):
|
||||||
|
_lastrowid: Optional[int] = None
|
||||||
|
|
||||||
|
def create_server_side_cursor(self) -> DBAPICursor:
|
||||||
|
return self._dbapi_connection.cursor(buffered=False)
|
||||||
|
|
||||||
|
def create_default_cursor(self) -> DBAPICursor:
|
||||||
|
return self._dbapi_connection.cursor(buffered=True)
|
||||||
|
|
||||||
|
def post_exec(self) -> None:
|
||||||
|
super().post_exec()
|
||||||
|
|
||||||
|
self._rowcount = self.cursor.rowcount
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert isinstance(self.compiled, SQLCompiler)
|
||||||
|
if self.isinsert and self.compiled.postfetch_lastrowid:
|
||||||
|
self._lastrowid = self.cursor.lastrowid
|
||||||
|
|
||||||
|
def get_lastrowid(self) -> int:
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert self._lastrowid is not None
|
||||||
|
return self._lastrowid
|
||||||
|
|
||||||
|
|
||||||
|
class MySQLCompiler_mariadbconnector(MySQLCompiler):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MySQLDialect_mariadbconnector(MySQLDialect):
|
||||||
|
driver = "mariadbconnector"
|
||||||
|
supports_statement_cache = True
|
||||||
|
|
||||||
|
# set this to True at the module level to prevent the driver from running
|
||||||
|
# against a backend that server detects as MySQL. currently this appears to
|
||||||
|
# be unnecessary as MariaDB client libraries have always worked against
|
||||||
|
# MySQL databases. However, if this changes at some point, this can be
|
||||||
|
# adjusted, but PLEASE ADD A TEST in test/dialect/mysql/test_dialect.py if
|
||||||
|
# this change is made at some point to ensure the correct exception
|
||||||
|
# is raised at the correct point when running the driver against
|
||||||
|
# a MySQL backend.
|
||||||
|
# is_mariadb = True
|
||||||
|
|
||||||
|
supports_unicode_statements = True
|
||||||
|
encoding = "utf8mb4"
|
||||||
|
convert_unicode = True
|
||||||
|
supports_sane_rowcount = True
|
||||||
|
supports_sane_multi_rowcount = True
|
||||||
|
supports_native_decimal = True
|
||||||
|
default_paramstyle = "qmark"
|
||||||
|
execution_ctx_cls = MySQLExecutionContext_mariadbconnector
|
||||||
|
statement_compiler = MySQLCompiler_mariadbconnector
|
||||||
|
|
||||||
|
supports_server_side_cursors = True
|
||||||
|
|
||||||
|
colspecs = util.update_copy(
|
||||||
|
MySQLDialect.colspecs, {sqltypes.Uuid: _MariaDBUUID}
|
||||||
|
)
|
||||||
|
|
||||||
|
@util.memoized_property
|
||||||
|
def _dbapi_version(self) -> Tuple[int, ...]:
|
||||||
|
if self.dbapi and hasattr(self.dbapi, "__version__"):
|
||||||
|
return tuple(
|
||||||
|
[
|
||||||
|
int(x)
|
||||||
|
for x in re.findall(
|
||||||
|
r"(\d+)(?:[-\.]?|$)", self.dbapi.__version__
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (99, 99, 99)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.paramstyle = "qmark"
|
||||||
|
if self.dbapi is not None:
|
||||||
|
if self._dbapi_version < mariadb_cpy_minimum_version:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"The minimum required version for MariaDB "
|
||||||
|
"Connector/Python is %s"
|
||||||
|
% ".".join(str(x) for x in mariadb_cpy_minimum_version)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_dbapi(cls) -> DBAPIModule:
|
||||||
|
return __import__("mariadb")
|
||||||
|
|
||||||
|
def is_disconnect(
|
||||||
|
self,
|
||||||
|
e: DBAPIModule.Error,
|
||||||
|
connection: Optional[Union[PoolProxiedConnection, DBAPIConnection]],
|
||||||
|
cursor: Optional[DBAPICursor],
|
||||||
|
) -> bool:
|
||||||
|
if super().is_disconnect(e, connection, cursor):
|
||||||
|
return True
|
||||||
|
elif isinstance(e, self.loaded_dbapi.Error):
|
||||||
|
str_e = str(e).lower()
|
||||||
|
return "not connected" in str_e or "isn't valid" in str_e
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_connect_args(self, url: URL) -> ConnectArgsType:
|
||||||
|
opts = url.translate_connect_args()
|
||||||
|
opts.update(url.query)
|
||||||
|
|
||||||
|
int_params = [
|
||||||
|
"connect_timeout",
|
||||||
|
"read_timeout",
|
||||||
|
"write_timeout",
|
||||||
|
"client_flag",
|
||||||
|
"port",
|
||||||
|
"pool_size",
|
||||||
|
]
|
||||||
|
bool_params = [
|
||||||
|
"local_infile",
|
||||||
|
"ssl_verify_cert",
|
||||||
|
"ssl",
|
||||||
|
"pool_reset_connection",
|
||||||
|
"compress",
|
||||||
|
]
|
||||||
|
|
||||||
|
for key in int_params:
|
||||||
|
util.coerce_kw_type(opts, key, int)
|
||||||
|
for key in bool_params:
|
||||||
|
util.coerce_kw_type(opts, key, bool)
|
||||||
|
|
||||||
|
# FOUND_ROWS must be set in CLIENT_FLAGS to enable
|
||||||
|
# supports_sane_rowcount.
|
||||||
|
client_flag = opts.get("client_flag", 0)
|
||||||
|
if self.dbapi is not None:
|
||||||
|
try:
|
||||||
|
CLIENT_FLAGS = __import__(
|
||||||
|
self.dbapi.__name__ + ".constants.CLIENT"
|
||||||
|
).constants.CLIENT
|
||||||
|
client_flag |= CLIENT_FLAGS.FOUND_ROWS
|
||||||
|
except (AttributeError, ImportError):
|
||||||
|
self.supports_sane_rowcount = False
|
||||||
|
opts["client_flag"] = client_flag
|
||||||
|
return [], opts
|
||||||
|
|
||||||
|
def _extract_error_code(self, exception: DBAPIModule.Error) -> int:
|
||||||
|
try:
|
||||||
|
rc: int = exception.errno
|
||||||
|
except:
|
||||||
|
rc = -1
|
||||||
|
return rc
|
||||||
|
|
||||||
|
def _detect_charset(self, connection: Connection) -> str:
|
||||||
|
return "utf8mb4"
|
||||||
|
|
||||||
|
def get_isolation_level_values(
|
||||||
|
self, dbapi_conn: DBAPIConnection
|
||||||
|
) -> Sequence[IsolationLevel]:
|
||||||
|
return (
|
||||||
|
"SERIALIZABLE",
|
||||||
|
"READ UNCOMMITTED",
|
||||||
|
"READ COMMITTED",
|
||||||
|
"REPEATABLE READ",
|
||||||
|
"AUTOCOMMIT",
|
||||||
|
)
|
||||||
|
|
||||||
|
def detect_autocommit_setting(self, dbapi_conn: DBAPIConnection) -> bool:
|
||||||
|
return bool(dbapi_conn.autocommit)
|
||||||
|
|
||||||
|
def set_isolation_level(
|
||||||
|
self, dbapi_connection: DBAPIConnection, level: IsolationLevel
|
||||||
|
) -> None:
|
||||||
|
if level == "AUTOCOMMIT":
|
||||||
|
dbapi_connection.autocommit = True
|
||||||
|
else:
|
||||||
|
dbapi_connection.autocommit = False
|
||||||
|
super().set_isolation_level(dbapi_connection, level)
|
||||||
|
|
||||||
|
def do_begin_twophase(self, connection: Connection, xid: Any) -> None:
|
||||||
|
connection.execute(
|
||||||
|
sql.text("XA BEGIN :xid").bindparams(
|
||||||
|
sql.bindparam("xid", xid, literal_execute=True)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_prepare_twophase(self, connection: Connection, xid: Any) -> None:
|
||||||
|
connection.execute(
|
||||||
|
sql.text("XA END :xid").bindparams(
|
||||||
|
sql.bindparam("xid", xid, literal_execute=True)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
sql.text("XA PREPARE :xid").bindparams(
|
||||||
|
sql.bindparam("xid", xid, literal_execute=True)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_rollback_twophase(
|
||||||
|
self,
|
||||||
|
connection: Connection,
|
||||||
|
xid: Any,
|
||||||
|
is_prepared: bool = True,
|
||||||
|
recover: bool = False,
|
||||||
|
) -> None:
|
||||||
|
if not is_prepared:
|
||||||
|
connection.execute(
|
||||||
|
sql.text("XA END :xid").bindparams(
|
||||||
|
sql.bindparam("xid", xid, literal_execute=True)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
sql.text("XA ROLLBACK :xid").bindparams(
|
||||||
|
sql.bindparam("xid", xid, literal_execute=True)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_commit_twophase(
|
||||||
|
self,
|
||||||
|
connection: Connection,
|
||||||
|
xid: Any,
|
||||||
|
is_prepared: bool = True,
|
||||||
|
recover: bool = False,
|
||||||
|
) -> None:
|
||||||
|
if not is_prepared:
|
||||||
|
self.do_prepare_twophase(connection, xid)
|
||||||
|
connection.execute(
|
||||||
|
sql.text("XA COMMIT :xid").bindparams(
|
||||||
|
sql.bindparam("xid", xid, literal_execute=True)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
dialect = MySQLDialect_mariadbconnector
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
# dialects/mysql/mysqlconnector.py
|
||||||
|
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
||||||
|
# <see AUTHORS file>
|
||||||
|
#
|
||||||
|
# This module is part of SQLAlchemy and is released under
|
||||||
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
||||||
|
|
||||||
|
|
||||||
|
r"""
|
||||||
|
.. dialect:: mysql+mysqlconnector
|
||||||
|
:name: MySQL Connector/Python
|
||||||
|
:dbapi: myconnpy
|
||||||
|
:connectstring: mysql+mysqlconnector://<user>:<password>@<host>[:<port>]/<dbname>
|
||||||
|
:url: https://pypi.org/project/mysql-connector-python/
|
||||||
|
|
||||||
|
Driver Status
|
||||||
|
-------------
|
||||||
|
|
||||||
|
MySQL Connector/Python is supported as of SQLAlchemy 2.0.39 to the
|
||||||
|
degree which the driver is functional. There are still ongoing issues
|
||||||
|
with features such as server side cursors which remain disabled until
|
||||||
|
upstream issues are repaired.
|
||||||
|
|
||||||
|
.. warning:: The MySQL Connector/Python driver published by Oracle is subject
|
||||||
|
to frequent, major regressions of essential functionality such as being able
|
||||||
|
to correctly persist simple binary strings which indicate it is not well
|
||||||
|
tested. The SQLAlchemy project is not able to maintain this dialect fully as
|
||||||
|
regressions in the driver prevent it from being included in continuous
|
||||||
|
integration.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0.39
|
||||||
|
|
||||||
|
The MySQL Connector/Python dialect has been updated to support the
|
||||||
|
latest version of this DBAPI. Previously, MySQL Connector/Python
|
||||||
|
was not fully supported. However, support remains limited due to ongoing
|
||||||
|
regressions introduced in this driver.
|
||||||
|
|
||||||
|
Connecting to MariaDB with MySQL Connector/Python
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
MySQL Connector/Python may attempt to pass an incompatible collation to the
|
||||||
|
database when connecting to MariaDB. Experimentation has shown that using
|
||||||
|
``?charset=utf8mb4&collation=utfmb4_general_ci`` or similar MariaDB-compatible
|
||||||
|
charset/collation will allow connectivity.
|
||||||
|
|
||||||
|
|
||||||
|
""" # noqa
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
from typing import cast
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Sequence
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from .base import MariaDBIdentifierPreparer
|
||||||
|
from .base import MySQLCompiler
|
||||||
|
from .base import MySQLDialect
|
||||||
|
from .base import MySQLExecutionContext
|
||||||
|
from .base import MySQLIdentifierPreparer
|
||||||
|
from .mariadb import MariaDBDialect
|
||||||
|
from .types import BIT
|
||||||
|
from ... import util
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
from ...engine.base import Connection
|
||||||
|
from ...engine.cursor import CursorResult
|
||||||
|
from ...engine.interfaces import ConnectArgsType
|
||||||
|
from ...engine.interfaces import DBAPIConnection
|
||||||
|
from ...engine.interfaces import DBAPICursor
|
||||||
|
from ...engine.interfaces import DBAPIModule
|
||||||
|
from ...engine.interfaces import IsolationLevel
|
||||||
|
from ...engine.interfaces import PoolProxiedConnection
|
||||||
|
from ...engine.row import Row
|
||||||
|
from ...engine.url import URL
|
||||||
|
from ...sql.elements import BinaryExpression
|
||||||
|
|
||||||
|
|
||||||
|
class MySQLExecutionContext_mysqlconnector(MySQLExecutionContext):
|
||||||
|
def create_server_side_cursor(self) -> DBAPICursor:
|
||||||
|
return self._dbapi_connection.cursor(buffered=False)
|
||||||
|
|
||||||
|
def create_default_cursor(self) -> DBAPICursor:
|
||||||
|
return self._dbapi_connection.cursor(buffered=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MySQLCompiler_mysqlconnector(MySQLCompiler):
|
||||||
|
def visit_mod_binary(
|
||||||
|
self, binary: BinaryExpression[Any], operator: Any, **kw: Any
|
||||||
|
) -> str:
|
||||||
|
return (
|
||||||
|
self.process(binary.left, **kw)
|
||||||
|
+ " % "
|
||||||
|
+ self.process(binary.right, **kw)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IdentifierPreparerCommon_mysqlconnector:
|
||||||
|
@property
|
||||||
|
def _double_percents(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@_double_percents.setter
|
||||||
|
def _double_percents(self, value: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _escape_identifier(self, value: str) -> str:
|
||||||
|
value = value.replace(
|
||||||
|
self.escape_quote, # type: ignore[attr-defined]
|
||||||
|
self.escape_to_quote, # type: ignore[attr-defined]
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class MySQLIdentifierPreparer_mysqlconnector(
|
||||||
|
IdentifierPreparerCommon_mysqlconnector, MySQLIdentifierPreparer
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MariaDBIdentifierPreparer_mysqlconnector(
|
||||||
|
IdentifierPreparerCommon_mysqlconnector, MariaDBIdentifierPreparer
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _myconnpyBIT(BIT):
|
||||||
|
def result_processor(self, dialect: Any, coltype: Any) -> None:
|
||||||
|
"""MySQL-connector already converts mysql bits, so."""
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class MySQLDialect_mysqlconnector(MySQLDialect):
|
||||||
|
driver = "mysqlconnector"
|
||||||
|
supports_statement_cache = True
|
||||||
|
|
||||||
|
supports_sane_rowcount = True
|
||||||
|
supports_sane_multi_rowcount = True
|
||||||
|
|
||||||
|
supports_native_decimal = True
|
||||||
|
|
||||||
|
supports_native_bit = True
|
||||||
|
|
||||||
|
# not until https://bugs.mysql.com/bug.php?id=117548
|
||||||
|
supports_server_side_cursors = False
|
||||||
|
|
||||||
|
default_paramstyle = "format"
|
||||||
|
statement_compiler = MySQLCompiler_mysqlconnector
|
||||||
|
|
||||||
|
execution_ctx_cls = MySQLExecutionContext_mysqlconnector
|
||||||
|
|
||||||
|
preparer: type[MySQLIdentifierPreparer] = (
|
||||||
|
MySQLIdentifierPreparer_mysqlconnector
|
||||||
|
)
|
||||||
|
|
||||||
|
colspecs = util.update_copy(MySQLDialect.colspecs, {BIT: _myconnpyBIT})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_dbapi(cls) -> DBAPIModule:
|
||||||
|
return cast("DBAPIModule", __import__("mysql.connector").connector)
|
||||||
|
|
||||||
|
def do_ping(self, dbapi_connection: DBAPIConnection) -> bool:
|
||||||
|
dbapi_connection.ping(False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_connect_args(self, url: URL) -> ConnectArgsType:
|
||||||
|
opts = url.translate_connect_args(username="user")
|
||||||
|
|
||||||
|
opts.update(url.query)
|
||||||
|
|
||||||
|
util.coerce_kw_type(opts, "allow_local_infile", bool)
|
||||||
|
util.coerce_kw_type(opts, "autocommit", bool)
|
||||||
|
util.coerce_kw_type(opts, "buffered", bool)
|
||||||
|
util.coerce_kw_type(opts, "client_flag", int)
|
||||||
|
util.coerce_kw_type(opts, "compress", bool)
|
||||||
|
util.coerce_kw_type(opts, "connection_timeout", int)
|
||||||
|
util.coerce_kw_type(opts, "connect_timeout", int)
|
||||||
|
util.coerce_kw_type(opts, "consume_results", bool)
|
||||||
|
util.coerce_kw_type(opts, "force_ipv6", bool)
|
||||||
|
util.coerce_kw_type(opts, "get_warnings", bool)
|
||||||
|
util.coerce_kw_type(opts, "pool_reset_session", bool)
|
||||||
|
util.coerce_kw_type(opts, "pool_size", int)
|
||||||
|
util.coerce_kw_type(opts, "raise_on_warnings", bool)
|
||||||
|
util.coerce_kw_type(opts, "raw", bool)
|
||||||
|
util.coerce_kw_type(opts, "ssl_verify_cert", bool)
|
||||||
|
util.coerce_kw_type(opts, "use_pure", bool)
|
||||||
|
util.coerce_kw_type(opts, "use_unicode", bool)
|
||||||
|
|
||||||
|
# note that "buffered" is set to False by default in MySQL/connector
|
||||||
|
# python. If you set it to True, then there is no way to get a server
|
||||||
|
# side cursor because the logic is written to disallow that.
|
||||||
|
|
||||||
|
# leaving this at True until
|
||||||
|
# https://bugs.mysql.com/bug.php?id=117548 can be fixed
|
||||||
|
opts["buffered"] = True
|
||||||
|
|
||||||
|
# FOUND_ROWS must be set in ClientFlag to enable
|
||||||
|
# supports_sane_rowcount.
|
||||||
|
if self.dbapi is not None:
|
||||||
|
try:
|
||||||
|
from mysql.connector import constants # type: ignore
|
||||||
|
|
||||||
|
ClientFlag = constants.ClientFlag
|
||||||
|
|
||||||
|
client_flags = opts.get(
|
||||||
|
"client_flags", ClientFlag.get_default()
|
||||||
|
)
|
||||||
|
client_flags |= ClientFlag.FOUND_ROWS
|
||||||
|
opts["client_flags"] = client_flags
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return [], opts
|
||||||
|
|
||||||
|
@util.memoized_property
|
||||||
|
def _mysqlconnector_version_info(self) -> Optional[Tuple[int, ...]]:
|
||||||
|
if self.dbapi and hasattr(self.dbapi, "__version__"):
|
||||||
|
m = re.match(r"(\d+)\.(\d+)(?:\.(\d+))?", self.dbapi.__version__)
|
||||||
|
if m:
|
||||||
|
return tuple(int(x) for x in m.group(1, 2, 3) if x is not None)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _detect_charset(self, connection: Connection) -> str:
|
||||||
|
return connection.connection.charset # type: ignore
|
||||||
|
|
||||||
|
def _extract_error_code(self, exception: BaseException) -> int:
|
||||||
|
return exception.errno # type: ignore
|
||||||
|
|
||||||
|
def is_disconnect(
|
||||||
|
self,
|
||||||
|
e: Exception,
|
||||||
|
connection: Optional[Union[PoolProxiedConnection, DBAPIConnection]],
|
||||||
|
cursor: Optional[DBAPICursor],
|
||||||
|
) -> bool:
|
||||||
|
errnos = (2006, 2013, 2014, 2045, 2055, 2048)
|
||||||
|
exceptions = (
|
||||||
|
self.loaded_dbapi.OperationalError, #
|
||||||
|
self.loaded_dbapi.InterfaceError,
|
||||||
|
self.loaded_dbapi.ProgrammingError,
|
||||||
|
)
|
||||||
|
if isinstance(e, exceptions):
|
||||||
|
return (
|
||||||
|
e.errno in errnos
|
||||||
|
or "MySQL Connection not available." in str(e)
|
||||||
|
or "Connection to MySQL is not available" in str(e)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _compat_fetchall(
|
||||||
|
self,
|
||||||
|
rp: CursorResult[Tuple[Any, ...]],
|
||||||
|
charset: Optional[str] = None,
|
||||||
|
) -> Sequence[Row[Tuple[Any, ...]]]:
|
||||||
|
return rp.fetchall()
|
||||||
|
|
||||||
|
def _compat_fetchone(
|
||||||
|
self,
|
||||||
|
rp: CursorResult[Tuple[Any, ...]],
|
||||||
|
charset: Optional[str] = None,
|
||||||
|
) -> Optional[Row[Tuple[Any, ...]]]:
|
||||||
|
return rp.fetchone()
|
||||||
|
|
||||||
|
def get_isolation_level_values(
|
||||||
|
self, dbapi_conn: DBAPIConnection
|
||||||
|
) -> Sequence[IsolationLevel]:
|
||||||
|
return (
|
||||||
|
"SERIALIZABLE",
|
||||||
|
"READ UNCOMMITTED",
|
||||||
|
"READ COMMITTED",
|
||||||
|
"REPEATABLE READ",
|
||||||
|
"AUTOCOMMIT",
|
||||||
|
)
|
||||||
|
|
||||||
|
def detect_autocommit_setting(self, dbapi_conn: DBAPIConnection) -> bool:
|
||||||
|
return bool(dbapi_conn.autocommit)
|
||||||
|
|
||||||
|
def set_isolation_level(
|
||||||
|
self, dbapi_connection: DBAPIConnection, level: IsolationLevel
|
||||||
|
) -> None:
|
||||||
|
if level == "AUTOCOMMIT":
|
||||||
|
dbapi_connection.autocommit = True
|
||||||
|
else:
|
||||||
|
dbapi_connection.autocommit = False
|
||||||
|
super().set_isolation_level(dbapi_connection, level)
|
||||||
|
|
||||||
|
|
||||||
|
class MariaDBDialect_mysqlconnector(
|
||||||
|
MariaDBDialect, MySQLDialect_mysqlconnector
|
||||||
|
):
|
||||||
|
supports_statement_cache = True
|
||||||
|
_allows_uuid_binds = False
|
||||||
|
preparer = MariaDBIdentifierPreparer_mysqlconnector
|
||||||
|
|
||||||
|
|
||||||
|
dialect = MySQLDialect_mysqlconnector
|
||||||
|
mariadb_dialect = MariaDBDialect_mysqlconnector
|
||||||
315
venv/Lib/site-packages/sqlalchemy/dialects/mysql/mysqldb.py
Normal file
315
venv/Lib/site-packages/sqlalchemy/dialects/mysql/mysqldb.py
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
# dialects/mysql/mysqldb.py
|
||||||
|
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
||||||
|
# <see AUTHORS file>
|
||||||
|
#
|
||||||
|
# This module is part of SQLAlchemy and is released under
|
||||||
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
.. dialect:: mysql+mysqldb
|
||||||
|
:name: mysqlclient (maintained fork of MySQL-Python)
|
||||||
|
:dbapi: mysqldb
|
||||||
|
:connectstring: mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>
|
||||||
|
:url: https://pypi.org/project/mysqlclient/
|
||||||
|
|
||||||
|
Driver Status
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The mysqlclient DBAPI is a maintained fork of the
|
||||||
|
`MySQL-Python <https://sourceforge.net/projects/mysql-python>`_ DBAPI
|
||||||
|
that is no longer maintained. `mysqlclient`_ supports Python 2 and Python 3
|
||||||
|
and is very stable.
|
||||||
|
|
||||||
|
.. _mysqlclient: https://github.com/PyMySQL/mysqlclient-python
|
||||||
|
|
||||||
|
.. _mysqldb_unicode:
|
||||||
|
|
||||||
|
Unicode
|
||||||
|
-------
|
||||||
|
|
||||||
|
Please see :ref:`mysql_unicode` for current recommendations on unicode
|
||||||
|
handling.
|
||||||
|
|
||||||
|
.. _mysqldb_ssl:
|
||||||
|
|
||||||
|
SSL Connections
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The mysqlclient and PyMySQL DBAPIs accept an additional dictionary under the
|
||||||
|
key "ssl", which may be specified using the
|
||||||
|
:paramref:`_sa.create_engine.connect_args` dictionary::
|
||||||
|
|
||||||
|
engine = create_engine(
|
||||||
|
"mysql+mysqldb://scott:tiger@192.168.0.134/test",
|
||||||
|
connect_args={
|
||||||
|
"ssl": {
|
||||||
|
"ca": "/home/gord/client-ssl/ca.pem",
|
||||||
|
"cert": "/home/gord/client-ssl/client-cert.pem",
|
||||||
|
"key": "/home/gord/client-ssl/client-key.pem",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
For convenience, the following keys may also be specified inline within the URL
|
||||||
|
where they will be interpreted into the "ssl" dictionary automatically:
|
||||||
|
"ssl_ca", "ssl_cert", "ssl_key", "ssl_capath", "ssl_cipher",
|
||||||
|
"ssl_check_hostname". An example is as follows::
|
||||||
|
|
||||||
|
connection_uri = (
|
||||||
|
"mysql+mysqldb://scott:tiger@192.168.0.134/test"
|
||||||
|
"?ssl_ca=/home/gord/client-ssl/ca.pem"
|
||||||
|
"&ssl_cert=/home/gord/client-ssl/client-cert.pem"
|
||||||
|
"&ssl_key=/home/gord/client-ssl/client-key.pem"
|
||||||
|
)
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:ref:`pymysql_ssl` in the PyMySQL dialect
|
||||||
|
|
||||||
|
|
||||||
|
Using MySQLdb with Google Cloud SQL
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
Google Cloud SQL now recommends use of the MySQLdb dialect. Connect
|
||||||
|
using a URL like the following:
|
||||||
|
|
||||||
|
.. sourcecode:: text
|
||||||
|
|
||||||
|
mysql+mysqldb://root@/<dbname>?unix_socket=/cloudsql/<projectid>:<instancename>
|
||||||
|
|
||||||
|
Server Side Cursors
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
The mysqldb dialect supports server-side cursors. See :ref:`mysql_ss_cursors`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
from typing import Callable
|
||||||
|
from typing import cast
|
||||||
|
from typing import Dict
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from .base import MySQLCompiler
|
||||||
|
from .base import MySQLDialect
|
||||||
|
from .base import MySQLExecutionContext
|
||||||
|
from .base import MySQLIdentifierPreparer
|
||||||
|
from ... import util
|
||||||
|
from ...util.typing import Literal
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
from ...engine.base import Connection
|
||||||
|
from ...engine.interfaces import _DBAPIMultiExecuteParams
|
||||||
|
from ...engine.interfaces import ConnectArgsType
|
||||||
|
from ...engine.interfaces import DBAPIConnection
|
||||||
|
from ...engine.interfaces import DBAPICursor
|
||||||
|
from ...engine.interfaces import DBAPIModule
|
||||||
|
from ...engine.interfaces import ExecutionContext
|
||||||
|
from ...engine.interfaces import IsolationLevel
|
||||||
|
from ...engine.url import URL
|
||||||
|
|
||||||
|
|
||||||
|
class MySQLExecutionContext_mysqldb(MySQLExecutionContext):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MySQLCompiler_mysqldb(MySQLCompiler):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MySQLDialect_mysqldb(MySQLDialect):
|
||||||
|
driver = "mysqldb"
|
||||||
|
supports_statement_cache = True
|
||||||
|
supports_unicode_statements = True
|
||||||
|
supports_sane_rowcount = True
|
||||||
|
supports_sane_multi_rowcount = True
|
||||||
|
|
||||||
|
supports_native_decimal = True
|
||||||
|
|
||||||
|
default_paramstyle = "format"
|
||||||
|
execution_ctx_cls = MySQLExecutionContext_mysqldb
|
||||||
|
statement_compiler = MySQLCompiler_mysqldb
|
||||||
|
preparer = MySQLIdentifierPreparer
|
||||||
|
server_version_info: Tuple[int, ...]
|
||||||
|
|
||||||
|
def __init__(self, **kwargs: Any):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._mysql_dbapi_version = (
|
||||||
|
self._parse_dbapi_version(self.dbapi.__version__)
|
||||||
|
if self.dbapi is not None and hasattr(self.dbapi, "__version__")
|
||||||
|
else (0, 0, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_dbapi_version(self, version: str) -> Tuple[int, ...]:
|
||||||
|
m = re.match(r"(\d+)\.(\d+)(?:\.(\d+))?", version)
|
||||||
|
if m:
|
||||||
|
return tuple(int(x) for x in m.group(1, 2, 3) if x is not None)
|
||||||
|
else:
|
||||||
|
return (0, 0, 0)
|
||||||
|
|
||||||
|
@util.langhelpers.memoized_property
|
||||||
|
def supports_server_side_cursors(self) -> bool:
|
||||||
|
try:
|
||||||
|
cursors = __import__("MySQLdb.cursors").cursors
|
||||||
|
self._sscursor = cursors.SSCursor
|
||||||
|
return True
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_dbapi(cls) -> DBAPIModule:
|
||||||
|
return __import__("MySQLdb")
|
||||||
|
|
||||||
|
def on_connect(self) -> Callable[[DBAPIConnection], None]:
|
||||||
|
super_ = super().on_connect()
|
||||||
|
|
||||||
|
def on_connect(conn: DBAPIConnection) -> None:
|
||||||
|
if super_ is not None:
|
||||||
|
super_(conn)
|
||||||
|
|
||||||
|
charset_name = conn.character_set_name()
|
||||||
|
|
||||||
|
if charset_name is not None:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SET NAMES %s" % charset_name)
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
return on_connect
|
||||||
|
|
||||||
|
def do_ping(self, dbapi_connection: DBAPIConnection) -> Literal[True]:
|
||||||
|
dbapi_connection.ping()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def do_executemany(
|
||||||
|
self,
|
||||||
|
cursor: DBAPICursor,
|
||||||
|
statement: str,
|
||||||
|
parameters: _DBAPIMultiExecuteParams,
|
||||||
|
context: Optional[ExecutionContext] = None,
|
||||||
|
) -> None:
|
||||||
|
rowcount = cursor.executemany(statement, parameters)
|
||||||
|
if context is not None:
|
||||||
|
cast(MySQLExecutionContext, context)._rowcount = rowcount
|
||||||
|
|
||||||
|
def create_connect_args(
|
||||||
|
self, url: URL, _translate_args: Optional[Dict[str, Any]] = None
|
||||||
|
) -> ConnectArgsType:
|
||||||
|
if _translate_args is None:
|
||||||
|
_translate_args = dict(
|
||||||
|
database="db", username="user", password="passwd"
|
||||||
|
)
|
||||||
|
|
||||||
|
opts = url.translate_connect_args(**_translate_args)
|
||||||
|
opts.update(url.query)
|
||||||
|
|
||||||
|
util.coerce_kw_type(opts, "compress", bool)
|
||||||
|
util.coerce_kw_type(opts, "connect_timeout", int)
|
||||||
|
util.coerce_kw_type(opts, "read_timeout", int)
|
||||||
|
util.coerce_kw_type(opts, "write_timeout", int)
|
||||||
|
util.coerce_kw_type(opts, "client_flag", int)
|
||||||
|
util.coerce_kw_type(opts, "local_infile", bool)
|
||||||
|
# Note: using either of the below will cause all strings to be
|
||||||
|
# returned as Unicode, both in raw SQL operations and with column
|
||||||
|
# types like String and MSString.
|
||||||
|
util.coerce_kw_type(opts, "use_unicode", bool)
|
||||||
|
util.coerce_kw_type(opts, "charset", str)
|
||||||
|
|
||||||
|
# Rich values 'cursorclass' and 'conv' are not supported via
|
||||||
|
# query string.
|
||||||
|
|
||||||
|
ssl = {}
|
||||||
|
keys = [
|
||||||
|
("ssl_ca", str),
|
||||||
|
("ssl_key", str),
|
||||||
|
("ssl_cert", str),
|
||||||
|
("ssl_capath", str),
|
||||||
|
("ssl_cipher", str),
|
||||||
|
("ssl_check_hostname", bool),
|
||||||
|
]
|
||||||
|
for key, kw_type in keys:
|
||||||
|
if key in opts:
|
||||||
|
ssl[key[4:]] = opts[key]
|
||||||
|
util.coerce_kw_type(ssl, key[4:], kw_type)
|
||||||
|
del opts[key]
|
||||||
|
if ssl:
|
||||||
|
opts["ssl"] = ssl
|
||||||
|
|
||||||
|
# FOUND_ROWS must be set in CLIENT_FLAGS to enable
|
||||||
|
# supports_sane_rowcount.
|
||||||
|
client_flag = opts.get("client_flag", 0)
|
||||||
|
|
||||||
|
client_flag_found_rows = self._found_rows_client_flag()
|
||||||
|
if client_flag_found_rows is not None:
|
||||||
|
client_flag |= client_flag_found_rows
|
||||||
|
opts["client_flag"] = client_flag
|
||||||
|
return [], opts
|
||||||
|
|
||||||
|
def _found_rows_client_flag(self) -> Optional[int]:
|
||||||
|
if self.dbapi is not None:
|
||||||
|
try:
|
||||||
|
CLIENT_FLAGS = __import__(
|
||||||
|
self.dbapi.__name__ + ".constants.CLIENT"
|
||||||
|
).constants.CLIENT
|
||||||
|
except (AttributeError, ImportError):
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return CLIENT_FLAGS.FOUND_ROWS # type: ignore
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _extract_error_code(self, exception: DBAPIModule.Error) -> int:
|
||||||
|
return exception.args[0] # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
def _detect_charset(self, connection: Connection) -> str:
|
||||||
|
"""Sniff out the character set in use for connection results."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# note: the SQL here would be
|
||||||
|
# "SHOW VARIABLES LIKE 'character_set%%'"
|
||||||
|
|
||||||
|
cset_name: Callable[[], str] = (
|
||||||
|
connection.connection.character_set_name
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
util.warn(
|
||||||
|
"No 'character_set_name' can be detected with "
|
||||||
|
"this MySQL-Python version; "
|
||||||
|
"please upgrade to a recent version of MySQL-Python. "
|
||||||
|
"Assuming latin1."
|
||||||
|
)
|
||||||
|
return "latin1"
|
||||||
|
else:
|
||||||
|
return cset_name()
|
||||||
|
|
||||||
|
def get_isolation_level_values(
|
||||||
|
self, dbapi_conn: DBAPIConnection
|
||||||
|
) -> Tuple[IsolationLevel, ...]:
|
||||||
|
return (
|
||||||
|
"SERIALIZABLE",
|
||||||
|
"READ UNCOMMITTED",
|
||||||
|
"READ COMMITTED",
|
||||||
|
"REPEATABLE READ",
|
||||||
|
"AUTOCOMMIT",
|
||||||
|
)
|
||||||
|
|
||||||
|
def detect_autocommit_setting(self, dbapi_conn: DBAPIConnection) -> bool:
|
||||||
|
return dbapi_conn.get_autocommit() # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
def set_isolation_level(
|
||||||
|
self, dbapi_connection: DBAPIConnection, level: IsolationLevel
|
||||||
|
) -> None:
|
||||||
|
if level == "AUTOCOMMIT":
|
||||||
|
dbapi_connection.autocommit(True)
|
||||||
|
else:
|
||||||
|
dbapi_connection.autocommit(False)
|
||||||
|
super().set_isolation_level(dbapi_connection, level)
|
||||||
|
|
||||||
|
|
||||||
|
dialect = MySQLDialect_mysqldb
|
||||||
153
venv/Lib/site-packages/sqlalchemy/dialects/mysql/provision.py
Normal file
153
venv/Lib/site-packages/sqlalchemy/dialects/mysql/provision.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# dialects/mysql/provision.py
|
||||||
|
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
||||||
|
# <see AUTHORS file>
|
||||||
|
#
|
||||||
|
# This module is part of SQLAlchemy and is released under
|
||||||
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
||||||
|
# mypy: ignore-errors
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
from ... import event
|
||||||
|
from ... import exc
|
||||||
|
from ...testing.provision import allow_stale_update_impl
|
||||||
|
from ...testing.provision import configure_follower
|
||||||
|
from ...testing.provision import create_db
|
||||||
|
from ...testing.provision import delete_from_all_tables
|
||||||
|
from ...testing.provision import drop_db
|
||||||
|
from ...testing.provision import generate_driver_url
|
||||||
|
from ...testing.provision import temp_table_keyword_args
|
||||||
|
from ...testing.provision import upsert
|
||||||
|
|
||||||
|
|
||||||
|
@generate_driver_url.for_db("mysql", "mariadb")
|
||||||
|
def generate_driver_url(url, driver, query_str):
|
||||||
|
backend = url.get_backend_name()
|
||||||
|
|
||||||
|
# NOTE: at the moment, tests are running mariadbconnector
|
||||||
|
# against both mariadb and mysql backends. if we want this to be
|
||||||
|
# limited, do the decision making here to reject a "mysql+mariadbconnector"
|
||||||
|
# URL. Optionally also re-enable the module level
|
||||||
|
# MySQLDialect_mariadbconnector.is_mysql flag as well, which must include
|
||||||
|
# a unit and/or functional test.
|
||||||
|
|
||||||
|
# all the Jenkins tests have been running mysqlclient Python library
|
||||||
|
# built against mariadb client drivers for years against all MySQL /
|
||||||
|
# MariaDB versions going back to MySQL 5.6, currently they can talk
|
||||||
|
# to MySQL databases without problems.
|
||||||
|
|
||||||
|
if backend == "mysql":
|
||||||
|
dialect_cls = url.get_dialect()
|
||||||
|
if dialect_cls._is_mariadb_from_url(url):
|
||||||
|
backend = "mariadb"
|
||||||
|
|
||||||
|
new_url = url.set(
|
||||||
|
drivername="%s+%s" % (backend, driver)
|
||||||
|
).update_query_string(query_str)
|
||||||
|
|
||||||
|
if driver == "mariadbconnector":
|
||||||
|
new_url = new_url.difference_update_query(["charset"])
|
||||||
|
elif driver == "mysqlconnector":
|
||||||
|
new_url = new_url.update_query_pairs(
|
||||||
|
[("collation", "utf8mb4_general_ci")]
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_url.get_dialect()
|
||||||
|
except exc.NoSuchModuleError:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return new_url
|
||||||
|
|
||||||
|
|
||||||
|
@create_db.for_db("mysql", "mariadb")
|
||||||
|
def _mysql_create_db(cfg, eng, ident):
|
||||||
|
with eng.begin() as conn:
|
||||||
|
try:
|
||||||
|
_mysql_drop_db(cfg, conn, ident)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with eng.begin() as conn:
|
||||||
|
conn.exec_driver_sql(
|
||||||
|
"CREATE DATABASE %s CHARACTER SET utf8mb4" % ident
|
||||||
|
)
|
||||||
|
conn.exec_driver_sql(
|
||||||
|
"CREATE DATABASE %s_test_schema CHARACTER SET utf8mb4" % ident
|
||||||
|
)
|
||||||
|
conn.exec_driver_sql(
|
||||||
|
"CREATE DATABASE %s_test_schema_2 CHARACTER SET utf8mb4" % ident
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@configure_follower.for_db("mysql", "mariadb")
|
||||||
|
def _mysql_configure_follower(config, ident):
|
||||||
|
config.test_schema = "%s_test_schema" % ident
|
||||||
|
config.test_schema_2 = "%s_test_schema_2" % ident
|
||||||
|
|
||||||
|
|
||||||
|
@drop_db.for_db("mysql", "mariadb")
|
||||||
|
def _mysql_drop_db(cfg, eng, ident):
|
||||||
|
with eng.begin() as conn:
|
||||||
|
conn.exec_driver_sql("DROP DATABASE %s_test_schema" % ident)
|
||||||
|
conn.exec_driver_sql("DROP DATABASE %s_test_schema_2" % ident)
|
||||||
|
conn.exec_driver_sql("DROP DATABASE %s" % ident)
|
||||||
|
|
||||||
|
|
||||||
|
@temp_table_keyword_args.for_db("mysql", "mariadb")
|
||||||
|
def _mysql_temp_table_keyword_args(cfg, eng):
|
||||||
|
return {"prefixes": ["TEMPORARY"]}
|
||||||
|
|
||||||
|
|
||||||
|
@upsert.for_db("mariadb")
|
||||||
|
def _upsert(
|
||||||
|
cfg,
|
||||||
|
table,
|
||||||
|
returning,
|
||||||
|
*,
|
||||||
|
set_lambda=None,
|
||||||
|
sort_by_parameter_order=False,
|
||||||
|
index_elements=None,
|
||||||
|
):
|
||||||
|
from sqlalchemy.dialects.mysql import insert
|
||||||
|
|
||||||
|
stmt = insert(table)
|
||||||
|
|
||||||
|
if set_lambda:
|
||||||
|
stmt = stmt.on_duplicate_key_update(**set_lambda(stmt.inserted))
|
||||||
|
else:
|
||||||
|
pk1 = table.primary_key.c[0]
|
||||||
|
stmt = stmt.on_duplicate_key_update({pk1.key: pk1})
|
||||||
|
|
||||||
|
stmt = stmt.returning(
|
||||||
|
*returning, sort_by_parameter_order=sort_by_parameter_order
|
||||||
|
)
|
||||||
|
return stmt
|
||||||
|
|
||||||
|
|
||||||
|
@delete_from_all_tables.for_db("mysql", "mariadb")
|
||||||
|
def _delete_from_all_tables(connection, cfg, metadata):
|
||||||
|
connection.exec_driver_sql("SET foreign_key_checks = 0")
|
||||||
|
try:
|
||||||
|
delete_from_all_tables.call_original(connection, cfg, metadata)
|
||||||
|
finally:
|
||||||
|
connection.exec_driver_sql("SET foreign_key_checks = 1")
|
||||||
|
|
||||||
|
|
||||||
|
@allow_stale_update_impl.for_db("mariadb")
|
||||||
|
def _allow_stale_update_impl(cfg):
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def go():
|
||||||
|
@event.listens_for(cfg.db, "engine_connect")
|
||||||
|
def turn_off_snapshot_isolation(conn):
|
||||||
|
conn.exec_driver_sql("SET innodb_snapshot_isolation = 'OFF'")
|
||||||
|
conn.rollback()
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
event.remove(cfg.db, "engine_connect", turn_off_snapshot_isolation)
|
||||||
|
|
||||||
|
# dispose the pool; quick way to just have those reset
|
||||||
|
cfg.db.dispose()
|
||||||
|
|
||||||
|
return go()
|
||||||
179
venv/Lib/site-packages/sqlalchemy/dialects/mysql/pymysql.py
Normal file
179
venv/Lib/site-packages/sqlalchemy/dialects/mysql/pymysql.py
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# dialects/mysql/pymysql.py
|
||||||
|
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
||||||
|
# <see AUTHORS file>
|
||||||
|
#
|
||||||
|
# This module is part of SQLAlchemy and is released under
|
||||||
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
||||||
|
|
||||||
|
r"""
|
||||||
|
|
||||||
|
.. dialect:: mysql+pymysql
|
||||||
|
:name: PyMySQL
|
||||||
|
:dbapi: pymysql
|
||||||
|
:connectstring: mysql+pymysql://<username>:<password>@<host>/<dbname>[?<options>]
|
||||||
|
:url: https://pymysql.readthedocs.io/
|
||||||
|
|
||||||
|
Unicode
|
||||||
|
-------
|
||||||
|
|
||||||
|
Please see :ref:`mysql_unicode` for current recommendations on unicode
|
||||||
|
handling.
|
||||||
|
|
||||||
|
.. _pymysql_ssl:
|
||||||
|
|
||||||
|
SSL Connections
|
||||||
|
------------------
|
||||||
|
|
||||||
|
The PyMySQL DBAPI accepts the same SSL arguments as that of MySQLdb,
|
||||||
|
described at :ref:`mysqldb_ssl`. See that section for additional examples.
|
||||||
|
|
||||||
|
If the server uses an automatically-generated certificate that is self-signed
|
||||||
|
or does not match the host name (as seen from the client), it may also be
|
||||||
|
necessary to indicate ``ssl_check_hostname=false`` in PyMySQL::
|
||||||
|
|
||||||
|
connection_uri = (
|
||||||
|
"mysql+pymysql://scott:tiger@192.168.0.134/test"
|
||||||
|
"?ssl_ca=/home/gord/client-ssl/ca.pem"
|
||||||
|
"&ssl_cert=/home/gord/client-ssl/client-cert.pem"
|
||||||
|
"&ssl_key=/home/gord/client-ssl/client-key.pem"
|
||||||
|
"&ssl_check_hostname=false"
|
||||||
|
)
|
||||||
|
|
||||||
|
MySQL-Python Compatibility
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
The pymysql DBAPI is a pure Python port of the MySQL-python (MySQLdb) driver,
|
||||||
|
and targets 100% compatibility. Most behavioral notes for MySQL-python apply
|
||||||
|
to the pymysql driver as well.
|
||||||
|
|
||||||
|
""" # noqa
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from typing import Dict
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Type
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from .mysqldb import MySQLDialect_mysqldb
|
||||||
|
from ...util import langhelpers
|
||||||
|
from ...util.typing import Literal
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
from ...engine.interfaces import ConnectArgsType
|
||||||
|
from ...engine.interfaces import DBAPIConnection
|
||||||
|
from ...engine.interfaces import DBAPICursor
|
||||||
|
from ...engine.interfaces import DBAPIModule
|
||||||
|
from ...engine.interfaces import PoolProxiedConnection
|
||||||
|
from ...engine.url import URL
|
||||||
|
|
||||||
|
|
||||||
|
def _connection_ping_reconnects_true(connection_cls: Type[Any]) -> bool:
|
||||||
|
"""Given a Connection class like pymysql.Connection, aiomysql.Connection,
|
||||||
|
asyncmy.Connection, inspect the ping() method and determine if it
|
||||||
|
has a "reconnect" parameter that either defaults to True, or is positional.
|
||||||
|
|
||||||
|
a return value of True here means that when we call ``connection.ping()``,
|
||||||
|
we **must** pass `reconnect=False`. a return value of False means that
|
||||||
|
we should call ``connection.ping()`` with **no arguments**.
|
||||||
|
|
||||||
|
This routine originates from issue #10492 for pymysql, however arg
|
||||||
|
signature mismatches in aiomysql/asyncmy tracked by issue #13306
|
||||||
|
necessitated a more open ended function.
|
||||||
|
|
||||||
|
"""
|
||||||
|
insp = langhelpers.get_callable_argspec(connection_cls.ping)
|
||||||
|
try:
|
||||||
|
reconnect_arg = insp.args[1]
|
||||||
|
except IndexError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return reconnect_arg == "reconnect" and (
|
||||||
|
not insp.defaults or insp.defaults[0] is not False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MySQLDialect_pymysql(MySQLDialect_mysqldb):
|
||||||
|
driver = "pymysql"
|
||||||
|
supports_statement_cache = True
|
||||||
|
|
||||||
|
description_encoding = None
|
||||||
|
|
||||||
|
@langhelpers.memoized_property
|
||||||
|
def supports_server_side_cursors(self) -> bool:
|
||||||
|
try:
|
||||||
|
cursors = __import__("pymysql.cursors").cursors
|
||||||
|
self._sscursor = cursors.SSCursor
|
||||||
|
return True
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_dbapi(cls) -> DBAPIModule:
|
||||||
|
return __import__("pymysql")
|
||||||
|
|
||||||
|
@langhelpers.memoized_property
|
||||||
|
def _send_false_to_ping(self) -> bool:
|
||||||
|
"""determine if pymysql has deprecated, changed the default of,
|
||||||
|
or removed the 'reconnect' argument of connection.ping().
|
||||||
|
|
||||||
|
See #10492 and
|
||||||
|
https://github.com/PyMySQL/mysqlclient/discussions/651#discussioncomment-7308971
|
||||||
|
for background.
|
||||||
|
|
||||||
|
Revised as part of #13306
|
||||||
|
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
try:
|
||||||
|
Connection = __import__(
|
||||||
|
"pymysql.connections"
|
||||||
|
).connections.Connection
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return _connection_ping_reconnects_true(Connection)
|
||||||
|
|
||||||
|
def do_ping(self, dbapi_connection: DBAPIConnection) -> Literal[True]:
|
||||||
|
if self._send_false_to_ping:
|
||||||
|
dbapi_connection.ping(False)
|
||||||
|
else:
|
||||||
|
dbapi_connection.ping()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_connect_args(
|
||||||
|
self, url: URL, _translate_args: Optional[Dict[str, Any]] = None
|
||||||
|
) -> ConnectArgsType:
|
||||||
|
if _translate_args is None:
|
||||||
|
_translate_args = dict(username="user")
|
||||||
|
return super().create_connect_args(
|
||||||
|
url, _translate_args=_translate_args
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_disconnect(
|
||||||
|
self,
|
||||||
|
e: DBAPIModule.Error,
|
||||||
|
connection: Optional[Union[PoolProxiedConnection, DBAPIConnection]],
|
||||||
|
cursor: Optional[DBAPICursor],
|
||||||
|
) -> bool:
|
||||||
|
if super().is_disconnect(e, connection, cursor):
|
||||||
|
return True
|
||||||
|
elif isinstance(e, self.loaded_dbapi.Error):
|
||||||
|
str_e = str(e).lower()
|
||||||
|
return (
|
||||||
|
"already closed" in str_e or "connection was killed" in str_e
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _extract_error_code(self, exception: BaseException) -> Any:
|
||||||
|
if isinstance(exception.args[0], Exception):
|
||||||
|
exception = exception.args[0]
|
||||||
|
return exception.args[0]
|
||||||
|
|
||||||
|
|
||||||
|
dialect = MySQLDialect_pymysql
|
||||||
Reference in New Issue
Block a user