mirror of
https://github.com/django/django.git
synced 2025-04-15 21:04:36 +00:00
[1.6.x] 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. Backport of 6983201 from master.
This commit is contained in:
parent
8e25b696ba
commit
838b7f8220
@ -70,6 +70,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
|
||||
@ -405,18 +418,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:
|
||||
@ -429,24 +460,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(str(' ')) 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"
|
||||
@ -671,14 +699,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.
|
||||
|
@ -26,3 +26,7 @@ Bugfixes
|
||||
* Fixed transaction handling when specifying non-default database in
|
||||
``createcachetable`` and ``flush``
|
||||
(`#23089 <https://code.djangoproject.com/ticket/23089>`_).
|
||||
|
||||
* Fixed the "ORA-01843: not a valid month" errors when using Unicode
|
||||
with older versions of Oracle server
|
||||
(`#20292 <https://code.djangoproject.com/ticket/20292>`_).
|
||||
|
@ -61,6 +61,12 @@ class BooleanModel(models.Model):
|
||||
bfield = models.BooleanField(default=None)
|
||||
string = models.CharField(max_length=10, default='abc')
|
||||
|
||||
class DateTimeModel(models.Model):
|
||||
d = models.DateField()
|
||||
dt = models.DateTimeField()
|
||||
t = models.TimeField()
|
||||
|
||||
|
||||
class FksToBooleans(models.Model):
|
||||
"""Model wih FKs to models with {Null,}BooleanField's, #15040"""
|
||||
bf = models.ForeignKey(BooleanModel)
|
||||
|
@ -20,7 +20,7 @@ from django.utils import unittest
|
||||
|
||||
from .models import (Foo, Bar, Whiz, BigD, BigS, Image, BigInt, Post,
|
||||
NullBooleanModel, BooleanModel, DataModel, Document, RenamedField,
|
||||
VerboseNameField, FksToBooleans)
|
||||
DateTimeModel, VerboseNameField, FksToBooleans)
|
||||
|
||||
|
||||
class BasicFieldTests(test.TestCase):
|
||||
@ -154,6 +154,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):
|
||||
from django.db import connection
|
||||
|
Loading…
x
Reference in New Issue
Block a user