summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorMatthew Schinckel <matt@schinckel.net>2017-02-27 19:31:52 +1030
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2019-08-29 09:45:29 +0200
commit4137fc2efce2dde48340728b8006fc6d66b9e3a5 (patch)
treedf3632a53ff2d1f7efccd501880601f29e06d54c /django
parent069bee7c1232458a0f13c2e30daa8df99dbd3680 (diff)
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>
Diffstat (limited to 'django')
-rw-r--r--django/db/backends/base/operations.py7
-rw-r--r--django/db/backends/oracle/operations.py13
-rw-r--r--django/db/models/expressions.py16
-rw-r--r--django/db/models/sql/query.py10
4 files changed, 45 insertions, 1 deletions
diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py
index 76abc6dcb2..a0c84a8ff4 100644
--- a/django/db/backends/base/operations.py
+++ b/django/db/backends/base/operations.py
@@ -581,6 +581,13 @@ class BaseDatabaseOperations:
"""
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):
"""
Combine a list of subexpressions into a single expression, using
diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py
index 3cad02fa41..95126d37be 100644
--- a/django/db/backends/oracle/operations.py
+++ b/django/db/backends/oracle/operations.py
@@ -6,6 +6,8 @@ from functools import lru_cache
from django.conf import settings
from django.db.backends.base.operations import BaseDatabaseOperations
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.utils import timezone
from django.utils.encoding import force_bytes, force_str
@@ -607,3 +609,14 @@ END;
if fields:
return self.connection.features.max_query_params // len(fields)
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
diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
index 1dd061c152..9429adf81c 100644
--- a/django/db/models/expressions.py
+++ b/django/db/models/expressions.py
@@ -90,6 +90,8 @@ class Combinable:
return self._combine(other, self.POW, False)
def __and__(self, other):
+ if getattr(self, 'conditional', False) and getattr(other, 'conditional', False):
+ return Q(self) & Q(other)
raise NotImplementedError(
"Use .bitand() and .bitor() for bitwise logical operations."
)
@@ -104,6 +106,8 @@ class Combinable:
return self._combine(other, self.BITRIGHTSHIFT, False)
def __or__(self, other):
+ if getattr(self, 'conditional', False) and getattr(other, 'conditional', False):
+ return Q(self) | Q(other)
raise NotImplementedError(
"Use .bitand() and .bitor() for bitwise logical operations."
)
@@ -246,6 +250,10 @@ class BaseExpression:
return c
@property
+ def conditional(self):
+ return isinstance(self.output_field, fields.BooleanField)
+
+ @property
def field(self):
return self.output_field
@@ -873,12 +881,17 @@ class ExpressionWrapper(Expression):
class When(Expression):
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):
if lookups and condition is None:
condition, lookups = Q(**lookups), None
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:
raise ValueError("An empty Q() can't be used as a When() condition.")
super().__init__(output_field=None)
@@ -1090,6 +1103,7 @@ class Exists(Subquery):
class OrderBy(BaseExpression):
template = '%(expression)s %(ordering)s'
+ conditional = False
def __init__(self, expression, descending=False, nulls_first=False, nulls_last=False):
if nulls_first and nulls_last:
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index 4ad1fb3f36..35f3c5e1ea 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -1229,6 +1229,16 @@ class Query(BaseExpression):
"""
if isinstance(filter_expr, 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
if not arg:
raise FieldError("Cannot parse keyword query %r" % arg)