diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 9b07b9dcb7..5ed9562d1e 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -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. diff --git a/docs/releases/1.6.6.txt b/docs/releases/1.6.6.txt index c8c7a037c0..fb182f5dcb 100644 --- a/docs/releases/1.6.6.txt +++ b/docs/releases/1.6.6.txt @@ -26,3 +26,7 @@ Bugfixes * Fixed transaction handling when specifying non-default database in ``createcachetable`` and ``flush`` (`#23089 `_). + +* Fixed the "ORA-01843: not a valid month" errors when using Unicode + with older versions of Oracle server + (`#20292 `_). diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py index a1e1a88595..6a7c122ea7 100644 --- a/tests/model_fields/models.py +++ b/tests/model_fields/models.py @@ -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) diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py index 704ae0b822..c6004f99a1 100644 --- a/tests/model_fields/tests.py +++ b/tests/model_fields/tests.py @@ -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