Fixed #20292: Pass datetime objects (not formatted dates) as params to Oracle
This seems worthwhile in its own right, but also works around an Oracle bug (in versions 10 -- 11.1) where the use of Unicode would reset the date/time formats, causing ORA-01843 errors. Thanks Trac users CarstenF for the report, jtiai for the initial patch, and everyone who contributed to the discussion on the ticket.
This commit is contained in:
parent
fc79c3fb3d
commit
6983201cfb
|
@ -78,6 +78,19 @@ else:
|
|||
convert_unicode = force_bytes
|
||||
|
||||
|
||||
class Oracle_datetime(datetime.datetime):
|
||||
"""
|
||||
A datetime object, with an additional class attribute
|
||||
to tell cx_Oracle to save the microseconds too.
|
||||
"""
|
||||
input_size = Database.TIMESTAMP
|
||||
|
||||
@classmethod
|
||||
def from_datetime(cls, dt):
|
||||
return Oracle_datetime(dt.year, dt.month, dt.day,
|
||||
dt.hour, dt.minute, dt.second, dt.microsecond)
|
||||
|
||||
|
||||
class DatabaseFeatures(BaseDatabaseFeatures):
|
||||
empty_fetchmany_value = ()
|
||||
needs_datetime_string_cast = False
|
||||
|
@ -421,18 +434,36 @@ WHEN (new.%(col_name)s IS NULL)
|
|||
else:
|
||||
return "TABLESPACE %s" % self.quote_name(tablespace)
|
||||
|
||||
def value_to_db_date(self, value):
|
||||
"""
|
||||
Transform a date value to an object compatible with what is expected
|
||||
by the backend driver for date columns.
|
||||
The default implementation transforms the date to text, but that is not
|
||||
necessary for Oracle.
|
||||
"""
|
||||
return value
|
||||
|
||||
def value_to_db_datetime(self, value):
|
||||
"""
|
||||
Transform a datetime value to an object compatible with what is expected
|
||||
by the backend driver for datetime columns.
|
||||
|
||||
If naive datetime is passed assumes that is in UTC. Normally Django
|
||||
models.DateTimeField makes sure that if USE_TZ is True passed datetime
|
||||
is timezone aware.
|
||||
"""
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Oracle doesn't support tz-aware datetimes
|
||||
# cx_Oracle doesn't support tz-aware datetimes
|
||||
if timezone.is_aware(value):
|
||||
if settings.USE_TZ:
|
||||
value = value.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
else:
|
||||
raise ValueError("Oracle backend does not support timezone-aware datetimes when USE_TZ is False.")
|
||||
|
||||
return six.text_type(value)
|
||||
return Oracle_datetime.from_datetime(value)
|
||||
|
||||
def value_to_db_time(self, value):
|
||||
if value is None:
|
||||
|
@ -445,24 +476,21 @@ WHEN (new.%(col_name)s IS NULL)
|
|||
if timezone.is_aware(value):
|
||||
raise ValueError("Oracle backend does not support timezone-aware times.")
|
||||
|
||||
return datetime.datetime(1900, 1, 1, value.hour, value.minute,
|
||||
value.second, value.microsecond)
|
||||
return Oracle_datetime(1900, 1, 1, value.hour, value.minute,
|
||||
value.second, value.microsecond)
|
||||
|
||||
def year_lookup_bounds_for_date_field(self, value):
|
||||
first = '%s-01-01'
|
||||
second = '%s-12-31'
|
||||
return [first % value, second % value]
|
||||
# Create bounds as real date values
|
||||
first = datetime.date(value, 1, 1)
|
||||
last = datetime.date(value, 12, 31)
|
||||
return [first, last]
|
||||
|
||||
def year_lookup_bounds_for_datetime_field(self, value):
|
||||
# The default implementation uses datetime objects for the bounds.
|
||||
# This must be overridden here, to use a formatted date (string) as
|
||||
# 'second' instead -- cx_Oracle chops the fraction-of-second part
|
||||
# off of datetime objects, leaving almost an entire second out of
|
||||
# the year under the default implementation.
|
||||
# cx_Oracle doesn't support tz-aware datetimes
|
||||
bounds = super(DatabaseOperations, self).year_lookup_bounds_for_datetime_field(value)
|
||||
if settings.USE_TZ:
|
||||
bounds = [b.astimezone(timezone.utc).replace(tzinfo=None) for b in bounds]
|
||||
return [b.isoformat(b' ') for b in bounds]
|
||||
bounds = [b.astimezone(timezone.utc) for b in bounds]
|
||||
return [Oracle_datetime.from_datetime(b) for b in bounds]
|
||||
|
||||
def combine_expression(self, connector, sub_expressions):
|
||||
"Oracle requires special cases for %% and & operators in query expressions"
|
||||
|
@ -695,14 +723,15 @@ class OracleParam(object):
|
|||
def __init__(self, param, cursor, strings_only=False):
|
||||
# With raw SQL queries, datetimes can reach this function
|
||||
# without being converted by DateTimeField.get_db_prep_value.
|
||||
if settings.USE_TZ and isinstance(param, datetime.datetime):
|
||||
if settings.USE_TZ and (isinstance(param, datetime.datetime) and
|
||||
not isinstance(param, Oracle_datetime)):
|
||||
if timezone.is_naive(param):
|
||||
warnings.warn("Oracle received a naive datetime (%s)"
|
||||
" while time zone support is active." % param,
|
||||
RuntimeWarning)
|
||||
default_timezone = timezone.get_default_timezone()
|
||||
param = timezone.make_aware(param, default_timezone)
|
||||
param = param.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
param = Oracle_datetime.from_datetime(param.astimezone(timezone.utc))
|
||||
|
||||
# Oracle doesn't recognize True and False correctly in Python 3.
|
||||
# The conversion done below works both in 2 and 3.
|
||||
|
|
|
@ -76,6 +76,12 @@ class BooleanModel(models.Model):
|
|||
string = models.CharField(max_length=10, default='abc')
|
||||
|
||||
|
||||
class DateTimeModel(models.Model):
|
||||
d = models.DateField()
|
||||
dt = models.DateTimeField()
|
||||
t = models.TimeField()
|
||||
|
||||
|
||||
class PrimaryKeyCharModel(models.Model):
|
||||
string = models.CharField(max_length=10, primary_key=True)
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ from django.utils.functional import lazy
|
|||
from .models import (
|
||||
Foo, Bar, Whiz, BigD, BigS, BigInt, Post, NullBooleanModel,
|
||||
BooleanModel, PrimaryKeyCharModel, DataModel, Document, RenamedField,
|
||||
VerboseNameField, FksToBooleans, FkToChar, FloatModel)
|
||||
DateTimeModel, VerboseNameField, FksToBooleans, FkToChar, FloatModel)
|
||||
|
||||
|
||||
class BasicFieldTests(test.TestCase):
|
||||
|
@ -197,6 +197,17 @@ class DateTimeFieldTests(unittest.TestCase):
|
|||
self.assertEqual(f.to_python('01:02:03.999999'),
|
||||
datetime.time(1, 2, 3, 999999))
|
||||
|
||||
def test_datetimes_save_completely(self):
|
||||
dat = datetime.date(2014, 3, 12)
|
||||
datetim = datetime.datetime(2014, 3, 12, 21, 22, 23, 240000)
|
||||
tim = datetime.time(21, 22, 23, 240000)
|
||||
DateTimeModel.objects.create(d=dat, dt=datetim, t=tim)
|
||||
obj = DateTimeModel.objects.first()
|
||||
self.assertTrue(obj)
|
||||
self.assertEqual(obj.d, dat)
|
||||
self.assertEqual(obj.dt, datetim)
|
||||
self.assertEqual(obj.t, tim)
|
||||
|
||||
|
||||
class BooleanFieldTests(unittest.TestCase):
|
||||
def _test_get_db_prep_lookup(self, f):
|
||||
|
|
Loading…
Reference in New Issue