mysqldb.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. # dialects/mysql/mysqldb.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. """
  8. .. dialect:: mysql+mysqldb
  9. :name: mysqlclient (maintained fork of MySQL-Python)
  10. :dbapi: mysqldb
  11. :connectstring: mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>
  12. :url: https://pypi.org/project/mysqlclient/
  13. Driver Status
  14. -------------
  15. The mysqlclient DBAPI is a maintained fork of the
  16. `MySQL-Python <https://sourceforge.net/projects/mysql-python>`_ DBAPI
  17. that is no longer maintained. `mysqlclient`_ supports Python 2 and Python 3
  18. and is very stable.
  19. .. _mysqlclient: https://github.com/PyMySQL/mysqlclient-python
  20. .. _mysqldb_unicode:
  21. Unicode
  22. -------
  23. Please see :ref:`mysql_unicode` for current recommendations on unicode
  24. handling.
  25. .. _mysqldb_ssl:
  26. SSL Connections
  27. ----------------
  28. The mysqlclient and PyMySQL DBAPIs accept an additional dictionary under the
  29. key "ssl", which may be specified using the
  30. :paramref:`_sa.create_engine.connect_args` dictionary::
  31. engine = create_engine(
  32. "mysql+mysqldb://scott:tiger@192.168.0.134/test",
  33. connect_args={
  34. "ssl": {
  35. "ca": "/home/gord/client-ssl/ca.pem",
  36. "cert": "/home/gord/client-ssl/client-cert.pem",
  37. "key": "/home/gord/client-ssl/client-key.pem",
  38. }
  39. },
  40. )
  41. For convenience, the following keys may also be specified inline within the URL
  42. where they will be interpreted into the "ssl" dictionary automatically:
  43. "ssl_ca", "ssl_cert", "ssl_key", "ssl_capath", "ssl_cipher",
  44. "ssl_check_hostname". An example is as follows::
  45. connection_uri = (
  46. "mysql+mysqldb://scott:tiger@192.168.0.134/test"
  47. "?ssl_ca=/home/gord/client-ssl/ca.pem"
  48. "&ssl_cert=/home/gord/client-ssl/client-cert.pem"
  49. "&ssl_key=/home/gord/client-ssl/client-key.pem"
  50. )
  51. .. seealso::
  52. :ref:`pymysql_ssl` in the PyMySQL dialect
  53. Using MySQLdb with Google Cloud SQL
  54. -----------------------------------
  55. Google Cloud SQL now recommends use of the MySQLdb dialect. Connect
  56. using a URL like the following:
  57. .. sourcecode:: text
  58. mysql+mysqldb://root@/<dbname>?unix_socket=/cloudsql/<projectid>:<instancename>
  59. Server Side Cursors
  60. -------------------
  61. The mysqldb dialect supports server-side cursors. See :ref:`mysql_ss_cursors`.
  62. """
  63. from __future__ import annotations
  64. import re
  65. from typing import Any
  66. from typing import Callable
  67. from typing import cast
  68. from typing import Dict
  69. from typing import Optional
  70. from typing import Tuple
  71. from typing import TYPE_CHECKING
  72. from .base import MySQLCompiler
  73. from .base import MySQLDialect
  74. from .base import MySQLExecutionContext
  75. from .base import MySQLIdentifierPreparer
  76. from ... import util
  77. from ...util.typing import Literal
  78. if TYPE_CHECKING:
  79. from ...engine.base import Connection
  80. from ...engine.interfaces import _DBAPIMultiExecuteParams
  81. from ...engine.interfaces import ConnectArgsType
  82. from ...engine.interfaces import DBAPIConnection
  83. from ...engine.interfaces import DBAPICursor
  84. from ...engine.interfaces import DBAPIModule
  85. from ...engine.interfaces import ExecutionContext
  86. from ...engine.interfaces import IsolationLevel
  87. from ...engine.url import URL
  88. class MySQLExecutionContext_mysqldb(MySQLExecutionContext):
  89. pass
  90. class MySQLCompiler_mysqldb(MySQLCompiler):
  91. pass
  92. class MySQLDialect_mysqldb(MySQLDialect):
  93. driver = "mysqldb"
  94. supports_statement_cache = True
  95. supports_unicode_statements = True
  96. supports_sane_rowcount = True
  97. supports_sane_multi_rowcount = True
  98. supports_native_decimal = True
  99. default_paramstyle = "format"
  100. execution_ctx_cls = MySQLExecutionContext_mysqldb
  101. statement_compiler = MySQLCompiler_mysqldb
  102. preparer = MySQLIdentifierPreparer
  103. server_version_info: Tuple[int, ...]
  104. def __init__(self, **kwargs: Any):
  105. super().__init__(**kwargs)
  106. self._mysql_dbapi_version = (
  107. self._parse_dbapi_version(self.dbapi.__version__)
  108. if self.dbapi is not None and hasattr(self.dbapi, "__version__")
  109. else (0, 0, 0)
  110. )
  111. def _parse_dbapi_version(self, version: str) -> Tuple[int, ...]:
  112. m = re.match(r"(\d+)\.(\d+)(?:\.(\d+))?", version)
  113. if m:
  114. return tuple(int(x) for x in m.group(1, 2, 3) if x is not None)
  115. else:
  116. return (0, 0, 0)
  117. @util.langhelpers.memoized_property
  118. def supports_server_side_cursors(self) -> bool:
  119. try:
  120. cursors = __import__("MySQLdb.cursors").cursors
  121. self._sscursor = cursors.SSCursor
  122. return True
  123. except (ImportError, AttributeError):
  124. return False
  125. @classmethod
  126. def import_dbapi(cls) -> DBAPIModule:
  127. return __import__("MySQLdb")
  128. def on_connect(self) -> Callable[[DBAPIConnection], None]:
  129. super_ = super().on_connect()
  130. def on_connect(conn: DBAPIConnection) -> None:
  131. if super_ is not None:
  132. super_(conn)
  133. charset_name = conn.character_set_name()
  134. if charset_name is not None:
  135. cursor = conn.cursor()
  136. cursor.execute("SET NAMES %s" % charset_name)
  137. cursor.close()
  138. return on_connect
  139. def do_ping(self, dbapi_connection: DBAPIConnection) -> Literal[True]:
  140. dbapi_connection.ping()
  141. return True
  142. def do_executemany(
  143. self,
  144. cursor: DBAPICursor,
  145. statement: str,
  146. parameters: _DBAPIMultiExecuteParams,
  147. context: Optional[ExecutionContext] = None,
  148. ) -> None:
  149. rowcount = cursor.executemany(statement, parameters)
  150. if context is not None:
  151. cast(MySQLExecutionContext, context)._rowcount = rowcount
  152. def create_connect_args(
  153. self, url: URL, _translate_args: Optional[Dict[str, Any]] = None
  154. ) -> ConnectArgsType:
  155. if _translate_args is None:
  156. _translate_args = dict(
  157. database="db", username="user", password="passwd"
  158. )
  159. opts = url.translate_connect_args(**_translate_args)
  160. opts.update(url.query)
  161. util.coerce_kw_type(opts, "compress", bool)
  162. util.coerce_kw_type(opts, "connect_timeout", int)
  163. util.coerce_kw_type(opts, "read_timeout", int)
  164. util.coerce_kw_type(opts, "write_timeout", int)
  165. util.coerce_kw_type(opts, "client_flag", int)
  166. util.coerce_kw_type(opts, "local_infile", bool)
  167. # Note: using either of the below will cause all strings to be
  168. # returned as Unicode, both in raw SQL operations and with column
  169. # types like String and MSString.
  170. util.coerce_kw_type(opts, "use_unicode", bool)
  171. util.coerce_kw_type(opts, "charset", str)
  172. # Rich values 'cursorclass' and 'conv' are not supported via
  173. # query string.
  174. ssl = {}
  175. keys = [
  176. ("ssl_ca", str),
  177. ("ssl_key", str),
  178. ("ssl_cert", str),
  179. ("ssl_capath", str),
  180. ("ssl_cipher", str),
  181. ("ssl_check_hostname", bool),
  182. ]
  183. for key, kw_type in keys:
  184. if key in opts:
  185. ssl[key[4:]] = opts[key]
  186. util.coerce_kw_type(ssl, key[4:], kw_type)
  187. del opts[key]
  188. if ssl:
  189. opts["ssl"] = ssl
  190. # FOUND_ROWS must be set in CLIENT_FLAGS to enable
  191. # supports_sane_rowcount.
  192. client_flag = opts.get("client_flag", 0)
  193. client_flag_found_rows = self._found_rows_client_flag()
  194. if client_flag_found_rows is not None:
  195. client_flag |= client_flag_found_rows
  196. opts["client_flag"] = client_flag
  197. return [], opts
  198. def _found_rows_client_flag(self) -> Optional[int]:
  199. if self.dbapi is not None:
  200. try:
  201. CLIENT_FLAGS = __import__(
  202. self.dbapi.__name__ + ".constants.CLIENT"
  203. ).constants.CLIENT
  204. except (AttributeError, ImportError):
  205. return None
  206. else:
  207. return CLIENT_FLAGS.FOUND_ROWS # type: ignore
  208. else:
  209. return None
  210. def _extract_error_code(self, exception: DBAPIModule.Error) -> int:
  211. return exception.args[0] # type: ignore[no-any-return]
  212. def _detect_charset(self, connection: Connection) -> str:
  213. """Sniff out the character set in use for connection results."""
  214. try:
  215. # note: the SQL here would be
  216. # "SHOW VARIABLES LIKE 'character_set%%'"
  217. cset_name: Callable[[], str] = (
  218. connection.connection.character_set_name
  219. )
  220. except AttributeError:
  221. util.warn(
  222. "No 'character_set_name' can be detected with "
  223. "this MySQL-Python version; "
  224. "please upgrade to a recent version of MySQL-Python. "
  225. "Assuming latin1."
  226. )
  227. return "latin1"
  228. else:
  229. return cset_name()
  230. def get_isolation_level_values(
  231. self, dbapi_conn: DBAPIConnection
  232. ) -> Tuple[IsolationLevel, ...]:
  233. return (
  234. "SERIALIZABLE",
  235. "READ UNCOMMITTED",
  236. "READ COMMITTED",
  237. "REPEATABLE READ",
  238. "AUTOCOMMIT",
  239. )
  240. def detect_autocommit_setting(self, dbapi_conn: DBAPIConnection) -> bool:
  241. return dbapi_conn.get_autocommit() # type: ignore[no-any-return]
  242. def set_isolation_level(
  243. self, dbapi_connection: DBAPIConnection, level: IsolationLevel
  244. ) -> None:
  245. if level == "AUTOCOMMIT":
  246. dbapi_connection.autocommit(True)
  247. else:
  248. dbapi_connection.autocommit(False)
  249. super().set_isolation_level(dbapi_connection, level)
  250. dialect = MySQLDialect_mysqldb