diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index 7926c05c09..126f30b462 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -332,6 +332,9 @@ class DatabaseWrapper(BaseDatabaseWrapper): yield cursor finally: conn.close() + break + else: + raise @cached_property def pg_version(self): diff --git a/docs/releases/3.1.7.txt b/docs/releases/3.1.7.txt index f6be18b0e8..1ef16b76c7 100644 --- a/docs/releases/3.1.7.txt +++ b/docs/releases/3.1.7.txt @@ -9,4 +9,6 @@ Django 3.1.7 fixes several bugs in 3.1.6. Bugfixes ======== -* ... +* Fixed a regression in Django 3.1 that caused ``RuntimeError`` instead of + connection errors when using only the ``'postgres'`` database + (:ticket:`32403`). diff --git a/tests/backends/postgresql/tests.py b/tests/backends/postgresql/tests.py index 94ebfbdaf8..7f27c69ffb 100644 --- a/tests/backends/postgresql/tests.py +++ b/tests/backends/postgresql/tests.py @@ -1,9 +1,10 @@ +import copy import unittest from io import StringIO from unittest import mock from django.core.exceptions import ImproperlyConfigured -from django.db import DatabaseError, connection, connections +from django.db import DEFAULT_DB_ALIAS, DatabaseError, connection, connections from django.db.backends.base.base import BaseDatabaseWrapper from django.test import TestCase, override_settings @@ -54,6 +55,53 @@ class Tests(TestCase): self.assertIsNone(cursor.db.connection) self.assertIsNotNone(cursor.db.settings_dict['NAME']) self.assertEqual(cursor.db.settings_dict['NAME'], connections['other'].settings_dict['NAME']) + # Cursor is yielded only for the first PostgreSQL database. + with self.assertWarnsMessage(RuntimeWarning, msg): + with mock.patch( + 'django.db.backends.base.base.BaseDatabaseWrapper.connect', + side_effect=mocked_connect, + autospec=True, + ): + with connection._nodb_cursor() as cursor: + self.assertIs(cursor.closed, False) + self.assertIsNotNone(cursor.db.connection) + + def test_nodb_cursor_raises_postgres_authentication_failure(self): + """ + _nodb_cursor() re-raises authentication failure to the 'postgres' db + when other connection to the PostgreSQL database isn't available. + """ + def mocked_connect(self): + raise DatabaseError() + + def mocked_all(self): + test_connection = copy.copy(connections[DEFAULT_DB_ALIAS]) + test_connection.settings_dict = copy.deepcopy(connection.settings_dict) + test_connection.settings_dict['NAME'] = 'postgres' + return [test_connection] + + msg = ( + "Normally Django will use a connection to the 'postgres' database " + "to avoid running initialization queries against the production " + "database when it's not needed (for example, when running tests). " + "Django was unable to create a connection to the 'postgres' " + "database and will use the first PostgreSQL database instead." + ) + with self.assertWarnsMessage(RuntimeWarning, msg): + mocker_connections_all = mock.patch( + 'django.utils.connection.BaseConnectionHandler.all', + side_effect=mocked_all, + autospec=True, + ) + mocker_connect = mock.patch( + 'django.db.backends.base.base.BaseDatabaseWrapper.connect', + side_effect=mocked_connect, + autospec=True, + ) + with mocker_connections_all, mocker_connect: + with self.assertRaises(DatabaseError): + with connection._nodb_cursor(): + pass def test_database_name_too_long(self): from django.db.backends.postgresql.base import DatabaseWrapper