Skip to content

Commit

Permalink
Fixed #33817 -- Added support for python-oracledb and deprecated cx_O…
Browse files Browse the repository at this point in the history
…racle.
  • Loading branch information
petronny authored and felixxm committed Aug 10, 2023
1 parent 59f13ce commit 9946f0b
Show file tree
Hide file tree
Showing 17 changed files with 80 additions and 50 deletions.
5 changes: 2 additions & 3 deletions django/contrib/gis/db/backends/oracle/adapter.py
Original file line number Diff line number Diff line change
@@ -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):
"""
Expand Down
5 changes: 2 additions & 3 deletions django/contrib/gis/db/backends/oracle/introspection.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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):
Expand Down
15 changes: 6 additions & 9 deletions django/db/backends/oracle/base.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,6 +13,7 @@
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
Expand Down Expand Up @@ -49,12 +50,8 @@ def _setup_environment(environ):
)


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
Expand All @@ -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
Expand Down Expand Up @@ -514,7 +511,7 @@ def _param_generator(self, params):
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.
Expand Down
2 changes: 1 addition & 1 deletion django/db/backends/oracle/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
33 changes: 16 additions & 17 deletions django/db/backends/oracle/introspection.py
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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:
Expand All @@ -52,7 +51,7 @@ def get_field_type(self, data_type, description):
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)
Expand Down Expand Up @@ -193,7 +192,7 @@ def get_table_description(self, cursor, table_name):
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),
Expand Down
8 changes: 4 additions & 4 deletions django/db/backends/oracle/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ def convert_booleanfield_value(self, value, expression, connection):
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.

Expand Down Expand Up @@ -311,10 +311,10 @@ def limit_offset_sql(self, low_mark, high_mark):
)

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:
Expand Down Expand Up @@ -592,7 +592,7 @@ def adapt_datetimefield_value(self, value):
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)
Expand Down
21 changes: 21 additions & 0 deletions django/db/backends/oracle/oracledb_any.py
Original file line number Diff line number Diff line change
@@ -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}")
2 changes: 1 addition & 1 deletion django/db/backends/oracle/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def get_value(self):
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
Expand Down
2 changes: 1 addition & 1 deletion django/db/backends/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def __exit__(self, type, value, traceback):

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 "
Expand Down
2 changes: 1 addition & 1 deletion django/db/models/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1036,7 +1036,7 @@ def as_sql(self, compiler, connection):
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", []
Expand Down
2 changes: 2 additions & 0 deletions docs/internals/deprecation.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions docs/ref/databases.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions docs/releases/5.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~

Expand Down Expand Up @@ -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
=======================

Expand Down
8 changes: 4 additions & 4 deletions docs/topics/install.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/dbshell/test_oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
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:
Expand Down
2 changes: 1 addition & 1 deletion tests/expressions/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ def test_subquery(self):

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"] == ""
Expand Down
2 changes: 1 addition & 1 deletion tests/requirements/oracle.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
cx_oracle >= 8.3
oracledb >= 1.3.2

0 comments on commit 9946f0b

Please sign in to comment.