summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShai Berger <shai@platonix.com>2014-03-12 23:43:45 +0200
committerShai Berger <shai@platonix.com>2014-03-12 23:43:45 +0200
commit6983201cfb172c2895ed6eea1e0f0eb3804d4c00 (patch)
treefe1fd3bbf0ce13483e5e969673b61062c21957a0
parentfc79c3fb3daf64b911b1ff8fed30f73c56630416 (diff)
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.
-rw-r--r--django/db/backends/oracle/base.py61
-rw-r--r--tests/model_fields/models.py6
-rw-r--r--tests/model_fields/tests.py13
3 files changed, 63 insertions, 17 deletions
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index d6aaf35ab0..0c799055fb 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -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.
diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py
index c9e6555525..c4e99c949a 100644
--- a/tests/model_fields/models.py
+++ b/tests/model_fields/models.py
@@ -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)
diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py
index 8cd58b8a44..c8b46d8c2f 100644
--- a/tests/model_fields/tests.py
+++ b/tests/model_fields/tests.py
@@ -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):