Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/oracle/oracledb.py: 51%

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

214 statements  

1# dialects/oracle/oracledb.py 

2# Copyright (C) 2005-2025 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""".. dialect:: oracle+oracledb 

10 :name: python-oracledb 

11 :dbapi: oracledb 

12 :connectstring: oracle+oracledb://user:pass@hostname:port[/dbname][?service_name=<service>[&key=value&key=value...]] 

13 :url: https://oracle.github.io/python-oracledb/ 

14 

15Description 

16----------- 

17 

18Python-oracledb is the Oracle Database driver for Python. It features a default 

19"thin" client mode that requires no dependencies, and an optional "thick" mode 

20that uses Oracle Client libraries. It supports SQLAlchemy features including 

21two phase transactions and Asyncio. 

22 

23Python-oracle is the renamed, updated cx_Oracle driver. Oracle is no longer 

24doing any releases in the cx_Oracle namespace. 

25 

26The SQLAlchemy ``oracledb`` dialect provides both a sync and an async 

27implementation under the same dialect name. The proper version is 

28selected depending on how the engine is created: 

29 

30* calling :func:`_sa.create_engine` with ``oracle+oracledb://...`` will 

31 automatically select the sync version:: 

32 

33 from sqlalchemy import create_engine 

34 

35 sync_engine = create_engine( 

36 "oracle+oracledb://scott:tiger@localhost?service_name=FREEPDB1" 

37 ) 

38 

39* calling :func:`_asyncio.create_async_engine` with ``oracle+oracledb://...`` 

40 will automatically select the async version:: 

41 

42 from sqlalchemy.ext.asyncio import create_async_engine 

43 

44 asyncio_engine = create_async_engine( 

45 "oracle+oracledb://scott:tiger@localhost?service_name=FREEPDB1" 

46 ) 

47 

48 The asyncio version of the dialect may also be specified explicitly using the 

49 ``oracledb_async`` suffix:: 

50 

51 from sqlalchemy.ext.asyncio import create_async_engine 

52 

53 asyncio_engine = create_async_engine( 

54 "oracle+oracledb_async://scott:tiger@localhost?service_name=FREEPDB1" 

55 ) 

56 

57.. versionadded:: 2.0.25 added support for the async version of oracledb. 

58 

59Thick mode support 

60------------------ 

61 

62By default, the python-oracledb driver runs in a "thin" mode that does not 

63require Oracle Client libraries to be installed. The driver also supports a 

64"thick" mode that uses Oracle Client libraries to get functionality such as 

65Oracle Application Continuity. 

66 

67To enable thick mode, call `oracledb.init_oracle_client() 

68<https://python-oracledb.readthedocs.io/en/latest/api_manual/module.html#oracledb.init_oracle_client>`_ 

69explicitly, or pass the parameter ``thick_mode=True`` to 

70:func:`_sa.create_engine`. To pass custom arguments to 

71``init_oracle_client()``, like the ``lib_dir`` path, a dict may be passed, for 

72example:: 

73 

74 engine = sa.create_engine( 

75 "oracle+oracledb://...", 

76 thick_mode={ 

77 "lib_dir": "/path/to/oracle/client/lib", 

78 "config_dir": "/path/to/network_config_file_directory", 

79 "driver_name": "my-app : 1.0.0", 

80 }, 

81 ) 

82 

83Note that passing a ``lib_dir`` path should only be done on macOS or 

84Windows. On Linux it does not behave as you might expect. 

85 

86.. seealso:: 

87 

88 python-oracledb documentation `Enabling python-oracledb Thick mode 

89 <https://python-oracledb.readthedocs.io/en/latest/user_guide/initialization.html#enabling-python-oracledb-thick-mode>`_ 

90 

91Connecting to Oracle Database 

92----------------------------- 

93 

94python-oracledb provides several methods of indicating the target database. 

95The dialect translates from a series of different URL forms. 

96 

97Given the hostname, port and service name of the target database, you can 

98connect in SQLAlchemy using the ``service_name`` query string parameter:: 

99 

100 engine = create_engine( 

101 "oracle+oracledb://scott:tiger@hostname:port?service_name=myservice" 

102 ) 

103 

104Connecting with Easy Connect strings 

105^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

106 

107You can pass any valid python-oracledb connection string as the ``dsn`` key 

108value in a :paramref:`_sa.create_engine.connect_args` dictionary. See 

109python-oracledb documentation `Oracle Net Services Connection Strings 

110<https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html#oracle-net-services-connection-strings>`_. 

111 

112For example to use an `Easy Connect string 

113<https://download.oracle.com/ocomdocs/global/Oracle-Net-Easy-Connect-Plus.pdf>`_ 

114with a timeout to prevent connection establishment from hanging if the network 

115transport to the database cannot be establishd in 30 seconds, and also setting 

116a keep-alive time of 60 seconds to stop idle network connections from being 

117terminated by a firewall:: 

118 

119 e = create_engine( 

120 "oracle+oracledb://@", 

121 connect_args={ 

122 "user": "scott", 

123 "password": "tiger", 

124 "dsn": "hostname:port/myservice?transport_connect_timeout=30&expire_time=60", 

125 }, 

126 ) 

127 

128The Easy Connect syntax has been enhanced during the life of Oracle Database. 

129Review the documentation for your database version. The current documentation 

130is at `Understanding the Easy Connect Naming Method 

131<https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=GUID-B0437826-43C1-49EC-A94D-B650B6A4A6EE>`_. 

132 

133The general syntax is similar to: 

134 

135.. sourcecode:: text 

136 

137 [[protocol:]//]host[:port][/[service_name]][?parameter_name=value{&parameter_name=value}] 

138 

139Note that although the SQLAlchemy URL syntax ``hostname:port/dbname`` looks 

140like Oracle's Easy Connect syntax, it is different. SQLAlchemy's URL requires a 

141system identifier (SID) for the ``dbname`` component:: 

142 

143 engine = create_engine("oracle+oracledb://scott:tiger@hostname:port/sid") 

144 

145Easy Connect syntax does not support SIDs. It uses services names, which are 

146the preferred choice for connecting to Oracle Database. 

147 

148Passing python-oracledb connect arguments 

149^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

150 

151Other python-oracledb driver `connection options 

152<https://python-oracledb.readthedocs.io/en/latest/api_manual/module.html#oracledb.connect>`_ 

153can be passed in ``connect_args``. For example:: 

154 

155 e = create_engine( 

156 "oracle+oracledb://@", 

157 connect_args={ 

158 "user": "scott", 

159 "password": "tiger", 

160 "dsn": "hostname:port/myservice", 

161 "events": True, 

162 "mode": oracledb.AUTH_MODE_SYSDBA, 

163 }, 

164 ) 

165 

166Connecting with tnsnames.ora TNS aliases 

167^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

168 

169If no port, database name, or service name is provided, the dialect will use an 

170Oracle Database DSN "connection string". This takes the "hostname" portion of 

171the URL as the data source name. For example, if the ``tnsnames.ora`` file 

172contains a `TNS Alias 

173<https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html#tns-aliases-for-connection-strings>`_ 

174of ``myalias`` as below: 

175 

176.. sourcecode:: text 

177 

178 myalias = 

179 (DESCRIPTION = 

180 (ADDRESS = (PROTOCOL = TCP)(HOST = mymachine.example.com)(PORT = 1521)) 

181 (CONNECT_DATA = 

182 (SERVER = DEDICATED) 

183 (SERVICE_NAME = orclpdb1) 

184 ) 

185 ) 

186 

187The python-oracledb dialect connects to this database service when ``myalias`` is the 

188hostname portion of the URL, without specifying a port, database name or 

189``service_name``:: 

190 

191 engine = create_engine("oracle+oracledb://scott:tiger@myalias") 

192 

193Connecting to Oracle Autonomous Database 

194^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

195 

196Users of Oracle Autonomous Database should use either use the TNS Alias URL 

197shown above, or pass the TNS Alias as the ``dsn`` key value in a 

198:paramref:`_sa.create_engine.connect_args` dictionary. 

199 

200If Oracle Autonomous Database is configured for mutual TLS ("mTLS") 

201connections, then additional configuration is required as shown in `Connecting 

202to Oracle Cloud Autonomous Databases 

203<https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html#connecting-to-oracle-cloud-autonomous-databases>`_. In 

204summary, Thick mode users should configure file locations and set the wallet 

205path in ``sqlnet.ora`` appropriately:: 

206 

207 e = create_engine( 

208 "oracle+oracledb://@", 

209 thick_mode={ 

210 # directory containing tnsnames.ora and cwallet.so 

211 "config_dir": "/opt/oracle/wallet_dir", 

212 }, 

213 connect_args={ 

214 "user": "scott", 

215 "password": "tiger", 

216 "dsn": "mydb_high", 

217 }, 

218 ) 

219 

220Thin mode users of mTLS should pass the appropriate directories and PEM wallet 

221password when creating the engine, similar to:: 

222 

223 e = create_engine( 

224 "oracle+oracledb://@", 

225 connect_args={ 

226 "user": "scott", 

227 "password": "tiger", 

228 "dsn": "mydb_high", 

229 "config_dir": "/opt/oracle/wallet_dir", # directory containing tnsnames.ora 

230 "wallet_location": "/opt/oracle/wallet_dir", # directory containing ewallet.pem 

231 "wallet_password": "top secret", # password for the PEM file 

232 }, 

233 ) 

234 

235Typically ``config_dir`` and ``wallet_location`` are the same directory, which 

236is where the Oracle Autonomous Database wallet zip file was extracted. Note 

237this directory should be protected. 

238 

239Connection Pooling 

240------------------ 

241 

242Applications with multiple concurrent users should use connection pooling. A 

243minimal sized connection pool is also beneficial for long-running, single-user 

244applications that do not frequently use a connection. 

245 

246The python-oracledb driver provides its own connection pool implementation that 

247may be used in place of SQLAlchemy's pooling functionality. The driver pool 

248gives support for high availability features such as dead connection detection, 

249connection draining for planned database downtime, support for Oracle 

250Application Continuity and Transparent Application Continuity, and gives 

251support for `Database Resident Connection Pooling (DRCP) 

252<https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html#database-resident-connection-pooling-drcp>`_. 

253 

254To take advantage of python-oracledb's pool, use the 

255:paramref:`_sa.create_engine.creator` parameter to provide a function that 

256returns a new connection, along with setting 

257:paramref:`_sa.create_engine.pool_class` to ``NullPool`` to disable 

258SQLAlchemy's pooling:: 

259 

260 import oracledb 

261 from sqlalchemy import create_engine 

262 from sqlalchemy import text 

263 from sqlalchemy.pool import NullPool 

264 

265 # Uncomment to use the optional python-oracledb Thick mode. 

266 # Review the python-oracledb doc for the appropriate parameters 

267 # oracledb.init_oracle_client(<your parameters>) 

268 

269 pool = oracledb.create_pool( 

270 user="scott", 

271 password="tiger", 

272 dsn="localhost:1521/freepdb1", 

273 min=1, 

274 max=4, 

275 increment=1, 

276 ) 

277 engine = create_engine( 

278 "oracle+oracledb://", creator=pool.acquire, poolclass=NullPool 

279 ) 

280 

281The above engine may then be used normally. Internally, python-oracledb handles 

282connection pooling:: 

283 

284 with engine.connect() as conn: 

285 print(conn.scalar(text("select 1 from dual"))) 

286 

287Refer to the python-oracledb documentation for `oracledb.create_pool() 

288<https://python-oracledb.readthedocs.io/en/latest/api_manual/module.html#oracledb.create_pool>`_ 

289for the arguments that can be used when creating a connection pool. 

290 

291.. _drcp: 

292 

293Using Oracle Database Resident Connection Pooling (DRCP) 

294-------------------------------------------------------- 

295 

296When using Oracle Database's Database Resident Connection Pooling (DRCP), the 

297best practice is to specify a connection class and "purity". Refer to the 

298`python-oracledb documentation on DRCP 

299<https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html#database-resident-connection-pooling-drcp>`_. 

300For example:: 

301 

302 import oracledb 

303 from sqlalchemy import create_engine 

304 from sqlalchemy import text 

305 from sqlalchemy.pool import NullPool 

306 

307 # Uncomment to use the optional python-oracledb Thick mode. 

308 # Review the python-oracledb doc for the appropriate parameters 

309 # oracledb.init_oracle_client(<your parameters>) 

310 

311 pool = oracledb.create_pool( 

312 user="scott", 

313 password="tiger", 

314 dsn="localhost:1521/freepdb1", 

315 min=1, 

316 max=4, 

317 increment=1, 

318 cclass="MYCLASS", 

319 purity=oracledb.PURITY_SELF, 

320 ) 

321 engine = create_engine( 

322 "oracle+oracledb://", creator=pool.acquire, poolclass=NullPool 

323 ) 

324 

325The above engine may then be used normally where python-oracledb handles 

326application connection pooling and Oracle Database additionally uses DRCP:: 

327 

328 with engine.connect() as conn: 

329 print(conn.scalar(text("select 1 from dual"))) 

330 

331If you wish to use different connection classes or purities for different 

332connections, then wrap ``pool.acquire()``:: 

333 

334 import oracledb 

335 from sqlalchemy import create_engine 

336 from sqlalchemy import text 

337 from sqlalchemy.pool import NullPool 

338 

339 # Uncomment to use python-oracledb Thick mode. 

340 # Review the python-oracledb doc for the appropriate parameters 

341 # oracledb.init_oracle_client(<your parameters>) 

342 

343 pool = oracledb.create_pool( 

344 user="scott", 

345 password="tiger", 

346 dsn="localhost:1521/freepdb1", 

347 min=1, 

348 max=4, 

349 increment=1, 

350 cclass="MYCLASS", 

351 purity=oracledb.PURITY_SELF, 

352 ) 

353 

354 

355 def creator(): 

356 return pool.acquire(cclass="MYOTHERCLASS", purity=oracledb.PURITY_NEW) 

357 

358 

359 engine = create_engine( 

360 "oracle+oracledb://", creator=creator, poolclass=NullPool 

361 ) 

362 

363Engine Options consumed by the SQLAlchemy oracledb dialect outside of the driver 

364-------------------------------------------------------------------------------- 

365 

366There are also options that are consumed by the SQLAlchemy oracledb dialect 

367itself. These options are always passed directly to :func:`_sa.create_engine`, 

368such as:: 

369 

370 e = create_engine("oracle+oracledb://user:pass@tnsalias", arraysize=500) 

371 

372The parameters accepted by the oracledb dialect are as follows: 

373 

374* ``arraysize`` - set the driver cursor.arraysize value. It defaults to 

375 ``None``, indicating that the driver default value of 100 should be used. 

376 This setting controls how many rows are buffered when fetching rows, and can 

377 have a significant effect on performance if increased for queries that return 

378 large numbers of rows. 

379 

380 .. versionchanged:: 2.0.26 - changed the default value from 50 to None, 

381 to use the default value of the driver itself. 

382 

383* ``auto_convert_lobs`` - defaults to True; See :ref:`oracledb_lob`. 

384 

385* ``coerce_to_decimal`` - see :ref:`oracledb_numeric` for detail. 

386 

387* ``encoding_errors`` - see :ref:`oracledb_unicode_encoding_errors` for detail. 

388 

389.. _oracledb_unicode: 

390 

391Unicode 

392------- 

393 

394As is the case for all DBAPIs under Python 3, all strings are inherently 

395Unicode strings. 

396 

397Ensuring the Correct Client Encoding 

398^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

399 

400In python-oracledb, the encoding used for all character data is "UTF-8". 

401 

402Unicode-specific Column datatypes 

403^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

404 

405The Core expression language handles unicode data by use of the 

406:class:`.Unicode` and :class:`.UnicodeText` datatypes. These types correspond 

407to the VARCHAR2 and CLOB Oracle Database datatypes by default. When using 

408these datatypes with Unicode data, it is expected that the database is 

409configured with a Unicode-aware character set so that the VARCHAR2 and CLOB 

410datatypes can accommodate the data. 

411 

412In the case that Oracle Database is not configured with a Unicode character 

413set, the two options are to use the :class:`_types.NCHAR` and 

414:class:`_oracle.NCLOB` datatypes explicitly, or to pass the flag 

415``use_nchar_for_unicode=True`` to :func:`_sa.create_engine`, which will cause 

416the SQLAlchemy dialect to use NCHAR/NCLOB for the :class:`.Unicode` / 

417:class:`.UnicodeText` datatypes instead of VARCHAR/CLOB. 

418 

419.. versionchanged:: 1.3 The :class:`.Unicode` and :class:`.UnicodeText` 

420 datatypes now correspond to the ``VARCHAR2`` and ``CLOB`` Oracle Database 

421 datatypes unless the ``use_nchar_for_unicode=True`` is passed to the dialect 

422 when :func:`_sa.create_engine` is called. 

423 

424 

425.. _oracledb_unicode_encoding_errors: 

426 

427Encoding Errors 

428^^^^^^^^^^^^^^^ 

429 

430For the unusual case that data in Oracle Database is present with a broken 

431encoding, the dialect accepts a parameter ``encoding_errors`` which will be 

432passed to Unicode decoding functions in order to affect how decoding errors are 

433handled. The value is ultimately consumed by the Python `decode 

434<https://docs.python.org/3/library/stdtypes.html#bytes.decode>`_ function, and 

435is passed both via python-oracledb's ``encodingErrors`` parameter consumed by 

436``Cursor.var()``, as well as SQLAlchemy's own decoding function, as the 

437python-oracledb dialect makes use of both under different circumstances. 

438 

439.. versionadded:: 1.3.11 

440 

441 

442.. _oracledb_setinputsizes: 

443 

444Fine grained control over python-oracledb data binding with setinputsizes 

445------------------------------------------------------------------------- 

446 

447The python-oracle DBAPI has a deep and fundamental reliance upon the usage of 

448the DBAPI ``setinputsizes()`` call. The purpose of this call is to establish 

449the datatypes that are bound to a SQL statement for Python values being passed 

450as parameters. While virtually no other DBAPI assigns any use to the 

451``setinputsizes()`` call, the python-oracledb DBAPI relies upon it heavily in 

452its interactions with the Oracle Database, and in some scenarios it is not 

453possible for SQLAlchemy to know exactly how data should be bound, as some 

454settings can cause profoundly different performance characteristics, while 

455altering the type coercion behavior at the same time. 

456 

457Users of the oracledb dialect are **strongly encouraged** to read through 

458python-oracledb's list of built-in datatype symbols at `Database Types 

459<https://python-oracledb.readthedocs.io/en/latest/api_manual/module.html#database-types>`_ 

460Note that in some cases, significant performance degradation can occur when 

461using these types vs. not. 

462 

463On the SQLAlchemy side, the :meth:`.DialectEvents.do_setinputsizes` event can 

464be used both for runtime visibility (e.g. logging) of the setinputsizes step as 

465well as to fully control how ``setinputsizes()`` is used on a per-statement 

466basis. 

467 

468.. versionadded:: 1.2.9 Added :meth:`.DialectEvents.setinputsizes` 

469 

470 

471Example 1 - logging all setinputsizes calls 

472^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

473 

474The following example illustrates how to log the intermediary values from a 

475SQLAlchemy perspective before they are converted to the raw ``setinputsizes()`` 

476parameter dictionary. The keys of the dictionary are :class:`.BindParameter` 

477objects which have a ``.key`` and a ``.type`` attribute:: 

478 

479 from sqlalchemy import create_engine, event 

480 

481 engine = create_engine( 

482 "oracle+oracledb://scott:tiger@localhost:1521?service_name=freepdb1" 

483 ) 

484 

485 

486 @event.listens_for(engine, "do_setinputsizes") 

487 def _log_setinputsizes(inputsizes, cursor, statement, parameters, context): 

488 for bindparam, dbapitype in inputsizes.items(): 

489 log.info( 

490 "Bound parameter name: %s SQLAlchemy type: %r DBAPI object: %s", 

491 bindparam.key, 

492 bindparam.type, 

493 dbapitype, 

494 ) 

495 

496Example 2 - remove all bindings to CLOB 

497^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

498 

499For performance, fetching LOB datatypes from Oracle Database is set by default 

500for the ``Text`` type within SQLAlchemy. This setting can be modified as 

501follows:: 

502 

503 

504 from sqlalchemy import create_engine, event 

505 from oracledb import CLOB 

506 

507 engine = create_engine( 

508 "oracle+oracledb://scott:tiger@localhost:1521?service_name=freepdb1" 

509 ) 

510 

511 

512 @event.listens_for(engine, "do_setinputsizes") 

513 def _remove_clob(inputsizes, cursor, statement, parameters, context): 

514 for bindparam, dbapitype in list(inputsizes.items()): 

515 if dbapitype is CLOB: 

516 del inputsizes[bindparam] 

517 

518.. _oracledb_lob: 

519 

520LOB Datatypes 

521-------------- 

522 

523LOB datatypes refer to the "large object" datatypes such as CLOB, NCLOB and 

524BLOB. Oracle Database can efficiently return these datatypes as a single 

525buffer. SQLAlchemy makes use of type handlers to do this by default. 

526 

527To disable the use of the type handlers and deliver LOB objects as classic 

528buffered objects with a ``read()`` method, the parameter 

529``auto_convert_lobs=False`` may be passed to :func:`_sa.create_engine`. 

530 

531.. _oracledb_returning: 

532 

533RETURNING Support 

534----------------- 

535 

536The oracledb dialect implements RETURNING using OUT parameters. The dialect 

537supports RETURNING fully. 

538 

539Two Phase Transaction Support 

540----------------------------- 

541 

542Two phase transactions are fully supported with python-oracledb. (Thin mode 

543requires python-oracledb 2.3). APIs for two phase transactions are provided at 

544the Core level via :meth:`_engine.Connection.begin_twophase` and 

545:paramref:`_orm.Session.twophase` for transparent ORM use. 

546 

547.. versionchanged:: 2.0.32 added support for two phase transactions 

548 

549.. _oracledb_numeric: 

550 

551Precision Numerics 

552------------------ 

553 

554SQLAlchemy's numeric types can handle receiving and returning values as Python 

555``Decimal`` objects or float objects. When a :class:`.Numeric` object, or a 

556subclass such as :class:`.Float`, :class:`_oracle.DOUBLE_PRECISION` etc. is in 

557use, the :paramref:`.Numeric.asdecimal` flag determines if values should be 

558coerced to ``Decimal`` upon return, or returned as float objects. To make 

559matters more complicated under Oracle Database, the ``NUMBER`` type can also 

560represent integer values if the "scale" is zero, so the Oracle 

561Database-specific :class:`_oracle.NUMBER` type takes this into account as well. 

562 

563The oracledb dialect makes extensive use of connection- and cursor-level 

564"outputtypehandler" callables in order to coerce numeric values as requested. 

565These callables are specific to the specific flavor of :class:`.Numeric` in 

566use, as well as if no SQLAlchemy typing objects are present. There are 

567observed scenarios where Oracle Database may send incomplete or ambiguous 

568information about the numeric types being returned, such as a query where the 

569numeric types are buried under multiple levels of subquery. The type handlers 

570do their best to make the right decision in all cases, deferring to the 

571underlying python-oracledb DBAPI for all those cases where the driver can make 

572the best decision. 

573 

574When no typing objects are present, as when executing plain SQL strings, a 

575default "outputtypehandler" is present which will generally return numeric 

576values which specify precision and scale as Python ``Decimal`` objects. To 

577disable this coercion to decimal for performance reasons, pass the flag 

578``coerce_to_decimal=False`` to :func:`_sa.create_engine`:: 

579 

580 engine = create_engine( 

581 "oracle+oracledb://scott:tiger@tnsalias", coerce_to_decimal=False 

582 ) 

583 

584The ``coerce_to_decimal`` flag only impacts the results of plain string 

585SQL statements that are not otherwise associated with a :class:`.Numeric` 

586SQLAlchemy type (or a subclass of such). 

587 

588.. versionchanged:: 1.2 The numeric handling system for the oracle dialects has 

589 been reworked to take advantage of newer driver features as well as better 

590 integration of outputtypehandlers. 

591 

592.. versionadded:: 2.0.0 added support for the python-oracledb driver. 

593 

594""" # noqa 

595from __future__ import annotations 

596 

597import collections 

598import re 

599from typing import Any 

600from typing import TYPE_CHECKING 

601 

602from . import cx_oracle as _cx_oracle 

603from ... import exc 

604from ... import pool 

605from ...connectors.asyncio import AsyncAdapt_dbapi_connection 

606from ...connectors.asyncio import AsyncAdapt_dbapi_cursor 

607from ...connectors.asyncio import AsyncAdapt_dbapi_ss_cursor 

608from ...connectors.asyncio import AsyncAdaptFallback_dbapi_connection 

609from ...engine import default 

610from ...util import asbool 

611from ...util import await_fallback 

612from ...util import await_only 

613 

614if TYPE_CHECKING: 

615 from oracledb import AsyncConnection 

616 from oracledb import AsyncCursor 

617 

618 

619class OracleExecutionContext_oracledb( 

620 _cx_oracle.OracleExecutionContext_cx_oracle 

621): 

622 pass 

623 

624 

625class OracleDialect_oracledb(_cx_oracle.OracleDialect_cx_oracle): 

626 supports_statement_cache = True 

627 execution_ctx_cls = OracleExecutionContext_oracledb 

628 

629 driver = "oracledb" 

630 _min_version = (1,) 

631 

632 def __init__( 

633 self, 

634 auto_convert_lobs=True, 

635 coerce_to_decimal=True, 

636 arraysize=None, 

637 encoding_errors=None, 

638 thick_mode=None, 

639 **kwargs, 

640 ): 

641 super().__init__( 

642 auto_convert_lobs, 

643 coerce_to_decimal, 

644 arraysize, 

645 encoding_errors, 

646 **kwargs, 

647 ) 

648 

649 if self.dbapi is not None and ( 

650 thick_mode or isinstance(thick_mode, dict) 

651 ): 

652 kw = thick_mode if isinstance(thick_mode, dict) else {} 

653 self.dbapi.init_oracle_client(**kw) 

654 

655 @classmethod 

656 def import_dbapi(cls): 

657 import oracledb 

658 

659 return oracledb 

660 

661 @classmethod 

662 def is_thin_mode(cls, connection): 

663 return connection.connection.dbapi_connection.thin 

664 

665 @classmethod 

666 def get_async_dialect_cls(cls, url): 

667 return OracleDialectAsync_oracledb 

668 

669 def _load_version(self, dbapi_module): 

670 version = (0, 0, 0) 

671 if dbapi_module is not None: 

672 m = re.match(r"(\d+)\.(\d+)(?:\.(\d+))?", dbapi_module.version) 

673 if m: 

674 version = tuple( 

675 int(x) for x in m.group(1, 2, 3) if x is not None 

676 ) 

677 self.oracledb_ver = version 

678 if ( 

679 self.oracledb_ver > (0, 0, 0) 

680 and self.oracledb_ver < self._min_version 

681 ): 

682 raise exc.InvalidRequestError( 

683 f"oracledb version {self._min_version} and above are supported" 

684 ) 

685 

686 def do_begin_twophase(self, connection, xid): 

687 conn_xis = connection.connection.xid(*xid) 

688 connection.connection.tpc_begin(conn_xis) 

689 connection.connection.info["oracledb_xid"] = conn_xis 

690 

691 def do_prepare_twophase(self, connection, xid): 

692 should_commit = connection.connection.tpc_prepare() 

693 connection.info["oracledb_should_commit"] = should_commit 

694 

695 def do_rollback_twophase( 

696 self, connection, xid, is_prepared=True, recover=False 

697 ): 

698 if recover: 

699 conn_xid = connection.connection.xid(*xid) 

700 else: 

701 conn_xid = None 

702 connection.connection.tpc_rollback(conn_xid) 

703 

704 def do_commit_twophase( 

705 self, connection, xid, is_prepared=True, recover=False 

706 ): 

707 conn_xid = None 

708 if not is_prepared: 

709 should_commit = connection.connection.tpc_prepare() 

710 elif recover: 

711 conn_xid = connection.connection.xid(*xid) 

712 should_commit = True 

713 else: 

714 should_commit = connection.info["oracledb_should_commit"] 

715 if should_commit: 

716 connection.connection.tpc_commit(conn_xid) 

717 

718 def do_recover_twophase(self, connection): 

719 return [ 

720 # oracledb seems to return bytes 

721 ( 

722 fi, 

723 gti.decode() if isinstance(gti, bytes) else gti, 

724 bq.decode() if isinstance(bq, bytes) else bq, 

725 ) 

726 for fi, gti, bq in connection.connection.tpc_recover() 

727 ] 

728 

729 def _check_max_identifier_length(self, connection): 

730 if self.oracledb_ver >= (2, 5): 

731 max_len = connection.connection.max_identifier_length 

732 if max_len is not None: 

733 return max_len 

734 return super()._check_max_identifier_length(connection) 

735 

736 

737class AsyncAdapt_oracledb_cursor(AsyncAdapt_dbapi_cursor): 

738 _cursor: AsyncCursor 

739 __slots__ = () 

740 

741 @property 

742 def outputtypehandler(self): 

743 return self._cursor.outputtypehandler 

744 

745 @outputtypehandler.setter 

746 def outputtypehandler(self, value): 

747 self._cursor.outputtypehandler = value 

748 

749 def var(self, *args, **kwargs): 

750 return self._cursor.var(*args, **kwargs) 

751 

752 def close(self): 

753 self._rows.clear() 

754 self._cursor.close() 

755 

756 def setinputsizes(self, *args: Any, **kwargs: Any) -> Any: 

757 return self._cursor.setinputsizes(*args, **kwargs) 

758 

759 def _aenter_cursor(self, cursor: AsyncCursor) -> AsyncCursor: 

760 try: 

761 return cursor.__enter__() 

762 except Exception as error: 

763 self._adapt_connection._handle_exception(error) 

764 

765 async def _execute_async(self, operation, parameters): 

766 # override to not use mutex, oracledb already has a mutex 

767 

768 if parameters is None: 

769 result = await self._cursor.execute(operation) 

770 else: 

771 result = await self._cursor.execute(operation, parameters) 

772 

773 if self._cursor.description and not self.server_side: 

774 self._rows = collections.deque(await self._cursor.fetchall()) 

775 return result 

776 

777 async def _executemany_async( 

778 self, 

779 operation, 

780 seq_of_parameters, 

781 ): 

782 # override to not use mutex, oracledb already has a mutex 

783 return await self._cursor.executemany(operation, seq_of_parameters) 

784 

785 def __enter__(self): 

786 return self 

787 

788 def __exit__(self, type_: Any, value: Any, traceback: Any) -> None: 

789 self.close() 

790 

791 

792class AsyncAdapt_oracledb_ss_cursor( 

793 AsyncAdapt_dbapi_ss_cursor, AsyncAdapt_oracledb_cursor 

794): 

795 __slots__ = () 

796 

797 def close(self) -> None: 

798 if self._cursor is not None: 

799 self._cursor.close() 

800 self._cursor = None # type: ignore 

801 

802 

803class AsyncAdapt_oracledb_connection(AsyncAdapt_dbapi_connection): 

804 _connection: AsyncConnection 

805 __slots__ = () 

806 

807 thin = True 

808 

809 _cursor_cls = AsyncAdapt_oracledb_cursor 

810 _ss_cursor_cls = None 

811 

812 @property 

813 def autocommit(self): 

814 return self._connection.autocommit 

815 

816 @autocommit.setter 

817 def autocommit(self, value): 

818 self._connection.autocommit = value 

819 

820 @property 

821 def outputtypehandler(self): 

822 return self._connection.outputtypehandler 

823 

824 @outputtypehandler.setter 

825 def outputtypehandler(self, value): 

826 self._connection.outputtypehandler = value 

827 

828 @property 

829 def version(self): 

830 return self._connection.version 

831 

832 @property 

833 def stmtcachesize(self): 

834 return self._connection.stmtcachesize 

835 

836 @stmtcachesize.setter 

837 def stmtcachesize(self, value): 

838 self._connection.stmtcachesize = value 

839 

840 @property 

841 def max_identifier_length(self): 

842 return self._connection.max_identifier_length 

843 

844 def cursor(self): 

845 return AsyncAdapt_oracledb_cursor(self) 

846 

847 def ss_cursor(self): 

848 return AsyncAdapt_oracledb_ss_cursor(self) 

849 

850 def xid(self, *args: Any, **kwargs: Any) -> Any: 

851 return self._connection.xid(*args, **kwargs) 

852 

853 def tpc_begin(self, *args: Any, **kwargs: Any) -> Any: 

854 return self.await_(self._connection.tpc_begin(*args, **kwargs)) 

855 

856 def tpc_commit(self, *args: Any, **kwargs: Any) -> Any: 

857 return self.await_(self._connection.tpc_commit(*args, **kwargs)) 

858 

859 def tpc_prepare(self, *args: Any, **kwargs: Any) -> Any: 

860 return self.await_(self._connection.tpc_prepare(*args, **kwargs)) 

861 

862 def tpc_recover(self, *args: Any, **kwargs: Any) -> Any: 

863 return self.await_(self._connection.tpc_recover(*args, **kwargs)) 

864 

865 def tpc_rollback(self, *args: Any, **kwargs: Any) -> Any: 

866 return self.await_(self._connection.tpc_rollback(*args, **kwargs)) 

867 

868 

869class AsyncAdaptFallback_oracledb_connection( 

870 AsyncAdaptFallback_dbapi_connection, AsyncAdapt_oracledb_connection 

871): 

872 __slots__ = () 

873 

874 

875class OracledbAdaptDBAPI: 

876 def __init__(self, oracledb) -> None: 

877 self.oracledb = oracledb 

878 

879 for k, v in self.oracledb.__dict__.items(): 

880 if k != "connect": 

881 self.__dict__[k] = v 

882 

883 def connect(self, *arg, **kw): 

884 async_fallback = kw.pop("async_fallback", False) 

885 creator_fn = kw.pop("async_creator_fn", self.oracledb.connect_async) 

886 

887 if asbool(async_fallback): 

888 return AsyncAdaptFallback_oracledb_connection( 

889 self, await_fallback(creator_fn(*arg, **kw)) 

890 ) 

891 

892 else: 

893 return AsyncAdapt_oracledb_connection( 

894 self, await_only(creator_fn(*arg, **kw)) 

895 ) 

896 

897 

898class OracleExecutionContextAsync_oracledb(OracleExecutionContext_oracledb): 

899 # restore default create cursor 

900 create_cursor = default.DefaultExecutionContext.create_cursor 

901 

902 def create_default_cursor(self): 

903 # copy of OracleExecutionContext_cx_oracle.create_cursor 

904 c = self._dbapi_connection.cursor() 

905 if self.dialect.arraysize: 

906 c.arraysize = self.dialect.arraysize 

907 

908 return c 

909 

910 def create_server_side_cursor(self): 

911 c = self._dbapi_connection.ss_cursor() 

912 if self.dialect.arraysize: 

913 c.arraysize = self.dialect.arraysize 

914 

915 return c 

916 

917 

918class OracleDialectAsync_oracledb(OracleDialect_oracledb): 

919 is_async = True 

920 supports_server_side_cursors = True 

921 supports_statement_cache = True 

922 execution_ctx_cls = OracleExecutionContextAsync_oracledb 

923 

924 _min_version = (2,) 

925 

926 # thick_mode mode is not supported by asyncio, oracledb will raise 

927 @classmethod 

928 def import_dbapi(cls): 

929 import oracledb 

930 

931 return OracledbAdaptDBAPI(oracledb) 

932 

933 @classmethod 

934 def get_pool_class(cls, url): 

935 async_fallback = url.query.get("async_fallback", False) 

936 

937 if asbool(async_fallback): 

938 return pool.FallbackAsyncAdaptedQueuePool 

939 else: 

940 return pool.AsyncAdaptedQueuePool 

941 

942 def get_driver_connection(self, connection): 

943 return connection._connection 

944 

945 

946dialect = OracleDialect_oracledb 

947dialect_async = OracleDialectAsync_oracledb