summaryrefslogtreecommitdiff
path: root/tests/aggregation
diff options
context:
space:
mode:
authorMariusz Felisiak <felisiak.mariusz@gmail.com>2026-04-18 08:53:21 +0200
committerGitHub <noreply@github.com>2026-04-18 08:53:21 +0200
commited79c5959add54b6e8ea589ec601e0d2e801517e (patch)
tree0d00f241bea6de88203d1314c7de92cb262b3a3f /tests/aggregation
parentd687d412a9abd9c80e31945f16ce32c020512394 (diff)
Fixed #37028 -- Added BitAnd(), BitOr(), and BitXor() aggregates.
Diffstat (limited to 'tests/aggregation')
-rw-r--r--tests/aggregation/models.py2
-rw-r--r--tests/aggregation/tests.py129
2 files changed, 127 insertions, 4 deletions
diff --git a/tests/aggregation/models.py b/tests/aggregation/models.py
index 6eed9b22fb..bbfeb1016e 100644
--- a/tests/aggregation/models.py
+++ b/tests/aggregation/models.py
@@ -13,7 +13,7 @@ class Author(models.Model):
class Publisher(models.Model):
name = models.CharField(max_length=255)
- num_awards = models.IntegerField()
+ num_awards = models.IntegerField(null=True)
duration = models.DurationField(blank=True, null=True)
def __str__(self):
diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py
index 0a975dcb52..af46e61609 100644
--- a/tests/aggregation/tests.py
+++ b/tests/aggregation/tests.py
@@ -10,6 +10,9 @@ from django.db import NotSupportedError, connection
from django.db.models import (
AnyValue,
Avg,
+ BitAnd,
+ BitOr,
+ BitXor,
Case,
CharField,
Count,
@@ -1957,7 +1960,20 @@ class AggregateTestCase(TestCase):
Count("age", default=0)
def test_aggregation_default_unset(self):
- for Aggregate in [Avg, Max, Min, StdDev, Sum, Variance]:
+ test_aggregates = [Avg, Max, Min, StdDev, Sum, Variance]
+ if (
+ connection.features.supports_bit_aggregations
+ and connection.features.supports_default_in_bit_aggregations
+ ):
+ # Some databases (e.g. Oracle, MySQL, MariaDB) don't return NULL on
+ # empty sets:
+ # - BitAnd on MySQL and MariaDB returns 18446744073709551615 (all
+ # bits set to 1),
+ # - BitAnd on Oracle returns 0
+ # - BitOr and BitXor on Oracle, MySQL, and MariaDB return 0
+ # As a consequence, they don't support the default parameter.
+ test_aggregates.extend([BitAnd, BitOr, BitXor])
+ for Aggregate in test_aggregates:
with self.subTest(Aggregate):
result = Author.objects.filter(age__gt=100).aggregate(
value=Aggregate("age"),
@@ -1965,7 +1981,13 @@ class AggregateTestCase(TestCase):
self.assertIsNone(result["value"])
def test_aggregation_default_zero(self):
- for Aggregate in [Avg, Max, Min, StdDev, Sum, Variance]:
+ test_aggregates = [Avg, Max, Min, StdDev, Sum, Variance]
+ if (
+ connection.features.supports_bit_aggregations
+ and connection.features.supports_default_in_bit_aggregations
+ ):
+ test_aggregates.extend([BitAnd, BitOr, BitXor])
+ for Aggregate in test_aggregates:
with self.subTest(Aggregate):
result = Author.objects.filter(age__gt=100).aggregate(
value=Aggregate("age", default=0),
@@ -1973,7 +1995,13 @@ class AggregateTestCase(TestCase):
self.assertEqual(result["value"], 0)
def test_aggregation_default_integer(self):
- for Aggregate in [Avg, Max, Min, StdDev, Sum, Variance]:
+ test_aggregates = [Avg, Max, Min, StdDev, Sum, Variance]
+ if (
+ connection.features.supports_bit_aggregations
+ and connection.features.supports_default_in_bit_aggregations
+ ):
+ test_aggregates.extend([BitAnd, BitOr, BitXor])
+ for Aggregate in test_aggregates:
with self.subTest(Aggregate):
result = Author.objects.filter(age__gt=100).aggregate(
value=Aggregate("age", default=21),
@@ -2354,6 +2382,101 @@ class AggregateTestCase(TestCase):
with self.assertRaisesMessage(TypeError, msg):
super(function, func_instance).__init__(Value(1), Value(2))
+ @skipIfDBFeature("supports_bit_aggregations")
+ def test_bit_aggregations_not_supported(self):
+ for BitAggregate in [BitAnd, BitOr, BitXor]:
+ msg = f"{BitAggregate.name} is not supported on {connection.vendor}."
+ with (
+ self.subTest(BitAggregate.name),
+ self.assertRaisesMessage(NotSupportedError, msg),
+ ):
+ Publisher.objects.aggregate(BitAggregate("num_awards"))
+
+ @skipUnlessDBFeature("supports_bit_aggregations")
+ @skipIfDBFeature("supports_default_in_bit_aggregations")
+ def test_bit_aggregations_default_not_supported(self):
+ for BitAggregate in [BitAnd, BitOr, BitXor]:
+ msg = (
+ f"{BitAggregate.name} does not support the default parameter on "
+ f"{connection.vendor}."
+ )
+ with (
+ self.subTest(BitAggregate.name),
+ self.assertRaisesMessage(NotSupportedError, msg),
+ ):
+ Publisher.objects.aggregate(BitAggregate("num_awards", default=21))
+
+ @skipUnlessDBFeature("supports_bit_aggregations")
+ def test_bit_and(self):
+ Publisher.objects.create(name="Albatros", num_awards=0)
+ Publisher.objects.create(name="Newish")
+ values = Publisher.objects.filter(
+ Q(num_awards__in=[0, 1]) | Q(num_awards__isnull=True)
+ ).aggregate(bitand=BitAnd("num_awards"))
+ self.assertEqual(values, {"bitand": 0})
+
+ @skipUnlessDBFeature("supports_bit_aggregations")
+ def test_bit_and_on_only_true_values(self):
+ values = Publisher.objects.filter(num_awards=1).aggregate(
+ bitand=BitAnd("num_awards")
+ )
+ self.assertEqual(values, {"bitand": 1})
+
+ @skipUnlessDBFeature("supports_bit_aggregations")
+ def test_bit_and_on_only_false_values(self):
+ Publisher.objects.create(name="Albatros", num_awards=0)
+ values = Publisher.objects.filter(num_awards=0).aggregate(
+ bitand=BitAnd("num_awards")
+ )
+ self.assertEqual(values, {"bitand": 0})
+
+ @skipUnlessDBFeature("supports_bit_aggregations")
+ def test_bit_or(self):
+ Publisher.objects.create(name="Albatros", num_awards=0)
+ Publisher.objects.create(name="Newish")
+ values = Publisher.objects.filter(
+ Q(num_awards__in=[0, 1]) | Q(num_awards__isnull=True)
+ ).aggregate(bitor=BitOr("num_awards"))
+ self.assertEqual(values, {"bitor": 1})
+
+ @skipUnlessDBFeature("supports_bit_aggregations")
+ def test_bit_or_on_only_true_values(self):
+ values = Publisher.objects.filter(num_awards=1).aggregate(
+ bitor=BitOr("num_awards")
+ )
+ self.assertEqual(values, {"bitor": 1})
+
+ @skipUnlessDBFeature("supports_bit_aggregations")
+ def test_bit_or_on_only_false_values(self):
+ Publisher.objects.create(name="Albatros", num_awards=0)
+ values = Publisher.objects.filter(num_awards=0).aggregate(
+ bitor=BitOr("num_awards")
+ )
+ self.assertEqual(values, {"bitor": 0})
+
+ @skipUnlessDBFeature("supports_bit_aggregations")
+ def test_bit_xor(self):
+ Publisher.objects.create(name="Newish")
+ values = Publisher.objects.filter(
+ Q(num_awards__in=[1, 3]) | Q(num_awards__isnull=True)
+ ).aggregate(bitxor=BitXor("num_awards"))
+ self.assertEqual(values, {"bitxor": 2})
+
+ @skipUnlessDBFeature("supports_bit_aggregations")
+ def test_bit_xor_on_only_true_values(self):
+ values = Publisher.objects.filter(
+ num_awards=1,
+ ).aggregate(bitxor=BitXor("num_awards"))
+ self.assertEqual(values, {"bitxor": 1})
+
+ @skipUnlessDBFeature("supports_bit_aggregations")
+ def test_bit_xor_on_only_false_values(self):
+ Publisher.objects.create(name="Albatros", num_awards=0)
+ values = Publisher.objects.filter(
+ num_awards=0,
+ ).aggregate(bitxor=BitXor("num_awards"))
+ self.assertEqual(values, {"bitxor": 0})
+
def test_string_agg_requires_delimiter(self):
with self.assertRaises(TypeError):
Book.objects.aggregate(stringagg=StringAgg("name"))