Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

[1.2.X] Fixed #12252 -- Ensure that queryset unions are commutative. …

…Thanks to benreynwar for the report, and draft patch, and to Karen and Ramiro for the review eyeballs and patch updates.

Backport of r15726 from trunk.

git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.2.X@15727 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 113a8c1f1c8de50b3192c3adbefd26b0067b1750 1 parent a973969
Russell Keith-Magee authored March 03, 2011
18  django/db/models/sql/query.py
@@ -446,6 +446,8 @@ def combine(self, rhs, connector):
446 446
             "Cannot combine a unique query with a non-unique query."
447 447
 
448 448
         self.remove_inherited_models()
  449
+        l_tables = set([a for a in self.tables if self.alias_refcount[a]])
  450
+        r_tables = set([a for a in rhs.tables if rhs.alias_refcount[a]])
449 451
         # Work out how to relabel the rhs aliases, if necessary.
450 452
         change_map = {}
451 453
         used = set()
@@ -463,13 +465,19 @@ def combine(self, rhs, connector):
463 465
             first = False
464 466
 
465 467
         # So that we don't exclude valid results in an "or" query combination,
466  
-        # the first join that is exclusive to the lhs (self) must be converted
  468
+        # all joins exclusive to either the lhs or the rhs must be converted
467 469
         # to an outer join.
468 470
         if not conjunction:
469  
-            for alias in self.tables[1:]:
470  
-                if self.alias_refcount[alias] == 1:
471  
-                    self.promote_alias(alias, True)
472  
-                    break
  471
+            # Update r_tables aliases.
  472
+            for alias in change_map:
  473
+                if alias in r_tables:
  474
+                    r_tables.remove(alias)
  475
+                    r_tables.add(change_map[alias])
  476
+            # Find aliases that are exclusive to rhs or lhs.
  477
+            # These are promoted to outer joins.
  478
+            outer_aliases = (l_tables | r_tables) - (l_tables & r_tables)
  479
+            for alias in outer_aliases:
  480
+                self.promote_alias(alias, True)
473 481
 
474 482
         # Now relabel a copy of the rhs where-clause and add it to the current
475 483
         # one.
23  tests/regressiontests/queries/models.py
@@ -294,3 +294,26 @@ class Node(models.Model):
294 294
 
295 295
     def __unicode__(self):
296 296
         return u"%s" % self.num
  297
+
  298
+# Bug #12252
  299
+class ObjectA(models.Model):
  300
+    name = models.CharField(max_length=50)
  301
+
  302
+    def __unicode__(self):
  303
+        return self.name
  304
+
  305
+class ObjectB(models.Model):
  306
+    name = models.CharField(max_length=50)
  307
+    objecta = models.ForeignKey(ObjectA)
  308
+    number = models.PositiveSmallIntegerField()
  309
+
  310
+    def __unicode__(self):
  311
+        return self.name
  312
+
  313
+class ObjectC(models.Model):
  314
+    name = models.CharField(max_length=50)
  315
+    objecta = models.ForeignKey(ObjectA)
  316
+    objectb = models.ForeignKey(ObjectB)
  317
+
  318
+    def __unicode__(self):
  319
+       return self.name
63  tests/regressiontests/queries/tests.py
@@ -14,7 +14,8 @@
14 14
 from models import (Annotation, Article, Author, Celebrity, Child, Cover, Detail,
15 15
     DumbCategory, ExtraInfo, Fan, Item, LeafA, LoopX, LoopZ, ManagedModel,
16 16
     Member, NamedCategory, Note, Number, Plaything, PointerA, Ranking, Related,
17  
-    Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten, Node)
  17
+    Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten, Node, ObjectA, ObjectB,
  18
+    ObjectC)
18 19
 
19 20
 
20 21
 class BaseQuerysetTest(TestCase):
@@ -1566,6 +1567,66 @@ def test_recursive_fk_reverse(self):
1566 1567
             [node1]
1567 1568
         )
1568 1569
 
  1570
+class UnionTests(unittest.TestCase):
  1571
+    """
  1572
+    Tests for the union of two querysets. Bug #12252.
  1573
+    """
  1574
+    def setUp(self):
  1575
+        objectas = []
  1576
+        objectbs = []
  1577
+        objectcs = []
  1578
+        a_info = ['one', 'two', 'three']
  1579
+        for name in a_info:
  1580
+            o = ObjectA(name=name)
  1581
+            o.save()
  1582
+            objectas.append(o)
  1583
+        b_info = [('un', 1, objectas[0]), ('deux', 2, objectas[0]), ('trois', 3, objectas[2])]
  1584
+        for name, number, objecta in b_info:
  1585
+            o = ObjectB(name=name, number=number, objecta=objecta)
  1586
+            o.save()
  1587
+            objectbs.append(o)
  1588
+        c_info = [('ein', objectas[2], objectbs[2]), ('zwei', objectas[1], objectbs[1])]
  1589
+        for name, objecta, objectb in c_info:
  1590
+            o = ObjectC(name=name, objecta=objecta, objectb=objectb)
  1591
+            o.save()
  1592
+            objectcs.append(o)
  1593
+
  1594
+    def check_union(self, model, Q1, Q2):
  1595
+        filter = model.objects.filter
  1596
+        self.assertEqual(set(filter(Q1) | filter(Q2)), set(filter(Q1 | Q2)))
  1597
+        self.assertEqual(set(filter(Q2) | filter(Q1)), set(filter(Q1 | Q2)))
  1598
+
  1599
+    def test_A_AB(self):
  1600
+        Q1 = Q(name='two')
  1601
+        Q2 = Q(objectb__name='deux')
  1602
+        self.check_union(ObjectA, Q1, Q2)
  1603
+
  1604
+    def test_A_AB2(self):
  1605
+        Q1 = Q(name='two')
  1606
+        Q2 = Q(objectb__name='deux', objectb__number=2)
  1607
+        self.check_union(ObjectA, Q1, Q2)
  1608
+
  1609
+    def test_AB_ACB(self):
  1610
+        Q1 = Q(objectb__name='deux')
  1611
+        Q2 = Q(objectc__objectb__name='deux')
  1612
+        self.check_union(ObjectA, Q1, Q2)
  1613
+
  1614
+    def test_BAB_BAC(self):
  1615
+        Q1 = Q(objecta__objectb__name='deux')
  1616
+        Q2 = Q(objecta__objectc__name='ein')
  1617
+        self.check_union(ObjectB, Q1, Q2)
  1618
+
  1619
+    def test_BAB_BACB(self):
  1620
+        Q1 = Q(objecta__objectb__name='deux')
  1621
+        Q2 = Q(objecta__objectc__objectb__name='trois')
  1622
+        self.check_union(ObjectB, Q1, Q2)
  1623
+
  1624
+    def test_BA_BCA__BAB_BAC_BCA(self):
  1625
+        Q1 = Q(objecta__name='one', objectc__objecta__name='two')
  1626
+        Q2 = Q(objecta__objectc__name='ein', objectc__objecta__name='three', objecta__objectb__name='trois')
  1627
+        self.check_union(ObjectB, Q1, Q2)
  1628
+
  1629
+
1569 1630
 
1570 1631
 # In Python 2.6 beta releases, exceptions raised in __len__ are swallowed
1571 1632
 # (Python issue 1242657), so these cases return an empty list, rather than

0 notes on commit 113a8c1

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