pymssql.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. # dialects/mssql/pymssql.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. """
  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. """ # noqa
  18. import re
  19. from .base import MSDialect
  20. from .base import MSIdentifierPreparer
  21. from ... import processors
  22. from ... import types as sqltypes
  23. from ... import util
  24. class _MSNumeric_pymssql(sqltypes.Numeric):
  25. def result_processor(self, dialect, type_):
  26. if not self.asdecimal:
  27. return processors.to_float
  28. else:
  29. return sqltypes.Numeric.result_processor(self, dialect, type_)
  30. class MSIdentifierPreparer_pymssql(MSIdentifierPreparer):
  31. def __init__(self, dialect):
  32. super(MSIdentifierPreparer_pymssql, self).__init__(dialect)
  33. # pymssql has the very unusual behavior that it uses pyformat
  34. # yet does not require that percent signs be doubled
  35. self._double_percents = False
  36. class MSDialect_pymssql(MSDialect):
  37. supports_statement_cache = True
  38. supports_native_decimal = True
  39. driver = "pymssql"
  40. preparer = MSIdentifierPreparer_pymssql
  41. colspecs = util.update_copy(
  42. MSDialect.colspecs,
  43. {sqltypes.Numeric: _MSNumeric_pymssql, sqltypes.Float: sqltypes.Float},
  44. )
  45. @classmethod
  46. def dbapi(cls):
  47. module = __import__("pymssql")
  48. # pymmsql < 2.1.1 doesn't have a Binary method. we use string
  49. client_ver = tuple(int(x) for x in module.__version__.split("."))
  50. if client_ver < (2, 1, 1):
  51. # TODO: monkeypatching here is less than ideal
  52. module.Binary = lambda x: x if hasattr(x, "decode") else str(x)
  53. if client_ver < (1,):
  54. util.warn(
  55. "The pymssql dialect expects at least "
  56. "the 1.0 series of the pymssql DBAPI."
  57. )
  58. return module
  59. def _get_server_version_info(self, connection):
  60. vers = connection.exec_driver_sql("select @@version").scalar()
  61. m = re.match(r"Microsoft .*? - (\d+)\.(\d+)\.(\d+)\.(\d+)", vers)
  62. if m:
  63. return tuple(int(x) for x in m.group(1, 2, 3, 4))
  64. else:
  65. return None
  66. def create_connect_args(self, url):
  67. opts = url.translate_connect_args(username="user")
  68. opts.update(url.query)
  69. port = opts.pop("port", None)
  70. if port and "host" in opts:
  71. opts["host"] = "%s:%s" % (opts["host"], port)
  72. return [[], opts]
  73. def is_disconnect(self, e, connection, cursor):
  74. for msg in (
  75. "Adaptive Server connection timed out",
  76. "Net-Lib error during Connection reset by peer",
  77. "message 20003", # connection timeout
  78. "Error 10054",
  79. "Not connected to any MS SQL server",
  80. "Connection is closed",
  81. "message 20006", # Write to the server failed
  82. "message 20017", # Unexpected EOF from the server
  83. "message 20047", # DBPROCESS is dead or not enabled
  84. ):
  85. if msg in str(e):
  86. return True
  87. else:
  88. return False
  89. def set_isolation_level(self, connection, level):
  90. if level == "AUTOCOMMIT":
  91. connection.autocommit(True)
  92. else:
  93. connection.autocommit(False)
  94. super(MSDialect_pymssql, self).set_isolation_level(
  95. connection, level
  96. )
  97. dialect = MSDialect_pymssql