diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py index 66945c4a1ad..a9abd82cd9b 100644 --- a/django/db/models/lookups.py +++ b/django/db/models/lookups.py @@ -245,6 +245,20 @@ class FieldGetDbPrepValueIterableMixin(FieldGetDbPrepValueMixin): class Exact(FieldGetDbPrepValueMixin, BuiltinLookup): lookup_name = 'exact' + def process_rhs(self, compiler, connection): + from django.db.models.sql.query import Query + if isinstance(self.rhs, Query): + if self.rhs.has_limit_one(): + # The subquery must select only the pk. + self.rhs.clear_select_clause() + self.rhs.add_fields(['pk']) + else: + raise ValueError( + 'The QuerySet value for an exact lookup must be limited to ' + 'one result using slicing.' + ) + return super().process_rhs(compiler, connection) + @Field.register_lookup class IExact(BuiltinLookup): diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index a962aabdf16..372431c620d 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1627,6 +1627,9 @@ class Query: """Clear any existing limits.""" self.low_mark, self.high_mark = 0, None + def has_limit_one(self): + return self.high_mark is not None and (self.high_mark - self.low_mark) == 1 + def can_filter(self): """ Return True if adding filters to this instance is still possible. diff --git a/tests/lookup/tests.py b/tests/lookup/tests.py index 7b08c778df4..0161782dbe8 100644 --- a/tests/lookup/tests.py +++ b/tests/lookup/tests.py @@ -848,6 +848,28 @@ class LookupTests(TestCase): self.assertTrue(Season.objects.filter(nulled_text_field__nulled__exact=None)) self.assertTrue(Season.objects.filter(nulled_text_field__nulled=None)) + def test_exact_sliced_queryset_limit_one(self): + self.assertCountEqual( + Article.objects.filter(author=Author.objects.all()[:1]), + [self.a1, self.a2, self.a3, self.a4] + ) + + def test_exact_sliced_queryset_limit_one_offset(self): + self.assertCountEqual( + Article.objects.filter(author=Author.objects.all()[1:2]), + [self.a5, self.a6, self.a7] + ) + + def test_exact_sliced_queryset_not_limited_to_one(self): + msg = ( + 'The QuerySet value for an exact lookup must be limited to one ' + 'result using slicing.' + ) + with self.assertRaisesMessage(ValueError, msg): + list(Article.objects.filter(author=Author.objects.all()[:2])) + with self.assertRaisesMessage(ValueError, msg): + list(Article.objects.filter(author=Author.objects.all()[1:])) + def test_custom_field_none_rhs(self): """ __exact=value is transformed to __isnull=True if Field.get_prep_value()