diff --git a/django/db/models/aggregates.py b/django/db/models/aggregates.py index b5be24e3bc..b7732a50ba 100644 --- a/django/db/models/aggregates.py +++ b/django/db/models/aggregates.py @@ -223,9 +223,9 @@ class JSONArrayAgg(Aggregate): arity = 1 def as_sql(self, compiler, connection, **extra_context): - if not connection.features.supports_aggregate_filter_clause: + if self.filter and not connection.features.supports_aggregate_filter_clause: raise NotSupportedError( - "JSONArrayAgg is not supported on this database backend." + "JSONArrayAgg(filter) is not supported on this database backend." ) return super().as_sql(compiler, connection, **extra_context) @@ -233,8 +233,10 @@ class JSONArrayAgg(Aggregate): sql, params = self.as_sql( compiler, connection, function="JSON_GROUP_ARRAY", **extra_context ) + # JSON_GROUP_ARRAY defaults to returning an empty array on an empty set. if (default := self.default) == []: return sql, params + # Ensure Count() is against the exact same parameters (filter, distinct) count = self.copy() count.__class__ = Count count_sql, count_params = compiler.compile(count) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index bbe77243ab..ea4e487f55 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -4050,6 +4050,8 @@ by the aggregate. ``JSONArrayAgg`` ~~~~~~~~~~~~~~~~ +.. versionadded:: 5.1 + .. class:: JSONArrayAgg(expression, output_field=None, sample=False, filter=None, default=None, **extra) Converts each expression to a JSON value and returns a single JSON array diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py index adbb7204ca..618ab98369 100644 --- a/tests/aggregation/tests.py +++ b/tests/aggregation/tests.py @@ -2159,85 +2159,6 @@ class AggregateTestCase(TestCase): ) self.assertEqual(list(author_qs), [337]) - @skipUnlessDBFeature("supports_aggregate_filter_clause") - def test_JSONArrayAgg(self): - vals = Store.objects.aggregate(jsonarrayagg=JSONArrayAgg("name")) - self.assertEqual( - vals, - {"jsonarrayagg": ["Amazon.com", "Books.com", "Mamma and Pappa's Books"]}, - ) - - @skipUnlessDBFeature("supports_aggregate_filter_clause") - def test_JSONArrayAgg_datefield(self): - vals = Book.objects.aggregate(jsonarrayagg=JSONArrayAgg("pubdate")) - self.assertEqual( - vals, - { - "jsonarrayagg": [ - "2007-12-06", - "2008-03-03", - "2008-06-23", - "2008-11-03", - "1995-01-15", - "1991-10-15", - ] - }, - ) - - @skipUnlessDBFeature("supports_aggregate_filter_clause") - def test_JSONArrayAgg_decimalfield(self): - vals = Book.objects.aggregate(jsonarrayagg=JSONArrayAgg("price")) - self.assertEqual( - vals, {"jsonarrayagg": [30.0, 23.09, 29.69, 29.69, 82.8, 75.0]} - ) - - @skipUnlessDBFeature("supports_aggregate_filter_clause") - def test_JSONArrayAgg_integerfield(self): - vals = Book.objects.aggregate(jsonarrayagg=JSONArrayAgg("pages")) - self.assertEqual(vals, {"jsonarrayagg": [447, 528, 300, 350, 1132, 946]}) - - @skipUnlessDBFeature("supports_aggregate_filter_clause") - def test_JSONArrayAgg_filter(self): - vals = Author.objects.aggregate( - jsonarrayagg=JSONArrayAgg("age", filter=Q(age__gt=29)) - ) - - self.assertEqual(vals, {"jsonarrayagg": [34, 35, 45, 37, 57, 46]}) - - @skipUnlessDBFeature("supports_aggregate_filter_clause") - def test_JSONArrayAgg_empty_result_set(self): - Author.objects.all().delete() - - val = Author.objects.aggregate(jsonarrayagg=JSONArrayAgg("age")) - - self.assertEqual(val, {"jsonarrayagg": None}) - - @skipUnlessDBFeature("supports_aggregate_filter_clause") - def test_JSONArrayAgg_default_set(self): - Author.objects.all().delete() - - val = Author.objects.aggregate( - jsonarrayagg=JSONArrayAgg("name", default=[""]) - ) - self.assertEqual(val, {"jsonarrayagg": [""]}) - - @skipUnlessDBFeature("supports_aggregate_filter_clause") - def test_JSONArrayAgg_distinct_false(self): - val = Author.objects.aggregate(jsonarrayagg=JSONArrayAgg("age", distinct=False)) - self.assertEqual(val, {"jsonarrayagg": [34, 35, 45, 29, 37, 29, 25, 57, 46]}) - - @skipUnlessDBFeature("supports_aggregate_filter_clause") - def test_JSONArrayAgg_distinct_true(self): - msg = "JSONArrayAgg does not allow distinct." - with self.assertRaisesMessage(TypeError, msg): - JSONArrayAgg("age", distinct=True) - - @skipIfDBFeature("supports_aggregate_filter_clause") - def test_JSONArrayAgg_not_supported(self): - msg = "JSONArrayAgg is not supported on this database backend." - with self.assertRaisesMessage(NotSupportedError, msg): - Author.objects.aggregate(arrayagg=JSONArrayAgg("age")) - class AggregateAnnotationPruningTests(TestCase): @classmethod @@ -2439,3 +2360,86 @@ class AggregateAnnotationPruningTests(TestCase): ) ) self.assertEqual(qs.count(), 3) + + +class JSONArrayAggTests(TestCase): + @classmethod + def setUpTestData(cls): + cls.a1 = Author.objects.create(name="Adrian Holovaty", age=34) + cls.a2 = Author.objects.create(name="Jacob Kaplan-Moss", age=35) + cls.a3 = Author.objects.create(name="Brad Dayley", age=45) + + cls.p1 = Publisher.objects.create( + name="Apress", num_awards=3, duration=datetime.timedelta(days=1) + ) + + cls.b1 = Book.objects.create( + isbn="159059725", + name="The Definitive Guide to Django: Web Development Done Right", + pages=447, + rating=4.5, + price=Decimal("30.00"), + contact=cls.a1, + publisher=cls.p1, + pubdate=datetime.date(2007, 12, 6), + ) + + def test_JSONArrayAgg(self): + vals = Author.objects.aggregate(jsonarrayagg=JSONArrayAgg("name")) + self.assertEqual( + vals, + {"jsonarrayagg": ["Adrian Holovaty", "Jacob Kaplan-Moss", "Brad Dayley"]}, + ) + + def test_JSONArrayAgg_datefield(self): + vals = Book.objects.aggregate(jsonarrayagg=JSONArrayAgg("pubdate")) + self.assertEqual( + vals, + { + "jsonarrayagg": [ + "2007-12-06", + ] + }, + ) + + def test_JSONArrayAgg_decimalfield(self): + vals = Book.objects.aggregate(jsonarrayagg=JSONArrayAgg("price")) + self.assertEqual(vals, {"jsonarrayagg": [30.0]}) + + def test_JSONArrayAgg_integerfield(self): + vals = Book.objects.aggregate(jsonarrayagg=JSONArrayAgg("pages")) + self.assertEqual(vals, {"jsonarrayagg": [447]}) + + @skipUnlessDBFeature("supports_aggregate_filter_clause") + def test_JSONArrayAgg_filter(self): + vals = Author.objects.aggregate( + jsonarrayagg=JSONArrayAgg("age", filter=Q(age__gt=35)) + ) + + self.assertEqual(vals, {"jsonarrayagg": [45]}) + + def test_JSONArrayAgg_empty_result_set(self): + Author.objects.all().delete() + + val = Author.objects.aggregate(jsonarrayagg=JSONArrayAgg("age")) + + self.assertEqual(val, {"jsonarrayagg": None}) + + def test_JSONArrayAgg_default_set(self): + Author.objects.all().delete() + + val = Author.objects.aggregate( + jsonarrayagg=JSONArrayAgg("name", default=[""]) + ) + self.assertEqual(val, {"jsonarrayagg": [""]}) + + def test_JSONArrayAgg_distinct_true(self): + msg = "JSONArrayAgg does not allow distinct." + with self.assertRaisesMessage(TypeError, msg): + JSONArrayAgg("age", distinct=True) + + @skipIfDBFeature("supports_aggregate_filter_clause") + def test_JSONArrayAgg_not_supported(self): + msg = "JSONArrayAgg(filter) is not supported on this database backend." + with self.assertRaisesMessage(NotSupportedError, msg): + Author.objects.aggregate(arrayagg=JSONArrayAgg("age", filter=Q(age__gt=35)))