mirror of https://github.com/django/django.git
Fixed #33319 -- Fixed crash when combining with the | operator querysets with aliases that conflict.
This commit is contained in:
parent
f04b44bad4
commit
81739a45b5
1
AUTHORS
1
AUTHORS
|
@ -731,6 +731,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
Oscar Ramirez <tuxskar@gmail.com>
|
Oscar Ramirez <tuxskar@gmail.com>
|
||||||
Ossama M. Khayat <okhayat@yahoo.com>
|
Ossama M. Khayat <okhayat@yahoo.com>
|
||||||
Owen Griffiths
|
Owen Griffiths
|
||||||
|
Ömer Faruk Abacı <https://github.com/omerfarukabaci/>
|
||||||
Pablo Martín <goinnn@gmail.com>
|
Pablo Martín <goinnn@gmail.com>
|
||||||
Panos Laganakos <panos.laganakos@gmail.com>
|
Panos Laganakos <panos.laganakos@gmail.com>
|
||||||
Paolo Melchiorre <paolo@melchiorre.org>
|
Paolo Melchiorre <paolo@melchiorre.org>
|
||||||
|
|
|
@ -572,6 +572,15 @@ class Query(BaseExpression):
|
||||||
if self.distinct_fields != rhs.distinct_fields:
|
if self.distinct_fields != rhs.distinct_fields:
|
||||||
raise TypeError('Cannot combine queries with different distinct fields.')
|
raise TypeError('Cannot combine queries with different distinct fields.')
|
||||||
|
|
||||||
|
# If lhs and rhs shares the same alias prefix, it is possible to have
|
||||||
|
# conflicting alias changes like T4 -> T5, T5 -> T6, which might end up
|
||||||
|
# as T4 -> T6 while combining two querysets. To prevent this, change an
|
||||||
|
# alias prefix of the rhs and update current aliases accordingly,
|
||||||
|
# except if the alias is the base table since it must be present in the
|
||||||
|
# query on both sides.
|
||||||
|
initial_alias = self.get_initial_alias()
|
||||||
|
rhs.bump_prefix(self, exclude={initial_alias})
|
||||||
|
|
||||||
# Work out how to relabel the rhs aliases, if necessary.
|
# Work out how to relabel the rhs aliases, if necessary.
|
||||||
change_map = {}
|
change_map = {}
|
||||||
conjunction = (connector == AND)
|
conjunction = (connector == AND)
|
||||||
|
@ -589,9 +598,6 @@ class Query(BaseExpression):
|
||||||
# the AND case. The results will be correct but this creates too many
|
# the AND case. The results will be correct but this creates too many
|
||||||
# joins. This is something that could be fixed later on.
|
# joins. This is something that could be fixed later on.
|
||||||
reuse = set() if conjunction else set(self.alias_map)
|
reuse = set() if conjunction else set(self.alias_map)
|
||||||
# Base table must be present in the query - this is the same
|
|
||||||
# table on both sides.
|
|
||||||
self.get_initial_alias()
|
|
||||||
joinpromoter = JoinPromoter(connector, 2, False)
|
joinpromoter = JoinPromoter(connector, 2, False)
|
||||||
joinpromoter.add_votes(
|
joinpromoter.add_votes(
|
||||||
j for j in self.alias_map if self.alias_map[j].join_type == INNER)
|
j for j in self.alias_map if self.alias_map[j].join_type == INNER)
|
||||||
|
@ -882,12 +888,12 @@ class Query(BaseExpression):
|
||||||
for alias, aliased in self.external_aliases.items()
|
for alias, aliased in self.external_aliases.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
def bump_prefix(self, outer_query):
|
def bump_prefix(self, other_query, exclude=None):
|
||||||
"""
|
"""
|
||||||
Change the alias prefix to the next letter in the alphabet in a way
|
Change the alias prefix to the next letter in the alphabet in a way
|
||||||
that the outer query's aliases and this query's aliases will not
|
that the other query's aliases and this query's aliases will not
|
||||||
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. To prevent changing aliases use the exclude parameter.
|
||||||
"""
|
"""
|
||||||
def prefix_gen():
|
def prefix_gen():
|
||||||
"""
|
"""
|
||||||
|
@ -907,7 +913,7 @@ class Query(BaseExpression):
|
||||||
yield ''.join(s)
|
yield ''.join(s)
|
||||||
prefix = None
|
prefix = None
|
||||||
|
|
||||||
if self.alias_prefix != outer_query.alias_prefix:
|
if self.alias_prefix != other_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
|
||||||
|
|
||||||
|
@ -925,10 +931,13 @@ class Query(BaseExpression):
|
||||||
'Maximum recursion depth exceeded: too many subqueries.'
|
'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)
|
other_query.subq_aliases = other_query.subq_aliases.union(self.subq_aliases)
|
||||||
|
if exclude is None:
|
||||||
|
exclude = {}
|
||||||
self.change_aliases({
|
self.change_aliases({
|
||||||
alias: '%s%d' % (self.alias_prefix, pos)
|
alias: '%s%d' % (self.alias_prefix, pos)
|
||||||
for pos, alias in enumerate(self.alias_map)
|
for pos, alias in enumerate(self.alias_map)
|
||||||
|
if alias not in exclude
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_initial_alias(self):
|
def get_initial_alias(self):
|
||||||
|
|
|
@ -613,13 +613,14 @@ class OrderItem(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class BaseUser(models.Model):
|
class BaseUser(models.Model):
|
||||||
pass
|
annotation = models.ForeignKey(Annotation, models.CASCADE, null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
class Task(models.Model):
|
class Task(models.Model):
|
||||||
title = models.CharField(max_length=10)
|
title = models.CharField(max_length=10)
|
||||||
owner = models.ForeignKey(BaseUser, models.CASCADE, related_name='owner')
|
owner = models.ForeignKey(BaseUser, models.CASCADE, related_name='owner')
|
||||||
creator = models.ForeignKey(BaseUser, models.CASCADE, related_name='creator')
|
creator = models.ForeignKey(BaseUser, models.CASCADE, related_name='creator')
|
||||||
|
note = models.ForeignKey(Note, on_delete=models.CASCADE, null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
|
@ -15,7 +15,7 @@ from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
|
||||||
from django.test.utils import CaptureQueriesContext
|
from django.test.utils import CaptureQueriesContext
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
FK1, Annotation, Article, Author, BaseA, Book, CategoryItem,
|
FK1, Annotation, Article, Author, BaseA, BaseUser, Book, CategoryItem,
|
||||||
CategoryRelationship, Celebrity, Channel, Chapter, Child, ChildObjectA,
|
CategoryRelationship, Celebrity, Channel, Chapter, Child, ChildObjectA,
|
||||||
Classroom, CommonMixedCaseForeignKeys, Company, Cover, CustomPk,
|
Classroom, CommonMixedCaseForeignKeys, Company, Cover, CustomPk,
|
||||||
CustomPkTag, DateTimePK, Detail, DumbCategory, Eaten, Employment,
|
CustomPkTag, DateTimePK, Detail, DumbCategory, Eaten, Employment,
|
||||||
|
@ -2094,6 +2094,15 @@ class QuerySetBitwiseOperationTests(TestCase):
|
||||||
cls.room_2 = Classroom.objects.create(school=cls.school, has_blackboard=True, name='Room 2')
|
cls.room_2 = Classroom.objects.create(school=cls.school, has_blackboard=True, name='Room 2')
|
||||||
cls.room_3 = Classroom.objects.create(school=cls.school, has_blackboard=True, name='Room 3')
|
cls.room_3 = Classroom.objects.create(school=cls.school, has_blackboard=True, name='Room 3')
|
||||||
cls.room_4 = Classroom.objects.create(school=cls.school, has_blackboard=False, name='Room 4')
|
cls.room_4 = Classroom.objects.create(school=cls.school, has_blackboard=False, name='Room 4')
|
||||||
|
tag = Tag.objects.create()
|
||||||
|
cls.annotation_1 = Annotation.objects.create(tag=tag)
|
||||||
|
annotation_2 = Annotation.objects.create(tag=tag)
|
||||||
|
note = cls.annotation_1.notes.create(tag=tag)
|
||||||
|
cls.base_user_1 = BaseUser.objects.create(annotation=cls.annotation_1)
|
||||||
|
cls.base_user_2 = BaseUser.objects.create(annotation=annotation_2)
|
||||||
|
cls.task = Task.objects.create(
|
||||||
|
owner=cls.base_user_2, creator=cls.base_user_2, note=note,
|
||||||
|
)
|
||||||
|
|
||||||
@skipUnlessDBFeature('allow_sliced_subqueries_with_in')
|
@skipUnlessDBFeature('allow_sliced_subqueries_with_in')
|
||||||
def test_or_with_rhs_slice(self):
|
def test_or_with_rhs_slice(self):
|
||||||
|
@ -2130,6 +2139,17 @@ class QuerySetBitwiseOperationTests(TestCase):
|
||||||
nested_combined = School.objects.filter(pk__in=combined.values('pk'))
|
nested_combined = School.objects.filter(pk__in=combined.values('pk'))
|
||||||
self.assertSequenceEqual(nested_combined, [self.school])
|
self.assertSequenceEqual(nested_combined, [self.school])
|
||||||
|
|
||||||
|
def test_conflicting_aliases_during_combine(self):
|
||||||
|
qs1 = self.annotation_1.baseuser_set.all()
|
||||||
|
qs2 = BaseUser.objects.filter(
|
||||||
|
Q(owner__note__in=self.annotation_1.notes.all()) |
|
||||||
|
Q(creator__note__in=self.annotation_1.notes.all())
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(qs1, [self.base_user_1])
|
||||||
|
self.assertSequenceEqual(qs2, [self.base_user_2])
|
||||||
|
self.assertCountEqual(qs2 | qs1, qs1 | qs2)
|
||||||
|
self.assertCountEqual(qs2 | qs1, [self.base_user_1, self.base_user_2])
|
||||||
|
|
||||||
|
|
||||||
class CloneTests(TestCase):
|
class CloneTests(TestCase):
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue