mirror of
https://github.com/django/django.git
synced 2025-08-04 10:59:45 +00:00
Fixed #35462 -- Added support for JSON_ArrayAgg aggregate function.
Supported the JSON_ARRAYAGG aggregate function and JSON_GROUP_ARRAY as a variant to add support for sqlite database. Added tests for the JSON_ARRAYAGG and JSON_GROUP_ARRAY aggregate functions. Documented JSONArrayAgg and added a release note.
This commit is contained in:
parent
1831f7733d
commit
51db6a80ba
4 changed files with 175 additions and 0 deletions
|
@ -14,6 +14,7 @@ from django.db.models.expressions import (
|
|||
When,
|
||||
)
|
||||
from django.db.models.fields import IntegerField, TextField
|
||||
from django.db.models.fields.json import JSONField
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models.functions.mixins import (
|
||||
FixDurationInputMixin,
|
||||
|
@ -30,6 +31,7 @@ __all__ = [
|
|||
"StringAgg",
|
||||
"Sum",
|
||||
"Variance",
|
||||
"JSONArrayAgg",
|
||||
]
|
||||
|
||||
|
||||
|
@ -392,3 +394,61 @@ class Variance(NumericOutputFieldMixin, Aggregate):
|
|||
|
||||
def _get_repr_options(self):
|
||||
return {**super()._get_repr_options(), "sample": self.function == "VAR_SAMP"}
|
||||
|
||||
|
||||
class JSONArrayAgg(Aggregate):
|
||||
function = "JSON_ARRAYAGG"
|
||||
output_field = JSONField()
|
||||
arity = 1
|
||||
|
||||
def as_sql(self, compiler, connection, **extra_context):
|
||||
if self.filter and not connection.features.supports_aggregate_filter_clause:
|
||||
raise NotSupportedError(
|
||||
"JSONArrayAgg(filter) is not supported on this database backend."
|
||||
)
|
||||
return super().as_sql(compiler, connection, **extra_context)
|
||||
|
||||
def as_sqlite(self, compiler, connection, **extra_context):
|
||||
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.
|
||||
# Modifies the SQL to support a custom default value to be returned,
|
||||
# if a default argument is not passed, null is returned instead of [].
|
||||
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)
|
||||
default_sql = ""
|
||||
default_params = ()
|
||||
if default is not None:
|
||||
default_sql, default_params = compiler.compile(default)
|
||||
default_sql = f" ELSE {default_sql}"
|
||||
sql = f"(CASE WHEN {count_sql} > 0 THEN {sql}{default_sql} END)"
|
||||
return sql, count_params + params + default_params
|
||||
|
||||
def as_postgresql(self, compiler, connection, **extra_context):
|
||||
if not connection.features.is_postgresql_16:
|
||||
sql, params = super().as_sql(
|
||||
compiler,
|
||||
connection,
|
||||
function="ARRAY_AGG",
|
||||
**extra_context,
|
||||
)
|
||||
return f"TO_JSONB({sql})", params
|
||||
extra_context.setdefault(
|
||||
"template", "%(function)s(%(distinct)s%(expressions)s RETURNING JSONB)"
|
||||
)
|
||||
return self.as_sql(compiler, connection, **extra_context)
|
||||
|
||||
def as_oracle(self, compiler, connection, **extra_context):
|
||||
# Return same date field format as on other supported backends.
|
||||
expression = self.get_source_expressions()[0]
|
||||
internal_type = expression.output_field.get_internal_type()
|
||||
if internal_type == "DateField":
|
||||
extra_context.setdefault(
|
||||
"template", "%(function)s(TO_CHAR(%(expressions)s, 'YYYY-MM-DD'))"
|
||||
)
|
||||
return self.as_sql(compiler, connection, **extra_context)
|
||||
|
|
|
@ -4078,6 +4078,19 @@ by the aggregate.
|
|||
A ``Value`` or expression representing the string that should separate
|
||||
each of the values. For example, ``Value(",")``.
|
||||
|
||||
``JSONArrayAgg``
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 6.0
|
||||
|
||||
.. 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
|
||||
containing those JSON values.
|
||||
|
||||
* Return type: ``list`` containing an element for each value
|
||||
in a set of JSON or SQL values.
|
||||
|
||||
Query-related tools
|
||||
===================
|
||||
|
||||
|
|
|
@ -196,6 +196,10 @@ Models
|
|||
values concatenated into a string, separated by the ``delimiter`` string.
|
||||
This aggregate was previously supported only for PostgreSQL.
|
||||
|
||||
* The new :class:`~django.db.models.JSONArrayAgg` aggregate function accepts
|
||||
a list of field names or expressions, converts each expression to a JSON
|
||||
value, and returns a single JSON array containing those JSON values.
|
||||
|
||||
* The :meth:`~django.db.models.Model.save` method now raises a specialized
|
||||
:exc:`Model.NotUpdated <django.db.models.Model.NotUpdated>` exception, when
|
||||
:ref:`a forced update <ref-models-force-insert>` results in no affected rows,
|
||||
|
|
|
@ -18,6 +18,7 @@ from django.db.models import (
|
|||
F,
|
||||
FloatField,
|
||||
IntegerField,
|
||||
JSONArrayAgg,
|
||||
Max,
|
||||
Min,
|
||||
OuterRef,
|
||||
|
@ -2640,3 +2641,100 @@ class AggregateAnnotationPruningTests(TestCase):
|
|||
)
|
||||
)
|
||||
self.assertEqual(qs.count(), 3)
|
||||
|
||||
|
||||
@skipUnlessDBFeature("supports_json_field")
|
||||
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=45)
|
||||
cls.p1 = Publisher.objects.create(num_awards=3)
|
||||
cls.p2 = Publisher.objects.create(num_awards=1)
|
||||
cls.b1 = Book.objects.create(
|
||||
isbn="159059725",
|
||||
name="b1",
|
||||
pages=447,
|
||||
rating=4.5,
|
||||
price=Decimal("30.00"),
|
||||
contact=cls.a1,
|
||||
publisher=cls.p1,
|
||||
pubdate=datetime.date(2007, 12, 6),
|
||||
)
|
||||
cls.b1.authors.add(cls.a1)
|
||||
cls.b2 = Book.objects.create(
|
||||
isbn="067232959",
|
||||
name="b2",
|
||||
pages=528,
|
||||
rating=3.0,
|
||||
price=Decimal("23.09"),
|
||||
contact=cls.a2,
|
||||
publisher=cls.p2,
|
||||
pubdate=datetime.date(2008, 3, 3),
|
||||
)
|
||||
cls.b2.authors.add(cls.a2)
|
||||
|
||||
def test(self):
|
||||
vals = Book.objects.aggregate(jsonarrayagg=JSONArrayAgg("contact__name"))
|
||||
self.assertEqual(
|
||||
vals,
|
||||
{"jsonarrayagg": ["Adrian Holovaty", "Jacob Kaplan-Moss"]},
|
||||
)
|
||||
|
||||
def test_datefield(self):
|
||||
vals = Author.objects.aggregate(jsonarrayagg=JSONArrayAgg("book__pubdate"))
|
||||
self.assertEqual(
|
||||
vals,
|
||||
{
|
||||
"jsonarrayagg": [
|
||||
"2007-12-06",
|
||||
"2008-03-03",
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
def test_decimalfield(self):
|
||||
vals = Author.objects.aggregate(jsonarrayagg=JSONArrayAgg("book__price"))
|
||||
self.assertEqual(vals, {"jsonarrayagg": [30.0, 23.09]})
|
||||
|
||||
def test_integerfield(self):
|
||||
vals = Author.objects.aggregate(jsonarrayagg=JSONArrayAgg("book__pages"))
|
||||
self.assertEqual(vals, {"jsonarrayagg": [447, 528]})
|
||||
|
||||
@skipUnlessDBFeature("supports_aggregate_filter_clause")
|
||||
def test_filter(self):
|
||||
vals = Book.objects.aggregate(
|
||||
jsonarrayagg=JSONArrayAgg("contact__age", filter=Q(contact__age__gt=35))
|
||||
)
|
||||
self.assertEqual(vals, {"jsonarrayagg": [45]})
|
||||
|
||||
def test_empty_result_set(self):
|
||||
Author.objects.all().delete()
|
||||
val = Author.objects.aggregate(jsonarrayagg=JSONArrayAgg("age"))
|
||||
self.assertEqual(val, {"jsonarrayagg": None})
|
||||
|
||||
def test_default_set(self):
|
||||
Author.objects.all().delete()
|
||||
val = Author.objects.aggregate(
|
||||
jsonarrayagg=JSONArrayAgg("name", default=["<empty>"])
|
||||
)
|
||||
self.assertEqual(val, {"jsonarrayagg": ["<empty>"]})
|
||||
|
||||
def test_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_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)))
|
||||
|
||||
|
||||
@skipIfDBFeature("supports_json_field")
|
||||
class JSONArrayAggNotSupportedTests(TestCase):
|
||||
def test_not_supported(self):
|
||||
msg = "JSONFields are not supported on this database backend."
|
||||
with self.assertRaisesMessage(NotSupportedError, msg):
|
||||
Book.objects.aggregate(jsonarrayagg=JSONArrayAgg("contact__name"))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue