mirror of https://github.com/django/django.git
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
|
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):
|
class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
empty_fetchmany_value = ()
|
empty_fetchmany_value = ()
|
||||||
needs_datetime_string_cast = False
|
needs_datetime_string_cast = False
|
||||||
|
@ -421,18 +434,36 @@ WHEN (new.%(col_name)s IS NULL)
|
||||||
else:
|
else:
|
||||||
return "TABLESPACE %s" % self.quote_name(tablespace)
|
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):
|
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:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Oracle doesn't support tz-aware datetimes
|
# cx_Oracle 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 = value.astimezone(timezone.utc).replace(tzinfo=None)
|
value = value.astimezone(timezone.utc).replace(tzinfo=None)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Oracle backend does not support timezone-aware datetimes when USE_TZ is False.")
|
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):
|
def value_to_db_time(self, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
|
@ -445,24 +476,21 @@ WHEN (new.%(col_name)s IS NULL)
|
||||||
if timezone.is_aware(value):
|
if timezone.is_aware(value):
|
||||||
raise ValueError("Oracle backend does not support timezone-aware times.")
|
raise ValueError("Oracle backend does not support timezone-aware times.")
|
||||||
|
|
||||||
return datetime.datetime(1900, 1, 1, value.hour, value.minute,
|
return Oracle_datetime(1900, 1, 1, value.hour, value.minute,
|
||||||
value.second, value.microsecond)
|
value.second, value.microsecond)
|
||||||
|
|
||||||
def year_lookup_bounds_for_date_field(self, value):
|
def year_lookup_bounds_for_date_field(self, value):
|
||||||
first = '%s-01-01'
|
# Create bounds as real date values
|
||||||
second = '%s-12-31'
|
first = datetime.date(value, 1, 1)
|
||||||
return [first % value, second % value]
|
last = datetime.date(value, 12, 31)
|
||||||
|
return [first, last]
|
||||||
|
|
||||||
def year_lookup_bounds_for_datetime_field(self, value):
|
def year_lookup_bounds_for_datetime_field(self, value):
|
||||||
# The default implementation uses datetime objects for the bounds.
|
# cx_Oracle doesn't support tz-aware datetimes
|
||||||
# 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.
|
|
||||||
bounds = super(DatabaseOperations, self).year_lookup_bounds_for_datetime_field(value)
|
bounds = super(DatabaseOperations, self).year_lookup_bounds_for_datetime_field(value)
|
||||||
if settings.USE_TZ:
|
if settings.USE_TZ:
|
||||||
bounds = [b.astimezone(timezone.utc).replace(tzinfo=None) for b in bounds]
|
bounds = [b.astimezone(timezone.utc) for b in bounds]
|
||||||
return [b.isoformat(b' ') for b in bounds]
|
return [Oracle_datetime.from_datetime(b) for b in bounds]
|
||||||
|
|
||||||
def combine_expression(self, connector, sub_expressions):
|
def combine_expression(self, connector, sub_expressions):
|
||||||
"Oracle requires special cases for %% and & operators in query 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):
|
def __init__(self, param, cursor, strings_only=False):
|
||||||
# With raw SQL queries, datetimes can reach this function
|
# With raw SQL queries, datetimes can reach this function
|
||||||
# without being converted by DateTimeField.get_db_prep_value.
|
# 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):
|
if timezone.is_naive(param):
|
||||||
warnings.warn("Oracle received a naive datetime (%s)"
|
warnings.warn("Oracle received a naive datetime (%s)"
|
||||||
" while time zone support is active." % param,
|
" while time zone support is active." % param,
|
||||||
RuntimeWarning)
|
RuntimeWarning)
|
||||||
default_timezone = timezone.get_default_timezone()
|
default_timezone = timezone.get_default_timezone()
|
||||||
param = timezone.make_aware(param, 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.
|
# Oracle doesn't recognize True and False correctly in Python 3.
|
||||||
# The conversion done below works both in 2 and 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')
|
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):
|
class PrimaryKeyCharModel(models.Model):
|
||||||
string = models.CharField(max_length=10, primary_key=True)
|
string = models.CharField(max_length=10, primary_key=True)
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ from django.utils.functional import lazy
|
||||||
from .models import (
|
from .models import (
|
||||||
Foo, Bar, Whiz, BigD, BigS, BigInt, Post, NullBooleanModel,
|
Foo, Bar, Whiz, BigD, BigS, BigInt, Post, NullBooleanModel,
|
||||||
BooleanModel, PrimaryKeyCharModel, DataModel, Document, RenamedField,
|
BooleanModel, PrimaryKeyCharModel, DataModel, Document, RenamedField,
|
||||||
VerboseNameField, FksToBooleans, FkToChar, FloatModel)
|
DateTimeModel, VerboseNameField, FksToBooleans, FkToChar, FloatModel)
|
||||||
|
|
||||||
|
|
||||||
class BasicFieldTests(test.TestCase):
|
class BasicFieldTests(test.TestCase):
|
||||||
|
@ -197,6 +197,17 @@ class DateTimeFieldTests(unittest.TestCase):
|
||||||
self.assertEqual(f.to_python('01:02:03.999999'),
|
self.assertEqual(f.to_python('01:02:03.999999'),
|
||||||
datetime.time(1, 2, 3, 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):
|
class BooleanFieldTests(unittest.TestCase):
|
||||||
def _test_get_db_prep_lookup(self, f):
|
def _test_get_db_prep_lookup(self, f):
|
||||||
|
|
Loading…
Reference in New Issue