From 41fc1c0b5eac156e200a10233c7c9210a1c0fed8 Mon Sep 17 00:00:00 2001 From: Piotr Pawlaczek Date: Sat, 15 Nov 2014 14:45:00 +0100 Subject: [PATCH] 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. --- django/db/models/sql/query.py | 34 ++++++++++++++++++++++++++++++---- docs/releases/1.7.2.txt | 3 +++ tests/queries/tests.py | 19 +++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 44ece3fdd5..d1e6d2b819 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -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() diff --git a/docs/releases/1.7.2.txt b/docs/releases/1.7.2.txt index dcb782fde9..c4938530e4 100644 --- a/docs/releases/1.7.2.txt +++ b/docs/releases/1.7.2.txt @@ -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`). diff --git a/tests/queries/tests.py b/tests/queries/tests.py index 00027c07a7..f2c4abf5f3 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -383,6 +383,25 @@ class Queries1Tests(BaseQuerysetTest): [''] ) + 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.