From fbbbf8b9a14ebd5752529009523729e7d87989e8 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee 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() +[] + +"""}