diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index a6957bab7b..3044882a86 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -851,7 +851,7 @@ class Query(object): return alias def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1, - used=None, requested=None, restricted=None): + used=None, requested=None, restricted=None, nullable=None): """ Fill in the information needed for a select_related query. The current depth is measured as the number of connections away from the root model @@ -883,6 +883,10 @@ class Query(object): (not restricted and f.null) or f.rel.parent_link): continue table = f.rel.to._meta.db_table + if nullable or f.null: + promote = True + else: + promote = False if model: int_opts = opts alias = root_alias @@ -891,12 +895,12 @@ class Query(object): int_opts = int_model._meta alias = self.join((alias, int_opts.db_table, lhs_col, int_opts.pk.column), exclusions=used, - promote=f.null) + promote=promote) else: alias = root_alias alias = self.join((alias, table, f.column, f.rel.get_related_field().column), exclusions=used, - promote=f.null) + promote=promote) used.add(alias) self.related_select_cols.extend([(alias, f2.column) for f2 in f.rel.to._meta.fields]) @@ -905,8 +909,12 @@ class Query(object): next = requested.get(f.name, {}) else: next = False + if f.null is not None: + new_nullable = f.null + else: + new_nullable = None self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1, - used, next, restricted) + used, next, restricted, new_nullable) def add_filter(self, filter_expr, connector=AND, negate=False, trim=False, can_reuse=None): diff --git a/tests/regressiontests/null_fk/__init__.py b/tests/regressiontests/null_fk/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/null_fk/models.py b/tests/regressiontests/null_fk/models.py new file mode 100644 index 0000000000..1bc266c033 --- /dev/null +++ b/tests/regressiontests/null_fk/models.py @@ -0,0 +1,55 @@ +""" +Regression tests for proper working of ForeignKey(null=True). Tests these bugs: + + * #7369: FK non-null after null relationship on select_related() generates an invalid query + +""" + +from django.db import models + +class SystemInfo(models.Model): + system_name = models.CharField(max_length=32) + +class Forum(models.Model): + system_info = models.ForeignKey(SystemInfo) + forum_name = models.CharField(max_length=32) + +class Post(models.Model): + forum = models.ForeignKey(Forum, null=True) + title = models.CharField(max_length=32) + + def __unicode__(self): + return self.title + +class Comment(models.Model): + post = models.ForeignKey(Post, null=True) + comment_text = models.CharField(max_length=250) + + def __unicode__(self): + return self.comment_text + +__test__ = {'API_TESTS':""" + +>>> s = SystemInfo.objects.create(system_name='First forum') +>>> f = Forum.objects.create(system_info=s, forum_name='First forum') +>>> p = Post.objects.create(forum=f, title='First Post') +>>> c1 = Comment.objects.create(post=p, comment_text='My first comment') +>>> c2 = Comment.objects.create(comment_text='My second comment') + +# Starting from comment, make sure that a .select_related(...) with a specified +# set of fields will properly LEFT JOIN multiple levels of NULLs (and the things +# that come after the NULLs, or else data that should exist won't). +>>> c = Comment.objects.select_related().get(id=1) +>>> c.post + +>>> c = Comment.objects.select_related().get(id=2) +>>> print c.post +None + +>>> comments = Comment.objects.select_related('post__forum__system_info').all() +>>> [(c.id, c.post.id) for c in comments] +[(1, 1), (2, None)] +>>> [(c.comment_text, c.post.title) for c in comments] +[(u'My first comment', u'First Post'), (u'My second comment', None)] + +"""}