mariadbconnector.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. # dialects/mysql/mariadbconnector.py
  2. # Copyright (C) 2005-2024 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+mariadbconnector
  9. :name: MariaDB Connector/Python
  10. :dbapi: mariadb
  11. :connectstring: mariadb+mariadbconnector://<user>:<password>@<host>[:<port>]/<dbname>
  12. :url: https://pypi.org/project/mariadb/
  13. Driver Status
  14. -------------
  15. MariaDB Connector/Python enables Python programs to access MariaDB and MySQL
  16. databases using an API which is compliant with the Python DB API 2.0 (PEP-249).
  17. It is written in C and uses MariaDB Connector/C client library for client server
  18. communication.
  19. Note that the default driver for a ``mariadb://`` connection URI continues to
  20. be ``mysqldb``. ``mariadb+mariadbconnector://`` is required to use this driver.
  21. .. mariadb: https://github.com/mariadb-corporation/mariadb-connector-python
  22. """ # noqa
  23. import re
  24. from .base import MySQLCompiler
  25. from .base import MySQLDialect
  26. from .base import MySQLExecutionContext
  27. from ... import sql
  28. from ... import util
  29. mariadb_cpy_minimum_version = (1, 0, 1)
  30. class MySQLExecutionContext_mariadbconnector(MySQLExecutionContext):
  31. _lastrowid = None
  32. def create_server_side_cursor(self):
  33. return self._dbapi_connection.cursor(buffered=False)
  34. def create_default_cursor(self):
  35. return self._dbapi_connection.cursor(buffered=True)
  36. def post_exec(self):
  37. if self.isinsert and self.compiled.postfetch_lastrowid:
  38. self._lastrowid = self.cursor.lastrowid
  39. def get_lastrowid(self):
  40. return self._lastrowid
  41. class MySQLCompiler_mariadbconnector(MySQLCompiler):
  42. pass
  43. class MySQLDialect_mariadbconnector(MySQLDialect):
  44. driver = "mariadbconnector"
  45. supports_statement_cache = True
  46. # set this to True at the module level to prevent the driver from running
  47. # against a backend that server detects as MySQL. currently this appears to
  48. # be unnecessary as MariaDB client libraries have always worked against
  49. # MySQL databases. However, if this changes at some point, this can be
  50. # adjusted, but PLEASE ADD A TEST in test/dialect/mysql/test_dialect.py if
  51. # this change is made at some point to ensure the correct exception
  52. # is raised at the correct point when running the driver against
  53. # a MySQL backend.
  54. # is_mariadb = True
  55. supports_unicode_statements = True
  56. encoding = "utf8mb4"
  57. convert_unicode = True
  58. supports_sane_rowcount = True
  59. supports_sane_multi_rowcount = True
  60. supports_native_decimal = True
  61. default_paramstyle = "qmark"
  62. execution_ctx_cls = MySQLExecutionContext_mariadbconnector
  63. statement_compiler = MySQLCompiler_mariadbconnector
  64. supports_server_side_cursors = True
  65. @util.memoized_property
  66. def _dbapi_version(self):
  67. if self.dbapi and hasattr(self.dbapi, "__version__"):
  68. return tuple(
  69. [
  70. int(x)
  71. for x in re.findall(
  72. r"(\d+)(?:[-\.]?|$)", self.dbapi.__version__
  73. )
  74. ]
  75. )
  76. else:
  77. return (99, 99, 99)
  78. def __init__(self, **kwargs):
  79. super(MySQLDialect_mariadbconnector, self).__init__(**kwargs)
  80. self.paramstyle = "qmark"
  81. if self.dbapi is not None:
  82. if self._dbapi_version < mariadb_cpy_minimum_version:
  83. raise NotImplementedError(
  84. "The minimum required version for MariaDB "
  85. "Connector/Python is %s"
  86. % ".".join(str(x) for x in mariadb_cpy_minimum_version)
  87. )
  88. @classmethod
  89. def dbapi(cls):
  90. return __import__("mariadb")
  91. def is_disconnect(self, e, connection, cursor):
  92. if super(MySQLDialect_mariadbconnector, self).is_disconnect(
  93. e, connection, cursor
  94. ):
  95. return True
  96. elif isinstance(e, self.dbapi.Error):
  97. str_e = str(e).lower()
  98. return "not connected" in str_e or "isn't valid" in str_e
  99. else:
  100. return False
  101. def create_connect_args(self, url):
  102. opts = url.translate_connect_args()
  103. int_params = [
  104. "connect_timeout",
  105. "read_timeout",
  106. "write_timeout",
  107. "client_flag",
  108. "port",
  109. "pool_size",
  110. ]
  111. bool_params = [
  112. "local_infile",
  113. "ssl_verify_cert",
  114. "ssl",
  115. "pool_reset_connection",
  116. ]
  117. for key in int_params:
  118. util.coerce_kw_type(opts, key, int)
  119. for key in bool_params:
  120. util.coerce_kw_type(opts, key, bool)
  121. # FOUND_ROWS must be set in CLIENT_FLAGS to enable
  122. # supports_sane_rowcount.
  123. client_flag = opts.get("client_flag", 0)
  124. if self.dbapi is not None:
  125. try:
  126. CLIENT_FLAGS = __import__(
  127. self.dbapi.__name__ + ".constants.CLIENT"
  128. ).constants.CLIENT
  129. client_flag |= CLIENT_FLAGS.FOUND_ROWS
  130. except (AttributeError, ImportError):
  131. self.supports_sane_rowcount = False
  132. opts["client_flag"] = client_flag
  133. return [[], opts]
  134. def _extract_error_code(self, exception):
  135. try:
  136. rc = exception.errno
  137. except:
  138. rc = -1
  139. return rc
  140. def _detect_charset(self, connection):
  141. return "utf8mb4"
  142. _isolation_lookup = set(
  143. [
  144. "SERIALIZABLE",
  145. "READ UNCOMMITTED",
  146. "READ COMMITTED",
  147. "REPEATABLE READ",
  148. "AUTOCOMMIT",
  149. ]
  150. )
  151. def _set_isolation_level(self, connection, level):
  152. if level == "AUTOCOMMIT":
  153. connection.autocommit = True
  154. else:
  155. connection.autocommit = False
  156. super(MySQLDialect_mariadbconnector, self)._set_isolation_level(
  157. connection, level
  158. )
  159. def do_begin_twophase(self, connection, xid):
  160. connection.execute(
  161. sql.text("XA BEGIN :xid").bindparams(
  162. sql.bindparam("xid", xid, literal_execute=True)
  163. )
  164. )
  165. def do_prepare_twophase(self, connection, xid):
  166. connection.execute(
  167. sql.text("XA END :xid").bindparams(
  168. sql.bindparam("xid", xid, literal_execute=True)
  169. )
  170. )
  171. connection.execute(
  172. sql.text("XA PREPARE :xid").bindparams(
  173. sql.bindparam("xid", xid, literal_execute=True)
  174. )
  175. )
  176. def do_rollback_twophase(
  177. self, connection, xid, is_prepared=True, recover=False
  178. ):
  179. if not is_prepared:
  180. connection.execute(
  181. sql.text("XA END :xid").bindparams(
  182. sql.bindparam("xid", xid, literal_execute=True)
  183. )
  184. )
  185. connection.execute(
  186. sql.text("XA ROLLBACK :xid").bindparams(
  187. sql.bindparam("xid", xid, literal_execute=True)
  188. )
  189. )
  190. def do_commit_twophase(
  191. self, connection, xid, is_prepared=True, recover=False
  192. ):
  193. if not is_prepared:
  194. self.do_prepare_twophase(connection, xid)
  195. connection.execute(
  196. sql.text("XA COMMIT :xid").bindparams(
  197. sql.bindparam("xid", xid, literal_execute=True)
  198. )
  199. )
  200. dialect = MySQLDialect_mariadbconnector