Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Simplified QuerySet field.null handling #21

Closed
wants to merge 1 commit into from

1 participant

Anssi Kääriäinen
Anssi Kääriäinen
Owner

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.

All tests passed on SQLite. I believe all tests should pass on other
backends, too.

The reason why this should be fixed is that it is very hard to see what
is happening in the negated field.null case currently. I hope this patch
makes the logic and why the condition is added clearer

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.
8f9fed5
Anssi Kääriäinen
Owner

I pushed the patch manually by git push - no need for this pull request any more.

Anssi Kääriäinen akaariai closed this April 29, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 1 unique commit by 1 author.

Apr 29, 2012
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.
8f9fed5
This page is out of date. Refresh to see the latest.
17  django/db/models/sql/query.py
@@ -1193,14 +1193,15 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
1193 1193
                             entry.negate()
1194 1194
                             self.where.add(entry, AND)
1195 1195
                             break
1196  
-                if not (lookup_type == 'in'
1197  
-                            and not hasattr(value, 'as_sql')
1198  
-                            and not hasattr(value, '_as_sql')
1199  
-                            and not value) and field.null:
1200  
-                    # Leaky abstraction artifact: We have to specifically
1201  
-                    # exclude the "foo__in=[]" case from this handling, because
1202  
-                    # it's short-circuited in the Where class.
1203  
-                    # We also need to handle the case where a subquery is provided
  1196
+                if field.null:
  1197
+                    # In SQL NULL = anyvalue returns unknown, and NOT unknown
  1198
+                    # is still unknown. However, in Python None = anyvalue is False
  1199
+                    # (and not False is True...), and we want to return this Python's
  1200
+                    # view of None handling. So we need to specifically exclude the
  1201
+                    # NULL values, and because we are inside NOT branch they will
  1202
+                    # be included in the final resultset. We are essentially creating
  1203
+                    # SQL like this here: NOT (col IS NOT NULL), where the first NOT
  1204
+                    # is added in upper layers of the code.
1204 1205
                     self.where.add((Constraint(alias, col, None), 'isnull', False), AND)
1205 1206
 
1206 1207
         if can_reuse is not None:
6  tests/regressiontests/queries/models.py
@@ -346,3 +346,9 @@ class OneToOneCategory(models.Model):
346 346
 
347 347
     def __unicode__(self):
348 348
         return "one2one " + self.new_name
  349
+
  350
+class NullableName(models.Model):
  351
+    name = models.CharField(max_length=20, null=True)
  352
+    
  353
+    class Meta:
  354
+        ordering = ['id']
38  tests/regressiontests/queries/tests.py
... ...
@@ -1,6 +1,7 @@
1 1
 from __future__ import absolute_import
2 2
 
3 3
 import datetime
  4
+from operator import attrgetter
4 5
 import pickle
5 6
 import sys
6 7
 
@@ -18,7 +19,7 @@
18 19
     ManagedModel, Member, NamedCategory, Note, Number, Plaything, PointerA,
19 20
     Ranking, Related, Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten,
20 21
     Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory,
21  
-    SpecialCategory, OneToOneCategory)
  22
+    SpecialCategory, OneToOneCategory, NullableName)
22 23
 
23 24
 
24 25
 class BaseQuerysetTest(TestCase):
@@ -1894,3 +1895,38 @@ def test_no_extra_params(self):
1894 1895
             DumbCategory.objects.create()
1895 1896
         except TypeError:
1896 1897
             self.fail("Creation of an instance of a model with only the PK field shouldn't error out after bulk insert refactoring (#17056)")
  1898
+
  1899
+class NullInExcludeTest(TestCase):
  1900
+    def setUp(self):
  1901
+        NullableName.objects.create(name='i1')
  1902
+        NullableName.objects.create()
  1903
+
  1904
+    def test_null_in_exclude_qs(self):
  1905
+        none_val = '' if connection.features.interprets_empty_strings_as_nulls else None
  1906
+        self.assertQuerysetEqual(
  1907
+            NullableName.objects.exclude(name__in=[]),
  1908
+            ['i1', none_val], attrgetter('name'))
  1909
+        self.assertQuerysetEqual(
  1910
+            NullableName.objects.exclude(name__in=['i1']),
  1911
+            [none_val], attrgetter('name'))
  1912
+        self.assertQuerysetEqual(
  1913
+            NullableName.objects.exclude(name__in=['i3']),
  1914
+            ['i1', none_val], attrgetter('name'))
  1915
+        inner_qs = NullableName.objects.filter(name='i1').values_list('name')
  1916
+        self.assertQuerysetEqual(
  1917
+            NullableName.objects.exclude(name__in=inner_qs),
  1918
+            [none_val], attrgetter('name'))
  1919
+        # Check that the inner queryset wasn't executed - it should be turned
  1920
+        # into subquery above
  1921
+        self.assertIs(inner_qs._result_cache, None)
  1922
+
  1923
+    @unittest.expectedFailure
  1924
+    def test_col_not_in_list_containing_null(self):
  1925
+        """
  1926
+        The following case is not handled properly because
  1927
+        SQL's COL NOT IN (list containing null) handling is too weird to
  1928
+        abstract away.
  1929
+        """
  1930
+        self.assertQuerysetEqual(
  1931
+            NullableName.objects.exclude(name__in=[None]),
  1932
+            ['i1'], attrgetter('name'))
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.