Fixed #23524 -- Allowed DATABASES['TIME_ZONE'] option on PostgreSQL.

This commit is contained in:
Aymeric Augustin 2019-07-20 15:38:43 +02:00 committed by Mariusz Felisiak
parent ad88524e4d
commit c06492dd87
7 changed files with 73 additions and 61 deletions

View File

@ -124,11 +124,11 @@ class BaseDatabaseWrapper:
When the database backend supports time zones, it doesn't matter which When the database backend supports time zones, it doesn't matter which
time zone Django uses, as long as aware datetimes are used everywhere. time zone Django uses, as long as aware datetimes are used everywhere.
For simplicity, Django selects UTC. Other users connecting to the database can choose their own time zone.
When the database backend doesn't support time zones, the time zone When the database backend doesn't support time zones, the time zone
Django uses can be selected with the TIME_ZONE configuration option, so Django uses may be constrained by the requirements of other users of
it can match what other users of the database expect. the database.
""" """
if not settings.USE_TZ: if not settings.USE_TZ:
return None return None
@ -205,15 +205,11 @@ class BaseDatabaseWrapper:
self.run_on_commit = [] self.run_on_commit = []
def check_settings(self): def check_settings(self):
if self.settings_dict['TIME_ZONE'] is not None: if self.settings_dict['TIME_ZONE'] is not None and not settings.USE_TZ:
if not settings.USE_TZ:
raise ImproperlyConfigured( raise ImproperlyConfigured(
"Connection '%s' cannot set TIME_ZONE because USE_TZ is " "Connection '%s' cannot set TIME_ZONE because USE_TZ is False."
"False." % self.alias) % self.alias
elif self.features.supports_timezones: )
raise ImproperlyConfigured(
"Connection '%s' cannot set TIME_ZONE because its engine "
"handles time zones conversions natively." % self.alias)
@async_unsafe @async_unsafe
def ensure_connection(self): def ensure_connection(self):

View File

@ -47,7 +47,6 @@ from .features import DatabaseFeatures # NOQA isort:skip
from .introspection import DatabaseIntrospection # NOQA isort:skip from .introspection import DatabaseIntrospection # NOQA isort:skip
from .operations import DatabaseOperations # NOQA isort:skip from .operations import DatabaseOperations # NOQA isort:skip
from .schema import DatabaseSchemaEditor # NOQA isort:skip from .schema import DatabaseSchemaEditor # NOQA isort:skip
from .utils import utc_tzinfo_factory # NOQA isort:skip
psycopg2.extensions.register_adapter(SafeString, psycopg2.extensions.QuotedString) psycopg2.extensions.register_adapter(SafeString, psycopg2.extensions.QuotedString)
psycopg2.extras.register_uuid() psycopg2.extras.register_uuid()
@ -231,9 +230,12 @@ class DatabaseWrapper(BaseDatabaseWrapper):
cursor = self.connection.cursor(name, scrollable=False, withhold=self.connection.autocommit) cursor = self.connection.cursor(name, scrollable=False, withhold=self.connection.autocommit)
else: else:
cursor = self.connection.cursor() cursor = self.connection.cursor()
cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None cursor.tzinfo_factory = self.tzinfo_factory if settings.USE_TZ else None
return cursor return cursor
def tzinfo_factory(self, offset):
return self.timezone
@async_unsafe @async_unsafe
def chunked_cursor(self): def chunked_cursor(self):
self._named_cursor_idx += 1 self._named_cursor_idx += 1

View File

@ -1,7 +0,0 @@
from django.utils.timezone import utc
def utc_tzinfo_factory(offset):
if offset != 0:
raise AssertionError("database connection isn't set to UTC")
return utc

View File

@ -121,8 +121,11 @@ Django needs the following parameters for its database connections:
- ``client_encoding``: ``'UTF8'``, - ``client_encoding``: ``'UTF8'``,
- ``default_transaction_isolation``: ``'read committed'`` by default, - ``default_transaction_isolation``: ``'read committed'`` by default,
or the value set in the connection options (see below), or the value set in the connection options (see below),
- ``timezone``: ``'UTC'`` when :setting:`USE_TZ` is ``True``, value of - ``timezone``:
:setting:`TIME_ZONE` otherwise. - when :setting:`USE_TZ` is ``True``, ``'UTC'`` by default, or the
:setting:`TIME_ZONE <DATABASE-TIME_ZONE>` value set for the connection,
- when :setting:`USE_TZ` is ``False``, the value of the global
:setting:`TIME_ZONE` setting.
If these parameters already have the correct values, Django won't set them for If these parameters already have the correct values, Django won't set them for
every new connection, which improves performance slightly. You can configure every new connection, which improves performance slightly. You can configure

View File

@ -632,24 +632,53 @@ default port. Not used with SQLite.
Default: ``None`` Default: ``None``
A string representing the time zone for datetimes stored in this database A string representing the time zone for this database connection or ``None``.
(assuming that it doesn't support time zones) or ``None``. This inner option of This inner option of the :setting:`DATABASES` setting accepts the same values
the :setting:`DATABASES` setting accepts the same values as the general as the general :setting:`TIME_ZONE` setting.
:setting:`TIME_ZONE` setting.
This allows interacting with third-party databases that store datetimes in
local time rather than UTC. To avoid issues around DST changes, you shouldn't
set this option for databases managed by Django.
When :setting:`USE_TZ` is ``True`` and the database doesn't support time zones
(e.g. SQLite, MySQL, Oracle), Django reads and writes datetimes in local time
according to this option if it is set and in UTC if it isn't.
When :setting:`USE_TZ` is ``True`` and the database supports time zones (e.g.
PostgreSQL), it is an error to set this option.
When :setting:`USE_TZ` is ``True`` and this option is set, reading datetimes
from the database returns aware datetimes in this time zone instead of UTC.
When :setting:`USE_TZ` is ``False``, it is an error to set this option. When :setting:`USE_TZ` is ``False``, it is an error to set this option.
* If the database backend doesn't support time zones (e.g. SQLite, MySQL,
Oracle), Django reads and writes datetimes in local time according to this
option if it is set and in UTC if it isn't.
Changing the connection time zone changes how datetimes are read from and
written to the database.
* If Django manages the database and you don't have a strong reason to do
otherwise, you should leave this option unset. It's best to store datetimes
in UTC because it avoids ambiguous or nonexistent datetimes during daylight
saving time changes. Also, receiving datetimes in UTC keeps datetime
arithmetic simple — there's no need for the ``normalize()`` method provided
by pytz.
* If you're connecting to a third-party database that stores datetimes in a
local time rather than UTC, then you must set this option to the
appropriate time zone. Likewise, if Django manages the database but
third-party systems connect to the same database and expect to find
datetimes in local time, then you must set this option.
* If the database backend supports time zones (e.g. PostgreSQL), the
``TIME_ZONE`` option is very rarely needed. It can be changed at any time;
the database takes care of converting datetimes to the desired time zone.
Setting the time zone of the database connection may be useful for running
raw SQL queries involving date/time functions provided by the database, such
as ``date_trunc``, because their results depend on the time zone.
However, this has a downside: receiving all datetimes in local time makes
datetime arithmetic more tricky — you must call the ``normalize()`` method
provided by pytz after each operation.
Consider converting to local time explicitly with ``AT TIME ZONE`` in raw SQL
queries instead of setting the ``TIME_ZONE`` option.
.. versionchanged:: 3.1
Using this option when the database backend supports time zones was allowed.
.. setting:: DATABASE-DISABLE_SERVER_SIDE_CURSORS .. setting:: DATABASE-DISABLE_SERVER_SIDE_CURSORS
``DISABLE_SERVER_SIDE_CURSORS`` ``DISABLE_SERVER_SIDE_CURSORS``

View File

@ -283,6 +283,9 @@ Miscellaneous
:class:`pathlib.Path` instead of :mod:`os.path` for building filesystem :class:`pathlib.Path` instead of :mod:`os.path` for building filesystem
paths. paths.
* The :setting:`TIME_ZONE <DATABASE-TIME_ZONE>` setting is now allowed on
databases that support time zones.
.. _backwards-incompatible-3.1: .. _backwards-incompatible-3.1:
Backwards incompatible changes in 3.1 Backwards incompatible changes in 3.1

View File

@ -9,8 +9,7 @@ import pytz
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import serializers from django.core import serializers
from django.core.exceptions import ImproperlyConfigured from django.db import connection
from django.db import connection, connections
from django.db.models import F, Max, Min from django.db.models import F, Max, Min
from django.http import HttpRequest from django.http import HttpRequest
from django.template import ( from django.template import (
@ -532,6 +531,14 @@ class NewDatabaseTests(TestCase):
cursor.execute('SELECT dt FROM timezones_event WHERE dt = %s', [utc_naive_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)
@skipUnlessDBFeature('supports_timezones')
def test_cursor_explicit_time_zone(self):
with override_database_connection_timezone('Europe/Paris'):
with connection.cursor() as cursor:
cursor.execute('SELECT CURRENT_TIMESTAMP')
now = cursor.fetchone()[0]
self.assertEqual(now.tzinfo.zone, 'Europe/Paris')
@requires_tz_support @requires_tz_support
def test_filter_date_field_with_aware_datetime(self): def test_filter_date_field_with_aware_datetime(self):
# Regression test for #17742 # Regression test for #17742
@ -595,27 +602,6 @@ class ForcedTimeZoneDatabaseTests(TransactionTestCase):
self.assertEqual(event.dt, fake_dt) self.assertEqual(event.dt, fake_dt)
@skipUnlessDBFeature('supports_timezones')
@override_settings(TIME_ZONE='Africa/Nairobi', USE_TZ=True)
class UnsupportedTimeZoneDatabaseTests(TestCase):
def test_time_zone_parameter_not_supported_if_database_supports_timezone(self):
connections.databases['tz'] = connections.databases['default'].copy()
connections.databases['tz']['TIME_ZONE'] = 'Asia/Bangkok'
tz_conn = connections['tz']
try:
msg = (
"Connection 'tz' cannot set TIME_ZONE because its engine "
"handles time zones conversions natively."
)
with self.assertRaisesMessage(ImproperlyConfigured, msg):
tz_conn.cursor()
finally:
connections['tz'].close() # in case the test fails
del connections['tz']
del connections.databases['tz']
@override_settings(TIME_ZONE='Africa/Nairobi') @override_settings(TIME_ZONE='Africa/Nairobi')
class SerializationTests(SimpleTestCase): class SerializationTests(SimpleTestCase):