Skip to content

Commit cec10f9

Browse files
Eddy-123jacobtylerwalls
authored andcommitted
Fixed django#20024 -- Fixed handling of __in lookups with None in exclude().
Thanks Simon Charette and Tim Graham for reviews, and Jason Hall for a prior iteration.
1 parent 3fb37ef commit cec10f9

3 files changed

Lines changed: 29 additions & 14 deletions

File tree

django/db/models/sql/query.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import sys
1414
import warnings
1515
from collections import Counter, namedtuple
16-
from collections.abc import Iterator, Mapping
16+
from collections.abc import Iterable, Iterator, Mapping
1717
from itertools import chain, count, product
1818
from string import ascii_uppercase
1919

@@ -1638,7 +1638,17 @@ def build_filter(
16381638
):
16391639
lookup_class = targets[0].get_lookup("isnull")
16401640
col = self._get_col(targets[0], join_info.targets[0], alias)
1641-
clause.add(lookup_class(col, False), AND)
1641+
# Use OR + IS NULL when RHS `in` values include None.
1642+
if (
1643+
lookup_type == "in"
1644+
# Check containers (not strings or bytes).
1645+
and isinstance(condition.rhs, Iterable)
1646+
and not isinstance(condition.rhs, (str, bytes))
1647+
and any(v is None for v in condition.rhs)
1648+
):
1649+
clause.add(lookup_class(col, True), OR)
1650+
else:
1651+
clause.add(lookup_class(col, False), AND)
16421652
# If someval is a nullable column, someval IS NOT NULL is
16431653
# added.
16441654
if isinstance(value, Col) and self.is_nullable(value.target):

tests/composite_pk/test_filter.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -617,9 +617,20 @@ def setUpTestData(cls):
617617
tenant=cls.tenant, id=1, email="exclude@example.com"
618618
)
619619

620-
def test_pk_in_with_partial_or_all_none(self):
621-
qs = User.objects
622-
self.assertQuerySetEqual(qs.filter(pk__in=[(1, None)]), [])
623-
self.assertQuerySetEqual(qs.filter(pk__in=[(None, None)]), [])
624-
self.assertQuerySetEqual(qs.exclude(pk__in=[(1, None)]), [self.user])
625-
self.assertQuerySetEqual(qs.exclude(pk__in=[(None, None)]), [self.user])
620+
def test_filter_pk_in_partial_none(self):
621+
self.assertQuerySetEqual(
622+
User.objects.filter(pk__in=[(self.user.pk[0], None)]), []
623+
)
624+
625+
def test_filter_pk_in_full_none(self):
626+
self.assertQuerySetEqual(User.objects.filter(pk__in=[(None, None)]), [])
627+
628+
def test_exclude_pk_in_partial_none(self):
629+
self.assertQuerySetEqual(
630+
User.objects.exclude(pk__in=[(self.user.pk[0], None)]), [self.user]
631+
)
632+
633+
def test_exclude_pk_in_full_none(self):
634+
self.assertQuerySetEqual(
635+
User.objects.exclude(pk__in=[(None, None)]), [self.user]
636+
)

tests/queries/tests.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3536,13 +3536,7 @@ def test_null_in_exclude_qs(self):
35363536
# into subquery above
35373537
self.assertIs(inner_qs._result_cache, None)
35383538

3539-
@unittest.expectedFailure
35403539
def test_col_not_in_list_containing_null(self):
3541-
"""
3542-
The following case is not handled properly because
3543-
SQL's COL NOT IN (list containing null) handling is too weird to
3544-
abstract away.
3545-
"""
35463540
self.assertQuerySetEqual(
35473541
NullableName.objects.exclude(name__in=[None]), ["i1"], attrgetter("name")
35483542
)

0 commit comments

Comments
 (0)