Explorar o código

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 <charette.s@gmail.com>
Devin Cox hai 7 meses
pai
achega
e03083917d

+ 2 - 0
django/db/models/expressions.py

@@ -182,6 +182,8 @@ class BaseExpression:
     allowed_default = False
     # Can the expression be used during a constraint validation?
     constraint_validation_compatible = True
+    # Does the expression possibly return more than one row?
+    set_returning = False
 
     def __init__(self, output_field=None):
         if output_field is not None:

+ 9 - 0
django/db/models/sql/query.py

@@ -491,6 +491,11 @@ class Query(BaseExpression):
             )
             or having
         )
+        set_returning_annotations = {
+            alias
+            for alias, annotation in self.annotation_select.items()
+            if getattr(annotation, "set_returning", False)
+        }
         # Decide if we need to use a subquery.
         #
         # Existing aggregations would cause incorrect results as
@@ -510,6 +515,7 @@ class Query(BaseExpression):
             or qualify
             or self.distinct
             or self.combinator
+            or set_returning_annotations
         ):
             from django.db.models.sql.subqueries import AggregateQuery
 
@@ -551,6 +557,9 @@ class Query(BaseExpression):
                         if annotation.get_group_by_cols():
                             annotation_mask.add(annotation_alias)
                     inner_query.set_annotation_mask(annotation_mask)
+                    # Annotations that possibly return multiple rows cannot
+                    # be masked as they might have an incidence on the query.
+                    annotation_mask |= set_returning_annotations
 
             # Add aggregates to the outer AggregateQuery. This requires making
             # sure all columns referenced by the aggregates are selected in the

+ 10 - 0
docs/ref/models/expressions.txt

@@ -1095,6 +1095,16 @@ calling the appropriate methods on the wrapped expression.
         :py:data:`NotImplemented` which forces the expression to be computed on
         the database.
 
+    .. attribute:: set_returning
+
+    .. versionadded:: 5.2
+
+        Tells Django that this expression contains a set-returning function,
+        enforcing subquery evaluation. It's used, for example, to allow some
+        Postgres set-returning functions (e.g. ``JSONB_PATH_QUERY``,
+        ``UNNEST``, etc.) to skip optimization and be properly evaluated when
+        annotations spawn rows themselves. Defaults to ``False``.
+
     .. method:: resolve_expression(query=None, allow_joins=True, reuse=None, summarize=False, for_save=False)
 
         Provides the chance to do any preprocessing or validation of

+ 4 - 0
docs/releases/5.2.txt

@@ -218,6 +218,10 @@ Models
 * Added support for validation of model constraints which use a
   :class:`~django.db.models.GeneratedField`.
 
+* The new :attr:`.Expression.set_returning` attribute specifies that the
+  expression contains a set-returning function, enforcing subquery evaluation.
+  This is necessary for many Postgres set-returning functions.
+
 Requests and Responses
 ~~~~~~~~~~~~~~~~~~~~~~
 

+ 8 - 0
tests/annotations/models.py

@@ -58,3 +58,11 @@ class Company(models.Model):
 class Ticket(models.Model):
     active_at = models.DateTimeField()
     duration = models.DurationField()
+
+
+class JsonModel(models.Model):
+    data = models.JSONField(default=dict, blank=True)
+    id = models.IntegerField(primary_key=True)
+
+    class Meta:
+        required_db_features = {"supports_json_field"}

+ 21 - 0
tests/annotations/tests.py

@@ -1,7 +1,9 @@
 import datetime
 from decimal import Decimal
+from unittest import skipUnless
 
 from django.core.exceptions import FieldDoesNotExist, FieldError
+from django.db import connection
 from django.db.models import (
     BooleanField,
     Case,
@@ -15,6 +17,7 @@ from django.db.models import (
     FloatField,
     Func,
     IntegerField,
+    JSONField,
     Max,
     OuterRef,
     Q,
@@ -43,6 +46,7 @@ from .models import (
     Company,
     DepartmentStore,
     Employee,
+    JsonModel,
     Publisher,
     Store,
     Ticket,
@@ -1167,6 +1171,23 @@ class NonAggregateAnnotationTestCase(TestCase):
                 with self.assertRaisesMessage(ValueError, msg):
                     Book.objects.annotate(**{crafted_alias: Value(1)})
 
+    @skipUnless(connection.vendor == "postgresql", "PostgreSQL tests")
+    @skipUnlessDBFeature("supports_json_field")
+    def test_set_returning_functions(self):
+        class JSONBPathQuery(Func):
+            function = "jsonb_path_query"
+            output_field = JSONField()
+            set_returning = True
+
+        test_model = JsonModel.objects.create(
+            data={"key": [{"id": 1, "name": "test1"}, {"id": 2, "name": "test2"}]}, id=1
+        )
+        qs = JsonModel.objects.annotate(
+            table_element=JSONBPathQuery("data", Value("$.key[*]"))
+        ).filter(pk=test_model.pk)
+
+        self.assertEqual(qs.count(), len(qs))
+
 
 class AliasTests(TestCase):
     @classmethod