From aa423575e7b464433fcfc4bf4b8e1d7627b17ce6 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 9 Jun 2012 15:59:52 +0200 Subject: [PATCH] Fixed #17760 -- Implemented callable database features as cached properties This does remove the requirement to call features.confirm() method before checking the properties. Thanks cdestiger and Ramiro Morales for their work on the patch. --- .../gis/db/backends/spatialite/creation.py | 3 -- django/db/backends/__init__.py | 31 ++++++------------- django/db/backends/creation.py | 3 -- django/db/backends/mysql/base.py | 30 +++++++++--------- django/db/backends/sqlite3/base.py | 4 ++- tests/regressiontests/backends/tests.py | 3 +- .../transactions_regress/tests.py | 2 +- 7 files changed, 29 insertions(+), 47 deletions(-) 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):