From 40b8a6174f001a310aa33f7880db0efeeb04d4c4 Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Wed, 29 Dec 2021 14:00:50 +0000 Subject: Fixed #33397 -- Corrected resolving output_field for DateField/DateTimeField/TimeField/DurationFields. This includes refactoring of CombinedExpression._resolve_output_field() so it no longer uses the behavior inherited from Expression of guessing same output type if argument types match, and instead we explicitly define the output type of all supported operations. This also makes nonsensical operations involving dates (e.g. date + date) raise a FieldError, and adds support for automatically inferring output_field for cases such as: * date - date * date + duration * date - duration * time + duration * time - time --- tests/aggregation/tests.py | 4 ++-- tests/expressions/tests.py | 39 +++++++++++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) (limited to 'tests') diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py index b779fe551c..3922478bf3 100644 --- a/tests/aggregation/tests.py +++ b/tests/aggregation/tests.py @@ -1126,8 +1126,8 @@ class AggregateTestCase(TestCase): def test_combine_different_types(self): msg = ( - "Expression contains mixed types: FloatField, DecimalField. " - "You must set output_field." + "Cannot infer type of '+' expression involving these types: FloatField, " + "DecimalField. You must set output_field." ) qs = Book.objects.annotate(sums=Sum("rating") + Sum("pages") + Sum("price")) with self.assertRaisesMessage(FieldError, msg): diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py index a3d8d9b187..72e6020fa0 100644 --- a/tests/expressions/tests.py +++ b/tests/expressions/tests.py @@ -1736,6 +1736,17 @@ class FTimeDeltaTests(TestCase): ], ) + def test_datetime_and_duration_field_addition_with_annotate_and_no_output_field( + self, + ): + test_set = Experiment.objects.annotate( + estimated_end=F("start") + F("estimated_time") + ) + self.assertEqual( + [e.estimated_end for e in test_set], + [e.start + e.estimated_time for e in test_set], + ) + @skipUnlessDBFeature("supports_temporal_subtraction") def test_datetime_subtraction_with_annotate_and_no_output_field(self): test_set = Experiment.objects.annotate( @@ -2438,8 +2449,10 @@ class CombinedExpressionTests(SimpleTestCase): (null, Combinable.ADD, DateTimeField), (DateField, Combinable.SUB, null), ] - msg = "Expression contains mixed types: " for lhs, connector, rhs in tests: + msg = ( + f"Cannot infer type of {connector!r} expression involving these types: " + ) with self.subTest(lhs=lhs, connector=connector, rhs=rhs): expr = CombinedExpression( Expression(lhs()), @@ -2452,16 +2465,34 @@ class CombinedExpressionTests(SimpleTestCase): def test_resolve_output_field_dates(self): tests = [ # Add - same type. + (DateField, Combinable.ADD, DateField, FieldError), + (DateTimeField, Combinable.ADD, DateTimeField, FieldError), + (TimeField, Combinable.ADD, TimeField, FieldError), (DurationField, Combinable.ADD, DurationField, DurationField), + # Add - different type. + (DateField, Combinable.ADD, DurationField, DateTimeField), + (DateTimeField, Combinable.ADD, DurationField, DateTimeField), + (TimeField, Combinable.ADD, DurationField, TimeField), + (DurationField, Combinable.ADD, DateField, DateTimeField), + (DurationField, Combinable.ADD, DateTimeField, DateTimeField), + (DurationField, Combinable.ADD, TimeField, TimeField), # Subtract - same type. + (DateField, Combinable.SUB, DateField, DurationField), + (DateTimeField, Combinable.SUB, DateTimeField, DurationField), + (TimeField, Combinable.SUB, TimeField, DurationField), (DurationField, Combinable.SUB, DurationField, DurationField), # Subtract - different type. + (DateField, Combinable.SUB, DurationField, DateTimeField), + (DateTimeField, Combinable.SUB, DurationField, DateTimeField), + (TimeField, Combinable.SUB, DurationField, TimeField), (DurationField, Combinable.SUB, DateField, FieldError), (DurationField, Combinable.SUB, DateTimeField, FieldError), (DurationField, Combinable.SUB, DateTimeField, FieldError), ] - msg = "Expression contains mixed types: " for lhs, connector, rhs, combined in tests: + msg = ( + f"Cannot infer type of {connector!r} expression involving these types: " + ) with self.subTest(lhs=lhs, connector=connector, rhs=rhs, combined=combined): expr = CombinedExpression( Expression(lhs()), @@ -2477,8 +2508,8 @@ class CombinedExpressionTests(SimpleTestCase): def test_mixed_char_date_with_annotate(self): queryset = Experiment.objects.annotate(nonsense=F("name") + F("assigned")) msg = ( - "Expression contains mixed types: CharField, DateField. You must set " - "output_field." + "Cannot infer type of '+' expression involving these types: CharField, " + "DateField. You must set output_field." ) with self.assertRaisesMessage(FieldError, msg): list(queryset) -- cgit v1.3