diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 6be2a64..da24b40 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -13,8 +13,7 @@ from django.core.exceptions import PermissionDenied, ValidationError
 from django.core.paginator import Paginator
 from django.core.urlresolvers import reverse
 from django.db import models, transaction, router
-from django.db.models.related import RelatedObject
-from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist
+from django.db.models.fields import BLANK_CHOICE_DASH
 from django.db.models.sql.constants import LOOKUP_SEP, QUERY_TERMS
 from django.http import Http404, HttpResponse, HttpResponseRedirect
 from django.shortcuts import get_object_or_404
@@ -233,42 +232,34 @@ class BaseModelAdmin(object):
                 if k == lookup and v == value:
                     return True
 
-        parts = lookup.split(LOOKUP_SEP)
+        parts, fields, _ = model._meta.resolve_lookup_path(lookup)
+        parts = list(parts)
+        num_parts = len(parts)
+        num_fields = len(fields)
 
-        # Last term in lookup is a query term (__exact, __startswith etc)
-        # This term can be ignored.
-        if len(parts) > 1 and parts[-1] in QUERY_TERMS:
+        if not num_fields:
+            # Lookups on non-existants fields are ok, since they're ignored
+            # later.
+            return True
+
+        elif num_parts > 1 and parts[-1] in QUERY_TERMS:
             parts.pop()
+            num_parts += -1
 
         # Special case -- foo__id__exact and foo__id queries are implied
         # if foo has been specificially included in the lookup list; so
         # drop __id if it is the last part. However, first we need to find
         # the pk attribute name.
-        pk_attr_name = None
-        for part in parts[:-1]:
-            field, _, _, _ = model._meta.get_field_by_name(part)
-            if hasattr(field, 'rel'):
-                model = field.rel.to
-                pk_attr_name = model._meta.pk.name
-            elif isinstance(field, RelatedObject):
-                model = field.model
-                pk_attr_name = model._meta.pk.name
-            else:
-                pk_attr_name = None
-        if pk_attr_name and len(parts) > 1 and parts[-1] == pk_attr_name:
+        if (num_fields == num_parts and
+            parts[-1] == fields[-1][0].model._meta.pk.name):
             parts.pop()
+            num_parts += -1
 
-        try:
-            self.model._meta.get_field_by_name(parts[0])
-        except FieldDoesNotExist:
-            # Lookups on non-existants fields are ok, since they're ignored
-            # later.
+        if num_parts == 1:
             return True
-        else:
-            if len(parts) == 1:
-                return True
-            clean_lookup = LOOKUP_SEP.join(parts)
-            return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy
+
+        clean_lookup = LOOKUP_SEP.join(parts)
+        return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy
 
     def has_add_permission(self, request):
         """
diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
index 7204a12..3c5d1ab 100644
--- a/django/contrib/admin/util.py
+++ b/django/contrib/admin/util.py
@@ -1,7 +1,7 @@
 from django.db import models
 from django.db.models.sql.constants import LOOKUP_SEP
 from django.db.models.deletion import Collector
-from django.db.models.related import RelatedObject
+from django.db.models.related import RelatedObject, get_model_from_relation, NotRelationField
 from django.forms.forms import pretty_name
 from django.utils import formats
 from django.utils.html import escape
@@ -302,20 +302,6 @@ def display_for_field(value, field):
     else:
         return smart_unicode(value)
 
-
-class NotRelationField(Exception):
-    pass
-
-
-def get_model_from_relation(field):
-    if isinstance(field, models.related.RelatedObject):
-        return field.model
-    elif getattr(field, 'rel'): # or isinstance?
-        return field.rel.to
-    else:
-        raise NotRelationField
-
-
 def reverse_field_path(model, path):
     """ Create a reversed field path.
 
@@ -343,27 +329,7 @@ def reverse_field_path(model, path):
             related_name = field.field.name
             parent = field.model
         reversed_path.insert(0, related_name)
-    return (parent, LOOKUP_SEP.join(reversed_path))
-
-
-def get_fields_from_path(model, path):
-    """ Return list of Fields given path relative to model.
-
-    e.g. (ModelX, "user__groups__name") -> [
-        <django.db.models.fields.related.ForeignKey object at 0x...>,
-        <django.db.models.fields.related.ManyToManyField object at 0x...>,
-        <django.db.models.fields.CharField object at 0x...>,
-    ]
-    """
-    pieces = path.split(LOOKUP_SEP)
-    fields = []
-    for piece in pieces:
-        if fields:
-            parent = get_model_from_relation(fields[-1])
-        else:
-            parent = model
-        fields.append(parent._meta.get_field_by_name(piece)[0])
-    return fields
+    return parent, LOOKUP_SEP.join(reversed_path)
 
 
 def remove_trailing_data_field(fields):
@@ -381,8 +347,8 @@ def get_limit_choices_to_from_path(model, path):
     If final model in path is linked via a ForeignKey or ManyToManyField which
     has a `limit_choices_to` attribute, return it as a Q object.
     """
-
-    fields = get_fields_from_path(model, path)
+    _, fields, _ = model._meta.resolve_lookup_path(path)
+    fields = [field[0] for field in fields]
     fields = remove_trailing_data_field(fields)
     limit_choices_to = (
         fields and hasattr(fields[-1], 'rel') and
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
index 733f89d..511a644 100644
--- a/django/contrib/admin/validation.py
+++ b/django/contrib/admin/validation.py
@@ -4,7 +4,6 @@ from django.db.models.fields import FieldDoesNotExist
 from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
     _get_foreign_key)
 from django.contrib.admin import ListFilter, FieldListFilter
-from django.contrib.admin.util import get_fields_from_path, NotRelationField
 from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin,
     HORIZONTAL, VERTICAL)
 
@@ -84,9 +83,8 @@ def validate(cls, model):
                     # item is option #1
                     field = item
                 # Validate the field string
-                try:
-                    get_fields_from_path(model, field)
-                except (NotRelationField, FieldDoesNotExist):
+                _, _, last_field = model._meta.resolve_lookup_path(field)
+                if not last_field:
                     raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'"
                             " which does not refer to a Field."
                             % (cls.__name__, idx, field))
diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index 616b249..0b52452 100644
--- a/django/contrib/admin/views/main.py
+++ b/django/contrib/admin/views/main.py
@@ -10,7 +10,7 @@ from django.utils.http import urlencode
 
 from django.contrib.admin import FieldListFilter
 from django.contrib.admin.options import IncorrectLookupParameters
-from django.contrib.admin.util import quote, get_fields_from_path
+from django.contrib.admin.util import quote
 
 # Changelist settings
 ALL_VAR = 'all'
@@ -105,8 +105,8 @@ class ChangeList(object):
                         field, field_list_filter_class = list_filter, FieldListFilter.create
                     if not isinstance(field, models.Field):
                         field_path = field
-                        field = get_fields_from_path(self.model, field_path)[-1]
-                    spec = field_list_filter_class(field, request, cleaned_params,
+                        _, _, field = self.model._meta.resolve_lookup_path(field_path)
+                    spec = field_list_filter_class(field[0], request, cleaned_params,
                         self.model, self.model_admin, field_path=field_path)
                 if spec and spec.has_output():
                     filter_specs.append(spec)
diff --git a/django/contrib/gis/db/models/sql/where.py b/django/contrib/gis/db/models/sql/where.py
index 0e15222..f36217d 100644
--- a/django/contrib/gis/db/models/sql/where.py
+++ b/django/contrib/gis/db/models/sql/where.py
@@ -1,5 +1,3 @@
-from django.db.models.fields import FieldDoesNotExist
-from django.db.models.sql.constants import LOOKUP_SEP
 from django.db.models.sql.expressions import SQLEvaluator
 from django.db.models.sql.where import Constraint, WhereNode
 from django.contrib.gis.db.models.fields import GeometryField
@@ -58,32 +56,10 @@ class GeoWhereNode(WhereNode):
         'address__point'.
 
         If a GeometryField exists according to the given lookup on the model
-        options, it will be returned.  Otherwise returns None.
+        options, it will be returned.  Otherwise returns False.
         """
-        # This takes into account the situation where the lookup is a
-        # lookup to a related geographic field, e.g., 'address__point'.
-        field_list = lookup.split(LOOKUP_SEP)
-
-        # Reversing so list operates like a queue of related lookups,
-        # and popping the top lookup.
-        field_list.reverse()
-        fld_name = field_list.pop()
-
-        try:
-            geo_fld = opts.get_field(fld_name)
-            # If the field list is still around, then it means that the
-            # lookup was for a geometry field across a relationship --
-            # thus we keep on getting the related model options and the
-            # model field associated with the next field in the list
-            # until there's no more left.
-            while len(field_list):
-                opts = geo_fld.rel.to._meta
-                geo_fld = opts.get_field(field_list.pop())
-        except (FieldDoesNotExist, AttributeError):
-            return False
-
-        # Finally, make sure we got a Geographic field and return.
-        if isinstance(geo_fld, GeometryField):
-            return geo_fld
+        _, _, field = opts.resolve_lookup_path(lookup)
+        if field[0] and isinstance(field[0], GeometryField):
+            return field[0]
         else:
             return False
diff --git a/django/db/models/options.py b/django/db/models/options.py
index 0cd52a3..0f48510 100644
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -1,8 +1,11 @@
+from copy import copy
 import re
 from bisect import bisect
 
+from django.core.exceptions import FieldError
+from django.db.models.sql.constants import LOOKUP_SEP
 from django.conf import settings
-from django.db.models.related import RelatedObject
+from django.db.models.related import RelatedObject, get_model_from_relation, NotRelationField
 from django.db.models.fields.related import ManyToManyRel
 from django.db.models.fields import AutoField, FieldDoesNotExist
 from django.db.models.fields.proxy import OrderWrt
@@ -54,6 +57,8 @@ class Options(object):
         # from *other* models. Needed for some admin checks. Internal use only.
         self.related_fkey_lookups = []
 
+        self._resolve_lookup_path_cache = {}
+
     def contribute_to_class(self, cls, name):
         from django.db import connection
         from django.db.backends.util import truncate_name
@@ -495,3 +500,76 @@ class Options(object):
         Returns the index of the primary key field in the self.fields list.
         """
         return self.fields.index(self.pk)
+
+    def resolve_lookup_path(self, path, allow_explicit_fk=False, query=None):
+        """
+        Resolves the given lookup path and returns a tuple (parts, fields,
+        last_field) where parts is a tuple containing the lookup parts split by
+        LOOKUP_SEP; fields is a tuple of information (as returned by
+        get_field_by_name()) for all the fields that have been resolved
+        traversing the relations; and last_field is the information (also as
+        returned by get_field_by_name()) for the last part of the lookup, if
+        that part actually is a field, or None if it isn't. Note that only
+        tuples are returned so that the calling code explicitly has to
+        convert them to lists if it wants to modify them -- this is required
+        since the results are cached and we want to preserve them.
+        """
+        # Look in the cache first.
+        if path in self._resolve_lookup_path_cache:
+            return self._resolve_lookup_path_cache[path]
+
+        parts = tuple(path.split(LOOKUP_SEP))
+        if not parts:
+            raise FieldError("Cannot parse lookup path %r" % path)
+
+        num_parts = len(parts)
+        fields = ()
+        last_field = None
+
+        # Traverse the lookup query to distinguish related fields from
+        # lookup types and aggregates.
+        try:
+            opts = self
+            for counter, field_name in enumerate(parts):
+                if field_name == 'pk':
+                    field_name = opts.pk.name
+                field_info = opts.get_field_by_name(field_name)
+                fields += (field_info,)
+                if (counter + 1) < num_parts:
+                    # If we haven't reached the end of the list of
+                    # lookups yet, then let's attempt to continue
+                    # traversing relations.
+                    related_model = get_model_from_relation(field_info[0])
+                    opts = related_model._meta
+            # We have reached the end of the query and the last lookup
+            # is a field.
+            last_field = fields[-1]
+        except FieldDoesNotExist:
+            if (allow_explicit_fk and
+                field_name not in query.query_terms and
+                field_name not in query.aggregate_select.keys() and
+                field_name not in query.aggregates.keys() and
+                LOOKUP_SEP.join(parts) not in query.aggregates.keys()):
+                # XXX: A hack to allow foo_id to work in values() for
+                # backwards compatibility purposes. If we dropped that
+                # feature, this could be removed.
+                for f in opts.fields:
+                    if field_name == f.attname:
+                        field_info = opts.get_field_by_name(f.name)
+                        fields += (field_info,)
+                        break
+                else:
+                    raise FieldError("Cannot resolve keyword %r into field. "
+                                     "Choices are: %s" % (
+                                       field_name,
+                                       ", ".join(opts.get_all_field_names())))
+            # The traversing didn't reach the end because at least one of
+            # the lookups wasn't a field.
+        except NotRelationField:
+            # The traversing didn't reach the end because at least one of
+            # the lookups wasn't a relation field.
+            pass
+        # Cache the result.
+        self._resolve_lookup_path_cache[path] = (
+            parts, fields, last_field)
+        return self._resolve_lookup_path_cache[path]
diff --git a/django/db/models/related.py b/django/db/models/related.py
index 90995d7..ad1fe47 100644
--- a/django/db/models/related.py
+++ b/django/db/models/related.py
@@ -36,7 +36,7 @@ class RelatedObject(object):
                 {'%s__isnull' % self.parent_model._meta.module_name: False})
         lst = [(x._get_pk_val(), smart_unicode(x)) for x in queryset]
         return first_choice + lst
-        
+
     def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
         # Defer to the actual field definition for db prep
         return self.field.get_db_prep_lookup(lookup_type, value,
@@ -67,3 +67,14 @@ class RelatedObject(object):
 
     def get_cache_name(self):
         return "_%s_cache" % self.get_accessor_name()
+
+class NotRelationField(Exception):
+    pass
+
+def get_model_from_relation(field):
+    if isinstance(field, RelatedObject):
+        return field.model
+    elif getattr(field, 'rel'): # or isinstance?
+        return field.rel.to
+    else:
+        raise NotRelationField
\ No newline at end of file
diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
index 6bf7de2..45d76a1 100644
--- a/django/db/models/sql/compiler.py
+++ b/django/db/models/sql/compiler.py
@@ -383,11 +383,10 @@ class SQLCompiler(object):
         The 'name' is of the form 'field1__field2__...__fieldN'.
         """
         name, order = get_order_dir(name, default_order)
-        pieces = name.split(LOOKUP_SEP)
         if not alias:
             alias = self.query.get_initial_alias()
-        field, target, opts, joins, last, extra = self.query.setup_joins(pieces,
-                opts, alias, False)
+        field, target, opts, joins, last, extra = self.query.setup_joins(
+            name, opts, alias, False)
         alias = joins[-1]
         col = target.column
         if not field.rel:
diff --git a/django/db/models/sql/expressions.py b/django/db/models/sql/expressions.py
index 1bbf742..dde46ea 100644
--- a/django/db/models/sql/expressions.py
+++ b/django/db/models/sql/expressions.py
@@ -36,17 +36,16 @@ class SQLEvaluator(object):
     def prepare_leaf(self, node, query, allow_joins):
         if not allow_joins and LOOKUP_SEP in node.name:
             raise FieldError("Joined field references are not permitted in this query")
-
-        field_list = node.name.split(LOOKUP_SEP)
-        if (len(field_list) == 1 and
+        opts = query.get_meta()
+        parts, _, _ = opts.resolve_lookup_path(node.name)
+        if (len(parts) == 1 and
             node.name in query.aggregate_select.keys()):
             self.contains_aggregate = True
             self.cols[node] = query.aggregate_select[node.name]
         else:
             try:
                 field, source, opts, join_list, last, _ = query.setup_joins(
-                    field_list, query.get_meta(),
-                    query.get_initial_alias(), False)
+                    node.name, opts, query.get_initial_alias(), False)
                 col, _, join_list = query.trim_joins(source, join_list, last, False)
 
                 self.cols[node] = (join_list[-1], col)
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index 61fd2be..cb71e65 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -971,17 +971,16 @@ class Query(object):
         Adds a single aggregate expression to the Query
         """
         opts = model._meta
-        field_list = aggregate.lookup.split(LOOKUP_SEP)
-        if len(field_list) == 1 and aggregate.lookup in self.aggregates:
+        parts, fields, _ = opts.resolve_lookup_path(aggregate.lookup)
+        if len(parts) == 1 and aggregate.lookup in self.aggregates:
             # Aggregate is over an annotation
-            field_name = field_list[0]
-            col = field_name
-            source = self.aggregates[field_name]
+            col = parts[0]
+            source = self.aggregates[col]
             if not is_summary:
                 raise FieldError("Cannot compute %s('%s'): '%s' is an aggregate" % (
-                    aggregate.name, field_name, field_name))
-        elif ((len(field_list) > 1) or
-            (field_list[0] not in [i.name for i in opts.fields]) or
+                    aggregate.name, col, col))
+        elif ((len(parts) > 1) or
+            (parts[0] not in [i.name for i in opts.fields]) or
             self.group_by is None or
             not is_summary):
             # If:
@@ -992,7 +991,7 @@ class Query(object):
             # then we need to explore the joins that are required.
 
             field, source, opts, join_list, last, _ = self.setup_joins(
-                field_list, opts, self.get_initial_alias(), False)
+                aggregate.lookup, opts, self.get_initial_alias(), False)
 
             # Process the join chain to see if it can be trimmed
             col, _, join_list = self.trim_joins(source, join_list, last, False)
@@ -1007,15 +1006,15 @@ class Query(object):
         else:
             # The simplest cases. No joins required -
             # just reference the provided column alias.
-            field_name = field_list[0]
-            source = opts.get_field(field_name)
-            col = field_name
+            source = fields[0][0]
+            col = source.name
 
         # Add the aggregate to the query
         aggregate.add_to_query(self, alias, col=col, source=source, is_summary=is_summary)
 
     def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
-            can_reuse=None, process_extras=True, force_having=False):
+            can_reuse=None, process_extras=True, force_having=False,
+            allow_explicit_fk=False):
         """
         Add a single filter to the query. The 'filter_expr' is a pair:
         (filter_string, value). E.g. ('name__contains', 'fred')
@@ -1041,15 +1040,18 @@ class Query(object):
         during the processing of extra filters to avoid infinite recursion.
         """
         arg, value = filter_expr
-        parts = arg.split(LOOKUP_SEP)
-        if not parts:
-            raise FieldError("Cannot parse keyword query %r" % arg)
 
-        # Work out the lookup type and remove it from 'parts', if necessary.
-        if len(parts) == 1 or parts[-1] not in self.query_terms:
-            lookup_type = 'exact'
-        else:
+        parts, _, last_field = self.model._meta.resolve_lookup_path(
+            arg, allow_explicit_fk, query=self)
+
+        # Work out the lookup type and remove it from the end of 'parts',
+        # if necessary.
+        if not last_field and len(parts) and parts[-1] in self.query_terms:
+            parts = list(parts)
             lookup_type = parts.pop()
+            arg = LOOKUP_SEP.join(parts)
+        else:
+            lookup_type = 'exact'
 
         # By default, this is a WHERE clause. If an aggregate is referenced
         # in the value, the filter will be promoted to a HAVING
@@ -1070,7 +1072,7 @@ class Query(object):
             having_clause = value.contains_aggregate
 
         for alias, aggregate in self.aggregates.items():
-            if alias in (parts[0], LOOKUP_SEP.join(parts)):
+            if alias in (parts[0], arg):
                 entry = self.where_class()
                 entry.add((aggregate, lookup_type, value), AND)
                 if negate:
@@ -1083,10 +1085,11 @@ class Query(object):
         allow_many = trim or not negate
 
         try:
-            field, target, opts, join_list, last, extra_filters = self.setup_joins(
-                    parts, opts, alias, True, allow_many, allow_explicit_fk=True,
+            field, target, opts, join_list, last, extra_filters = (
+                self.setup_joins(
+                    arg, opts, alias, True, allow_many, allow_explicit_fk=True,
                     can_reuse=can_reuse, negate=negate,
-                    process_extras=process_extras)
+                    process_extras=process_extras))
         except MultiJoin, e:
             self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level]),
                     can_reuse)
@@ -1214,7 +1217,8 @@ class Query(object):
                     self.add_q(child, used_aliases, force_having=force_having)
                 else:
                     self.add_filter(child, connector, q_object.negated,
-                            can_reuse=used_aliases, force_having=force_having)
+                        can_reuse=used_aliases, force_having=force_having,
+                        allow_explicit_fk=True)
                 if force_having:
                     self.having.end_subtree()
                 else:
@@ -1234,7 +1238,7 @@ class Query(object):
         if self.filter_is_sticky:
             self.used_aliases = used_aliases
 
-    def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
+    def setup_joins(self, lookup, opts, alias, dupe_multis, allow_many=True,
             allow_explicit_fk=False, can_reuse=None, negate=False,
             process_extras=True):
         """
@@ -1260,28 +1264,24 @@ class Query(object):
         exclusions = set()
         extra_filters = []
         int_alias = None
-        for pos, name in enumerate(names):
+
+        names, fields, _ = opts.resolve_lookup_path(lookup,
+            allow_explicit_fk=allow_explicit_fk, query=self)
+
+        num_fields = len(fields)
+        num_names = len(names)
+        if num_fields != num_names:
+            if num_fields and num_fields == num_names - 1:
+                raise FieldError("Join on field %r not permitted. Did you misspell %r for the lookup type?" % (fields[-1][0].name, names[num_fields]))
+            else:
+                raise FieldError("Join on field %r not permitted." % names[num_fields])
+
+        for pos, field_info in enumerate(fields):
             if int_alias is not None:
                 exclusions.add(int_alias)
             exclusions.add(alias)
             last.append(len(joins))
-            if name == 'pk':
-                name = opts.pk.name
-            try:
-                field, model, direct, m2m = opts.get_field_by_name(name)
-            except FieldDoesNotExist:
-                for f in opts.fields:
-                    if allow_explicit_fk and name == f.attname:
-                        # XXX: A hack to allow foo_id to work in values() for
-                        # backwards compatibility purposes. If we dropped that
-                        # feature, this could be removed.
-                        field, model, direct, m2m = opts.get_field_by_name(f.name)
-                        break
-                else:
-                    names = opts.get_all_field_names() + self.aggregate_select.keys()
-                    raise FieldError("Cannot resolve keyword %r into field. "
-                            "Choices are: %s" % (name, ", ".join(names)))
-
+            field, model, direct, m2m = field_info
             if not allow_many and (m2m or not direct):
                 for alias in joins:
                     self.unref_alias(alias)
@@ -1309,7 +1309,7 @@ class Query(object):
                         for (dupe_opts, dupe_col) in dupe_set:
                             self.update_dupe_avoidance(dupe_opts, dupe_col,
                                     alias)
-            cached_data = opts._join_cache.get(name)
+            cached_data = opts._join_cache.get(names[pos])
             orig_opts = opts
             dupe_col = direct and field.column or field.field.column
             dedupe = dupe_col in opts.duplicate_targets
@@ -1338,7 +1338,7 @@ class Query(object):
                         to_col2 = opts.get_field_by_name(
                             field.m2m_reverse_target_field_name())[0].column
                         target = opts.pk
-                        orig_opts._join_cache[name] = (table1, from_col1,
+                        orig_opts._join_cache[names[pos]] = (table1, from_col1,
                                 to_col1, table2, from_col2, to_col2, opts,
                                 target)
 
@@ -1364,7 +1364,7 @@ class Query(object):
                         table = opts.db_table
                         from_col = field.column
                         to_col = target.column
-                        orig_opts._join_cache[name] = (table, from_col, to_col,
+                        orig_opts._join_cache[names[pos]] = (table, from_col, to_col,
                                 opts, target)
 
                     alias = self.join((alias, table, from_col, to_col),
@@ -1393,7 +1393,7 @@ class Query(object):
                         to_col2 = opts.get_field_by_name(
                             field.m2m_target_field_name())[0].column
                         target = opts.pk
-                        orig_opts._join_cache[name] = (table1, from_col1,
+                        orig_opts._join_cache[names[pos]] = (table1, from_col1,
                                 to_col1, table2, from_col2, to_col2, opts,
                                 target)
 
@@ -1422,7 +1422,7 @@ class Query(object):
                                 field.rel.field_name)[0]
                         else:
                             target = opts.pk
-                        orig_opts._join_cache[name] = (table, from_col, to_col,
+                        orig_opts._join_cache[names[pos]] = (table, from_col, to_col,
                                 opts, target)
 
                     alias = self.join((alias, table, from_col, to_col),
@@ -1437,12 +1437,6 @@ class Query(object):
                     to_avoid = int_alias
                 self.update_dupe_avoidance(dupe_opts, dupe_col, to_avoid)
 
-        if pos != len(names) - 1:
-            if pos == len(names) - 2:
-                raise FieldError("Join on field %r not permitted. Did you misspell %r for the lookup type?" % (name, names[pos + 1]))
-            else:
-                raise FieldError("Join on field %r not permitted." % name)
-
         return field, target, opts, joins, last, extra_filters
 
     def trim_joins(self, target, join_list, last, trim, nonnull_check=False):
@@ -1606,8 +1600,7 @@ class Query(object):
         try:
             for name in field_names:
                 field, target, u2, joins, u3, u4 = self.setup_joins(
-                        name.split(LOOKUP_SEP), opts, alias, False, allow_m2m,
-                        True)
+                    name, opts, alias, False, allow_m2m, True)
                 final_alias = joins[-1]
                 col = target.column
                 if len(joins) > 1:
@@ -1888,7 +1881,7 @@ class Query(object):
         opts = self.model._meta
         alias = self.get_initial_alias()
         field, col, opts, joins, last, extra = self.setup_joins(
-                start.split(LOOKUP_SEP), opts, alias, False)
+            start, opts, alias, False)
         select_col = self.alias_map[joins[1]][LHS_JOIN_COL]
         select_alias = alias
 
diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py
index 1b03647..18935a7 100644
--- a/django/db/models/sql/subqueries.py
+++ b/django/db/models/sql/subqueries.py
@@ -177,7 +177,7 @@ class DateQuery(Query):
         """
         try:
             result = self.setup_joins(
-                field_name.split(LOOKUP_SEP),
+                field_name,
                 self.get_meta(),
                 self.get_initial_alias(),
                 False
diff --git a/tests/modeltests/lookup/models.py b/tests/modeltests/lookup/models.py
index 82434bb..dbd52e3 100644
--- a/tests/modeltests/lookup/models.py
+++ b/tests/modeltests/lookup/models.py
@@ -26,3 +26,25 @@ class Tag(models.Model):
     name = models.CharField(max_length=100)
     class Meta:
         ordering = ('name', )
+
+class Season(models.Model):
+    year = models.PositiveSmallIntegerField()
+    gt = models.IntegerField(null=True, blank=True)
+
+    def __unicode__(self):
+        return unicode(self.year)
+
+class Game(models.Model):
+    season = models.ForeignKey(Season, related_name='games')
+    home = models.CharField(max_length=100)
+    away = models.CharField(max_length=100)
+
+    def __unicode__(self):
+        return u"%s at %s" % (self.away, self.home)
+
+class Player(models.Model):
+    name = models.CharField(max_length=100)
+    games = models.ManyToManyField(Game, related_name='players')
+
+    def __unicode__(self):
+        return self.name
\ No newline at end of file
diff --git a/tests/modeltests/lookup/tests.py b/tests/modeltests/lookup/tests.py
index 33eeae7..6e4d98e 100644
--- a/tests/modeltests/lookup/tests.py
+++ b/tests/modeltests/lookup/tests.py
@@ -1,8 +1,10 @@
 from datetime import datetime
 from operator import attrgetter
+
 from django.core.exceptions import FieldError
 from django.test import TestCase, skipUnlessDBFeature
-from models import Author, Article, Tag
+
+from models import Author, Article, Tag, Game, Season, Player
 
 
 class LookupTests(TestCase):
@@ -243,7 +245,7 @@ class LookupTests(TestCase):
         self.assertQuerysetEqual(Article.objects.filter(id=self.a5.id).values(),
             [{
                 'id': self.a5.id,
-                'author_id': self.au2.id, 
+                'author_id': self.au2.id,
                 'headline': 'Article 5',
                 'pub_date': datetime(2005, 8, 1, 9, 0)
             }], transform=identity)
@@ -606,3 +608,70 @@ class LookupTests(TestCase):
         a16.save()
         self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'b(.).*b\1'),
             ['<Article: barfoobaz>', '<Article: bazbaRFOO>', '<Article: foobarbaz>'])
+
+class LookupCollisionTests(TestCase):
+
+    def setUp(self):
+        # Here we're using 'gt' as a code number for the year, e.g. 111=>2009.
+        season_2009 = Season.objects.create(year=2009, gt=111)
+        season_2009.games.create(home="Houston Astros", away="St. Louis Cardinals")
+        season_2010 = Season.objects.create(year=2010, gt=222)
+        season_2010.games.create(home="Houston Astros", away="Chicago Cubs")
+        season_2010.games.create(home="Houston Astros", away="Milwaukee Brewers")
+        season_2010.games.create(home="Houston Astros", away="St. Louis Cardinals")
+        season_2011 = Season.objects.create(year=2011, gt=333)
+        season_2011.games.create(home="Houston Astros", away="St. Louis Cardinals")
+        season_2011.games.create(home="Houston Astros", away="Milwaukee Brewers")
+        hunter_pence = Player.objects.create(name="Hunter Pence")
+        hunter_pence.games = Game.objects.filter(season__year__in=[2009, 2010])
+        pudge = Player.objects.create(name="Ivan Rodriquez")
+        pudge.games = Game.objects.filter(season__year=2009)
+        pedro_feliz = Player.objects.create(name="Pedro Feliz")
+        pedro_feliz.games = Game.objects.filter(season__year__in=[2011])
+        johnson = Player.objects.create(name="Johnson")
+        johnson.games = Game.objects.filter(season__year__in=[2011])
+
+    def test_lookup_collision(self):
+        """
+        Ensure that genuine field names don't collide with built-in lookup
+        types ('year', 'gt', 'range', 'in' etc.).
+        Refs #11670.
+        """
+        # Games in 2010
+        self.assertEqual(Game.objects.filter(season__year=2010).count(), 3)
+        self.assertEqual(Game.objects.filter(season__year__exact=2010).count(), 3)
+        self.assertEqual(Game.objects.filter(season__gt=222).count(), 3)
+        self.assertEqual(Game.objects.filter(season__gt__exact=222).count(), 3)
+
+        # Games in 2011
+        self.assertEqual(Game.objects.filter(season__year=2011).count(), 2)
+        self.assertEqual(Game.objects.filter(season__year__exact=2011).count(), 2)
+        self.assertEqual(Game.objects.filter(season__gt=333).count(), 2)
+        self.assertEqual(Game.objects.filter(season__gt__exact=333).count(), 2)
+        self.assertEqual(Game.objects.filter(season__year__gt=2010).count(), 2)
+        self.assertEqual(Game.objects.filter(season__gt__gt=222).count(), 2)
+
+        # Games played in 2010 and 2011
+        self.assertEqual(Game.objects.filter(season__year__in=[2010, 2011]).count(), 5)
+        self.assertEqual(Game.objects.filter(season__year__gt=2009).count(), 5)
+        self.assertEqual(Game.objects.filter(season__gt__in=[222, 333]).count(), 5)
+        self.assertEqual(Game.objects.filter(season__gt__gt=111).count(), 5)
+
+        # Players who played in 2009
+        self.assertEqual(Player.objects.filter(games__season__year=2009).distinct().count(), 2)
+        self.assertEqual(Player.objects.filter(games__season__year__exact=2009).distinct().count(), 2)
+        self.assertEqual(Player.objects.filter(games__season__gt=111).distinct().count(), 2)
+        self.assertEqual(Player.objects.filter(games__season__gt__exact=111).distinct().count(), 2)
+
+        # Players who played in 2010
+        self.assertEqual(Player.objects.filter(games__season__year=2010).distinct().count(), 1)
+        self.assertEqual(Player.objects.filter(games__season__year__exact=2010).distinct().count(), 1)
+        self.assertEqual(Player.objects.filter(games__season__gt=222).distinct().count(), 1)
+        self.assertEqual(Player.objects.filter(games__season__gt__exact=222).distinct().count(), 1)
+
+        # Players who played in 2011
+        self.assertEqual(Player.objects.filter(games__season__year=2011).distinct().count(), 2)
+        self.assertEqual(Player.objects.filter(games__season__year__exact=2011).distinct().count(), 2)
+        self.assertEqual(Player.objects.filter(games__season__gt=333).distinct().count(), 2)
+        self.assertEqual(Player.objects.filter(games__season__year__gt=2010).distinct().count(), 2)
+        self.assertEqual(Player.objects.filter(games__season__gt__gt=222).distinct().count(), 2)
diff --git a/tests/regressiontests/null_queries/tests.py b/tests/regressiontests/null_queries/tests.py
index ab66ff6..35f2d2d 100644
--- a/tests/regressiontests/null_queries/tests.py
+++ b/tests/regressiontests/null_queries/tests.py
@@ -39,8 +39,8 @@ class NullQueriesTests(TestCase):
         # Can't use None on anything other than __exact
         self.assertRaises(ValueError, Choice.objects.filter, id__gt=None)
 
-        # Can't use None on anything other than __exact
-        self.assertRaises(ValueError, Choice.objects.filter, foo__gt=None)
+        # Valid query, but fails because foo isn't a keyword
+        self.assertRaises(FieldError, Choice.objects.filter, foo__gt=None)
 
         # Related managers use __exact=None implicitly if the object hasn't been saved.
         p2 = Poll(question="How?")
