Fixed #16969 -- Don't connect to named database when possible

Thanks Andreas Pelme for the report and initial patch, and
Aymeric Augustin, Shai Berger and Tim Graham for the reviews.
This commit is contained in:
Claude Paroz 2013-11-09 09:41:03 +01:00
parent 1830f50493
commit e953c78eeb
3 changed files with 36 additions and 24 deletions

View File

@ -6,6 +6,7 @@ import warnings
from django.conf import settings from django.conf import settings
from django.db.utils import load_backend from django.db.utils import load_backend
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from django.utils.functional import cached_property
from django.utils.six.moves import input from django.utils.six.moves import input
from .utils import truncate_name from .utils import truncate_name
@ -29,6 +30,24 @@ class BaseDatabaseCreation(object):
def __init__(self, connection): def __init__(self, connection):
self.connection = connection self.connection = connection
@cached_property
def _nodb_connection(self):
"""
Alternative connection to be used when there is no need to access
the main database, specifically for test db creation/deletion.
This also prevents the production database from being exposed to
potential child threads while (or after) the test database is destroyed.
Refs #10868, #17786, #16969.
"""
settings_dict = self.connection.settings_dict.copy()
settings_dict['NAME'] = None
backend = load_backend(settings_dict['ENGINE'])
nodb_connection = backend.DatabaseWrapper(
settings_dict,
alias='__no_db__',
allow_thread_sharing=False)
return nodb_connection
@classmethod @classmethod
def _digest(cls, *args): def _digest(cls, *args):
""" """
@ -386,7 +405,7 @@ class BaseDatabaseCreation(object):
qn = self.connection.ops.quote_name qn = self.connection.ops.quote_name
# Create the test database and connect to it. # Create the test database and connect to it.
cursor = self.connection.cursor() cursor = self._nodb_connection.cursor()
try: try:
cursor.execute( cursor.execute(
"CREATE DATABASE %s %s" % (qn(test_database_name), suffix)) "CREATE DATABASE %s %s" % (qn(test_database_name), suffix))
@ -431,18 +450,7 @@ class BaseDatabaseCreation(object):
print("Destroying test database for alias '%s'%s..." % ( print("Destroying test database for alias '%s'%s..." % (
self.connection.alias, test_db_repr)) self.connection.alias, test_db_repr))
# Temporarily use a new connection and a copy of the settings dict. self._destroy_test_db(test_database_name, verbosity)
# This prevents the production database from being exposed to potential
# child threads while (or after) the test database is destroyed.
# Refs #10868 and #17786.
settings_dict = self.connection.settings_dict.copy()
settings_dict['NAME'] = old_database_name
backend = load_backend(settings_dict['ENGINE'])
new_connection = backend.DatabaseWrapper(
settings_dict,
alias='__destroy_test_db__',
allow_thread_sharing=False)
new_connection.creation._destroy_test_db(test_database_name, verbosity)
def _destroy_test_db(self, test_database_name, verbosity): def _destroy_test_db(self, test_database_name, verbosity):
""" """
@ -452,12 +460,11 @@ class BaseDatabaseCreation(object):
# ourselves. Connect to the previous database (not the test database) # ourselves. Connect to the previous database (not the test database)
# to do so, because it's not allowed to delete a database while being # to do so, because it's not allowed to delete a database while being
# connected to it. # connected to it.
cursor = self.connection.cursor() cursor = self._nodb_connection.cursor()
# Wait to avoid "database is being accessed by other users" errors. # Wait to avoid "database is being accessed by other users" errors.
time.sleep(1) time.sleep(1)
cursor.execute("DROP DATABASE %s" cursor.execute("DROP DATABASE %s"
% self.connection.ops.quote_name(test_database_name)) % self.connection.ops.quote_name(test_database_name))
self.connection.close()
def set_autocommit(self): def set_autocommit(self):
""" """

View File

@ -101,13 +101,14 @@ class DatabaseWrapper(BaseDatabaseWrapper):
def get_connection_params(self): def get_connection_params(self):
settings_dict = self.settings_dict settings_dict = self.settings_dict
if not settings_dict['NAME']: # None may be used to connect to the default 'postgres' db
if settings_dict['NAME'] == '':
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
raise ImproperlyConfigured( raise ImproperlyConfigured(
"settings.DATABASES is improperly configured. " "settings.DATABASES is improperly configured. "
"Please supply the NAME value.") "Please supply the NAME value.")
conn_params = { conn_params = {
'database': settings_dict['NAME'], 'database': settings_dict['NAME'] or 'postgres',
} }
conn_params.update(settings_dict['OPTIONS']) conn_params.update(settings_dict['OPTIONS'])
if 'autocommit' in conn_params: if 'autocommit' in conn_params:

View File

@ -76,17 +76,21 @@ If you're using a backend that isn't SQLite, you will need to provide other
details for each database: details for each database:
* The :setting:`USER` option needs to specify an existing user account * The :setting:`USER` option needs to specify an existing user account
for the database. for the database. That user needs permission to execute ``CREATE DATABASE``
so that the test database can be created.
* The :setting:`PASSWORD` option needs to provide the password for * The :setting:`PASSWORD` option needs to provide the password for
the :setting:`USER` that has been specified. the :setting:`USER` that has been specified.
* The :setting:`NAME` option must be the name of an existing database to Test databases get their names by prepending ``test_`` to the value of the
which the given user has permission to connect. The unit tests will not :setting:`NAME` settings for the databases defined in :setting:`DATABASES`.
touch this database; the test runner creates a new database whose name These test databases are deleted when the tests are finished.
is :setting:`NAME` prefixed with ``test_``, and this test database is
deleted when the tests are finished. This means your user account needs .. versionchanged:: 1.7
permission to execute ``CREATE DATABASE``.
Before Django 1.7, the :setting:`NAME` setting was mandatory and had to
be the name of an existing database to which the given user had permission
to connect.
You will also need to ensure that your database uses UTF-8 as the default You will also need to ensure that your database uses UTF-8 as the default
character set. If your database server doesn't use UTF-8 as a default charset, character set. If your database server doesn't use UTF-8 as a default charset,