mirror of https://github.com/django/django.git
Fixed #24699 -- Added aggregate support for DurationField on Oracle
This commit is contained in:
parent
e60cce4e40
commit
c7805ee214
|
@ -157,9 +157,6 @@ class BaseDatabaseFeatures(object):
|
||||||
# Support for the DISTINCT ON clause
|
# Support for the DISTINCT ON clause
|
||||||
can_distinct_on_fields = False
|
can_distinct_on_fields = False
|
||||||
|
|
||||||
# Can the backend use an Avg aggregate on DurationField?
|
|
||||||
can_avg_on_durationfield = True
|
|
||||||
|
|
||||||
# Does the backend decide to commit before SAVEPOINT statements
|
# Does the backend decide to commit before SAVEPOINT statements
|
||||||
# when autocommit is disabled? http://bugs.python.org/issue8145#msg109965
|
# when autocommit is disabled? http://bugs.python.org/issue8145#msg109965
|
||||||
autocommits_when_autocommit_is_off = False
|
autocommits_when_autocommit_is_off = False
|
||||||
|
|
|
@ -39,7 +39,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
uppercases_column_names = True
|
uppercases_column_names = True
|
||||||
# select for update with limit can be achieved on Oracle, but not with the current backend.
|
# select for update with limit can be achieved on Oracle, but not with the current backend.
|
||||||
supports_select_for_update_with_limit = False
|
supports_select_for_update_with_limit = False
|
||||||
can_avg_on_durationfield = False # Pending implementation (#24699).
|
|
||||||
|
|
||||||
def introspected_boolean_field_type(self, field=None, created_separately=False):
|
def introspected_boolean_field_type(self, field=None, created_separately=False):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
from django.db.models import DecimalField, DurationField, Func
|
||||||
|
|
||||||
|
|
||||||
|
class IntervalToSeconds(Func):
|
||||||
|
function = ''
|
||||||
|
template = """
|
||||||
|
EXTRACT(day from %(expressions)s) * 86400 +
|
||||||
|
EXTRACT(hour from %(expressions)s) * 3600 +
|
||||||
|
EXTRACT(minute from %(expressions)s) * 60 +
|
||||||
|
EXTRACT(second from %(expressions)s)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, expression, **extra):
|
||||||
|
output_field = extra.pop('output_field', DecimalField())
|
||||||
|
super(IntervalToSeconds, self).__init__(expression, output_field=output_field, **extra)
|
||||||
|
|
||||||
|
|
||||||
|
class SecondsToInterval(Func):
|
||||||
|
function = 'NUMTODSINTERVAL'
|
||||||
|
template = "%(function)s(%(expressions)s, 'SECOND')"
|
||||||
|
|
||||||
|
def __init__(self, expression, **extra):
|
||||||
|
output_field = extra.pop('output_field', DurationField())
|
||||||
|
super(SecondsToInterval, self).__init__(expression, output_field=output_field, **extra)
|
|
@ -78,6 +78,15 @@ class Avg(Aggregate):
|
||||||
output_field = extra.pop('output_field', FloatField())
|
output_field = extra.pop('output_field', FloatField())
|
||||||
super(Avg, self).__init__(expression, output_field=output_field, **extra)
|
super(Avg, self).__init__(expression, output_field=output_field, **extra)
|
||||||
|
|
||||||
|
def as_oracle(self, compiler, connection):
|
||||||
|
if self.output_field.get_internal_type() == 'DurationField':
|
||||||
|
expression = self.get_source_expressions()[0]
|
||||||
|
from django.db.backends.oracle.functions import IntervalToSeconds, SecondsToInterval
|
||||||
|
return compiler.compile(
|
||||||
|
SecondsToInterval(Avg(IntervalToSeconds(expression)))
|
||||||
|
)
|
||||||
|
return super(Avg, self).as_sql(compiler, connection)
|
||||||
|
|
||||||
|
|
||||||
class Count(Aggregate):
|
class Count(Aggregate):
|
||||||
function = 'COUNT'
|
function = 'COUNT'
|
||||||
|
@ -137,6 +146,15 @@ class Sum(Aggregate):
|
||||||
function = 'SUM'
|
function = 'SUM'
|
||||||
name = 'Sum'
|
name = 'Sum'
|
||||||
|
|
||||||
|
def as_oracle(self, compiler, connection):
|
||||||
|
if self.output_field.get_internal_type() == 'DurationField':
|
||||||
|
expression = self.get_source_expressions()[0]
|
||||||
|
from django.db.backends.oracle.functions import IntervalToSeconds, SecondsToInterval
|
||||||
|
return compiler.compile(
|
||||||
|
SecondsToInterval(Sum(IntervalToSeconds(expression)))
|
||||||
|
)
|
||||||
|
return super(Sum, self).as_sql(compiler, connection)
|
||||||
|
|
||||||
|
|
||||||
class Variance(Aggregate):
|
class Variance(Aggregate):
|
||||||
name = 'Variance'
|
name = 'Variance'
|
||||||
|
|
|
@ -10,7 +10,7 @@ from django.db.models import (
|
||||||
F, Aggregate, Avg, Count, DecimalField, DurationField, FloatField, Func,
|
F, Aggregate, Avg, Count, DecimalField, DurationField, FloatField, Func,
|
||||||
IntegerField, Max, Min, Sum, Value,
|
IntegerField, Max, Min, Sum, Value,
|
||||||
)
|
)
|
||||||
from django.test import TestCase, ignore_warnings, skipUnlessDBFeature
|
from django.test import TestCase, ignore_warnings
|
||||||
from django.test.utils import Approximate, CaptureQueriesContext
|
from django.test.utils import Approximate, CaptureQueriesContext
|
||||||
from django.utils import six, timezone
|
from django.utils import six, timezone
|
||||||
from django.utils.deprecation import RemovedInDjango20Warning
|
from django.utils.deprecation import RemovedInDjango20Warning
|
||||||
|
@ -441,11 +441,16 @@ class AggregateTestCase(TestCase):
|
||||||
vals = Book.objects.annotate(num_authors=Count("authors__id")).aggregate(Avg("num_authors"))
|
vals = Book.objects.annotate(num_authors=Count("authors__id")).aggregate(Avg("num_authors"))
|
||||||
self.assertEqual(vals, {"num_authors__avg": Approximate(1.66, places=1)})
|
self.assertEqual(vals, {"num_authors__avg": Approximate(1.66, places=1)})
|
||||||
|
|
||||||
@skipUnlessDBFeature('can_avg_on_durationfield')
|
|
||||||
def test_avg_duration_field(self):
|
def test_avg_duration_field(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Publisher.objects.aggregate(Avg('duration', output_field=DurationField())),
|
Publisher.objects.aggregate(Avg('duration', output_field=DurationField())),
|
||||||
{'duration__avg': datetime.timedelta(1, 43200)} # 1.5 days
|
{'duration__avg': datetime.timedelta(days=1, hours=12)}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sum_duration_field(self):
|
||||||
|
self.assertEqual(
|
||||||
|
Publisher.objects.aggregate(Sum('duration', output_field=DurationField())),
|
||||||
|
{'duration__sum': datetime.timedelta(days=3)}
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_sum_distinct_aggregate(self):
|
def test_sum_distinct_aggregate(self):
|
||||||
|
@ -984,47 +989,50 @@ class AggregateTestCase(TestCase):
|
||||||
Book.objects.annotate(Max('id')).annotate(Sum('id__max'))
|
Book.objects.annotate(Max('id')).annotate(Sum('id__max'))
|
||||||
|
|
||||||
def test_add_implementation(self):
|
def test_add_implementation(self):
|
||||||
try:
|
class MySum(Sum):
|
||||||
# test completely changing how the output is rendered
|
pass
|
||||||
def lower_case_function_override(self, compiler, connection):
|
|
||||||
sql, params = compiler.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'),
|
# test completely changing how the output is rendered
|
||||||
output_field=IntegerField()))
|
def lower_case_function_override(self, compiler, connection):
|
||||||
self.assertEqual(str(qs.query).count('sum('), 1)
|
sql, params = compiler.compile(self.source_expressions[0])
|
||||||
b1 = qs.get(pk=self.b4.pk)
|
substitutions = dict(function=self.function.lower(), expressions=sql)
|
||||||
self.assertEqual(b1.sums, 383)
|
substitutions.update(self.extra)
|
||||||
|
return self.template % substitutions, params
|
||||||
|
setattr(MySum, 'as_' + connection.vendor, lower_case_function_override)
|
||||||
|
|
||||||
# test changing the dict and delegating
|
qs = Book.objects.annotate(
|
||||||
def lower_case_function_super(self, compiler, connection):
|
sums=MySum(F('rating') + F('pages') + F('price'), output_field=IntegerField())
|
||||||
self.extra['function'] = self.function.lower()
|
)
|
||||||
return super(Sum, self).as_sql(compiler, connection)
|
self.assertEqual(str(qs.query).count('sum('), 1)
|
||||||
setattr(Sum, 'as_' + connection.vendor, lower_case_function_super)
|
b1 = qs.get(pk=self.b4.pk)
|
||||||
|
self.assertEqual(b1.sums, 383)
|
||||||
|
|
||||||
qs = Book.objects.annotate(sums=Sum(F('rating') + F('pages') + F('price'),
|
# test changing the dict and delegating
|
||||||
output_field=IntegerField()))
|
def lower_case_function_super(self, compiler, connection):
|
||||||
self.assertEqual(str(qs.query).count('sum('), 1)
|
self.extra['function'] = self.function.lower()
|
||||||
b1 = qs.get(pk=self.b4.pk)
|
return super(MySum, self).as_sql(compiler, connection)
|
||||||
self.assertEqual(b1.sums, 383)
|
setattr(MySum, 'as_' + connection.vendor, lower_case_function_super)
|
||||||
|
|
||||||
# test overriding all parts of the template
|
qs = Book.objects.annotate(
|
||||||
def be_evil(self, compiler, connection):
|
sums=MySum(F('rating') + F('pages') + F('price'), output_field=IntegerField())
|
||||||
substitutions = dict(function='MAX', expressions='2')
|
)
|
||||||
substitutions.update(self.extra)
|
self.assertEqual(str(qs.query).count('sum('), 1)
|
||||||
return self.template % substitutions, ()
|
b1 = qs.get(pk=self.b4.pk)
|
||||||
setattr(Sum, 'as_' + connection.vendor, be_evil)
|
self.assertEqual(b1.sums, 383)
|
||||||
|
|
||||||
qs = Book.objects.annotate(sums=Sum(F('rating') + F('pages') + F('price'),
|
# test overriding all parts of the template
|
||||||
output_field=IntegerField()))
|
def be_evil(self, compiler, connection):
|
||||||
self.assertEqual(str(qs.query).count('MAX('), 1)
|
substitutions = dict(function='MAX', expressions='2')
|
||||||
b1 = qs.get(pk=self.b4.pk)
|
substitutions.update(self.extra)
|
||||||
self.assertEqual(b1.sums, 2)
|
return self.template % substitutions, ()
|
||||||
finally:
|
setattr(MySum, 'as_' + connection.vendor, be_evil)
|
||||||
delattr(Sum, 'as_' + connection.vendor)
|
|
||||||
|
qs = Book.objects.annotate(
|
||||||
|
sums=MySum(F('rating') + F('pages') + F('price'), output_field=IntegerField())
|
||||||
|
)
|
||||||
|
self.assertEqual(str(qs.query).count('MAX('), 1)
|
||||||
|
b1 = qs.get(pk=self.b4.pk)
|
||||||
|
self.assertEqual(b1.sums, 2)
|
||||||
|
|
||||||
def test_complex_values_aggregation(self):
|
def test_complex_values_aggregation(self):
|
||||||
max_rating = Book.objects.values('rating').aggregate(
|
max_rating = Book.objects.values('rating').aggregate(
|
||||||
|
|
Loading…
Reference in New Issue