From 1f7b25c1a7a85426675a04380f37b180edc08bbc Mon Sep 17 00:00:00 2001 From: Valentina Mukhamedzhanova Date: Fri, 14 Aug 2015 20:22:08 +0200 Subject: [PATCH] Fixed #24986 -- Added support for annotations in DISTINCT queries. --- AUTHORS | 2 ++ django/db/models/sql/compiler.py | 5 ++++- django/db/models/sql/query.py | 13 +++++++++-- docs/releases/1.9.txt | 2 ++ tests/annotations/tests.py | 37 +++++++++++++++++++++++++++++++- 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/AUTHORS b/AUTHORS index 7d594f0329..b553edd095 100644 --- a/AUTHORS +++ b/AUTHORS @@ -208,6 +208,7 @@ answer newbie questions, and generally made Django that much better: Dmitri Fedortchenko Dmitry Jemerov dne@mayonnaise.net + Donald Harvey Donald Stufft Don Spaulding Doug Beck @@ -710,6 +711,7 @@ answer newbie questions, and generally made Django that much better: Tyler Tarabula Tyson Tate Unai Zalakain + Valentina Mukhamedzhanova valtron Vasiliy Stavenko Vasil Vangelovski diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index f27cd87d5f..9d89269ee6 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -548,7 +548,10 @@ class SQLCompiler(object): _, targets, alias, joins, path, _ = self._setup_joins(parts, opts, None) targets, alias, _ = self.query.trim_joins(targets, joins, path) for target in targets: - result.append("%s.%s" % (qn(alias), qn2(target.column))) + if name in self.query.annotation_select: + result.append(name) + else: + result.append("%s.%s" % (qn(alias), qn2(target.column))) return result def find_ordering_name(self, name, opts, alias=None, default_order='ASC', diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index e6af36b4e9..f0845cd73a 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1292,9 +1292,15 @@ class Query(object): cur_names_with_path = (name, []) if name == 'pk': name = opts.pk.name + + field = None try: field = opts.get_field(name) + except FieldDoesNotExist: + if name in self.annotation_select: + field = self.annotation_select[name].output_field + if field is not None: # Fields that contain one-to-many relations with a generic # model (like a GenericForeignKey) cannot generate reverse # relations and therefore cannot be used for reverse querying. @@ -1305,8 +1311,11 @@ class Query(object): "querying. If it is a GenericForeignKey, consider " "adding a GenericRelation." % name ) - model = field.model._meta.concrete_model - except FieldDoesNotExist: + try: + model = field.model._meta.concrete_model + except AttributeError: + model = None + else: # We didn't find the current field, so move position back # one step. pos -= 1 diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index be5c3427a1..b06375dbab 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -462,6 +462,8 @@ Models :attr:`~django.db.models.SlugField.allow_unicode` argument to allow Unicode characters in slugs. +* Added support for referencing annotations in ``QuerySet.distinct()``. + CSRF ^^^^ diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py index 35b1b2bc51..5ff4df100d 100644 --- a/tests/annotations/tests.py +++ b/tests/annotations/tests.py @@ -8,7 +8,8 @@ from django.db.models import ( F, BooleanField, CharField, Count, DateTimeField, ExpressionWrapper, Func, IntegerField, Sum, Value, ) -from django.test import TestCase +from django.db.models.functions import Lower +from django.test import TestCase, skipUnlessDBFeature from django.utils import six from .models import ( @@ -160,6 +161,40 @@ class NonAggregateAnnotationTestCase(TestCase): other_agg = Author.objects.aggregate(age_sum=Sum('age')) self.assertEqual(agg['otherage_sum'], other_agg['age_sum']) + @skipUnlessDBFeature('can_distinct_on_fields') + def test_distinct_on_with_annotation(self): + store = Store.objects.create( + name='test store', + original_opening=datetime.datetime.now(), + friday_night_closing=datetime.time(21, 00, 00), + ) + names = [ + 'Theodore Roosevelt', + 'Eleanor Roosevelt', + 'Franklin Roosevelt', + 'Ned Stark', + 'Catelyn Stark', + ] + for name in names: + Employee.objects.create( + store=store, + first_name=name.split()[0], + last_name=name.split()[1], + age=30, salary=2000, + ) + + people = Employee.objects.annotate( + name_lower=Lower('last_name'), + ).distinct('name_lower') + + self.assertEqual(set(p.last_name for p in people), {'Stark', 'Roosevelt'}) + self.assertEqual(len(people), 2) + + people2 = Employee.objects.annotate( + test_alias=F('store__name'), + ).distinct('test_alias') + self.assertEqual(len(people2), 1) + def test_filter_annotation(self): books = Book.objects.annotate( is_book=Value(1, output_field=IntegerField())