diff --git a/django/db/backends/postgresql/creation.py b/django/db/backends/postgresql/creation.py index 3efcfc7692..2f26b14e52 100644 --- a/django/db/backends/postgresql/creation.py +++ b/django/db/backends/postgresql/creation.py @@ -5,28 +5,42 @@ from django.db.backends.base.creation import BaseDatabaseCreation class DatabaseCreation(BaseDatabaseCreation): + def _quote_name(self, name): + return self.connection.ops.quote_name(name) + + def _get_database_create_suffix(self, encoding=None, template=None): + suffix = "" + if encoding: + suffix += " ENCODING '{}'".format(encoding) + if template: + suffix += " TEMPLATE {}".format(self._quote_name(template)) + if suffix: + suffix = "WITH" + suffix + return suffix + def sql_table_creation_suffix(self): test_settings = self.connection.settings_dict['TEST'] assert test_settings['COLLATION'] is None, ( "PostgreSQL does not support collation setting at database creation time." ) - if test_settings['CHARSET']: - return "WITH ENCODING '%s'" % test_settings['CHARSET'] - return '' + return self._get_database_create_suffix( + encoding=test_settings['CHARSET'], + template=test_settings.get('TEMPLATE'), + ) def _clone_test_db(self, number, verbosity, keepdb=False): # CREATE DATABASE ... WITH TEMPLATE ... requires closing connections # to the template database. self.connection.close() - qn = self.connection.ops.quote_name source_database_name = self.connection.settings_dict['NAME'] target_database_name = self.get_test_db_clone_settings(number)['NAME'] + suffix = self._get_database_create_suffix(template=source_database_name) + creation_sql = "CREATE DATABASE {} {}".format(self._quote_name(target_database_name), suffix) with self._nodb_connection.cursor() as cursor: try: - cursor.execute("CREATE DATABASE %s WITH TEMPLATE %s" % ( - qn(target_database_name), qn(source_database_name))) + cursor.execute(creation_sql) except Exception as e: if keepdb: return @@ -35,9 +49,8 @@ class DatabaseCreation(BaseDatabaseCreation): print("Destroying old test database for alias %s..." % ( self._get_database_display_str(verbosity, target_database_name), )) - cursor.execute("DROP DATABASE %s" % qn(target_database_name)) - cursor.execute("CREATE DATABASE %s WITH TEMPLATE %s" % ( - qn(target_database_name), qn(source_database_name))) + cursor.execute("DROP DATABASE %s" % self._quote_name(target_database_name)) + cursor.execute(creation_sql) except Exception as e: sys.stderr.write("Got an error cloning the test database: %s\n" % e) sys.exit(2) diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 983b7525c3..257324330d 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -164,6 +164,16 @@ lookups that use the ``LIKE`` operator in their SQL, as is done with the .. _PostgreSQL operator class: http://www.postgresql.org/docs/current/static/indexes-opclass.html +Test database templates +----------------------- + +.. versionadded:: 1.11 + +You can use the :setting:`TEST['TEMPLATE'] ` setting to specify +a `template`_ (e.g. ``'template0'``) from which to create a test database. + +.. _template: https://www.postgresql.org/docs/current/static/sql-createdatabase.html + Speeding up test execution with non-durable settings ---------------------------------------------------- diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 445e504e98..856d9186a8 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -738,6 +738,20 @@ the database state between tests if you don't have transactions). You can set this to ``False`` to speed up creation time if you don't have any test classes with :ref:`serialized_rollback=True `. +.. setting:: TEST_TEMPLATE + +``TEMPLATE`` +^^^^^^^^^^^^ + +.. versionadded:: 1.11 + +This is a PostgreSQL-specific setting. + +The name of a `template`_ (e.g. ``'template0'``) from which to create the test +database. + +.. _template: https://www.postgresql.org/docs/current/static/sql-createdatabase.html + .. setting:: TEST_CREATE ``CREATE_DB`` diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index 802a09e6a9..b9fe8de41d 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -159,6 +159,9 @@ Database backends on PostgreSQL 9.5+ and Oracle to execute queries with ``FOR UPDATE SKIP LOCKED``. +* Added the :setting:`TEST['TEMPLATE'] ` setting to let + PostgreSQL users specify a template for creating the test database. + Email ~~~~~ diff --git a/tests/backends/test_creation.py b/tests/backends/test_creation.py index 519b3f049c..d2a615a7e4 100644 --- a/tests/backends/test_creation.py +++ b/tests/backends/test_creation.py @@ -1,9 +1,12 @@ import copy +import unittest +from contextlib import contextmanager -from django.db import DEFAULT_DB_ALIAS, connections +from django.db import DEFAULT_DB_ALIAS, connection, connections from django.db.backends.base.creation import ( TEST_DATABASE_PREFIX, BaseDatabaseCreation, ) +from django.db.backends.postgresql.creation import DatabaseCreation from django.test import SimpleTestCase @@ -40,3 +43,48 @@ class TestDbSignatureTests(SimpleTestCase): test_connection.settings_dict['TEST'] = {'NAME': test_name} signature = BaseDatabaseCreation(test_connection).test_db_signature() self.assertEqual(signature[3], test_name) + + +@unittest.skipUnless(connection.vendor == 'postgresql', "PostgreSQL-specific tests") +class PostgreSQLDatabaseCreationTests(SimpleTestCase): + + @contextmanager + def changed_test_settings(self, **kwargs): + settings = connection.settings_dict['TEST'] + saved_values = {} + for name in kwargs: + if name in settings: + saved_values[name] = settings[name] + + for name, value in kwargs.items(): + settings[name] = value + try: + yield + finally: + for name, value in kwargs.items(): + if name in saved_values: + settings[name] = saved_values[name] + else: + del settings[name] + + def check_sql_table_creation_suffix(self, settings, expected): + with self.changed_test_settings(**settings): + creation = DatabaseCreation(connection) + suffix = creation.sql_table_creation_suffix() + self.assertEqual(suffix, expected) + + def test_sql_table_creation_suffix_with_none_settings(self): + settings = dict(CHARSET=None, TEMPLATE=None) + self.check_sql_table_creation_suffix(settings, "") + + def test_sql_table_creation_suffix_with_encoding(self): + settings = dict(CHARSET='UTF8') + self.check_sql_table_creation_suffix(settings, "WITH ENCODING 'UTF8'") + + def test_sql_table_creation_suffix_with_template(self): + settings = dict(TEMPLATE='template0') + self.check_sql_table_creation_suffix(settings, 'WITH TEMPLATE "template0"') + + def test_sql_table_creation_suffix_with_encoding_and_template(self): + settings = dict(CHARSET='UTF8', TEMPLATE='template0') + self.check_sql_table_creation_suffix(settings, '''WITH ENCODING 'UTF8' TEMPLATE "template0"''')