Skip to content

Commit

Permalink
Fixed #17755 -- Ensured datetime objects that bypass the model layer …
Browse files Browse the repository at this point in the history
…(for instance, in raw SQL queries) are converted to UTC before sending them to the database when time zone support is enabled. Thanks Anssi for the report.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@17596 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
aaugustin committed Feb 27, 2012
1 parent 7061da5 commit ce88b57
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 23 deletions.
35 changes: 25 additions & 10 deletions django/db/backends/mysql/base.py
Expand Up @@ -4,6 +4,7 @@
Requires MySQLdb: http://sourceforge.net/projects/mysql-python
"""

import datetime
import re
import sys

Expand All @@ -24,6 +25,7 @@

from MySQLdb.converters import conversions
from MySQLdb.constants import FIELD_TYPE, CLIENT
from _mysql import string_literal

from django.db import utils
from django.db.backends import *
Expand All @@ -33,7 +35,7 @@
from django.db.backends.mysql.introspection import DatabaseIntrospection
from django.db.backends.mysql.validation import DatabaseValidation
from django.utils.safestring import SafeString, SafeUnicode
from django.utils.timezone import is_aware, is_naive, utc
from django.utils import timezone

# Raise exceptions for database warnings if DEBUG is on
from django.conf import settings
Expand All @@ -45,15 +47,27 @@
IntegrityError = Database.IntegrityError

# It's impossible to import datetime_or_None directly from MySQLdb.times
datetime_or_None = conversions[FIELD_TYPE.DATETIME]
parse_datetime = conversions[FIELD_TYPE.DATETIME]

def datetime_or_None_with_timezone_support(value):
dt = datetime_or_None(value)
def parse_datetime_with_timezone_support(value):
dt = parse_datetime(value)
# Confirm that dt is naive before overwriting its tzinfo.
if dt is not None and settings.USE_TZ and is_naive(dt):
dt = dt.replace(tzinfo=utc)
if dt is not None and settings.USE_TZ and timezone.is_naive(dt):
dt = dt.replace(tzinfo=timezone.utc)
return dt

def adapt_datetime_with_timezone_support(value, conv):
# Equivalent to DateTimeField.get_db_prep_value. Used only by raw SQL.
if settings.USE_TZ:
if timezone.is_naive(value):
warnings.warn(u"SQLite received a naive datetime (%s)"
u" while time zone support is active." % value,
RuntimeWarning)
default_timezone = timezone.get_default_timezone()
value = timezone.make_aware(value, default_timezone)
value = value.astimezone(timezone.utc).replace(tzinfo=None)
return string_literal(value.strftime("%Y-%m-%d %H:%M:%S"), conv)

# MySQLdb-1.2.1 returns TIME columns as timedelta -- they are more like
# timedelta in terms of actual behavior as they are signed and include days --
# and Django expects time, so we still need to override that. We also need to
Expand All @@ -66,7 +80,8 @@ def datetime_or_None_with_timezone_support(value):
FIELD_TYPE.TIME: util.typecast_time,
FIELD_TYPE.DECIMAL: util.typecast_decimal,
FIELD_TYPE.NEWDECIMAL: util.typecast_decimal,
FIELD_TYPE.DATETIME: datetime_or_None_with_timezone_support,
FIELD_TYPE.DATETIME: parse_datetime_with_timezone_support,
datetime.datetime: adapt_datetime_with_timezone_support,
})

# This should match the numerical portion of the version numbers (we can treat
Expand Down Expand Up @@ -268,9 +283,9 @@ def value_to_db_datetime(self, value):
return None

# MySQL doesn't support tz-aware datetimes
if is_aware(value):
if timezone.is_aware(value):
if settings.USE_TZ:
value = value.astimezone(utc).replace(tzinfo=None)
value = value.astimezone(timezone.utc).replace(tzinfo=None)
else:
raise ValueError("MySQL backend does not support timezone-aware datetimes when USE_TZ is False.")

Expand All @@ -282,7 +297,7 @@ def value_to_db_time(self, value):
return None

# MySQL doesn't support tz-aware times
if is_aware(value):
if timezone.is_aware(value):
raise ValueError("MySQL backend does not support timezone-aware times.")

# MySQL doesn't support microseconds
Expand Down
23 changes: 17 additions & 6 deletions django/db/backends/oracle/base.py
Expand Up @@ -52,7 +52,7 @@ def _setup_environment(environ):
from django.db.backends.oracle.creation import DatabaseCreation
from django.db.backends.oracle.introspection import DatabaseIntrospection
from django.utils.encoding import smart_str, force_unicode
from django.utils.timezone import is_aware, is_naive, utc
from django.utils import timezone

DatabaseError = Database.DatabaseError
IntegrityError = Database.IntegrityError
Expand Down Expand Up @@ -339,9 +339,9 @@ def value_to_db_datetime(self, value):
return None

# Oracle doesn't support tz-aware datetimes
if is_aware(value):
if timezone.is_aware(value):
if settings.USE_TZ:
value = value.astimezone(utc).replace(tzinfo=None)
value = value.astimezone(timezone.utc).replace(tzinfo=None)
else:
raise ValueError("Oracle backend does not support timezone-aware datetimes when USE_TZ is False.")

Expand All @@ -355,7 +355,7 @@ def value_to_db_time(self, value):
return datetime.datetime.strptime(value, '%H:%M:%S')

# Oracle doesn't support tz-aware times
if is_aware(value):
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,
Expand Down Expand Up @@ -561,6 +561,17 @@ 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 timezone.is_naive(param):
warnings.warn(u"Oracle received a naive datetime (%s)"
u" 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)

if hasattr(param, 'bind_parameter'):
self.smart_str = param.bind_parameter(cursor)
else:
Expand Down Expand Up @@ -783,8 +794,8 @@ def _rowfactory(row, cursor):
# of "dates" queries, which are returned as DATETIME.
elif desc[1] in (Database.TIMESTAMP, Database.DATETIME):
# Confirm that dt is naive before overwriting its tzinfo.
if settings.USE_TZ and value is not None and is_naive(value):
value = value.replace(tzinfo=utc)
if settings.USE_TZ and value is not None and timezone.is_naive(value):
value = value.replace(tzinfo=timezone.utc)
elif desc[1] in (Database.STRING, Database.FIXED_CHAR,
Database.LONG_STRING):
value = to_unicode(value)
Expand Down
27 changes: 20 additions & 7 deletions django/db/backends/sqlite3/base.py
Expand Up @@ -19,7 +19,7 @@
from django.db.backends.sqlite3.introspection import DatabaseIntrospection
from django.utils.dateparse import parse_date, parse_datetime, parse_time
from django.utils.safestring import SafeString
from django.utils.timezone import is_aware, is_naive, utc
from django.utils import timezone

try:
try:
Expand All @@ -37,24 +37,37 @@
def parse_datetime_with_timezone_support(value):
dt = parse_datetime(value)
# Confirm that dt is naive before overwriting its tzinfo.
if dt is not None and settings.USE_TZ and is_naive(dt):
dt = dt.replace(tzinfo=utc)
if dt is not None and settings.USE_TZ and timezone.is_naive(dt):
dt = dt.replace(tzinfo=timezone.utc)
return dt

def adapt_datetime_with_timezone_support(value):
# Equivalent to DateTimeField.get_db_prep_value. Used only by raw SQL.
if settings.USE_TZ:
if timezone.is_naive(value):
warnings.warn(u"SQLite received a naive datetime (%s)"
u" while time zone support is active." % value,
RuntimeWarning)
default_timezone = timezone.get_default_timezone()
value = timezone.make_aware(value, default_timezone)
value = value.astimezone(timezone.utc).replace(tzinfo=None)
return value.isoformat(" ")

Database.register_converter("bool", lambda s: str(s) == '1')
Database.register_converter("time", parse_time)
Database.register_converter("date", parse_date)
Database.register_converter("datetime", parse_datetime_with_timezone_support)
Database.register_converter("timestamp", parse_datetime_with_timezone_support)
Database.register_converter("TIMESTAMP", parse_datetime_with_timezone_support)
Database.register_converter("decimal", util.typecast_decimal)
Database.register_adapter(datetime.datetime, adapt_datetime_with_timezone_support)
Database.register_adapter(decimal.Decimal, util.rev_typecast_decimal)
if Database.version_info >= (2, 4, 1):
# Starting in 2.4.1, the str type is not accepted anymore, therefore,
# we convert all str objects to Unicode
# As registering a adapter for a primitive type causes a small
# slow-down, this adapter is only registered for sqlite3 versions
# needing it.
# needing it (Python 2.6 and up).
Database.register_adapter(str, lambda s: s.decode('utf-8'))
Database.register_adapter(SafeString, lambda s: s.decode('utf-8'))

Expand Down Expand Up @@ -147,9 +160,9 @@ def value_to_db_datetime(self, value):
return None

# SQLite doesn't support tz-aware datetimes
if is_aware(value):
if timezone.is_aware(value):
if settings.USE_TZ:
value = value.astimezone(utc).replace(tzinfo=None)
value = value.astimezone(timezone.utc).replace(tzinfo=None)
else:
raise ValueError("SQLite backend does not support timezone-aware datetimes when USE_TZ is False.")

Expand All @@ -160,7 +173,7 @@ def value_to_db_time(self, value):
return None

# SQLite doesn't support tz-aware datetimes
if is_aware(value):
if timezone.is_aware(value):
raise ValueError("SQLite backend does not support timezone-aware times.")

return unicode(value)
Expand Down
18 changes: 18 additions & 0 deletions tests/modeltests/timezones/tests.py
Expand Up @@ -263,6 +263,15 @@ def test_query_dates(self):
self.assertQuerysetEqual(Event.objects.dates('dt', 'day'),
[datetime.datetime(2011, 1, 1)], transform=lambda d: d)

def test_raw_sql(self):
# Regression test for #17755
dt = datetime.datetime(2011, 9, 1, 13, 20, 30)
event = Event.objects.create(dt=dt)
self.assertQuerysetEqual(
Event.objects.raw('SELECT * FROM timezones_event WHERE dt = %s', [dt]),
[event],
transform=lambda d: d)

LegacyDatabaseTests = override_settings(USE_TZ=False)(LegacyDatabaseTests)


Expand Down Expand Up @@ -473,6 +482,15 @@ def test_query_dates(self):
datetime.datetime(2011, 1, 1, tzinfo=UTC)],
transform=lambda d: d)

def test_raw_sql(self):
# Regression test for #17755
dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT)
event = Event.objects.create(dt=dt)
self.assertQuerysetEqual(
Event.objects.raw('SELECT * FROM timezones_event WHERE dt = %s', [dt]),
[event],
transform=lambda d: d)

def test_null_datetime(self):
# Regression for #17294
e = MaybeEvent.objects.create()
Expand Down

0 comments on commit ce88b57

Please sign in to comment.