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:
parent
478d6a9503
commit
41fc1c0b5e
|
@ -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()
|
||||
|
|
|
@ -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`).
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue