Fixed #33319 -- Fixed crash when combining with the | operator querysets with aliases that conflict.

This commit is contained in:
Ömer Faruk Abacı 2021-11-30 16:50:13 +03:00 committed by Mariusz Felisiak
parent f04b44bad4
commit 81739a45b5
4 changed files with 41 additions and 10 deletions

View File

@ -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>

View File

@ -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):

View File

@ -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

View File

@ -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):