Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Infinite recursion case reproduced

  • Loading branch information...
commit 87d60b4958dc9c63c1dd970930e8c20d03e5eb36 1 parent d359647
@AndrewPashkin authored
Showing with 152 additions and 3 deletions.
  1. +33 −1 tests/prefetch_related/models.py
  2. +119 −2 tests/prefetch_related/tests.py
View
34 tests/prefetch_related/models.py
@@ -1,3 +1,4 @@
+from django.db.models import Prefetch
from django.contrib.contenttypes.fields import (
GenericForeignKey, GenericRelation
)
@@ -58,7 +59,6 @@ def __str__(self):
class Meta:
ordering = ['id']
-
class BookWithYear(Book):
book = models.OneToOneField(Book, parent_link=True)
published_year = models.IntegerField()
@@ -257,3 +257,35 @@ def __str__(self):
class Meta:
ordering = ['id']
+
+class ThisManager(models.Manager):
+ use_for_related_fields = True
+
+ def get_queryset(self):
+ return (super(ThisManager, self).get_queryset()
+ .prefetch_related('stuff'))
+
+class This(models.Model):
+ name = models.CharField(max_length=10, default='this')
+ stuff = models.ManyToManyField('That', related_name='things')
+ objects = ThisManager()
+
+class ThatManager(models.Manager):
+ use_for_related_fields = True
+
+ def get_queryset(self):
+ return (super(ThatManager, self).get_queryset()
+ .prefetch_related(
+ Prefetch(
+ 'things',
+ This.objects.prefetch_related(
+ 'stuff__things'
+ )
+ ),
+ 'things'
+ )
+ )
+
+class That(models.Model):
+ name = models.CharField(max_length=10, default='that')
+ objects = ThatManager()
View
121 tests/prefetch_related/tests.py
@@ -1,8 +1,9 @@
from __future__ import unicode_literals
+from collections import deque
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.contenttypes.models import ContentType
-from django.db import connection
+from django.db import connection, reset_queries
from django.db.models import Prefetch
from django.db.models.query import get_prefetcher
from django.test import TestCase, override_settings
@@ -12,7 +13,7 @@
from .models import (Author, Bio, Book, Reader, Qualification, Teacher, Department,
TaggedItem, Bookmark, AuthorAddress, FavoriteAuthors, AuthorWithAge,
BookWithYear, BookReview, Person, House, Room, Employee, Comment,
- LessonEntry, WordEntry, Author2)
+ LessonEntry, WordEntry, Author2, This, That)
class PrefetchRelatedTests(TestCase):
@@ -1159,3 +1160,119 @@ def test_bug(self):
prefetcher = get_prefetcher(self.rooms[0], 'house')[0]
queryset = prefetcher.get_prefetch_queryset(list(Room.objects.all()))[0]
self.assertNotIn(' JOIN ', force_text(queryset.query))
+
+
+class RecursionProtectionTests(TestCase):
+ def setUp(self):
+ self.book1 = Book.objects.create()
+ self.book2 = Book.objects.create()
+ self.author1 = Author.objects.create(first_book=self.book1, name='Author #1')
+ self.author2 = Author.objects.create(first_book=self.book1, name='Author #2')
+ self.author1.books.add(self.book1)
+ self.author1.save()
+ self.author2.books.add(self.book2)
+ self.author2.save()
+ self.this = This.objects.create()
+ self.that = That.objects.create()
+ self.this.stuff.add(self.that)
+ self.this.save()
+
+ def walk(self, qs):
+ get_authors = lambda b: b.authors.all()
+ get_books = lambda a: a.books.all()
+
+ fetched_books = []
+ for e in qs:
+ for b in get_books(e):
+ for a in get_authors(b):
+ for fetched_book in get_books(a):
+ fetched_books.append(fetched_book)
+ return fetched_books
+
+ @override_settings(DEBUG=True)
+ def test_descriptors_storing_protection(self):
+ """For two queries, that are ueqal, except second has a duplicated
+ lookup - number of SQL queries must be equal.
+
+ What happens here:
+ ------------------
+ There is the check in `prefetch_related_objects`:
+ ::
+
+ if not (lookup in auto_lookups and descriptor in followed_descriptors):
+ done_queries[prefetch_to] = obj_list
+ auto_lookups.extend(normalize_prefetch_lookups(additional_lookups, prefetch_to))
+
+ if lookup is not autolookup and descriptor for it not been seen
+ lookup being added to `done_queries`.
+
+ This tests creates situation where:
+ lookup is autolookup and descriptor for it was added while
+ traversing same field but for different lookup.
+ So it never would be added to `done_lookups`.
+ """
+ qs1 = (Author.objects
+ .filter(pk=self.author1.pk)
+ .prefetch_related(
+ Prefetch( # here we add autolookup
+ 'books',
+ Book.objects.prefetch_related(
+ 'authors__books'
+ )
+ ),
+ 'books__authors__books',
+ )
+ )
+ qs2 = (This.objects
+ .filter(pk=self.author1.pk)
+ .prefetch_related(
+ Prefetch(
+ 'stuff',
+ That.objects.prefetch_related(
+ 'authors__books'
+ )
+ ),
+ 'books__authors__books',
+ 'books__authors__books'
+ )
+ )
+
+ def check_num_queries(qs):
+ old_num_queries = len(connection.queries)
+ self.assertEqual(len(self.walk(qs)), 2)
+ return connection.queries[old_num_queries:]
+
+ queries_qs1 = check_num_queries(qs1)
+ queries_qs2 = check_num_queries(qs2)
+ self.assertEqual(len(queries_qs1), len(queries_qs2))
+
+ @override_settings(DEBUG=True)
+ def test_only_unique_queries(self):
+ """Shows the situation where prefetch_related produces duplicate
+ queries. Then more lookup ``'books__authors__books'`` would be
+ doubled, then more equal queries would be produced.
+ """
+ qs = (Author.objects
+ .filter(pk=self.author1.pk)
+ .prefetch_related(
+ Prefetch(
+ 'books',
+ Book.objects.prefetch_related(
+ 'authors__books'
+ )
+ ),
+ # duplicate this to increase
+ # number of queries
+ 'books__authors__books',
+ )
+ )
+ reset_queries() # to clear connection.queries
+ self.walk(qs)
+ queries = [e['sql'] for e in connection.queries]
+ self.assertEqual(len(queries), len(set(queries)))
+
+ def test_real_infinite_recursion(self):
+ """Reproduces situation with infinite recursion."""
+ for this in This.objects.all():
+ for that in this.stuff.all():
+ pass
Please sign in to comment.
Something went wrong with that request. Please try again.