mirror of
https://github.com/django/django.git
synced 2025-10-23 21:59:11 +00:00
Refactored qs.add_q() and utils/tree.py
The sql/query.py add_q method did a lot of where/having tree hacking to get complex queries to work correctly. The logic was refactored so that it should be simpler to understand. The new logic should also produce leaner WHERE conditions. The changes cascade somewhat, as some other parts of Django (like add_filter() and WhereNode) expect boolean trees in certain format or they fail to work. So to fix the add_q() one must fix utils/tree.py, some things in add_filter(), WhereNode and so on. This commit also fixed add_filter to see negate clauses up the path. A query like .exclude(Q(reversefk__in=a_list)) didn't work similarly to .filter(~Q(reversefk__in=a_list)). The reason for this is that only the immediate parent negate clauses were seen by add_filter, and thus a tree like AND: (NOT AND: (AND: condition)) will not be handled correctly, as there is one intermediary AND node in the tree. The example tree is generated by .exclude(~Q(reversefk__in=a_list)). Still, aggregation lost connectors in OR cases, and F() objects and aggregates in same filter clause caused GROUP BY problems on some databases. Fixed #17600, fixed #13198, fixed #17025, fixed #17000, fixed #11293.
This commit is contained in:
@@ -23,9 +23,9 @@ from .models import (Annotation, Article, Author, Celebrity, Child, Cover,
|
||||
Ranking, Related, Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten,
|
||||
Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory,
|
||||
SpecialCategory, OneToOneCategory, NullableName, ProxyCategory,
|
||||
SingleObject, RelatedObject, ModelA, ModelD, Responsibility, Job,
|
||||
JobResponsibilities, BaseA, Identifier, Program, Channel, Page, Paragraph,
|
||||
Chapter, Book, MyObject)
|
||||
SingleObject, RelatedObject, ModelA, ModelB, ModelC, ModelD, Responsibility,
|
||||
Job, JobResponsibilities, BaseA, Identifier, Program, Channel, Page,
|
||||
Paragraph, Chapter, Book, MyObject, Order, OrderItem)
|
||||
|
||||
|
||||
class BaseQuerysetTest(TestCase):
|
||||
@@ -834,7 +834,6 @@ class Queries1Tests(BaseQuerysetTest):
|
||||
Note.objects.filter(Q(extrainfo__author=self.a1)|Q(extrainfo=xx)),
|
||||
['<Note: n1>', '<Note: n3>']
|
||||
)
|
||||
xx.delete()
|
||||
q = Note.objects.filter(Q(extrainfo__author=self.a1)|Q(extrainfo=xx)).query
|
||||
self.assertEqual(
|
||||
len([x[2] for x in q.alias_map.values() if x[2] == q.LOUTER and q.alias_refcount[x[1]]]),
|
||||
@@ -880,7 +879,6 @@ class Queries1Tests(BaseQuerysetTest):
|
||||
Item.objects.filter(Q(tags__name='t4')),
|
||||
[repr(i) for i in Item.objects.filter(~Q(~Q(tags__name='t4')))])
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_exclude_in(self):
|
||||
self.assertQuerysetEqual(
|
||||
Item.objects.exclude(Q(tags__name__in=['t4', 't3'])),
|
||||
@@ -2291,6 +2289,103 @@ class ExcludeTest(TestCase):
|
||||
Responsibility.objects.exclude(jobs__name='Manager'),
|
||||
['<Responsibility: Programming>'])
|
||||
|
||||
class ExcludeTest17600(TestCase):
|
||||
"""
|
||||
Some regressiontests for ticket #17600. Some of these likely duplicate
|
||||
other existing tests.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
# Create a few Orders.
|
||||
self.o1 = Order.objects.create(pk=1)
|
||||
self.o2 = Order.objects.create(pk=2)
|
||||
self.o3 = Order.objects.create(pk=3)
|
||||
|
||||
# Create some OrderItems for the first order with homogeneous
|
||||
# status_id values
|
||||
self.oi1 = OrderItem.objects.create(order=self.o1, status=1)
|
||||
self.oi2 = OrderItem.objects.create(order=self.o1, status=1)
|
||||
self.oi3 = OrderItem.objects.create(order=self.o1, status=1)
|
||||
|
||||
# Create some OrderItems for the second order with heterogeneous
|
||||
# status_id values
|
||||
self.oi4 = OrderItem.objects.create(order=self.o2, status=1)
|
||||
self.oi5 = OrderItem.objects.create(order=self.o2, status=2)
|
||||
self.oi6 = OrderItem.objects.create(order=self.o2, status=3)
|
||||
|
||||
# Create some OrderItems for the second order with heterogeneous
|
||||
# status_id values
|
||||
self.oi7 = OrderItem.objects.create(order=self.o3, status=2)
|
||||
self.oi8 = OrderItem.objects.create(order=self.o3, status=3)
|
||||
self.oi9 = OrderItem.objects.create(order=self.o3, status=4)
|
||||
|
||||
def test_exclude_plain(self):
|
||||
"""
|
||||
This should exclude Orders which have some items with status 1
|
||||
|
||||
"""
|
||||
self.assertQuerysetEqual(
|
||||
Order.objects.exclude(items__status=1),
|
||||
['<Order: 3>'])
|
||||
|
||||
def test_exclude_plain_distinct(self):
|
||||
"""
|
||||
This should exclude Orders which have some items with status 1
|
||||
|
||||
"""
|
||||
self.assertQuerysetEqual(
|
||||
Order.objects.exclude(items__status=1).distinct(),
|
||||
['<Order: 3>'])
|
||||
|
||||
def test_exclude_with_q_object_distinct(self):
|
||||
"""
|
||||
This should exclude Orders which have some items with status 1
|
||||
|
||||
"""
|
||||
self.assertQuerysetEqual(
|
||||
Order.objects.exclude(Q(items__status=1)).distinct(),
|
||||
['<Order: 3>'])
|
||||
|
||||
def test_exclude_with_q_object_no_distinct(self):
|
||||
"""
|
||||
This should exclude Orders which have some items with status 1
|
||||
|
||||
"""
|
||||
self.assertQuerysetEqual(
|
||||
Order.objects.exclude(Q(items__status=1)),
|
||||
['<Order: 3>'])
|
||||
|
||||
def test_exclude_with_q_is_equal_to_plain_exclude(self):
|
||||
"""
|
||||
Using exclude(condition) and exclude(Q(condition)) should
|
||||
yield the same QuerySet
|
||||
|
||||
"""
|
||||
self.assertEqual(
|
||||
list(Order.objects.exclude(items__status=1).distinct()),
|
||||
list(Order.objects.exclude(Q(items__status=1)).distinct()))
|
||||
|
||||
def test_exclude_with_q_is_equal_to_plain_exclude_variation(self):
|
||||
"""
|
||||
Using exclude(condition) and exclude(Q(condition)) should
|
||||
yield the same QuerySet
|
||||
|
||||
"""
|
||||
self.assertEqual(
|
||||
list(Order.objects.exclude(items__status=1)),
|
||||
list(Order.objects.exclude(Q(items__status=1)).distinct()))
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_only_orders_with_all_items_having_status_1(self):
|
||||
"""
|
||||
This should only return orders having ALL items set to status 1, or
|
||||
those items not having any orders at all. The correct way to write
|
||||
this query in SQL seems to be using two nested subqueries.
|
||||
"""
|
||||
self.assertQuerysetEqual(
|
||||
Order.objects.exclude(~Q(items__status=1)).distinct(),
|
||||
['<Order: 1>'])
|
||||
|
||||
class NullInExcludeTest(TestCase):
|
||||
def setUp(self):
|
||||
NullableName.objects.create(name='i1')
|
||||
@@ -2326,6 +2421,14 @@ class NullInExcludeTest(TestCase):
|
||||
NullableName.objects.exclude(name__in=[None]),
|
||||
['i1'], attrgetter('name'))
|
||||
|
||||
def test_double_exclude(self):
|
||||
self.assertEqual(
|
||||
list(NullableName.objects.filter(~~Q(name='i1'))),
|
||||
list(NullableName.objects.filter(Q(name='i1'))))
|
||||
self.assertNotIn(
|
||||
'IS NOT NULL',
|
||||
str(NullableName.objects.filter(~~Q(name='i1')).query))
|
||||
|
||||
class EmptyStringsAsNullTest(TestCase):
|
||||
"""
|
||||
Test that filtering on non-null character fields works as expected.
|
||||
@@ -2433,8 +2536,12 @@ class WhereNodeTest(TestCase):
|
||||
|
||||
class NullJoinPromotionOrTest(TestCase):
|
||||
def setUp(self):
|
||||
d = ModelD.objects.create(name='foo')
|
||||
ModelA.objects.create(name='bar', d=d)
|
||||
self.d1 = ModelD.objects.create(name='foo')
|
||||
d2 = ModelD.objects.create(name='bar')
|
||||
self.a1 = ModelA.objects.create(name='a1', d=self.d1)
|
||||
c = ModelC.objects.create(name='c')
|
||||
b = ModelB.objects.create(name='b', c=c)
|
||||
self.a2 = ModelA.objects.create(name='a2', b=b, d=d2)
|
||||
|
||||
def test_ticket_17886(self):
|
||||
# The first Q-object is generating the match, the rest of the filters
|
||||
@@ -2448,12 +2555,38 @@ class NullJoinPromotionOrTest(TestCase):
|
||||
Q(b__c__name='foo')
|
||||
)
|
||||
qset = ModelA.objects.filter(q_obj)
|
||||
self.assertEqual(len(qset), 1)
|
||||
self.assertEqual(list(qset), [self.a1])
|
||||
# We generate one INNER JOIN to D. The join is direct and not nullable
|
||||
# so we can use INNER JOIN for it. However, we can NOT use INNER JOIN
|
||||
# for the b->c join, as a->b is nullable.
|
||||
self.assertEqual(str(qset.query).count('INNER JOIN'), 1)
|
||||
|
||||
def test_isnull_filter_promotion(self):
|
||||
qs = ModelA.objects.filter(Q(b__name__isnull=True))
|
||||
self.assertEqual(str(qs.query).count('LEFT OUTER'), 1)
|
||||
self.assertEqual(list(qs), [self.a1])
|
||||
|
||||
qs = ModelA.objects.filter(~Q(b__name__isnull=True))
|
||||
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
|
||||
self.assertEqual(list(qs), [self.a2])
|
||||
|
||||
qs = ModelA.objects.filter(~~Q(b__name__isnull=True))
|
||||
self.assertEqual(str(qs.query).count('LEFT OUTER'), 1)
|
||||
self.assertEqual(list(qs), [self.a1])
|
||||
|
||||
qs = ModelA.objects.filter(Q(b__name__isnull=False))
|
||||
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
|
||||
self.assertEqual(list(qs), [self.a2])
|
||||
|
||||
qs = ModelA.objects.filter(~Q(b__name__isnull=False))
|
||||
self.assertEqual(str(qs.query).count('LEFT OUTER'), 1)
|
||||
self.assertEqual(list(qs), [self.a1])
|
||||
|
||||
qs = ModelA.objects.filter(~~Q(b__name__isnull=False))
|
||||
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
|
||||
self.assertEqual(list(qs), [self.a2])
|
||||
|
||||
|
||||
class ReverseJoinTrimmingTest(TestCase):
|
||||
def test_reverse_trimming(self):
|
||||
# Check that we don't accidentally trim reverse joins - we can't know
|
||||
|
||||
Reference in New Issue
Block a user