Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #23797 -- Fixed QuerySet.exclude() when rhs is a nullable column. #13118

Merged
merged 1 commit into from Jul 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -392,6 +392,7 @@ answer newbie questions, and generally made Django that much better:
Jacob Burch <jacobburch@gmail.com>
Jacob Green
Jacob Kaplan-Moss <jacob@jacobian.org>
Jacob Walls <http://www.jacobtylerwalls.com/>
Jakub Paczkowski <jakub@paczkowski.eu>
Jakub Wilk <jwilk@jwilk.net>
Jakub Wiśniowski <restless.being@gmail.com>
Expand Down
19 changes: 13 additions & 6 deletions django/db/models/sql/query.py
Expand Up @@ -1324,9 +1324,7 @@ def build_filter(self, filter_expr, branch_negated=False, current_negated=False,
require_outer = lookup_type == 'isnull' and condition.rhs is True and not current_negated
if current_negated and (lookup_type != 'isnull' or condition.rhs is False) and condition.rhs is not None:
require_outer = True
if (lookup_type != 'isnull' and (
self.is_nullable(targets[0]) or
self.alias_map[join_list[-1]].join_type == LOUTER)):
if lookup_type != 'isnull':
# The condition added here will be SQL like this:
# NOT (col IS NOT NULL), where the first NOT is added in
# upper layers of code. The reason for addition is that if col
Expand All @@ -1336,9 +1334,18 @@ def build_filter(self, filter_expr, branch_negated=False, current_negated=False,
# (col IS NULL OR col != someval)
# <=>
# NOT (col IS NOT NULL AND col = someval).
lookup_class = targets[0].get_lookup('isnull')
col = self._get_col(targets[0], join_info.targets[0], alias)
clause.add(lookup_class(col, False), AND)
if (
self.is_nullable(targets[0]) or
self.alias_map[join_list[-1]].join_type == LOUTER
):
lookup_class = targets[0].get_lookup('isnull')
col = self._get_col(targets[0], join_info.targets[0], alias)
clause.add(lookup_class(col, False), AND)
# If someval is a nullable column, someval IS NOT NULL is
# added.
if isinstance(value, Col) and self.is_nullable(value.target):
lookup_class = value.target.get_lookup('isnull')
clause.add(lookup_class(value, False), AND)
return clause, used_joins if not require_outer else ()

def add_filter(self, filter_clause):
Expand Down
1 change: 1 addition & 0 deletions tests/queries/models.py
Expand Up @@ -142,6 +142,7 @@ def __str__(self):
class Number(models.Model):
num = models.IntegerField()
other_num = models.IntegerField(null=True)
another_num = models.IntegerField(null=True)

def __str__(self):
return str(self.num)
Expand Down
17 changes: 16 additions & 1 deletion tests/queries/tests.py
Expand Up @@ -2372,7 +2372,10 @@ def test_named_values_list_without_fields(self):
qs = Number.objects.extra(select={'num2': 'num+1'}).annotate(Count('id'))
values = qs.values_list(named=True).first()
self.assertEqual(type(values).__name__, 'Row')
self.assertEqual(values._fields, ('num2', 'id', 'num', 'other_num', 'id__count'))
self.assertEqual(
values._fields,
('num2', 'id', 'num', 'other_num', 'another_num', 'id__count'),
)
self.assertEqual(values.num, 72)
self.assertEqual(values.num2, 73)
self.assertEqual(values.id__count, 1)
Expand Down Expand Up @@ -2855,6 +2858,18 @@ def test_subquery_exclude_outerref(self):
self.r1.delete()
self.assertFalse(qs.exists())

def test_exclude_nullable_fields(self):
number = Number.objects.create(num=1, other_num=1)
Number.objects.create(num=2, other_num=2, another_num=2)
self.assertSequenceEqual(
Number.objects.exclude(other_num=F('another_num')),
[number],
)
self.assertSequenceEqual(
Number.objects.exclude(num=F('another_num')),
[number],
)


class ExcludeTest17600(TestCase):
"""
Expand Down