diff --git a/django/contrib/gis/db/backends/spatialite/creation.py b/django/contrib/gis/db/backends/spatialite/creation.py index 7efab3e073..31f2fca1bd 100644 --- a/django/contrib/gis/db/backends/spatialite/creation.py +++ b/django/contrib/gis/db/backends/spatialite/creation.py @@ -31,9 +31,6 @@ class SpatiaLiteCreation(DatabaseCreation): self.connection.close() self.connection.settings_dict["NAME"] = test_database_name - # Confirm the feature set of the test database - self.connection.features.confirm() - # Need to load the SpatiaLite initialization SQL before running `syncdb`. self.load_spatialite_sql() diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index f26653f7b4..d70fe54bdb 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -10,6 +10,7 @@ from django.conf import settings from django.db import DEFAULT_DB_ALIAS from django.db.backends import util from django.db.transaction import TransactionManagementError +from django.utils.functional import cached_property from django.utils.importlib import import_module from django.utils.timezone import is_aware @@ -402,12 +403,10 @@ class BaseDatabaseFeatures(object): # Does the backend reset sequences between tests? supports_sequence_reset = True - # Features that need to be confirmed at runtime - # Cache whether the confirmation has been performed. - _confirmed = False - supports_transactions = None - supports_stddev = None - can_introspect_foreign_keys = None + # Confirm support for introspected foreign keys + # Every database can do this reliably, except MySQL, + # which can't do it for MyISAM tables + can_introspect_foreign_keys = True # Support for the DISTINCT ON clause can_distinct_on_fields = False @@ -415,15 +414,8 @@ class BaseDatabaseFeatures(object): def __init__(self, connection): self.connection = connection - def confirm(self): - "Perform manual checks of any database features that might vary between installs" - if not self._confirmed: - self._confirmed = True - self.supports_transactions = self._supports_transactions() - self.supports_stddev = self._supports_stddev() - self.can_introspect_foreign_keys = self._can_introspect_foreign_keys() - - def _supports_transactions(self): + @cached_property + def supports_transactions(self): "Confirm support for transactions" cursor = self.connection.cursor() cursor.execute('CREATE TABLE ROLLBACK_TEST (X INT)') @@ -436,7 +428,8 @@ class BaseDatabaseFeatures(object): self.connection._commit() return count == 0 - def _supports_stddev(self): + @cached_property + def supports_stddev(self): "Confirm support for STDDEV and related stats functions" class StdDevPop(object): sql_function = 'STDDEV_POP' @@ -447,12 +440,6 @@ class BaseDatabaseFeatures(object): except NotImplementedError: return False - def _can_introspect_foreign_keys(self): - "Confirm support for introspected foreign keys" - # Every database can do this reliably, except MySQL, - # which can't do it for MyISAM tables - return True - class BaseDatabaseOperations(object): """ diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index ba90cb970b..0f06131bc4 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -264,9 +264,6 @@ class BaseDatabaseCreation(object): self.connection.close() self.connection.settings_dict["NAME"] = test_database_name - # Confirm the feature set of the test database - self.connection.features.confirm() - # Report syncdb messages at one level lower than that requested. # This ensures we don't get flooded with messages during testing # (unless you really ask to be flooded) diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index ff227b6e7e..9de3287608 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -37,6 +37,7 @@ from django.db.backends.mysql.client import DatabaseClient from django.db.backends.mysql.creation import DatabaseCreation from django.db.backends.mysql.introspection import DatabaseIntrospection from django.db.backends.mysql.validation import DatabaseValidation +from django.utils.functional import cached_property from django.utils.safestring import SafeString, SafeUnicode from django.utils import timezone @@ -170,26 +171,25 @@ class DatabaseFeatures(BaseDatabaseFeatures): def __init__(self, connection): super(DatabaseFeatures, self).__init__(connection) - self._storage_engine = None + @cached_property def _mysql_storage_engine(self): "Internal method used in Django tests. Don't rely on this from your code" - if self._storage_engine is None: - cursor = self.connection.cursor() - cursor.execute('CREATE TABLE INTROSPECT_TEST (X INT)') - # This command is MySQL specific; the second column - # will tell you the default table type of the created - # table. Since all Django's test tables will have the same - # table type, that's enough to evaluate the feature. - cursor.execute("SHOW TABLE STATUS WHERE Name='INTROSPECT_TEST'") - result = cursor.fetchone() - cursor.execute('DROP TABLE INTROSPECT_TEST') - self._storage_engine = result[1] - return self._storage_engine + cursor = self.connection.cursor() + cursor.execute('CREATE TABLE INTROSPECT_TEST (X INT)') + # This command is MySQL specific; the second column + # will tell you the default table type of the created + # table. Since all Django's test tables will have the same + # table type, that's enough to evaluate the feature. + cursor.execute("SHOW TABLE STATUS WHERE Name='INTROSPECT_TEST'") + result = cursor.fetchone() + cursor.execute('DROP TABLE INTROSPECT_TEST') + return result[1] - def _can_introspect_foreign_keys(self): + @cached_property + def can_introspect_foreign_keys(self): "Confirm support for introspected foreign keys" - return self._mysql_storage_engine() != 'MyISAM' + return self._mysql_storage_engine != 'MyISAM' class DatabaseOperations(BaseDatabaseOperations): compiler_module = "django.db.backends.mysql.compiler" diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 75e1d9792c..c59905b29a 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -19,6 +19,7 @@ from django.db.backends.sqlite3.client import DatabaseClient from django.db.backends.sqlite3.creation import DatabaseCreation from django.db.backends.sqlite3.introspection import DatabaseIntrospection from django.utils.dateparse import parse_date, parse_datetime, parse_time +from django.utils.functional import cached_property from django.utils.safestring import SafeString from django.utils import timezone @@ -86,7 +87,8 @@ class DatabaseFeatures(BaseDatabaseFeatures): has_bulk_insert = True can_combine_inserts_with_and_without_auto_increment_pk = True - def _supports_stddev(self): + @cached_property + def supports_stddev(self): """Confirm support for STDDEV and related stats functions SQLite supports STDDEV as an extension package; so diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index 2569a3236f..53551fa54d 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -403,8 +403,7 @@ class BackendTestCase(TestCase): self.assertTrue(hasattr(connection.ops, 'connection')) self.assertEqual(connection, connection.ops.connection) - def test_supports_needed_confirm(self): - connection.features.confirm() + def test_cached_db_features(self): self.assertIn(connection.features.supports_transactions, (True, False)) self.assertIn(connection.features.supports_stddev, (True, False)) self.assertIn(connection.features.can_introspect_foreign_keys, (True, False)) diff --git a/tests/regressiontests/transactions_regress/tests.py b/tests/regressiontests/transactions_regress/tests.py index 59722630df..abd7a4ceaa 100644 --- a/tests/regressiontests/transactions_regress/tests.py +++ b/tests/regressiontests/transactions_regress/tests.py @@ -208,7 +208,7 @@ class SavepointTest(TransactionTestCase): work() @skipIf(connection.vendor == 'mysql' and \ - connection.features._mysql_storage_engine() == 'MyISAM', + connection.features._mysql_storage_engine == 'MyISAM', "MyISAM MySQL storage engine doesn't support savepoints") @skipUnlessDBFeature('uses_savepoints') def test_savepoint_rollback(self):