diff options
| author | Tom <tom@tomforb.es> | 2017-09-10 15:34:18 +0100 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2018-04-19 10:52:19 -0400 |
| commit | c1c163b42717ed5e051098ebf0e2f5c77810f20e (patch) | |
| tree | b3c42bcf178fe8f33394f41252a92238cb8baff3 /tests | |
| parent | df90e462d91d3a77aa89b69d791bf17c2bf7ff9b (diff) | |
Fixed #28574 -- Added QuerySet.explain().
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/basic/tests.py | 1 | ||||
| -rw-r--r-- | tests/queries/test_explain.py | 99 | ||||
| -rwxr-xr-x | tests/runtests.py | 8 |
3 files changed, 108 insertions, 0 deletions
diff --git a/tests/basic/tests.py b/tests/basic/tests.py index 1a5e95e9d6..76d92a7591 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -555,6 +555,7 @@ class ManagerTest(SimpleTestCase): 'only', 'using', 'exists', + 'explain', '_insert', '_update', 'raw', diff --git a/tests/queries/test_explain.py b/tests/queries/test_explain.py new file mode 100644 index 0000000000..26baf6fb30 --- /dev/null +++ b/tests/queries/test_explain.py @@ -0,0 +1,99 @@ +import unittest + +from django.db import NotSupportedError, connection, transaction +from django.db.models import Count +from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature +from django.test.utils import CaptureQueriesContext + +from .models import Tag + + +@skipUnlessDBFeature('supports_explaining_query_execution') +class ExplainTests(TestCase): + + def test_basic(self): + querysets = [ + Tag.objects.filter(name='test'), + Tag.objects.filter(name='test').select_related('parent'), + Tag.objects.filter(name='test').prefetch_related('children'), + Tag.objects.filter(name='test').annotate(Count('children')), + Tag.objects.filter(name='test').values_list('name'), + Tag.objects.order_by().union(Tag.objects.order_by().filter(name='test')), + Tag.objects.all().select_for_update().filter(name='test'), + ] + supported_formats = connection.features.supported_explain_formats + all_formats = (None,) + tuple(supported_formats) + tuple(f.lower() for f in supported_formats) + for idx, queryset in enumerate(querysets): + for format in all_formats: + with self.subTest(format=format, queryset=idx): + if connection.vendor == 'mysql': + # This does a query and caches the result. + connection.features.needs_explain_extended + with self.assertNumQueries(1), CaptureQueriesContext(connection) as captured_queries: + result = queryset.explain(format=format) + self.assertTrue(captured_queries[0]['sql'].startswith(connection.ops.explain_prefix)) + self.assertIsInstance(result, str) + self.assertTrue(result) + + @skipUnlessDBFeature('validates_explain_options') + def test_unknown_options(self): + with self.assertRaisesMessage(ValueError, 'Unknown options: test, test2'): + Tag.objects.all().explain(test=1, test2=1) + + def test_unknown_format(self): + msg = 'DOES NOT EXIST is not a recognized format.' + if connection.features.supported_explain_formats: + msg += ' Allowed formats: %s' % ', '.join(sorted(connection.features.supported_explain_formats)) + with self.assertRaisesMessage(ValueError, msg): + Tag.objects.all().explain(format='does not exist') + + @unittest.skipUnless(connection.vendor == 'postgresql', 'PostgreSQL specific') + def test_postgres_options(self): + qs = Tag.objects.filter(name='test') + test_options = [ + {'COSTS': False, 'BUFFERS': True, 'ANALYZE': True}, + {'costs': False, 'buffers': True, 'analyze': True}, + {'verbose': True, 'timing': True, 'analyze': True}, + {'verbose': False, 'timing': False, 'analyze': True}, + ] + if connection.pg_version >= 100000: + test_options.append({'summary': True}) + for options in test_options: + with self.subTest(**options), transaction.atomic(): + with CaptureQueriesContext(connection) as captured_queries: + qs.explain(format='text', **options) + self.assertEqual(len(captured_queries), 1) + for name, value in options.items(): + option = '{} {}'.format(name.upper(), 'true' if value else 'false') + self.assertIn(option, captured_queries[0]['sql']) + + @unittest.skipUnless(connection.vendor == 'mysql', 'MySQL specific') + def test_mysql_text_to_traditional(self): + with CaptureQueriesContext(connection) as captured_queries: + Tag.objects.filter(name='test').explain(format='text') + self.assertEqual(len(captured_queries), 1) + self.assertIn('FORMAT=TRADITIONAL', captured_queries[0]['sql']) + + @unittest.skipUnless(connection.vendor == 'mysql', 'MySQL < 5.7 specific') + def test_mysql_extended(self): + # Inner skip to avoid module level query for MySQL version. + if not connection.features.needs_explain_extended: + raise unittest.SkipTest('MySQL < 5.7 specific') + qs = Tag.objects.filter(name='test') + with CaptureQueriesContext(connection) as captured_queries: + qs.explain(format='json') + self.assertEqual(len(captured_queries), 1) + self.assertNotIn('EXTENDED', captured_queries[0]['sql']) + with CaptureQueriesContext(connection) as captured_queries: + qs.explain(format='text') + self.assertEqual(len(captured_queries), 1) + self.assertNotIn('EXTENDED', captured_queries[0]['sql']) + + +@skipIfDBFeature('supports_explaining_query_execution') +class ExplainUnsupportedTests(TestCase): + + def test_message(self): + msg = 'This backend does not support explaining query execution.' + with self.assertRaisesMessage(NotSupportedError, msg): + Tag.objects.filter(name='test').explain() diff --git a/tests/runtests.py b/tests/runtests.py index 0302137dbb..c3d5ee427f 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -20,6 +20,14 @@ from django.test.utils import get_runner from django.utils.deprecation import RemovedInDjango30Warning from django.utils.log import DEFAULT_LOGGING +try: + import MySQLdb +except ImportError: + pass +else: + # Ignore informational warnings from QuerySet.explain(). + warnings.filterwarnings('ignore', '\(1003, *', category=MySQLdb.Warning) + # Make deprecation warnings errors to ensure no usage of deprecated features. warnings.simplefilter("error", RemovedInDjango30Warning) # Make runtime warning errors to ensure no usage of error prone patterns. |
