Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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

  • Loading branch information...
commit bec0b2a8c637ba2dc3745ad0c1e68ccc6c2f78bd 1 parent 7548aa8
Anssi Kääriäinen authored November 02, 2013
15  django/db/models/sql/datastructures.py
@@ -4,6 +4,21 @@
4 4
 """
5 5
 
6 6
 
  7
+class Col(object):
  8
+    def __init__(self, alias, col):
  9
+        self.alias = alias
  10
+        self.col = col
  11
+
  12
+    def as_sql(self, qn, connection):
  13
+        return '%s.%s' % (qn(self.alias), self.col), []
  14
+
  15
+    def prepare(self):
  16
+        return self
  17
+
  18
+    def relabeled_clone(self, relabels):
  19
+        return self.__class__(relabels.get(self.alias, self.alias), self.col)
  20
+
  21
+
7 22
 class EmptyResultSet(Exception):
8 23
     pass
9 24
 
17  django/db/models/sql/query.py
@@ -22,7 +22,7 @@
22 22
 from django.db.models.sql import aggregates as base_aggregates_module
23 23
 from django.db.models.sql.constants import (QUERY_TERMS, ORDER_DIR, SINGLE,
24 24
         ORDER_PATTERN, JoinInfo, SelectInfo)
25  
-from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin
  25
+from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin, Col
26 26
 from django.db.models.sql.expressions import SQLEvaluator
27 27
 from django.db.models.sql.where import (WhereNode, Constraint, EverythingNode,
28 28
     ExtraWhere, AND, OR, EmptyWhere)
@@ -820,6 +820,9 @@ def bump_prefix(self, outer_query):
820 820
         conflict. Even tables that previously had no alias will get an alias
821 821
         after this call.
822 822
         """
  823
+        if self.alias_prefix != outer_query.alias_prefix:
  824
+            # No clashes between self and outer query should be possible.
  825
+            return
823 826
         self.alias_prefix = chr(ord(self.alias_prefix) + 1)
824 827
         while self.alias_prefix in self.subq_aliases:
825 828
             self.alias_prefix = chr(ord(self.alias_prefix) + 1)
@@ -1514,9 +1517,19 @@ def split_exclude(self, filter_expr, prefix, can_reuse, names_with_path):
1514 1517
         # since we are adding a IN <subquery> clause. This prevents the
1515 1518
         # database from tripping over IN (...,NULL,...) selects and returning
1516 1519
         # nothing
  1520
+        alias, col = query.select[0].col
1517 1521
         if self.is_nullable(query.select[0].field):
1518  
-            alias, col = query.select[0].col
1519 1522
             query.where.add((Constraint(alias, col, query.select[0].field), 'isnull', False), AND)
  1523
+        if alias in can_reuse:
  1524
+            pk = query.select[0].field.model._meta.pk
  1525
+            # Need to add a restriction so that outer query's filters are in effect for
  1526
+            # the subquery, too.
  1527
+            query.bump_prefix(self)
  1528
+            query.where.add(
  1529
+                (Constraint(query.select[0].col[0], pk.column, pk),
  1530
+                 'exact', Col(alias, pk.column)),
  1531
+                AND
  1532
+            )
1520 1533
 
1521 1534
         condition = self.build_filter(
1522 1535
             ('%s__in' % trimmed_prefix, query),
19  tests/queries/models.py
@@ -544,3 +544,22 @@ class Ticket21203Parent(models.Model):
544 544
 class Ticket21203Child(models.Model):
545 545
     childid = models.AutoField(primary_key=True)
546 546
     parent = models.ForeignKey(Ticket21203Parent)
  547
+
  548
+
  549
+class Person(models.Model):
  550
+    name = models.CharField(max_length=128)
  551
+
  552
+
  553
+@python_2_unicode_compatible
  554
+class Company(models.Model):
  555
+    name = models.CharField(max_length=128)
  556
+    employees = models.ManyToManyField(Person, related_name='employers', through='Employment')
  557
+
  558
+    def __str__(self):
  559
+        return self.name
  560
+
  561
+
  562
+class Employment(models.Model):
  563
+    employer = models.ForeignKey(Company)
  564
+    employee = models.ForeignKey(Person)
  565
+    title = models.CharField(max_length=128)
39  tests/queries/tests.py
@@ -25,7 +25,8 @@
25 25
     ModelA, ModelB, ModelC, ModelD, Responsibility, Job, JobResponsibilities,
26 26
     BaseA, FK1, Identifier, Program, Channel, Page, Paragraph, Chapter, Book,
27 27
     MyObject, Order, OrderItem, SharedConnection, Task, Staff, StaffUser,
28  
-    CategoryRelationship, Ticket21203Parent, Ticket21203Child)
  28
+    CategoryRelationship, Ticket21203Parent, Ticket21203Child, Person,
  29
+    Company, Employment)
29 30
 
30 31
 class BaseQuerysetTest(TestCase):
31 32
     def assertValueQuerysetEqual(self, qs, values):
@@ -2352,7 +2353,7 @@ def test_no_extra_params(self):
2352 2353
         except TypeError:
2353 2354
             self.fail("Creation of an instance of a model with only the PK field shouldn't error out after bulk insert refactoring (#17056)")
2354 2355
 
2355  
-class ExcludeTest(TestCase):
  2356
+class ExcludeTests(TestCase):
2356 2357
     def setUp(self):
2357 2358
         f1 = Food.objects.create(name='apples')
2358 2359
         Food.objects.create(name='oranges')
@@ -2375,6 +2376,37 @@ def test_to_field(self):
2375 2376
             Responsibility.objects.exclude(jobs__name='Manager'),
2376 2377
             ['<Responsibility: Programming>'])
2377 2378
 
  2379
+    def test_ticket14511(self):
  2380
+        alex = Person.objects.get_or_create(name='Alex')[0]
  2381
+        jane = Person.objects.get_or_create(name='Jane')[0]
  2382
+
  2383
+        oracle = Company.objects.get_or_create(name='Oracle')[0]
  2384
+        google = Company.objects.get_or_create(name='Google')[0]
  2385
+        microsoft = Company.objects.get_or_create(name='Microsoft')[0]
  2386
+        intel = Company.objects.get_or_create(name='Intel')[0]
  2387
+
  2388
+        def employ(employer, employee, title):
  2389
+            Employment.objects.get_or_create(employee=employee, employer=employer, title=title)
  2390
+
  2391
+        employ(oracle, alex, 'Engineer')
  2392
+        employ(oracle, alex, 'Developer')
  2393
+        employ(google, alex, 'Engineer')
  2394
+        employ(google, alex, 'Manager')
  2395
+        employ(microsoft, alex, 'Manager')
  2396
+        employ(intel, alex, 'Manager')
  2397
+
  2398
+        employ(microsoft, jane, 'Developer')
  2399
+        employ(intel, jane, 'Manager')
  2400
+
  2401
+        alex_tech_employers = alex.employers.filter(
  2402
+            employment__title__in=('Engineer', 'Developer')).distinct().order_by('name')
  2403
+        self.assertQuerysetEqual(alex_tech_employers, [google, oracle], lambda x: x)
  2404
+
  2405
+        alex_nontech_employers = alex.employers.exclude(
  2406
+            employment__title__in=('Engineer', 'Developer')).distinct().order_by('name')
  2407
+        self.assertQuerysetEqual(alex_nontech_employers, [google, intel, microsoft], lambda x: x)
  2408
+
  2409
+
2378 2410
 class ExcludeTest17600(TestCase):
2379 2411
     """
2380 2412
     Some regressiontests for ticket #17600. Some of these likely duplicate
@@ -3135,3 +3167,6 @@ def test_values_no_promotion_for_existing(self):
3135 3167
     def test_non_nullable_fk_not_promoted(self):
3136 3168
         qs = ObjectB.objects.values('objecta__name')
3137 3169
         self.assertTrue(' INNER JOIN ' in str(qs.query))
  3170
+
  3171
+
  3172
+class ExcludeJoinTest(TestCase):

0 notes on commit bec0b2a

Please sign in to comment.
Something went wrong with that request. Please try again.