Commit 41fc1c0b authored by Piotr Pawlaczek's avatar Piotr Pawlaczek Committed by Tim Graham
Browse files

Fixed #23758 -- Allowed more than 5 levels of subqueries

Refactored bump_prefix() to avoid infinite loop and allow more than
than 5 subquires by extending the alphabet to use multi-letters.
parent 478d6a95
Loading
Loading
Loading
Loading
+30 −4
Original line number Diff line number Diff line
@@ -6,6 +6,8 @@ themselves do not have to (and could be backed by things other than SQL
databases). The abstraction barrier only works one way: this module has to know
all about the internals of models in order to get the information it needs.
"""
from string import ascii_uppercase
from itertools import count, product

from collections import Mapping, OrderedDict
import copy
@@ -815,13 +817,37 @@ class Query(object):
        conflict. Even tables that previously had no alias will get an alias
        after this call.
        """
        def prefix_gen():
            """
            Generates a sequence of characters in alphabetical order:
                -> 'A', 'B', 'C', ...

            When the alphabet is finished, the sequence will continue with the
            Cartesian product:
                -> 'AA', 'AB', 'AC', ...
            """
            alphabet = ascii_uppercase
            prefix = chr(ord(self.alias_prefix) + 1)
            yield prefix
            for n in count(1):
                seq = alphabet[alphabet.index(prefix):] if prefix else alphabet
                for s in product(seq, repeat=n):
                    yield ''.join(s)
                prefix = None

        if self.alias_prefix != outer_query.alias_prefix:
            # No clashes between self and outer query should be possible.
            return
        self.alias_prefix = chr(ord(self.alias_prefix) + 1)
        while self.alias_prefix in self.subq_aliases:
            self.alias_prefix = chr(ord(self.alias_prefix) + 1)
            assert self.alias_prefix < 'Z'

        local_recursion_limit = 127  # explicitly avoid infinite loop
        for pos, prefix in enumerate(prefix_gen()):
            if prefix not in self.subq_aliases:
                self.alias_prefix = prefix
                break
            if pos > local_recursion_limit:
                raise RuntimeError(
                    'Maximum recursion depth exceeded: too many subqueries.'
                )
        self.subq_aliases = self.subq_aliases.union([self.alias_prefix])
        outer_query.subq_aliases = outer_query.subq_aliases.union(self.subq_aliases)
        change_map = OrderedDict()
+3 −0
Original line number Diff line number Diff line
@@ -183,3 +183,6 @@ Bugfixes
  convention in the template engine (:ticket:`23831`).

* Prevented extraneous ``DROP DEFAULT`` SQL in migrations (:ticket:`23581`).

* Restored the ability to use more than five levels of subqueries
  (:ticket:`23758`).
+19 −0
Original line number Diff line number Diff line
@@ -383,6 +383,25 @@ class Queries1Tests(BaseQuerysetTest):
            ['<Item: four>']
        )

    def test_avoid_infinite_loop_on_too_many_subqueries(self):
        x = Tag.objects.filter(pk=1)
        local_recursion_limit = 127
        msg = 'Maximum recursion depth exceeded: too many subqueries.'
        with self.assertRaisesMessage(RuntimeError, msg):
            for i in six.moves.range(local_recursion_limit * 2):
                x = Tag.objects.filter(pk__in=x)

    def test_reasonable_number_of_subq_aliases(self):
        x = Tag.objects.filter(pk=1)
        for _ in xrange(20):
            x = Tag.objects.filter(pk__in=x)
        self.assertEqual(
            x.query.subq_aliases, {
                'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA', 'AB', 'AC', 'AD',
                'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'AM', 'AN',
            }
        )

    def test_heterogeneous_qs_combination(self):
        # Combining querysets built on different models should behave in a well-defined
        # fashion. We raise an error.