mxodbc.py 5.4 KB

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