summaryrefslogtreecommitdiff
path: root/tests/aggregation
diff options
context:
space:
mode:
authorJosh Smeaton <josh.smeaton@gmail.com>2013-12-26 00:13:18 +1100
committerMarc Tamlyn <marc.tamlyn@gmail.com>2014-11-15 14:00:43 +0000
commitf59fd15c4928caf3dfcbd50f6ab47be409a43b01 (patch)
treefe4a04d98359e1ffcbfe991303eb97d9a8e16afc /tests/aggregation
parent39e3ef88c237e3f4cedc89cd36494a6d3f490812 (diff)
Fixed #14030 -- Allowed annotations to accept all expressions
Diffstat (limited to 'tests/aggregation')
-rw-r--r--tests/aggregation/tests.py279
1 files changed, 278 insertions, 1 deletions
diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py
index b1b6199ffa..851ce69db0 100644
--- a/tests/aggregation/tests.py
+++ b/tests/aggregation/tests.py
@@ -3,12 +3,21 @@ from __future__ import unicode_literals
import datetime
from decimal import Decimal
import re
+import warnings
+from django.core.exceptions import FieldError
from django.db import connection
-from django.db.models import Avg, Sum, Count, Max, Min
+from django.db.models import (
+ Avg, Sum, Count, Max, Min,
+ Aggregate, F, Value, Func,
+ IntegerField, FloatField, DecimalField)
+with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ from django.db.models.sql import aggregates as sql_aggregates
from django.test import TestCase
from django.test.utils import Approximate
from django.test.utils import CaptureQueriesContext
+from django.utils.deprecation import RemovedInDjango20Warning
from .models import Author, Publisher, Book, Store
@@ -678,3 +687,271 @@ class BaseAggregateTestCase(TestCase):
else:
self.assertNotIn('order by', qstr)
self.assertEqual(qstr.count(' join '), 0)
+
+
+class ComplexAggregateTestCase(TestCase):
+ fixtures = ["aggregation.json"]
+
+ def test_nonaggregate_aggregation_throws(self):
+ with self.assertRaisesRegexp(TypeError, 'fail is not an aggregate expression'):
+ Book.objects.aggregate(fail=F('price'))
+
+ def test_nonfield_annotation(self):
+ book = Book.objects.annotate(val=Max(Value(2, output_field=IntegerField())))[0]
+ self.assertEqual(book.val, 2)
+ book = Book.objects.annotate(val=Max(Value(2), output_field=IntegerField()))[0]
+ self.assertEqual(book.val, 2)
+
+ def test_missing_output_field_raises_error(self):
+ with self.assertRaisesRegexp(FieldError, 'Cannot resolve expression type, unknown output_field'):
+ Book.objects.annotate(val=Max(Value(2)))[0]
+
+ def test_annotation_expressions(self):
+ authors = Author.objects.annotate(combined_ages=Sum(F('age') + F('friends__age'))).order_by('name')
+ authors2 = Author.objects.annotate(combined_ages=Sum('age') + Sum('friends__age')).order_by('name')
+ for qs in (authors, authors2):
+ self.assertEqual(len(qs), 9)
+ self.assertQuerysetEqual(
+ qs, [
+ ('Adrian Holovaty', 132),
+ ('Brad Dayley', None),
+ ('Jacob Kaplan-Moss', 129),
+ ('James Bennett', 63),
+ ('Jeffrey Forcier', 128),
+ ('Paul Bissex', 120),
+ ('Peter Norvig', 103),
+ ('Stuart Russell', 103),
+ ('Wesley J. Chun', 176)
+ ],
+ lambda a: (a.name, a.combined_ages)
+ )
+
+ def test_aggregation_expressions(self):
+ a1 = Author.objects.aggregate(av_age=Sum('age') / Count('*'))
+ a2 = Author.objects.aggregate(av_age=Sum('age') / Count('age'))
+ a3 = Author.objects.aggregate(av_age=Avg('age'))
+ self.assertEqual(a1, {'av_age': 37})
+ self.assertEqual(a2, {'av_age': 37})
+ self.assertEqual(a3, {'av_age': Approximate(37.4, places=1)})
+
+ def test_order_of_precedence(self):
+ p1 = Book.objects.filter(rating=4).aggregate(avg_price=(Avg('price') + 2) * 3)
+ self.assertEqual(p1, {'avg_price': Approximate(148.18, places=2)})
+
+ p2 = Book.objects.filter(rating=4).aggregate(avg_price=Avg('price') + 2 * 3)
+ self.assertEqual(p2, {'avg_price': Approximate(53.39, places=2)})
+
+ def test_combine_different_types(self):
+ with self.assertRaisesRegexp(FieldError, 'Expression contains mixed types. You must set output_field'):
+ Book.objects.annotate(sums=Sum('rating') + Sum('pages') + Sum('price')).get(pk=4)
+
+ b1 = Book.objects.annotate(sums=Sum(F('rating') + F('pages') + F('price'),
+ output_field=IntegerField())).get(pk=4)
+ self.assertEqual(b1.sums, 383)
+
+ b2 = Book.objects.annotate(sums=Sum(F('rating') + F('pages') + F('price'),
+ output_field=FloatField())).get(pk=4)
+ self.assertEqual(b2.sums, 383.69)
+
+ b3 = Book.objects.annotate(sums=Sum(F('rating') + F('pages') + F('price'),
+ output_field=DecimalField(max_digits=6, decimal_places=2))).get(pk=4)
+ self.assertEqual(b3.sums, Decimal("383.69"))
+
+ def test_complex_aggregations_require_kwarg(self):
+ with self.assertRaisesRegexp(TypeError, 'Complex expressions require an alias'):
+ Author.objects.annotate(Sum(F('age') + F('friends__age')))
+ with self.assertRaisesRegexp(TypeError, 'Complex aggregates require an alias'):
+ Author.objects.aggregate(Sum('age') / Count('age'))
+
+ def test_aggregate_over_complex_annotation(self):
+ qs = Author.objects.annotate(
+ combined_ages=Sum(F('age') + F('friends__age')))
+
+ age = qs.aggregate(max_combined_age=Max('combined_ages'))
+ self.assertEqual(age['max_combined_age'], 176)
+
+ age = qs.aggregate(max_combined_age_doubled=Max('combined_ages') * 2)
+ self.assertEqual(age['max_combined_age_doubled'], 176 * 2)
+
+ age = qs.aggregate(
+ max_combined_age_doubled=Max('combined_ages') + Max('combined_ages'))
+ self.assertEqual(age['max_combined_age_doubled'], 176 * 2)
+
+ age = qs.aggregate(
+ max_combined_age_doubled=Max('combined_ages') + Max('combined_ages'),
+ sum_combined_age=Sum('combined_ages'))
+ self.assertEqual(age['max_combined_age_doubled'], 176 * 2)
+ self.assertEqual(age['sum_combined_age'], 954)
+
+ age = qs.aggregate(
+ max_combined_age_doubled=Max('combined_ages') + Max('combined_ages'),
+ sum_combined_age_doubled=Sum('combined_ages') + Sum('combined_ages'))
+ self.assertEqual(age['max_combined_age_doubled'], 176 * 2)
+ self.assertEqual(age['sum_combined_age_doubled'], 954 * 2)
+
+ def test_values_annotation_with_expression(self):
+ # ensure the F() is promoted to the group by clause
+ qs = Author.objects.values('name').annotate(another_age=Sum('age') + F('age'))
+ a = qs.get(pk=1)
+ self.assertEqual(a['another_age'], 68)
+
+ qs = qs.annotate(friend_count=Count('friends'))
+ a = qs.get(pk=1)
+ self.assertEqual(a['friend_count'], 2)
+
+ qs = qs.annotate(combined_age=Sum('age') + F('friends__age')).filter(pk=1).order_by('-combined_age')
+ self.assertEqual(
+ list(qs), [
+ {
+ "name": 'Adrian Holovaty',
+ "another_age": 68,
+ "friend_count": 1,
+ "combined_age": 69
+ },
+ {
+ "name": 'Adrian Holovaty',
+ "another_age": 68,
+ "friend_count": 1,
+ "combined_age": 63
+ }
+ ]
+ )
+
+ vals = qs.values('name', 'combined_age')
+ self.assertEqual(
+ list(vals), [
+ {
+ "name": 'Adrian Holovaty',
+ "combined_age": 69
+ },
+ {
+ "name": 'Adrian Holovaty',
+ "combined_age": 63
+ }
+ ]
+ )
+
+ def test_annotate_values_aggregate(self):
+ alias_age = Author.objects.annotate(
+ age_alias=F('age')
+ ).values(
+ 'age_alias',
+ ).aggregate(sum_age=Sum('age_alias'))
+
+ age = Author.objects.values('age').aggregate(sum_age=Sum('age'))
+
+ self.assertEqual(alias_age['sum_age'], age['sum_age'])
+
+ def test_annotate_over_annotate(self):
+ author = Author.objects.annotate(
+ age_alias=F('age')
+ ).annotate(
+ sum_age=Sum('age_alias')
+ ).get(pk=1)
+
+ other_author = Author.objects.annotate(
+ sum_age=Sum('age')
+ ).get(pk=1)
+
+ self.assertEqual(author.sum_age, other_author.sum_age)
+
+ def test_annotated_aggregate_over_annotated_aggregate(self):
+ with self.assertRaisesRegexp(FieldError, "Cannot compute Sum\('id__max'\): 'id__max' is an aggregate"):
+ Book.objects.annotate(Max('id')).annotate(Sum('id__max'))
+
+ def test_add_implementation(self):
+ try:
+ # test completely changing how the output is rendered
+ def lower_case_function_override(self, qn, connection):
+ sql, params = qn.compile(self.source_expressions[0])
+ substitutions = dict(function=self.function.lower(), expressions=sql)
+ substitutions.update(self.extra)
+ return self.template % substitutions, params
+ setattr(Sum, 'as_' + connection.vendor, lower_case_function_override)
+
+ qs = Book.objects.annotate(sums=Sum(F('rating') + F('pages') + F('price'),
+ output_field=IntegerField()))
+ self.assertEqual(str(qs.query).count('sum('), 1)
+ b1 = qs.get(pk=4)
+ self.assertEqual(b1.sums, 383)
+
+ # test changing the dict and delegating
+ def lower_case_function_super(self, qn, connection):
+ self.extra['function'] = self.function.lower()
+ return super(Sum, self).as_sql(qn, connection)
+ setattr(Sum, 'as_' + connection.vendor, lower_case_function_super)
+
+ qs = Book.objects.annotate(sums=Sum(F('rating') + F('pages') + F('price'),
+ output_field=IntegerField()))
+ self.assertEqual(str(qs.query).count('sum('), 1)
+ b1 = qs.get(pk=4)
+ self.assertEqual(b1.sums, 383)
+
+ # test overriding all parts of the template
+ def be_evil(self, qn, connection):
+ substitutions = dict(function='MAX', expressions='2')
+ substitutions.update(self.extra)
+ return self.template % substitutions, ()
+ setattr(Sum, 'as_' + connection.vendor, be_evil)
+
+ qs = Book.objects.annotate(sums=Sum(F('rating') + F('pages') + F('price'),
+ output_field=IntegerField()))
+ self.assertEqual(str(qs.query).count('MAX('), 1)
+ b1 = qs.get(pk=4)
+ self.assertEqual(b1.sums, 2)
+ finally:
+ delattr(Sum, 'as_' + connection.vendor)
+
+ def test_complex_values_aggregation(self):
+ max_rating = Book.objects.values('rating').aggregate(
+ double_max_rating=Max('rating') + Max('rating'))
+ self.assertEqual(max_rating['double_max_rating'], 5 * 2)
+
+ max_books_per_rating = Book.objects.values('rating').annotate(
+ books_per_rating=Count('id') + 5
+ ).aggregate(Max('books_per_rating'))
+ self.assertEqual(
+ max_books_per_rating,
+ {'books_per_rating__max': 3 + 5})
+
+ def test_expression_on_aggregation(self):
+
+ # Create a plain expression
+ class Greatest(Func):
+ function = 'GREATEST'
+
+ def as_sqlite(self, qn, connection):
+ return super(Greatest, self).as_sql(qn, connection, function='MAX')
+
+ qs = Publisher.objects.annotate(
+ price_or_median=Greatest(Avg('book__rating'), Avg('book__price'))
+ ).filter(price_or_median__gte=F('num_awards')).order_by('pk')
+ self.assertQuerysetEqual(
+ qs, [1, 2, 3, 4], lambda v: v.pk)
+
+ qs2 = Publisher.objects.annotate(
+ rating_or_num_awards=Greatest(Avg('book__rating'), F('num_awards'),
+ output_field=FloatField())
+ ).filter(rating_or_num_awards__gt=F('num_awards')).order_by('pk')
+ self.assertQuerysetEqual(
+ qs2, [1, 2], lambda v: v.pk)
+
+ def test_backwards_compatibility(self):
+
+ class SqlNewSum(sql_aggregates.Aggregate):
+ sql_function = 'SUM'
+
+ class NewSum(Aggregate):
+ name = 'Sum'
+
+ def add_to_query(self, query, alias, col, source, is_summary):
+ klass = SqlNewSum
+ aggregate = klass(
+ col, source=source, is_summary=is_summary, **self.extra)
+ query.annotations[alias] = aggregate
+
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", RemovedInDjango20Warning)
+ qs = Author.objects.values('name').annotate(another_age=NewSum('age') + F('age'))
+ a = qs.get(pk=1)
+ self.assertEqual(a['another_age'], 68)