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

This commit is contained in:
Jacob Walls 2020-06-27 17:41:32 -04:00 committed by Mariusz Felisiak
parent b7b7df5fbc
commit 512da9d585
4 changed files with 31 additions and 7 deletions

View File

@ -392,6 +392,7 @@ answer newbie questions, and generally made Django that much better:
Jacob Burch <jacobburch@gmail.com> Jacob Burch <jacobburch@gmail.com>
Jacob Green Jacob Green
Jacob Kaplan-Moss <jacob@jacobian.org> Jacob Kaplan-Moss <jacob@jacobian.org>
Jacob Walls <http://www.jacobtylerwalls.com/>
Jakub Paczkowski <jakub@paczkowski.eu> Jakub Paczkowski <jakub@paczkowski.eu>
Jakub Wilk <jwilk@jwilk.net> Jakub Wilk <jwilk@jwilk.net>
Jakub Wiśniowski <restless.being@gmail.com> Jakub Wiśniowski <restless.being@gmail.com>

View File

@ -1324,9 +1324,7 @@ class Query(BaseExpression):
require_outer = lookup_type == 'isnull' and condition.rhs is True and not current_negated 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: if current_negated and (lookup_type != 'isnull' or condition.rhs is False) and condition.rhs is not None:
require_outer = True require_outer = True
if (lookup_type != 'isnull' and ( if lookup_type != 'isnull':
self.is_nullable(targets[0]) or
self.alias_map[join_list[-1]].join_type == LOUTER)):
# The condition added here will be SQL like this: # The condition added here will be SQL like this:
# NOT (col IS NOT NULL), where the first NOT is added in # NOT (col IS NOT NULL), where the first NOT is added in
# upper layers of code. The reason for addition is that if col # upper layers of code. The reason for addition is that if col
@ -1336,9 +1334,18 @@ class Query(BaseExpression):
# (col IS NULL OR col != someval) # (col IS NULL OR col != someval)
# <=> # <=>
# NOT (col IS NOT NULL AND col = someval). # NOT (col IS NOT NULL AND col = someval).
lookup_class = targets[0].get_lookup('isnull') if (
col = self._get_col(targets[0], join_info.targets[0], alias) self.is_nullable(targets[0]) or
clause.add(lookup_class(col, False), AND) 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 () return clause, used_joins if not require_outer else ()
def add_filter(self, filter_clause): def add_filter(self, filter_clause):

View File

@ -142,6 +142,7 @@ class Cover(models.Model):
class Number(models.Model): class Number(models.Model):
num = models.IntegerField() num = models.IntegerField()
other_num = models.IntegerField(null=True) other_num = models.IntegerField(null=True)
another_num = models.IntegerField(null=True)
def __str__(self): def __str__(self):
return str(self.num) return str(self.num)

View File

@ -2372,7 +2372,10 @@ class ValuesQuerysetTests(TestCase):
qs = Number.objects.extra(select={'num2': 'num+1'}).annotate(Count('id')) qs = Number.objects.extra(select={'num2': 'num+1'}).annotate(Count('id'))
values = qs.values_list(named=True).first() values = qs.values_list(named=True).first()
self.assertEqual(type(values).__name__, 'Row') 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.num, 72)
self.assertEqual(values.num2, 73) self.assertEqual(values.num2, 73)
self.assertEqual(values.id__count, 1) self.assertEqual(values.id__count, 1)
@ -2855,6 +2858,18 @@ class ExcludeTests(TestCase):
self.r1.delete() self.r1.delete()
self.assertFalse(qs.exists()) 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): class ExcludeTest17600(TestCase):
""" """