1
0
mirror of https://github.com/django/django.git synced 2025-01-03 06:55:47 +00:00

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

This commit is contained in:
Jingbei Li 2022-07-12 14:22:40 +08:00 committed by Mariusz Felisiak
parent 59f13ce545
commit 9946f0b0d9
17 changed files with 80 additions and 50 deletions

View File

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

View File

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

View File

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

View File

@ -125,7 +125,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
}, },
} }
django_test_expected_failures = { 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",
"annotations.tests.NonAggregateAnnotationTestCase." "annotations.tests.NonAggregateAnnotationTestCase."
"test_custom_functions_can_ref_other_functions", "test_custom_functions_can_ref_other_functions",

View File

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

View File

@ -247,7 +247,7 @@ END;
value = bool(value) value = bool(value)
return 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 # DATE and TIMESTAMP columns, but Django wants to see a
# python datetime.date, .time, or .datetime. # python datetime.date, .time, or .datetime.
@ -311,10 +311,10 @@ END;
) )
def last_executed_query(self, cursor, sql, params): 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. # The DB API definition does not define this attribute.
statement = cursor.statement 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 # `statement` doesn't contain the query parameters. Substitute
# parameters manually. # parameters manually.
if params: if params:
@ -592,7 +592,7 @@ END;
if hasattr(value, "resolve_expression"): if hasattr(value, "resolve_expression"):
return value return value
# cx_Oracle doesn't support tz-aware datetimes # oracledb doesn't support tz-aware datetimes
if timezone.is_aware(value): if timezone.is_aware(value):
if settings.USE_TZ: if settings.USE_TZ:
value = timezone.make_naive(value, self.connection.timezone) value = timezone.make_naive(value, self.connection.timezone)

View File

@ -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}")

View File

@ -42,7 +42,7 @@ class InsertVar:
class Oracle_datetime(datetime.datetime): class Oracle_datetime(datetime.datetime):
""" """
A datetime object, with an additional class attribute 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 input_size = Database.TIMESTAMP

View File

@ -47,7 +47,7 @@ class CursorWrapper:
def callproc(self, procname, params=None, kparams=None): def callproc(self, procname, params=None, kparams=None):
# Keyword parameters for callproc aren't supported in PEP 249, but the # 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: if kparams is not None and not self.db.features.supports_callproc_kwargs:
raise NotSupportedError( raise NotSupportedError(
"Keyword parameters for callproc are not supported on this " "Keyword parameters for callproc are not supported on this "

View File

@ -1036,7 +1036,7 @@ class Value(SQLiteNumericMixin, Expression):
if hasattr(output_field, "get_placeholder"): if hasattr(output_field, "get_placeholder"):
return output_field.get_placeholder(val, compiler, connection), [val] return output_field.get_placeholder(val, compiler, connection), [val]
if val is None: 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 # NULL type (like in case expressions using numbers), so we
# use a literal SQL NULL # use a literal SQL NULL
return "NULL", [] return "NULL", []

View File

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

View File

@ -919,11 +919,15 @@ To enable the JSON1 extension you can follow the instruction on
Oracle notes Oracle notes
============ ============
Django supports `Oracle Database Server`_ versions 19c and higher. Version 8.3 Django supports `Oracle Database Server`_ versions 19c and higher. Version
or higher of the `cx_Oracle`_ Python driver is required. 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/ .. _`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 In order for the ``python manage.py migrate`` command to work, your Oracle
database user must have privileges to run the following commands: database user must have privileges to run the following commands:

View File

@ -386,6 +386,10 @@ Models
``CHAR(32)`` column. See the migration guide above for more details on ``CHAR(32)`` column. See the migration guide above for more details on
:ref:`migrating-uuidfield`. :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 Pagination
~~~~~~~~~~ ~~~~~~~~~~
@ -606,6 +610,11 @@ Miscellaneous
* Support for calling ``format_html()`` without passing args or kwargs will be * Support for calling ``format_html()`` without passing args or kwargs will be
removed. 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 Features removed in 5.0
======================= =======================

View File

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

View File

@ -5,7 +5,7 @@ from django.db.backends.oracle.client import DatabaseClient
from django.test import SimpleTestCase 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): class OracleDbshellTests(SimpleTestCase):
def settings_to_cmd_args_env(self, settings_dict, parameters=None, rlwrap=False): def settings_to_cmd_args_env(self, settings_dict, parameters=None, rlwrap=False):
if parameters is None: if parameters is None:

View File

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

View File

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