From 3954bf50fb814e7362e69f1020a747e601cc73fe Mon Sep 17 00:00:00 2001 From: Artur Beltsov Date: Sun, 16 May 2021 15:25:35 +0500 Subject: [PATCH] Fixed #32750 -- Fixed crash of Extract() transform on OuterRef() expressions. Thanks Simon Charette for the review. --- django/db/models/functions/datetime.py | 4 ++- .../datetime/test_extract_trunc.py | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/django/db/models/functions/datetime.py b/django/db/models/functions/datetime.py index ce249b2854..7fc49897b5 100644 --- a/django/db/models/functions/datetime.py +++ b/django/db/models/functions/datetime.py @@ -64,7 +64,9 @@ class Extract(TimezoneMixin, Transform): def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False): copy = super().resolve_expression(query, allow_joins, reuse, summarize, for_save) - field = copy.lhs.output_field + field = getattr(copy.lhs, 'output_field', None) + if field is None: + return copy if not isinstance(field, (DateField, DateTimeField, TimeField, DurationField)): raise ValueError( 'Extract input expression must be DateField, DateTimeField, ' diff --git a/tests/db_functions/datetime/test_extract_trunc.py b/tests/db_functions/datetime/test_extract_trunc.py index fe80904330..e9e069c1cf 100644 --- a/tests/db_functions/datetime/test_extract_trunc.py +++ b/tests/db_functions/datetime/test_extract_trunc.py @@ -266,6 +266,15 @@ class DateFunctionTests(TestCase): with self.subTest(t): self.assertIsNone(DTModel.objects.annotate(extracted=t).first().extracted) + def test_extract_outerref_validation(self): + inner_qs = DTModel.objects.filter(name=ExtractMonth(OuterRef('name'))) + msg = ( + 'Extract input expression must be DateField, DateTimeField, ' + 'TimeField, or DurationField.' + ) + with self.assertRaisesMessage(ValueError, msg): + DTModel.objects.annotate(related_name=Subquery(inner_qs.values('name')[:1])) + @skipUnlessDBFeature('has_native_duration_field') def test_extract_duration(self): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) @@ -1098,6 +1107,31 @@ class DateFunctionTests(TestCase): ] ) + def test_extract_outerref(self): + datetime_1 = datetime(2000, 1, 1) + datetime_2 = datetime(2001, 3, 5) + datetime_3 = datetime(2002, 1, 3) + if settings.USE_TZ: + datetime_1 = timezone.make_aware(datetime_1, is_dst=False) + datetime_2 = timezone.make_aware(datetime_2, is_dst=False) + datetime_3 = timezone.make_aware(datetime_3, is_dst=False) + obj_1 = self.create_model(datetime_1, datetime_3) + obj_2 = self.create_model(datetime_2, datetime_1) + obj_3 = self.create_model(datetime_3, datetime_2) + + inner_qs = DTModel.objects.filter( + start_datetime__year=2000, + start_datetime__month=ExtractMonth(OuterRef('end_datetime')), + ) + qs = DTModel.objects.annotate( + related_pk=Subquery(inner_qs.values('pk')[:1]), + ) + self.assertSequenceEqual(qs.order_by('name').values('pk', 'related_pk'), [ + {'pk': obj_1.pk, 'related_pk': obj_1.pk}, + {'pk': obj_2.pk, 'related_pk': obj_1.pk}, + {'pk': obj_3.pk, 'related_pk': None}, + ]) + @override_settings(USE_TZ=True, TIME_ZONE='UTC') class DateFunctionWithTimeZoneTests(DateFunctionTests):