mariadbconnector.py 8.5 KB

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