You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

167 lines
5.6KB

  1. # connectors/mxodbc.py
  2. # Copyright (C) 2005-2021 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: http://www.opensource.org/licenses/mit-license.php
  7. """
  8. Provide a SQLALchemy connector for the eGenix mxODBC commercial
  9. Python adapter for ODBC. This is not a free product, but eGenix
  10. provides SQLAlchemy with a license for use in continuous integration
  11. testing.
  12. This has been tested for use with mxODBC 3.1.2 on SQL Server 2005
  13. and 2008, using the SQL Server Native driver. However, it is
  14. possible for this to be used on other database platforms.
  15. For more info on mxODBC, see http://www.egenix.com/
  16. .. deprecated:: 1.4 The mxODBC DBAPI is deprecated and will be removed
  17. in a future version. Please use one of the supported DBAPIs to
  18. connect to mssql.
  19. """
  20. import re
  21. import sys
  22. import warnings
  23. from . import Connector
  24. from ..util import warn_deprecated
  25. class MxODBCConnector(Connector):
  26. driver = "mxodbc"
  27. supports_sane_multi_rowcount = False
  28. supports_unicode_statements = True
  29. supports_unicode_binds = True
  30. supports_native_decimal = True
  31. @classmethod
  32. def dbapi(cls):
  33. # this classmethod will normally be replaced by an instance
  34. # attribute of the same name, so this is normally only called once.
  35. cls._load_mx_exceptions()
  36. platform = sys.platform
  37. if platform == "win32":
  38. from mx.ODBC import Windows as Module
  39. # this can be the string "linux2", and possibly others
  40. elif "linux" in platform:
  41. from mx.ODBC import unixODBC as Module
  42. elif platform == "darwin":
  43. from mx.ODBC import iODBC as Module
  44. else:
  45. raise ImportError("Unrecognized platform for mxODBC import")
  46. warn_deprecated(
  47. "The mxODBC DBAPI is deprecated and will be removed"
  48. "in a future version. Please use one of the supported DBAPIs to"
  49. "connect to mssql.",
  50. version="1.4",
  51. )
  52. return Module
  53. @classmethod
  54. def _load_mx_exceptions(cls):
  55. """Import mxODBC exception classes into the module namespace,
  56. as if they had been imported normally. This is done here
  57. to avoid requiring all SQLAlchemy users to install mxODBC.
  58. """
  59. global InterfaceError, ProgrammingError
  60. from mx.ODBC import InterfaceError
  61. from mx.ODBC import ProgrammingError
  62. def on_connect(self):
  63. def connect(conn):
  64. conn.stringformat = self.dbapi.MIXED_STRINGFORMAT
  65. conn.datetimeformat = self.dbapi.PYDATETIME_DATETIMEFORMAT
  66. conn.decimalformat = self.dbapi.DECIMAL_DECIMALFORMAT
  67. conn.errorhandler = self._error_handler()
  68. return connect
  69. def _error_handler(self):
  70. """Return a handler that adjusts mxODBC's raised Warnings to
  71. emit Python standard warnings.
  72. """
  73. from mx.ODBC.Error import Warning as MxOdbcWarning
  74. def error_handler(connection, cursor, errorclass, errorvalue):
  75. if issubclass(errorclass, MxOdbcWarning):
  76. errorclass.__bases__ = (Warning,)
  77. warnings.warn(
  78. message=str(errorvalue), category=errorclass, stacklevel=2
  79. )
  80. else:
  81. raise errorclass(errorvalue)
  82. return error_handler
  83. def create_connect_args(self, url):
  84. r"""Return a tuple of \*args, \**kwargs for creating a connection.
  85. The mxODBC 3.x connection constructor looks like this:
  86. connect(dsn, user='', password='',
  87. clear_auto_commit=1, errorhandler=None)
  88. This method translates the values in the provided URI
  89. into args and kwargs needed to instantiate an mxODBC Connection.
  90. The arg 'errorhandler' is not used by SQLAlchemy and will
  91. not be populated.
  92. """
  93. opts = url.translate_connect_args(username="user")
  94. opts.update(url.query)
  95. args = opts.pop("host")
  96. opts.pop("port", None)
  97. opts.pop("database", None)
  98. return (args,), opts
  99. def is_disconnect(self, e, connection, cursor):
  100. # TODO: eGenix recommends checking connection.closed here
  101. # Does that detect dropped connections ?
  102. if isinstance(e, self.dbapi.ProgrammingError):
  103. return "connection already closed" in str(e)
  104. elif isinstance(e, self.dbapi.Error):
  105. return "[08S01]" in str(e)
  106. else:
  107. return False
  108. def _get_server_version_info(self, connection):
  109. # eGenix suggests using conn.dbms_version instead
  110. # of what we're doing here
  111. dbapi_con = connection.connection
  112. version = []
  113. r = re.compile(r"[.\-]")
  114. # 18 == pyodbc.SQL_DBMS_VER
  115. for n in r.split(dbapi_con.getinfo(18)[1]):
  116. try:
  117. version.append(int(n))
  118. except ValueError:
  119. version.append(n)
  120. return tuple(version)
  121. def _get_direct(self, context):
  122. if context:
  123. native_odbc_execute = context.execution_options.get(
  124. "native_odbc_execute", "auto"
  125. )
  126. # default to direct=True in all cases, is more generally
  127. # compatible especially with SQL Server
  128. return False if native_odbc_execute is True else True
  129. else:
  130. return True
  131. def do_executemany(self, cursor, statement, parameters, context=None):
  132. cursor.executemany(
  133. statement, parameters, direct=self._get_direct(context)
  134. )
  135. def do_execute(self, cursor, statement, parameters, context=None):
  136. cursor.execute(statement, parameters, direct=self._get_direct(context))