From fbbbf8b9a14ebd5752529009523729e7d87989e8 Mon Sep 17 00:00:00 2001
From: Russell Keith-Magee <russell@keith-magee.com>
Date: Sat, 14 Oct 2006 02:48:05 +0000
Subject: [PATCH] Fixes #2737 -- Added code to allow None as a query value for
 __exact queries, raising an error otherwise. __exact=None is interpreted as
 the SQL 'value = NULL'. This fixes some minor problems with queries on
 unsaved objects with related object sets, and stops queries with a value of
 None being outright ignored (even if they reference an unknown attribute).

git-svn-id: http://code.djangoproject.com/svn/django/trunk@3902 bcc190cf-cafb-0310-a4f2-bffc1f526a37
---
 django/db/models/query.py                     | 49 +++++++++--------
 docs/db-api.txt                               | 21 ++++++--
 .../regressiontests/null_queries/__init__.py  |  0
 tests/regressiontests/null_queries/models.py  | 54 +++++++++++++++++++
 4 files changed, 97 insertions(+), 27 deletions(-)
 create mode 100644 tests/regressiontests/null_queries/__init__.py
 create mode 100644 tests/regressiontests/null_queries/models.py

diff --git a/django/db/models/query.py b/django/db/models/query.py
index eb4b3b63ae..53ed63ae5b 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -707,30 +707,35 @@ def parse_lookup(kwarg_items, opts):
     joins, where, params = SortedDict(), [], []
 
     for kwarg, value in kwarg_items:
-        if value is not None:
-            path = kwarg.split(LOOKUP_SEPARATOR)
-            # Extract the last elements of the kwarg.
-            # The very-last is the lookup_type (equals, like, etc).
-            # The second-last is the table column on which the lookup_type is
-            # to be performed. If this name is 'pk', it will be substituted with
-            # the name of the primary key.
-            # If there is only one part, or the last part is not a query
-            # term, assume that the query is an __exact
-            lookup_type = path.pop()
-            if lookup_type == 'pk':
-                lookup_type = 'exact'
-                path.append(None)
-            elif len(path) == 0 or lookup_type not in QUERY_TERMS:
-                path.append(lookup_type)
-                lookup_type = 'exact'
+        path = kwarg.split(LOOKUP_SEPARATOR)
+        # Extract the last elements of the kwarg.
+        # The very-last is the lookup_type (equals, like, etc).
+        # The second-last is the table column on which the lookup_type is
+        # to be performed. If this name is 'pk', it will be substituted with
+        # the name of the primary key.
+        # If there is only one part, or the last part is not a query
+        # term, assume that the query is an __exact
+        lookup_type = path.pop()
+        if lookup_type == 'pk':
+            lookup_type = 'exact'
+            path.append(None)
+        elif len(path) == 0 or lookup_type not in QUERY_TERMS:
+            path.append(lookup_type)
+            lookup_type = 'exact'
 
-            if len(path) < 1:
-                raise TypeError, "Cannot parse keyword query %r" % kwarg
+        if len(path) < 1:
+            raise TypeError, "Cannot parse keyword query %r" % kwarg
+        
+        if value is None:
+            # Interpret '__exact=None' as the sql '= NULL'; otherwise, reject
+            # all uses of None as a query value.
+            if lookup_type != 'exact':
+                raise ValueError, "Cannot use None as a query value"
 
-            joins2, where2, params2 = lookup_inner(path, lookup_type, value, opts, opts.db_table, None)
-            joins.update(joins2)
-            where.extend(where2)
-            params.extend(params2)
+        joins2, where2, params2 = lookup_inner(path, lookup_type, value, opts, opts.db_table, None)
+        joins.update(joins2)
+        where.extend(where2)
+        params.extend(params2)
     return joins, where, params
 
 class FieldFound(Exception):
diff --git a/docs/db-api.txt b/docs/db-api.txt
index 0d1f049601..2f0c8b0589 100644
--- a/docs/db-api.txt
+++ b/docs/db-api.txt
@@ -876,15 +876,18 @@ The database API supports the following lookup types:
 exact
 ~~~~~
 
-Exact match.
+Exact match. If the value provided for comparison is ``None``, it will 
+be interpreted as an SQL ``NULL`` (See isnull_ for more details).  
 
-Example::
+Examples::
 
     Entry.objects.get(id__exact=14)
+    Entry.objects.get(id__exact=None)
 
-SQL equivalent::
+SQL equivalents::
 
     SELECT ... WHERE id = 14;
+    SELECT ... WHERE id = NULL;
 
 iexact
 ~~~~~~
@@ -1103,8 +1106,8 @@ such as January 3, July 3, etc.
 isnull
 ~~~~~~
 
-``NULL`` or ``IS NOT NULL`` match. Takes either ``True`` or ``False``, which
-correspond to ``IS NULL`` and ``IS NOT NULL``, respectively.
+Takes either ``True`` or ``False``, which correspond to SQL queries of 
+``IS NULL`` and ``IS NOT NULL``, respectively.
 
 Example::
 
@@ -1114,6 +1117,14 @@ SQL equivalent::
 
     SELECT ... WHERE pub_date IS NULL;
 
+.. admonition:: ``__isnull=True`` vs ``__exact=None``
+
+    There is an important difference between ``__isnull=True`` and 
+    ``__exact=None``. ``__exact=None`` will *always* return an empty result
+    set, because SQL requires that no value is equal to ``NULL``. 
+    ``__isnull`` determines if the field is currently holding the value 
+    of ``NULL`` without performing a comparison.
+
 search
 ~~~~~~
 
diff --git a/tests/regressiontests/null_queries/__init__.py b/tests/regressiontests/null_queries/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/regressiontests/null_queries/models.py b/tests/regressiontests/null_queries/models.py
new file mode 100644
index 0000000000..09024f18c2
--- /dev/null
+++ b/tests/regressiontests/null_queries/models.py
@@ -0,0 +1,54 @@
+from django.db import models
+
+class Poll(models.Model):
+    question = models.CharField(maxlength=200)
+
+    def __str__(self):
+        return "Q: %s " % self.question
+
+class Choice(models.Model):
+    poll = models.ForeignKey(Poll)
+    choice = models.CharField(maxlength=200)
+
+    def __str__(self):
+        return "Choice: %s in poll %s" % (self.choice, self.poll)
+
+__test__ = {'API_TESTS':"""
+# Regression test for the use of None as a query value. None is interpreted as 
+# an SQL NULL, but only in __exact queries.
+# Set up some initial polls and choices
+>>> p1 = Poll(question='Why?')
+>>> p1.save()
+>>> c1 = Choice(poll=p1, choice='Because.')
+>>> c1.save()
+>>> c2 = Choice(poll=p1, choice='Why Not?')
+>>> c2.save()
+
+# Exact query with value None returns nothing (=NULL in sql)
+>>> Choice.objects.filter(id__exact=None)
+[]
+
+# Valid query, but fails because foo isn't a keyword
+>>> Choice.objects.filter(foo__exact=None) 
+Traceback (most recent call last):
+...
+TypeError: Cannot resolve keyword 'foo' into field
+
+# Can't use None on anything other than __exact
+>>> Choice.objects.filter(id__gt=None)
+Traceback (most recent call last):
+...
+ValueError: Cannot use None as a query value
+
+# Can't use None on anything other than __exact
+>>> Choice.objects.filter(foo__gt=None)
+Traceback (most recent call last):
+...
+ValueError: Cannot use None as a query value
+
+# Related managers use __exact=None implicitly if the object hasn't been saved.
+>>> p2 = Poll(question="How?")
+>>> p2.choice_set.all()
+[]
+
+"""}