Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/mssql/pyodbc.py: 42%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

154 statements  

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