Fixed #17755 -- Ensured datetime objects that bypass the model layer (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
This commit is contained in:
parent
7061da514a
commit
ce88b57b9a
|
@ -4,6 +4,7 @@ MySQL database backend for Django.
|
||||||
Requires MySQLdb: http://sourceforge.net/projects/mysql-python
|
Requires MySQLdb: http://sourceforge.net/projects/mysql-python
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ if (version < (1,2,1) or (version[:3] == (1, 2, 1) and
|
||||||
|
|
||||||
from MySQLdb.converters import conversions
|
from MySQLdb.converters import conversions
|
||||||
from MySQLdb.constants import FIELD_TYPE, CLIENT
|
from MySQLdb.constants import FIELD_TYPE, CLIENT
|
||||||
|
from _mysql import string_literal
|
||||||
|
|
||||||
from django.db import utils
|
from django.db import utils
|
||||||
from django.db.backends import *
|
from django.db.backends import *
|
||||||
|
@ -33,7 +35,7 @@ from django.db.backends.mysql.creation import DatabaseCreation
|
||||||
from django.db.backends.mysql.introspection import DatabaseIntrospection
|
from django.db.backends.mysql.introspection import DatabaseIntrospection
|
||||||
from django.db.backends.mysql.validation import DatabaseValidation
|
from django.db.backends.mysql.validation import DatabaseValidation
|
||||||
from django.utils.safestring import SafeString, SafeUnicode
|
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
|
# Raise exceptions for database warnings if DEBUG is on
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -45,15 +47,27 @@ DatabaseError = Database.DatabaseError
|
||||||
IntegrityError = Database.IntegrityError
|
IntegrityError = Database.IntegrityError
|
||||||
|
|
||||||
# It's impossible to import datetime_or_None directly from MySQLdb.times
|
# 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):
|
def parse_datetime_with_timezone_support(value):
|
||||||
dt = datetime_or_None(value)
|
dt = parse_datetime(value)
|
||||||
# Confirm that dt is naive before overwriting its tzinfo.
|
# Confirm that dt is naive before overwriting its tzinfo.
|
||||||
if dt is not None and settings.USE_TZ and is_naive(dt):
|
if dt is not None and settings.USE_TZ and timezone.is_naive(dt):
|
||||||
dt = dt.replace(tzinfo=utc)
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
return dt
|
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
|
# 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 --
|
# 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
|
# and Django expects time, so we still need to override that. We also need to
|
||||||
|
@ -66,7 +80,8 @@ django_conversions.update({
|
||||||
FIELD_TYPE.TIME: util.typecast_time,
|
FIELD_TYPE.TIME: util.typecast_time,
|
||||||
FIELD_TYPE.DECIMAL: util.typecast_decimal,
|
FIELD_TYPE.DECIMAL: util.typecast_decimal,
|
||||||
FIELD_TYPE.NEWDECIMAL: 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
|
# This should match the numerical portion of the version numbers (we can treat
|
||||||
|
@ -268,9 +283,9 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# MySQL doesn't support tz-aware datetimes
|
# MySQL doesn't support tz-aware datetimes
|
||||||
if is_aware(value):
|
if timezone.is_aware(value):
|
||||||
if settings.USE_TZ:
|
if settings.USE_TZ:
|
||||||
value = value.astimezone(utc).replace(tzinfo=None)
|
value = value.astimezone(timezone.utc).replace(tzinfo=None)
|
||||||
else:
|
else:
|
||||||
raise ValueError("MySQL backend does not support timezone-aware datetimes when USE_TZ is False.")
|
raise ValueError("MySQL backend does not support timezone-aware datetimes when USE_TZ is False.")
|
||||||
|
|
||||||
|
@ -282,7 +297,7 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# MySQL doesn't support tz-aware times
|
# 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.")
|
raise ValueError("MySQL backend does not support timezone-aware times.")
|
||||||
|
|
||||||
# MySQL doesn't support microseconds
|
# MySQL doesn't support microseconds
|
||||||
|
|
|
@ -52,7 +52,7 @@ from django.db.backends.oracle.client import DatabaseClient
|
||||||
from django.db.backends.oracle.creation import DatabaseCreation
|
from django.db.backends.oracle.creation import DatabaseCreation
|
||||||
from django.db.backends.oracle.introspection import DatabaseIntrospection
|
from django.db.backends.oracle.introspection import DatabaseIntrospection
|
||||||
from django.utils.encoding import smart_str, force_unicode
|
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
|
DatabaseError = Database.DatabaseError
|
||||||
IntegrityError = Database.IntegrityError
|
IntegrityError = Database.IntegrityError
|
||||||
|
@ -339,9 +339,9 @@ WHEN (new.%(col_name)s IS NULL)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Oracle doesn't support tz-aware datetimes
|
# Oracle doesn't support tz-aware datetimes
|
||||||
if is_aware(value):
|
if timezone.is_aware(value):
|
||||||
if settings.USE_TZ:
|
if settings.USE_TZ:
|
||||||
value = value.astimezone(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.")
|
||||||
|
|
||||||
|
@ -355,7 +355,7 @@ WHEN (new.%(col_name)s IS NULL)
|
||||||
return datetime.datetime.strptime(value, '%H:%M:%S')
|
return datetime.datetime.strptime(value, '%H:%M:%S')
|
||||||
|
|
||||||
# Oracle doesn't support tz-aware times
|
# 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.")
|
raise ValueError("Oracle backend does not support timezone-aware times.")
|
||||||
|
|
||||||
return datetime.datetime(1900, 1, 1, value.hour, value.minute,
|
return datetime.datetime(1900, 1, 1, value.hour, value.minute,
|
||||||
|
@ -561,6 +561,17 @@ 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
|
||||||
|
# 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'):
|
if hasattr(param, 'bind_parameter'):
|
||||||
self.smart_str = param.bind_parameter(cursor)
|
self.smart_str = param.bind_parameter(cursor)
|
||||||
else:
|
else:
|
||||||
|
@ -783,8 +794,8 @@ def _rowfactory(row, cursor):
|
||||||
# of "dates" queries, which are returned as DATETIME.
|
# of "dates" queries, which are returned as DATETIME.
|
||||||
elif desc[1] in (Database.TIMESTAMP, Database.DATETIME):
|
elif desc[1] in (Database.TIMESTAMP, Database.DATETIME):
|
||||||
# Confirm that dt is naive before overwriting its tzinfo.
|
# Confirm that dt is naive before overwriting its tzinfo.
|
||||||
if settings.USE_TZ and value is not None and is_naive(value):
|
if settings.USE_TZ and value is not None and timezone.is_naive(value):
|
||||||
value = value.replace(tzinfo=utc)
|
value = value.replace(tzinfo=timezone.utc)
|
||||||
elif desc[1] in (Database.STRING, Database.FIXED_CHAR,
|
elif desc[1] in (Database.STRING, Database.FIXED_CHAR,
|
||||||
Database.LONG_STRING):
|
Database.LONG_STRING):
|
||||||
value = to_unicode(value)
|
value = to_unicode(value)
|
||||||
|
|
|
@ -19,7 +19,7 @@ from django.db.backends.sqlite3.creation import DatabaseCreation
|
||||||
from django.db.backends.sqlite3.introspection import DatabaseIntrospection
|
from django.db.backends.sqlite3.introspection import DatabaseIntrospection
|
||||||
from django.utils.dateparse import parse_date, parse_datetime, parse_time
|
from django.utils.dateparse import parse_date, parse_datetime, parse_time
|
||||||
from django.utils.safestring import SafeString
|
from django.utils.safestring import SafeString
|
||||||
from django.utils.timezone import is_aware, is_naive, utc
|
from django.utils import timezone
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
|
@ -37,10 +37,22 @@ IntegrityError = Database.IntegrityError
|
||||||
def parse_datetime_with_timezone_support(value):
|
def parse_datetime_with_timezone_support(value):
|
||||||
dt = parse_datetime(value)
|
dt = parse_datetime(value)
|
||||||
# Confirm that dt is naive before overwriting its tzinfo.
|
# Confirm that dt is naive before overwriting its tzinfo.
|
||||||
if dt is not None and settings.USE_TZ and is_naive(dt):
|
if dt is not None and settings.USE_TZ and timezone.is_naive(dt):
|
||||||
dt = dt.replace(tzinfo=utc)
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
return dt
|
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("bool", lambda s: str(s) == '1')
|
||||||
Database.register_converter("time", parse_time)
|
Database.register_converter("time", parse_time)
|
||||||
Database.register_converter("date", parse_date)
|
Database.register_converter("date", parse_date)
|
||||||
|
@ -48,13 +60,14 @@ 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("TIMESTAMP", parse_datetime_with_timezone_support)
|
Database.register_converter("TIMESTAMP", parse_datetime_with_timezone_support)
|
||||||
Database.register_converter("decimal", util.typecast_decimal)
|
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)
|
Database.register_adapter(decimal.Decimal, util.rev_typecast_decimal)
|
||||||
if Database.version_info >= (2, 4, 1):
|
if Database.version_info >= (2, 4, 1):
|
||||||
# Starting in 2.4.1, the str type is not accepted anymore, therefore,
|
# Starting in 2.4.1, the str type is not accepted anymore, therefore,
|
||||||
# we convert all str objects to Unicode
|
# we convert all str objects to Unicode
|
||||||
# As registering a adapter for a primitive type causes a small
|
# As registering a adapter for a primitive type causes a small
|
||||||
# slow-down, this adapter is only registered for sqlite3 versions
|
# 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(str, lambda s: s.decode('utf-8'))
|
||||||
Database.register_adapter(SafeString, lambda s: s.decode('utf-8'))
|
Database.register_adapter(SafeString, lambda s: s.decode('utf-8'))
|
||||||
|
|
||||||
|
@ -147,9 +160,9 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# SQLite doesn't support tz-aware datetimes
|
# SQLite doesn't support tz-aware datetimes
|
||||||
if is_aware(value):
|
if timezone.is_aware(value):
|
||||||
if settings.USE_TZ:
|
if settings.USE_TZ:
|
||||||
value = value.astimezone(utc).replace(tzinfo=None)
|
value = value.astimezone(timezone.utc).replace(tzinfo=None)
|
||||||
else:
|
else:
|
||||||
raise ValueError("SQLite backend does not support timezone-aware datetimes when USE_TZ is False.")
|
raise ValueError("SQLite backend does not support timezone-aware datetimes when USE_TZ is False.")
|
||||||
|
|
||||||
|
@ -160,7 +173,7 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# SQLite doesn't support tz-aware datetimes
|
# 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.")
|
raise ValueError("SQLite backend does not support timezone-aware times.")
|
||||||
|
|
||||||
return unicode(value)
|
return unicode(value)
|
||||||
|
|
|
@ -263,6 +263,15 @@ class LegacyDatabaseTests(BaseDateTimeTests):
|
||||||
self.assertQuerysetEqual(Event.objects.dates('dt', 'day'),
|
self.assertQuerysetEqual(Event.objects.dates('dt', 'day'),
|
||||||
[datetime.datetime(2011, 1, 1)], transform=lambda d: d)
|
[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)
|
LegacyDatabaseTests = override_settings(USE_TZ=False)(LegacyDatabaseTests)
|
||||||
|
|
||||||
|
|
||||||
|
@ -473,6 +482,15 @@ class NewDatabaseTests(BaseDateTimeTests):
|
||||||
datetime.datetime(2011, 1, 1, tzinfo=UTC)],
|
datetime.datetime(2011, 1, 1, tzinfo=UTC)],
|
||||||
transform=lambda d: d)
|
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):
|
def test_null_datetime(self):
|
||||||
# Regression for #17294
|
# Regression for #17294
|
||||||
e = MaybeEvent.objects.create()
|
e = MaybeEvent.objects.create()
|
||||||
|
|
Loading…
Reference in New Issue