From 69fa2e8eb25d9bdd395fa8857177d4637aaf4c55 Mon Sep 17 00:00:00 2001
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
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:])))