From f7f38f3a0b44d8c6d14344dae66b6ce52cd77b55 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Wed, 2 Apr 2025 13:32:38 -0400 Subject: [PATCH] Fixed #36290 -- Made TupleIn() lookup discard tuples containing None. Just like the In() lookup discards of None members TupleIn() should discard tuples containing any None as NULL != NULL in SQL and the framework expects such queries to be elided under some circumstances. Refs #31667, #36116. Thanks Basptise Mispelon for bisecting the regression to 626d77e. --- django/db/models/fields/tuple_lookups.py | 13 +++++++++++++ docs/releases/5.2.1.txt | 3 +++ tests/composite_pk/test_filter.py | 7 +++++++ tests/foreign_object/models/person.py | 2 +- tests/foreign_object/tests.py | 13 +++++++++++++ 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/django/db/models/fields/tuple_lookups.py b/django/db/models/fields/tuple_lookups.py index e3cb696249..6a4e322589 100644 --- a/django/db/models/fields/tuple_lookups.py +++ b/django/db/models/fields/tuple_lookups.py @@ -329,6 +329,10 @@ class TupleIn(TupleLookupMixin, In): lhs = self.lhs for vals in rhs: + # Remove any tuple containing None from the list as NULL is never + # equal to anything. + if any(val is None for val in vals): + continue result.append( Tuple( *[ @@ -338,6 +342,9 @@ class TupleIn(TupleLookupMixin, In): ) ) + if not result: + raise EmptyResultSet + return compiler.compile(Tuple(*result)) def get_fallback_sql(self, compiler, connection): @@ -353,9 +360,15 @@ class TupleIn(TupleLookupMixin, In): lhs = self.lhs for vals in rhs: + # Remove any tuple containing None from the list as NULL is never + # equal to anything. + if any(val is None for val in vals): + continue lookups = [Exact(col, val) for col, val in zip(lhs, vals)] root.children.append(WhereNode(lookups, connector=AND)) + if not root.children: + raise EmptyResultSet return root.as_sql(compiler, connection) diff --git a/docs/releases/5.2.1.txt b/docs/releases/5.2.1.txt index a4e73841bd..b17e63ebd1 100644 --- a/docs/releases/5.2.1.txt +++ b/docs/releases/5.2.1.txt @@ -12,3 +12,6 @@ Bugfixes * Fixed a regression in Django 5.2 that caused a crash when annotating aggregate expressions over query that uses explicit grouping by transforms followed by field references (:ticket:`36292`). + +* Fixed a regression in Django 5.2 that caused unnecessary queries when + prefetching nullable foreign key relationships (:ticket:`36290`). diff --git a/tests/composite_pk/test_filter.py b/tests/composite_pk/test_filter.py index c633348cc1..39f19daecc 100644 --- a/tests/composite_pk/test_filter.py +++ b/tests/composite_pk/test_filter.py @@ -206,6 +206,13 @@ class CompositePKFilterTests(TestCase): [self.comment_1], ) + def test_filter_by_pk_in_none(self): + with self.assertNumQueries(0): + self.assertSequenceEqual( + Comment.objects.filter(pk__in=[(None, 1), (1, None)]), + [], + ) + def test_filter_comments_by_user_and_order_by_pk_asc(self): self.assertSequenceEqual( Comment.objects.filter(user=self.user_1).order_by("pk"), diff --git a/tests/foreign_object/models/person.py b/tests/foreign_object/models/person.py index d536ab63d7..4290ad6d3b 100644 --- a/tests/foreign_object/models/person.py +++ b/tests/foreign_object/models/person.py @@ -84,7 +84,7 @@ class Friendship(models.Model): ) from_friend_id = models.IntegerField() to_friend_country_id = models.IntegerField() - to_friend_id = models.IntegerField() + to_friend_id = models.IntegerField(null=True) # Relation Fields from_friend = models.ForeignObject( diff --git a/tests/foreign_object/tests.py b/tests/foreign_object/tests.py index de80c705ab..00b6a8350e 100644 --- a/tests/foreign_object/tests.py +++ b/tests/foreign_object/tests.py @@ -296,6 +296,19 @@ class MultiColumnFKTests(TestCase): self.assertEqual(friendships[0].to_friend, self.george) self.assertEqual(friendships[1].to_friend, self.sam) + def test_prefetch_foreignobject_null_hidden_forward_skipped(self): + fiendship = Friendship.objects.create( + from_friend_country=self.usa, + from_friend_id=self.bob.id, + to_friend_country_id=self.usa.id, + to_friend_id=None, + ) + with self.assertNumQueries(1): + self.assertEqual( + Friendship.objects.prefetch_related("to_friend").get(), + fiendship, + ) + def test_prefetch_foreignobject_reverse(self): Membership.objects.create( membership_country=self.usa, person=self.bob, group=self.cia