From d4b88f068571cb5e5e4da48533b1f06aa2cf7618 Mon Sep 17 00:00:00 2001
From: Chris Adams <chris@improbable.org>
Date: Mon, 7 Oct 2013 10:48:19 -0400
Subject: [PATCH 1/2] Admin: optimize search filter construction

Using the database this way is inherently slow but the previous implementation would call QuerySet.filter() once per whitespace-separated term in the query text, causing an extra set of JOINs for each term. With the MySQL backend, this actually causes database errors once you hit the server limit of 61.

This commit logically ANDs each of the query components together so
`queryset.filter()` can be called a single time.
---
 django/contrib/admin/options.py | 10 +++++++---
 tests/admin_changelist/tests.py | 19 +++++++++++++++++++
 2 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 3b02ac0..f366b5d 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -849,10 +849,14 @@ def construct_search(field_name):
         if search_fields and search_term:
             orm_lookups = [construct_search(str(search_field))
                            for search_field in search_fields]
+
+            query_parts = []
             for bit in search_term.split():
-                or_queries = [models.Q(**{orm_lookup: bit})
-                              for orm_lookup in orm_lookups]
-                queryset = queryset.filter(reduce(operator.or_, or_queries))
+                or_queries = (models.Q(**{orm_lookup: bit})
+                              for orm_lookup in orm_lookups)
+                query_parts.append(reduce(operator.or_, or_queries))
+            queryset = queryset.filter(reduce(operator.and_, query_parts))
+
             if not use_distinct:
                 for search_spec in orm_lookups:
                     if lookup_needs_distinct(self.opts, search_spec):
diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py
index f9ef079..e49145f 100644
--- a/tests/admin_changelist/tests.py
+++ b/tests/admin_changelist/tests.py
@@ -643,6 +643,25 @@ def test_pagination_page_range(self):
                 list(real_page_range),
             )
 
+    def test_search_query_efficiency(self):
+        """Ensure that search queries only add one ORM filter rather than one per term"""
+        new_parent = Parent.objects.create(name='parent')
+        for i in range(200):
+            Child.objects.create(name='foo bar baz qux quux corge %s' % i,
+                                 parent=new_parent)
+
+        m = ParentAdmin(Parent, admin.site)
+
+        request = self.factory.get('/parent/', data={'q': 'foo bar baz'})
+
+        cl = ChangeList(request, Parent, m.list_display, m.list_display_links,
+                        m.list_filter, m.date_hierarchy, m.search_fields,
+                        m.list_select_related, m.list_per_page,
+                        m.list_max_show_all, m.list_editable, m)
+
+        self.assertEqual(2, cl.queryset.query.count_active_tables(),
+                         "ChangeList search filters should not cause duplicate JOINs")
+
 
 class AdminLogNodeTestCase(TestCase):
 
-- 
1.8.4


From 76ea5651266d0749701a238fb844333a6a0fc418 Mon Sep 17 00:00:00 2001
From: Chris Adams <chris@improbable.org>
Date: Mon, 7 Oct 2013 11:50:11 -0400
Subject: [PATCH 2/2] Tests: confirm that admin changelist search terms are
 ANDed

Performance work on #881 inadvertently demonstrated that there wasn't a
test for the documented behaviour of admin changelist terms being
evaluated as logical ANDs
---
 tests/admin_changelist/tests.py | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py
index e49145f..f63de41 100644
--- a/tests/admin_changelist/tests.py
+++ b/tests/admin_changelist/tests.py
@@ -662,6 +662,37 @@ def test_search_query_efficiency(self):
         self.assertEqual(2, cl.queryset.query.count_active_tables(),
                          "ChangeList search filters should not cause duplicate JOINs")
 
+    def test_search_query_logic(self):
+        """Changelist search terms should be ANDed"""
+
+        parent1 = Parent.objects.create(name='parent 1')
+        parent2 = Parent.objects.create(name='parent 2')
+
+        Child.objects.create(name='foo bar baz', parent=parent1)
+        Child.objects.create(name='bar baz qux', parent=parent2)
+
+        m = ParentAdmin(Parent, admin.site)
+
+        request = self.factory.get('/parent/', data={'q': 'foo bar baz'})
+
+        cl = ChangeList(request, Parent, m.list_display, m.list_display_links,
+                        m.list_filter, m.date_hierarchy, m.search_fields,
+                        m.list_select_related, m.list_per_page,
+                        m.list_max_show_all, m.list_editable, m)
+
+        cl.get_results(request)
+        self.assertListEqual(["parent 1"], list(cl.queryset.values_list("name", flat=True)))
+
+
+        request2 = self.factory.get('/parent/', data={'q': 'bar baz'})
+        cl2 = ChangeList(request2, Parent, m.list_display, m.list_display_links,
+                         m.list_filter, m.date_hierarchy, m.search_fields,
+                         m.list_select_related, m.list_per_page,
+                         m.list_max_show_all, m.list_editable, m)
+        cl2.get_results(request2)
+        self.assertListEqual(['parent 1', 'parent 2'],
+                             list(cl2.queryset.order_by("name").values_list("name", flat=True)))
+
 
 class AdminLogNodeTestCase(TestCase):
 
-- 
1.8.4

