pymssql.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. # mssql/pymssql.py
  2. # Copyright (C) 2005-2022 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:: mssql+pymssql
  9. :name: pymssql
  10. :dbapi: pymssql
  11. :connectstring: mssql+pymssql://<username>:<password>@<freetds_name>/?charset=utf8
  12. pymssql is a Python module that provides a Python DBAPI interface around
  13. `FreeTDS <https://www.freetds.org/>`_.
  14. .. note::
  15. pymssql is currently not included in SQLAlchemy's continuous integration
  16. (CI) testing.
  17. Modern versions of this driver worked very well with SQL Server and FreeTDS
  18. from Linux and were highly recommended. However, pymssql is currently
  19. unmaintained and has fallen behind the progress of the Microsoft ODBC driver in
  20. its support for newer features of SQL Server. The latest official release of
  21. pymssql at the time of this document is version 2.1.4 (August, 2018) and it
  22. lacks support for:
  23. 1. table-valued parameters (TVPs),
  24. 2. ``datetimeoffset`` columns using timezone-aware ``datetime`` objects
  25. (values are sent and retrieved as strings), and
  26. 3. encrypted connections (e.g., to Azure SQL), when pymssql is installed from
  27. the pre-built wheels. Support for encrypted connections requires building
  28. pymssql from source, which can be a nuisance, especially under Windows.
  29. The above features are all supported by mssql+pyodbc when using Microsoft's
  30. ODBC Driver for SQL Server (msodbcsql), which is now available for Windows,
  31. (several flavors of) Linux, and macOS.
  32. """ # noqa
  33. import re
  34. from .base import MSDialect
  35. from .base import MSIdentifierPreparer
  36. from ... import processors
  37. from ... import types as sqltypes
  38. from ... import util
  39. class _MSNumeric_pymssql(sqltypes.Numeric):
  40. def result_processor(self, dialect, type_):
  41. if not self.asdecimal:
  42. return processors.to_float
  43. else:
  44. return sqltypes.Numeric.result_processor(self, dialect, type_)
  45. class MSIdentifierPreparer_pymssql(MSIdentifierPreparer):
  46. def __init__(self, dialect):
  47. super(MSIdentifierPreparer_pymssql, self).__init__(dialect)
  48. # pymssql has the very unusual behavior that it uses pyformat
  49. # yet does not require that percent signs be doubled
  50. self._double_percents = False
  51. class MSDialect_pymssql(MSDialect):
  52. supports_statement_cache = True
  53. supports_native_decimal = True
  54. driver = "pymssql"
  55. preparer = MSIdentifierPreparer_pymssql
  56. colspecs = util.update_copy(
  57. MSDialect.colspecs,
  58. {sqltypes.Numeric: _MSNumeric_pymssql, sqltypes.Float: sqltypes.Float},
  59. )
  60. @classmethod
  61. def dbapi(cls):
  62. module = __import__("pymssql")
  63. # pymmsql < 2.1.1 doesn't have a Binary method. we use string
  64. client_ver = tuple(int(x) for x in module.__version__.split("."))
  65. if client_ver < (2, 1, 1):
  66. # TODO: monkeypatching here is less than ideal
  67. module.Binary = lambda x: x if hasattr(x, "decode") else str(x)
  68. if client_ver < (1,):
  69. util.warn(
  70. "The pymssql dialect expects at least "
  71. "the 1.0 series of the pymssql DBAPI."
  72. )
  73. return module
  74. def _get_server_version_info(self, connection):
  75. vers = connection.exec_driver_sql("select @@version").scalar()
  76. m = re.match(r"Microsoft .*? - (\d+)\.(\d+)\.(\d+)\.(\d+)", vers)
  77. if m:
  78. return tuple(int(x) for x in m.group(1, 2, 3, 4))
  79. else:
  80. return None
  81. def create_connect_args(self, url):
  82. opts = url.translate_connect_args(username="user")
  83. opts.update(url.query)
  84. port = opts.pop("port", None)
  85. if port and "host" in opts:
  86. opts["host"] = "%s:%s" % (opts["host"], port)
  87. return [[], opts]
  88. def is_disconnect(self, e, connection, cursor):
  89. for msg in (
  90. "Adaptive Server connection timed out",
  91. "Net-Lib error during Connection reset by peer",
  92. "message 20003", # connection timeout
  93. "Error 10054",
  94. "Not connected to any MS SQL server",
  95. "Connection is closed",
  96. "message 20006", # Write to the server failed
  97. "message 20017", # Unexpected EOF from the server
  98. "message 20047", # DBPROCESS is dead or not enabled
  99. ):
  100. if msg in str(e):
  101. return True
  102. else:
  103. return False
  104. def set_isolation_level(self, connection, level):
  105. if level == "AUTOCOMMIT":
  106. connection.autocommit(True)
  107. else:
  108. connection.autocommit(False)
  109. super(MSDialect_pymssql, self).set_isolation_level(
  110. connection, level
  111. )
  112. dialect = MSDialect_pymssql