Загрузить файлы в «venv/Lib/site-packages/sqlalchemy/dialects/mysql»

This commit is contained in:
2026-07-02 20:22:00 +00:00
parent c5a6188804
commit fa2c5b9cee
5 changed files with 1273 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View 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

View 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()

View 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