mysqldb.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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. # mypy: ignore-errors
  8. """
  9. .. dialect:: mysql+mysqldb
  10. :name: mysqlclient (maintained fork of MySQL-Python)
  11. :dbapi: mysqldb
  12. :connectstring: mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>
  13. :url: https://pypi.org/project/mysqlclient/
  14. Driver Status
  15. -------------
  16. The mysqlclient DBAPI is a maintained fork of the
  17. `MySQL-Python <https://sourceforge.net/projects/mysql-python>`_ DBAPI
  18. that is no longer maintained. `mysqlclient`_ supports Python 2 and Python 3
  19. and is very stable.
  20. .. _mysqlclient: https://github.com/PyMySQL/mysqlclient-python
  21. .. _mysqldb_unicode:
  22. Unicode
  23. -------
  24. Please see :ref:`mysql_unicode` for current recommendations on unicode
  25. handling.
  26. .. _mysqldb_ssl:
  27. SSL Connections
  28. ----------------
  29. The mysqlclient and PyMySQL DBAPIs accept an additional dictionary under the
  30. key "ssl", which may be specified using the
  31. :paramref:`_sa.create_engine.connect_args` dictionary::
  32. engine = create_engine(
  33. "mysql+mysqldb://scott:tiger@192.168.0.134/test",
  34. connect_args={
  35. "ssl": {
  36. "ca": "/home/gord/client-ssl/ca.pem",
  37. "cert": "/home/gord/client-ssl/client-cert.pem",
  38. "key": "/home/gord/client-ssl/client-key.pem",
  39. }
  40. },
  41. )
  42. For convenience, the following keys may also be specified inline within the URL
  43. where they will be interpreted into the "ssl" dictionary automatically:
  44. "ssl_ca", "ssl_cert", "ssl_key", "ssl_capath", "ssl_cipher",
  45. "ssl_check_hostname". An example is as follows::
  46. connection_uri = (
  47. "mysql+mysqldb://scott:tiger@192.168.0.134/test"
  48. "?ssl_ca=/home/gord/client-ssl/ca.pem"
  49. "&ssl_cert=/home/gord/client-ssl/client-cert.pem"
  50. "&ssl_key=/home/gord/client-ssl/client-key.pem"
  51. )
  52. .. seealso::
  53. :ref:`pymysql_ssl` in the PyMySQL dialect
  54. Using MySQLdb with Google Cloud SQL
  55. -----------------------------------
  56. Google Cloud SQL now recommends use of the MySQLdb dialect. Connect
  57. using a URL like the following:
  58. .. sourcecode:: text
  59. mysql+mysqldb://root@/<dbname>?unix_socket=/cloudsql/<projectid>:<instancename>
  60. Server Side Cursors
  61. -------------------
  62. The mysqldb dialect supports server-side cursors. See :ref:`mysql_ss_cursors`.
  63. """
  64. import re
  65. from .base import MySQLCompiler
  66. from .base import MySQLDialect
  67. from .base import MySQLExecutionContext
  68. from .base import MySQLIdentifierPreparer
  69. from .base import TEXT
  70. from ... import sql
  71. from ... import util
  72. class MySQLExecutionContext_mysqldb(MySQLExecutionContext):
  73. pass
  74. class MySQLCompiler_mysqldb(MySQLCompiler):
  75. pass
  76. class MySQLDialect_mysqldb(MySQLDialect):
  77. driver = "mysqldb"
  78. supports_statement_cache = True
  79. supports_unicode_statements = True
  80. supports_sane_rowcount = True
  81. supports_sane_multi_rowcount = True
  82. supports_native_decimal = True
  83. default_paramstyle = "format"
  84. execution_ctx_cls = MySQLExecutionContext_mysqldb
  85. statement_compiler = MySQLCompiler_mysqldb
  86. preparer = MySQLIdentifierPreparer
  87. def __init__(self, **kwargs):
  88. super().__init__(**kwargs)
  89. self._mysql_dbapi_version = (
  90. self._parse_dbapi_version(self.dbapi.__version__)
  91. if self.dbapi is not None and hasattr(self.dbapi, "__version__")
  92. else (0, 0, 0)
  93. )
  94. def _parse_dbapi_version(self, version):
  95. m = re.match(r"(\d+)\.(\d+)(?:\.(\d+))?", version)
  96. if m:
  97. return tuple(int(x) for x in m.group(1, 2, 3) if x is not None)
  98. else:
  99. return (0, 0, 0)
  100. @util.langhelpers.memoized_property
  101. def supports_server_side_cursors(self):
  102. try:
  103. cursors = __import__("MySQLdb.cursors").cursors
  104. self._sscursor = cursors.SSCursor
  105. return True
  106. except (ImportError, AttributeError):
  107. return False
  108. @classmethod
  109. def import_dbapi(cls):
  110. return __import__("MySQLdb")
  111. def on_connect(self):
  112. super_ = super().on_connect()
  113. def on_connect(conn):
  114. if super_ is not None:
  115. super_(conn)
  116. charset_name = conn.character_set_name()
  117. if charset_name is not None:
  118. cursor = conn.cursor()
  119. cursor.execute("SET NAMES %s" % charset_name)
  120. cursor.close()
  121. return on_connect
  122. def do_ping(self, dbapi_connection):
  123. dbapi_connection.ping()
  124. return True
  125. def do_executemany(self, cursor, statement, parameters, context=None):
  126. rowcount = cursor.executemany(statement, parameters)
  127. if context is not None:
  128. context._rowcount = rowcount
  129. def _check_unicode_returns(self, connection):
  130. # work around issue fixed in
  131. # https://github.com/farcepest/MySQLdb1/commit/cd44524fef63bd3fcb71947392326e9742d520e8
  132. # specific issue w/ the utf8mb4_bin collation and unicode returns
  133. collation = connection.exec_driver_sql(
  134. "show collation where %s = 'utf8mb4' and %s = 'utf8mb4_bin'"
  135. % (
  136. self.identifier_preparer.quote("Charset"),
  137. self.identifier_preparer.quote("Collation"),
  138. )
  139. ).scalar()
  140. has_utf8mb4_bin = self.server_version_info > (5,) and collation
  141. if has_utf8mb4_bin:
  142. additional_tests = [
  143. sql.collate(
  144. sql.cast(
  145. sql.literal_column("'test collated returns'"),
  146. TEXT(charset="utf8mb4"),
  147. ),
  148. "utf8mb4_bin",
  149. )
  150. ]
  151. else:
  152. additional_tests = []
  153. return super()._check_unicode_returns(connection, additional_tests)
  154. def create_connect_args(self, url, _translate_args=None):
  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):
  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
  208. else:
  209. return None
  210. def _extract_error_code(self, exception):
  211. return exception.args[0]
  212. def _detect_charset(self, connection):
  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 = connection.connection.character_set_name
  218. except AttributeError:
  219. util.warn(
  220. "No 'character_set_name' can be detected with "
  221. "this MySQL-Python version; "
  222. "please upgrade to a recent version of MySQL-Python. "
  223. "Assuming latin1."
  224. )
  225. return "latin1"
  226. else:
  227. return cset_name()
  228. def get_isolation_level_values(self, dbapi_connection):
  229. return (
  230. "SERIALIZABLE",
  231. "READ UNCOMMITTED",
  232. "READ COMMITTED",
  233. "REPEATABLE READ",
  234. "AUTOCOMMIT",
  235. )
  236. def set_isolation_level(self, dbapi_connection, level):
  237. if level == "AUTOCOMMIT":
  238. dbapi_connection.autocommit(True)
  239. else:
  240. dbapi_connection.autocommit(False)
  241. super().set_isolation_level(dbapi_connection, level)
  242. dialect = MySQLDialect_mysqldb