From 69fa2e8eb25d9bdd395fa8857177d4637aaf4c55 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Tue, 6 Sep 2022 05:54:35 +0200 Subject: [PATCH] Refs #26780 -- Made prefetch_related() don't use window expressions fo sliced queries if not supported. --- .../db/models/fields/related_descriptors.py | 19 +++++++++++---- tests/prefetch_related/tests.py | 23 +++++++++++++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/django/db/models/fields/related_descriptors.py b/django/db/models/fields/related_descriptors.py index e61ee077f0..beb15f9cad 100644 --- a/django/db/models/fields/related_descriptors.py +++ b/django/db/models/fields/related_descriptors.py @@ -64,7 +64,13 @@ and two directions (forward and reverse) for a total of six combinations. """ from django.core.exceptions import FieldError -from django.db import DEFAULT_DB_ALIAS, connections, router, transaction +from django.db import ( + DEFAULT_DB_ALIAS, + NotSupportedError, + connections, + router, + transaction, +) from django.db.models import Q, Window, signals from django.db.models.functions import RowNumber from django.db.models.lookups import GreaterThan, LessThanOrEqual @@ -85,13 +91,16 @@ class ForeignKeyDeferredAttribute(DeferredAttribute): def _filter_prefetch_queryset(queryset, field_name, instances): predicate = Q(**{f"{field_name}__in": instances}) + db = queryset._db or DEFAULT_DB_ALIAS if queryset.query.is_sliced: + if not connections[db].features.supports_over_clause: + raise NotSupportedError( + "Prefetching from a limited queryset is only supported on backends " + "that support window functions." + ) low_mark, high_mark = queryset.query.low_mark, queryset.query.high_mark order_by = [ - expr - for expr, _ in queryset.query.get_compiler( - using=queryset._db or DEFAULT_DB_ALIAS - ).get_order_by() + expr for expr, _ in queryset.query.get_compiler(using=db).get_order_by() ] window = Window(RowNumber(), partition_by=field_name, order_by=order_by) predicate &= GreaterThan(window, low_mark) diff --git a/tests/prefetch_related/tests.py b/tests/prefetch_related/tests.py index 0ac0586476..4c92ccc9ca 100644 --- a/tests/prefetch_related/tests.py +++ b/tests/prefetch_related/tests.py @@ -2,11 +2,16 @@ from unittest import mock from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist -from django.db import connection +from django.db import NotSupportedError, connection from django.db.models import Prefetch, QuerySet, prefetch_related_objects from django.db.models.query import get_prefetcher from django.db.models.sql import Query -from django.test import TestCase, override_settings +from django.test import ( + TestCase, + override_settings, + skipIfDBFeature, + skipUnlessDBFeature, +) from django.test.utils import CaptureQueriesContext, ignore_warnings from django.utils.deprecation import RemovedInDjango50Warning @@ -1911,6 +1916,7 @@ class NestedPrefetchTests(TestCase): class PrefetchLimitTests(TestDataMixin, TestCase): + @skipUnlessDBFeature("supports_over_clause") def test_m2m_forward(self): authors = Author.objects.all() # Meta.ordering with self.assertNumQueries(3): @@ -1924,6 +1930,7 @@ class PrefetchLimitTests(TestDataMixin, TestCase): with self.subTest(book=book): self.assertEqual(book.authors_sliced, list(book.authors.all())[1:]) + @skipUnlessDBFeature("supports_over_clause") def test_m2m_reverse(self): books = Book.objects.order_by("title") with self.assertNumQueries(3): @@ -1937,6 +1944,7 @@ class PrefetchLimitTests(TestDataMixin, TestCase): with self.subTest(author=author): self.assertEqual(author.books_sliced, list(author.books.all())[1:2]) + @skipUnlessDBFeature("supports_over_clause") def test_foreignkey_reverse(self): authors = Author.objects.order_by("-name") with self.assertNumQueries(3): @@ -1960,6 +1968,7 @@ class PrefetchLimitTests(TestDataMixin, TestCase): list(book.first_time_authors.all())[1:], ) + @skipUnlessDBFeature("supports_over_clause") def test_reverse_ordering(self): authors = Author.objects.reverse() # Reverse Meta.ordering with self.assertNumQueries(3): @@ -1972,3 +1981,13 @@ class PrefetchLimitTests(TestDataMixin, TestCase): for book in books: with self.subTest(book=book): self.assertEqual(book.authors_sliced, list(book.authors.all())[1:]) + + @skipIfDBFeature("supports_over_clause") + def test_window_not_supported(self): + authors = Author.objects.all() + msg = ( + "Prefetching from a limited queryset is only supported on backends that " + "support window functions." + ) + with self.assertRaisesMessage(NotSupportedError, msg): + list(Book.objects.prefetch_related(Prefetch("authors", authors[1:])))