diff options
| author | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2026-04-18 08:53:21 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-18 08:53:21 +0200 |
| commit | ed79c5959add54b6e8ea589ec601e0d2e801517e (patch) | |
| tree | 0d00f241bea6de88203d1314c7de92cb262b3a3f /django | |
| parent | d687d412a9abd9c80e31945f16ce32c020512394 (diff) | |
Fixed #37028 -- Added BitAnd(), BitOr(), and BitXor() aggregates.
Diffstat (limited to 'django')
| -rw-r--r-- | django/contrib/postgres/aggregates/general.py | 45 | ||||
| -rw-r--r-- | django/db/backends/base/features.py | 6 | ||||
| -rw-r--r-- | django/db/backends/mysql/features.py | 1 | ||||
| -rw-r--r-- | django/db/backends/oracle/features.py | 5 | ||||
| -rw-r--r-- | django/db/backends/sqlite3/_functions.py | 28 | ||||
| -rw-r--r-- | django/db/models/aggregates.py | 51 |
6 files changed, 126 insertions, 10 deletions
diff --git a/django/contrib/postgres/aggregates/general.py b/django/contrib/postgres/aggregates/general.py index 76dc7e2633..be43e899be 100644 --- a/django/contrib/postgres/aggregates/general.py +++ b/django/contrib/postgres/aggregates/general.py @@ -1,16 +1,20 @@ import warnings from django.contrib.postgres.fields import ArrayField -from django.db.models import Aggregate, BooleanField, JSONField +from django.db.models import Aggregate +from django.db.models import BitAnd as _BitAnd +from django.db.models import BitOr as _BitOr +from django.db.models import BitXor as _BitXor +from django.db.models import BooleanField, JSONField from django.db.models import StringAgg as _StringAgg from django.db.models import Value from django.utils.deprecation import RemovedInDjango70Warning __all__ = [ "ArrayAgg", - "BitAnd", - "BitOr", - "BitXor", + "BitAnd", # RemovedInDjango70Warning + "BitOr", # RemovedInDjango70Warning + "BitXor", # RemovedInDjango70Warning "BoolAnd", "BoolOr", "JSONBAgg", @@ -28,16 +32,37 @@ class ArrayAgg(Aggregate): return ArrayField(self.source_expressions[0].output_field) -class BitAnd(Aggregate): - function = "BIT_AND" +class BitAnd(_BitAnd): + def __init__(self, expression, **extra): + warnings.warn( + "The PostgreSQL-specific BitAnd function is deprecated. Use " + "django.db.models.aggregates.BitAnd instead.", + category=RemovedInDjango70Warning, + stacklevel=2, + ) + super().__init__(expression, **extra) -class BitOr(Aggregate): - function = "BIT_OR" +class BitOr(_BitOr): + def __init__(self, expression, **extra): + warnings.warn( + "The PostgreSQL-specific BitOr function is deprecated. Use " + "django.db.models.aggregates.BitOr instead.", + category=RemovedInDjango70Warning, + stacklevel=2, + ) + super().__init__(expression, **extra) -class BitXor(Aggregate): - function = "BIT_XOR" +class BitXor(_BitXor): + def __init__(self, expression, **extra): + warnings.warn( + "The PostgreSQL-specific BitXor function is deprecated. Use " + "django.db.models.aggregates.BitXor instead.", + category=RemovedInDjango70Warning, + stacklevel=2, + ) + super().__init__(expression, **extra) class BoolAnd(Aggregate): diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index 0d9178dae8..6770c177c1 100644 --- a/django/db/backends/base/features.py +++ b/django/db/backends/base/features.py @@ -274,6 +274,12 @@ class BaseDatabaseFeatures: # Does the database support SQL 2023 ANY_VALUE in GROUP BY? supports_any_value = False + # Does the database support bitwise aggregations: BIT_AND, BIT_OR, and + # BIT_XOR? + supports_bit_aggregations = True + # Does the backend support the default parameter in bitwise aggregations? + supports_default_in_bit_aggregations = True + # Does the backend support indexing a TextField? supports_index_on_text_field = True diff --git a/django/db/backends/mysql/features.py b/django/db/backends/mysql/features.py index eb9601bcef..e7970cb063 100644 --- a/django/db/backends/mysql/features.py +++ b/django/db/backends/mysql/features.py @@ -28,6 +28,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_over_clause = True supports_frame_range_fixed_distance = True supports_update_conflicts = True + supports_default_in_bit_aggregations = False can_rename_index = True delete_can_self_reference_subquery = False create_test_procedure_without_params_sql = """ diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py index 39d857be59..55c64a818d 100644 --- a/django/db/backends/oracle/features.py +++ b/django/db/backends/oracle/features.py @@ -79,6 +79,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_collation_on_textfield = False supports_on_delete_db_default = False supports_no_precision_decimalfield = True + supports_default_in_bit_aggregations = False test_now_utc_template = "CURRENT_TIMESTAMP AT TIME ZONE 'UTC'" django_test_expected_failures = { # A bug in Django/oracledb with respect to string handling (#23843). @@ -240,3 +241,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): @cached_property def supports_stored_generated_columns(self): return self.connection.oracle_version >= (23, 7) + + @cached_property + def supports_bit_aggregations(self): + return self.connection.oracle_version >= (21,) diff --git a/django/db/backends/sqlite3/_functions.py b/django/db/backends/sqlite3/_functions.py index aa4100e83f..73a5f4d987 100644 --- a/django/db/backends/sqlite3/_functions.py +++ b/django/db/backends/sqlite3/_functions.py @@ -3,6 +3,7 @@ Implementations of SQL functions for SQLite. """ import functools +import operator import random import statistics import zoneinfo @@ -86,6 +87,9 @@ def register(connection): connection.create_aggregate("VAR_POP", 1, VarPop) connection.create_aggregate("VAR_SAMP", 1, VarSamp) connection.create_aggregate("ANY_VALUE", 1, AnyValue) + connection.create_aggregate("BIT_AND", 1, BitAnd) + connection.create_aggregate("BIT_OR", 1, BitOr) + connection.create_aggregate("BIT_XOR", 1, BitXor) connection.create_function("UUIDV4", 0, _sqlite_uuid4) if PY314: connection.create_function("UUIDV7", 0, _sqlite_uuid7) @@ -535,3 +539,27 @@ class VarSamp(ListAggregate): class AnyValue(ListAggregate): def finalize(self): return self[0] + + +class BitAggregate(ListAggregate): + bit_operator = None + + def finalize(self): + items = (item for item in self if item is not None) + try: + return functools.reduce(self.bit_operator, items) + except TypeError: + # items may be an empty iterator when all elements are None. + return None + + +class BitAnd(BitAggregate): + bit_operator = operator.and_ + + +class BitOr(BitAggregate): + bit_operator = operator.or_ + + +class BitXor(BitAggregate): + bit_operator = operator.xor diff --git a/django/db/models/aggregates.py b/django/db/models/aggregates.py index d1139e8bcc..efbf7e5b0c 100644 --- a/django/db/models/aggregates.py +++ b/django/db/models/aggregates.py @@ -24,6 +24,9 @@ __all__ = [ "Aggregate", "AnyValue", "Avg", + "BitAnd", + "BitOr", + "BitXor", "Count", "Max", "Min", @@ -241,6 +244,54 @@ class Avg(FixDurationInputMixin, NumericOutputFieldMixin, Aggregate): arity = 1 +class BitAggregate(Aggregate): + arity = 1 + + def __init__(self, expression, **extra): + super().__init__(expression, **extra) + # self.default is reset in Aggregate.resolve_expression(). Store the + # information for the later check in as_sql(). + self._has_default = self.default is not None + + def as_sql(self, compiler, connection, **extra_context): + if not connection.features.supports_bit_aggregations: + raise NotSupportedError( + f"{self.name} is not supported on {connection.vendor}." + ) + if ( + self._has_default + and not connection.features.supports_default_in_bit_aggregations + ): + raise NotSupportedError( + f"{self.name} does not support the default parameter on " + f"{connection.vendor}." + ) + return super().as_sql(compiler, connection, **extra_context) + + def as_oracle(self, compiler, connection, **extra_context): + return self.as_sql( + compiler, + connection, + function=f"{self.function}_AGG", + **extra_context, + ) + + +class BitAnd(BitAggregate): + function = "BIT_AND" + name = "BitAnd" + + +class BitOr(BitAggregate): + function = "BIT_OR" + name = "BitOr" + + +class BitXor(BitAggregate): + function = "BIT_XOR" + name = "BitXor" + + class Count(Aggregate): function = "COUNT" name = "Count" |
