diff options
| author | Matthew Schinckel <matt@schinckel.net> | 2017-02-27 19:31:52 +1030 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2019-08-29 09:45:29 +0200 |
| commit | 4137fc2efce2dde48340728b8006fc6d66b9e3a5 (patch) | |
| tree | df3632a53ff2d1f7efccd501880601f29e06d54c /django | |
| parent | 069bee7c1232458a0f13c2e30daa8df99dbd3680 (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.py | 7 | ||||
| -rw-r--r-- | django/db/backends/oracle/operations.py | 13 | ||||
| -rw-r--r-- | django/db/models/expressions.py | 16 | ||||
| -rw-r--r-- | django/db/models/sql/query.py | 10 |
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) |
