Fixed #24033 -- Use interval type on Oracle.

Use INTERVAL DAY(9) TO SECOND(6) for Durationfield on Oracle rather than
storing as a NUMBER(19) of microseconds.

There are issues with cx_Oracle which require some extra data
manipulation in the database backend when constructing queries, but it
handles the conversion back to timedelta objects cleanly.

Thanks to Shai for the review.
This commit is contained in:
Marc Tamlyn 2014-12-21 14:36:37 +00:00
parent 803947161b
commit 5ca82e710e
8 changed files with 25 additions and 10 deletions

View File

@ -580,6 +580,12 @@ class BaseDatabaseFeatures(object):
# Is there a true datatype for timedeltas?
has_native_duration_field = False
# Does the database driver support timedeltas as arguments?
# This is only relevant when there is a native duration field.
# Specifically, there is a bug with cx_Oracle:
# https://bitbucket.org/anthony_tuininga/cx_oracle/issue/7/
driver_supports_timedelta_args = False
# Do time/datetime fields have microsecond precision?
supports_microsecond_precision = True

View File

@ -62,6 +62,7 @@ from django.db.backends.oracle.introspection import DatabaseIntrospection
from django.db.backends.oracle.schema import DatabaseSchemaEditor
from django.db.utils import InterfaceError
from django.utils import six, timezone
from django.utils.duration import duration_string
from django.utils.encoding import force_bytes, force_text
from django.utils.functional import cached_property
@ -106,6 +107,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_timezones = False
has_zoneinfo_database = pytz is not None
supports_bitwise_or = False
has_native_duration_field = True
can_defer_constraint_checks = True
supports_partially_nullable_unique_constraints = False
truncates_names = True
@ -212,9 +214,6 @@ WHEN (new.%(col_name)s IS NULL)
return fmt % (days, hours, minutes, seconds, timedelta.microseconds,
day_precision), []
def format_for_duration_arithmetic(self, sql):
return "NUMTODSINTERVAL(%s / 1000000, 'SECOND')" % sql
def date_trunc_sql(self, lookup_type, field_name):
# http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions230.htm#i1002084
if lookup_type in ('year', 'month'):
@ -796,6 +795,11 @@ class OracleParam(object):
param = timezone.make_aware(param, default_timezone)
param = Oracle_datetime.from_datetime(param.astimezone(timezone.utc))
if isinstance(param, datetime.timedelta):
param = duration_string(param)
if ' ' not in param:
param = '0 ' + param
string_size = 0
# Oracle doesn't recognize True and False correctly in Python 3.
# The conversion done below works both in 2 and 3.

View File

@ -29,7 +29,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'DateField': 'DATE',
'DateTimeField': 'TIMESTAMP',
'DecimalField': 'NUMBER(%(max_digits)s, %(decimal_places)s)',
'DurationField': 'NUMBER(19)',
'DurationField': 'INTERVAL DAY(9) TO SECOND(6)',
'FileField': 'NVARCHAR2(%(max_length)s)',
'FilePathField': 'NVARCHAR2(%(max_length)s)',
'FloatField': 'DOUBLE PRECISION',

View File

@ -48,6 +48,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
can_return_id_from_insert = True
has_real_datatype = True
has_native_duration_field = True
driver_supports_timedelta_args = True
can_defer_constraint_checks = True
has_select_for_update = True
has_select_for_update_nowait = True

View File

@ -488,7 +488,8 @@ class Value(ExpressionNode):
class DurationValue(Value):
def as_sql(self, compiler, connection):
if connection.features.has_native_duration_field:
if (connection.features.has_native_duration_field and
connection.features.driver_supports_timedelta_args):
return super(DurationValue, self).as_sql(compiler, connection)
return connection.ops.date_interval_sql(self.value)

View File

@ -1577,7 +1577,8 @@ class DecimalField(Field):
class DurationField(Field):
"""Stores timedelta objects.
Uses interval on postgres, bigint of microseconds on other databases.
Uses interval on postgres, INVERAL DAY TO SECOND on Oracle, and bigint of
microseconds on other databases.
"""
empty_strings_allowed = False
default_error_messages = {

View File

@ -551,7 +551,8 @@ The default form widget for this field is a :class:`~django.forms.TextInput`.
A field for storing periods of time - modeled in Python by
:class:`~python:datetime.timedelta`. When used on PostgreSQL, the data type
used is an ``interval``, otherwise a ``bigint`` of microseconds is used.
used is an ``interval`` and on Oracle the data type is ``INTERVAL DAY(9) TO
SECOND(6)``. Otherwise a ``bigint`` of microseconds is used.
.. note::

View File

@ -60,9 +60,10 @@ New data types
* Django now has a :class:`~django.db.models.DurationField` for storing periods
of time - modeled in Python by :class:`~python:datetime.timedelta`. It is
stored in the native ``interval`` data type on PostgreSQL and as a ``bigint``
of microseconds on other backends. Date and time related arithmetic has also
been improved on all backends. There is a corresponding :class:`form field
stored in the native ``interval`` data type on PostgreSQL, as a ``INTERVAL
DAY(9) TO SECOND(6)`` on Oracle, and as a ``bigint`` of microseconds on other
backends. Date and time related arithmetic has also been improved on all
backends. There is a corresponding :class:`form field
<django.forms.DurationField>`.
Query Expressions