summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Pope <nick@nickpope.me.uk>2021-07-23 16:57:26 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2021-10-01 10:32:39 +0200
commitbd47b9bc816bf213b6d0027ed9a9a44955bb7694 (patch)
tree6d91331c5207696c3434a1c44c679e4c9164f376
parent000d4302341f49f2ec5f219d94d5dddac1a2acf5 (diff)
Fixed #32961 -- Added BitXor() aggregate to django.contrib.postgres.
-rw-r--r--django/contrib/postgres/aggregates/general.py7
-rw-r--r--django/db/backends/postgresql/features.py5
-rw-r--r--docs/ref/contrib/postgres/aggregates.txt10
-rw-r--r--docs/releases/4.1.txt4
-rw-r--r--tests/postgres_tests/test_aggregates.py34
5 files changed, 55 insertions, 5 deletions
diff --git a/django/contrib/postgres/aggregates/general.py b/django/contrib/postgres/aggregates/general.py
index 99fe0d8739..d90ca50e2b 100644
--- a/django/contrib/postgres/aggregates/general.py
+++ b/django/contrib/postgres/aggregates/general.py
@@ -9,7 +9,8 @@ from django.utils.deprecation import RemovedInDjango50Warning
from .mixins import OrderableAggMixin
__all__ = [
- 'ArrayAgg', 'BitAnd', 'BitOr', 'BoolAnd', 'BoolOr', 'JSONBAgg', 'StringAgg',
+ 'ArrayAgg', 'BitAnd', 'BitOr', 'BitXor', 'BoolAnd', 'BoolOr', 'JSONBAgg',
+ 'StringAgg',
]
@@ -60,6 +61,10 @@ class BitOr(Aggregate):
function = 'BIT_OR'
+class BitXor(Aggregate):
+ function = 'BIT_XOR'
+
+
class BoolAnd(Aggregate):
function = 'BOOL_AND'
output_field = BooleanField()
diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py
index 09c157505e..caa37335e0 100644
--- a/django/db/backends/postgresql/features.py
+++ b/django/db/backends/postgresql/features.py
@@ -91,6 +91,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
def is_postgresql_13(self):
return self.connection.pg_version >= 130000
+ @cached_property
+ def is_postgresql_14(self):
+ return self.connection.pg_version >= 140000
+
+ has_bit_xor = property(operator.attrgetter('is_postgresql_14'))
has_websearch_to_tsquery = property(operator.attrgetter('is_postgresql_11'))
supports_covering_indexes = property(operator.attrgetter('is_postgresql_11'))
supports_covering_gist_indexes = property(operator.attrgetter('is_postgresql_12'))
diff --git a/docs/ref/contrib/postgres/aggregates.txt b/docs/ref/contrib/postgres/aggregates.txt
index cc177aca87..7c901a0bad 100644
--- a/docs/ref/contrib/postgres/aggregates.txt
+++ b/docs/ref/contrib/postgres/aggregates.txt
@@ -75,6 +75,16 @@ General-purpose aggregation functions
Returns an ``int`` of the bitwise ``OR`` of all non-null input values, or
``default`` if all values are null.
+``BitXor``
+----------
+
+.. versionadded:: 4.1
+
+.. class:: BitXor(expression, filter=None, default=None, **extra)
+
+ Returns an ``int`` of the bitwise ``XOR`` of all non-null input values, or
+ ``default`` if all values are null. It requires PostgreSQL 14+.
+
``BoolAnd``
-----------
diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index 8401103767..e089ea6cf9 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -64,7 +64,9 @@ Minor features
:mod:`django.contrib.postgres`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-* ...
+* The new :class:`BitXor() <django.contrib.postgres.aggregates.BitXor>`
+ aggregate function returns an ``int`` of the bitwise ``XOR`` of all non-null
+ input values.
:mod:`django.contrib.redirects`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/postgres_tests/test_aggregates.py b/tests/postgres_tests/test_aggregates.py
index 07200f9f97..399c4fa8a9 100644
--- a/tests/postgres_tests/test_aggregates.py
+++ b/tests/postgres_tests/test_aggregates.py
@@ -1,8 +1,10 @@
+from django.db import connection
from django.db.models import (
CharField, F, Func, IntegerField, OuterRef, Q, Subquery, Value,
)
from django.db.models.fields.json import KeyTextTransform, KeyTransform
from django.db.models.functions import Cast, Concat, Substr
+from django.test import skipUnlessDBFeature
from django.test.utils import Approximate, ignore_warnings
from django.utils import timezone
from django.utils.deprecation import RemovedInDjango50Warning
@@ -12,9 +14,9 @@ from .models import AggregateTestModel, HotelReservation, Room, StatTestModel
try:
from django.contrib.postgres.aggregates import (
- ArrayAgg, BitAnd, BitOr, BoolAnd, BoolOr, Corr, CovarPop, JSONBAgg,
- RegrAvgX, RegrAvgY, RegrCount, RegrIntercept, RegrR2, RegrSlope,
- RegrSXX, RegrSXY, RegrSYY, StatAggregate, StringAgg,
+ ArrayAgg, BitAnd, BitOr, BitXor, BoolAnd, BoolOr, Corr, CovarPop,
+ JSONBAgg, RegrAvgX, RegrAvgY, RegrCount, RegrIntercept, RegrR2,
+ RegrSlope, RegrSXX, RegrSXY, RegrSYY, StatAggregate, StringAgg,
)
from django.contrib.postgres.fields import ArrayField
except ImportError:
@@ -68,6 +70,8 @@ class TestGeneralAggregate(PostgreSQLTestCase):
(JSONBAgg('integer_field'), []),
(StringAgg('char_field', delimiter=';'), ''),
]
+ if connection.features.has_bit_xor:
+ tests.append((BitXor('integer_field'), None))
for aggregation, expected_result in tests:
with self.subTest(aggregation=aggregation):
# Empty result with non-execution optimization.
@@ -96,6 +100,8 @@ class TestGeneralAggregate(PostgreSQLTestCase):
(JSONBAgg('integer_field', default=Value('["<empty>"]')), ['<empty>']),
(StringAgg('char_field', delimiter=';', default=Value('<empty>')), '<empty>'),
]
+ if connection.features.has_bit_xor:
+ tests.append((BitXor('integer_field', default=0), 0))
for aggregation, expected_result in tests:
with self.subTest(aggregation=aggregation):
# Empty result with non-execution optimization.
@@ -275,6 +281,28 @@ class TestGeneralAggregate(PostgreSQLTestCase):
integer_field=0).aggregate(bitor=BitOr('integer_field'))
self.assertEqual(values, {'bitor': 0})
+ @skipUnlessDBFeature('has_bit_xor')
+ def test_bit_xor_general(self):
+ AggregateTestModel.objects.create(integer_field=3)
+ values = AggregateTestModel.objects.filter(
+ integer_field__in=[1, 3],
+ ).aggregate(bitxor=BitXor('integer_field'))
+ self.assertEqual(values, {'bitxor': 2})
+
+ @skipUnlessDBFeature('has_bit_xor')
+ def test_bit_xor_on_only_true_values(self):
+ values = AggregateTestModel.objects.filter(
+ integer_field=1,
+ ).aggregate(bitxor=BitXor('integer_field'))
+ self.assertEqual(values, {'bitxor': 1})
+
+ @skipUnlessDBFeature('has_bit_xor')
+ def test_bit_xor_on_only_false_values(self):
+ values = AggregateTestModel.objects.filter(
+ integer_field=0,
+ ).aggregate(bitxor=BitXor('integer_field'))
+ self.assertEqual(values, {'bitxor': 0})
+
def test_bool_and_general(self):
values = AggregateTestModel.objects.aggregate(booland=BoolAnd('boolean_field'))
self.assertEqual(values, {'booland': False})