diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 64a3e7075b..9af7544db3 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1204,7 +1204,8 @@ class Query(object): if negate: self.promote_joins(join_list) - if lookup_type != 'isnull' and (self.is_nullable(target) or len(join_list) > 1): + if (lookup_type != 'isnull' and ( + self.is_nullable(target) or self.alias_map[join_list[-1]].join_type == self.LOUTER)): # The condition added here will be SQL like this: # NOT (col IS NOT NULL), where the first NOT is added in # upper layers of code. The reason for addition is that if col @@ -1447,7 +1448,7 @@ class Query(object): # nothing if self.is_nullable(query.select[0].field): alias, col = query.select[0].col - query.where.add((Constraint(alias, col, None), 'isnull', False), AND) + query.where.add((Constraint(alias, col, query.select[0].field), 'isnull', False), AND) # Still make sure that the trimmed parts in the inner query and # trimmed prefix are in sync. So, use the trimmed_joins to make sure diff --git a/tests/queries/models.py b/tests/queries/models.py index 28c858557b..c8598371a6 100644 --- a/tests/queries/models.py +++ b/tests/queries/models.py @@ -454,3 +454,19 @@ class Program(models.Model): class Channel(models.Model): programs = models.ManyToManyField(Program) identifier = models.OneToOneField(Identifier) + +class Book(models.Model): + title = models.TextField() + chapter = models.ForeignKey('Chapter') + +class Chapter(models.Model): + title = models.TextField() + paragraph = models.ForeignKey('Paragraph') + + +class Paragraph(models.Model): + text = models.TextField() + page = models.ManyToManyField('Page') + +class Page(models.Model): + text = models.TextField() diff --git a/tests/queries/tests.py b/tests/queries/tests.py index 82a8de08be..2c17cb5e76 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -24,7 +24,8 @@ from .models import (Annotation, Article, Author, Celebrity, Child, Cover, Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory, SpecialCategory, OneToOneCategory, NullableName, ProxyCategory, SingleObject, RelatedObject, ModelA, ModelD, Responsibility, Job, - JobResponsibilities, BaseA, Identifier, Program, Channel) + JobResponsibilities, BaseA, Identifier, Program, Channel, Page, Paragraph, + Chapter, Book) class BaseQuerysetTest(TestCase): @@ -2638,3 +2639,25 @@ class ManyToManyExcludeTest(TestCase): Identifier.objects.exclude(program__channel=None).order_by('name'), [''] ) + + def test_ticket_12823(self): + pg3 = Page.objects.create(text='pg3') + pg2 = Page.objects.create(text='pg2') + pg1 = Page.objects.create(text='pg1') + pa1 = Paragraph.objects.create(text='pa1') + pa1.page = [pg1, pg2] + pa2 = Paragraph.objects.create(text='pa2') + pa2.page = [pg2, pg3] + pa3 = Paragraph.objects.create(text='pa3') + ch1 = Chapter.objects.create(title='ch1', paragraph=pa1) + ch2 = Chapter.objects.create(title='ch2', paragraph=pa2) + ch3 = Chapter.objects.create(title='ch3', paragraph=pa3) + b1 = Book.objects.create(title='b1', chapter=ch1) + b2 = Book.objects.create(title='b2', chapter=ch2) + b3 = Book.objects.create(title='b3', chapter=ch3) + q = Book.objects.exclude(chapter__paragraph__page__text='pg1') + self.assertNotIn('IS NOT NULL', str(q.query)) + self.assertEqual(len(q), 2) + self.assertNotIn(b1, q) + self.assertIn(b2, q) + self.assertIn(b3, q)