|
- # Copyright (C) 2005-2021 the SQLAlchemy authors and contributors
- # <see AUTHORS file>
- #
- # This module is part of SQLAlchemy and is released under
- # the MIT License: http://www.opensource.org/licenses/mit-license.php
-
- r"""
- .. dialect:: oracle+cx_oracle
- :name: cx-Oracle
- :dbapi: cx_oracle
- :connectstring: oracle+cx_oracle://user:pass@host:port/dbname[?key=value&key=value...]
- :url: https://oracle.github.io/python-cx_Oracle/
-
- DSN vs. Hostname connections
- -----------------------------
-
- The dialect will connect to a DSN if no database name portion is presented,
- such as::
-
- engine = create_engine("oracle+cx_oracle://scott:tiger@oracle1120/?encoding=UTF-8&nencoding=UTF-8")
-
- Above, ``oracle1120`` is passed to cx_Oracle as an Oracle datasource name.
-
- Alternatively, if a database name is present, the ``cx_Oracle.makedsn()``
- function is used to create an ad-hoc "datasource" name assuming host
- and port::
-
- engine = create_engine("oracle+cx_oracle://scott:tiger@hostname:1521/dbname?encoding=UTF-8&nencoding=UTF-8")
-
- Above, the DSN would be created as follows::
-
- >>> import cx_Oracle
- >>> cx_Oracle.makedsn("hostname", 1521, sid="dbname")
- '(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=hostname)(PORT=1521))(CONNECT_DATA=(SID=dbname)))'
-
- The ``service_name`` parameter, also consumed by ``cx_Oracle.makedsn()``, may
- be specified in the URL query string, e.g. ``?service_name=my_service``.
-
-
- Passing cx_Oracle connect arguments
- -----------------------------------
-
- Additional connection arguments can usually be passed via the URL
- query string; particular symbols like ``cx_Oracle.SYSDBA`` are intercepted
- and converted to the correct symbol::
-
- e = create_engine(
- "oracle+cx_oracle://user:pass@dsn?encoding=UTF-8&nencoding=UTF-8&mode=SYSDBA&events=true")
-
- .. versionchanged:: 1.3 the cx_oracle dialect now accepts all argument names
- within the URL string itself, to be passed to the cx_Oracle DBAPI. As
- was the case earlier but not correctly documented, the
- :paramref:`_sa.create_engine.connect_args` parameter also accepts all
- cx_Oracle DBAPI connect arguments.
-
- To pass arguments directly to ``.connect()`` without using the query
- string, use the :paramref:`_sa.create_engine.connect_args` dictionary.
- Any cx_Oracle parameter value and/or constant may be passed, such as::
-
- import cx_Oracle
- e = create_engine(
- "oracle+cx_oracle://user:pass@dsn",
- connect_args={
- "encoding": "UTF-8",
- "nencoding": "UTF-8",
- "mode": cx_Oracle.SYSDBA,
- "events": True
- }
- )
-
- Options consumed by the SQLAlchemy cx_Oracle dialect outside of the driver
- --------------------------------------------------------------------------
-
- There are also options that are consumed by the SQLAlchemy cx_oracle dialect
- itself. These options are always passed directly to :func:`_sa.create_engine`
- , such as::
-
- e = create_engine(
- "oracle+cx_oracle://user:pass@dsn", coerce_to_unicode=False)
-
- The parameters accepted by the cx_oracle dialect are as follows:
-
- * ``arraysize`` - set the cx_oracle.arraysize value on cursors, defaulted
- to 50. This setting is significant with cx_Oracle as the contents of LOB
- objects are only readable within a "live" row (e.g. within a batch of
- 50 rows).
-
- * ``auto_convert_lobs`` - defaults to True; See :ref:`cx_oracle_lob`.
-
- * ``coerce_to_unicode`` - see :ref:`cx_oracle_unicode` for detail.
-
- * ``coerce_to_decimal`` - see :ref:`cx_oracle_numeric` for detail.
-
- * ``encoding_errors`` - see :ref:`cx_oracle_unicode_encoding_errors` for detail.
-
- .. _cx_oracle_sessionpool:
-
- Using cx_Oracle SessionPool
- ---------------------------
-
- The cx_Oracle library provides its own connectivity services that may be
- used in place of SQLAlchemy's pooling functionality. This can be achieved
- by using the :paramref:`_sa.create_engine.creator` parameter to provide a
- function that returns a new connection, along with setting
- :paramref:`_sa.create_engine.pool_class` to ``NullPool`` to disable
- SQLAlchemy's pooling::
-
- import cx_Oracle
- from sqlalchemy import create_engine
- from sqlalchemy.pool import NullPool
-
- pool = cx_Oracle.SessionPool(
- user="scott", password="tiger", dsn="oracle1120",
- min=2, max=5, increment=1, threaded=True
- )
-
- engine = create_engine("oracle://", creator=pool.acquire, poolclass=NullPool)
-
- The above engine may then be used normally where cx_Oracle's pool handles
- connection pooling::
-
- with engine.connect() as conn:
- print(conn.scalar("select 1 FROM dual"))
-
-
- .. _cx_oracle_unicode:
-
- Unicode
- -------
-
- As is the case for all DBAPIs under Python 3, all strings are inherently
- Unicode strings. Under Python 2, cx_Oracle also supports Python Unicode
- objects directly. In all cases however, the driver requires an explicit
- encoding configuration.
-
- Ensuring the Correct Client Encoding
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
- The long accepted standard for establishing client encoding for nearly all
- Oracle related software is via the `NLS_LANG <https://www.oracle.com/database/technologies/faq-nls-lang.html>`_
- environment variable. cx_Oracle like most other Oracle drivers will use
- this environment variable as the source of its encoding configuration. The
- format of this variable is idiosyncratic; a typical value would be
- ``AMERICAN_AMERICA.AL32UTF8``.
-
- The cx_Oracle driver also supports a programmatic alternative which is to
- pass the ``encoding`` and ``nencoding`` parameters directly to its
- ``.connect()`` function. These can be present in the URL as follows::
-
- engine = create_engine("oracle+cx_oracle://scott:tiger@oracle1120/?encoding=UTF-8&nencoding=UTF-8")
-
- For the meaning of the ``encoding`` and ``nencoding`` parameters, please
- consult
- `Characters Sets and National Language Support (NLS) <https://cx-oracle.readthedocs.io/en/latest/user_guide/globalization.html#globalization>`_.
-
- .. seealso::
-
- `Characters Sets and National Language Support (NLS) <https://cx-oracle.readthedocs.io/en/latest/user_guide/globalization.html#globalization>`_
- - in the cx_Oracle documentation.
-
-
- Unicode-specific Column datatypes
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
- The Core expression language handles unicode data by use of the :class:`.Unicode`
- and :class:`.UnicodeText`
- datatypes. These types correspond to the VARCHAR2 and CLOB Oracle datatypes by
- default. When using these datatypes with Unicode data, it is expected that
- the Oracle database is configured with a Unicode-aware character set, as well
- as that the ``NLS_LANG`` environment variable is set appropriately, so that
- the VARCHAR2 and CLOB datatypes can accommodate the data.
-
- In the case that the Oracle database is not configured with a Unicode character
- set, the two options are to use the :class:`_types.NCHAR` and
- :class:`_oracle.NCLOB` datatypes explicitly, or to pass the flag
- ``use_nchar_for_unicode=True`` to :func:`_sa.create_engine`,
- which will cause the
- SQLAlchemy dialect to use NCHAR/NCLOB for the :class:`.Unicode` /
- :class:`.UnicodeText` datatypes instead of VARCHAR/CLOB.
-
- .. versionchanged:: 1.3 The :class:`.Unicode` and :class:`.UnicodeText`
- datatypes now correspond to the ``VARCHAR2`` and ``CLOB`` Oracle datatypes
- unless the ``use_nchar_for_unicode=True`` is passed to the dialect
- when :func:`_sa.create_engine` is called.
-
- Unicode Coercion of result rows under Python 2
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
- When result sets are fetched that include strings, under Python 3 the cx_Oracle
- DBAPI returns all strings as Python Unicode objects, since Python 3 only has a
- Unicode string type. This occurs for data fetched from datatypes such as
- VARCHAR2, CHAR, CLOB, NCHAR, NCLOB, etc. In order to provide cross-
- compatibility under Python 2, the SQLAlchemy cx_Oracle dialect will add
- Unicode-conversion to string data under Python 2 as well. Historically, this
- made use of converters that were supplied by cx_Oracle but were found to be
- non-performant; SQLAlchemy's own converters are used for the string to Unicode
- conversion under Python 2. To disable the Python 2 Unicode conversion for
- VARCHAR2, CHAR, and CLOB, the flag ``coerce_to_unicode=False`` can be passed to
- :func:`_sa.create_engine`.
-
- .. versionchanged:: 1.3 Unicode conversion is applied to all string values
- by default under python 2. The ``coerce_to_unicode`` now defaults to True
- and can be set to False to disable the Unicode coercion of strings that are
- delivered as VARCHAR2/CHAR/CLOB data.
-
- .. _cx_oracle_unicode_encoding_errors:
-
- Encoding Errors
- ^^^^^^^^^^^^^^^
-
- For the unusual case that data in the Oracle database is present with a broken
- encoding, the dialect accepts a parameter ``encoding_errors`` which will be
- passed to Unicode decoding functions in order to affect how decoding errors are
- handled. The value is ultimately consumed by the Python `decode
- <https://docs.python.org/3/library/stdtypes.html#bytes.decode>`_ function, and
- is passed both via cx_Oracle's ``encodingErrors`` parameter consumed by
- ``Cursor.var()``, as well as SQLAlchemy's own decoding function, as the
- cx_Oracle dialect makes use of both under different circumstances.
-
- .. versionadded:: 1.3.11
-
-
- .. _cx_oracle_setinputsizes:
-
- Fine grained control over cx_Oracle data binding performance with setinputsizes
- -------------------------------------------------------------------------------
-
- The cx_Oracle DBAPI has a deep and fundamental reliance upon the usage of the
- DBAPI ``setinputsizes()`` call. The purpose of this call is to establish the
- datatypes that are bound to a SQL statement for Python values being passed as
- parameters. While virtually no other DBAPI assigns any use to the
- ``setinputsizes()`` call, the cx_Oracle DBAPI relies upon it heavily in its
- interactions with the Oracle client interface, and in some scenarios it is not
- possible for SQLAlchemy to know exactly how data should be bound, as some
- settings can cause profoundly different performance characteristics, while
- altering the type coercion behavior at the same time.
-
- Users of the cx_Oracle dialect are **strongly encouraged** to read through
- cx_Oracle's list of built-in datatype symbols at
- http://cx-oracle.readthedocs.io/en/latest/module.html#database-types.
- Note that in some cases, significant performance degradation can occur when
- using these types vs. not, in particular when specifying ``cx_Oracle.CLOB``.
-
- On the SQLAlchemy side, the :meth:`.DialectEvents.do_setinputsizes` event can
- be used both for runtime visibility (e.g. logging) of the setinputsizes step as
- well as to fully control how ``setinputsizes()`` is used on a per-statement
- basis.
-
- .. versionadded:: 1.2.9 Added :meth:`.DialectEvents.setinputsizes`
-
-
- Example 1 - logging all setinputsizes calls
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
- The following example illustrates how to log the intermediary values from a
- SQLAlchemy perspective before they are converted to the raw ``setinputsizes()``
- parameter dictionary. The keys of the dictionary are :class:`.BindParameter`
- objects which have a ``.key`` and a ``.type`` attribute::
-
- from sqlalchemy import create_engine, event
-
- engine = create_engine("oracle+cx_oracle://scott:tiger@host/xe")
-
- @event.listens_for(engine, "do_setinputsizes")
- def _log_setinputsizes(inputsizes, cursor, statement, parameters, context):
- for bindparam, dbapitype in inputsizes.items():
- log.info(
- "Bound parameter name: %s SQLAlchemy type: %r "
- "DBAPI object: %s",
- bindparam.key, bindparam.type, dbapitype)
-
- Example 2 - remove all bindings to CLOB
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
- The ``CLOB`` datatype in cx_Oracle incurs a significant performance overhead,
- however is set by default for the ``Text`` type within the SQLAlchemy 1.2
- series. This setting can be modified as follows::
-
- from sqlalchemy import create_engine, event
- from cx_Oracle import CLOB
-
- engine = create_engine("oracle+cx_oracle://scott:tiger@host/xe")
-
- @event.listens_for(engine, "do_setinputsizes")
- def _remove_clob(inputsizes, cursor, statement, parameters, context):
- for bindparam, dbapitype in list(inputsizes.items()):
- if dbapitype is CLOB:
- del inputsizes[bindparam]
-
- .. _cx_oracle_returning:
-
- RETURNING Support
- -----------------
-
- The cx_Oracle dialect implements RETURNING using OUT parameters.
- The dialect supports RETURNING fully, however cx_Oracle 6 is recommended
- for complete support.
-
- .. _cx_oracle_lob:
-
- LOB Objects
- -----------
-
- cx_oracle returns oracle LOBs using the cx_oracle.LOB object. SQLAlchemy
- converts these to strings so that the interface of the Binary type is
- consistent with that of other backends, which takes place within a cx_Oracle
- outputtypehandler.
-
- cx_Oracle prior to version 6 would require that LOB objects be read before
- a new batch of rows would be read, as determined by the ``cursor.arraysize``.
- As of the 6 series, this limitation has been lifted. Nevertheless, because
- SQLAlchemy pre-reads these LOBs up front, this issue is avoided in any case.
-
- To disable the auto "read()" feature of the dialect, the flag
- ``auto_convert_lobs=False`` may be passed to :func:`_sa.create_engine`. Under
- the cx_Oracle 5 series, having this flag turned off means there is the chance
- of reading from a stale LOB object if not read as it is fetched. With
- cx_Oracle 6, this issue is resolved.
-
- .. versionchanged:: 1.2 the LOB handling system has been greatly simplified
- internally to make use of outputtypehandlers, and no longer makes use
- of alternate "buffered" result set objects.
-
- Two Phase Transactions Not Supported
- -------------------------------------
-
- Two phase transactions are **not supported** under cx_Oracle due to poor
- driver support. As of cx_Oracle 6.0b1, the interface for
- two phase transactions has been changed to be more of a direct pass-through
- to the underlying OCI layer with less automation. The additional logic
- to support this system is not implemented in SQLAlchemy.
-
- .. _cx_oracle_numeric:
-
- Precision Numerics
- ------------------
-
- SQLAlchemy's numeric types can handle receiving and returning values as Python
- ``Decimal`` objects or float objects. When a :class:`.Numeric` object, or a
- subclass such as :class:`.Float`, :class:`_oracle.DOUBLE_PRECISION` etc. is in
- use, the :paramref:`.Numeric.asdecimal` flag determines if values should be
- coerced to ``Decimal`` upon return, or returned as float objects. To make
- matters more complicated under Oracle, Oracle's ``NUMBER`` type can also
- represent integer values if the "scale" is zero, so the Oracle-specific
- :class:`_oracle.NUMBER` type takes this into account as well.
-
- The cx_Oracle dialect makes extensive use of connection- and cursor-level
- "outputtypehandler" callables in order to coerce numeric values as requested.
- These callables are specific to the specific flavor of :class:`.Numeric` in
- use, as well as if no SQLAlchemy typing objects are present. There are
- observed scenarios where Oracle may sends incomplete or ambiguous information
- about the numeric types being returned, such as a query where the numeric types
- are buried under multiple levels of subquery. The type handlers do their best
- to make the right decision in all cases, deferring to the underlying cx_Oracle
- DBAPI for all those cases where the driver can make the best decision.
-
- When no typing objects are present, as when executing plain SQL strings, a
- default "outputtypehandler" is present which will generally return numeric
- values which specify precision and scale as Python ``Decimal`` objects. To
- disable this coercion to decimal for performance reasons, pass the flag
- ``coerce_to_decimal=False`` to :func:`_sa.create_engine`::
-
- engine = create_engine("oracle+cx_oracle://dsn", coerce_to_decimal=False)
-
- The ``coerce_to_decimal`` flag only impacts the results of plain string
- SQL statements that are not otherwise associated with a :class:`.Numeric`
- SQLAlchemy type (or a subclass of such).
-
- .. versionchanged:: 1.2 The numeric handling system for cx_Oracle has been
- reworked to take advantage of newer cx_Oracle features as well
- as better integration of outputtypehandlers.
-
- """ # noqa
-
- from __future__ import absolute_import
-
- import decimal
- import random
- import re
-
- from . import base as oracle
- from .base import OracleCompiler
- from .base import OracleDialect
- from .base import OracleExecutionContext
- from ... import exc
- from ... import processors
- from ... import types as sqltypes
- from ... import util
- from ...engine import cursor as _cursor
- from ...util import compat
-
-
- class _OracleInteger(sqltypes.Integer):
- def get_dbapi_type(self, dbapi):
- # see https://github.com/oracle/python-cx_Oracle/issues/
- # 208#issuecomment-409715955
- return int
-
- def _cx_oracle_var(self, dialect, cursor):
- cx_Oracle = dialect.dbapi
- return cursor.var(
- cx_Oracle.STRING, 255, arraysize=cursor.arraysize, outconverter=int
- )
-
- def _cx_oracle_outputtypehandler(self, dialect):
- def handler(cursor, name, default_type, size, precision, scale):
- return self._cx_oracle_var(dialect, cursor)
-
- return handler
-
-
- class _OracleNumeric(sqltypes.Numeric):
- is_number = False
-
- def bind_processor(self, dialect):
- if self.scale == 0:
- return None
- elif self.asdecimal:
- processor = processors.to_decimal_processor_factory(
- decimal.Decimal, self._effective_decimal_return_scale
- )
-
- def process(value):
- if isinstance(value, (int, float)):
- return processor(value)
- elif value is not None and value.is_infinite():
- return float(value)
- else:
- return value
-
- return process
- else:
- return processors.to_float
-
- def result_processor(self, dialect, coltype):
- return None
-
- def _cx_oracle_outputtypehandler(self, dialect):
- cx_Oracle = dialect.dbapi
-
- is_cx_oracle_6 = dialect._is_cx_oracle_6
-
- def handler(cursor, name, default_type, size, precision, scale):
- outconverter = None
-
- if precision:
- if self.asdecimal:
- if default_type == cx_Oracle.NATIVE_FLOAT:
- # receiving float and doing Decimal after the fact
- # allows for float("inf") to be handled
- type_ = default_type
- outconverter = decimal.Decimal
- elif is_cx_oracle_6:
- type_ = decimal.Decimal
- else:
- type_ = cx_Oracle.STRING
- outconverter = dialect._to_decimal
- else:
- if self.is_number and scale == 0:
- # integer. cx_Oracle is observed to handle the widest
- # variety of ints when no directives are passed,
- # from 5.2 to 7.0. See [ticket:4457]
- return None
- else:
- type_ = cx_Oracle.NATIVE_FLOAT
-
- else:
- if self.asdecimal:
- if default_type == cx_Oracle.NATIVE_FLOAT:
- type_ = default_type
- outconverter = decimal.Decimal
- elif is_cx_oracle_6:
- type_ = decimal.Decimal
- else:
- type_ = cx_Oracle.STRING
- outconverter = dialect._to_decimal
- else:
- if self.is_number and scale == 0:
- # integer. cx_Oracle is observed to handle the widest
- # variety of ints when no directives are passed,
- # from 5.2 to 7.0. See [ticket:4457]
- return None
- else:
- type_ = cx_Oracle.NATIVE_FLOAT
-
- return cursor.var(
- type_,
- 255,
- arraysize=cursor.arraysize,
- outconverter=outconverter,
- )
-
- return handler
-
-
- class _OracleBinaryFloat(_OracleNumeric):
- def get_dbapi_type(self, dbapi):
- return dbapi.NATIVE_FLOAT
-
-
- class _OracleBINARY_FLOAT(_OracleBinaryFloat, oracle.BINARY_FLOAT):
- pass
-
-
- class _OracleBINARY_DOUBLE(_OracleBinaryFloat, oracle.BINARY_DOUBLE):
- pass
-
-
- class _OracleNUMBER(_OracleNumeric):
- is_number = True
-
-
- class _OracleDate(sqltypes.Date):
- def bind_processor(self, dialect):
- return None
-
- def result_processor(self, dialect, coltype):
- def process(value):
- if value is not None:
- return value.date()
- else:
- return value
-
- return process
-
-
- # TODO: the names used across CHAR / VARCHAR / NCHAR / NVARCHAR
- # here are inconsistent and not very good
- class _OracleChar(sqltypes.CHAR):
- def get_dbapi_type(self, dbapi):
- return dbapi.FIXED_CHAR
-
-
- class _OracleNChar(sqltypes.NCHAR):
- def get_dbapi_type(self, dbapi):
- return dbapi.FIXED_NCHAR
-
-
- class _OracleUnicodeStringNCHAR(oracle.NVARCHAR2):
- def get_dbapi_type(self, dbapi):
- return dbapi.NCHAR
-
-
- class _OracleUnicodeStringCHAR(sqltypes.Unicode):
- def get_dbapi_type(self, dbapi):
- return dbapi.LONG_STRING
-
-
- class _OracleUnicodeTextNCLOB(oracle.NCLOB):
- def get_dbapi_type(self, dbapi):
- return dbapi.NCLOB
-
-
- class _OracleUnicodeTextCLOB(sqltypes.UnicodeText):
- def get_dbapi_type(self, dbapi):
- return dbapi.CLOB
-
-
- class _OracleText(sqltypes.Text):
- def get_dbapi_type(self, dbapi):
- return dbapi.CLOB
-
-
- class _OracleLong(oracle.LONG):
- def get_dbapi_type(self, dbapi):
- return dbapi.LONG_STRING
-
-
- class _OracleString(sqltypes.String):
- pass
-
-
- class _OracleEnum(sqltypes.Enum):
- def bind_processor(self, dialect):
- enum_proc = sqltypes.Enum.bind_processor(self, dialect)
-
- def process(value):
- raw_str = enum_proc(value)
- return raw_str
-
- return process
-
-
- class _OracleBinary(sqltypes.LargeBinary):
- def get_dbapi_type(self, dbapi):
- return dbapi.BLOB
-
- def bind_processor(self, dialect):
- return None
-
- def result_processor(self, dialect, coltype):
- if not dialect.auto_convert_lobs:
- return None
- else:
- return super(_OracleBinary, self).result_processor(
- dialect, coltype
- )
-
-
- class _OracleInterval(oracle.INTERVAL):
- def get_dbapi_type(self, dbapi):
- return dbapi.INTERVAL
-
-
- class _OracleRaw(oracle.RAW):
- pass
-
-
- class _OracleRowid(oracle.ROWID):
- def get_dbapi_type(self, dbapi):
- return dbapi.ROWID
-
-
- class OracleCompiler_cx_oracle(OracleCompiler):
- _oracle_cx_sql_compiler = True
-
- def bindparam_string(self, name, **kw):
- quote = getattr(name, "quote", None)
- if (
- quote is True
- or quote is not False
- and self.preparer._bindparam_requires_quotes(name)
- and not kw.get("post_compile", False)
- ):
- # interesting to note about expanding parameters - since the
- # new parameters take the form <paramname>_<int>, at least if
- # they are originally formed from reserved words, they no longer
- # need quoting :). names that include illegal characters
- # won't work however.
- quoted_name = '"%s"' % name
- kw["escaped_from"] = name
- name = quoted_name
-
- return OracleCompiler.bindparam_string(self, name, **kw)
-
-
- class OracleExecutionContext_cx_oracle(OracleExecutionContext):
- out_parameters = None
-
- def _generate_out_parameter_vars(self):
- # check for has_out_parameters or RETURNING, create cx_Oracle.var
- # objects if so
- if self.compiled.returning or self.compiled.has_out_parameters:
- quoted_bind_names = self.compiled.escaped_bind_names
- for bindparam in self.compiled.binds.values():
- if bindparam.isoutparam:
- name = self.compiled.bind_names[bindparam]
- type_impl = bindparam.type.dialect_impl(self.dialect)
-
- if hasattr(type_impl, "_cx_oracle_var"):
- self.out_parameters[name] = type_impl._cx_oracle_var(
- self.dialect, self.cursor
- )
- else:
- dbtype = type_impl.get_dbapi_type(self.dialect.dbapi)
-
- cx_Oracle = self.dialect.dbapi
-
- if dbtype is None:
- raise exc.InvalidRequestError(
- "Cannot create out parameter for "
- "parameter "
- "%r - its type %r is not supported by"
- " cx_oracle" % (bindparam.key, bindparam.type)
- )
-
- if compat.py2k and dbtype in (
- cx_Oracle.CLOB,
- cx_Oracle.NCLOB,
- ):
- outconverter = (
- processors.to_unicode_processor_factory(
- self.dialect.encoding,
- errors=self.dialect.encoding_errors,
- )
- )
- self.out_parameters[name] = self.cursor.var(
- dbtype,
- outconverter=lambda value: outconverter(
- value.read()
- ),
- )
-
- elif dbtype in (
- cx_Oracle.BLOB,
- cx_Oracle.CLOB,
- cx_Oracle.NCLOB,
- ):
- self.out_parameters[name] = self.cursor.var(
- dbtype, outconverter=lambda value: value.read()
- )
- elif compat.py2k and isinstance(
- type_impl, sqltypes.Unicode
- ):
- outconverter = (
- processors.to_unicode_processor_factory(
- self.dialect.encoding,
- errors=self.dialect.encoding_errors,
- )
- )
- self.out_parameters[name] = self.cursor.var(
- dbtype, outconverter=outconverter
- )
- else:
- self.out_parameters[name] = self.cursor.var(dbtype)
- self.parameters[0][
- quoted_bind_names.get(name, name)
- ] = self.out_parameters[name]
-
- def _generate_cursor_outputtype_handler(self):
- output_handlers = {}
-
- for (keyname, name, objects, type_) in self.compiled._result_columns:
- handler = type_._cached_custom_processor(
- self.dialect,
- "cx_oracle_outputtypehandler",
- self._get_cx_oracle_type_handler,
- )
-
- if handler:
- denormalized_name = self.dialect.denormalize_name(keyname)
- output_handlers[denormalized_name] = handler
-
- if output_handlers:
- default_handler = self._dbapi_connection.outputtypehandler
-
- def output_type_handler(
- cursor, name, default_type, size, precision, scale
- ):
- if name in output_handlers:
- return output_handlers[name](
- cursor, name, default_type, size, precision, scale
- )
- else:
- return default_handler(
- cursor, name, default_type, size, precision, scale
- )
-
- self.cursor.outputtypehandler = output_type_handler
-
- def _get_cx_oracle_type_handler(self, impl):
- if hasattr(impl, "_cx_oracle_outputtypehandler"):
- return impl._cx_oracle_outputtypehandler(self.dialect)
- else:
- return None
-
- def pre_exec(self):
- if not getattr(self.compiled, "_oracle_cx_sql_compiler", False):
- return
-
- self.out_parameters = {}
-
- self._generate_out_parameter_vars()
-
- self._generate_cursor_outputtype_handler()
-
- self.include_set_input_sizes = self.dialect._include_setinputsizes
-
- def post_exec(self):
- if self.compiled and self.out_parameters and self.compiled.returning:
- # create a fake cursor result from the out parameters. unlike
- # get_out_parameter_values(), the result-row handlers here will be
- # applied at the Result level
- returning_params = [
- self.dialect._returningval(self.out_parameters["ret_%d" % i])
- for i in range(len(self.out_parameters))
- ]
-
- fetch_strategy = _cursor.FullyBufferedCursorFetchStrategy(
- self.cursor,
- [
- (getattr(col, "name", col._anon_name_label), None)
- for col in self.compiled.returning
- ],
- initial_buffer=[tuple(returning_params)],
- )
-
- self.cursor_fetch_strategy = fetch_strategy
-
- def create_cursor(self):
- c = self._dbapi_connection.cursor()
- if self.dialect.arraysize:
- c.arraysize = self.dialect.arraysize
-
- return c
-
- def get_out_parameter_values(self, out_param_names):
- # this method should not be called when the compiler has
- # RETURNING as we've turned the has_out_parameters flag set to
- # False.
- assert not self.compiled.returning
-
- return [
- self.dialect._paramval(self.out_parameters[name])
- for name in out_param_names
- ]
-
-
- class OracleDialect_cx_oracle(OracleDialect):
- supports_statement_cache = True
- execution_ctx_cls = OracleExecutionContext_cx_oracle
- statement_compiler = OracleCompiler_cx_oracle
-
- supports_sane_rowcount = True
- supports_sane_multi_rowcount = True
-
- supports_unicode_statements = True
- supports_unicode_binds = True
-
- use_setinputsizes = True
-
- driver = "cx_oracle"
-
- colspecs = {
- sqltypes.Numeric: _OracleNumeric,
- sqltypes.Float: _OracleNumeric,
- oracle.BINARY_FLOAT: _OracleBINARY_FLOAT,
- oracle.BINARY_DOUBLE: _OracleBINARY_DOUBLE,
- sqltypes.Integer: _OracleInteger,
- oracle.NUMBER: _OracleNUMBER,
- sqltypes.Date: _OracleDate,
- sqltypes.LargeBinary: _OracleBinary,
- sqltypes.Boolean: oracle._OracleBoolean,
- sqltypes.Interval: _OracleInterval,
- oracle.INTERVAL: _OracleInterval,
- sqltypes.Text: _OracleText,
- sqltypes.String: _OracleString,
- sqltypes.UnicodeText: _OracleUnicodeTextCLOB,
- sqltypes.CHAR: _OracleChar,
- sqltypes.NCHAR: _OracleNChar,
- sqltypes.Enum: _OracleEnum,
- oracle.LONG: _OracleLong,
- oracle.RAW: _OracleRaw,
- sqltypes.Unicode: _OracleUnicodeStringCHAR,
- sqltypes.NVARCHAR: _OracleUnicodeStringNCHAR,
- oracle.NCLOB: _OracleUnicodeTextNCLOB,
- oracle.ROWID: _OracleRowid,
- }
-
- execute_sequence_format = list
-
- _cx_oracle_threaded = None
-
- @util.deprecated_params(
- threaded=(
- "1.3",
- "The 'threaded' parameter to the cx_oracle dialect "
- "is deprecated as a dialect-level argument, and will be removed "
- "in a future release. As of version 1.3, it defaults to False "
- "rather than True. The 'threaded' option can be passed to "
- "cx_Oracle directly in the URL query string passed to "
- ":func:`_sa.create_engine`.",
- )
- )
- def __init__(
- self,
- auto_convert_lobs=True,
- coerce_to_unicode=True,
- coerce_to_decimal=True,
- arraysize=50,
- encoding_errors=None,
- threaded=None,
- **kwargs
- ):
-
- OracleDialect.__init__(self, **kwargs)
- self.arraysize = arraysize
- self.encoding_errors = encoding_errors
- if threaded is not None:
- self._cx_oracle_threaded = threaded
- self.auto_convert_lobs = auto_convert_lobs
- self.coerce_to_unicode = coerce_to_unicode
- self.coerce_to_decimal = coerce_to_decimal
- if self._use_nchar_for_unicode:
- self.colspecs = self.colspecs.copy()
- self.colspecs[sqltypes.Unicode] = _OracleUnicodeStringNCHAR
- self.colspecs[sqltypes.UnicodeText] = _OracleUnicodeTextNCLOB
-
- cx_Oracle = self.dbapi
-
- if cx_Oracle is None:
- self._include_setinputsizes = {}
- self.cx_oracle_ver = (0, 0, 0)
- else:
- self.cx_oracle_ver = self._parse_cx_oracle_ver(cx_Oracle.version)
- if self.cx_oracle_ver < (5, 2) and self.cx_oracle_ver > (0, 0, 0):
- raise exc.InvalidRequestError(
- "cx_Oracle version 5.2 and above are supported"
- )
-
- self._include_setinputsizes = {
- cx_Oracle.DATETIME,
- cx_Oracle.NCLOB,
- cx_Oracle.CLOB,
- cx_Oracle.LOB,
- cx_Oracle.NCHAR,
- cx_Oracle.FIXED_NCHAR,
- cx_Oracle.BLOB,
- cx_Oracle.FIXED_CHAR,
- cx_Oracle.TIMESTAMP,
- _OracleInteger,
- _OracleBINARY_FLOAT,
- _OracleBINARY_DOUBLE,
- }
-
- self._paramval = lambda value: value.getvalue()
-
- # https://github.com/oracle/python-cx_Oracle/issues/176#issuecomment-386821291
- # https://github.com/oracle/python-cx_Oracle/issues/224
- self._values_are_lists = self.cx_oracle_ver >= (6, 3)
- if self._values_are_lists:
- cx_Oracle.__future__.dml_ret_array_val = True
-
- def _returningval(value):
- try:
- return value.values[0][0]
- except IndexError:
- return None
-
- self._returningval = _returningval
- else:
- self._returningval = self._paramval
-
- self._is_cx_oracle_6 = self.cx_oracle_ver >= (6,)
-
- @property
- def _cursor_var_unicode_kwargs(self):
- if self.encoding_errors:
- if self.cx_oracle_ver >= (6, 4):
- return {"encodingErrors": self.encoding_errors}
- else:
- util.warn(
- "cx_oracle version %r does not support encodingErrors"
- % (self.cx_oracle_ver,)
- )
-
- return {}
-
- def _parse_cx_oracle_ver(self, version):
- m = re.match(r"(\d+)\.(\d+)(?:\.(\d+))?", version)
- if m:
- return tuple(int(x) for x in m.group(1, 2, 3) if x is not None)
- else:
- return (0, 0, 0)
-
- @classmethod
- def dbapi(cls):
- import cx_Oracle
-
- return cx_Oracle
-
- def initialize(self, connection):
- super(OracleDialect_cx_oracle, self).initialize(connection)
- if self._is_oracle_8:
- self.supports_unicode_binds = False
-
- self._detect_decimal_char(connection)
-
- def get_isolation_level(self, connection):
- # sources:
-
- # general idea of transaction id, have to start one, etc.
- # https://stackoverflow.com/questions/10711204/how-to-check-isoloation-level
-
- # how to decode xid cols from v$transaction to match
- # https://asktom.oracle.com/pls/apex/f?p=100:11:0::::P11_QUESTION_ID:9532779900346079444
-
- # Oracle tuple comparison without using IN:
- # https://www.sql-workbench.eu/comparison/tuple_comparison.html
-
- with connection.cursor() as cursor:
- # this is the only way to ensure a transaction is started without
- # actually running DML. There's no way to see the configured
- # isolation level without getting it from v$transaction which
- # means transaction has to be started.
- outval = cursor.var(str)
- cursor.execute(
- """
- begin
- :trans_id := dbms_transaction.local_transaction_id( TRUE );
- end;
- """,
- {"trans_id": outval},
- )
- trans_id = outval.getvalue()
- xidusn, xidslot, xidsqn = trans_id.split(".", 2)
-
- cursor.execute(
- "SELECT CASE BITAND(t.flag, POWER(2, 28)) "
- "WHEN 0 THEN 'READ COMMITTED' "
- "ELSE 'SERIALIZABLE' END AS isolation_level "
- "FROM v$transaction t WHERE "
- "(t.xidusn, t.xidslot, t.xidsqn) = "
- "((:xidusn, :xidslot, :xidsqn))",
- {"xidusn": xidusn, "xidslot": xidslot, "xidsqn": xidsqn},
- )
- row = cursor.fetchone()
- if row is None:
- raise exc.InvalidRequestError(
- "could not retrieve isolation level"
- )
- result = row[0]
-
- return result
-
- def set_isolation_level(self, connection, level):
- if hasattr(connection, "connection"):
- dbapi_connection = connection.connection
- else:
- dbapi_connection = connection
- if level == "AUTOCOMMIT":
- dbapi_connection.autocommit = True
- else:
- dbapi_connection.autocommit = False
- connection.rollback()
- with connection.cursor() as cursor:
- cursor.execute("ALTER SESSION SET ISOLATION_LEVEL=%s" % level)
-
- def _detect_decimal_char(self, connection):
- # we have the option to change this setting upon connect,
- # or just look at what it is upon connect and convert.
- # to minimize the chance of interference with changes to
- # NLS_TERRITORY or formatting behavior of the DB, we opt
- # to just look at it
-
- self._decimal_char = connection.exec_driver_sql(
- "select value from nls_session_parameters "
- "where parameter = 'NLS_NUMERIC_CHARACTERS'"
- ).scalar()[0]
- if self._decimal_char != ".":
- _detect_decimal = self._detect_decimal
- _to_decimal = self._to_decimal
-
- self._detect_decimal = lambda value: _detect_decimal(
- value.replace(self._decimal_char, ".")
- )
- self._to_decimal = lambda value: _to_decimal(
- value.replace(self._decimal_char, ".")
- )
-
- def _detect_decimal(self, value):
- if "." in value:
- return self._to_decimal(value)
- else:
- return int(value)
-
- _to_decimal = decimal.Decimal
-
- def _generate_connection_outputtype_handler(self):
- """establish the default outputtypehandler established at the
- connection level.
-
- """
-
- dialect = self
- cx_Oracle = dialect.dbapi
-
- number_handler = _OracleNUMBER(
- asdecimal=True
- )._cx_oracle_outputtypehandler(dialect)
- float_handler = _OracleNUMBER(
- asdecimal=False
- )._cx_oracle_outputtypehandler(dialect)
-
- def output_type_handler(
- cursor, name, default_type, size, precision, scale
- ):
-
- if (
- default_type == cx_Oracle.NUMBER
- and default_type is not cx_Oracle.NATIVE_FLOAT
- ):
- if not dialect.coerce_to_decimal:
- return None
- elif precision == 0 and scale in (0, -127):
- # ambiguous type, this occurs when selecting
- # numbers from deep subqueries
- return cursor.var(
- cx_Oracle.STRING,
- 255,
- outconverter=dialect._detect_decimal,
- arraysize=cursor.arraysize,
- )
- elif precision and scale > 0:
- return number_handler(
- cursor, name, default_type, size, precision, scale
- )
- else:
- return float_handler(
- cursor, name, default_type, size, precision, scale
- )
-
- # allow all strings to come back natively as Unicode
- elif (
- dialect.coerce_to_unicode
- and default_type
- in (
- cx_Oracle.STRING,
- cx_Oracle.FIXED_CHAR,
- )
- and default_type is not cx_Oracle.CLOB
- and default_type is not cx_Oracle.NCLOB
- ):
- if compat.py2k:
- outconverter = processors.to_unicode_processor_factory(
- dialect.encoding, errors=dialect.encoding_errors
- )
- return cursor.var(
- cx_Oracle.STRING,
- size,
- cursor.arraysize,
- outconverter=outconverter,
- )
- else:
- return cursor.var(
- util.text_type,
- size,
- cursor.arraysize,
- **dialect._cursor_var_unicode_kwargs
- )
-
- elif dialect.auto_convert_lobs and default_type in (
- cx_Oracle.CLOB,
- cx_Oracle.NCLOB,
- ):
- if compat.py2k:
- outconverter = processors.to_unicode_processor_factory(
- dialect.encoding, errors=dialect.encoding_errors
- )
- return cursor.var(
- cx_Oracle.LONG_STRING,
- size,
- cursor.arraysize,
- outconverter=outconverter,
- )
- else:
- return cursor.var(
- cx_Oracle.LONG_STRING,
- size,
- cursor.arraysize,
- **dialect._cursor_var_unicode_kwargs
- )
-
- elif dialect.auto_convert_lobs and default_type in (
- cx_Oracle.BLOB,
- ):
- return cursor.var(
- cx_Oracle.LONG_BINARY,
- size,
- cursor.arraysize,
- )
-
- return output_type_handler
-
- def on_connect(self):
-
- output_type_handler = self._generate_connection_outputtype_handler()
-
- def on_connect(conn):
- conn.outputtypehandler = output_type_handler
-
- return on_connect
-
- def create_connect_args(self, url):
- opts = dict(url.query)
-
- for opt in ("use_ansi", "auto_convert_lobs"):
- if opt in opts:
- util.warn_deprecated(
- "cx_oracle dialect option %r should only be passed to "
- "create_engine directly, not within the URL string" % opt,
- version="1.3",
- )
- util.coerce_kw_type(opts, opt, bool)
- setattr(self, opt, opts.pop(opt))
-
- database = url.database
- service_name = opts.pop("service_name", None)
- if database or service_name:
- # if we have a database, then we have a remote host
- port = url.port
- if port:
- port = int(port)
- else:
- port = 1521
-
- if database and service_name:
- raise exc.InvalidRequestError(
- '"service_name" option shouldn\'t '
- 'be used with a "database" part of the url'
- )
- if database:
- makedsn_kwargs = {"sid": database}
- if service_name:
- makedsn_kwargs = {"service_name": service_name}
-
- dsn = self.dbapi.makedsn(url.host, port, **makedsn_kwargs)
- else:
- # we have a local tnsname
- dsn = url.host
-
- if dsn is not None:
- opts["dsn"] = dsn
- if url.password is not None:
- opts["password"] = url.password
- if url.username is not None:
- opts["user"] = url.username
-
- if self._cx_oracle_threaded is not None:
- opts.setdefault("threaded", self._cx_oracle_threaded)
-
- def convert_cx_oracle_constant(value):
- if isinstance(value, util.string_types):
- try:
- int_val = int(value)
- except ValueError:
- value = value.upper()
- return getattr(self.dbapi, value)
- else:
- return int_val
- else:
- return value
-
- util.coerce_kw_type(opts, "mode", convert_cx_oracle_constant)
- util.coerce_kw_type(opts, "threaded", bool)
- util.coerce_kw_type(opts, "events", bool)
- util.coerce_kw_type(opts, "purity", convert_cx_oracle_constant)
- return ([], opts)
-
- def _get_server_version_info(self, connection):
- return tuple(int(x) for x in connection.connection.version.split("."))
-
- def is_disconnect(self, e, connection, cursor):
- (error,) = e.args
- if isinstance(
- e, (self.dbapi.InterfaceError, self.dbapi.DatabaseError)
- ) and "not connected" in str(e):
- return True
-
- if hasattr(error, "code"):
- # ORA-00028: your session has been killed
- # ORA-03114: not connected to ORACLE
- # ORA-03113: end-of-file on communication channel
- # ORA-03135: connection lost contact
- # ORA-01033: ORACLE initialization or shutdown in progress
- # ORA-02396: exceeded maximum idle time, please connect again
- # TODO: Others ?
- return error.code in (28, 3114, 3113, 3135, 1033, 2396)
- else:
- return False
-
- def create_xid(self):
- """create a two-phase transaction ID.
-
- this id will be passed to do_begin_twophase(), do_rollback_twophase(),
- do_commit_twophase(). its format is unspecified.
-
- """
-
- id_ = random.randint(0, 2 ** 128)
- return (0x1234, "%032x" % id_, "%032x" % 9)
-
- def do_executemany(self, cursor, statement, parameters, context=None):
- if isinstance(parameters, tuple):
- parameters = list(parameters)
- cursor.executemany(statement, parameters)
-
- def do_begin_twophase(self, connection, xid):
- connection.connection.begin(*xid)
- connection.connection.info["cx_oracle_xid"] = xid
-
- def do_prepare_twophase(self, connection, xid):
- result = connection.connection.prepare()
- connection.info["cx_oracle_prepared"] = result
-
- def do_rollback_twophase(
- self, connection, xid, is_prepared=True, recover=False
- ):
- self.do_rollback(connection.connection)
- # TODO: need to end XA state here
-
- def do_commit_twophase(
- self, connection, xid, is_prepared=True, recover=False
- ):
-
- if not is_prepared:
- self.do_commit(connection.connection)
- else:
- if recover:
- raise NotImplementedError(
- "2pc recovery not implemented for cx_Oracle"
- )
- oci_prepared = connection.info["cx_oracle_prepared"]
- if oci_prepared:
- self.do_commit(connection.connection)
- # TODO: need to end XA state here
-
- def do_set_input_sizes(self, cursor, list_of_tuples, context):
- if self.positional:
- # not usually used, here to support if someone is modifying
- # the dialect to use positional style
- cursor.setinputsizes(
- *[dbtype for key, dbtype, sqltype in list_of_tuples]
- )
- else:
- collection = (
- (key, dbtype)
- for key, dbtype, sqltype in list_of_tuples
- if dbtype
- )
-
- if not self.supports_unicode_binds:
- # oracle 8 only
- collection = (
- (self.dialect._encoder(key)[0], dbtype)
- for key, dbtype in collection
- )
-
- cursor.setinputsizes(**{key: dbtype for key, dbtype in collection})
-
- def do_recover_twophase(self, connection):
- raise NotImplementedError(
- "recover two phase query for cx_Oracle not implemented"
- )
-
-
- dialect = OracleDialect_cx_oracle
|