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.
This commit is contained in:
Piotr Pawlaczek 2014-11-15 14:45:00 +01:00 committed by Tim Graham
parent 478d6a9503
commit 41fc1c0b5e
3 changed files with 52 additions and 4 deletions

View File

@ -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 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. 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 from collections import Mapping, OrderedDict
import copy import copy
@ -815,13 +817,37 @@ class Query(object):
conflict. Even tables that previously had no alias will get an alias conflict. Even tables that previously had no alias will get an alias
after this call. 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: if self.alias_prefix != outer_query.alias_prefix:
# No clashes between self and outer query should be possible. # No clashes between self and outer query should be possible.
return return
self.alias_prefix = chr(ord(self.alias_prefix) + 1)
while self.alias_prefix in self.subq_aliases: local_recursion_limit = 127 # explicitly avoid infinite loop
self.alias_prefix = chr(ord(self.alias_prefix) + 1) for pos, prefix in enumerate(prefix_gen()):
assert self.alias_prefix < 'Z' 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]) self.subq_aliases = self.subq_aliases.union([self.alias_prefix])
outer_query.subq_aliases = outer_query.subq_aliases.union(self.subq_aliases) outer_query.subq_aliases = outer_query.subq_aliases.union(self.subq_aliases)
change_map = OrderedDict() change_map = OrderedDict()

View File

@ -183,3 +183,6 @@ Bugfixes
convention in the template engine (:ticket:`23831`). convention in the template engine (:ticket:`23831`).
* Prevented extraneous ``DROP DEFAULT`` SQL in migrations (:ticket:`23581`). * Prevented extraneous ``DROP DEFAULT`` SQL in migrations (:ticket:`23581`).
* Restored the ability to use more than five levels of subqueries
(:ticket:`23758`).

View File

@ -383,6 +383,25 @@ class Queries1Tests(BaseQuerysetTest):
['<Item: four>'] ['<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): def test_heterogeneous_qs_combination(self):
# Combining querysets built on different models should behave in a well-defined # Combining querysets built on different models should behave in a well-defined
# fashion. We raise an error. # fashion. We raise an error.