Fixed #32961 -- Added BitXor() aggregate to django.contrib.postgres.

This commit is contained in:
Nick Pope 2021-07-23 16:57:26 +01:00 committed by Mariusz Felisiak
parent 000d430234
commit bd47b9bc81
5 changed files with 55 additions and 5 deletions

View File

@ -9,7 +9,8 @@ from django.utils.deprecation import RemovedInDjango50Warning
from .mixins import OrderableAggMixin from .mixins import OrderableAggMixin
__all__ = [ __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' function = 'BIT_OR'
class BitXor(Aggregate):
function = 'BIT_XOR'
class BoolAnd(Aggregate): class BoolAnd(Aggregate):
function = 'BOOL_AND' function = 'BOOL_AND'
output_field = BooleanField() output_field = BooleanField()

View File

@ -91,6 +91,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
def is_postgresql_13(self): def is_postgresql_13(self):
return self.connection.pg_version >= 130000 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')) has_websearch_to_tsquery = property(operator.attrgetter('is_postgresql_11'))
supports_covering_indexes = 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')) supports_covering_gist_indexes = property(operator.attrgetter('is_postgresql_12'))

View File

@ -75,6 +75,16 @@ General-purpose aggregation functions
Returns an ``int`` of the bitwise ``OR`` of all non-null input values, or Returns an ``int`` of the bitwise ``OR`` of all non-null input values, or
``default`` if all values are null. ``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`` ``BoolAnd``
----------- -----------

View File

@ -64,7 +64,9 @@ Minor features
:mod:`django.contrib.postgres` :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` :mod:`django.contrib.redirects`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -1,8 +1,10 @@
from django.db import connection
from django.db.models import ( from django.db.models import (
CharField, F, Func, IntegerField, OuterRef, Q, Subquery, Value, CharField, F, Func, IntegerField, OuterRef, Q, Subquery, Value,
) )
from django.db.models.fields.json import KeyTextTransform, KeyTransform from django.db.models.fields.json import KeyTextTransform, KeyTransform
from django.db.models.functions import Cast, Concat, Substr from django.db.models.functions import Cast, Concat, Substr
from django.test import skipUnlessDBFeature
from django.test.utils import Approximate, ignore_warnings from django.test.utils import Approximate, ignore_warnings
from django.utils import timezone from django.utils import timezone
from django.utils.deprecation import RemovedInDjango50Warning from django.utils.deprecation import RemovedInDjango50Warning
@ -12,9 +14,9 @@ from .models import AggregateTestModel, HotelReservation, Room, StatTestModel
try: try:
from django.contrib.postgres.aggregates import ( from django.contrib.postgres.aggregates import (
ArrayAgg, BitAnd, BitOr, BoolAnd, BoolOr, Corr, CovarPop, JSONBAgg, ArrayAgg, BitAnd, BitOr, BitXor, BoolAnd, BoolOr, Corr, CovarPop,
RegrAvgX, RegrAvgY, RegrCount, RegrIntercept, RegrR2, RegrSlope, JSONBAgg, RegrAvgX, RegrAvgY, RegrCount, RegrIntercept, RegrR2,
RegrSXX, RegrSXY, RegrSYY, StatAggregate, StringAgg, RegrSlope, RegrSXX, RegrSXY, RegrSYY, StatAggregate, StringAgg,
) )
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
except ImportError: except ImportError:
@ -68,6 +70,8 @@ class TestGeneralAggregate(PostgreSQLTestCase):
(JSONBAgg('integer_field'), []), (JSONBAgg('integer_field'), []),
(StringAgg('char_field', delimiter=';'), ''), (StringAgg('char_field', delimiter=';'), ''),
] ]
if connection.features.has_bit_xor:
tests.append((BitXor('integer_field'), None))
for aggregation, expected_result in tests: for aggregation, expected_result in tests:
with self.subTest(aggregation=aggregation): with self.subTest(aggregation=aggregation):
# Empty result with non-execution optimization. # Empty result with non-execution optimization.
@ -96,6 +100,8 @@ class TestGeneralAggregate(PostgreSQLTestCase):
(JSONBAgg('integer_field', default=Value('["<empty>"]')), ['<empty>']), (JSONBAgg('integer_field', default=Value('["<empty>"]')), ['<empty>']),
(StringAgg('char_field', delimiter=';', 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: for aggregation, expected_result in tests:
with self.subTest(aggregation=aggregation): with self.subTest(aggregation=aggregation):
# Empty result with non-execution optimization. # Empty result with non-execution optimization.
@ -275,6 +281,28 @@ class TestGeneralAggregate(PostgreSQLTestCase):
integer_field=0).aggregate(bitor=BitOr('integer_field')) integer_field=0).aggregate(bitor=BitOr('integer_field'))
self.assertEqual(values, {'bitor': 0}) 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): def test_bool_and_general(self):
values = AggregateTestModel.objects.aggregate(booland=BoolAnd('boolean_field')) values = AggregateTestModel.objects.aggregate(booland=BoolAnd('boolean_field'))
self.assertEqual(values, {'booland': False}) self.assertEqual(values, {'booland': False})