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:
parent
59f13ce545
commit
9946f0b0d9
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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):
|
||||||
|
@ -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.
|
||||||
|
@ -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",
|
||||||
|
@ -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),
|
||||||
|
@ -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)
|
||||||
|
21
django/db/backends/oracle/oracledb_any.py
Normal file
21
django/db/backends/oracle/oracledb_any.py
Normal 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}")
|
@ -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
|
||||||
|
@ -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 "
|
||||||
|
@ -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", []
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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"] == ""
|
||||||
|
@ -1 +1 @@
|
|||||||
cx_oracle >= 8.3
|
oracledb >= 1.3.2
|
||||||
|
Loading…
Reference in New Issue
Block a user