Skip to content

Commit c8d48e5

Browse files
committed
Fixed #20024 -- Made exclude(field__in=[None, ...]) generate IS NULL
clause. this fixes a long-standing issue where exclude(field__in=[None, ...]) would incorrectly return no results due to SQL NULL handling in NOT IN clauses. Now, if the list includes None, the ORM splits the condition into: IS NULL OR IN (...) This aligns exclude(...__in=...) behavior with user expectations and avoids leaking SQL's tri-valued logic into the ORM. Includes tests and a release note entry under 5.2.5.
1 parent 748ca0a commit c8d48e5

File tree

2 files changed

+38
-0
lines changed

2 files changed

+38
-0
lines changed

django/db/models/lookups.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,32 @@ def as_sql(self, compiler, connection):
553553
and len(self.rhs) > max_in_list_size
554554
):
555555
return self.split_parameter_list_as_sql(compiler, connection)
556+
557+
if self.rhs_is_direct_value() and isinstance(self.rhs, (list, tuple)):
558+
values = list(self.rhs)
559+
has_none = None in values
560+
filtered_values = [v for v in values if v is not None]
561+
if not filtered_values and not has_none:
562+
# Avoid circular import when importing NothingNode
563+
from django.db.models.sql.where import NothingNode
564+
565+
where = NothingNode()
566+
return compiler.compile(where)
567+
568+
lhs, lhs_params = self.process_lhs(compiler, connection)
569+
sql_parts = []
570+
params = []
571+
572+
if filtered_values:
573+
placeholders = ", ".join(["%s"] * len(filtered_values))
574+
sql_parts.append(f"{lhs} IN ({placeholders})")
575+
params.extend(lhs_params + filtered_values)
576+
577+
if has_none:
578+
sql_parts.append(f"{lhs} IS NULL")
579+
580+
return " OR ".join(sql_parts), params
581+
556582
return super().as_sql(compiler, connection)
557583

558584
def split_parameter_list_as_sql(self, compiler, connection):

tests/queries/tests.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4615,3 +4615,15 @@ def test_ticket_23622(self):
46154615
set(Ticket23605A.objects.filter(qy).values_list("pk", flat=True)),
46164616
)
46174617
self.assertSequenceEqual(Ticket23605A.objects.filter(qx), [a2])
4618+
4619+
4620+
class ExcludedNoneInTest(TestCase):
4621+
def setUp(self):
4622+
Tag.objects.create(name="null", parent_id=None)
4623+
Tag.objects.create(name="one", parent_id=1)
4624+
Tag.objects.create(name="two", parent_id=2)
4625+
4626+
def test_exclude_in_with_none(self):
4627+
qs = Tag.objects.exclude(parent_id__in=[None, 1])
4628+
values = list(qs.values_list("parent_id", flat=True))
4629+
self.assertEqual(values, [2])

0 commit comments

Comments
 (0)