Skip to content

Commit 1ada749

Browse files
committed
Prevented unnecessary distinct() call in SearchFilter.
1 parent 79dad01 commit 1ada749

File tree

2 files changed

+41
-4
lines changed

2 files changed

+41
-4
lines changed

rest_framework/filters.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from django.conf import settings
1111
from django.core.exceptions import ImproperlyConfigured
1212
from django.db import models
13+
from django.db.models.constants import LOOKUP_SEP
1314
from django.template import loader
1415
from django.utils import six
1516
from django.utils.translation import ugettext_lazy as _
@@ -153,6 +154,25 @@ def construct_search(self, field_name):
153154
else:
154155
return "%s__icontains" % field_name
155156

157+
def must_call_distinct(self, opts, lookups):
158+
"""
159+
Return True if 'distinct()' should be used to query the given lookups.
160+
"""
161+
for lookup in lookups:
162+
if lookup[0] in {'^', '=', '@', '$'}:
163+
lookup = lookup[1:]
164+
parts = lookup.split(LOOKUP_SEP)
165+
for part in parts:
166+
field = opts.get_field(part)
167+
if hasattr(field, 'get_path_info'):
168+
# This field is a relation, update opts to follow the relation
169+
path_info = field.get_path_info()
170+
opts = path_info[-1].to_opts
171+
if any(path.m2m for path in path_info):
172+
# This field is a m2m relation so we know we need to call distinct
173+
return True
174+
return False
175+
156176
def filter_queryset(self, request, queryset, view):
157177
search_fields = getattr(view, 'search_fields', None)
158178
search_terms = self.get_search_terms(request)
@@ -173,10 +193,12 @@ def filter_queryset(self, request, queryset, view):
173193
]
174194
queryset = queryset.filter(reduce(operator.or_, queries))
175195

176-
# Filtering against a many-to-many field requires us to
177-
# call queryset.distinct() in order to avoid duplicate items
178-
# in the resulting queryset.
179-
return distinct(queryset, base)
196+
if self.must_call_distinct(queryset.model._meta, search_fields):
197+
# Filtering against a many-to-many field requires us to
198+
# call queryset.distinct() in order to avoid duplicate items
199+
# in the resulting queryset.
200+
queryset = distinct(queryset, base)
201+
return queryset
180202

181203
def to_html(self, request, queryset, view):
182204
if not getattr(view, 'search_fields', None):

tests/test_filters.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,21 @@ class SearchListView(generics.ListAPIView):
497497
response = view(request)
498498
self.assertEqual(len(response.data), 1)
499499

500+
def test_must_call_distinct(self):
501+
filter_ = filters.SearchFilter()
502+
prefixes = ['', '^', '=', '@', '$']
503+
for prefix in prefixes:
504+
self.assertFalse(
505+
filter_.must_call_distinct(
506+
SearchFilterModelM2M._meta, ["%stitle" % prefix]
507+
)
508+
)
509+
self.assertTrue(
510+
filter_.must_call_distinct(
511+
SearchFilterModelM2M._meta, ["%stitle" % prefix, "%sattributes__label" % prefix]
512+
)
513+
)
514+
500515

501516
class OrderingFilterModel(models.Model):
502517
title = models.CharField(max_length=20)

0 commit comments

Comments
 (0)