Fixed #7210 -- Added F() expressions to query language. See the documentation for details on usage.

Many thanks to:
    * Nicolas Lara, who worked on this feature during the 2008 Google Summer of Code.
    * Alex Gaynor for his help debugging and fixing a number of issues.
    * Malcolm Tredinnick for his invaluable review notes.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@9792 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2009-01-29 10:46:36 +00:00
parent 08dd4176ed
commit cf37e4624a
16 changed files with 586 additions and 48 deletions

View File

@ -3,6 +3,7 @@ from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
from django.db import connection
from django.db.models.loading import get_apps, get_app, get_models, get_model, register_models
from django.db.models.query import Q
from django.db.models.expressions import F
from django.db.models.manager import Manager
from django.db.models.base import Model
from django.db.models.aggregates import *

View File

@ -0,0 +1,110 @@
from copy import deepcopy
from datetime import datetime
from django.utils import tree
class ExpressionNode(tree.Node):
"""
Base class for all query expressions.
"""
# Arithmetic connectors
ADD = '+'
SUB = '-'
MUL = '*'
DIV = '/'
MOD = '%%' # This is a quoted % operator - it is quoted
# because it can be used in strings that also
# have parameter substitution.
# Bitwise operators
AND = '&'
OR = '|'
def __init__(self, children=None, connector=None, negated=False):
if children is not None and len(children) > 1 and connector is None:
raise TypeError('You have to specify a connector.')
super(ExpressionNode, self).__init__(children, connector, negated)
def _combine(self, other, connector, reversed, node=None):
if reversed:
obj = ExpressionNode([other], connector)
obj.add(node or self, connector)
else:
obj = node or ExpressionNode([self], connector)
obj.add(other, connector)
return obj
###################
# VISITOR METHODS #
###################
def prepare(self, evaluator, query, allow_joins):
return evaluator.prepare_node(self, query, allow_joins)
def evaluate(self, evaluator, qn):
return evaluator.evaluate_node(self, qn)
#############
# OPERATORS #
#############
def __add__(self, other):
return self._combine(other, self.ADD, False)
def __sub__(self, other):
return self._combine(other, self.SUB, False)
def __mul__(self, other):
return self._combine(other, self.MUL, False)
def __div__(self, other):
return self._combine(other, self.DIV, False)
def __mod__(self, other):
return self._combine(other, self.MOD, False)
def __and__(self, other):
return self._combine(other, self.AND, False)
def __or__(self, other):
return self._combine(other, self.OR, False)
def __radd__(self, other):
return self._combine(other, self.ADD, True)
def __rsub__(self, other):
return self._combine(other, self.SUB, True)
def __rmul__(self, other):
return self._combine(other, self.MUL, True)
def __rdiv__(self, other):
return self._combine(other, self.DIV, True)
def __rmod__(self, other):
return self._combine(other, self.MOD, True)
def __rand__(self, other):
return self._combine(other, self.AND, True)
def __ror__(self, other):
return self._combine(other, self.OR, True)
class F(ExpressionNode):
"""
An expression representing the value of the given field.
"""
def __init__(self, name):
super(F, self).__init__(None, None, False)
self.name = name
def __deepcopy__(self, memodict):
obj = super(F, self).__deepcopy__(memodict)
obj.name = self.name
return obj
def prepare(self, evaluator, query, allow_joins):
return evaluator.prepare_leaf(self, query, allow_joins)
def evaluate(self, evaluator, qn):
return evaluator.evaluate_leaf(self, qn)

View File

@ -194,8 +194,13 @@ class Field(object):
def get_db_prep_lookup(self, lookup_type, value):
"Returns field's value prepared for database lookup."
if hasattr(value, 'as_sql'):
# If the value has a relabel_aliases method, it will need to
# be invoked before the final SQL is evaluated
if hasattr(value, 'relabel_aliases'):
return value
sql, params = value.as_sql()
return QueryWrapper(('(%s)' % sql), params)
if lookup_type in ('regex', 'iregex', 'month', 'day', 'search'):
return [value]
elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte'):
@ -309,7 +314,7 @@ class Field(object):
if callable(self.default):
defaults['show_hidden_initial'] = True
if self.choices:
# Fields with choices get special treatment.
# Fields with choices get special treatment.
include_blank = self.blank or not (self.has_default() or 'initial' in kwargs)
defaults['choices'] = self.get_choices(include_blank=include_blank)
defaults['coerce'] = self.to_python

View File

@ -141,6 +141,10 @@ class RelatedField(object):
return v
if hasattr(value, 'as_sql'):
# If the value has a relabel_aliases method, it will need to
# be invoked before the final SQL is evaluated
if hasattr(value, 'relabel_aliases'):
return value
sql, params = value.as_sql()
return QueryWrapper(('(%s)' % sql), params)

View File

@ -17,6 +17,9 @@ class QueryWrapper(object):
def __init__(self, sql, params):
self.data = sql, params
def as_sql(self, qn=None):
return self.data
class Q(tree.Node):
"""
Encapsulates filters as objects that can then be combined logically (using

View File

@ -0,0 +1,92 @@
from django.core.exceptions import FieldError
from django.db import connection
from django.db.models.fields import FieldDoesNotExist
from django.db.models.sql.constants import LOOKUP_SEP
class SQLEvaluator(object):
def __init__(self, expression, query, allow_joins=True):
self.expression = expression
self.opts = query.get_meta()
self.cols = {}
self.contains_aggregate = False
self.expression.prepare(self, query, allow_joins)
def as_sql(self, qn=None):
return self.expression.evaluate(self, qn)
def relabel_aliases(self, change_map):
for node, col in self.cols.items():
self.cols[node] = (change_map.get(col[0], col[0]), col[1])
#####################################################
# Vistor methods for initial expression preparation #
#####################################################
def prepare_node(self, node, query, allow_joins):
for child in node.children:
if hasattr(child, 'prepare'):
child.prepare(self, query, allow_joins)
def prepare_leaf(self, node, query, allow_joins):
if not allow_joins and LOOKUP_SEP in node.name:
raise FieldError("Joined field references are not permitted in this query")
field_list = node.name.split(LOOKUP_SEP)
if (len(field_list) == 1 and
node.name in query.aggregate_select.keys()):
self.contains_aggregate = True
self.cols[node] = query.aggregate_select[node.name]
else:
try:
field, source, opts, join_list, last, _ = query.setup_joins(
field_list, query.get_meta(),
query.get_initial_alias(), False)
_, _, col, _, join_list = query.trim_joins(source, join_list, last, False)
self.cols[node] = (join_list[-1], col)
except FieldDoesNotExist:
raise FieldError("Cannot resolve keyword %r into field. "
"Choices are: %s" % (self.name,
[f.name for f in self.opts.fields]))
##################################################
# Vistor methods for final expression evaluation #
##################################################
def evaluate_node(self, node, qn):
if not qn:
qn = connection.ops.quote_name
expressions = []
expression_params = []
for child in node.children:
if hasattr(child, 'evaluate'):
sql, params = child.evaluate(self, qn)
else:
try:
sql, params = qn(child), ()
except:
sql, params = str(child), ()
if hasattr(child, 'children') > 1:
format = '(%s)'
else:
format = '%s'
if sql:
expressions.append(format % sql)
expression_params.extend(params)
conn = ' %s ' % node.connector
return conn.join(expressions), expression_params
def evaluate_leaf(self, node, qn):
if not qn:
qn = connection.ops.quote_name
col = self.cols[node]
if hasattr(col, 'as_sql'):
return col.as_sql(qn), ()
else:
return '%s.%s' % (qn(col[0]), qn(col[1])), ()

View File

@ -18,6 +18,7 @@ from django.db.models import signals
from django.db.models.fields import FieldDoesNotExist
from django.db.models.query_utils import select_related_descend
from django.db.models.sql import aggregates as base_aggregates_module
from django.db.models.sql.expressions import SQLEvaluator
from django.db.models.sql.where import WhereNode, Constraint, EverythingNode, AND, OR
from django.core.exceptions import FieldError
from datastructures import EmptyResultSet, Empty, MultiJoin
@ -1271,6 +1272,10 @@ class BaseQuery(object):
else:
lookup_type = parts.pop()
# By default, this is a WHERE clause. If an aggregate is referenced
# in the value, the filter will be promoted to a HAVING
having_clause = False
# Interpret '__exact=None' as the sql 'is NULL'; otherwise, reject all
# uses of None as a query value.
if value is None:
@ -1284,6 +1289,10 @@ class BaseQuery(object):
value = True
elif callable(value):
value = value()
elif hasattr(value, 'evaluate'):
# If value is a query expression, evaluate it
value = SQLEvaluator(value, self)
having_clause = value.contains_aggregate
for alias, aggregate in self.aggregate_select.items():
if alias == parts[0]:
@ -1340,8 +1349,13 @@ class BaseQuery(object):
self.promote_alias_chain(join_it, join_promote)
self.promote_alias_chain(table_it, table_promote)
self.where.add((Constraint(alias, col, field), lookup_type, value),
connector)
if having_clause:
self.having.add((Constraint(alias, col, field), lookup_type, value),
connector)
else:
self.where.add((Constraint(alias, col, field), lookup_type, value),
connector)
if negate:
self.promote_alias_chain(join_list)

View File

@ -5,6 +5,7 @@ Query subclasses which provide extra functionality beyond simple data retrieval.
from django.core.exceptions import FieldError
from django.db.models.sql.constants import *
from django.db.models.sql.datastructures import Date
from django.db.models.sql.expressions import SQLEvaluator
from django.db.models.sql.query import Query
from django.db.models.sql.where import AND, Constraint
@ -136,7 +137,11 @@ class UpdateQuery(Query):
result.append('SET')
values, update_params = [], []
for name, val, placeholder in self.values:
if val is not None:
if hasattr(val, 'as_sql'):
sql, params = val.as_sql(qn)
values.append('%s = %s' % (qn(name), sql))
update_params.extend(params)
elif val is not None:
values.append('%s = %s' % (qn(name), placeholder))
update_params.append(val)
else:
@ -251,6 +256,8 @@ class UpdateQuery(Query):
else:
placeholder = '%s'
if hasattr(val, 'evaluate'):
val = SQLEvaluator(val, self, allow_joins=False)
if model:
self.add_related_update(model, field.column, val, placeholder)
else:

View File

@ -97,6 +97,7 @@ class WhereNode(tree.Node):
else:
# A leaf node in the tree.
sql, params = self.make_atom(child, qn)
except EmptyResultSet:
if self.connector == AND and not self.negated:
# We can bail out early in this particular case (only).
@ -114,6 +115,7 @@ class WhereNode(tree.Node):
if self.negated:
empty = True
continue
empty = False
if sql:
result.append(sql)
@ -151,8 +153,9 @@ class WhereNode(tree.Node):
else:
cast_sql = '%s'
if isinstance(params, QueryWrapper):
extra, params = params.data
if hasattr(params, 'as_sql'):
extra, params = params.as_sql(qn)
cast_sql = ''
else:
extra = ''
@ -214,6 +217,9 @@ class WhereNode(tree.Node):
if elt[0] in change_map:
elt[0] = change_map[elt[0]]
node.children[pos] = (tuple(elt),) + child[1:]
# Check if the query value also requires relabelling
if hasattr(child[3], 'relabel_aliases'):
child[3].relabel_aliases(change_map)
class EverythingNode(object):
"""

View File

@ -163,7 +163,7 @@ table (usually called ``django_session`` and the table
Connecting to the database
--------------------------
Refer to the :ref:`settings documentation <ref-settings>`.
Refer to the :ref:`settings documentation <ref-settings>`.
Connection settings are used in this order:
@ -262,9 +262,9 @@ of whether ``unique=True`` is specified or not.
.. _sqlite-notes:
SQLite notes
============
SQLite notes
============
SQLite_ provides an excellent development alternative for applications that
are predominantly read-only or require a smaller installation footprint. As
with all database servers, though, there are some differences that are
@ -294,21 +294,21 @@ the ``extra()`` QuerySet method. The bug can be identified by the error message
``OperationalError: ORDER BY terms must not be non-integer constants``. The
problem can be solved updating SQLite to version 3.3.6 or newer, possibly also
updating the ``pysqlite2`` Python module in the process.
.. _contain a bug: http://www.sqlite.org/cvstrac/tktview?tn=1768
This has a very low impact because 3.3.6 was released in April 2006, so most
current binary distributions for different platforms include newer version of
SQLite usable from Python through either the ``pysqlite2`` or the ``sqlite3``
modules.
However, in the case of Windows, the official binary distribution of the stable
release of Python 2.5 (2.5.2, as of this writing) includes SQLite 3.3.4, so the bug can
make itself evident in that platform. There are (as of Django 1.0) even three
tests in the Django test suite that will fail when run under this setup. As
described above, this can be solved by downloading and installing a newer
version of ``pysqlite2`` (``pysqlite-2.x.x.win32-py2.5.exe``) that includes and
uses a newer version of SQLite. Python 2.6 ships with a newer version of
.. _contain a bug: http://www.sqlite.org/cvstrac/tktview?tn=1768
This has a very low impact because 3.3.6 was released in April 2006, so most
current binary distributions for different platforms include newer version of
SQLite usable from Python through either the ``pysqlite2`` or the ``sqlite3``
modules.
However, in the case of Windows, the official binary distribution of the stable
release of Python 2.5 (2.5.2, as of this writing) includes SQLite 3.3.4, so the bug can
make itself evident in that platform. There are (as of Django 1.0) even three
tests in the Django test suite that will fail when run under this setup. As
described above, this can be solved by downloading and installing a newer
version of ``pysqlite2`` (``pysqlite-2.x.x.win32-py2.5.exe``) that includes and
uses a newer version of SQLite. Python 2.6 ships with a newer version of
SQLite and is not affected by this issue.
If you are in such platform and find yourself in the need to update
@ -317,6 +317,23 @@ If you are in such platform and find yourself in the need to update
attempts to import ``pysqlite2`` before than ``sqlite3`` and so it can take
advantage of the new ``pysqlite2``/SQLite versions.
Version 3.5.9
-------------
The Ubuntu "Intrepid Ibex" SQLite 3.5.9-3 package contains a bug that causes
problems with the evaluation of query expressions. If you are using Ubuntu
"Intrepid Ibex", you will need to find an alternate source for SQLite
packages, or install SQLite from source.
At one time, Debian Lenny shipped with the same malfunctioning SQLite 3.5.9-3
package. However the Debian project has subsequently issued updated versions
of the SQLite package that correct these bugs. If you find you are getting
unexpected results under Debian, ensure you have updated your SQLite package
to 3.5.9-5 or later.
The problem does not appear to exist with other versions of SQLite packaged
with other operating systems.
Version 3.6.2
--------------
@ -348,14 +365,14 @@ database user must have privileges to run the following commands:
* CREATE SEQUENCE
* CREATE PROCEDURE
* CREATE TRIGGER
To run Django's test suite, the user needs these *additional* privileges:
* CREATE USER
* DROP USER
* CREATE TABLESPACE
* DROP TABLESPACE
Connecting to the database
--------------------------

View File

@ -8,8 +8,8 @@ Making queries
Once you've created your :ref:`data models <topics-db-models>`, Django
automatically gives you a database-abstraction API that lets you create,
retrieve, update and delete objects. This document explains how to use this
API. Refer to the :ref:`data model reference <ref-models-index>` for full
retrieve, update and delete objects. This document explains how to use this
API. Refer to the :ref:`data model reference <ref-models-index>` for full
details of all the various model lookup options.
Throughout this guide (and in the reference), we'll refer to the following
@ -39,6 +39,9 @@ models, which comprise a weblog application:
body_text = models.TextField()
pub_date = models.DateTimeField()
authors = models.ManyToManyField(Author)
n_comments = models.IntegerField()
n_pingbacks = models.IntegerField()
rating = models.IntegerField()
def __unicode__(self):
return self.headline
@ -94,11 +97,11 @@ Saving ``ForeignKey`` and ``ManyToManyField`` fields
----------------------------------------------------
Updating ``ForeignKey`` fields works exactly the same way as saving a normal
field; simply assign an object of the right type to the field in question::
field; simply assign an object of the right type to the field in question::
>>> cheese_blog = Blog.objects.get(name="Cheddar Talk")
>>> entry.blog = cheese_blog
>>> entry.save()
>>> cheese_blog = Blog.objects.get(name="Cheddar Talk")
>>> entry.blog = cheese_blog
>>> entry.save()
Updating a ``ManyToManyField`` works a little differently; use the ``add()``
method on the field to add a record to the relation::
@ -245,7 +248,7 @@ this example::
>>> q = q.filter(pub_date__lte=datetime.now())
>>> q = q.exclude(body_text__icontains="food")
>>> print q
Though this looks like three database hits, in fact it hits the database only
once, at the last line (``print q``). In general, the results of a ``QuerySet``
aren't fetched from the database until you "ask" for them. When you do, the
@ -333,15 +336,15 @@ you'll probably use:
:lookup:`exact`
An "exact" match. For example::
>>> Entry.objects.get(headline__exact="Man bites dog")
Would generate SQL along these lines:
.. code-block:: sql
SELECT ... WHERE headline = 'Man bites dog';
If you don't provide a lookup type -- that is, if your keyword argument
doesn't contain a double underscore -- the lookup type is assumed to be
``exact``.
@ -352,36 +355,36 @@ you'll probably use:
>>> Blog.objects.get(id=14) # __exact is implied
This is for convenience, because ``exact`` lookups are the common case.
:lookup:`iexact`
A case-insensitive match. So, the query::
>>> Blog.objects.get(name__iexact="beatles blog")
Would match a ``Blog`` titled "Beatles Blog", "beatles blog", or even
"BeAtlES blOG".
:lookup:`contains`
Case-sensitive containment test. For example::
Entry.objects.get(headline__contains='Lennon')
Roughly translates to this SQL:
.. code-block:: sql
SELECT ... WHERE headline LIKE '%Lennon%';
Note this will match the headline ``'Today Lennon honored'`` but not
``'today lennon honored'``.
There's also a case-insensitive version, :lookup:`icontains`.
:lookup:`startswith`, :lookup:`endswith`
Starts-with and ends-with search, respectively. There are also
case-insensitive versions called :lookup:`istartswith` and
:lookup:`iendswith`.
Again, this only scratches the surface. A complete reference can be found in the
:ref:`field lookup reference <field-lookups>`.
@ -485,6 +488,48 @@ are talking about the same multi-valued relation). Conditions in subsequent
``filter()`` or ``exclude()`` calls that refer to the same relation may end up
filtering on different linked objects.
.. _query-expressions:
Filters can reference fields on the model
-----------------------------------------
.. versionadded:: 1.1
In the examples given so far, we have constructed filters that compare
the value of a model field with a constant. But what if you want to compare
the value of a model field with another field on the same model?
Django provides the ``F()`` object to allow such comparisons. Instances
of ``F()`` act as a reference to a model field within a query. These
references can then be used in query filters to compare the values of two
different fields on the same model instance.
For example, to find a list of all blog entries that have had more comments
than pingbacks, we construct an ``F()`` object to reference the comment count,
and use that ``F()`` object in the query::
>>> Entry.objects.filter(n_pingbacks__lt=F('n_comments'))
Django supports the use of addition, subtraction, multiplication,
division and modulo arithmetic with ``F()`` objects, both with constants
and with other ``F()`` objects. To find all the blog entries with *twice* as
many comments as pingbacks, we modify the query::
>>> Entry.objects.filter(n_pingbacks__lt=F('n_comments') * 2)
To find all the entries where the sum of the pingback count and comment count
is greater than the rating of the entry, we would issue the query::
>>> Entry.objects.filter(rating__lt=F('n_comments') + F('n_pingbacks'))
You can also use the double underscore notation to span relationships in
an ``F()`` object. An ``F()`` object with a double underscore will introduce
any joins needed to access the related object. For example, to retrieve all
the entries where the author's name is the same as the blog name, we could
issue the query:
>>> Entry.objects.filter(author__name=F('blog__name'))
The pk lookup shortcut
----------------------
@ -503,7 +548,7 @@ can be combined with ``pk`` to perform a query on the primary key of a model::
# Get blogs entries with id 1, 4 and 7
>>> Blog.objects.filter(pk__in=[1,4,7])
# Get all blog entries with id > 14
>>> Blog.objects.filter(pk__gt=14)
@ -728,7 +773,7 @@ To update ``ForeignKey`` fields, set the new value to be the new model
instance you want to point to. Example::
>>> b = Blog.objects.get(pk=1)
# Change every Entry so that it belongs to this Blog.
>>> Entry.objects.all().update(blog=b)
@ -749,6 +794,21 @@ Just loop over them and call ``save()``::
for item in my_queryset:
item.save()
Calls to update can also use :ref:`F() objects <query-expressions>` to update
one field based on the value of another field in the model. This is especially
useful for incrementing counters based upon their current value. For example, to
increment the pingback count for every entry in the blog::
>>> Entry.objects.all().update(n_pingbacks=F('n_pingbacks') + 1)
However, unlike ``F()`` objects in filter and exclude clauses, you can't
introduce joins when you use ``F()`` objects in an update -- you can only
reference fields local to the model being updated. If you attempt to introduce
a join with an ``F()`` object, a ``FieldError`` will be raised::
# THIS WILL RAISE A FieldError
>>> Entry.objects.update(headline=F('blog__name'))
Related objects
===============

View File

View File

@ -0,0 +1,71 @@
"""
Tests for F() query expression syntax.
"""
from django.db import models
class Employee(models.Model):
firstname = models.CharField(max_length=50)
lastname = models.CharField(max_length=50)
def __unicode__(self):
return u'%s %s' % (self.firstname, self.lastname)
class Company(models.Model):
name = models.CharField(max_length=100)
num_employees = models.PositiveIntegerField()
num_chairs = models.PositiveIntegerField()
ceo = models.ForeignKey(
Employee,
related_name='company_ceo_set')
point_of_contact = models.ForeignKey(
Employee,
related_name='company_point_of_contact_set',
null=True)
def __unicode__(self):
return self.name
__test__ = {'API_TESTS': """
>>> from django.db.models import F
>>> Company(name='Example Inc.', num_employees=2300, num_chairs=5,
... ceo=Employee.objects.create(firstname='Joe', lastname='Smith')).save()
>>> Company(name='Foobar Ltd.', num_employees=3, num_chairs=3,
... ceo=Employee.objects.create(firstname='Frank', lastname='Meyer')).save()
>>> Company(name='Test GmbH', num_employees=32, num_chairs=1,
... ceo=Employee.objects.create(firstname='Max', lastname='Mustermann')).save()
# We can filter for companies where the number of employees is greater than the
# number of chairs.
>>> Company.objects.filter(num_employees__gt=F('num_chairs'))
[<Company: Example Inc.>, <Company: Test GmbH>]
# The relation of a foreign key can become copied over to an other foreign key.
>>> Company.objects.update(point_of_contact=F('ceo'))
3
>>> [c.point_of_contact for c in Company.objects.all()]
[<Employee: Joe Smith>, <Employee: Frank Meyer>, <Employee: Max Mustermann>]
>>> c = Company.objects.all()[0]
>>> c.point_of_contact = Employee.objects.create(firstname="Guido", lastname="van Rossum")
>>> c.save()
# F Expressions can also span joins
>>> Company.objects.filter(ceo__firstname=F('point_of_contact__firstname')).distinct()
[<Company: Foobar Ltd.>, <Company: Test GmbH>]
>>> _ = Company.objects.exclude(ceo__firstname=F('point_of_contact__firstname')).update(name='foo')
>>> Company.objects.exclude(ceo__firstname=F('point_of_contact__firstname')).get().name
u'foo'
>>> _ = Company.objects.exclude(ceo__firstname=F('point_of_contact__firstname')).update(name=F('point_of_contact__lastname'))
Traceback (most recent call last):
...
FieldError: Joined field references are not permitted in this query
"""}

View File

@ -50,7 +50,7 @@ class Store(models.Model):
#Extra does not play well with values. Modify the tests if/when this is fixed.
__test__ = {'API_TESTS': """
>>> from django.core import management
>>> from django.db.models import get_app
>>> from django.db.models import get_app, F
# Reset the database representation of this app.
# This will return the database to a clean initial state.
@ -164,6 +164,21 @@ FieldError: Cannot resolve keyword 'foo' into field. Choices are: authors, id, i
>>> len(Book.objects.annotate(num_authors=Count('authors')).exclude(num_authors__lt=2).filter(num_authors__lt=3))
2
# Aggregates can be used with F() expressions
# ... where the F() is pushed into the HAVING clause
>>> Publisher.objects.annotate(num_books=Count('book')).filter(num_books__lt=F('num_awards')/2).values('name','num_books','num_awards')
[{'num_books': 2, 'name': u'Prentice Hall', 'num_awards': 7}, {'num_books': 1, 'name': u'Morgan Kaufmann', 'num_awards': 9}]
>>> Publisher.objects.annotate(num_books=Count('book')).exclude(num_books__lt=F('num_awards')/2).values('name','num_books','num_awards')
[{'num_books': 2, 'name': u'Apress', 'num_awards': 3}, {'num_books': 1, 'name': u'Sams', 'num_awards': 1}, {'num_books': 0, 'name': u"Jonno's House of Books", 'num_awards': 0}]
# ... and where the F() references an aggregate
>>> Publisher.objects.annotate(num_books=Count('book')).filter(num_awards__gt=2*F('num_books')).values('name','num_books','num_awards')
[{'num_books': 2, 'name': u'Prentice Hall', 'num_awards': 7}, {'num_books': 1, 'name': u'Morgan Kaufmann', 'num_awards': 9}]
>>> Publisher.objects.annotate(num_books=Count('book')).exclude(num_books__lt=F('num_awards')/2).values('name','num_books','num_awards')
[{'num_books': 2, 'name': u'Apress', 'num_awards': 3}, {'num_books': 1, 'name': u'Sams', 'num_awards': 1}, {'num_books': 0, 'name': u"Jonno's House of Books", 'num_awards': 0}]
# Regression for #10089: Check handling of empty result sets with aggregates
>>> Book.objects.filter(id__in=[]).count()
0

View File

@ -0,0 +1,133 @@
"""
Spanning tests for all the operations that F() expressions can perform.
"""
from django.db import models
#
# Model for testing arithmetic expressions.
#
class Number(models.Model):
integer = models.IntegerField()
float = models.FloatField(null=True)
def __unicode__(self):
return u'%i, %.3f' % (self.integer, self.float)
__test__ = {'API_TESTS': """
>>> from django.db.models import F
>>> Number(integer=-1).save()
>>> Number(integer=42).save()
>>> Number(integer=1337).save()
We can fill a value in all objects with an other value of the same object.
>>> Number.objects.update(float=F('integer'))
3
>>> Number.objects.all()
[<Number: -1, -1.000>, <Number: 42, 42.000>, <Number: 1337, 1337.000>]
We can increment a value of all objects in a query set.
>>> Number.objects.filter(integer__gt=0).update(integer=F('integer') + 1)
2
>>> Number.objects.all()
[<Number: -1, -1.000>, <Number: 43, 42.000>, <Number: 1338, 1337.000>]
We can filter for objects, where a value is not equals the value of an other field.
>>> Number.objects.exclude(float=F('integer'))
[<Number: 43, 42.000>, <Number: 1338, 1337.000>]
Complex expressions of different connection types are possible.
>>> n = Number.objects.create(integer=10, float=123.45)
>>> Number.objects.filter(pk=n.pk).update(float=F('integer') + F('float') * 2)
1
>>> Number.objects.get(pk=n.pk)
<Number: 10, 256.900>
# All supported operators work as expected.
>>> n = Number.objects.create(integer=42, float=15.5)
# Left hand operators
>>> _ = Number.objects.filter(pk=n.pk).update(integer=42, float=15.5)
>>> _ = Number.objects.filter(pk=n.pk).update(integer=F('integer') + 15, float=F('float') + 42.7)
>>> Number.objects.get(pk=n.pk) # LH Addition of floats and integers
<Number: 57, 58.200>
>>> _ = Number.objects.filter(pk=n.pk).update(integer=42, float=15.5)
>>> _ = Number.objects.filter(pk=n.pk).update(integer=F('integer') - 15, float=F('float') - 42.7)
>>> Number.objects.get(pk=n.pk) # LH Subtraction of floats and integers
<Number: 27, -27.200>
>>> _ = Number.objects.filter(pk=n.pk).update(integer=42, float=15.5)
>>> _ = Number.objects.filter(pk=n.pk).update(integer=F('integer') * 15, float=F('float') * 42.7)
>>> Number.objects.get(pk=n.pk) # Multiplication of floats and integers
<Number: 630, 661.850>
>>> _ = Number.objects.filter(pk=n.pk).update(integer=42, float=15.5)
>>> _ = Number.objects.filter(pk=n.pk).update(integer=F('integer') / 2, float=F('float') / 42.7)
>>> Number.objects.get(pk=n.pk) # LH Division of floats and integers
<Number: 21, 0.363>
>>> _ = Number.objects.filter(pk=n.pk).update(integer=42, float=15.5)
>>> _ = Number.objects.filter(pk=n.pk).update(integer=F('integer') % 20)
>>> Number.objects.get(pk=n.pk) # LH Modulo arithmetic on integers
<Number: 2, 15.500>
>>> _ = Number.objects.filter(pk=n.pk).update(integer=42, float=15.5)
>>> _ = Number.objects.filter(pk=n.pk).update(integer=F('integer') & 56)
>>> Number.objects.get(pk=n.pk) # LH Bitwise ands on integers
<Number: 40, 15.500>
>>> _ = Number.objects.filter(pk=n.pk).update(integer=42, float=15.5)
>>> _ = Number.objects.filter(pk=n.pk).update(integer=F('integer') | 48)
>>> Number.objects.get(pk=n.pk) # LH Bitwise or on integers
<Number: 58, 15.500>
# Right hand operators
>>> _ = Number.objects.filter(pk=n.pk).update(integer=42, float=15.5)
>>> _ = Number.objects.filter(pk=n.pk).update(integer=15 + F('integer'), float=42.7 + F('float'))
>>> Number.objects.get(pk=n.pk) # RH Addition of floats and integers
<Number: 57, 58.200>
>>> _ = Number.objects.filter(pk=n.pk).update(integer=42, float=15.5)
>>> _ = Number.objects.filter(pk=n.pk).update(integer=15 - F('integer'), float=42.7 - F('float'))
>>> Number.objects.get(pk=n.pk) # RH Subtraction of floats and integers
<Number: -27, 27.200>
>>> _ = Number.objects.filter(pk=n.pk).update(integer=42, float=15.5)
>>> _ = Number.objects.filter(pk=n.pk).update(integer=15 * F('integer'), float=42.7 * F('float'))
>>> Number.objects.get(pk=n.pk) # RH Multiplication of floats and integers
<Number: 630, 661.850>
>>> _ = Number.objects.filter(pk=n.pk).update(integer=42, float=15.5)
>>> _ = Number.objects.filter(pk=n.pk).update(integer=640 / F('integer'), float=42.7 / F('float'))
>>> Number.objects.get(pk=n.pk) # RH Division of floats and integers
<Number: 15, 2.755>
>>> _ = Number.objects.filter(pk=n.pk).update(integer=42, float=15.5)
>>> _ = Number.objects.filter(pk=n.pk).update(integer=69 % F('integer'))
>>> Number.objects.get(pk=n.pk) # RH Modulo arithmetic on integers
<Number: 27, 15.500>
>>> _ = Number.objects.filter(pk=n.pk).update(integer=42, float=15.5)
>>> _ = Number.objects.filter(pk=n.pk).update(integer=15 & F('integer'))
>>> Number.objects.get(pk=n.pk) # RH Bitwise ands on integers
<Number: 10, 15.500>
>>> _ = Number.objects.filter(pk=n.pk).update(integer=42, float=15.5)
>>> _ = Number.objects.filter(pk=n.pk).update(integer=15 | F('integer'))
>>> Number.objects.get(pk=n.pk) # RH Bitwise or on integers
<Number: 47, 15.500>
"""}