Browse files

Fixed #19385 -- Introduced multicolumn joins to the ORM

This patch consist of two main parts. The first part deals with adding
RelatedField and ForeignObject fields and basing current relation
fields on those. The second part deals with altering the ORM to be able
to handle multicolumn joins.

There is still a lot of work to be done for full multicolumn ForeignKey
support. The ForeignObject based fields do not integrate to other parts
of Django (admin, ModelForms, ...). There is no support for multicolumn
primary keys or any public way to use multicolumn ForeignKeys. Even
with these limitations the ForeignObject based fields allows
implementing some interesting constructs, for examples see the added
tests in tests/foreign_object/

The downside of using ForeignObject is that it is private API and should
remain so until full virtual field support is implemented. Changes are

The patch was written by Jeremy Tillman with some final polish by
committer. A very big thank you for the work done!
  • Loading branch information...
1 parent 2279066 commit 4810cefd0a99f6963ad57415dd8771e8b2b4a279 @jtillman jtillman committed with Jan 2, 2013
@@ -10,7 +10,7 @@
from django.db import connection
from django.db.models import signals
from django.db import models, router, DEFAULT_DB_ALIAS
-from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
+from django.db.models.fields.related import ForeignObject, ManyToManyRel
from django.db.models.related import PathInfo
from django.forms import ModelForm
from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance
@@ -149,7 +149,7 @@ def __set__(self, instance, value):
setattr(instance, self.fk_field, fk)
setattr(instance, self.cache_attr, value)
-class GenericRelation(RelatedField, Field):
+class GenericRelation(ForeignObject):
"""Provides an accessor to generic related objects (e.g. comments)"""
def __init__(self, to, **kwargs):
@@ -167,25 +167,35 @@ def __init__(self, to, **kwargs):
kwargs['blank'] = True
kwargs['editable'] = False
kwargs['serialize'] = False
- Field.__init__(self, **kwargs)
+ super(GenericRelation, self).__init__(
+ to,
+ to_fields=[],
+ from_fields=[self.object_id_field_name],
+ **kwargs)
+ def resolve_relation_fields(self):
+ self.to_fields = []
+ return [([0],
def get_path_info(self):
- from_field =
opts =
target = opts.get_field_by_name(self.object_id_field_name)[0]
- # Note that we are using different field for the join_field
- # than from_field or to_field. This is a hack, but we need the
- # GenericRelation to generate the extra SQL.
- return ([PathInfo(from_field, target, self.model._meta, opts, self, True, False)],
- opts, target, self)
+ # Still need the path from here to parent.
+ return [PathInfo(self.model._meta, opts, (target,), self, True, False)]
def get_choices_default(self):
- return Field.get_choices(self, include_blank=False)
+ return super(GenericRelation, self).get_choices(include_blank=False)
def value_to_string(self, obj):
qs = getattr(obj,
return smart_text([instance._get_pk_val() for instance in qs])
+ def get_joining_columns(self, reverse_join=False):
+ if not reverse_join:
+ raise ValueError('GenericRelation only supports reverse joins.')
+ return super(GenericRelation, self).get_joining_columns(reverse_join)
def m2m_db_table(self):
@@ -153,8 +153,16 @@ def get_validation_errors(outfile, app=None):
# Make sure the related field specified by a ForeignKey is unique
- if not
- e.add(opts, "Field '%s' under model '%s' must have a unique=True constraint." % (f.rel.field_name,
+ if f.requires_unique_target:
+ if len(f.foreign_related_fields) > 1:
+ has_unique_field = False
+ for rel_field in f.foreign_related_fields:
+ has_unique_field = has_unique_field or rel_field.unique
+ if not has_unique_field:
+ e.add(opts, "Field combination '%s' under model '%s' must have a unique=True constraint" % (','.join([ for rel_field in f.foreign_related_fields]),
+ else:
+ if not f.foreign_related_fields[0].unique:
+ e.add(opts, "Field '%s' under model '%s' must have a unique=True constraint." % (f.foreign_related_fields[0].name,
rel_opts =
rel_name = f.related.get_accessor_name()
@@ -17,6 +17,12 @@ def resolve_columns(self, row, fields=()):
return row[:index_extra_select] + tuple(values)
+ def as_subquery_condition(self, alias, columns):
+ qn = self.quote_name_unless_alias
+ qn2 = self.connection.ops.quote_name
+ sql, params = self.as_sql()
+ return '(%s) IN (%s)' % (', '.join(['%s.%s' % (qn(alias), qn2(column)) for column in columns]), sql), params
class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler):
@@ -8,7 +8,7 @@
from django.db.models.fields import *
from django.db.models.fields.subclassing import SubfieldBase
from django.db.models.fields.files import FileField, ImageField
-from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel
+from django.db.models.fields.related import ForeignKey, ForeignObject, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel
from django.db.models.deletion import CASCADE, PROTECT, SET, SET_NULL, SET_DEFAULT, DO_NOTHING, ProtectedError
from django.db.models import signals
from django.utils.decorators import wraps
@@ -10,7 +10,7 @@
from django.core.exceptions import (ObjectDoesNotExist,
MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS)
from django.db.models.fields import AutoField, FieldDoesNotExist
-from django.db.models.fields.related import (ManyToOneRel,
+from django.db.models.fields.related import (ForeignObjectRel, ManyToOneRel,
OneToOneField, add_lazy_relation)
from django.db import (router, transaction, DatabaseError,
@@ -333,12 +333,12 @@ def __init__(self, *args, **kwargs):
# The reason for the kwargs check is that standard iterator passes in by
# args, and instantiation for iteration is 33% faster.
args_len = len(args)
- if args_len > len(self._meta.fields):
+ if args_len > len(self._meta.concrete_fields):
# Daft, but matches old exception sans the err msg.
raise IndexError("Number of args exceeds number of fields")
- fields_iter = iter(self._meta.fields)
if not kwargs:
+ fields_iter = iter(self._meta.concrete_fields)
# The ordering of the zip calls matter - zip throws StopIteration
# when an iter throws it. So if the first iter throws it, the second
# is *not* consumed. We rely on this, so don't change the order
@@ -347,6 +347,7 @@ def __init__(self, *args, **kwargs):
setattr(self, field.attname, val)
# Slower, kwargs-ready version.
+ fields_iter = iter(self._meta.fields)
for val, field in zip(args, fields_iter):
setattr(self, field.attname, val)
kwargs.pop(, None)
@@ -363,11 +364,12 @@ def __init__(self, *args, **kwargs):
# data-descriptor object (DeferredAttribute) without triggering its
# __get__ method.
if (field.attname not in kwargs and
- isinstance(self.__class__.__dict__.get(field.attname), DeferredAttribute)):
+ (isinstance(self.__class__.__dict__.get(field.attname), DeferredAttribute)
+ or field.column is None)):
# This field will be populated on request.
if kwargs:
- if isinstance(field.rel, ManyToOneRel):
+ if isinstance(field.rel, ForeignObjectRel):
# Assume object instance was passed in.
rel_obj = kwargs.pop(
@@ -394,6 +396,7 @@ def __init__(self, *args, **kwargs):
val = field.get_default()
val = field.get_default()
if is_related_object:
# If we are passed a related instance, set it using the
# instead of field.attname (e.g. "user" instead of
@@ -528,7 +531,7 @@ def save(self, force_insert=False, force_update=False, using=None,
# automatically do a "update_fields" save on the loaded fields.
elif not force_insert and self._deferred and using == self._state.db:
field_names = set()
- for field in self._meta.fields:
+ for field in self._meta.concrete_fields:
if not field.primary_key and not hasattr(field, 'through'):
deferred_fields = [
@@ -614,7 +617,7 @@ def _save_table(self, raw=False, cls=None, force_insert=False,
for a single table.
meta = cls._meta
- non_pks = [f for f in meta.local_fields if not f.primary_key]
+ non_pks = [f for f in meta.local_concrete_fields if not f.primary_key]
if update_fields:
non_pks = [f for f in non_pks
@@ -652,7 +655,7 @@ def _save_table(self, raw=False, cls=None, force_insert=False,
**{ getattr(self, field.attname)}).count()
self._order = order_value
- fields = meta.local_fields
+ fields = meta.local_concrete_fields
if not pk_set:
fields = [f for f in fields if not isinstance(f, AutoField)]
Oops, something went wrong.

0 comments on commit 4810cef

Please sign in to comment.