Fixed #24687 -- Added select_related() validation for nested non-relational fields.

The removed test was added in the original select_related() validation
patch (45d4e43d2d), but there doesn't
seem to be any reason for it.

Thanks Claude Paroz for help and review.
This commit is contained in:
Tim Graham 2015-10-09 21:03:04 -04:00
parent 5171f56fae
commit 67732a9b18
7 changed files with 31 additions and 13 deletions

View File

@ -10,6 +10,7 @@ from django.contrib.gis.db.models.sql import (
from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.measure import Area, Distance from django.contrib.gis.measure import Area, Distance
from django.db import connections from django.db import connections
from django.db.models.constants import LOOKUP_SEP
from django.db.models.expressions import RawSQL from django.db.models.expressions import RawSQL
from django.db.models.fields import Field from django.db.models.fields import Field
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
@ -675,9 +676,10 @@ class GeoQuerySet(QuerySet):
if geo_field not in opts.fields: if geo_field not in opts.fields:
# Is this operation going to be on a related geographic field? # Is this operation going to be on a related geographic field?
# If so, it'll have to be added to the select related information # If so, it'll have to be added to the select related information
# (e.g., if 'location__point' was given as the field name). # (e.g., if 'location__point' was given as the field name, then
# chop the non-relational field and add select_related('location')).
# Note: the operation really is defined as "must add select related!" # Note: the operation really is defined as "must add select related!"
self.query.add_select_related([field_name]) self.query.add_select_related([field_name.rsplit(LOOKUP_SEP, 1)[0]])
# Call pre_sql_setup() so that compiler.select gets populated. # Call pre_sql_setup() so that compiler.select gets populated.
compiler.pre_sql_setup() compiler.pre_sql_setup()
for col, _, _ in compiler.select: for col, _, _ in compiler.select:

View File

@ -673,7 +673,7 @@ class SQLCompiler(object):
if not f.is_relation: if not f.is_relation:
# If a non-related field is used like a relation, # If a non-related field is used like a relation,
# or if a single non-relational field is given. # or if a single non-relational field is given.
if next or (cur_depth == 1 and f.name in requested): if next or f.name in requested:
raise FieldError( raise FieldError(
"Non-relational field given in select_related: '%s'. " "Non-relational field given in select_related: '%s'. "
"Choices are: %s" % ( "Choices are: %s" % (

View File

@ -217,6 +217,23 @@ Database backend API
* ... * ...
``select_related()`` prohibits non-relational fields for nested relations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Django 1.8 added validation for non-relational fields in ``select_related()``::
>>> Book.objects.select_related('title')
Traceback (most recent call last):
...
FieldError: Non-relational field given in select_related: 'title'
But it didn't prohibit nested non-relation fields as it does now::
>>> Book.objects.select_related('author__name')
Traceback (most recent call last):
...
FieldError: Non-relational field given in select_related: 'name'
Miscellaneous Miscellaneous
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

View File

@ -36,7 +36,7 @@ class ChildAdmin(admin.ModelAdmin):
list_filter = ['parent', 'age'] list_filter = ['parent', 'age']
def get_queryset(self, request): def get_queryset(self, request):
return super(ChildAdmin, self).get_queryset(request).select_related("parent__name") return super(ChildAdmin, self).get_queryset(request).select_related("parent")
class CustomPaginationAdmin(ChildAdmin): class CustomPaginationAdmin(ChildAdmin):

View File

@ -75,9 +75,7 @@ class ChangeListTests(TestCase):
request, Child, request, Child,
*get_changelist_args(m, list_select_related=m.get_list_select_related(request)) *get_changelist_args(m, list_select_related=m.get_list_select_related(request))
) )
self.assertEqual(cl.queryset.query.select_related, { self.assertEqual(cl.queryset.query.select_related, {'parent': {}})
'parent': {'name': {}}
})
def test_select_related_as_tuple(self): def test_select_related_as_tuple(self):
ia = InvitationAdmin(Invitation, custom_site) ia = InvitationAdmin(Invitation, custom_site)

View File

@ -62,6 +62,9 @@ class RelatedGeoModelTest(TestCase):
qs = list(City.objects.filter(name=name).transform(srid, field_name='location__point')) qs = list(City.objects.filter(name=name).transform(srid, field_name='location__point'))
check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point) check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point)
# Relations more than one level deep can be queried.
self.assertEqual(list(Parcel.objects.transform(srid, field_name='city__location__point')), [])
@skipUnlessDBFeature("supports_extent_aggr") @skipUnlessDBFeature("supports_extent_aggr")
def test_related_extent_aggregate(self): def test_related_extent_aggregate(self):
"Testing the `Extent` aggregate on related geographic models." "Testing the `Extent` aggregate on related geographic models."

View File

@ -134,12 +134,6 @@ class SelectRelatedTests(TestCase):
orders = [o.genus.family.order.name for o in world] orders = [o.genus.family.order.name for o in world]
self.assertEqual(orders, ['Agaricales']) self.assertEqual(orders, ['Agaricales'])
def test_single_related_field(self):
with self.assertNumQueries(1):
species = Species.objects.select_related('genus__name')
names = [s.genus.name for s in species]
self.assertEqual(sorted(names), ['Amanita', 'Drosophila', 'Homo', 'Pisum'])
def test_field_traversal(self): def test_field_traversal(self):
with self.assertNumQueries(1): with self.assertNumQueries(1):
s = (Species.objects.all() s = (Species.objects.all()
@ -206,6 +200,10 @@ class SelectRelatedValidationTests(SimpleTestCase):
with self.assertRaisesMessage(FieldError, self.non_relational_error % ('name', '(none)')): with self.assertRaisesMessage(FieldError, self.non_relational_error % ('name', '(none)')):
list(Domain.objects.select_related('name')) list(Domain.objects.select_related('name'))
def test_non_relational_field_nested(self):
with self.assertRaisesMessage(FieldError, self.non_relational_error % ('name', 'family')):
list(Species.objects.select_related('genus__name'))
def test_many_to_many_field(self): def test_many_to_many_field(self):
with self.assertRaisesMessage(FieldError, self.invalid_error % ('toppings', '(none)')): with self.assertRaisesMessage(FieldError, self.invalid_error % ('toppings', '(none)')):
list(Pizza.objects.select_related('toppings')) list(Pizza.objects.select_related('toppings'))