Removed global timezone-aware datetime adapters.
Refs #23820. Fixed #19738. Refs #17755. In order not to introduce a regression for raw queries, parameters are passed through the connection.ops.value_to_db_* methods, depending on their type.
This commit is contained in:
parent
fe6b5e62b1
commit
d9521f66b1
|
@ -433,6 +433,25 @@ class BaseDatabaseOperations(object):
|
||||||
"""
|
"""
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def value_to_db_unknown(self, value):
|
||||||
|
"""
|
||||||
|
Transforms a value to something compatible with the backend driver.
|
||||||
|
|
||||||
|
This method only depends on the type of the value. It's designed for
|
||||||
|
cases where the target type isn't known, such as .raw() SQL queries.
|
||||||
|
As a consequence it may not work perfectly in all circumstances.
|
||||||
|
"""
|
||||||
|
if isinstance(value, datetime.datetime): # must be before date
|
||||||
|
return self.value_to_db_datetime(value)
|
||||||
|
elif isinstance(value, datetime.date):
|
||||||
|
return self.value_to_db_date(value)
|
||||||
|
elif isinstance(value, datetime.time):
|
||||||
|
return self.value_to_db_time(value)
|
||||||
|
elif isinstance(value, decimal.Decimal):
|
||||||
|
return self.value_to_db_decimal(value)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
def value_to_db_date(self, value):
|
def value_to_db_date(self, value):
|
||||||
"""
|
"""
|
||||||
Transforms a date value to an object compatible with what is expected
|
Transforms a date value to an object compatible with what is expected
|
||||||
|
@ -486,6 +505,8 @@ class BaseDatabaseOperations(object):
|
||||||
"""
|
"""
|
||||||
first = datetime.date(value, 1, 1)
|
first = datetime.date(value, 1, 1)
|
||||||
second = datetime.date(value, 12, 31)
|
second = datetime.date(value, 12, 31)
|
||||||
|
first = self.value_to_db_date(first)
|
||||||
|
second = self.value_to_db_date(second)
|
||||||
return [first, second]
|
return [first, second]
|
||||||
|
|
||||||
def year_lookup_bounds_for_datetime_field(self, value):
|
def year_lookup_bounds_for_datetime_field(self, value):
|
||||||
|
@ -502,6 +523,8 @@ class BaseDatabaseOperations(object):
|
||||||
tz = timezone.get_current_timezone()
|
tz = timezone.get_current_timezone()
|
||||||
first = timezone.make_aware(first, tz)
|
first = timezone.make_aware(first, tz)
|
||||||
second = timezone.make_aware(second, tz)
|
second = timezone.make_aware(second, tz)
|
||||||
|
first = self.value_to_db_datetime(first)
|
||||||
|
second = self.value_to_db_datetime(second)
|
||||||
return [first, second]
|
return [first, second]
|
||||||
|
|
||||||
def get_db_converters(self, expression):
|
def get_db_converters(self, expression):
|
||||||
|
|
|
@ -16,6 +16,7 @@ from django.db import utils
|
||||||
from django.db.backends import utils as backend_utils
|
from django.db.backends import utils as backend_utils
|
||||||
from django.db.backends.base.base import BaseDatabaseWrapper
|
from django.db.backends.base.base import BaseDatabaseWrapper
|
||||||
from django.utils import six, timezone
|
from django.utils import six, timezone
|
||||||
|
from django.utils.deprecation import RemovedInDjango21Warning
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.safestring import SafeBytes, SafeText
|
from django.utils.safestring import SafeBytes, SafeText
|
||||||
|
@ -52,15 +53,14 @@ DatabaseError = Database.DatabaseError
|
||||||
IntegrityError = Database.IntegrityError
|
IntegrityError = Database.IntegrityError
|
||||||
|
|
||||||
|
|
||||||
def adapt_datetime_with_timezone_support(value, conv):
|
def adapt_datetime_warn_on_aware_datetime(value, conv):
|
||||||
# Equivalent to DateTimeField.get_db_prep_value. Used only by raw SQL.
|
# Remove this function and rely on the default adapter in Django 2.1.
|
||||||
if settings.USE_TZ:
|
if settings.USE_TZ and timezone.is_aware(value):
|
||||||
if timezone.is_naive(value):
|
warnings.warn(
|
||||||
warnings.warn("MySQL received a naive datetime (%s)"
|
"The MySQL database adapter received an aware datetime (%s), "
|
||||||
" while time zone support is active." % value,
|
"probably from cursor.execute(). Update your code to pass a "
|
||||||
RuntimeWarning)
|
"naive datetime in the database connection's time zone (UTC by "
|
||||||
default_timezone = timezone.get_default_timezone()
|
"default).", RemovedInDjango21Warning)
|
||||||
value = timezone.make_aware(value, default_timezone)
|
|
||||||
value = value.astimezone(timezone.utc).replace(tzinfo=None)
|
value = value.astimezone(timezone.utc).replace(tzinfo=None)
|
||||||
return Thing2Literal(value.strftime("%Y-%m-%d %H:%M:%S.%f"), conv)
|
return Thing2Literal(value.strftime("%Y-%m-%d %H:%M:%S.%f"), conv)
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ django_conversions.update({
|
||||||
FIELD_TYPE.TIME: backend_utils.typecast_time,
|
FIELD_TYPE.TIME: backend_utils.typecast_time,
|
||||||
FIELD_TYPE.DECIMAL: backend_utils.typecast_decimal,
|
FIELD_TYPE.DECIMAL: backend_utils.typecast_decimal,
|
||||||
FIELD_TYPE.NEWDECIMAL: backend_utils.typecast_decimal,
|
FIELD_TYPE.NEWDECIMAL: backend_utils.typecast_decimal,
|
||||||
datetime.datetime: adapt_datetime_with_timezone_support,
|
datetime.datetime: adapt_datetime_warn_on_aware_datetime,
|
||||||
})
|
})
|
||||||
|
|
||||||
# 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
|
||||||
|
|
|
@ -17,6 +17,7 @@ from django.db import utils
|
||||||
from django.db.backends.base.base import BaseDatabaseWrapper
|
from django.db.backends.base.base import BaseDatabaseWrapper
|
||||||
from django.db.backends.base.validation import BaseDatabaseValidation
|
from django.db.backends.base.validation import BaseDatabaseValidation
|
||||||
from django.utils import six, timezone
|
from django.utils import six, timezone
|
||||||
|
from django.utils.deprecation import RemovedInDjango21Warning
|
||||||
from django.utils.duration import duration_string
|
from django.utils.duration import duration_string
|
||||||
from django.utils.encoding import force_bytes, force_text
|
from django.utils.encoding import force_bytes, force_text
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
@ -336,13 +337,14 @@ class OracleParam(object):
|
||||||
# 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) and
|
if settings.USE_TZ and (isinstance(param, datetime.datetime) and
|
||||||
not isinstance(param, Oracle_datetime)):
|
not isinstance(param, Oracle_datetime)):
|
||||||
if timezone.is_naive(param):
|
if timezone.is_aware(param):
|
||||||
warnings.warn("Oracle received a naive datetime (%s)"
|
warnings.warn(
|
||||||
" while time zone support is active." % param,
|
"The Oracle database adapter received an aware datetime (%s), "
|
||||||
RuntimeWarning)
|
"probably from cursor.execute(). Update your code to pass a "
|
||||||
default_timezone = timezone.get_default_timezone()
|
"naive datetime in the database connection's time zone (UTC by "
|
||||||
param = timezone.make_aware(param, default_timezone)
|
"default).", RemovedInDjango21Warning)
|
||||||
param = Oracle_datetime.from_datetime(param.astimezone(timezone.utc))
|
param = param.astimezone(timezone.utc).replace(tzinfo=None)
|
||||||
|
param = Oracle_datetime.from_datetime(param)
|
||||||
|
|
||||||
if isinstance(param, datetime.timedelta):
|
if isinstance(param, datetime.timedelta):
|
||||||
param = duration_string(param)
|
param = duration_string(param)
|
||||||
|
|
|
@ -419,19 +419,6 @@ WHEN (new.%(col_name)s IS NULL)
|
||||||
return Oracle_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):
|
|
||||||
# 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):
|
|
||||||
# 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) 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"
|
||||||
if connector == '%%':
|
if connector == '%%':
|
||||||
|
|
|
@ -20,6 +20,7 @@ from django.utils import six, timezone
|
||||||
from django.utils.dateparse import (
|
from django.utils.dateparse import (
|
||||||
parse_date, parse_datetime, parse_duration, parse_time,
|
parse_date, parse_datetime, parse_duration, parse_time,
|
||||||
)
|
)
|
||||||
|
from django.utils.deprecation import RemovedInDjango21Warning
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.safestring import SafeBytes
|
from django.utils.safestring import SafeBytes
|
||||||
|
|
||||||
|
@ -49,15 +50,14 @@ DatabaseError = Database.DatabaseError
|
||||||
IntegrityError = Database.IntegrityError
|
IntegrityError = Database.IntegrityError
|
||||||
|
|
||||||
|
|
||||||
def adapt_datetime_with_timezone_support(value):
|
def adapt_datetime_warn_on_aware_datetime(value):
|
||||||
# Equivalent to DateTimeField.get_db_prep_value. Used only by raw SQL.
|
# Remove this function and rely on the default adapter in Django 2.1.
|
||||||
if settings.USE_TZ:
|
if settings.USE_TZ and timezone.is_aware(value):
|
||||||
if timezone.is_naive(value):
|
warnings.warn(
|
||||||
warnings.warn("SQLite received a naive datetime (%s)"
|
"The SQLite database adapter received an aware datetime (%s), "
|
||||||
" while time zone support is active." % value,
|
"probably from cursor.execute(). Update your code to pass a "
|
||||||
RuntimeWarning)
|
"naive datetime in the database connection's time zone (UTC by "
|
||||||
default_timezone = timezone.get_default_timezone()
|
"default).", RemovedInDjango21Warning)
|
||||||
value = timezone.make_aware(value, default_timezone)
|
|
||||||
value = value.astimezone(timezone.utc).replace(tzinfo=None)
|
value = value.astimezone(timezone.utc).replace(tzinfo=None)
|
||||||
return value.isoformat(str(" "))
|
return value.isoformat(str(" "))
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ Database.register_converter(str("timestamp"), decoder(parse_datetime))
|
||||||
Database.register_converter(str("TIMESTAMP"), decoder(parse_datetime))
|
Database.register_converter(str("TIMESTAMP"), decoder(parse_datetime))
|
||||||
Database.register_converter(str("decimal"), decoder(backend_utils.typecast_decimal))
|
Database.register_converter(str("decimal"), decoder(backend_utils.typecast_decimal))
|
||||||
|
|
||||||
Database.register_adapter(datetime.datetime, adapt_datetime_with_timezone_support)
|
Database.register_adapter(datetime.datetime, adapt_datetime_warn_on_aware_datetime)
|
||||||
Database.register_adapter(decimal.Decimal, backend_utils.rev_typecast_decimal)
|
Database.register_adapter(decimal.Decimal, backend_utils.rev_typecast_decimal)
|
||||||
if six.PY2:
|
if six.PY2:
|
||||||
Database.register_adapter(str, lambda s: s.decode('utf-8'))
|
Database.register_adapter(str, lambda s: s.decode('utf-8'))
|
||||||
|
|
|
@ -86,13 +86,29 @@ class RawQuery(object):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<RawQuery: %s>" % self
|
return "<RawQuery: %s>" % self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def params_type(self):
|
||||||
|
return dict if isinstance(self.params, Mapping) else tuple
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
_type = dict if isinstance(self.params, Mapping) else tuple
|
return self.sql % self.params_type(self.params)
|
||||||
return self.sql % _type(self.params)
|
|
||||||
|
|
||||||
def _execute_query(self):
|
def _execute_query(self):
|
||||||
self.cursor = connections[self.using].cursor()
|
connection = connections[self.using]
|
||||||
self.cursor.execute(self.sql, self.params)
|
|
||||||
|
# Adapt parameters to the database, as much as possible considering
|
||||||
|
# that the target type isn't known. See #17755.
|
||||||
|
params_type = self.params_type
|
||||||
|
adapter = connection.ops.value_to_db_unknown
|
||||||
|
if params_type is tuple:
|
||||||
|
params = tuple(adapter(val) for val in self.params)
|
||||||
|
elif params_type is dict:
|
||||||
|
params = dict((key, adapter(val)) for key, val in six.iteritems(self.params))
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Unexpected params type: %s" % params_type)
|
||||||
|
|
||||||
|
self.cursor = connection.cursor()
|
||||||
|
self.cursor.execute(self.sql, params)
|
||||||
|
|
||||||
|
|
||||||
class Query(object):
|
class Query(object):
|
||||||
|
|
|
@ -32,6 +32,11 @@ details on these changes.
|
||||||
|
|
||||||
* ``django.db.models.fields.add_lazy_relation()`` will be removed.
|
* ``django.db.models.fields.add_lazy_relation()`` will be removed.
|
||||||
|
|
||||||
|
* When time zone support is enabled, database backends that don't support time
|
||||||
|
zones won't convert aware datetimes to naive values in UTC anymore when such
|
||||||
|
values are passed as parameters to SQL queries executed outside of the ORM,
|
||||||
|
e.g. with ``cursor.execute()``.
|
||||||
|
|
||||||
* The ``django.contrib.auth.tests.utils.skipIfCustomUser()`` decorator will be
|
* The ``django.contrib.auth.tests.utils.skipIfCustomUser()`` decorator will be
|
||||||
removed.
|
removed.
|
||||||
|
|
||||||
|
|
|
@ -313,11 +313,15 @@ Database backend API
|
||||||
doesn't implement this. You may want to review the implementation on the
|
doesn't implement this. You may want to review the implementation on the
|
||||||
backends that Django includes for reference (:ticket:`24245`).
|
backends that Django includes for reference (:ticket:`24245`).
|
||||||
|
|
||||||
* The recommended way to add time zone information to datetimes fetched from
|
* Registering a global adapter or converter at the level of the DB-API module
|
||||||
databases that don't support time zones is to register a converter for
|
to handle time zone information of :class:`~datetime.datetime` values passed
|
||||||
``DateTimeField``. Do this in ``DatabaseOperations.get_db_converters()``.
|
as query parameters or returned as query results on databases that don't
|
||||||
Registering a global converter at the level of the DB-API module is
|
support time zones is discouraged. It can conflict with other libraries.
|
||||||
discouraged because it can conflict with other libraries.
|
|
||||||
|
The recommended way to add a time zone to :class:`~datetime.datetime` values
|
||||||
|
fetched from the database is to register a converter for ``DateTimeField``
|
||||||
|
in ``DatabaseOperations.get_db_converters()``.
|
||||||
|
|
||||||
|
|
||||||
Default settings that were tuples are now lists
|
Default settings that were tuples are now lists
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -418,20 +422,40 @@ console, for example.
|
||||||
If you are overriding Django's default logging, you should check to see how
|
If you are overriding Django's default logging, you should check to see how
|
||||||
your configuration merges with the new defaults.
|
your configuration merges with the new defaults.
|
||||||
|
|
||||||
Removal of time zone aware global converters for datetimes
|
Removal of time zone aware global adapters and converters for datetimes
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Django no longer registers global converters for returning time zone aware
|
Django no longer registers global adapters and converters for managing time
|
||||||
datetimes in database query results when :setting:`USE_TZ` is ``True``.
|
zone information on :class:`~datetime.datetime` values sent to the database as
|
||||||
Instead the ORM adds suitable time zone information.
|
query parameters or read from the database in query results. This change
|
||||||
|
affects projects that meet all the following conditions:
|
||||||
|
|
||||||
As a consequence, SQL queries executed outside of the ORM, for instance with
|
* The :setting:`USE_TZ` setting is ``True``.
|
||||||
``cursor.execute(query, params)``, now return naive datetimes instead of aware
|
* The database is SQLite, MySQL, Oracle, or a third-party database that
|
||||||
datetimes on databases that do not support time zones: SQLite, MySQL, and
|
doesn't support time zones. In doubt, you can check the value of
|
||||||
Oracle. Since these datetimes are in UTC, you can make them aware as follows::
|
``connection.features.supports_timezones``.
|
||||||
|
* The code queries the database outside of the ORM, typically with
|
||||||
|
``cursor.execute(sql, params)``.
|
||||||
|
|
||||||
|
If you're passing aware :class:`~datetime.datetime` parameters to such
|
||||||
|
queries, you should turn them into naive datetimes in UTC::
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
value = value.replace(tzinfo=timezone.utc)
|
param = timezone.make_naive(param, timezone.utc)
|
||||||
|
|
||||||
|
If you fail to do so, Django 1.9 and 2.0 will perform the conversion like
|
||||||
|
earlier versions but emit a deprecation warning. Django 2.1 won't perform any
|
||||||
|
conversion, which may result in data corruption.
|
||||||
|
|
||||||
|
If you're reading :class:`~datetime.datetime` values from the results, they
|
||||||
|
will be naive instead of aware. You can compensate as follows::
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
value = timezone.make_aware(value, timezone.utc)
|
||||||
|
|
||||||
|
You don't need any of this if you're querying the database through the ORM,
|
||||||
|
even if you're using :meth:`raw() <django.db.models.query.QuerySet.raw>`
|
||||||
|
queries. The ORM takes care of managing time zone information.
|
||||||
|
|
||||||
Miscellaneous
|
Miscellaneous
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
|
@ -266,6 +266,13 @@ class LegacyDatabaseTests(TestCase):
|
||||||
[event],
|
[event],
|
||||||
transform=lambda d: d)
|
transform=lambda d: d)
|
||||||
|
|
||||||
|
def test_cursor_execute_accepts_naive_datetime(self):
|
||||||
|
dt = datetime.datetime(2011, 9, 1, 13, 20, 30)
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute('INSERT INTO timezones_event (dt) VALUES (%s)', [dt])
|
||||||
|
event = Event.objects.get()
|
||||||
|
self.assertEqual(event.dt, dt)
|
||||||
|
|
||||||
def test_cursor_execute_returns_naive_datetime(self):
|
def test_cursor_execute_returns_naive_datetime(self):
|
||||||
dt = datetime.datetime(2011, 9, 1, 13, 20, 30)
|
dt = datetime.datetime(2011, 9, 1, 13, 20, 30)
|
||||||
Event.objects.create(dt=dt)
|
Event.objects.create(dt=dt)
|
||||||
|
@ -564,6 +571,23 @@ class NewDatabaseTests(TestCase):
|
||||||
[event],
|
[event],
|
||||||
transform=lambda d: d)
|
transform=lambda d: d)
|
||||||
|
|
||||||
|
@skipUnlessDBFeature('supports_timezones')
|
||||||
|
def test_cursor_execute_accepts_aware_datetime(self):
|
||||||
|
dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT)
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute('INSERT INTO timezones_event (dt) VALUES (%s)', [dt])
|
||||||
|
event = Event.objects.get()
|
||||||
|
self.assertEqual(event.dt, dt)
|
||||||
|
|
||||||
|
@skipIfDBFeature('supports_timezones')
|
||||||
|
def test_cursor_execute_accepts_naive_datetime(self):
|
||||||
|
dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT)
|
||||||
|
utc_naive_dt = timezone.make_naive(dt, timezone.utc)
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute('INSERT INTO timezones_event (dt) VALUES (%s)', [utc_naive_dt])
|
||||||
|
event = Event.objects.get()
|
||||||
|
self.assertEqual(event.dt, dt)
|
||||||
|
|
||||||
@skipUnlessDBFeature('supports_timezones')
|
@skipUnlessDBFeature('supports_timezones')
|
||||||
def test_cursor_execute_returns_aware_datetime(self):
|
def test_cursor_execute_returns_aware_datetime(self):
|
||||||
dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT)
|
dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT)
|
||||||
|
@ -578,7 +602,7 @@ class NewDatabaseTests(TestCase):
|
||||||
utc_naive_dt = timezone.make_naive(dt, timezone.utc)
|
utc_naive_dt = timezone.make_naive(dt, timezone.utc)
|
||||||
Event.objects.create(dt=dt)
|
Event.objects.create(dt=dt)
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute('SELECT dt FROM timezones_event WHERE dt = %s', [dt])
|
cursor.execute('SELECT dt FROM timezones_event WHERE dt = %s', [utc_naive_dt])
|
||||||
self.assertEqual(cursor.fetchall()[0][0], utc_naive_dt)
|
self.assertEqual(cursor.fetchall()[0][0], utc_naive_dt)
|
||||||
|
|
||||||
@requires_tz_support
|
@requires_tz_support
|
||||||
|
|
Loading…
Reference in New Issue