Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e030839

Browse files
devin13coxcharettes
authored andcommitted
Fixed #35586 -- Added support for set-returning database functions.
Aggregation optimization didn't account for not referenced set-returning annotations on Postgres. Co-authored-by: Simon Charette <[email protected]>
1 parent 2281286 commit e030839

6 files changed

Lines changed: 54 additions & 0 deletions

File tree

django/db/models/expressions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ class BaseExpression:
182182
allowed_default = False
183183
# Can the expression be used during a constraint validation?
184184
constraint_validation_compatible = True
185+
# Does the expression possibly return more than one row?
186+
set_returning = False
185187

186188
def __init__(self, output_field=None):
187189
if output_field is not None:

django/db/models/sql/query.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,11 @@ def get_aggregation(self, using, aggregate_exprs):
491491
)
492492
or having
493493
)
494+
set_returning_annotations = {
495+
alias
496+
for alias, annotation in self.annotation_select.items()
497+
if getattr(annotation, "set_returning", False)
498+
}
494499
# Decide if we need to use a subquery.
495500
#
496501
# Existing aggregations would cause incorrect results as
@@ -510,6 +515,7 @@ def get_aggregation(self, using, aggregate_exprs):
510515
or qualify
511516
or self.distinct
512517
or self.combinator
518+
or set_returning_annotations
513519
):
514520
from django.db.models.sql.subqueries import AggregateQuery
515521

@@ -551,6 +557,9 @@ def get_aggregation(self, using, aggregate_exprs):
551557
if annotation.get_group_by_cols():
552558
annotation_mask.add(annotation_alias)
553559
inner_query.set_annotation_mask(annotation_mask)
560+
# Annotations that possibly return multiple rows cannot
561+
# be masked as they might have an incidence on the query.
562+
annotation_mask |= set_returning_annotations
554563

555564
# Add aggregates to the outer AggregateQuery. This requires making
556565
# sure all columns referenced by the aggregates are selected in the

docs/ref/models/expressions.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,16 @@ calling the appropriate methods on the wrapped expression.
10951095
:py:data:`NotImplemented` which forces the expression to be computed on
10961096
the database.
10971097

1098+
.. attribute:: set_returning
1099+
1100+
.. versionadded:: 5.2
1101+
1102+
Tells Django that this expression contains a set-returning function,
1103+
enforcing subquery evaluation. It's used, for example, to allow some
1104+
Postgres set-returning functions (e.g. ``JSONB_PATH_QUERY``,
1105+
``UNNEST``, etc.) to skip optimization and be properly evaluated when
1106+
annotations spawn rows themselves. Defaults to ``False``.
1107+
10981108
.. method:: resolve_expression(query=None, allow_joins=True, reuse=None, summarize=False, for_save=False)
10991109

11001110
Provides the chance to do any preprocessing or validation of

docs/releases/5.2.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ Models
218218
* Added support for validation of model constraints which use a
219219
:class:`~django.db.models.GeneratedField`.
220220

221+
* The new :attr:`.Expression.set_returning` attribute specifies that the
222+
expression contains a set-returning function, enforcing subquery evaluation.
223+
This is necessary for many Postgres set-returning functions.
224+
221225
Requests and Responses
222226
~~~~~~~~~~~~~~~~~~~~~~
223227

tests/annotations/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,11 @@ class Company(models.Model):
5858
class Ticket(models.Model):
5959
active_at = models.DateTimeField()
6060
duration = models.DurationField()
61+
62+
63+
class JsonModel(models.Model):
64+
data = models.JSONField(default=dict, blank=True)
65+
id = models.IntegerField(primary_key=True)
66+
67+
class Meta:
68+
required_db_features = {"supports_json_field"}

tests/annotations/tests.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import datetime
22
from decimal import Decimal
3+
from unittest import skipUnless
34

45
from django.core.exceptions import FieldDoesNotExist, FieldError
6+
from django.db import connection
57
from django.db.models import (
68
BooleanField,
79
Case,
@@ -15,6 +17,7 @@
1517
FloatField,
1618
Func,
1719
IntegerField,
20+
JSONField,
1821
Max,
1922
OuterRef,
2023
Q,
@@ -43,6 +46,7 @@
4346
Company,
4447
DepartmentStore,
4548
Employee,
49+
JsonModel,
4650
Publisher,
4751
Store,
4852
Ticket,
@@ -1167,6 +1171,23 @@ def test_alias_forbidden_chars(self):
11671171
with self.assertRaisesMessage(ValueError, msg):
11681172
Book.objects.annotate(**{crafted_alias: Value(1)})
11691173

1174+
@skipUnless(connection.vendor == "postgresql", "PostgreSQL tests")
1175+
@skipUnlessDBFeature("supports_json_field")
1176+
def test_set_returning_functions(self):
1177+
class JSONBPathQuery(Func):
1178+
function = "jsonb_path_query"
1179+
output_field = JSONField()
1180+
set_returning = True
1181+
1182+
test_model = JsonModel.objects.create(
1183+
data={"key": [{"id": 1, "name": "test1"}, {"id": 2, "name": "test2"}]}, id=1
1184+
)
1185+
qs = JsonModel.objects.annotate(
1186+
table_element=JSONBPathQuery("data", Value("$.key[*]"))
1187+
).filter(pk=test_model.pk)
1188+
1189+
self.assertEqual(qs.count(), len(qs))
1190+
11701191

11711192
class AliasTests(TestCase):
11721193
@classmethod

0 commit comments

Comments
 (0)