Commit bec0b2a8 authored by Anssi Kääriäinen's avatar Anssi Kääriäinen
Browse files

Fixed #14511 -- bug in .exclude() query

parent 7548aa8f
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -4,6 +4,21 @@ the SQL domain.
"""


class Col(object):
    def __init__(self, alias, col):
        self.alias = alias
        self.col = col

    def as_sql(self, qn, connection):
        return '%s.%s' % (qn(self.alias), self.col), []

    def prepare(self):
        return self

    def relabeled_clone(self, relabels):
        return self.__class__(relabels.get(self.alias, self.alias), self.col)


class EmptyResultSet(Exception):
    pass

+15 −2
Original line number Diff line number Diff line
@@ -22,7 +22,7 @@ from django.db.models.related import PathInfo
from django.db.models.sql import aggregates as base_aggregates_module
from django.db.models.sql.constants import (QUERY_TERMS, ORDER_DIR, SINGLE,
        ORDER_PATTERN, JoinInfo, SelectInfo)
from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin
from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin, Col
from django.db.models.sql.expressions import SQLEvaluator
from django.db.models.sql.where import (WhereNode, Constraint, EverythingNode,
    ExtraWhere, AND, OR, EmptyWhere)
@@ -820,6 +820,9 @@ class Query(object):
        conflict. Even tables that previously had no alias will get an alias
        after this call.
        """
        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)
@@ -1514,9 +1517,19 @@ class Query(object):
        # since we are adding a IN <subquery> clause. This prevents the
        # database from tripping over IN (...,NULL,...) selects and returning
        # nothing
        if self.is_nullable(query.select[0].field):
        alias, col = query.select[0].col
        if self.is_nullable(query.select[0].field):
            query.where.add((Constraint(alias, col, query.select[0].field), 'isnull', False), AND)
        if alias in can_reuse:
            pk = query.select[0].field.model._meta.pk
            # Need to add a restriction so that outer query's filters are in effect for
            # the subquery, too.
            query.bump_prefix(self)
            query.where.add(
                (Constraint(query.select[0].col[0], pk.column, pk),
                 'exact', Col(alias, pk.column)),
                AND
            )

        condition = self.build_filter(
            ('%s__in' % trimmed_prefix, query),
+19 −0
Original line number Diff line number Diff line
@@ -544,3 +544,22 @@ class Ticket21203Parent(models.Model):
class Ticket21203Child(models.Model):
    childid = models.AutoField(primary_key=True)
    parent = models.ForeignKey(Ticket21203Parent)


class Person(models.Model):
    name = models.CharField(max_length=128)


@python_2_unicode_compatible
class Company(models.Model):
    name = models.CharField(max_length=128)
    employees = models.ManyToManyField(Person, related_name='employers', through='Employment')

    def __str__(self):
        return self.name


class Employment(models.Model):
    employer = models.ForeignKey(Company)
    employee = models.ForeignKey(Person)
    title = models.CharField(max_length=128)
+37 −2
Original line number Diff line number Diff line
@@ -25,7 +25,8 @@ from .models import (
    ModelA, ModelB, ModelC, ModelD, Responsibility, Job, JobResponsibilities,
    BaseA, FK1, Identifier, Program, Channel, Page, Paragraph, Chapter, Book,
    MyObject, Order, OrderItem, SharedConnection, Task, Staff, StaffUser,
    CategoryRelationship, Ticket21203Parent, Ticket21203Child)
    CategoryRelationship, Ticket21203Parent, Ticket21203Child, Person,
    Company, Employment)

class BaseQuerysetTest(TestCase):
    def assertValueQuerysetEqual(self, qs, values):
@@ -2352,7 +2353,7 @@ class DefaultValuesInsertTest(TestCase):
        except TypeError:
            self.fail("Creation of an instance of a model with only the PK field shouldn't error out after bulk insert refactoring (#17056)")

class ExcludeTest(TestCase):
class ExcludeTests(TestCase):
    def setUp(self):
        f1 = Food.objects.create(name='apples')
        Food.objects.create(name='oranges')
@@ -2375,6 +2376,37 @@ class ExcludeTest(TestCase):
            Responsibility.objects.exclude(jobs__name='Manager'),
            ['<Responsibility: Programming>'])

    def test_ticket14511(self):
        alex = Person.objects.get_or_create(name='Alex')[0]
        jane = Person.objects.get_or_create(name='Jane')[0]

        oracle = Company.objects.get_or_create(name='Oracle')[0]
        google = Company.objects.get_or_create(name='Google')[0]
        microsoft = Company.objects.get_or_create(name='Microsoft')[0]
        intel = Company.objects.get_or_create(name='Intel')[0]

        def employ(employer, employee, title):
            Employment.objects.get_or_create(employee=employee, employer=employer, title=title)

        employ(oracle, alex, 'Engineer')
        employ(oracle, alex, 'Developer')
        employ(google, alex, 'Engineer')
        employ(google, alex, 'Manager')
        employ(microsoft, alex, 'Manager')
        employ(intel, alex, 'Manager')

        employ(microsoft, jane, 'Developer')
        employ(intel, jane, 'Manager')

        alex_tech_employers = alex.employers.filter(
            employment__title__in=('Engineer', 'Developer')).distinct().order_by('name')
        self.assertQuerysetEqual(alex_tech_employers, [google, oracle], lambda x: x)

        alex_nontech_employers = alex.employers.exclude(
            employment__title__in=('Engineer', 'Developer')).distinct().order_by('name')
        self.assertQuerysetEqual(alex_nontech_employers, [google, intel, microsoft], lambda x: x)


class ExcludeTest17600(TestCase):
    """
    Some regressiontests for ticket #17600. Some of these likely duplicate
@@ -3135,3 +3167,6 @@ class ValuesJoinPromotionTests(TestCase):
    def test_non_nullable_fk_not_promoted(self):
        qs = ObjectB.objects.values('objecta__name')
        self.assertTrue(' INNER JOIN ' in str(qs.query))


class ExcludeJoinTest(TestCase):