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
|
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()
|
||||||
|
|
|
@ -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`).
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue