pyodbc.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. # dialects/mysql/pyodbc.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. r"""
  8. .. dialect:: mysql+pyodbc
  9. :name: PyODBC
  10. :dbapi: pyodbc
  11. :connectstring: mysql+pyodbc://<username>:<password>@<dsnname>
  12. :url: https://pypi.org/project/pyodbc/
  13. .. note::
  14. The PyODBC for MySQL dialect is **not tested as part of
  15. SQLAlchemy's continuous integration**.
  16. The recommended MySQL dialects are mysqlclient and PyMySQL.
  17. However, if you want to use the mysql+pyodbc dialect and require
  18. full support for ``utf8mb4`` characters (including supplementary
  19. characters like emoji) be sure to use a current release of
  20. MySQL Connector/ODBC and specify the "ANSI" (**not** "Unicode")
  21. version of the driver in your DSN or connection string.
  22. Pass through exact pyodbc connection string::
  23. import urllib
  24. connection_string = (
  25. "DRIVER=MySQL ODBC 8.0 ANSI Driver;"
  26. "SERVER=localhost;"
  27. "PORT=3307;"
  28. "DATABASE=mydb;"
  29. "UID=root;"
  30. "PWD=(whatever);"
  31. "charset=utf8mb4;"
  32. )
  33. params = urllib.parse.quote_plus(connection_string)
  34. connection_uri = "mysql+pyodbc:///?odbc_connect=%s" % params
  35. """ # noqa
  36. from __future__ import annotations
  37. import datetime
  38. import re
  39. from typing import Any
  40. from typing import Callable
  41. from typing import Optional
  42. from typing import Tuple
  43. from typing import TYPE_CHECKING
  44. from typing import Union
  45. from .base import MySQLDialect
  46. from .base import MySQLExecutionContext
  47. from .types import TIME
  48. from ... import exc
  49. from ... import util
  50. from ...connectors.pyodbc import PyODBCConnector
  51. from ...sql.sqltypes import Time
  52. if TYPE_CHECKING:
  53. from ...engine import Connection
  54. from ...engine.interfaces import DBAPIConnection
  55. from ...engine.interfaces import Dialect
  56. from ...sql.type_api import _ResultProcessorType
  57. class _pyodbcTIME(TIME):
  58. def result_processor(
  59. self, dialect: Dialect, coltype: object
  60. ) -> _ResultProcessorType[datetime.time]:
  61. def process(value: Any) -> Union[datetime.time, None]:
  62. # pyodbc returns a datetime.time object; no need to convert
  63. return value # type: ignore[no-any-return]
  64. return process
  65. class MySQLExecutionContext_pyodbc(MySQLExecutionContext):
  66. def get_lastrowid(self) -> int:
  67. cursor = self.create_cursor()
  68. cursor.execute("SELECT LAST_INSERT_ID()")
  69. lastrowid = cursor.fetchone()[0] # type: ignore[index]
  70. cursor.close()
  71. return lastrowid # type: ignore[no-any-return]
  72. class MySQLDialect_pyodbc(PyODBCConnector, MySQLDialect):
  73. supports_statement_cache = True
  74. colspecs = util.update_copy(MySQLDialect.colspecs, {Time: _pyodbcTIME})
  75. supports_unicode_statements = True
  76. execution_ctx_cls = MySQLExecutionContext_pyodbc
  77. pyodbc_driver_name = "MySQL"
  78. def _detect_charset(self, connection: Connection) -> str:
  79. """Sniff out the character set in use for connection results."""
  80. # Prefer 'character_set_results' for the current connection over the
  81. # value in the driver. SET NAMES or individual variable SETs will
  82. # change the charset without updating the driver's view of the world.
  83. #
  84. # If it's decided that issuing that sort of SQL leaves you SOL, then
  85. # this can prefer the driver value.
  86. # set this to None as _fetch_setting attempts to use it (None is OK)
  87. self._connection_charset = None
  88. try:
  89. value = self._fetch_setting(connection, "character_set_client")
  90. if value:
  91. return value
  92. except exc.DBAPIError:
  93. pass
  94. util.warn(
  95. "Could not detect the connection character set. "
  96. "Assuming latin1."
  97. )
  98. return "latin1"
  99. def _get_server_version_info(
  100. self, connection: Connection
  101. ) -> Tuple[int, ...]:
  102. return MySQLDialect._get_server_version_info(self, connection)
  103. def _extract_error_code(self, exception: BaseException) -> Optional[int]:
  104. m = re.compile(r"\((\d+)\)").search(str(exception.args))
  105. if m is None:
  106. return None
  107. c: Optional[str] = m.group(1)
  108. if c:
  109. return int(c)
  110. else:
  111. return None
  112. def on_connect(self) -> Callable[[DBAPIConnection], None]:
  113. super_ = super().on_connect()
  114. def on_connect(conn: DBAPIConnection) -> None:
  115. if super_ is not None:
  116. super_(conn)
  117. # declare Unicode encoding for pyodbc as per
  118. # https://github.com/mkleehammer/pyodbc/wiki/Unicode
  119. pyodbc_SQL_CHAR = 1 # pyodbc.SQL_CHAR
  120. pyodbc_SQL_WCHAR = -8 # pyodbc.SQL_WCHAR
  121. conn.setdecoding(pyodbc_SQL_CHAR, encoding="utf-8")
  122. conn.setdecoding(pyodbc_SQL_WCHAR, encoding="utf-8")
  123. conn.setencoding(encoding="utf-8")
  124. return on_connect
  125. dialect = MySQLDialect_pyodbc