1# dialects/mssql/pyodbc.py
2# Copyright (C) 2005-2026 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# mypy: ignore-errors
8
9r"""
10.. dialect:: mssql+pyodbc
11 :name: PyODBC
12 :dbapi: pyodbc
13 :connectstring: mssql+pyodbc://<username>:<password>@<dsnname>
14 :url: https://pypi.org/project/pyodbc/
15
16Connecting to PyODBC
17--------------------
18
19The URL here is to be translated to PyODBC connection strings, as
20detailed in `ConnectionStrings <https://code.google.com/p/pyodbc/wiki/ConnectionStrings>`_.
21
22DSN Connections
23^^^^^^^^^^^^^^^
24
25A DSN connection in ODBC means that a pre-existing ODBC datasource is
26configured on the client machine. The application then specifies the name
27of this datasource, which encompasses details such as the specific ODBC driver
28in use as well as the network address of the database. Assuming a datasource
29is configured on the client, a basic DSN-based connection looks like::
30
31 engine = create_engine("mssql+pyodbc://scott:tiger@some_dsn")
32
33Which above, will pass the following connection string to PyODBC:
34
35.. sourcecode:: text
36
37 DSN=some_dsn;UID=scott;PWD=tiger
38
39If the username and password are omitted, the DSN form will also add
40the ``Trusted_Connection=yes`` directive to the ODBC string.
41
42Hostname Connections
43^^^^^^^^^^^^^^^^^^^^
44
45Hostname-based connections are also supported by pyodbc. These are often
46easier to use than a DSN and have the additional advantage that the specific
47database name to connect towards may be specified locally in the URL, rather
48than it being fixed as part of a datasource configuration.
49
50When using a hostname connection, the driver name must also be specified in the
51query parameters of the URL. As these names usually have spaces in them, the
52name must be URL encoded which means using plus signs for spaces::
53
54 engine = create_engine(
55 "mssql+pyodbc://scott:tiger@myhost:port/databasename?driver=ODBC+Driver+17+for+SQL+Server"
56 )
57
58The ``driver`` keyword is significant to the pyodbc dialect and must be
59specified in lowercase.
60
61Any other names passed in the query string are passed through in the pyodbc
62connect string, such as ``authentication``, ``TrustServerCertificate``, etc.
63Multiple keyword arguments must be separated by an ampersand (``&``); these
64will be translated to semicolons when the pyodbc connect string is generated
65internally::
66
67 e = create_engine(
68 "mssql+pyodbc://scott:tiger@mssql2017:1433/test?"
69 "driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes"
70 "&authentication=ActiveDirectoryIntegrated"
71 )
72
73The equivalent URL can be constructed using :class:`_sa.engine.URL`::
74
75 from sqlalchemy.engine import URL
76
77 connection_url = URL.create(
78 "mssql+pyodbc",
79 username="scott",
80 password="tiger",
81 host="mssql2017",
82 port=1433,
83 database="test",
84 query={
85 "driver": "ODBC Driver 18 for SQL Server",
86 "TrustServerCertificate": "yes",
87 "authentication": "ActiveDirectoryIntegrated",
88 },
89 )
90
91Pass through exact Pyodbc string
92^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
93
94A PyODBC connection string can also be sent in pyodbc's format directly, as
95specified in `the PyODBC documentation
96<https://github.com/mkleehammer/pyodbc/wiki/Connecting-to-databases>`_,
97using the parameter ``odbc_connect``. A :class:`_sa.engine.URL` object
98can help make this easier::
99
100 from sqlalchemy.engine import URL
101
102 connection_string = "DRIVER={SQL Server Native Client 10.0};SERVER=dagger;DATABASE=test;UID=user;PWD=password"
103 connection_url = URL.create(
104 "mssql+pyodbc", query={"odbc_connect": connection_string}
105 )
106
107 engine = create_engine(connection_url)
108
109.. _mssql_pyodbc_access_tokens:
110
111Connecting to databases with access tokens
112^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
113
114Some database servers are set up to only accept access tokens for login. For
115example, SQL Server allows the use of Azure Active Directory tokens to connect
116to databases. This requires creating a credential object using the
117``azure-identity`` library. More information about the authentication step can be
118found in `Microsoft's documentation
119<https://docs.microsoft.com/en-us/azure/developer/python/azure-sdk-authenticate?tabs=bash>`_.
120
121After getting an engine, the credentials need to be sent to ``pyodbc.connect``
122each time a connection is requested. One way to do this is to set up an event
123listener on the engine that adds the credential token to the dialect's connect
124call. This is discussed more generally in :ref:`engines_dynamic_tokens`. For
125SQL Server in particular, this is passed as an ODBC connection attribute with
126a data structure `described by Microsoft
127<https://docs.microsoft.com/en-us/sql/connect/odbc/using-azure-active-directory#authenticating-with-an-access-token>`_.
128
129The following code snippet will create an engine that connects to an Azure SQL
130database using Azure credentials::
131
132 import struct
133 from sqlalchemy import create_engine, event
134 from sqlalchemy.engine.url import URL
135 from azure import identity
136
137 # Connection option for access tokens, as defined in msodbcsql.h
138 SQL_COPT_SS_ACCESS_TOKEN = 1256
139 TOKEN_URL = "https://database.windows.net/" # The token URL for any Azure SQL database
140
141 connection_string = "mssql+pyodbc://@my-server.database.windows.net/myDb?driver=ODBC+Driver+17+for+SQL+Server"
142
143 engine = create_engine(connection_string)
144
145 azure_credentials = identity.DefaultAzureCredential()
146
147
148 @event.listens_for(engine, "do_connect")
149 def provide_token(dialect, conn_rec, cargs, cparams):
150 # remove the "Trusted_Connection" parameter that SQLAlchemy adds
151 cargs[0] = cargs[0].replace(";Trusted_Connection=Yes", "")
152
153 # create token credential
154 raw_token = azure_credentials.get_token(TOKEN_URL).token.encode(
155 "utf-16-le"
156 )
157 token_struct = struct.pack(
158 f"<I{len(raw_token)}s", len(raw_token), raw_token
159 )
160
161 # apply it to keyword arguments
162 cparams["attrs_before"] = {SQL_COPT_SS_ACCESS_TOKEN: token_struct}
163
164.. tip::
165
166 The ``Trusted_Connection`` token is currently added by the SQLAlchemy
167 pyodbc dialect when no username or password is present. This needs
168 to be removed per Microsoft's
169 `documentation for Azure access tokens
170 <https://docs.microsoft.com/en-us/sql/connect/odbc/using-azure-active-directory#authenticating-with-an-access-token>`_,
171 stating that a connection string when using an access token must not contain
172 ``UID``, ``PWD``, ``Authentication`` or ``Trusted_Connection`` parameters.
173
174.. _azure_synapse_ignore_no_transaction_on_rollback:
175
176Avoiding transaction-related exceptions on Azure Synapse Analytics
177^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
178
179Azure Synapse Analytics has a significant difference in its transaction
180handling compared to plain SQL Server; in some cases an error within a Synapse
181transaction can cause it to be arbitrarily terminated on the server side, which
182then causes the DBAPI ``.rollback()`` method (as well as ``.commit()``) to
183fail. The issue prevents the usual DBAPI contract of allowing ``.rollback()``
184to pass silently if no transaction is present as the driver does not expect
185this condition. The symptom of this failure is an exception with a message
186resembling 'No corresponding transaction found. (111214)' when attempting to
187emit a ``.rollback()`` after an operation had a failure of some kind.
188
189This specific case can be handled by passing ``ignore_no_transaction_on_rollback=True`` to
190the SQL Server dialect via the :func:`_sa.create_engine` function as follows::
191
192 engine = create_engine(
193 connection_url, ignore_no_transaction_on_rollback=True
194 )
195
196Using the above parameter, the dialect will catch ``ProgrammingError``
197exceptions raised during ``connection.rollback()`` and emit a warning
198if the error message contains code ``111214``, however will not raise
199an exception.
200
201.. versionadded:: 1.4.40 Added the
202 ``ignore_no_transaction_on_rollback=True`` parameter.
203
204Enable autocommit for Azure SQL Data Warehouse (DW) connections
205^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
206
207Azure SQL Data Warehouse does not support transactions,
208and that can cause problems with SQLAlchemy's "autobegin" (and implicit
209commit/rollback) behavior. We can avoid these problems by enabling autocommit
210at both the pyodbc and engine levels::
211
212 connection_url = sa.engine.URL.create(
213 "mssql+pyodbc",
214 username="scott",
215 password="tiger",
216 host="dw.azure.example.com",
217 database="mydb",
218 query={
219 "driver": "ODBC Driver 17 for SQL Server",
220 "autocommit": "True",
221 },
222 )
223
224 engine = create_engine(connection_url).execution_options(
225 isolation_level="AUTOCOMMIT"
226 )
227
228Avoiding sending large string parameters as TEXT/NTEXT
229^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
230
231By default, for historical reasons, Microsoft's ODBC drivers for SQL Server
232send long string parameters (greater than 4000 SBCS characters or 2000 Unicode
233characters) as TEXT/NTEXT values. TEXT and NTEXT have been deprecated for many
234years and are starting to cause compatibility issues with newer versions of
235SQL_Server/Azure. For example, see `this
236issue <https://github.com/mkleehammer/pyodbc/issues/835>`_.
237
238Starting with ODBC Driver 18 for SQL Server we can override the legacy
239behavior and pass long strings as varchar(max)/nvarchar(max) using the
240``LongAsMax=Yes`` connection string parameter::
241
242 connection_url = sa.engine.URL.create(
243 "mssql+pyodbc",
244 username="scott",
245 password="tiger",
246 host="mssqlserver.example.com",
247 database="mydb",
248 query={
249 "driver": "ODBC Driver 18 for SQL Server",
250 "LongAsMax": "Yes",
251 },
252 )
253
254Pyodbc Pooling / connection close behavior
255------------------------------------------
256
257PyODBC uses internal `pooling
258<https://github.com/mkleehammer/pyodbc/wiki/The-pyodbc-Module#pooling>`_ by
259default, which means connections will be longer lived than they are within
260SQLAlchemy itself. As SQLAlchemy has its own pooling behavior, it is often
261preferable to disable this behavior. This behavior can only be disabled
262globally at the PyODBC module level, **before** any connections are made::
263
264 import pyodbc
265
266 pyodbc.pooling = False
267
268 # don't use the engine before pooling is set to False
269 engine = create_engine("mssql+pyodbc://user:pass@dsn")
270
271If this variable is left at its default value of ``True``, **the application
272will continue to maintain active database connections**, even when the
273SQLAlchemy engine itself fully discards a connection or if the engine is
274disposed.
275
276.. seealso::
277
278 `pooling <https://github.com/mkleehammer/pyodbc/wiki/The-pyodbc-Module#pooling>`_ -
279 in the PyODBC documentation.
280
281Driver / Unicode Support
282-------------------------
283
284PyODBC works best with Microsoft ODBC drivers, particularly in the area
285of Unicode support on both Python 2 and Python 3.
286
287Using the FreeTDS ODBC drivers on Linux or OSX with PyODBC is **not**
288recommended; there have been historically many Unicode-related issues
289in this area, including before Microsoft offered ODBC drivers for Linux
290and OSX. Now that Microsoft offers drivers for all platforms, for
291PyODBC support these are recommended. FreeTDS remains relevant for
292non-ODBC drivers such as pymssql where it works very well.
293
294
295Rowcount Support
296----------------
297
298Previous limitations with the SQLAlchemy ORM's "versioned rows" feature with
299Pyodbc have been resolved as of SQLAlchemy 2.0.5. See the notes at
300:ref:`mssql_rowcount_versioning`.
301
302.. _mssql_pyodbc_fastexecutemany:
303
304Fast Executemany Mode
305---------------------
306
307The PyODBC driver includes support for a "fast executemany" mode of execution
308which greatly reduces round trips for a DBAPI ``executemany()`` call when using
309Microsoft ODBC drivers, for **limited size batches that fit in memory**. The
310feature is enabled by setting the attribute ``.fast_executemany`` on the DBAPI
311cursor when an executemany call is to be used. The SQLAlchemy PyODBC SQL
312Server dialect supports this parameter by passing the
313``fast_executemany`` parameter to
314:func:`_sa.create_engine` , when using the **Microsoft ODBC driver only**::
315
316 engine = create_engine(
317 "mssql+pyodbc://scott:tiger@mssql2017:1433/test?driver=ODBC+Driver+17+for+SQL+Server",
318 fast_executemany=True,
319 )
320
321.. versionchanged:: 2.0.9 - the ``fast_executemany`` parameter now has its
322 intended effect of this PyODBC feature taking effect for all INSERT
323 statements that are executed with multiple parameter sets, which don't
324 include RETURNING. Previously, SQLAlchemy 2.0's :term:`insertmanyvalues`
325 feature would cause ``fast_executemany`` to not be used in most cases
326 even if specified.
327
328.. seealso::
329
330 `fast executemany <https://github.com/mkleehammer/pyodbc/wiki/Features-beyond-the-DB-API#fast_executemany>`_
331 - on github
332
333.. _mssql_pyodbc_setinputsizes:
334
335Setinputsizes Support
336-----------------------
337
338As of version 2.0, the pyodbc ``cursor.setinputsizes()`` method is used for
339all statement executions, except for ``cursor.executemany()`` calls when
340fast_executemany=True where it is not supported (assuming
341:ref:`insertmanyvalues <engine_insertmanyvalues>` is kept enabled,
342"fastexecutemany" will not take place for INSERT statements in any case).
343
344The use of ``cursor.setinputsizes()`` can be disabled by passing
345``use_setinputsizes=False`` to :func:`_sa.create_engine`.
346
347When ``use_setinputsizes`` is left at its default of ``True``, the
348specific per-type symbols passed to ``cursor.setinputsizes()`` can be
349programmatically customized using the :meth:`.DialectEvents.do_setinputsizes`
350hook. See that method for usage examples.
351
352.. versionchanged:: 2.0 The mssql+pyodbc dialect now defaults to using
353 ``use_setinputsizes=True`` for all statement executions with the exception of
354 cursor.executemany() calls when fast_executemany=True. The behavior can
355 be turned off by passing ``use_setinputsizes=False`` to
356 :func:`_sa.create_engine`.
357
358""" # noqa
359
360import datetime
361import decimal
362import re
363import struct
364
365from .base import _MSDateTime
366from .base import _MSUnicode
367from .base import _MSUnicodeText
368from .base import BINARY
369from .base import DATETIMEOFFSET
370from .base import MSDialect
371from .base import VARBINARY
372from .json import JSON as _MSJson
373from .json import JSONIndexType as _MSJsonIndexType
374from .json import JSONPathType as _MSJsonPathType
375from ... import exc
376from ... import types as sqltypes
377from ... import util
378from ...connectors.pyodbc import PyODBCConnector
379
380
381class _ms_numeric_pyodbc:
382 """Turns Decimals with adjusted() < 0 or > 7 into strings.
383
384 The routines here are needed for older pyodbc versions
385 as well as current mxODBC versions.
386
387 """
388
389 def bind_processor(self, dialect):
390 super_process = super().bind_processor(dialect)
391
392 if not dialect._need_decimal_fix:
393 return super_process
394
395 def process(value):
396 if self.asdecimal and isinstance(value, decimal.Decimal):
397 adjusted = value.adjusted()
398 if adjusted < 0:
399 return self._small_dec_to_string(value)
400 elif adjusted > 7:
401 return self._large_dec_to_string(value)
402
403 if super_process:
404 return super_process(value)
405 else:
406 return value
407
408 return process
409
410 # these routines needed for older versions of pyodbc.
411 # as of 2.1.8 this logic is integrated.
412
413 def _small_dec_to_string(self, value):
414 return "%s0.%s%s" % (
415 (value < 0 and "-" or ""),
416 "0" * (abs(value.adjusted()) - 1),
417 "".join([str(nint) for nint in value.as_tuple()[1]]),
418 )
419
420 def _large_dec_to_string(self, value):
421 _int = value.as_tuple()[1]
422 if "E" in str(value):
423 result = "%s%s%s" % (
424 (value < 0 and "-" or ""),
425 "".join([str(s) for s in _int]),
426 "0" * (value.adjusted() - (len(_int) - 1)),
427 )
428 else:
429 if (len(_int) - 1) > value.adjusted():
430 result = "%s%s.%s" % (
431 (value < 0 and "-" or ""),
432 "".join([str(s) for s in _int][0 : value.adjusted() + 1]),
433 "".join([str(s) for s in _int][value.adjusted() + 1 :]),
434 )
435 else:
436 result = "%s%s" % (
437 (value < 0 and "-" or ""),
438 "".join([str(s) for s in _int][0 : value.adjusted() + 1]),
439 )
440 return result
441
442
443class _MSNumeric_pyodbc(_ms_numeric_pyodbc, sqltypes.Numeric):
444 pass
445
446
447class _MSFloat_pyodbc(_ms_numeric_pyodbc, sqltypes.Float):
448 pass
449
450
451class _ms_binary_pyodbc:
452 """Wraps binary values in dialect-specific Binary wrapper.
453 If the value is null, return a pyodbc-specific BinaryNull
454 object to prevent pyODBC [and FreeTDS] from defaulting binary
455 NULL types to SQLWCHAR and causing implicit conversion errors.
456 """
457
458 def bind_processor(self, dialect):
459 if dialect.dbapi is None:
460 return None
461
462 DBAPIBinary = dialect.dbapi.Binary
463
464 def process(value):
465 if value is not None:
466 return DBAPIBinary(value)
467 else:
468 # pyodbc-specific
469 return dialect.dbapi.BinaryNull
470
471 return process
472
473
474class _ODBCDateTimeBindProcessor:
475 """Add bind processors to handle datetimeoffset behaviors"""
476
477 has_tz = False
478
479 def bind_processor(self, dialect):
480 def process(value):
481 if value is None:
482 return None
483 elif isinstance(value, str):
484 # if a string was passed directly, allow it through
485 return value
486 elif not value.tzinfo or (not self.timezone and not self.has_tz):
487 # for DateTime(timezone=False)
488 return value
489 else:
490 # for DATETIMEOFFSET or DateTime(timezone=True)
491 #
492 # Convert to string format required by T-SQL. isoformat()
493 # zero-pads the year, which strftime("%Y") does not do on
494 # all platforms, and renders the offset with a colon,
495 # e.g. -07:00. backend currently rejects seconds /
496 # fractional seconds in the offset
497 return value.isoformat(sep=" ", timespec="microseconds")
498
499 return process
500
501
502class _ODBCDateTime(_ODBCDateTimeBindProcessor, _MSDateTime):
503 pass
504
505
506class _ODBCDATETIMEOFFSET(_ODBCDateTimeBindProcessor, DATETIMEOFFSET):
507 has_tz = True
508
509
510class _VARBINARY_pyodbc(_ms_binary_pyodbc, VARBINARY):
511 pass
512
513
514class _BINARY_pyodbc(_ms_binary_pyodbc, BINARY):
515 pass
516
517
518class _String_pyodbc(sqltypes.String):
519 def get_dbapi_type(self, dbapi):
520 if self.length in (None, "max") or self.length >= 2000:
521 return (dbapi.SQL_VARCHAR, 0, 0)
522 else:
523 return dbapi.SQL_VARCHAR
524
525
526class _Unicode_pyodbc(_MSUnicode):
527 def get_dbapi_type(self, dbapi):
528 if self.length in (None, "max") or self.length >= 2000:
529 return (dbapi.SQL_WVARCHAR, 0, 0)
530 else:
531 return dbapi.SQL_WVARCHAR
532
533
534class _UnicodeText_pyodbc(_MSUnicodeText):
535 def get_dbapi_type(self, dbapi):
536 if self.length in (None, "max") or self.length >= 2000:
537 return (dbapi.SQL_WVARCHAR, 0, 0)
538 else:
539 return dbapi.SQL_WVARCHAR
540
541
542class _JSON_pyodbc(_MSJson):
543 def get_dbapi_type(self, dbapi):
544 return (dbapi.SQL_WVARCHAR, 0, 0)
545
546
547class _JSONIndexType_pyodbc(_MSJsonIndexType):
548 def get_dbapi_type(self, dbapi):
549 return dbapi.SQL_WVARCHAR
550
551
552class _JSONPathType_pyodbc(_MSJsonPathType):
553 def get_dbapi_type(self, dbapi):
554 return dbapi.SQL_WVARCHAR
555
556
557class MSDialect_pyodbc(PyODBCConnector, MSDialect):
558 supports_statement_cache = True
559
560 scope_identity_must_be_embedded = True
561
562 # note this parameter is no longer used by the ORM or default dialect
563 # see #9414
564 supports_sane_rowcount_returning = False
565
566 colspecs = util.update_copy(
567 MSDialect.colspecs,
568 {
569 sqltypes.Numeric: _MSNumeric_pyodbc,
570 sqltypes.Float: _MSFloat_pyodbc,
571 BINARY: _BINARY_pyodbc,
572 # support DateTime(timezone=True)
573 sqltypes.DateTime: _ODBCDateTime,
574 DATETIMEOFFSET: _ODBCDATETIMEOFFSET,
575 # SQL Server dialect has a VARBINARY that is just to support
576 # "deprecate_large_types" w/ VARBINARY(max), but also we must
577 # handle the usual SQL standard VARBINARY
578 VARBINARY: _VARBINARY_pyodbc,
579 sqltypes.VARBINARY: _VARBINARY_pyodbc,
580 sqltypes.LargeBinary: _VARBINARY_pyodbc,
581 sqltypes.String: _String_pyodbc,
582 sqltypes.Unicode: _Unicode_pyodbc,
583 sqltypes.UnicodeText: _UnicodeText_pyodbc,
584 sqltypes.JSON: _JSON_pyodbc,
585 sqltypes.JSON.JSONIndexType: _JSONIndexType_pyodbc,
586 sqltypes.JSON.JSONPathType: _JSONPathType_pyodbc,
587 # this excludes Enum from the string/VARCHAR thing for now
588 # it looks like Enum's adaptation doesn't really support the
589 # String type itself having a dialect-level impl
590 sqltypes.Enum: sqltypes.Enum,
591 },
592 )
593
594 def __init__(
595 self,
596 fast_executemany=False,
597 use_setinputsizes=True,
598 **params,
599 ):
600 super().__init__(use_setinputsizes=use_setinputsizes, **params)
601 self.use_scope_identity = (
602 self.use_scope_identity
603 and self.dbapi
604 and hasattr(self.dbapi.Cursor, "nextset")
605 )
606 # a version which can't be determined is assumed to be an old one
607 version = self._dbapi_version_or_none
608 self._need_decimal_fix = bool(self.dbapi) and (
609 version is None or version < (2, 1, 8)
610 )
611 self.fast_executemany = fast_executemany
612 if fast_executemany:
613 self.use_insertmanyvalues_wo_returning = False
614
615 def _get_server_version_info(self, connection):
616 try:
617 # "Version of the instance of SQL Server, in the form
618 # of 'major.minor.build.revision'"
619 raw = connection.exec_driver_sql(
620 "SELECT CAST(SERVERPROPERTY('ProductVersion') AS VARCHAR)"
621 ).scalar()
622 except exc.DBAPIError:
623 # SQL Server docs indicate this function isn't present prior to
624 # 2008. Before we had the VARCHAR cast above, pyodbc would also
625 # fail on this query.
626 return super()._get_server_version_info(connection)
627 else:
628 version = []
629 r = re.compile(r"[.\-]")
630 for n in r.split(raw):
631 try:
632 version.append(int(n))
633 except ValueError:
634 pass
635 return tuple(version)
636
637 def on_connect(self):
638 super_ = super().on_connect()
639
640 def on_connect(conn):
641 if super_ is not None:
642 super_(conn)
643
644 self._setup_timestampoffset_type(conn)
645
646 return on_connect
647
648 def _setup_timestampoffset_type(self, connection):
649 # output converter function for datetimeoffset
650 def _handle_datetimeoffset(dto_value):
651 tup = struct.unpack("<6hI2h", dto_value)
652 return datetime.datetime(
653 tup[0],
654 tup[1],
655 tup[2],
656 tup[3],
657 tup[4],
658 tup[5],
659 tup[6] // 1000,
660 datetime.timezone(
661 datetime.timedelta(hours=tup[7], minutes=tup[8])
662 ),
663 )
664
665 odbc_SQL_SS_TIMESTAMPOFFSET = -155 # as defined in SQLNCLI.h
666 connection.add_output_converter(
667 odbc_SQL_SS_TIMESTAMPOFFSET, _handle_datetimeoffset
668 )
669
670 def do_executemany(self, cursor, statement, parameters, context=None):
671 if self.fast_executemany:
672 cursor.fast_executemany = True
673 super().do_executemany(cursor, statement, parameters, context=context)
674
675 def is_disconnect(self, e, connection, cursor):
676 if isinstance(e, self.dbapi.Error):
677 code = e.args[0]
678 if code in {
679 "08S01",
680 "01000",
681 "01002",
682 "08003",
683 "08007",
684 "08S02",
685 "08001",
686 "HYT00",
687 "HY010",
688 "10054",
689 }:
690 return True
691 return super().is_disconnect(e, connection, cursor)
692
693
694dialect = MSDialect_pyodbc