Skip to content

Commit ed79c59

Browse files
authored
Fixed #37028 -- Added BitAnd(), BitOr(), and BitXor() aggregates.
1 parent d687d41 commit ed79c59

13 files changed

Lines changed: 341 additions & 78 deletions

File tree

django/contrib/postgres/aggregates/general.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import warnings
22

33
from django.contrib.postgres.fields import ArrayField
4-
from django.db.models import Aggregate, BooleanField, JSONField
4+
from django.db.models import Aggregate
5+
from django.db.models import BitAnd as _BitAnd
6+
from django.db.models import BitOr as _BitOr
7+
from django.db.models import BitXor as _BitXor
8+
from django.db.models import BooleanField, JSONField
59
from django.db.models import StringAgg as _StringAgg
610
from django.db.models import Value
711
from django.utils.deprecation import RemovedInDjango70Warning
812

913
__all__ = [
1014
"ArrayAgg",
11-
"BitAnd",
12-
"BitOr",
13-
"BitXor",
15+
"BitAnd", # RemovedInDjango70Warning
16+
"BitOr", # RemovedInDjango70Warning
17+
"BitXor", # RemovedInDjango70Warning
1418
"BoolAnd",
1519
"BoolOr",
1620
"JSONBAgg",
@@ -28,16 +32,37 @@ def output_field(self):
2832
return ArrayField(self.source_expressions[0].output_field)
2933

3034

31-
class BitAnd(Aggregate):
32-
function = "BIT_AND"
35+
class BitAnd(_BitAnd):
36+
def __init__(self, expression, **extra):
37+
warnings.warn(
38+
"The PostgreSQL-specific BitAnd function is deprecated. Use "
39+
"django.db.models.aggregates.BitAnd instead.",
40+
category=RemovedInDjango70Warning,
41+
stacklevel=2,
42+
)
43+
super().__init__(expression, **extra)
3344

3445

35-
class BitOr(Aggregate):
36-
function = "BIT_OR"
46+
class BitOr(_BitOr):
47+
def __init__(self, expression, **extra):
48+
warnings.warn(
49+
"The PostgreSQL-specific BitOr function is deprecated. Use "
50+
"django.db.models.aggregates.BitOr instead.",
51+
category=RemovedInDjango70Warning,
52+
stacklevel=2,
53+
)
54+
super().__init__(expression, **extra)
3755

3856

39-
class BitXor(Aggregate):
40-
function = "BIT_XOR"
57+
class BitXor(_BitXor):
58+
def __init__(self, expression, **extra):
59+
warnings.warn(
60+
"The PostgreSQL-specific BitXor function is deprecated. Use "
61+
"django.db.models.aggregates.BitXor instead.",
62+
category=RemovedInDjango70Warning,
63+
stacklevel=2,
64+
)
65+
super().__init__(expression, **extra)
4166

4267

4368
class BoolAnd(Aggregate):

django/db/backends/base/features.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,12 @@ class BaseDatabaseFeatures:
274274
# Does the database support SQL 2023 ANY_VALUE in GROUP BY?
275275
supports_any_value = False
276276

277+
# Does the database support bitwise aggregations: BIT_AND, BIT_OR, and
278+
# BIT_XOR?
279+
supports_bit_aggregations = True
280+
# Does the backend support the default parameter in bitwise aggregations?
281+
supports_default_in_bit_aggregations = True
282+
277283
# Does the backend support indexing a TextField?
278284
supports_index_on_text_field = True
279285

django/db/backends/mysql/features.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
2828
supports_over_clause = True
2929
supports_frame_range_fixed_distance = True
3030
supports_update_conflicts = True
31+
supports_default_in_bit_aggregations = False
3132
can_rename_index = True
3233
delete_can_self_reference_subquery = False
3334
create_test_procedure_without_params_sql = """

django/db/backends/oracle/features.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
7979
supports_collation_on_textfield = False
8080
supports_on_delete_db_default = False
8181
supports_no_precision_decimalfield = True
82+
supports_default_in_bit_aggregations = False
8283
test_now_utc_template = "CURRENT_TIMESTAMP AT TIME ZONE 'UTC'"
8384
django_test_expected_failures = {
8485
# A bug in Django/oracledb with respect to string handling (#23843).
@@ -240,3 +241,7 @@ def supports_uuid4_function(self):
240241
@cached_property
241242
def supports_stored_generated_columns(self):
242243
return self.connection.oracle_version >= (23, 7)
244+
245+
@cached_property
246+
def supports_bit_aggregations(self):
247+
return self.connection.oracle_version >= (21,)

django/db/backends/sqlite3/_functions.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import functools
6+
import operator
67
import random
78
import statistics
89
import zoneinfo
@@ -86,6 +87,9 @@ def register(connection):
8687
connection.create_aggregate("VAR_POP", 1, VarPop)
8788
connection.create_aggregate("VAR_SAMP", 1, VarSamp)
8889
connection.create_aggregate("ANY_VALUE", 1, AnyValue)
90+
connection.create_aggregate("BIT_AND", 1, BitAnd)
91+
connection.create_aggregate("BIT_OR", 1, BitOr)
92+
connection.create_aggregate("BIT_XOR", 1, BitXor)
8993
connection.create_function("UUIDV4", 0, _sqlite_uuid4)
9094
if PY314:
9195
connection.create_function("UUIDV7", 0, _sqlite_uuid7)
@@ -535,3 +539,27 @@ class VarSamp(ListAggregate):
535539
class AnyValue(ListAggregate):
536540
def finalize(self):
537541
return self[0]
542+
543+
544+
class BitAggregate(ListAggregate):
545+
bit_operator = None
546+
547+
def finalize(self):
548+
items = (item for item in self if item is not None)
549+
try:
550+
return functools.reduce(self.bit_operator, items)
551+
except TypeError:
552+
# items may be an empty iterator when all elements are None.
553+
return None
554+
555+
556+
class BitAnd(BitAggregate):
557+
bit_operator = operator.and_
558+
559+
560+
class BitOr(BitAggregate):
561+
bit_operator = operator.or_
562+
563+
564+
class BitXor(BitAggregate):
565+
bit_operator = operator.xor

django/db/models/aggregates.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
"Aggregate",
2525
"AnyValue",
2626
"Avg",
27+
"BitAnd",
28+
"BitOr",
29+
"BitXor",
2730
"Count",
2831
"Max",
2932
"Min",
@@ -241,6 +244,54 @@ class Avg(FixDurationInputMixin, NumericOutputFieldMixin, Aggregate):
241244
arity = 1
242245

243246

247+
class BitAggregate(Aggregate):
248+
arity = 1
249+
250+
def __init__(self, expression, **extra):
251+
super().__init__(expression, **extra)
252+
# self.default is reset in Aggregate.resolve_expression(). Store the
253+
# information for the later check in as_sql().
254+
self._has_default = self.default is not None
255+
256+
def as_sql(self, compiler, connection, **extra_context):
257+
if not connection.features.supports_bit_aggregations:
258+
raise NotSupportedError(
259+
f"{self.name} is not supported on {connection.vendor}."
260+
)
261+
if (
262+
self._has_default
263+
and not connection.features.supports_default_in_bit_aggregations
264+
):
265+
raise NotSupportedError(
266+
f"{self.name} does not support the default parameter on "
267+
f"{connection.vendor}."
268+
)
269+
return super().as_sql(compiler, connection, **extra_context)
270+
271+
def as_oracle(self, compiler, connection, **extra_context):
272+
return self.as_sql(
273+
compiler,
274+
connection,
275+
function=f"{self.function}_AGG",
276+
**extra_context,
277+
)
278+
279+
280+
class BitAnd(BitAggregate):
281+
function = "BIT_AND"
282+
name = "BitAnd"
283+
284+
285+
class BitOr(BitAggregate):
286+
function = "BIT_OR"
287+
name = "BitOr"
288+
289+
290+
class BitXor(BitAggregate):
291+
function = "BIT_XOR"
292+
name = "BitXor"
293+
294+
244295
class Count(Aggregate):
245296
function = "COUNT"
246297
name = "Count"

docs/internals/deprecation.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ details on these changes.
6969

7070
* The ``SQLCompiler.quote_name_unless_alias()`` method will be removed.
7171

72+
* The ``django.contrib.postgres.aggregates.BitAnd``,
73+
``django.contrib.postgres.aggregates.BitOr``, and
74+
``django.contrib.postgres.aggregates.BitXor`` classes will be removed.
75+
7276
.. _deprecation-removed-in-6.1:
7377

7478
6.1

docs/ref/contrib/postgres/aggregates.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ General-purpose aggregation functions
6262
Returns an ``int`` of the bitwise ``AND`` of all non-null input values, or
6363
``default`` if all values are null.
6464

65+
.. deprecated:: 6.1
66+
67+
This class is deprecated in favor of the generally available
68+
:class:`~django.db.models.BitAnd` class.
69+
6570
``BitOr``
6671
---------
6772

@@ -70,6 +75,11 @@ General-purpose aggregation functions
7075
Returns an ``int`` of the bitwise ``OR`` of all non-null input values, or
7176
``default`` if all values are null.
7277

78+
.. deprecated:: 6.1
79+
80+
This class is deprecated in favor of the generally available
81+
:class:`~django.db.models.BitOr` class.
82+
7383
``BitXor``
7484
----------
7585

@@ -78,6 +88,11 @@ General-purpose aggregation functions
7888
Returns an ``int`` of the bitwise ``XOR`` of all non-null input values, or
7989
``default`` if all values are null.
8090

91+
.. deprecated:: 6.1
92+
93+
This class is deprecated in favor of the generally available
94+
:class:`~django.db.models.BitXor` class.
95+
8196
``BoolAnd``
8297
-----------
8398

docs/ref/models/querysets.txt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4085,6 +4085,42 @@ by the aggregate.
40854085
unique values. This is the SQL equivalent of ``AVG(DISTINCT <field>)``.
40864086
The default value is ``False``.
40874087

4088+
``BitAnd``
4089+
~~~~~~~~~~
4090+
4091+
.. class:: BitAnd(expression, filter=None, default=None, **extra)
4092+
4093+
.. versionadded:: 6.1
4094+
4095+
Returns an ``int`` of the bitwise ``AND`` of all non-null input values, or
4096+
``default`` if all values are null.
4097+
4098+
The ``default`` parameter is not supported on MariaDB, MySQL, and Oracle.
4099+
4100+
``BitOr``
4101+
~~~~~~~~~
4102+
4103+
.. class:: BitOr(expression, filter=None, default=None, **extra)
4104+
4105+
.. versionadded:: 6.1
4106+
4107+
Returns an ``int`` of the bitwise ``OR`` of all non-null input values, or
4108+
``default`` if all values are null.
4109+
4110+
The ``default`` parameter is not supported on MariaDB, MySQL, and Oracle.
4111+
4112+
``BitXor``
4113+
~~~~~~~~~~
4114+
4115+
.. class:: BitXor(expression, filter=None, default=None, **extra)
4116+
4117+
.. versionadded:: 6.1
4118+
4119+
Returns an ``int`` of the bitwise ``XOR`` of all non-null input values, or
4120+
``default`` if all values are null.
4121+
4122+
The ``default`` parameter is not supported on MariaDB, MySQL, and Oracle.
4123+
40884124
``Count``
40894125
~~~~~~~~~
40904126

docs/releases/6.1.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,11 @@ Models
315315
:class:`~django.db.models.query.QuerySet` is ordered and the ordering is
316316
deterministic.
317317

318+
* The new :class:`~django.db.models.BitAnd`, :class:`~django.db.models.BitOr`,
319+
and :class:`~django.db.models.BitXor` aggregates return the bitwise ``AND``,
320+
``OR``, ``XOR``, respectively. These aggregates were previously included only
321+
in ``contrib.postgres``.
322+
318323
Pagination
319324
~~~~~~~~~~
320325

@@ -420,6 +425,9 @@ backends.
420425
``get_geom_placeholder_sql`` and is expected to return a two-elements tuple
421426
composed of an SQL format string and a tuple of associated parameters.
422427

428+
* Set the new ``DatabaseFeatures.supports_bit_aggregations`` attribute to
429+
``False`` if the database doesn't support bitwise aggregations.
430+
423431
:mod:`django.contrib.admin`
424432
---------------------------
425433

@@ -539,6 +547,12 @@ Miscellaneous
539547
:ref:`expressions <writing-your-own-query-expressions>`, is deprecated in
540548
favor of the newly introduced ``quote_name()`` method.
541549

550+
* The ``BitAnd``, ``BitOr``, and ``BitXor`` classes in
551+
``django.contrib.postgres.aggregates`` are deprecated in favor of the
552+
generally available :class:`~django.db.models.BitAnd`,
553+
:class:`~django.db.models.BitOr`, and :class:`~django.db.models.BitXor`
554+
classes.
555+
542556
Features removed in 6.1
543557
=======================
544558

0 commit comments

Comments
 (0)