Skip to content

Commit

Permalink
[1.6.x] Fixed #20292: Pass datetime objects (not formatted dates) as …
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
shaib authored and timgraham committed Jul 29, 2014
1 parent 8e25b69 commit 838b7f8
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 17 deletions.
61 changes: 45 additions & 16 deletions django/db/backends/oracle/base.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ def _setup_environment(environ):
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
Expand Down Expand Up @@ -405,18 +418,36 @@ def tablespace_sql(self, tablespace, inline=False):
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:
Expand All @@ -429,24 +460,21 @@ def value_to_db_time(self, value):
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(str(' ')) 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"
Expand Down Expand Up @@ -671,14 +699,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.
Expand Down
4 changes: 4 additions & 0 deletions docs/releases/1.6.6.txt
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ Bugfixes
* Fixed transaction handling when specifying non-default database in * Fixed transaction handling when specifying non-default database in
``createcachetable`` and ``flush`` ``createcachetable`` and ``flush``
(`#23089 <https://code.djangoproject.com/ticket/23089>`_). (`#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>`_).
6 changes: 6 additions & 0 deletions tests/model_fields/models.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ class BooleanModel(models.Model):
bfield = models.BooleanField(default=None) bfield = models.BooleanField(default=None)
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 FksToBooleans(models.Model): class FksToBooleans(models.Model):
"""Model wih FKs to models with {Null,}BooleanField's, #15040""" """Model wih FKs to models with {Null,}BooleanField's, #15040"""
bf = models.ForeignKey(BooleanModel) bf = models.ForeignKey(BooleanModel)
Expand Down
13 changes: 12 additions & 1 deletion tests/model_fields/tests.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@


from .models import (Foo, Bar, Whiz, BigD, BigS, Image, BigInt, Post, from .models import (Foo, Bar, Whiz, BigD, BigS, Image, BigInt, Post,
NullBooleanModel, BooleanModel, DataModel, Document, RenamedField, NullBooleanModel, BooleanModel, DataModel, Document, RenamedField,
VerboseNameField, FksToBooleans) DateTimeModel, VerboseNameField, FksToBooleans)




class BasicFieldTests(test.TestCase): class BasicFieldTests(test.TestCase):
Expand Down Expand Up @@ -154,6 +154,17 @@ def test_timefield_to_python_usecs(self):
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):
from django.db import connection from django.db import connection
Expand Down

1 comment on commit 838b7f8

@SalahAdDin
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, i try with this, maybe will solve my troubles with mysql-connector-python oracle connector.!

Please sign in to comment.