mysqldb.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. # mysql/mysqldb.py
  2. # Copyright (C) 2005-2022 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. "ssl_ca": "/home/gord/client-ssl/ca.pem",
  36. "ssl_cert": "/home/gord/client-ssl/client-cert.pem",
  37. "ssl_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. If the server uses an automatically-generated certificate that is self-signed
  52. or does not match the host name (as seen from the client), it may also be
  53. necessary to indicate ``ssl_check_hostname=false``::
  54. connection_uri = (
  55. "mysql+pymysql://scott:tiger@192.168.0.134/test"
  56. "?ssl_ca=/home/gord/client-ssl/ca.pem"
  57. "&ssl_cert=/home/gord/client-ssl/client-cert.pem"
  58. "&ssl_key=/home/gord/client-ssl/client-key.pem"
  59. "&ssl_check_hostname=false"
  60. )
  61. .. seealso::
  62. :ref:`pymysql_ssl` in the PyMySQL dialect
  63. Using MySQLdb with Google Cloud SQL
  64. -----------------------------------
  65. Google Cloud SQL now recommends use of the MySQLdb dialect. Connect
  66. using a URL like the following::
  67. mysql+mysqldb://root@/<dbname>?unix_socket=/cloudsql/<projectid>:<instancename>
  68. Server Side Cursors
  69. -------------------
  70. The mysqldb dialect supports server-side cursors. See :ref:`mysql_ss_cursors`.
  71. """
  72. import re
  73. from .base import MySQLCompiler
  74. from .base import MySQLDialect
  75. from .base import MySQLExecutionContext
  76. from .base import MySQLIdentifierPreparer
  77. from .base import TEXT
  78. from ... import sql
  79. from ... import util
  80. class MySQLExecutionContext_mysqldb(MySQLExecutionContext):
  81. @property
  82. def rowcount(self):
  83. if hasattr(self, "_rowcount"):
  84. return self._rowcount
  85. else:
  86. return self.cursor.rowcount
  87. class MySQLCompiler_mysqldb(MySQLCompiler):
  88. pass
  89. class MySQLDialect_mysqldb(MySQLDialect):
  90. driver = "mysqldb"
  91. supports_statement_cache = True
  92. supports_unicode_statements = True
  93. supports_sane_rowcount = True
  94. supports_sane_multi_rowcount = True
  95. supports_native_decimal = True
  96. default_paramstyle = "format"
  97. execution_ctx_cls = MySQLExecutionContext_mysqldb
  98. statement_compiler = MySQLCompiler_mysqldb
  99. preparer = MySQLIdentifierPreparer
  100. def __init__(self, **kwargs):
  101. super(MySQLDialect_mysqldb, self).__init__(**kwargs)
  102. self._mysql_dbapi_version = (
  103. self._parse_dbapi_version(self.dbapi.__version__)
  104. if self.dbapi is not None and hasattr(self.dbapi, "__version__")
  105. else (0, 0, 0)
  106. )
  107. def _parse_dbapi_version(self, version):
  108. m = re.match(r"(\d+)\.(\d+)(?:\.(\d+))?", version)
  109. if m:
  110. return tuple(int(x) for x in m.group(1, 2, 3) if x is not None)
  111. else:
  112. return (0, 0, 0)
  113. @util.langhelpers.memoized_property
  114. def supports_server_side_cursors(self):
  115. try:
  116. cursors = __import__("MySQLdb.cursors").cursors
  117. self._sscursor = cursors.SSCursor
  118. return True
  119. except (ImportError, AttributeError):
  120. return False
  121. @classmethod
  122. def dbapi(cls):
  123. return __import__("MySQLdb")
  124. def on_connect(self):
  125. super_ = super(MySQLDialect_mysqldb, self).on_connect()
  126. def on_connect(conn):
  127. if super_ is not None:
  128. super_(conn)
  129. charset_name = conn.character_set_name()
  130. if charset_name is not None:
  131. cursor = conn.cursor()
  132. cursor.execute("SET NAMES %s" % charset_name)
  133. cursor.close()
  134. return on_connect
  135. def do_ping(self, dbapi_connection):
  136. try:
  137. dbapi_connection.ping(False)
  138. except self.dbapi.Error as err:
  139. if self.is_disconnect(err, dbapi_connection, None):
  140. return False
  141. else:
  142. raise
  143. else:
  144. return True
  145. def do_executemany(self, cursor, statement, parameters, context=None):
  146. rowcount = cursor.executemany(statement, parameters)
  147. if context is not None:
  148. context._rowcount = rowcount
  149. def _check_unicode_returns(self, connection):
  150. # work around issue fixed in
  151. # https://github.com/farcepest/MySQLdb1/commit/cd44524fef63bd3fcb71947392326e9742d520e8
  152. # specific issue w/ the utf8mb4_bin collation and unicode returns
  153. collation = connection.exec_driver_sql(
  154. "show collation where %s = 'utf8mb4' and %s = 'utf8mb4_bin'"
  155. % (
  156. self.identifier_preparer.quote("Charset"),
  157. self.identifier_preparer.quote("Collation"),
  158. )
  159. ).scalar()
  160. has_utf8mb4_bin = self.server_version_info > (5,) and collation
  161. if has_utf8mb4_bin:
  162. additional_tests = [
  163. sql.collate(
  164. sql.cast(
  165. sql.literal_column("'test collated returns'"),
  166. TEXT(charset="utf8mb4"),
  167. ),
  168. "utf8mb4_bin",
  169. )
  170. ]
  171. else:
  172. additional_tests = []
  173. return super(MySQLDialect_mysqldb, self)._check_unicode_returns(
  174. connection, additional_tests
  175. )
  176. def create_connect_args(self, url, _translate_args=None):
  177. if _translate_args is None:
  178. _translate_args = dict(
  179. database="db", username="user", password="passwd"
  180. )
  181. opts = url.translate_connect_args(**_translate_args)
  182. opts.update(url.query)
  183. util.coerce_kw_type(opts, "compress", bool)
  184. util.coerce_kw_type(opts, "connect_timeout", int)
  185. util.coerce_kw_type(opts, "read_timeout", int)
  186. util.coerce_kw_type(opts, "write_timeout", int)
  187. util.coerce_kw_type(opts, "client_flag", int)
  188. util.coerce_kw_type(opts, "local_infile", int)
  189. # Note: using either of the below will cause all strings to be
  190. # returned as Unicode, both in raw SQL operations and with column
  191. # types like String and MSString.
  192. util.coerce_kw_type(opts, "use_unicode", bool)
  193. util.coerce_kw_type(opts, "charset", str)
  194. # Rich values 'cursorclass' and 'conv' are not supported via
  195. # query string.
  196. ssl = {}
  197. keys = [
  198. ("ssl_ca", str),
  199. ("ssl_key", str),
  200. ("ssl_cert", str),
  201. ("ssl_capath", str),
  202. ("ssl_cipher", str),
  203. ("ssl_check_hostname", bool),
  204. ]
  205. for key, kw_type in keys:
  206. if key in opts:
  207. ssl[key[4:]] = opts[key]
  208. util.coerce_kw_type(ssl, key[4:], kw_type)
  209. del opts[key]
  210. if ssl:
  211. opts["ssl"] = ssl
  212. # FOUND_ROWS must be set in CLIENT_FLAGS to enable
  213. # supports_sane_rowcount.
  214. client_flag = opts.get("client_flag", 0)
  215. client_flag_found_rows = self._found_rows_client_flag()
  216. if client_flag_found_rows is not None:
  217. client_flag |= client_flag_found_rows
  218. opts["client_flag"] = client_flag
  219. return [[], opts]
  220. def _found_rows_client_flag(self):
  221. if self.dbapi is not None:
  222. try:
  223. CLIENT_FLAGS = __import__(
  224. self.dbapi.__name__ + ".constants.CLIENT"
  225. ).constants.CLIENT
  226. except (AttributeError, ImportError):
  227. return None
  228. else:
  229. return CLIENT_FLAGS.FOUND_ROWS
  230. else:
  231. return None
  232. def _extract_error_code(self, exception):
  233. return exception.args[0]
  234. def _detect_charset(self, connection):
  235. """Sniff out the character set in use for connection results."""
  236. try:
  237. # note: the SQL here would be
  238. # "SHOW VARIABLES LIKE 'character_set%%'"
  239. cset_name = connection.connection.character_set_name
  240. except AttributeError:
  241. util.warn(
  242. "No 'character_set_name' can be detected with "
  243. "this MySQL-Python version; "
  244. "please upgrade to a recent version of MySQL-Python. "
  245. "Assuming latin1."
  246. )
  247. return "latin1"
  248. else:
  249. return cset_name()
  250. _isolation_lookup = set(
  251. [
  252. "SERIALIZABLE",
  253. "READ UNCOMMITTED",
  254. "READ COMMITTED",
  255. "REPEATABLE READ",
  256. "AUTOCOMMIT",
  257. ]
  258. )
  259. def _set_isolation_level(self, connection, level):
  260. if level == "AUTOCOMMIT":
  261. connection.autocommit(True)
  262. else:
  263. connection.autocommit(False)
  264. super(MySQLDialect_mysqldb, self)._set_isolation_level(
  265. connection, level
  266. )
  267. dialect = MySQLDialect_mysqldb