Browse Source

Fixed #33817 -- Added support for python-oracledb and deprecated cx_Oracle.

Jingbei Li 2 years ago
parent
commit
9946f0b0d9

+ 2 - 3
django/contrib/gis/db/backends/oracle/adapter.py

@@ -1,11 +1,10 @@
-from cx_Oracle import CLOB
-
 from django.contrib.gis.db.backends.base.adapter import WKTAdapter
 from django.contrib.gis.geos import GeometryCollection, Polygon
+from django.db.backends.oracle.oracledb_any import oracledb
 
 
 class OracleSpatialAdapter(WKTAdapter):
-    input_size = CLOB
+    input_size = oracledb.CLOB
 
     def __init__(self, geom):
         """

+ 2 - 3
django/contrib/gis/db/backends/oracle/introspection.py

@@ -1,6 +1,5 @@
-import cx_Oracle
-
 from django.db.backends.oracle.introspection import DatabaseIntrospection
+from django.db.backends.oracle.oracledb_any import oracledb
 from django.utils.functional import cached_property
 
 
@@ -12,7 +11,7 @@ class OracleIntrospection(DatabaseIntrospection):
     def data_types_reverse(self):
         return {
             **super().data_types_reverse,
-            cx_Oracle.OBJECT: "GeometryField",
+            oracledb.DB_TYPE_OBJECT: "GeometryField",
         }
 
     def get_geometry_type(self, table_name, description):

+ 6 - 9
django/db/backends/oracle/base.py

@@ -1,7 +1,7 @@
 """
 Oracle database backend for Django.
 
-Requires cx_Oracle: https://oracle.github.io/python-cx_Oracle/
+Requires oracledb: https://oracle.github.io/python-oracledb/
 """
 import datetime
 import decimal
@@ -13,6 +13,7 @@ from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
 from django.db import IntegrityError
 from django.db.backends.base.base import BaseDatabaseWrapper
+from django.db.backends.oracle.oracledb_any import oracledb as Database
 from django.db.backends.utils import debug_transaction
 from django.utils.asyncio import async_unsafe
 from django.utils.encoding import force_bytes, force_str
@@ -49,12 +50,8 @@ _setup_environment(
 )
 
 
-try:
-    import cx_Oracle as Database
-except ImportError as e:
-    raise ImproperlyConfigured("Error loading cx_Oracle module: %s" % e)
-
-# Some of these import cx_Oracle, so import them after checking if it's installed.
+# Some of these import oracledb, so import them after checking if it's
+# installed.
 from .client import DatabaseClient  # NOQA
 from .creation import DatabaseCreation  # NOQA
 from .features import DatabaseFeatures  # NOQA
@@ -70,7 +67,7 @@ def wrap_oracle_errors():
     try:
         yield
     except Database.DatabaseError as e:
-        # cx_Oracle raises a cx_Oracle.DatabaseError exception with the
+        # oracledb raises a oracledb.DatabaseError exception with the
         # following attributes and values:
         #  code = 2091
         #  message = 'ORA-02091: transaction rolled back
@@ -514,7 +511,7 @@ class FormatStylePlaceholderCursor:
             return [p.force_bytes for p in params]
 
     def _fix_for_params(self, query, params, unify_by_values=False):
-        # cx_Oracle wants no trailing ';' for SQL statements.  For PL/SQL, it
+        # oracledb wants no trailing ';' for SQL statements.  For PL/SQL, it
         # it does want a trailing ';' but not a trailing '/'.  However, these
         # characters must be included in the original query in case the query
         # is being passed to SQL*Plus.

+ 1 - 1
django/db/backends/oracle/features.py

@@ -125,7 +125,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
         },
     }
     django_test_expected_failures = {
-        # A bug in Django/cx_Oracle with respect to string handling (#23843).
+        # A bug in Django/oracledb with respect to string handling (#23843).
         "annotations.tests.NonAggregateAnnotationTestCase.test_custom_functions",
         "annotations.tests.NonAggregateAnnotationTestCase."
         "test_custom_functions_can_ref_other_functions",

+ 16 - 17
django/db/backends/oracle/introspection.py

@@ -1,11 +1,10 @@
 from collections import namedtuple
 
-import cx_Oracle
-
 from django.db import models
 from django.db.backends.base.introspection import BaseDatabaseIntrospection
 from django.db.backends.base.introspection import FieldInfo as BaseFieldInfo
 from django.db.backends.base.introspection import TableInfo as BaseTableInfo
+from django.db.backends.oracle.oracledb_any import oracledb
 
 FieldInfo = namedtuple(
     "FieldInfo", BaseFieldInfo._fields + ("is_autofield", "is_json", "comment")
@@ -18,22 +17,22 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
 
     # Maps type objects to Django Field types.
     data_types_reverse = {
-        cx_Oracle.DB_TYPE_DATE: "DateField",
-        cx_Oracle.DB_TYPE_BINARY_DOUBLE: "FloatField",
-        cx_Oracle.DB_TYPE_BLOB: "BinaryField",
-        cx_Oracle.DB_TYPE_CHAR: "CharField",
-        cx_Oracle.DB_TYPE_CLOB: "TextField",
-        cx_Oracle.DB_TYPE_INTERVAL_DS: "DurationField",
-        cx_Oracle.DB_TYPE_NCHAR: "CharField",
-        cx_Oracle.DB_TYPE_NCLOB: "TextField",
-        cx_Oracle.DB_TYPE_NVARCHAR: "CharField",
-        cx_Oracle.DB_TYPE_NUMBER: "DecimalField",
-        cx_Oracle.DB_TYPE_TIMESTAMP: "DateTimeField",
-        cx_Oracle.DB_TYPE_VARCHAR: "CharField",
+        oracledb.DB_TYPE_DATE: "DateField",
+        oracledb.DB_TYPE_BINARY_DOUBLE: "FloatField",
+        oracledb.DB_TYPE_BLOB: "BinaryField",
+        oracledb.DB_TYPE_CHAR: "CharField",
+        oracledb.DB_TYPE_CLOB: "TextField",
+        oracledb.DB_TYPE_INTERVAL_DS: "DurationField",
+        oracledb.DB_TYPE_NCHAR: "CharField",
+        oracledb.DB_TYPE_NCLOB: "TextField",
+        oracledb.DB_TYPE_NVARCHAR: "CharField",
+        oracledb.DB_TYPE_NUMBER: "DecimalField",
+        oracledb.DB_TYPE_TIMESTAMP: "DateTimeField",
+        oracledb.DB_TYPE_VARCHAR: "CharField",
     }
 
     def get_field_type(self, data_type, description):
-        if data_type == cx_Oracle.NUMBER:
+        if data_type == oracledb.NUMBER:
             precision, scale = description[4:6]
             if scale == 0:
                 if precision > 11:
@@ -52,7 +51,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
                     return "IntegerField"
             elif scale == -127:
                 return "FloatField"
-        elif data_type == cx_Oracle.NCLOB and description.is_json:
+        elif data_type == oracledb.NCLOB and description.is_json:
             return "JSONField"
 
         return super().get_field_type(data_type, description)
@@ -193,7 +192,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
                 is_json,
                 comment,
             ) = field_map[name]
-            name %= {}  # cx_Oracle, for some reason, doubles percent signs.
+            name %= {}  # oracledb, for some reason, doubles percent signs.
             description.append(
                 FieldInfo(
                     self.identifier_converter(name),

+ 4 - 4
django/db/backends/oracle/operations.py

@@ -247,7 +247,7 @@ END;
             value = bool(value)
         return value
 
-    # cx_Oracle always returns datetime.datetime objects for
+    # oracledb always returns datetime.datetime objects for
     # DATE and TIMESTAMP columns, but Django wants to see a
     # python datetime.date, .time, or .datetime.
 
@@ -311,10 +311,10 @@ END;
         )
 
     def last_executed_query(self, cursor, sql, params):
-        # https://cx-oracle.readthedocs.io/en/latest/api_manual/cursor.html#Cursor.statement
+        # https://python-oracledb.readthedocs.io/en/latest/api_manual/cursor.html#Cursor.statement
         # The DB API definition does not define this attribute.
         statement = cursor.statement
-        # Unlike Psycopg's `query` and MySQLdb`'s `_executed`, cx_Oracle's
+        # Unlike Psycopg's `query` and MySQLdb`'s `_executed`, oracledb's
         # `statement` doesn't contain the query parameters. Substitute
         # parameters manually.
         if params:
@@ -592,7 +592,7 @@ END;
         if hasattr(value, "resolve_expression"):
             return value
 
-        # cx_Oracle doesn't support tz-aware datetimes
+        # oracledb doesn't support tz-aware datetimes
         if timezone.is_aware(value):
             if settings.USE_TZ:
                 value = timezone.make_naive(value, self.connection.timezone)

+ 21 - 0
django/db/backends/oracle/oracledb_any.py

@@ -0,0 +1,21 @@
+import warnings
+
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.deprecation import RemovedInDjango60Warning
+
+try:
+    import oracledb
+
+    is_oracledb = True
+except ImportError as e:
+    try:
+        import cx_Oracle as oracledb  # NOQA
+
+        warnings.warn(
+            "cx_Oracle is deprecated. Use oracledb instead.",
+            RemovedInDjango60Warning,
+            stacklevel=2,
+        )
+        is_oracledb = False
+    except ImportError:
+        raise ImproperlyConfigured(f"Error loading oracledb module: {e}")

+ 1 - 1
django/db/backends/oracle/utils.py

@@ -42,7 +42,7 @@ class InsertVar:
 class Oracle_datetime(datetime.datetime):
     """
     A datetime object, with an additional class attribute
-    to tell cx_Oracle to save the microseconds too.
+    to tell oracledb to save the microseconds too.
     """
 
     input_size = Database.TIMESTAMP

+ 1 - 1
django/db/backends/utils.py

@@ -47,7 +47,7 @@ class CursorWrapper:
 
     def callproc(self, procname, params=None, kparams=None):
         # Keyword parameters for callproc aren't supported in PEP 249, but the
-        # database driver may support them (e.g. cx_Oracle).
+        # database driver may support them (e.g. oracledb).
         if kparams is not None and not self.db.features.supports_callproc_kwargs:
             raise NotSupportedError(
                 "Keyword parameters for callproc are not supported on this "

+ 1 - 1
django/db/models/expressions.py

@@ -1036,7 +1036,7 @@ class Value(SQLiteNumericMixin, Expression):
             if hasattr(output_field, "get_placeholder"):
                 return output_field.get_placeholder(val, compiler, connection), [val]
         if val is None:
-            # cx_Oracle does not always convert None to the appropriate
+            # oracledb does not always convert None to the appropriate
             # NULL type (like in case expressions using numbers), so we
             # use a literal SQL NULL
             return "NULL", []

+ 2 - 0
docs/internals/deprecation.txt

@@ -38,6 +38,8 @@ details on these changes.
 * Support for calling ``format_html()`` without passing args or kwargs will be
   removed.
 
+* Support for ``cx_Oracle`` will be removed.
+
 .. _deprecation-removed-in-5.1:
 
 5.1

+ 7 - 3
docs/ref/databases.txt

@@ -919,11 +919,15 @@ To enable the JSON1 extension you can follow the instruction on
 Oracle notes
 ============
 
-Django supports `Oracle Database Server`_ versions 19c and higher. Version 8.3
-or higher of the `cx_Oracle`_ Python driver is required.
+Django supports `Oracle Database Server`_ versions 19c and higher. Version
+1.3.2 or higher of the `oracledb`_ Python driver is required.
+
+.. deprecated:: 5.0
+
+    Support for ``cx_Oracle`` is deprecated.
 
 .. _`Oracle Database Server`: https://www.oracle.com/
-.. _`cx_Oracle`: https://oracle.github.io/python-cx_Oracle/
+.. _`oracledb`: https://oracle.github.io/python-oracledb/
 
 In order for the ``python manage.py migrate`` command to work, your Oracle
 database user must have privileges to run the following commands:

+ 9 - 0
docs/releases/5.0.txt

@@ -386,6 +386,10 @@ Models
   ``CHAR(32)`` column. See the migration guide above for more details on
   :ref:`migrating-uuidfield`.
 
+* Django now supports `oracledb`_ version 1.3.2 or higher. Support for
+  ``cx_Oracle`` is deprecated as of this release and will be removed in Django
+  6.0.
+
 Pagination
 ~~~~~~~~~~
 
@@ -606,6 +610,11 @@ Miscellaneous
 * Support for calling ``format_html()`` without passing args or kwargs will be
   removed.
 
+* Support for ``cx_Oracle`` is deprecated in favor of `oracledb`_ 1.3.2+ Python
+  driver.
+
+.. _`oracledb`: https://oracle.github.io/python-oracledb/
+
 Features removed in 5.0
 =======================
 

+ 4 - 4
docs/topics/install.txt

@@ -90,9 +90,9 @@ database bindings are installed.
 * If you're using SQLite you might want to read the :ref:`SQLite backend notes
   <sqlite-notes>`.
 
-* If you're using Oracle, you'll need a copy of cx_Oracle_, but please
-  read the :ref:`notes for the Oracle backend <oracle-notes>` for details
-  regarding supported versions of both Oracle and ``cx_Oracle``.
+* If you're using Oracle, you'll need to install oracledb_, but please read the
+  :ref:`notes for the Oracle backend <oracle-notes>` for details regarding
+  supported versions of both Oracle and ``oracledb``.
 
 * If you're using an unofficial 3rd party backend, please consult the
   documentation provided for any additional requirements.
@@ -115,7 +115,7 @@ database queries, Django will need permission to create a test database.
 .. _psycopg: https://www.psycopg.org/psycopg3/
 .. _psycopg2: https://www.psycopg.org/
 .. _SQLite: https://www.sqlite.org/
-.. _cx_Oracle: https://oracle.github.io/python-cx_Oracle/
+.. _oracledb: https://oracle.github.io/python-oracledb/
 .. _Oracle: https://www.oracle.com/
 
 .. _install-django-code:

+ 1 - 1
tests/dbshell/test_oracle.py

@@ -5,7 +5,7 @@ from django.db.backends.oracle.client import DatabaseClient
 from django.test import SimpleTestCase
 
 
-@skipUnless(connection.vendor == "oracle", "Requires cx_Oracle to be installed")
+@skipUnless(connection.vendor == "oracle", "Requires oracledb to be installed")
 class OracleDbshellTests(SimpleTestCase):
     def settings_to_cmd_args_env(self, settings_dict, parameters=None, rlwrap=False):
         if parameters is None:

+ 1 - 1
tests/expressions/tests.py

@@ -536,7 +536,7 @@ class BasicExpressionsTests(TestCase):
 
         results = list(qs)
         # Could use Coalesce(subq, Value('')) instead except for the bug in
-        # cx_Oracle mentioned in #23843.
+        # oracledb mentioned in #23843.
         bob = results[0]
         if (
             bob["largest_company"] == ""

+ 1 - 1
tests/requirements/oracle.txt

@@ -1 +1 @@
-cx_oracle >= 8.3
+oracledb >= 1.3.2