Fixed #25367 -- Allowed boolean expressions in QuerySet.filter() and exclude().
This allows using expressions that have an output_field that is a BooleanField to be used directly in a queryset filters, or in the When() clauses of a Case() expression. Thanks Josh Smeaton, Tim Graham, Simon Charette, Mariusz Felisiak, and Adam Johnson for reviews. Co-Authored-By: NyanKiyoshi <hello@vanille.bid>
This commit is contained in:
parent
069bee7c12
commit
4137fc2efc
|
@ -581,6 +581,13 @@ class BaseDatabaseOperations:
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def conditional_expression_supported_in_where_clause(self, expression):
|
||||||
|
"""
|
||||||
|
Return True, if the conditional expression is supported in the WHERE
|
||||||
|
clause.
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
def combine_expression(self, connector, sub_expressions):
|
def combine_expression(self, connector, sub_expressions):
|
||||||
"""
|
"""
|
||||||
Combine a list of subexpressions into a single expression, using
|
Combine a list of subexpressions into a single expression, using
|
||||||
|
|
|
@ -6,6 +6,8 @@ from functools import lru_cache
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.backends.base.operations import BaseDatabaseOperations
|
from django.db.backends.base.operations import BaseDatabaseOperations
|
||||||
from django.db.backends.utils import strip_quotes, truncate_name
|
from django.db.backends.utils import strip_quotes, truncate_name
|
||||||
|
from django.db.models.expressions import Exists, ExpressionWrapper
|
||||||
|
from django.db.models.query_utils import Q
|
||||||
from django.db.utils import DatabaseError
|
from django.db.utils import DatabaseError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.encoding import force_bytes, force_str
|
from django.utils.encoding import force_bytes, force_str
|
||||||
|
@ -607,3 +609,14 @@ END;
|
||||||
if fields:
|
if fields:
|
||||||
return self.connection.features.max_query_params // len(fields)
|
return self.connection.features.max_query_params // len(fields)
|
||||||
return len(objs)
|
return len(objs)
|
||||||
|
|
||||||
|
def conditional_expression_supported_in_where_clause(self, expression):
|
||||||
|
"""
|
||||||
|
Oracle supports only EXISTS(...) or filters in the WHERE clause, others
|
||||||
|
must be compared with True.
|
||||||
|
"""
|
||||||
|
if isinstance(expression, Exists):
|
||||||
|
return True
|
||||||
|
if isinstance(expression, ExpressionWrapper) and isinstance(expression.expression, Q):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
|
@ -90,6 +90,8 @@ class Combinable:
|
||||||
return self._combine(other, self.POW, False)
|
return self._combine(other, self.POW, False)
|
||||||
|
|
||||||
def __and__(self, other):
|
def __and__(self, other):
|
||||||
|
if getattr(self, 'conditional', False) and getattr(other, 'conditional', False):
|
||||||
|
return Q(self) & Q(other)
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"Use .bitand() and .bitor() for bitwise logical operations."
|
"Use .bitand() and .bitor() for bitwise logical operations."
|
||||||
)
|
)
|
||||||
|
@ -104,6 +106,8 @@ class Combinable:
|
||||||
return self._combine(other, self.BITRIGHTSHIFT, False)
|
return self._combine(other, self.BITRIGHTSHIFT, False)
|
||||||
|
|
||||||
def __or__(self, other):
|
def __or__(self, other):
|
||||||
|
if getattr(self, 'conditional', False) and getattr(other, 'conditional', False):
|
||||||
|
return Q(self) | Q(other)
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"Use .bitand() and .bitor() for bitwise logical operations."
|
"Use .bitand() and .bitor() for bitwise logical operations."
|
||||||
)
|
)
|
||||||
|
@ -245,6 +249,10 @@ class BaseExpression:
|
||||||
])
|
])
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
@property
|
||||||
|
def conditional(self):
|
||||||
|
return isinstance(self.output_field, fields.BooleanField)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def field(self):
|
def field(self):
|
||||||
return self.output_field
|
return self.output_field
|
||||||
|
@ -873,12 +881,17 @@ class ExpressionWrapper(Expression):
|
||||||
|
|
||||||
class When(Expression):
|
class When(Expression):
|
||||||
template = 'WHEN %(condition)s THEN %(result)s'
|
template = 'WHEN %(condition)s THEN %(result)s'
|
||||||
|
# This isn't a complete conditional expression, must be used in Case().
|
||||||
|
conditional = False
|
||||||
|
|
||||||
def __init__(self, condition=None, then=None, **lookups):
|
def __init__(self, condition=None, then=None, **lookups):
|
||||||
if lookups and condition is None:
|
if lookups and condition is None:
|
||||||
condition, lookups = Q(**lookups), None
|
condition, lookups = Q(**lookups), None
|
||||||
if condition is None or not getattr(condition, 'conditional', False) or lookups:
|
if condition is None or not getattr(condition, 'conditional', False) or lookups:
|
||||||
raise TypeError("__init__() takes either a Q object or lookups as keyword arguments")
|
raise TypeError(
|
||||||
|
'When() supports a Q object, a boolean expression, or lookups '
|
||||||
|
'as a condition.'
|
||||||
|
)
|
||||||
if isinstance(condition, Q) and not condition:
|
if isinstance(condition, Q) and not condition:
|
||||||
raise ValueError("An empty Q() can't be used as a When() condition.")
|
raise ValueError("An empty Q() can't be used as a When() condition.")
|
||||||
super().__init__(output_field=None)
|
super().__init__(output_field=None)
|
||||||
|
@ -1090,6 +1103,7 @@ class Exists(Subquery):
|
||||||
|
|
||||||
class OrderBy(BaseExpression):
|
class OrderBy(BaseExpression):
|
||||||
template = '%(expression)s %(ordering)s'
|
template = '%(expression)s %(ordering)s'
|
||||||
|
conditional = False
|
||||||
|
|
||||||
def __init__(self, expression, descending=False, nulls_first=False, nulls_last=False):
|
def __init__(self, expression, descending=False, nulls_first=False, nulls_last=False):
|
||||||
if nulls_first and nulls_last:
|
if nulls_first and nulls_last:
|
||||||
|
|
|
@ -1229,6 +1229,16 @@ class Query(BaseExpression):
|
||||||
"""
|
"""
|
||||||
if isinstance(filter_expr, dict):
|
if isinstance(filter_expr, dict):
|
||||||
raise FieldError("Cannot parse keyword query as dict")
|
raise FieldError("Cannot parse keyword query as dict")
|
||||||
|
if hasattr(filter_expr, 'resolve_expression') and getattr(filter_expr, 'conditional', False):
|
||||||
|
if connections[DEFAULT_DB_ALIAS].ops.conditional_expression_supported_in_where_clause(filter_expr):
|
||||||
|
condition = filter_expr.resolve_expression(self)
|
||||||
|
else:
|
||||||
|
# Expression is not supported in the WHERE clause, add
|
||||||
|
# comparison with True.
|
||||||
|
condition = self.build_lookup(['exact'], filter_expr.resolve_expression(self), True)
|
||||||
|
clause = self.where_class()
|
||||||
|
clause.add(condition, AND)
|
||||||
|
return clause, []
|
||||||
arg, value = filter_expr
|
arg, value = filter_expr
|
||||||
if not arg:
|
if not arg:
|
||||||
raise FieldError("Cannot parse keyword query %r" % arg)
|
raise FieldError("Cannot parse keyword query %r" % arg)
|
||||||
|
|
|
@ -42,9 +42,15 @@ We'll be using the following model in the subsequent examples::
|
||||||
A ``When()`` object is used to encapsulate a condition and its result for use
|
A ``When()`` object is used to encapsulate a condition and its result for use
|
||||||
in the conditional expression. Using a ``When()`` object is similar to using
|
in the conditional expression. Using a ``When()`` object is similar to using
|
||||||
the :meth:`~django.db.models.query.QuerySet.filter` method. The condition can
|
the :meth:`~django.db.models.query.QuerySet.filter` method. The condition can
|
||||||
be specified using :ref:`field lookups <field-lookups>` or
|
be specified using :ref:`field lookups <field-lookups>`,
|
||||||
:class:`~django.db.models.Q` objects. The result is provided using the ``then``
|
:class:`~django.db.models.Q` objects, or :class:`~django.db.models.Expression`
|
||||||
keyword.
|
objects that have an ``output_field`` that is a
|
||||||
|
:class:`~django.db.models.BooleanField`. The result is provided using the
|
||||||
|
``then`` keyword.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
|
||||||
|
Support for boolean :class:`~django.db.models.Expression` was added.
|
||||||
|
|
||||||
Some examples::
|
Some examples::
|
||||||
|
|
||||||
|
@ -60,6 +66,12 @@ Some examples::
|
||||||
>>> # Complex conditions can be created using Q objects
|
>>> # Complex conditions can be created using Q objects
|
||||||
>>> When(Q(name__startswith="John") | Q(name__startswith="Paul"),
|
>>> When(Q(name__startswith="John") | Q(name__startswith="Paul"),
|
||||||
... then='name')
|
... then='name')
|
||||||
|
>>> # Condition can be created using boolean expressions.
|
||||||
|
>>> from django.db.models import Exists, OuterRef
|
||||||
|
>>> non_unique_account_type = Client.objects.filter(
|
||||||
|
... account_type=OuterRef('account_type'),
|
||||||
|
... ).exclude(pk=OuterRef('pk')).values('pk')
|
||||||
|
>>> When(Exists(non_unique_account_type), then=Value('non unique'))
|
||||||
|
|
||||||
Keep in mind that each of these values can be an expression.
|
Keep in mind that each of these values can be an expression.
|
||||||
|
|
||||||
|
@ -158,9 +170,9 @@ registered more than a year ago::
|
||||||
Advanced queries
|
Advanced queries
|
||||||
================
|
================
|
||||||
|
|
||||||
Conditional expressions can be used in annotations, aggregations, lookups, and
|
Conditional expressions can be used in annotations, aggregations, filters,
|
||||||
updates. They can also be combined and nested with other expressions. This
|
lookups, and updates. They can also be combined and nested with other
|
||||||
allows you to make powerful conditional queries.
|
expressions. This allows you to make powerful conditional queries.
|
||||||
|
|
||||||
Conditional update
|
Conditional update
|
||||||
------------------
|
------------------
|
||||||
|
@ -236,3 +248,29 @@ On other databases, this is emulated using a ``CASE`` statement:
|
||||||
|
|
||||||
The two SQL statements are functionally equivalent but the more explicit
|
The two SQL statements are functionally equivalent but the more explicit
|
||||||
``FILTER`` may perform better.
|
``FILTER`` may perform better.
|
||||||
|
|
||||||
|
Conditional filter
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
|
||||||
|
When a conditional expression returns a boolean value, it is possible to use it
|
||||||
|
directly in filters. This means that it will not be added to the ``SELECT``
|
||||||
|
columns, but you can still use it to filter results::
|
||||||
|
|
||||||
|
>>> non_unique_account_type = Client.objects.filter(
|
||||||
|
... account_type=OuterRef('account_type'),
|
||||||
|
... ).exclude(pk=OuterRef('pk')).values('pk')
|
||||||
|
>>> Client.objects.filter(~Exists(non_unique_account_type))
|
||||||
|
|
||||||
|
In SQL terms, that evaluates to:
|
||||||
|
|
||||||
|
.. code-block:: sql
|
||||||
|
|
||||||
|
SELECT ...
|
||||||
|
FROM client c0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT c1.id
|
||||||
|
FROM client c1
|
||||||
|
WHERE c1.account_type = c0.account_type AND NOT c1.id = c0.id
|
||||||
|
)
|
||||||
|
|
|
@ -5,10 +5,11 @@ Query Expressions
|
||||||
.. currentmodule:: django.db.models
|
.. currentmodule:: django.db.models
|
||||||
|
|
||||||
Query expressions describe a value or a computation that can be used as part of
|
Query expressions describe a value or a computation that can be used as part of
|
||||||
an update, create, filter, order by, annotation, or aggregate. There are a
|
an update, create, filter, order by, annotation, or aggregate. When an
|
||||||
number of built-in expressions (documented below) that can be used to help you
|
expression outputs a boolean value, it may be used directly in filters. There
|
||||||
write queries. Expressions can be combined, or in some cases nested, to form
|
are a number of built-in expressions (documented below) that can be used to
|
||||||
more complex computations.
|
help you write queries. Expressions can be combined, or in some cases nested,
|
||||||
|
to form more complex computations.
|
||||||
|
|
||||||
Supported arithmetic
|
Supported arithmetic
|
||||||
====================
|
====================
|
||||||
|
@ -69,6 +70,12 @@ Some examples
|
||||||
CharField.register_lookup(Length)
|
CharField.register_lookup(Length)
|
||||||
Company.objects.order_by('name__length')
|
Company.objects.order_by('name__length')
|
||||||
|
|
||||||
|
# Boolean expression can be used directly in filters.
|
||||||
|
from django.db.models import Exists
|
||||||
|
Company.objects.filter(
|
||||||
|
Exists(Employee.objects.filter(company=OuterRef('pk'), salary__gt=10))
|
||||||
|
)
|
||||||
|
|
||||||
Built-in Expressions
|
Built-in Expressions
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
@ -626,22 +633,25 @@ degrade performance, it's automatically removed.
|
||||||
|
|
||||||
You can query using ``NOT EXISTS`` with ``~Exists()``.
|
You can query using ``NOT EXISTS`` with ``~Exists()``.
|
||||||
|
|
||||||
Filtering on a ``Subquery`` expression
|
Filtering on a ``Subquery()`` or ``Exists()`` expressions
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
It's not possible to filter directly using ``Subquery`` and ``Exists``, e.g.::
|
``Subquery()`` that returns a boolean value and ``Exists()`` may be used as a
|
||||||
|
``condition`` in :class:`~django.db.models.expressions.When` expressions, or to
|
||||||
|
directly filter a queryset::
|
||||||
|
|
||||||
|
>>> recent_comments = Comment.objects.filter(...) # From above
|
||||||
>>> Post.objects.filter(Exists(recent_comments))
|
>>> Post.objects.filter(Exists(recent_comments))
|
||||||
...
|
|
||||||
TypeError: 'Exists' object is not iterable
|
|
||||||
|
|
||||||
|
This will ensure that the subquery will not be added to the ``SELECT`` columns,
|
||||||
|
which may result in a better performance.
|
||||||
|
|
||||||
You must filter on a subquery expression by first annotating the queryset
|
.. versionchanged:: 3.0
|
||||||
and then filtering based on that annotation::
|
|
||||||
|
|
||||||
>>> Post.objects.annotate(
|
In previous versions of Django, it was necessary to first annotate and then
|
||||||
... recent_comment=Exists(recent_comments),
|
filter against the annotation. This resulted in the annotated value always
|
||||||
... ).filter(recent_comment=True)
|
being present in the query result, and often resulted in a query that took
|
||||||
|
more time to execute.
|
||||||
|
|
||||||
Using aggregates within a ``Subquery`` expression
|
Using aggregates within a ``Subquery`` expression
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -74,6 +74,13 @@ enable adding exclusion constraints on PostgreSQL. Constraints are added to
|
||||||
models using the
|
models using the
|
||||||
:attr:`Meta.constraints <django.db.models.Options.constraints>` option.
|
:attr:`Meta.constraints <django.db.models.Options.constraints>` option.
|
||||||
|
|
||||||
|
Filter expressions
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Expressions that outputs :class:`~django.db.models.BooleanField` may now be
|
||||||
|
used directly in ``QuerySet`` filters, without having to first annotate and
|
||||||
|
then filter against the annotation.
|
||||||
|
|
||||||
Minor features
|
Minor features
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ class Company(models.Model):
|
||||||
related_name='company_point_of_contact_set',
|
related_name='company_point_of_contact_set',
|
||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
based_in_eu = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
|
@ -37,7 +37,7 @@ class BasicExpressionsTests(TestCase):
|
||||||
ceo=Employee.objects.create(firstname="Joe", lastname="Smith", salary=10)
|
ceo=Employee.objects.create(firstname="Joe", lastname="Smith", salary=10)
|
||||||
)
|
)
|
||||||
cls.foobar_ltd = Company.objects.create(
|
cls.foobar_ltd = Company.objects.create(
|
||||||
name="Foobar Ltd.", num_employees=3, num_chairs=4,
|
name="Foobar Ltd.", num_employees=3, num_chairs=4, based_in_eu=True,
|
||||||
ceo=Employee.objects.create(firstname="Frank", lastname="Meyer", salary=20)
|
ceo=Employee.objects.create(firstname="Frank", lastname="Meyer", salary=20)
|
||||||
)
|
)
|
||||||
cls.max = Employee.objects.create(firstname='Max', lastname='Mustermann', salary=30)
|
cls.max = Employee.objects.create(firstname='Max', lastname='Mustermann', salary=30)
|
||||||
|
@ -83,6 +83,14 @@ class BasicExpressionsTests(TestCase):
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_filtering_on_q_that_is_boolean(self):
|
||||||
|
self.assertEqual(
|
||||||
|
Company.objects.filter(
|
||||||
|
ExpressionWrapper(Q(num_employees__gt=3), output_field=models.BooleanField())
|
||||||
|
).count(),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
|
||||||
def test_filter_inter_attribute(self):
|
def test_filter_inter_attribute(self):
|
||||||
# We can filter on attribute relationships on same model obj, e.g.
|
# We can filter on attribute relationships on same model obj, e.g.
|
||||||
# find companies where the number of employees is greater
|
# find companies where the number of employees is greater
|
||||||
|
@ -642,6 +650,56 @@ class BasicExpressionsTests(TestCase):
|
||||||
with self.assertRaisesMessage(FieldError, "Cannot resolve keyword 'nope' into field."):
|
with self.assertRaisesMessage(FieldError, "Cannot resolve keyword 'nope' into field."):
|
||||||
list(Company.objects.filter(ceo__pk=F('point_of_contact__nope')))
|
list(Company.objects.filter(ceo__pk=F('point_of_contact__nope')))
|
||||||
|
|
||||||
|
def test_exists_in_filter(self):
|
||||||
|
inner = Company.objects.filter(ceo=OuterRef('pk')).values('pk')
|
||||||
|
qs1 = Employee.objects.filter(Exists(inner))
|
||||||
|
qs2 = Employee.objects.annotate(found=Exists(inner)).filter(found=True)
|
||||||
|
self.assertCountEqual(qs1, qs2)
|
||||||
|
self.assertFalse(Employee.objects.exclude(Exists(inner)).exists())
|
||||||
|
self.assertCountEqual(qs2, Employee.objects.exclude(~Exists(inner)))
|
||||||
|
|
||||||
|
def test_subquery_in_filter(self):
|
||||||
|
inner = Company.objects.filter(ceo=OuterRef('pk')).values('based_in_eu')
|
||||||
|
self.assertSequenceEqual(
|
||||||
|
Employee.objects.filter(Subquery(inner)),
|
||||||
|
[self.foobar_ltd.ceo],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_case_in_filter_if_boolean_output_field(self):
|
||||||
|
is_ceo = Company.objects.filter(ceo=OuterRef('pk'))
|
||||||
|
is_poc = Company.objects.filter(point_of_contact=OuterRef('pk'))
|
||||||
|
qs = Employee.objects.filter(
|
||||||
|
Case(
|
||||||
|
When(Exists(is_ceo), then=True),
|
||||||
|
When(Exists(is_poc), then=True),
|
||||||
|
default=False,
|
||||||
|
output_field=models.BooleanField(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(qs, [self.example_inc.ceo, self.foobar_ltd.ceo, self.max])
|
||||||
|
|
||||||
|
def test_boolean_expression_combined(self):
|
||||||
|
is_ceo = Company.objects.filter(ceo=OuterRef('pk'))
|
||||||
|
is_poc = Company.objects.filter(point_of_contact=OuterRef('pk'))
|
||||||
|
self.gmbh.point_of_contact = self.max
|
||||||
|
self.gmbh.save()
|
||||||
|
self.assertSequenceEqual(
|
||||||
|
Employee.objects.filter(Exists(is_ceo) | Exists(is_poc)),
|
||||||
|
[self.example_inc.ceo, self.foobar_ltd.ceo, self.max],
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(
|
||||||
|
Employee.objects.filter(Exists(is_ceo) & Exists(is_poc)),
|
||||||
|
[self.max],
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(
|
||||||
|
Employee.objects.filter(Exists(is_ceo) & Q(salary__gte=30)),
|
||||||
|
[self.max],
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(
|
||||||
|
Employee.objects.filter(Exists(is_poc) | Q(salary__lt=15)),
|
||||||
|
[self.example_inc.ceo, self.max],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class IterableLookupInnerExpressionsTests(TestCase):
|
class IterableLookupInnerExpressionsTests(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -1327,7 +1327,10 @@ class CaseWhenTests(SimpleTestCase):
|
||||||
Case(When(Q(pk__in=[])), object())
|
Case(When(Q(pk__in=[])), object())
|
||||||
|
|
||||||
def test_invalid_when_constructor_args(self):
|
def test_invalid_when_constructor_args(self):
|
||||||
msg = '__init__() takes either a Q object or lookups as keyword arguments'
|
msg = (
|
||||||
|
'When() supports a Q object, a boolean expression, or lookups as '
|
||||||
|
'a condition.'
|
||||||
|
)
|
||||||
with self.assertRaisesMessage(TypeError, msg):
|
with self.assertRaisesMessage(TypeError, msg):
|
||||||
When(condition=object())
|
When(condition=object())
|
||||||
with self.assertRaisesMessage(TypeError, msg):
|
with self.assertRaisesMessage(TypeError, msg):
|
||||||
|
|
Loading…
Reference in New Issue