Kaydet (Commit) 5aa51fa9 authored tarafından Anssi Kääriäinen's avatar Anssi Kääriäinen

Simplified QuerySet field.null handling

QuerySet had previously some complex logic for dealing with nullable
fields in negated add_filter() calls. It seems the logic is leftover
from a time where the WhereNode wasn't as intelligent in handling
field__in=[] conditions.

Thanks to aaugustin for comments on the patch.
üst 9350d1d5
...@@ -1193,14 +1193,15 @@ class Query(object): ...@@ -1193,14 +1193,15 @@ class Query(object):
entry.negate() entry.negate()
self.where.add(entry, AND) self.where.add(entry, AND)
break break
if not (lookup_type == 'in' if field.null:
and not hasattr(value, 'as_sql') # In SQL NULL = anyvalue returns unknown, and NOT unknown
and not hasattr(value, '_as_sql') # is still unknown. However, in Python None = anyvalue is False
and not value) and field.null: # (and not False is True...), and we want to return this Python's
# Leaky abstraction artifact: We have to specifically # view of None handling. So we need to specifically exclude the
# exclude the "foo__in=[]" case from this handling, because # NULL values, and because we are inside NOT branch they will
# it's short-circuited in the Where class. # be included in the final resultset. We are essentially creating
# We also need to handle the case where a subquery is provided # SQL like this here: NOT (col IS NOT NULL), where the first NOT
# is added in upper layers of the code.
self.where.add((Constraint(alias, col, None), 'isnull', False), AND) self.where.add((Constraint(alias, col, None), 'isnull', False), AND)
if can_reuse is not None: if can_reuse is not None:
......
...@@ -346,3 +346,9 @@ class OneToOneCategory(models.Model): ...@@ -346,3 +346,9 @@ class OneToOneCategory(models.Model):
def __unicode__(self): def __unicode__(self):
return "one2one " + self.new_name return "one2one " + self.new_name
class NullableName(models.Model):
name = models.CharField(max_length=20, null=True)
class Meta:
ordering = ['id']
from __future__ import absolute_import from __future__ import absolute_import
import datetime import datetime
from operator import attrgetter
import pickle import pickle
import sys import sys
...@@ -18,7 +19,7 @@ from .models import (Annotation, Article, Author, Celebrity, Child, Cover, ...@@ -18,7 +19,7 @@ from .models import (Annotation, Article, Author, Celebrity, Child, Cover,
ManagedModel, Member, NamedCategory, Note, Number, Plaything, PointerA, ManagedModel, Member, NamedCategory, Note, Number, Plaything, PointerA,
Ranking, Related, Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten, Ranking, Related, Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten,
Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory, Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory,
SpecialCategory, OneToOneCategory) SpecialCategory, OneToOneCategory, NullableName)
class BaseQuerysetTest(TestCase): class BaseQuerysetTest(TestCase):
...@@ -1894,3 +1895,38 @@ class DefaultValuesInsertTest(TestCase): ...@@ -1894,3 +1895,38 @@ class DefaultValuesInsertTest(TestCase):
DumbCategory.objects.create() DumbCategory.objects.create()
except TypeError: except TypeError:
self.fail("Creation of an instance of a model with only the PK field shouldn't error out after bulk insert refactoring (#17056)") self.fail("Creation of an instance of a model with only the PK field shouldn't error out after bulk insert refactoring (#17056)")
class NullInExcludeTest(TestCase):
def setUp(self):
NullableName.objects.create(name='i1')
NullableName.objects.create()
def test_null_in_exclude_qs(self):
none_val = '' if connection.features.interprets_empty_strings_as_nulls else None
self.assertQuerysetEqual(
NullableName.objects.exclude(name__in=[]),
['i1', none_val], attrgetter('name'))
self.assertQuerysetEqual(
NullableName.objects.exclude(name__in=['i1']),
[none_val], attrgetter('name'))
self.assertQuerysetEqual(
NullableName.objects.exclude(name__in=['i3']),
['i1', none_val], attrgetter('name'))
inner_qs = NullableName.objects.filter(name='i1').values_list('name')
self.assertQuerysetEqual(
NullableName.objects.exclude(name__in=inner_qs),
[none_val], attrgetter('name'))
# Check that the inner queryset wasn't executed - it should be turned
# into subquery above
self.assertIs(inner_qs._result_cache, None)
@unittest.expectedFailure
def test_col_not_in_list_containing_null(self):
"""
The following case is not handled properly because
SQL's COL NOT IN (list containing null) handling is too weird to
abstract away.
"""
self.assertQuerysetEqual(
NullableName.objects.exclude(name__in=[None]),
['i1'], attrgetter('name'))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment