oracledb.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947
  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. r""".. dialect:: oracle+oracledb
  9. :name: python-oracledb
  10. :dbapi: oracledb
  11. :connectstring: oracle+oracledb://user:pass@hostname:port[/dbname][?service_name=<service>[&key=value&key=value...]]
  12. :url: https://oracle.github.io/python-oracledb/
  13. Description
  14. -----------
  15. Python-oracledb is the Oracle Database driver for Python. It features a default
  16. "thin" client mode that requires no dependencies, and an optional "thick" mode
  17. that uses Oracle Client libraries. It supports SQLAlchemy features including
  18. two phase transactions and Asyncio.
  19. Python-oracle is the renamed, updated cx_Oracle driver. Oracle is no longer
  20. doing any releases in the cx_Oracle namespace.
  21. The SQLAlchemy ``oracledb`` dialect provides both a sync and an async
  22. implementation under the same dialect name. The proper version is
  23. selected depending on how the engine is created:
  24. * calling :func:`_sa.create_engine` with ``oracle+oracledb://...`` will
  25. automatically select the sync version::
  26. from sqlalchemy import create_engine
  27. sync_engine = create_engine(
  28. "oracle+oracledb://scott:tiger@localhost?service_name=FREEPDB1"
  29. )
  30. * calling :func:`_asyncio.create_async_engine` with ``oracle+oracledb://...``
  31. will automatically select the async version::
  32. from sqlalchemy.ext.asyncio import create_async_engine
  33. asyncio_engine = create_async_engine(
  34. "oracle+oracledb://scott:tiger@localhost?service_name=FREEPDB1"
  35. )
  36. The asyncio version of the dialect may also be specified explicitly using the
  37. ``oracledb_async`` suffix::
  38. from sqlalchemy.ext.asyncio import create_async_engine
  39. asyncio_engine = create_async_engine(
  40. "oracle+oracledb_async://scott:tiger@localhost?service_name=FREEPDB1"
  41. )
  42. .. versionadded:: 2.0.25 added support for the async version of oracledb.
  43. Thick mode support
  44. ------------------
  45. By default, the python-oracledb driver runs in a "thin" mode that does not
  46. require Oracle Client libraries to be installed. The driver also supports a
  47. "thick" mode that uses Oracle Client libraries to get functionality such as
  48. Oracle Application Continuity.
  49. To enable thick mode, call `oracledb.init_oracle_client()
  50. <https://python-oracledb.readthedocs.io/en/latest/api_manual/module.html#oracledb.init_oracle_client>`_
  51. explicitly, or pass the parameter ``thick_mode=True`` to
  52. :func:`_sa.create_engine`. To pass custom arguments to
  53. ``init_oracle_client()``, like the ``lib_dir`` path, a dict may be passed, for
  54. example::
  55. engine = sa.create_engine(
  56. "oracle+oracledb://...",
  57. thick_mode={
  58. "lib_dir": "/path/to/oracle/client/lib",
  59. "config_dir": "/path/to/network_config_file_directory",
  60. "driver_name": "my-app : 1.0.0",
  61. },
  62. )
  63. Note that passing a ``lib_dir`` path should only be done on macOS or
  64. Windows. On Linux it does not behave as you might expect.
  65. .. seealso::
  66. python-oracledb documentation `Enabling python-oracledb Thick mode
  67. <https://python-oracledb.readthedocs.io/en/latest/user_guide/initialization.html#enabling-python-oracledb-thick-mode>`_
  68. Connecting to Oracle Database
  69. -----------------------------
  70. python-oracledb provides several methods of indicating the target database.
  71. The dialect translates from a series of different URL forms.
  72. Given the hostname, port and service name of the target database, you can
  73. connect in SQLAlchemy using the ``service_name`` query string parameter::
  74. engine = create_engine(
  75. "oracle+oracledb://scott:tiger@hostname:port?service_name=myservice"
  76. )
  77. Connecting with Easy Connect strings
  78. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  79. You can pass any valid python-oracledb connection string as the ``dsn`` key
  80. value in a :paramref:`_sa.create_engine.connect_args` dictionary. See
  81. python-oracledb documentation `Oracle Net Services Connection Strings
  82. <https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html#oracle-net-services-connection-strings>`_.
  83. For example to use an `Easy Connect string
  84. <https://download.oracle.com/ocomdocs/global/Oracle-Net-Easy-Connect-Plus.pdf>`_
  85. with a timeout to prevent connection establishment from hanging if the network
  86. transport to the database cannot be establishd in 30 seconds, and also setting
  87. a keep-alive time of 60 seconds to stop idle network connections from being
  88. terminated by a firewall::
  89. e = create_engine(
  90. "oracle+oracledb://@",
  91. connect_args={
  92. "user": "scott",
  93. "password": "tiger",
  94. "dsn": "hostname:port/myservice?transport_connect_timeout=30&expire_time=60",
  95. },
  96. )
  97. The Easy Connect syntax has been enhanced during the life of Oracle Database.
  98. Review the documentation for your database version. The current documentation
  99. is at `Understanding the Easy Connect Naming Method
  100. <https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=GUID-B0437826-43C1-49EC-A94D-B650B6A4A6EE>`_.
  101. The general syntax is similar to:
  102. .. sourcecode:: text
  103. [[protocol:]//]host[:port][/[service_name]][?parameter_name=value{&parameter_name=value}]
  104. Note that although the SQLAlchemy URL syntax ``hostname:port/dbname`` looks
  105. like Oracle's Easy Connect syntax, it is different. SQLAlchemy's URL requires a
  106. system identifier (SID) for the ``dbname`` component::
  107. engine = create_engine("oracle+oracledb://scott:tiger@hostname:port/sid")
  108. Easy Connect syntax does not support SIDs. It uses services names, which are
  109. the preferred choice for connecting to Oracle Database.
  110. Passing python-oracledb connect arguments
  111. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  112. Other python-oracledb driver `connection options
  113. <https://python-oracledb.readthedocs.io/en/latest/api_manual/module.html#oracledb.connect>`_
  114. can be passed in ``connect_args``. For example::
  115. e = create_engine(
  116. "oracle+oracledb://@",
  117. connect_args={
  118. "user": "scott",
  119. "password": "tiger",
  120. "dsn": "hostname:port/myservice",
  121. "events": True,
  122. "mode": oracledb.AUTH_MODE_SYSDBA,
  123. },
  124. )
  125. Connecting with tnsnames.ora TNS aliases
  126. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  127. If no port, database name, or service name is provided, the dialect will use an
  128. Oracle Database DSN "connection string". This takes the "hostname" portion of
  129. the URL as the data source name. For example, if the ``tnsnames.ora`` file
  130. contains a `TNS Alias
  131. <https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html#tns-aliases-for-connection-strings>`_
  132. of ``myalias`` as below:
  133. .. sourcecode:: text
  134. myalias =
  135. (DESCRIPTION =
  136. (ADDRESS = (PROTOCOL = TCP)(HOST = mymachine.example.com)(PORT = 1521))
  137. (CONNECT_DATA =
  138. (SERVER = DEDICATED)
  139. (SERVICE_NAME = orclpdb1)
  140. )
  141. )
  142. The python-oracledb dialect connects to this database service when ``myalias`` is the
  143. hostname portion of the URL, without specifying a port, database name or
  144. ``service_name``::
  145. engine = create_engine("oracle+oracledb://scott:tiger@myalias")
  146. Connecting to Oracle Autonomous Database
  147. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  148. Users of Oracle Autonomous Database should use either use the TNS Alias URL
  149. shown above, or pass the TNS Alias as the ``dsn`` key value in a
  150. :paramref:`_sa.create_engine.connect_args` dictionary.
  151. If Oracle Autonomous Database is configured for mutual TLS ("mTLS")
  152. connections, then additional configuration is required as shown in `Connecting
  153. to Oracle Cloud Autonomous Databases
  154. <https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html#connecting-to-oracle-cloud-autonomous-databases>`_. In
  155. summary, Thick mode users should configure file locations and set the wallet
  156. path in ``sqlnet.ora`` appropriately::
  157. e = create_engine(
  158. "oracle+oracledb://@",
  159. thick_mode={
  160. # directory containing tnsnames.ora and cwallet.so
  161. "config_dir": "/opt/oracle/wallet_dir",
  162. },
  163. connect_args={
  164. "user": "scott",
  165. "password": "tiger",
  166. "dsn": "mydb_high",
  167. },
  168. )
  169. Thin mode users of mTLS should pass the appropriate directories and PEM wallet
  170. password when creating the engine, similar to::
  171. e = create_engine(
  172. "oracle+oracledb://@",
  173. connect_args={
  174. "user": "scott",
  175. "password": "tiger",
  176. "dsn": "mydb_high",
  177. "config_dir": "/opt/oracle/wallet_dir", # directory containing tnsnames.ora
  178. "wallet_location": "/opt/oracle/wallet_dir", # directory containing ewallet.pem
  179. "wallet_password": "top secret", # password for the PEM file
  180. },
  181. )
  182. Typically ``config_dir`` and ``wallet_location`` are the same directory, which
  183. is where the Oracle Autonomous Database wallet zip file was extracted. Note
  184. this directory should be protected.
  185. Connection Pooling
  186. ------------------
  187. Applications with multiple concurrent users should use connection pooling. A
  188. minimal sized connection pool is also beneficial for long-running, single-user
  189. applications that do not frequently use a connection.
  190. The python-oracledb driver provides its own connection pool implementation that
  191. may be used in place of SQLAlchemy's pooling functionality. The driver pool
  192. gives support for high availability features such as dead connection detection,
  193. connection draining for planned database downtime, support for Oracle
  194. Application Continuity and Transparent Application Continuity, and gives
  195. support for `Database Resident Connection Pooling (DRCP)
  196. <https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html#database-resident-connection-pooling-drcp>`_.
  197. To take advantage of python-oracledb's pool, use the
  198. :paramref:`_sa.create_engine.creator` parameter to provide a function that
  199. returns a new connection, along with setting
  200. :paramref:`_sa.create_engine.pool_class` to ``NullPool`` to disable
  201. SQLAlchemy's pooling::
  202. import oracledb
  203. from sqlalchemy import create_engine
  204. from sqlalchemy import text
  205. from sqlalchemy.pool import NullPool
  206. # Uncomment to use the optional python-oracledb Thick mode.
  207. # Review the python-oracledb doc for the appropriate parameters
  208. # oracledb.init_oracle_client(<your parameters>)
  209. pool = oracledb.create_pool(
  210. user="scott",
  211. password="tiger",
  212. dsn="localhost:1521/freepdb1",
  213. min=1,
  214. max=4,
  215. increment=1,
  216. )
  217. engine = create_engine(
  218. "oracle+oracledb://", creator=pool.acquire, poolclass=NullPool
  219. )
  220. The above engine may then be used normally. Internally, python-oracledb handles
  221. connection pooling::
  222. with engine.connect() as conn:
  223. print(conn.scalar(text("select 1 from dual")))
  224. Refer to the python-oracledb documentation for `oracledb.create_pool()
  225. <https://python-oracledb.readthedocs.io/en/latest/api_manual/module.html#oracledb.create_pool>`_
  226. for the arguments that can be used when creating a connection pool.
  227. .. _drcp:
  228. Using Oracle Database Resident Connection Pooling (DRCP)
  229. --------------------------------------------------------
  230. When using Oracle Database's Database Resident Connection Pooling (DRCP), the
  231. best practice is to specify a connection class and "purity". Refer to the
  232. `python-oracledb documentation on DRCP
  233. <https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html#database-resident-connection-pooling-drcp>`_.
  234. For example::
  235. import oracledb
  236. from sqlalchemy import create_engine
  237. from sqlalchemy import text
  238. from sqlalchemy.pool import NullPool
  239. # Uncomment to use the optional python-oracledb Thick mode.
  240. # Review the python-oracledb doc for the appropriate parameters
  241. # oracledb.init_oracle_client(<your parameters>)
  242. pool = oracledb.create_pool(
  243. user="scott",
  244. password="tiger",
  245. dsn="localhost:1521/freepdb1",
  246. min=1,
  247. max=4,
  248. increment=1,
  249. cclass="MYCLASS",
  250. purity=oracledb.PURITY_SELF,
  251. )
  252. engine = create_engine(
  253. "oracle+oracledb://", creator=pool.acquire, poolclass=NullPool
  254. )
  255. The above engine may then be used normally where python-oracledb handles
  256. application connection pooling and Oracle Database additionally uses DRCP::
  257. with engine.connect() as conn:
  258. print(conn.scalar(text("select 1 from dual")))
  259. If you wish to use different connection classes or purities for different
  260. connections, then wrap ``pool.acquire()``::
  261. import oracledb
  262. from sqlalchemy import create_engine
  263. from sqlalchemy import text
  264. from sqlalchemy.pool import NullPool
  265. # Uncomment to use python-oracledb Thick mode.
  266. # Review the python-oracledb doc for the appropriate parameters
  267. # oracledb.init_oracle_client(<your parameters>)
  268. pool = oracledb.create_pool(
  269. user="scott",
  270. password="tiger",
  271. dsn="localhost:1521/freepdb1",
  272. min=1,
  273. max=4,
  274. increment=1,
  275. cclass="MYCLASS",
  276. purity=oracledb.PURITY_SELF,
  277. )
  278. def creator():
  279. return pool.acquire(cclass="MYOTHERCLASS", purity=oracledb.PURITY_NEW)
  280. engine = create_engine(
  281. "oracle+oracledb://", creator=creator, poolclass=NullPool
  282. )
  283. Engine Options consumed by the SQLAlchemy oracledb dialect outside of the driver
  284. --------------------------------------------------------------------------------
  285. There are also options that are consumed by the SQLAlchemy oracledb dialect
  286. itself. These options are always passed directly to :func:`_sa.create_engine`,
  287. such as::
  288. e = create_engine("oracle+oracledb://user:pass@tnsalias", arraysize=500)
  289. The parameters accepted by the oracledb dialect are as follows:
  290. * ``arraysize`` - set the driver cursor.arraysize value. It defaults to
  291. ``None``, indicating that the driver default value of 100 should be used.
  292. This setting controls how many rows are buffered when fetching rows, and can
  293. have a significant effect on performance if increased for queries that return
  294. large numbers of rows.
  295. .. versionchanged:: 2.0.26 - changed the default value from 50 to None,
  296. to use the default value of the driver itself.
  297. * ``auto_convert_lobs`` - defaults to True; See :ref:`oracledb_lob`.
  298. * ``coerce_to_decimal`` - see :ref:`oracledb_numeric` for detail.
  299. * ``encoding_errors`` - see :ref:`oracledb_unicode_encoding_errors` for detail.
  300. .. _oracledb_unicode:
  301. Unicode
  302. -------
  303. As is the case for all DBAPIs under Python 3, all strings are inherently
  304. Unicode strings.
  305. Ensuring the Correct Client Encoding
  306. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  307. In python-oracledb, the encoding used for all character data is "UTF-8".
  308. Unicode-specific Column datatypes
  309. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  310. The Core expression language handles unicode data by use of the
  311. :class:`.Unicode` and :class:`.UnicodeText` datatypes. These types correspond
  312. to the VARCHAR2 and CLOB Oracle Database datatypes by default. When using
  313. these datatypes with Unicode data, it is expected that the database is
  314. configured with a Unicode-aware character set so that the VARCHAR2 and CLOB
  315. datatypes can accommodate the data.
  316. In the case that Oracle Database is not configured with a Unicode character
  317. set, the two options are to use the :class:`_types.NCHAR` and
  318. :class:`_oracle.NCLOB` datatypes explicitly, or to pass the flag
  319. ``use_nchar_for_unicode=True`` to :func:`_sa.create_engine`, which will cause
  320. the SQLAlchemy dialect to use NCHAR/NCLOB for the :class:`.Unicode` /
  321. :class:`.UnicodeText` datatypes instead of VARCHAR/CLOB.
  322. .. versionchanged:: 1.3 The :class:`.Unicode` and :class:`.UnicodeText`
  323. datatypes now correspond to the ``VARCHAR2`` and ``CLOB`` Oracle Database
  324. datatypes unless the ``use_nchar_for_unicode=True`` is passed to the dialect
  325. when :func:`_sa.create_engine` is called.
  326. .. _oracledb_unicode_encoding_errors:
  327. Encoding Errors
  328. ^^^^^^^^^^^^^^^
  329. For the unusual case that data in Oracle Database is present with a broken
  330. encoding, the dialect accepts a parameter ``encoding_errors`` which will be
  331. passed to Unicode decoding functions in order to affect how decoding errors are
  332. handled. The value is ultimately consumed by the Python `decode
  333. <https://docs.python.org/3/library/stdtypes.html#bytes.decode>`_ function, and
  334. is passed both via python-oracledb's ``encodingErrors`` parameter consumed by
  335. ``Cursor.var()``, as well as SQLAlchemy's own decoding function, as the
  336. python-oracledb dialect makes use of both under different circumstances.
  337. .. versionadded:: 1.3.11
  338. .. _oracledb_setinputsizes:
  339. Fine grained control over python-oracledb data binding with setinputsizes
  340. -------------------------------------------------------------------------
  341. The python-oracle DBAPI has a deep and fundamental reliance upon the usage of
  342. the DBAPI ``setinputsizes()`` call. The purpose of this call is to establish
  343. the datatypes that are bound to a SQL statement for Python values being passed
  344. as parameters. While virtually no other DBAPI assigns any use to the
  345. ``setinputsizes()`` call, the python-oracledb DBAPI relies upon it heavily in
  346. its interactions with the Oracle Database, and in some scenarios it is not
  347. possible for SQLAlchemy to know exactly how data should be bound, as some
  348. settings can cause profoundly different performance characteristics, while
  349. altering the type coercion behavior at the same time.
  350. Users of the oracledb dialect are **strongly encouraged** to read through
  351. python-oracledb's list of built-in datatype symbols at `Database Types
  352. <https://python-oracledb.readthedocs.io/en/latest/api_manual/module.html#database-types>`_
  353. Note that in some cases, significant performance degradation can occur when
  354. using these types vs. not.
  355. On the SQLAlchemy side, the :meth:`.DialectEvents.do_setinputsizes` event can
  356. be used both for runtime visibility (e.g. logging) of the setinputsizes step as
  357. well as to fully control how ``setinputsizes()`` is used on a per-statement
  358. basis.
  359. .. versionadded:: 1.2.9 Added :meth:`.DialectEvents.setinputsizes`
  360. Example 1 - logging all setinputsizes calls
  361. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  362. The following example illustrates how to log the intermediary values from a
  363. SQLAlchemy perspective before they are converted to the raw ``setinputsizes()``
  364. parameter dictionary. The keys of the dictionary are :class:`.BindParameter`
  365. objects which have a ``.key`` and a ``.type`` attribute::
  366. from sqlalchemy import create_engine, event
  367. engine = create_engine(
  368. "oracle+oracledb://scott:tiger@localhost:1521?service_name=freepdb1"
  369. )
  370. @event.listens_for(engine, "do_setinputsizes")
  371. def _log_setinputsizes(inputsizes, cursor, statement, parameters, context):
  372. for bindparam, dbapitype in inputsizes.items():
  373. log.info(
  374. "Bound parameter name: %s SQLAlchemy type: %r DBAPI object: %s",
  375. bindparam.key,
  376. bindparam.type,
  377. dbapitype,
  378. )
  379. Example 2 - remove all bindings to CLOB
  380. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  381. For performance, fetching LOB datatypes from Oracle Database is set by default
  382. for the ``Text`` type within SQLAlchemy. This setting can be modified as
  383. follows::
  384. from sqlalchemy import create_engine, event
  385. from oracledb import CLOB
  386. engine = create_engine(
  387. "oracle+oracledb://scott:tiger@localhost:1521?service_name=freepdb1"
  388. )
  389. @event.listens_for(engine, "do_setinputsizes")
  390. def _remove_clob(inputsizes, cursor, statement, parameters, context):
  391. for bindparam, dbapitype in list(inputsizes.items()):
  392. if dbapitype is CLOB:
  393. del inputsizes[bindparam]
  394. .. _oracledb_lob:
  395. LOB Datatypes
  396. --------------
  397. LOB datatypes refer to the "large object" datatypes such as CLOB, NCLOB and
  398. BLOB. Oracle Database can efficiently return these datatypes as a single
  399. buffer. SQLAlchemy makes use of type handlers to do this by default.
  400. To disable the use of the type handlers and deliver LOB objects as classic
  401. buffered objects with a ``read()`` method, the parameter
  402. ``auto_convert_lobs=False`` may be passed to :func:`_sa.create_engine`.
  403. .. _oracledb_returning:
  404. RETURNING Support
  405. -----------------
  406. The oracledb dialect implements RETURNING using OUT parameters. The dialect
  407. supports RETURNING fully.
  408. Two Phase Transaction Support
  409. -----------------------------
  410. Two phase transactions are fully supported with python-oracledb. (Thin mode
  411. requires python-oracledb 2.3). APIs for two phase transactions are provided at
  412. the Core level via :meth:`_engine.Connection.begin_twophase` and
  413. :paramref:`_orm.Session.twophase` for transparent ORM use.
  414. .. versionchanged:: 2.0.32 added support for two phase transactions
  415. .. _oracledb_numeric:
  416. Precision Numerics
  417. ------------------
  418. SQLAlchemy's numeric types can handle receiving and returning values as Python
  419. ``Decimal`` objects or float objects. When a :class:`.Numeric` object, or a
  420. subclass such as :class:`.Float`, :class:`_oracle.DOUBLE_PRECISION` etc. is in
  421. use, the :paramref:`.Numeric.asdecimal` flag determines if values should be
  422. coerced to ``Decimal`` upon return, or returned as float objects. To make
  423. matters more complicated under Oracle Database, the ``NUMBER`` type can also
  424. represent integer values if the "scale" is zero, so the Oracle
  425. Database-specific :class:`_oracle.NUMBER` type takes this into account as well.
  426. The oracledb dialect makes extensive use of connection- and cursor-level
  427. "outputtypehandler" callables in order to coerce numeric values as requested.
  428. These callables are specific to the specific flavor of :class:`.Numeric` in
  429. use, as well as if no SQLAlchemy typing objects are present. There are
  430. observed scenarios where Oracle Database may send incomplete or ambiguous
  431. information about the numeric types being returned, such as a query where the
  432. numeric types are buried under multiple levels of subquery. The type handlers
  433. do their best to make the right decision in all cases, deferring to the
  434. underlying python-oracledb DBAPI for all those cases where the driver can make
  435. the best decision.
  436. When no typing objects are present, as when executing plain SQL strings, a
  437. default "outputtypehandler" is present which will generally return numeric
  438. values which specify precision and scale as Python ``Decimal`` objects. To
  439. disable this coercion to decimal for performance reasons, pass the flag
  440. ``coerce_to_decimal=False`` to :func:`_sa.create_engine`::
  441. engine = create_engine(
  442. "oracle+oracledb://scott:tiger@tnsalias", coerce_to_decimal=False
  443. )
  444. The ``coerce_to_decimal`` flag only impacts the results of plain string
  445. SQL statements that are not otherwise associated with a :class:`.Numeric`
  446. SQLAlchemy type (or a subclass of such).
  447. .. versionchanged:: 1.2 The numeric handling system for the oracle dialects has
  448. been reworked to take advantage of newer driver features as well as better
  449. integration of outputtypehandlers.
  450. .. versionadded:: 2.0.0 added support for the python-oracledb driver.
  451. """ # noqa
  452. from __future__ import annotations
  453. import collections
  454. import re
  455. from typing import Any
  456. from typing import TYPE_CHECKING
  457. from . import cx_oracle as _cx_oracle
  458. from ... import exc
  459. from ... import pool
  460. from ...connectors.asyncio import AsyncAdapt_dbapi_connection
  461. from ...connectors.asyncio import AsyncAdapt_dbapi_cursor
  462. from ...connectors.asyncio import AsyncAdapt_dbapi_ss_cursor
  463. from ...connectors.asyncio import AsyncAdaptFallback_dbapi_connection
  464. from ...engine import default
  465. from ...util import asbool
  466. from ...util import await_fallback
  467. from ...util import await_only
  468. if TYPE_CHECKING:
  469. from oracledb import AsyncConnection
  470. from oracledb import AsyncCursor
  471. class OracleExecutionContext_oracledb(
  472. _cx_oracle.OracleExecutionContext_cx_oracle
  473. ):
  474. pass
  475. class OracleDialect_oracledb(_cx_oracle.OracleDialect_cx_oracle):
  476. supports_statement_cache = True
  477. execution_ctx_cls = OracleExecutionContext_oracledb
  478. driver = "oracledb"
  479. _min_version = (1,)
  480. def __init__(
  481. self,
  482. auto_convert_lobs=True,
  483. coerce_to_decimal=True,
  484. arraysize=None,
  485. encoding_errors=None,
  486. thick_mode=None,
  487. **kwargs,
  488. ):
  489. super().__init__(
  490. auto_convert_lobs,
  491. coerce_to_decimal,
  492. arraysize,
  493. encoding_errors,
  494. **kwargs,
  495. )
  496. if self.dbapi is not None and (
  497. thick_mode or isinstance(thick_mode, dict)
  498. ):
  499. kw = thick_mode if isinstance(thick_mode, dict) else {}
  500. self.dbapi.init_oracle_client(**kw)
  501. @classmethod
  502. def import_dbapi(cls):
  503. import oracledb
  504. return oracledb
  505. @classmethod
  506. def is_thin_mode(cls, connection):
  507. return connection.connection.dbapi_connection.thin
  508. @classmethod
  509. def get_async_dialect_cls(cls, url):
  510. return OracleDialectAsync_oracledb
  511. def _load_version(self, dbapi_module):
  512. version = (0, 0, 0)
  513. if dbapi_module is not None:
  514. m = re.match(r"(\d+)\.(\d+)(?:\.(\d+))?", dbapi_module.version)
  515. if m:
  516. version = tuple(
  517. int(x) for x in m.group(1, 2, 3) if x is not None
  518. )
  519. self.oracledb_ver = version
  520. if (
  521. self.oracledb_ver > (0, 0, 0)
  522. and self.oracledb_ver < self._min_version
  523. ):
  524. raise exc.InvalidRequestError(
  525. f"oracledb version {self._min_version} and above are supported"
  526. )
  527. def do_begin_twophase(self, connection, xid):
  528. conn_xis = connection.connection.xid(*xid)
  529. connection.connection.tpc_begin(conn_xis)
  530. connection.connection.info["oracledb_xid"] = conn_xis
  531. def do_prepare_twophase(self, connection, xid):
  532. should_commit = connection.connection.tpc_prepare()
  533. connection.info["oracledb_should_commit"] = should_commit
  534. def do_rollback_twophase(
  535. self, connection, xid, is_prepared=True, recover=False
  536. ):
  537. if recover:
  538. conn_xid = connection.connection.xid(*xid)
  539. else:
  540. conn_xid = None
  541. connection.connection.tpc_rollback(conn_xid)
  542. def do_commit_twophase(
  543. self, connection, xid, is_prepared=True, recover=False
  544. ):
  545. conn_xid = None
  546. if not is_prepared:
  547. should_commit = connection.connection.tpc_prepare()
  548. elif recover:
  549. conn_xid = connection.connection.xid(*xid)
  550. should_commit = True
  551. else:
  552. should_commit = connection.info["oracledb_should_commit"]
  553. if should_commit:
  554. connection.connection.tpc_commit(conn_xid)
  555. def do_recover_twophase(self, connection):
  556. return [
  557. # oracledb seems to return bytes
  558. (
  559. fi,
  560. gti.decode() if isinstance(gti, bytes) else gti,
  561. bq.decode() if isinstance(bq, bytes) else bq,
  562. )
  563. for fi, gti, bq in connection.connection.tpc_recover()
  564. ]
  565. def _check_max_identifier_length(self, connection):
  566. if self.oracledb_ver >= (2, 5):
  567. max_len = connection.connection.max_identifier_length
  568. if max_len is not None:
  569. return max_len
  570. return super()._check_max_identifier_length(connection)
  571. class AsyncAdapt_oracledb_cursor(AsyncAdapt_dbapi_cursor):
  572. _cursor: AsyncCursor
  573. __slots__ = ()
  574. @property
  575. def outputtypehandler(self):
  576. return self._cursor.outputtypehandler
  577. @outputtypehandler.setter
  578. def outputtypehandler(self, value):
  579. self._cursor.outputtypehandler = value
  580. def var(self, *args, **kwargs):
  581. return self._cursor.var(*args, **kwargs)
  582. def close(self):
  583. self._rows.clear()
  584. self._cursor.close()
  585. def setinputsizes(self, *args: Any, **kwargs: Any) -> Any:
  586. return self._cursor.setinputsizes(*args, **kwargs)
  587. def _aenter_cursor(self, cursor: AsyncCursor) -> AsyncCursor:
  588. try:
  589. return cursor.__enter__()
  590. except Exception as error:
  591. self._adapt_connection._handle_exception(error)
  592. async def _execute_async(self, operation, parameters):
  593. # override to not use mutex, oracledb already has a mutex
  594. if parameters is None:
  595. result = await self._cursor.execute(operation)
  596. else:
  597. result = await self._cursor.execute(operation, parameters)
  598. if self._cursor.description and not self.server_side:
  599. self._rows = collections.deque(await self._cursor.fetchall())
  600. return result
  601. async def _executemany_async(
  602. self,
  603. operation,
  604. seq_of_parameters,
  605. ):
  606. # override to not use mutex, oracledb already has a mutex
  607. return await self._cursor.executemany(operation, seq_of_parameters)
  608. def __enter__(self):
  609. return self
  610. def __exit__(self, type_: Any, value: Any, traceback: Any) -> None:
  611. self.close()
  612. class AsyncAdapt_oracledb_ss_cursor(
  613. AsyncAdapt_dbapi_ss_cursor, AsyncAdapt_oracledb_cursor
  614. ):
  615. __slots__ = ()
  616. def close(self) -> None:
  617. if self._cursor is not None:
  618. self._cursor.close()
  619. self._cursor = None # type: ignore
  620. class AsyncAdapt_oracledb_connection(AsyncAdapt_dbapi_connection):
  621. _connection: AsyncConnection
  622. __slots__ = ()
  623. thin = True
  624. _cursor_cls = AsyncAdapt_oracledb_cursor
  625. _ss_cursor_cls = None
  626. @property
  627. def autocommit(self):
  628. return self._connection.autocommit
  629. @autocommit.setter
  630. def autocommit(self, value):
  631. self._connection.autocommit = value
  632. @property
  633. def outputtypehandler(self):
  634. return self._connection.outputtypehandler
  635. @outputtypehandler.setter
  636. def outputtypehandler(self, value):
  637. self._connection.outputtypehandler = value
  638. @property
  639. def version(self):
  640. return self._connection.version
  641. @property
  642. def stmtcachesize(self):
  643. return self._connection.stmtcachesize
  644. @stmtcachesize.setter
  645. def stmtcachesize(self, value):
  646. self._connection.stmtcachesize = value
  647. @property
  648. def max_identifier_length(self):
  649. return self._connection.max_identifier_length
  650. def cursor(self):
  651. return AsyncAdapt_oracledb_cursor(self)
  652. def ss_cursor(self):
  653. return AsyncAdapt_oracledb_ss_cursor(self)
  654. def xid(self, *args: Any, **kwargs: Any) -> Any:
  655. return self._connection.xid(*args, **kwargs)
  656. def tpc_begin(self, *args: Any, **kwargs: Any) -> Any:
  657. return self.await_(self._connection.tpc_begin(*args, **kwargs))
  658. def tpc_commit(self, *args: Any, **kwargs: Any) -> Any:
  659. return self.await_(self._connection.tpc_commit(*args, **kwargs))
  660. def tpc_prepare(self, *args: Any, **kwargs: Any) -> Any:
  661. return self.await_(self._connection.tpc_prepare(*args, **kwargs))
  662. def tpc_recover(self, *args: Any, **kwargs: Any) -> Any:
  663. return self.await_(self._connection.tpc_recover(*args, **kwargs))
  664. def tpc_rollback(self, *args: Any, **kwargs: Any) -> Any:
  665. return self.await_(self._connection.tpc_rollback(*args, **kwargs))
  666. class AsyncAdaptFallback_oracledb_connection(
  667. AsyncAdaptFallback_dbapi_connection, AsyncAdapt_oracledb_connection
  668. ):
  669. __slots__ = ()
  670. class OracledbAdaptDBAPI:
  671. def __init__(self, oracledb) -> None:
  672. self.oracledb = oracledb
  673. for k, v in self.oracledb.__dict__.items():
  674. if k != "connect":
  675. self.__dict__[k] = v
  676. def connect(self, *arg, **kw):
  677. async_fallback = kw.pop("async_fallback", False)
  678. creator_fn = kw.pop("async_creator_fn", self.oracledb.connect_async)
  679. if asbool(async_fallback):
  680. return AsyncAdaptFallback_oracledb_connection(
  681. self, await_fallback(creator_fn(*arg, **kw))
  682. )
  683. else:
  684. return AsyncAdapt_oracledb_connection(
  685. self, await_only(creator_fn(*arg, **kw))
  686. )
  687. class OracleExecutionContextAsync_oracledb(OracleExecutionContext_oracledb):
  688. # restore default create cursor
  689. create_cursor = default.DefaultExecutionContext.create_cursor
  690. def create_default_cursor(self):
  691. # copy of OracleExecutionContext_cx_oracle.create_cursor
  692. c = self._dbapi_connection.cursor()
  693. if self.dialect.arraysize:
  694. c.arraysize = self.dialect.arraysize
  695. return c
  696. def create_server_side_cursor(self):
  697. c = self._dbapi_connection.ss_cursor()
  698. if self.dialect.arraysize:
  699. c.arraysize = self.dialect.arraysize
  700. return c
  701. class OracleDialectAsync_oracledb(OracleDialect_oracledb):
  702. is_async = True
  703. supports_server_side_cursors = True
  704. supports_statement_cache = True
  705. execution_ctx_cls = OracleExecutionContextAsync_oracledb
  706. _min_version = (2,)
  707. # thick_mode mode is not supported by asyncio, oracledb will raise
  708. @classmethod
  709. def import_dbapi(cls):
  710. import oracledb
  711. return OracledbAdaptDBAPI(oracledb)
  712. @classmethod
  713. def get_pool_class(cls, url):
  714. async_fallback = url.query.get("async_fallback", False)
  715. if asbool(async_fallback):
  716. return pool.FallbackAsyncAdaptedQueuePool
  717. else:
  718. return pool.AsyncAdaptedQueuePool
  719. def get_driver_connection(self, connection):
  720. return connection._connection
  721. dialect = OracleDialect_oracledb
  722. dialect_async = OracleDialectAsync_oracledb