diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 5dca423de4..12cb390e98 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1278,10 +1278,6 @@ class Query(BaseExpression): # supports both transform and lookup for the name. lookup_class = lhs.get_lookup(lookup_name) if not lookup_class: - if lhs.field.is_relation: - raise FieldError( - "Related Field got invalid lookup: {}".format(lookup_name) - ) # A lookup wasn't found. Try to interpret the name as a transform # and do an Exact lookup against it. lhs = self.try_transform(lhs, lookup_name) @@ -1450,12 +1446,6 @@ class Query(BaseExpression): can_reuse.update(join_list) if join_info.final_field.is_relation: - # No support for transforms for relational fields - num_lookups = len(lookups) - if num_lookups > 1: - raise FieldError( - "Related Field got invalid lookup: {}".format(lookups[0]) - ) if len(targets) == 1: col = self._get_col(targets[0], join_info.final_field, alias) else: diff --git a/tests/lookup/tests.py b/tests/lookup/tests.py index 11a277d7e0..f43cd3f91c 100644 --- a/tests/lookup/tests.py +++ b/tests/lookup/tests.py @@ -786,10 +786,16 @@ class LookupTests(TestCase): def test_relation_nested_lookup_error(self): # An invalid nested lookup on a related field raises a useful error. - msg = "Related Field got invalid lookup: editor" + msg = ( + "Unsupported lookup 'editor' for ForeignKey or join on the field not " + "permitted." + ) with self.assertRaisesMessage(FieldError, msg): Article.objects.filter(author__editor__name="James") - msg = "Related Field got invalid lookup: foo" + msg = ( + "Unsupported lookup 'foo' for ForeignKey or join on the field not " + "permitted." + ) with self.assertRaisesMessage(FieldError, msg): Tag.objects.filter(articles__foo="bar") diff --git a/tests/queries/models.py b/tests/queries/models.py index 155b74b0c9..0e8d6dd174 100644 --- a/tests/queries/models.py +++ b/tests/queries/models.py @@ -1,6 +1,8 @@ """ Various complex queries that have been problematic in the past. """ +import datetime + from django.db import models from django.db.models.functions import Now @@ -64,7 +66,7 @@ class Annotation(models.Model): class DateTimePK(models.Model): - date = models.DateTimeField(primary_key=True, auto_now_add=True) + date = models.DateTimeField(primary_key=True, default=datetime.datetime.now) class ExtraInfo(models.Model): diff --git a/tests/queries/tests.py b/tests/queries/tests.py index 1bd72dd8b8..59f9a5c177 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -7,12 +7,13 @@ from threading import Lock from django.core.exceptions import EmptyResultSet, FieldError from django.db import DEFAULT_DB_ALIAS, connection -from django.db.models import Count, Exists, F, Max, OuterRef, Q +from django.db.models import CharField, Count, Exists, F, Max, OuterRef, Q from django.db.models.expressions import RawSQL +from django.db.models.functions import ExtractYear, Length, LTrim from django.db.models.sql.constants import LOUTER from django.db.models.sql.where import AND, OR, NothingNode, WhereNode from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature -from django.test.utils import CaptureQueriesContext, ignore_warnings +from django.test.utils import CaptureQueriesContext, ignore_warnings, register_lookup from django.utils.deprecation import RemovedInDjango50Warning from .models import ( @@ -391,6 +392,33 @@ class Queries1Tests(TestCase): qs = qs.order_by("id") self.assertNotIn("OUTER JOIN", str(qs.query)) + def test_filter_by_related_field_transform(self): + extra_old = ExtraInfo.objects.create( + info="extra 12", + date=DateTimePK.objects.create(date=datetime.datetime(2020, 12, 10)), + ) + ExtraInfo.objects.create(info="extra 11", date=DateTimePK.objects.create()) + a5 = Author.objects.create(name="a5", num=5005, extra=extra_old) + + fk_field = ExtraInfo._meta.get_field("date") + with register_lookup(fk_field, ExtractYear): + self.assertSequenceEqual( + ExtraInfo.objects.filter(date__year=2020), + [extra_old], + ) + self.assertSequenceEqual( + Author.objects.filter(extra__date__year=2020), [a5] + ) + + def test_filter_by_related_field_nested_transforms(self): + extra = ExtraInfo.objects.create(info=" extra") + a5 = Author.objects.create(name="a5", num=5005, extra=extra) + info_field = ExtraInfo._meta.get_field("info") + with register_lookup(info_field, Length), register_lookup(CharField, LTrim): + self.assertSequenceEqual( + Author.objects.filter(extra__info__ltrim__length=5), [a5] + ) + def test_get_clears_ordering(self): """ get() should clear ordering for optimization purposes.