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 /tests | |
| parent | d687d412a9abd9c80e31945f16ce32c020512394 (diff) | |
Fixed #37028 -- Added BitAnd(), BitOr(), and BitXor() aggregates.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/aggregation/models.py | 2 | ||||
| -rw-r--r-- | tests/aggregation/tests.py | 129 | ||||
| -rw-r--r-- | tests/postgres_tests/test_aggregates.py | 83 |
3 files changed, 146 insertions, 68 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")) diff --git a/tests/postgres_tests/test_aggregates.py b/tests/postgres_tests/test_aggregates.py index 4355eb2f98..bd57b8e338 100644 --- a/tests/postgres_tests/test_aggregates.py +++ b/tests/postgres_tests/test_aggregates.py @@ -21,14 +21,14 @@ from . import PostgreSQLTestCase from .models import AggregateTestModel, HotelReservation, Room, StatTestModel try: + from django.contrib.postgres.aggregates import BitAnd # RemovedInDjango70Warning + from django.contrib.postgres.aggregates import BitOr # RemovedInDjango70Warning + from django.contrib.postgres.aggregates import BitXor # RemovedInDjango70Warning from django.contrib.postgres.aggregates import ( StringAgg, # RemovedInDjango70Warning. ) from django.contrib.postgres.aggregates import ( ArrayAgg, - BitAnd, - BitOr, - BitXor, BoolAnd, BoolOr, Corr, @@ -91,12 +91,9 @@ class TestGeneralAggregate(PostgreSQLTestCase): ArrayAgg("char_field"), ArrayAgg("integer_field"), ArrayAgg("boolean_field"), - BitAnd("integer_field"), - BitOr("integer_field"), BoolAnd("boolean_field"), BoolOr("boolean_field"), JSONBAgg("integer_field"), - BitXor("integer_field"), ] for aggregation in tests: with self.subTest(aggregation=aggregation): @@ -119,8 +116,6 @@ class TestGeneralAggregate(PostgreSQLTestCase): (ArrayAgg("char_field", default=["<empty>"]), ["<empty>"]), (ArrayAgg("integer_field", default=[0]), [0]), (ArrayAgg("boolean_field", default=[False]), [False]), - (BitAnd("integer_field", default=0), 0), - (BitOr("integer_field", default=0), 0), (BoolAnd("boolean_field", default=False), False), (BoolOr("boolean_field", default=False), False), (JSONBAgg("integer_field", default=["<empty>"]), ["<empty>"]), @@ -128,7 +123,6 @@ class TestGeneralAggregate(PostgreSQLTestCase): JSONBAgg("integer_field", default=Value(["<empty>"], JSONField())), ["<empty>"], ), - (BitXor("integer_field", default=0), 0), ] for aggregation, expected_result in tests: with self.subTest(aggregation=aggregation): @@ -334,61 +328,6 @@ class TestGeneralAggregate(PostgreSQLTestCase): ) ) - def test_bit_and_general(self): - values = AggregateTestModel.objects.filter(integer_field__in=[0, 1]).aggregate( - bitand=BitAnd("integer_field") - ) - self.assertEqual(values, {"bitand": 0}) - - def test_bit_and_on_only_true_values(self): - values = AggregateTestModel.objects.filter(integer_field=1).aggregate( - bitand=BitAnd("integer_field") - ) - self.assertEqual(values, {"bitand": 1}) - - def test_bit_and_on_only_false_values(self): - values = AggregateTestModel.objects.filter(integer_field=0).aggregate( - bitand=BitAnd("integer_field") - ) - self.assertEqual(values, {"bitand": 0}) - - def test_bit_or_general(self): - values = AggregateTestModel.objects.filter(integer_field__in=[0, 1]).aggregate( - bitor=BitOr("integer_field") - ) - self.assertEqual(values, {"bitor": 1}) - - def test_bit_or_on_only_true_values(self): - values = AggregateTestModel.objects.filter(integer_field=1).aggregate( - bitor=BitOr("integer_field") - ) - self.assertEqual(values, {"bitor": 1}) - - def test_bit_or_on_only_false_values(self): - values = AggregateTestModel.objects.filter(integer_field=0).aggregate( - bitor=BitOr("integer_field") - ) - self.assertEqual(values, {"bitor": 0}) - - 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}) - - 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}) - - 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}) @@ -651,6 +590,22 @@ class TestGeneralAggregate(PostgreSQLTestCase): self.assertEqual(values, {"stringagg": "Foo1'Foo2'Foo4'Foo3"}) self.assertEqual(ctx.filename, __file__) + def test_bit_agg_deprecation(self): + for BitAggregate in [BitAnd, BitOr, BitXor]: + msg = ( + f"The PostgreSQL-specific {BitAggregate.__name__} function is " + f"deprecated. Use django.db.models.aggregates.{BitAggregate.__name__} " + "instead." + ) + with ( + self.subTest(BitAggregate.__name__), + self.assertWarnsMessage(RemovedInDjango70Warning, msg) as ctx, + ): + AggregateTestModel.objects.aggregate( + bitagg=BitAggregate("integer_field") + ) + self.assertEqual(ctx.filename, __file__) + class TestAggregateDistinct(PostgreSQLTestCase): @classmethod |
