mysqlconnector.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. # dialects/mysql/mysqlconnector.py
  2. # Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: https://www.opensource.org/licenses/mit-license.php
  7. r"""
  8. .. dialect:: mysql+mysqlconnector
  9. :name: MySQL Connector/Python
  10. :dbapi: myconnpy
  11. :connectstring: mysql+mysqlconnector://<user>:<password>@<host>[:<port>]/<dbname>
  12. :url: https://pypi.org/project/mysql-connector-python/
  13. Driver Status
  14. -------------
  15. MySQL Connector/Python is supported as of SQLAlchemy 2.0.39 to the
  16. degree which the driver is functional. There are still ongoing issues
  17. with features such as server side cursors which remain disabled until
  18. upstream issues are repaired.
  19. .. warning:: The MySQL Connector/Python driver published by Oracle is subject
  20. to frequent, major regressions of essential functionality such as being able
  21. to correctly persist simple binary strings which indicate it is not well
  22. tested. The SQLAlchemy project is not able to maintain this dialect fully as
  23. regressions in the driver prevent it from being included in continuous
  24. integration.
  25. .. versionchanged:: 2.0.39
  26. The MySQL Connector/Python dialect has been updated to support the
  27. latest version of this DBAPI. Previously, MySQL Connector/Python
  28. was not fully supported. However, support remains limited due to ongoing
  29. regressions introduced in this driver.
  30. Connecting to MariaDB with MySQL Connector/Python
  31. --------------------------------------------------
  32. MySQL Connector/Python may attempt to pass an incompatible collation to the
  33. database when connecting to MariaDB. Experimentation has shown that using
  34. ``?charset=utf8mb4&collation=utfmb4_general_ci`` or similar MariaDB-compatible
  35. charset/collation will allow connectivity.
  36. """ # noqa
  37. from __future__ import annotations
  38. import re
  39. from typing import Any
  40. from typing import cast
  41. from typing import Optional
  42. from typing import Sequence
  43. from typing import Tuple
  44. from typing import TYPE_CHECKING
  45. from typing import Union
  46. from .base import MariaDBIdentifierPreparer
  47. from .base import MySQLCompiler
  48. from .base import MySQLDialect
  49. from .base import MySQLExecutionContext
  50. from .base import MySQLIdentifierPreparer
  51. from .mariadb import MariaDBDialect
  52. from .types import BIT
  53. from ... import util
  54. if TYPE_CHECKING:
  55. from ...engine.base import Connection
  56. from ...engine.cursor import CursorResult
  57. from ...engine.interfaces import ConnectArgsType
  58. from ...engine.interfaces import DBAPIConnection
  59. from ...engine.interfaces import DBAPICursor
  60. from ...engine.interfaces import DBAPIModule
  61. from ...engine.interfaces import IsolationLevel
  62. from ...engine.interfaces import PoolProxiedConnection
  63. from ...engine.row import Row
  64. from ...engine.url import URL
  65. from ...sql.elements import BinaryExpression
  66. class MySQLExecutionContext_mysqlconnector(MySQLExecutionContext):
  67. def create_server_side_cursor(self) -> DBAPICursor:
  68. return self._dbapi_connection.cursor(buffered=False)
  69. def create_default_cursor(self) -> DBAPICursor:
  70. return self._dbapi_connection.cursor(buffered=True)
  71. class MySQLCompiler_mysqlconnector(MySQLCompiler):
  72. def visit_mod_binary(
  73. self, binary: BinaryExpression[Any], operator: Any, **kw: Any
  74. ) -> str:
  75. return (
  76. self.process(binary.left, **kw)
  77. + " % "
  78. + self.process(binary.right, **kw)
  79. )
  80. class IdentifierPreparerCommon_mysqlconnector:
  81. @property
  82. def _double_percents(self) -> bool:
  83. return False
  84. @_double_percents.setter
  85. def _double_percents(self, value: Any) -> None:
  86. pass
  87. def _escape_identifier(self, value: str) -> str:
  88. value = value.replace(
  89. self.escape_quote, # type:ignore[attr-defined]
  90. self.escape_to_quote, # type:ignore[attr-defined]
  91. )
  92. return value
  93. class MySQLIdentifierPreparer_mysqlconnector(
  94. IdentifierPreparerCommon_mysqlconnector, MySQLIdentifierPreparer
  95. ):
  96. pass
  97. class MariaDBIdentifierPreparer_mysqlconnector(
  98. IdentifierPreparerCommon_mysqlconnector, MariaDBIdentifierPreparer
  99. ):
  100. pass
  101. class _myconnpyBIT(BIT):
  102. def result_processor(self, dialect: Any, coltype: Any) -> None:
  103. """MySQL-connector already converts mysql bits, so."""
  104. return None
  105. class MySQLDialect_mysqlconnector(MySQLDialect):
  106. driver = "mysqlconnector"
  107. supports_statement_cache = True
  108. supports_sane_rowcount = True
  109. supports_sane_multi_rowcount = True
  110. supports_native_decimal = True
  111. supports_native_bit = True
  112. # not until https://bugs.mysql.com/bug.php?id=117548
  113. supports_server_side_cursors = False
  114. default_paramstyle = "format"
  115. statement_compiler = MySQLCompiler_mysqlconnector
  116. execution_ctx_cls = MySQLExecutionContext_mysqlconnector
  117. preparer: type[MySQLIdentifierPreparer] = (
  118. MySQLIdentifierPreparer_mysqlconnector
  119. )
  120. colspecs = util.update_copy(MySQLDialect.colspecs, {BIT: _myconnpyBIT})
  121. @classmethod
  122. def import_dbapi(cls) -> DBAPIModule:
  123. return cast("DBAPIModule", __import__("mysql.connector").connector)
  124. def do_ping(self, dbapi_connection: DBAPIConnection) -> bool:
  125. dbapi_connection.ping(False)
  126. return True
  127. def create_connect_args(self, url: URL) -> ConnectArgsType:
  128. opts = url.translate_connect_args(username="user")
  129. opts.update(url.query)
  130. util.coerce_kw_type(opts, "allow_local_infile", bool)
  131. util.coerce_kw_type(opts, "autocommit", bool)
  132. util.coerce_kw_type(opts, "buffered", bool)
  133. util.coerce_kw_type(opts, "client_flag", int)
  134. util.coerce_kw_type(opts, "compress", bool)
  135. util.coerce_kw_type(opts, "connection_timeout", int)
  136. util.coerce_kw_type(opts, "connect_timeout", int)
  137. util.coerce_kw_type(opts, "consume_results", bool)
  138. util.coerce_kw_type(opts, "force_ipv6", bool)
  139. util.coerce_kw_type(opts, "get_warnings", bool)
  140. util.coerce_kw_type(opts, "pool_reset_session", bool)
  141. util.coerce_kw_type(opts, "pool_size", int)
  142. util.coerce_kw_type(opts, "raise_on_warnings", bool)
  143. util.coerce_kw_type(opts, "raw", bool)
  144. util.coerce_kw_type(opts, "ssl_verify_cert", bool)
  145. util.coerce_kw_type(opts, "use_pure", bool)
  146. util.coerce_kw_type(opts, "use_unicode", bool)
  147. # note that "buffered" is set to False by default in MySQL/connector
  148. # python. If you set it to True, then there is no way to get a server
  149. # side cursor because the logic is written to disallow that.
  150. # leaving this at True until
  151. # https://bugs.mysql.com/bug.php?id=117548 can be fixed
  152. opts["buffered"] = True
  153. # FOUND_ROWS must be set in ClientFlag to enable
  154. # supports_sane_rowcount.
  155. if self.dbapi is not None:
  156. try:
  157. from mysql.connector import constants # type: ignore
  158. ClientFlag = constants.ClientFlag
  159. client_flags = opts.get(
  160. "client_flags", ClientFlag.get_default()
  161. )
  162. client_flags |= ClientFlag.FOUND_ROWS
  163. opts["client_flags"] = client_flags
  164. except Exception:
  165. pass
  166. return [], opts
  167. @util.memoized_property
  168. def _mysqlconnector_version_info(self) -> Optional[Tuple[int, ...]]:
  169. if self.dbapi and hasattr(self.dbapi, "__version__"):
  170. m = re.match(r"(\d+)\.(\d+)(?:\.(\d+))?", self.dbapi.__version__)
  171. if m:
  172. return tuple(int(x) for x in m.group(1, 2, 3) if x is not None)
  173. return None
  174. def _detect_charset(self, connection: Connection) -> str:
  175. return connection.connection.charset # type: ignore
  176. def _extract_error_code(self, exception: BaseException) -> int:
  177. return exception.errno # type: ignore
  178. def is_disconnect(
  179. self,
  180. e: Exception,
  181. connection: Optional[Union[PoolProxiedConnection, DBAPIConnection]],
  182. cursor: Optional[DBAPICursor],
  183. ) -> bool:
  184. errnos = (2006, 2013, 2014, 2045, 2055, 2048)
  185. exceptions = (
  186. self.loaded_dbapi.OperationalError, #
  187. self.loaded_dbapi.InterfaceError,
  188. self.loaded_dbapi.ProgrammingError,
  189. )
  190. if isinstance(e, exceptions):
  191. return (
  192. e.errno in errnos
  193. or "MySQL Connection not available." in str(e)
  194. or "Connection to MySQL is not available" in str(e)
  195. )
  196. else:
  197. return False
  198. def _compat_fetchall(
  199. self,
  200. rp: CursorResult[Tuple[Any, ...]],
  201. charset: Optional[str] = None,
  202. ) -> Sequence[Row[Tuple[Any, ...]]]:
  203. return rp.fetchall()
  204. def _compat_fetchone(
  205. self,
  206. rp: CursorResult[Tuple[Any, ...]],
  207. charset: Optional[str] = None,
  208. ) -> Optional[Row[Tuple[Any, ...]]]:
  209. return rp.fetchone()
  210. def get_isolation_level_values(
  211. self, dbapi_conn: DBAPIConnection
  212. ) -> Sequence[IsolationLevel]:
  213. return (
  214. "SERIALIZABLE",
  215. "READ UNCOMMITTED",
  216. "READ COMMITTED",
  217. "REPEATABLE READ",
  218. "AUTOCOMMIT",
  219. )
  220. def detect_autocommit_setting(self, dbapi_conn: DBAPIConnection) -> bool:
  221. return bool(dbapi_conn.autocommit)
  222. def set_isolation_level(
  223. self, dbapi_connection: DBAPIConnection, level: IsolationLevel
  224. ) -> None:
  225. if level == "AUTOCOMMIT":
  226. dbapi_connection.autocommit = True
  227. else:
  228. dbapi_connection.autocommit = False
  229. super().set_isolation_level(dbapi_connection, level)
  230. class MariaDBDialect_mysqlconnector(
  231. MariaDBDialect, MySQLDialect_mysqlconnector
  232. ):
  233. supports_statement_cache = True
  234. _allows_uuid_binds = False
  235. preparer = MariaDBIdentifierPreparer_mysqlconnector
  236. dialect = MySQLDialect_mysqlconnector
  237. mariadb_dialect = MariaDBDialect_mysqlconnector