diff options
| author | Ryan Heard <ryanwheard@gmail.com> | 2021-07-02 15:09:13 -0500 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2022-03-04 12:55:37 +0100 |
| commit | c6b4d62fa2c7f73b87f6ae7e8cf1d64ee5312dc5 (patch) | |
| tree | 238bd8c3045c8d4577e09bd913de9748dcd49968 /django | |
| parent | 795da6306a048b18c0158496b0d49e8e4f197a32 (diff) | |
Fixed #29865 -- Added logical XOR support for Q() and querysets.
Diffstat (limited to 'django')
| -rw-r--r-- | django/db/backends/base/features.py | 3 | ||||
| -rw-r--r-- | django/db/backends/mysql/features.py | 1 | ||||
| -rw-r--r-- | django/db/models/expressions.py | 20 | ||||
| -rw-r--r-- | django/db/models/query.py | 19 | ||||
| -rw-r--r-- | django/db/models/query_utils.py | 4 | ||||
| -rw-r--r-- | django/db/models/sql/__init__.py | 4 | ||||
| -rw-r--r-- | django/db/models/sql/where.py | 30 |
7 files changed, 71 insertions, 10 deletions
diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index ccf9104c21..ebf29e80d6 100644 --- a/django/db/backends/base/features.py +++ b/django/db/backends/base/features.py @@ -325,6 +325,9 @@ class BaseDatabaseFeatures: # Does the backend support non-deterministic collations? supports_non_deterministic_collations = True + # Does the backend support the logical XOR operator? + supports_logical_xor = False + # Collation names for use by the Django test suite. test_collations = { "ci": None, # Case-insensitive. diff --git a/django/db/backends/mysql/features.py b/django/db/backends/mysql/features.py index 1996208b46..357e431524 100644 --- a/django/db/backends/mysql/features.py +++ b/django/db/backends/mysql/features.py @@ -47,6 +47,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_order_by_nulls_modifier = False order_by_nulls_first = True + supports_logical_xor = True @cached_property def minimum_database_version(self): diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index a2da1f6e38..25c2803085 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -94,7 +94,7 @@ class Combinable: 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." + "Use .bitand(), .bitor(), and .bitxor() for bitwise logical operations." ) def bitand(self, other): @@ -106,6 +106,13 @@ class Combinable: def bitrightshift(self, other): return self._combine(other, self.BITRIGHTSHIFT, False) + def __xor__(self, other): + if getattr(self, "conditional", False) and getattr(other, "conditional", False): + return Q(self) ^ Q(other) + raise NotImplementedError( + "Use .bitand(), .bitor(), and .bitxor() for bitwise logical operations." + ) + def bitxor(self, other): return self._combine(other, self.BITXOR, False) @@ -113,7 +120,7 @@ class Combinable: 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." + "Use .bitand(), .bitor(), and .bitxor() for bitwise logical operations." ) def bitor(self, other): @@ -139,12 +146,17 @@ class Combinable: def __rand__(self, other): raise NotImplementedError( - "Use .bitand() and .bitor() for bitwise logical operations." + "Use .bitand(), .bitor(), and .bitxor() for bitwise logical operations." ) def __ror__(self, other): raise NotImplementedError( - "Use .bitand() and .bitor() for bitwise logical operations." + "Use .bitand(), .bitor(), and .bitxor() for bitwise logical operations." + ) + + def __rxor__(self, other): + raise NotImplementedError( + "Use .bitand(), .bitor(), and .bitxor() for bitwise logical operations." ) diff --git a/django/db/models/query.py b/django/db/models/query.py index 0cebcc70d6..5c78c6e315 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -396,6 +396,25 @@ class QuerySet: combined.query.combine(other.query, sql.OR) return combined + def __xor__(self, other): + self._check_operator_queryset(other, "^") + self._merge_sanity_check(other) + if isinstance(self, EmptyQuerySet): + return other + if isinstance(other, EmptyQuerySet): + return self + query = ( + self + if self.query.can_filter() + else self.model._base_manager.filter(pk__in=self.values("pk")) + ) + combined = query._chain() + combined._merge_known_related_objects(other) + if not other.query.can_filter(): + other = other.model._base_manager.filter(pk__in=other.values("pk")) + combined.query.combine(other.query, sql.XOR) + return combined + #################################### # METHODS THAT DO DATABASE QUERIES # #################################### diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py index 6ea82b6520..fde686b2cd 100644 --- a/django/db/models/query_utils.py +++ b/django/db/models/query_utils.py @@ -38,6 +38,7 @@ class Q(tree.Node): # Connection types AND = "AND" OR = "OR" + XOR = "XOR" default = AND conditional = True @@ -70,6 +71,9 @@ class Q(tree.Node): def __and__(self, other): return self._combine(other, self.AND) + def __xor__(self, other): + return self._combine(other, self.XOR) + def __invert__(self): obj = type(self)() obj.add(self, self.AND) diff --git a/django/db/models/sql/__init__.py b/django/db/models/sql/__init__.py index 2956e047b1..dd31a6ea9e 100644 --- a/django/db/models/sql/__init__.py +++ b/django/db/models/sql/__init__.py @@ -1,6 +1,6 @@ from django.db.models.sql.query import * # NOQA from django.db.models.sql.query import Query from django.db.models.sql.subqueries import * # NOQA -from django.db.models.sql.where import AND, OR +from django.db.models.sql.where import AND, OR, XOR -__all__ = ["Query", "AND", "OR"] +__all__ = ["Query", "AND", "OR", "XOR"] diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 532780fd98..8e3ad74d65 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -1,14 +1,19 @@ """ Code to manage the creation and SQL rendering of 'where' constraints. """ +import operator +from functools import reduce from django.core.exceptions import EmptyResultSet +from django.db.models.expressions import Case, When +from django.db.models.lookups import Exact from django.utils import tree from django.utils.functional import cached_property # Connection types AND = "AND" OR = "OR" +XOR = "XOR" class WhereNode(tree.Node): @@ -39,10 +44,12 @@ class WhereNode(tree.Node): if not self.contains_aggregate: return self, None in_negated = negated ^ self.negated - # If the effective connector is OR and this node contains an aggregate, - # then we need to push the whole branch to HAVING clause. - may_need_split = (in_negated and self.connector == AND) or ( - not in_negated and self.connector == OR + # If the effective connector is OR or XOR and this node contains an + # aggregate, then we need to push the whole branch to HAVING clause. + may_need_split = ( + (in_negated and self.connector == AND) + or (not in_negated and self.connector == OR) + or self.connector == XOR ) if may_need_split and self.contains_aggregate: return None, self @@ -85,6 +92,21 @@ class WhereNode(tree.Node): else: full_needed, empty_needed = 1, len(self.children) + if self.connector == XOR and not connection.features.supports_logical_xor: + # Convert if the database doesn't support XOR: + # a XOR b XOR c XOR ... + # to: + # (a OR b OR c OR ...) AND (a + b + c + ...) == 1 + lhs = self.__class__(self.children, OR) + rhs_sum = reduce( + operator.add, + (Case(When(c, then=1), default=0) for c in self.children), + ) + rhs = Exact(1, rhs_sum) + return self.__class__([lhs, rhs], AND, self.negated).as_sql( + compiler, connection + ) + for child in self.children: try: sql, params = compiler.compile(child) |
