mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	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:
		
				
					committed by
					
						 Mariusz Felisiak
						Mariusz Felisiak
					
				
			
			
				
	
			
			
			
						parent
						
							069bee7c12
						
					
				
				
					commit
					4137fc2efc
				
			| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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." | ||||
|         ) | ||||
| @@ -245,6 +249,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: | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
| in the conditional expression. Using a ``When()`` object is similar to using | ||||
| the :meth:`~django.db.models.query.QuerySet.filter` method. The condition can | ||||
| be specified using :ref:`field lookups <field-lookups>` or | ||||
| :class:`~django.db.models.Q` objects. The result is provided using the ``then`` | ||||
| keyword. | ||||
| be specified using :ref:`field lookups <field-lookups>`, | ||||
| :class:`~django.db.models.Q` objects, or :class:`~django.db.models.Expression` | ||||
| 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:: | ||||
|  | ||||
| @@ -60,6 +66,12 @@ Some examples:: | ||||
|     >>> # Complex conditions can be created using Q objects | ||||
|     >>> When(Q(name__startswith="John") | Q(name__startswith="Paul"), | ||||
|     ...      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. | ||||
|  | ||||
| @@ -158,9 +170,9 @@ registered more than a year ago:: | ||||
| Advanced queries | ||||
| ================ | ||||
|  | ||||
| Conditional expressions can be used in annotations, aggregations, lookups, and | ||||
| updates. They can also be combined and nested with other expressions. This | ||||
| allows you to make powerful conditional queries. | ||||
| Conditional expressions can be used in annotations, aggregations, filters, | ||||
| lookups, and updates. They can also be combined and nested with other | ||||
| expressions. This allows you to make powerful conditional queries. | ||||
|  | ||||
| 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 | ||||
| ``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 | ||||
|  | ||||
| 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 | ||||
| number of built-in expressions (documented below) that can be used to help you | ||||
| write queries. Expressions can be combined, or in some cases nested, to form | ||||
| more complex computations. | ||||
| an update, create, filter, order by, annotation, or aggregate. When an | ||||
| expression outputs a boolean value, it may be used directly in filters. There | ||||
| are a number of built-in expressions (documented below) that can be used to | ||||
| help you write queries. Expressions can be combined, or in some cases nested, | ||||
| to form more complex computations. | ||||
|  | ||||
| Supported arithmetic | ||||
| ==================== | ||||
| @@ -69,6 +70,12 @@ Some examples | ||||
|     CharField.register_lookup(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 | ||||
| ==================== | ||||
|  | ||||
| @@ -626,22 +633,25 @@ degrade performance, it's automatically removed. | ||||
|  | ||||
| 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)) | ||||
|     ... | ||||
|     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 | ||||
| and then filtering based on that annotation:: | ||||
| .. versionchanged:: 3.0 | ||||
|  | ||||
|     >>> Post.objects.annotate( | ||||
|     ...     recent_comment=Exists(recent_comments), | ||||
|     ... ).filter(recent_comment=True) | ||||
|     In previous versions of Django, it was necessary to first annotate and then | ||||
|     filter against the annotation. This resulted in the annotated value always | ||||
|     being present in the query result, and often resulted in a query that took | ||||
|     more time to execute. | ||||
|  | ||||
| Using aggregates within a ``Subquery`` expression | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
|   | ||||
| @@ -74,6 +74,13 @@ enable adding exclusion constraints on PostgreSQL. Constraints are added to | ||||
| models using the | ||||
| :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 | ||||
| -------------- | ||||
|  | ||||
|   | ||||
| @@ -34,6 +34,7 @@ class Company(models.Model): | ||||
|         related_name='company_point_of_contact_set', | ||||
|         null=True, | ||||
|     ) | ||||
|     based_in_eu = models.BooleanField(default=False) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|   | ||||
| @@ -37,7 +37,7 @@ class BasicExpressionsTests(TestCase): | ||||
|             ceo=Employee.objects.create(firstname="Joe", lastname="Smith", salary=10) | ||||
|         ) | ||||
|         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) | ||||
|         ) | ||||
|         cls.max = Employee.objects.create(firstname='Max', lastname='Mustermann', salary=30) | ||||
| @@ -83,6 +83,14 @@ class BasicExpressionsTests(TestCase): | ||||
|             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): | ||||
|         # We can filter on attribute relationships on same model obj, e.g. | ||||
|         # 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."): | ||||
|             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): | ||||
|     @classmethod | ||||
|   | ||||
| @@ -1327,7 +1327,10 @@ class CaseWhenTests(SimpleTestCase): | ||||
|             Case(When(Q(pk__in=[])), object()) | ||||
|  | ||||
|     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): | ||||
|             When(condition=object()) | ||||
|         with self.assertRaisesMessage(TypeError, msg): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user