PageRenderTime 26ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/sqlalchemy/dialects/mssql/pyodbc.py

https://gitlab.com/ztane/sqlalchemy
Python | 260 lines | 163 code | 11 blank | 86 comment | 11 complexity | c1b5681ec365bc845471a194e39b4882 MD5 | raw file
  1. # mssql/pyodbc.py
  2. # Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
  3. #
  4. # This module is part of SQLAlchemy and is released under
  5. # the MIT License: http://www.opensource.org/licenses/mit-license.php
  6. """
  7. .. dialect:: mssql+pyodbc
  8. :name: PyODBC
  9. :dbapi: pyodbc
  10. :connectstring: mssql+pyodbc://<username>:<password>@<dsnname>
  11. :url: http://pypi.python.org/pypi/pyodbc/
  12. Additional Connection Examples
  13. -------------------------------
  14. Examples of pyodbc connection string URLs:
  15. * ``mssql+pyodbc://mydsn`` - connects using the specified DSN named ``mydsn``.
  16. The connection string that is created will appear like::
  17. dsn=mydsn;Trusted_Connection=Yes
  18. * ``mssql+pyodbc://user:pass@mydsn`` - connects using the DSN named
  19. ``mydsn`` passing in the ``UID`` and ``PWD`` information. The
  20. connection string that is created will appear like::
  21. dsn=mydsn;UID=user;PWD=pass
  22. * ``mssql+pyodbc://user:pass@mydsn/?LANGUAGE=us_english`` - connects
  23. using the DSN named ``mydsn`` passing in the ``UID`` and ``PWD``
  24. information, plus the additional connection configuration option
  25. ``LANGUAGE``. The connection string that is created will appear
  26. like::
  27. dsn=mydsn;UID=user;PWD=pass;LANGUAGE=us_english
  28. * ``mssql+pyodbc://user:pass@host/db`` - connects using a connection
  29. that would appear like::
  30. DRIVER={SQL Server};Server=host;Database=db;UID=user;PWD=pass
  31. * ``mssql+pyodbc://user:pass@host:123/db`` - connects using a connection
  32. string which includes the port
  33. information using the comma syntax. This will create the following
  34. connection string::
  35. DRIVER={SQL Server};Server=host,123;Database=db;UID=user;PWD=pass
  36. * ``mssql+pyodbc://user:pass@host/db?port=123`` - connects using a connection
  37. string that includes the port
  38. information as a separate ``port`` keyword. This will create the
  39. following connection string::
  40. DRIVER={SQL Server};Server=host;Database=db;UID=user;PWD=pass;port=123
  41. * ``mssql+pyodbc://user:pass@host/db?driver=MyDriver`` - connects using a connection
  42. string that includes a custom
  43. ODBC driver name. This will create the following connection string::
  44. DRIVER={MyDriver};Server=host;Database=db;UID=user;PWD=pass
  45. If you require a connection string that is outside the options
  46. presented above, use the ``odbc_connect`` keyword to pass in a
  47. urlencoded connection string. What gets passed in will be urldecoded
  48. and passed directly.
  49. For example::
  50. mssql+pyodbc:///?odbc_connect=dsn%3Dmydsn%3BDatabase%3Ddb
  51. would create the following connection string::
  52. dsn=mydsn;Database=db
  53. Encoding your connection string can be easily accomplished through
  54. the python shell. For example::
  55. >>> import urllib
  56. >>> urllib.quote_plus('dsn=mydsn;Database=db')
  57. 'dsn%3Dmydsn%3BDatabase%3Ddb'
  58. Unicode Binds
  59. -------------
  60. The current state of PyODBC on a unix backend with FreeTDS and/or
  61. EasySoft is poor regarding unicode; different OS platforms and versions of UnixODBC
  62. versus IODBC versus FreeTDS/EasySoft versus PyODBC itself dramatically
  63. alter how strings are received. The PyODBC dialect attempts to use all the information
  64. it knows to determine whether or not a Python unicode literal can be
  65. passed directly to the PyODBC driver or not; while SQLAlchemy can encode
  66. these to bytestrings first, some users have reported that PyODBC mis-handles
  67. bytestrings for certain encodings and requires a Python unicode object,
  68. while the author has observed widespread cases where a Python unicode
  69. is completely misinterpreted by PyODBC, particularly when dealing with
  70. the information schema tables used in table reflection, and the value
  71. must first be encoded to a bytestring.
  72. It is for this reason that whether or not unicode literals for bound
  73. parameters be sent to PyODBC can be controlled using the
  74. ``supports_unicode_binds`` parameter to ``create_engine()``. When
  75. left at its default of ``None``, the PyODBC dialect will use its
  76. best guess as to whether or not the driver deals with unicode literals
  77. well. When ``False``, unicode literals will be encoded first, and when
  78. ``True`` unicode literals will be passed straight through. This is an interim
  79. flag that hopefully should not be needed when the unicode situation stabilizes
  80. for unix + PyODBC.
  81. .. versionadded:: 0.7.7
  82. ``supports_unicode_binds`` parameter to ``create_engine()``\ .
  83. """
  84. from .base import MSExecutionContext, MSDialect
  85. from ...connectors.pyodbc import PyODBCConnector
  86. from ... import types as sqltypes, util
  87. import decimal
  88. class _ms_numeric_pyodbc(object):
  89. """Turns Decimals with adjusted() < 0 or > 7 into strings.
  90. The routines here are needed for older pyodbc versions
  91. as well as current mxODBC versions.
  92. """
  93. def bind_processor(self, dialect):
  94. super_process = super(_ms_numeric_pyodbc, self).\
  95. bind_processor(dialect)
  96. if not dialect._need_decimal_fix:
  97. return super_process
  98. def process(value):
  99. if self.asdecimal and \
  100. isinstance(value, decimal.Decimal):
  101. adjusted = value.adjusted()
  102. if adjusted < 0:
  103. return self._small_dec_to_string(value)
  104. elif adjusted > 7:
  105. return self._large_dec_to_string(value)
  106. if super_process:
  107. return super_process(value)
  108. else:
  109. return value
  110. return process
  111. # these routines needed for older versions of pyodbc.
  112. # as of 2.1.8 this logic is integrated.
  113. def _small_dec_to_string(self, value):
  114. return "%s0.%s%s" % (
  115. (value < 0 and '-' or ''),
  116. '0' * (abs(value.adjusted()) - 1),
  117. "".join([str(nint) for nint in value.as_tuple()[1]]))
  118. def _large_dec_to_string(self, value):
  119. _int = value.as_tuple()[1]
  120. if 'E' in str(value):
  121. result = "%s%s%s" % (
  122. (value < 0 and '-' or ''),
  123. "".join([str(s) for s in _int]),
  124. "0" * (value.adjusted() - (len(_int) - 1)))
  125. else:
  126. if (len(_int) - 1) > value.adjusted():
  127. result = "%s%s.%s" % (
  128. (value < 0 and '-' or ''),
  129. "".join(
  130. [str(s) for s in _int][0:value.adjusted() + 1]),
  131. "".join(
  132. [str(s) for s in _int][value.adjusted() + 1:]))
  133. else:
  134. result = "%s%s" % (
  135. (value < 0 and '-' or ''),
  136. "".join(
  137. [str(s) for s in _int][0:value.adjusted() + 1]))
  138. return result
  139. class _MSNumeric_pyodbc(_ms_numeric_pyodbc, sqltypes.Numeric):
  140. pass
  141. class _MSFloat_pyodbc(_ms_numeric_pyodbc, sqltypes.Float):
  142. pass
  143. class MSExecutionContext_pyodbc(MSExecutionContext):
  144. _embedded_scope_identity = False
  145. def pre_exec(self):
  146. """where appropriate, issue "select scope_identity()" in the same
  147. statement.
  148. Background on why "scope_identity()" is preferable to "@@identity":
  149. http://msdn.microsoft.com/en-us/library/ms190315.aspx
  150. Background on why we attempt to embed "scope_identity()" into the same
  151. statement as the INSERT:
  152. http://code.google.com/p/pyodbc/wiki/FAQs#How_do_I_retrieve_autogenerated/identity_values?
  153. """
  154. super(MSExecutionContext_pyodbc, self).pre_exec()
  155. # don't embed the scope_identity select into an
  156. # "INSERT .. DEFAULT VALUES"
  157. if self._select_lastrowid and \
  158. self.dialect.use_scope_identity and \
  159. len(self.parameters[0]):
  160. self._embedded_scope_identity = True
  161. self.statement += "; select scope_identity()"
  162. def post_exec(self):
  163. if self._embedded_scope_identity:
  164. # Fetch the last inserted id from the manipulated statement
  165. # We may have to skip over a number of result sets with
  166. # no data (due to triggers, etc.)
  167. while True:
  168. try:
  169. # fetchall() ensures the cursor is consumed
  170. # without closing it (FreeTDS particularly)
  171. row = self.cursor.fetchall()[0]
  172. break
  173. except self.dialect.dbapi.Error as e:
  174. # no way around this - nextset() consumes the previous set
  175. # so we need to just keep flipping
  176. self.cursor.nextset()
  177. self._lastrowid = int(row[0])
  178. else:
  179. super(MSExecutionContext_pyodbc, self).post_exec()
  180. class MSDialect_pyodbc(PyODBCConnector, MSDialect):
  181. execution_ctx_cls = MSExecutionContext_pyodbc
  182. pyodbc_driver_name = 'SQL Server'
  183. colspecs = util.update_copy(
  184. MSDialect.colspecs,
  185. {
  186. sqltypes.Numeric: _MSNumeric_pyodbc,
  187. sqltypes.Float: _MSFloat_pyodbc
  188. }
  189. )
  190. def __init__(self, description_encoding=None, **params):
  191. super(MSDialect_pyodbc, self).__init__(**params)
  192. self.description_encoding = description_encoding
  193. self.use_scope_identity = self.use_scope_identity and \
  194. self.dbapi and \
  195. hasattr(self.dbapi.Cursor, 'nextset')
  196. self._need_decimal_fix = self.dbapi and \
  197. self._dbapi_version() < (2, 1, 8)
  198. dialect = MSDialect_pyodbc