<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array"/>
  <modified type="array">
    <modified>
      <diff>@@ -11,7 +11,9 @@ from django.utils.encoding import smart_unicode, iri_to_uri
 from django.utils.translation import ugettext as _
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
+from django.contrib.admin.models import PRIMARY_KEY_URL_SEPARATOR
 import datetime
+import itertools
 
 class FilterSpec(object):
     filter_specs = []
@@ -58,8 +60,9 @@ class RelatedFilterSpec(FilterSpec):
             self.lookup_title = f.rel.to._meta.verbose_name
         else:
             self.lookup_title = f.verbose_name
-        self.lookup_kwarg = '%s__%s__exact' % (f.name, f.rel.to._meta.pk.name)
-        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
+        self.lookup_kwarg = '%s__exact' % (f.name,)
+        self.lookup_val = request.GET.get(self.lookup_kwarg, '').split(PRIMARY_KEY_URL_SEPARATOR)
+        self.lookup_val = filter(None, self.lookup_val)
         self.lookup_choices = f.get_choices(include_blank=False)
 
     def has_output(self):
@@ -69,12 +72,15 @@ class RelatedFilterSpec(FilterSpec):
         return self.lookup_title
 
     def choices(self, cl):
-        yield {'selected': self.lookup_val is None,
+        yield {'selected': not len(self.lookup_val),
                'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
                'display': _('All')}
-        for pk_val, val in self.lookup_choices:
-            yield {'selected': self.lookup_val == smart_unicode(pk_val),
-                   'query_string': cl.get_query_string({self.lookup_kwarg: pk_val}),
+
+        for val in self.lookup_choices:
+            pk_val = [smart_unicode(getattr(val, f.attname)) for f in self.field.rel.to._meta.pks]
+            # TODO: fix for composites
+            yield {'selected': self.lookup_val == pk_val,
+                   'query_string': cl.get_query_string({self.lookup_kwarg: PRIMARY_KEY_URL_SEPARATOR.join(pk_val)}),
                    'display': val}
 
 FilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec)</diff>
      <filename>django/contrib/admin/filterspecs.py</filename>
    </modified>
    <modified>
      <diff>@@ -6,6 +6,8 @@ from django.utils.translation import ugettext_lazy as _
 from django.utils.encoding import smart_unicode
 from django.utils.safestring import mark_safe
 
+PRIMARY_KEY_URL_SEPARATOR = ','
+
 ADDITION = 1
 CHANGE = 2
 DELETION = 3</diff>
      <filename>django/contrib/admin/models.py</filename>
    </modified>
    <modified>
      <diff>@@ -5,6 +5,7 @@ from django.forms.models import BaseInlineFormSet
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.admin import widgets
 from django.contrib.admin import helpers
+from django.contrib.admin.models import PRIMARY_KEY_URL_SEPARATOR
 from django.contrib.admin.util import quote, unquote, flatten_fieldsets, get_deleted_objects
 from django.core.exceptions import PermissionDenied
 from django.db import models, transaction
@@ -15,6 +16,7 @@ from django.utils.safestring import mark_safe
 from django.utils.text import capfirst, get_text_list
 from django.utils.translation import ugettext as _
 from django.utils.encoding import force_unicode
+import itertools
 try:
     set
 except NameError:
@@ -190,11 +192,11 @@ class ModelAdmin(BaseModelAdmin):
         elif url == &quot;add&quot;:
             return self.add_view(request)
         elif url.endswith('/history'):
-            return self.history_view(request, unquote(url[:-8]))
+            return self.history_view(request, [unquote(part) for part in url[:-8].split(PRIMARY_KEY_URL_SEPARATOR)])
         elif url.endswith('/delete'):
-            return self.delete_view(request, unquote(url[:-7]))
+            return self.delete_view(request, [unquote(part) for part in url[:-7].split(PRIMARY_KEY_URL_SEPARATOR)])
         else:
-            return self.change_view(request, unquote(url))
+            return self.change_view(request, [unquote(part) for part in url.split(PRIMARY_KEY_URL_SEPARATOR)])
 
     def _media(self):
         from django.conf import settings
@@ -401,6 +403,8 @@ class ModelAdmin(BaseModelAdmin):
             'save_on_top': self.save_on_top,
             'root_path': self.admin_site.root_path,
         })
+        if context['has_absolute_url']:
+            context['absolute_url'] = obj.get_absolute_url()
         return render_to_response(self.change_form_template or [
             &quot;admin/%s/%s/change_form.html&quot; % (app_label, opts.object_name.lower()),
             &quot;admin/%s/change_form.html&quot; % app_label,
@@ -545,7 +549,8 @@ class ModelAdmin(BaseModelAdmin):
         opts = model._meta
 
         try:
-            obj = model._default_manager.get(pk=object_id)
+            kwargs = dict([(pk.name, val) for pk, val in itertools.izip(opts.pks, object_id)])
+            obj = model._default_manager.get(**kwargs)
         except model.DoesNotExist:
             # Don't raise Http404 just yet, because we haven't checked
             # permissions yet. We don't want an unauthenticated user to be able</diff>
      <filename>django/contrib/admin/options.py</filename>
    </modified>
    <modified>
      <diff>@@ -25,7 +25,7 @@
 {% block object-tools %}
 {% if change %}{% if not is_popup %}
   &lt;ul class=&quot;object-tools&quot;&gt;&lt;li&gt;&lt;a href=&quot;history/&quot; class=&quot;historylink&quot;&gt;{% trans &quot;History&quot; %}&lt;/a&gt;&lt;/li&gt;
-  {% if has_absolute_url %}&lt;li&gt;&lt;a href=&quot;../../../r/{{ content_type_id }}/{{ object_id }}/&quot; class=&quot;viewsitelink&quot;&gt;{% trans &quot;View on site&quot; %}&lt;/a&gt;&lt;/li&gt;{% endif%}
+  {% if has_absolute_url %}&lt;li&gt;&lt;a href=&quot;{{ absolute_url }}&quot; class=&quot;viewsitelink&quot;&gt;{% trans &quot;View on site&quot; %}&lt;/a&gt;&lt;/li&gt;{% endif%}
   &lt;/ul&gt;
 {% endif %}{% endif %}
 {% endblock %}</diff>
      <filename>django/contrib/admin/templates/admin/change_form.html</filename>
    </modified>
    <modified>
      <diff>@@ -1,6 +1,7 @@
 from django.conf import settings
 from django.contrib.admin.views.main import ALL_VAR, EMPTY_CHANGELIST_VALUE
 from django.contrib.admin.views.main import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, SEARCH_VAR
+from django.contrib.admin.models import PRIMARY_KEY_URL_SEPARATOR
 from django.core.exceptions import ObjectDoesNotExist
 from django.db import models
 from django.utils import dateformat
@@ -135,7 +136,6 @@ def _boolean_icon(field_val):
 
 def items_for_result(cl, result):
     first = True
-    pk = cl.lookup_opts.pk.attname
     for field_name in cl.list_display:
         row_class = ''
         try:
@@ -218,11 +218,13 @@ def items_for_result(cl, result):
             url = cl.url_for_result(result)
             # Convert the pk to something that can be used in Javascript.
             # Problem cases are long ints (23L) and non-ASCII strings.
+    
             if cl.to_field:
-                attr = str(cl.to_field)
+                attr = tuple(cl.to_field)
             else:
-                attr = pk
-            result_id = repr(force_unicode(getattr(result, attr)))[1:]
+                attr = cl.pk.attnames
+            
+            result_id = repr(PRIMARY_KEY_URL_SEPARATOR.join([force_unicode(getattr(result, pk)) for pk in attr]))[1:]
             yield mark_safe(u'&lt;%s%s&gt;&lt;a href=&quot;%s&quot;%s&gt;%s&lt;/a&gt;&lt;/%s&gt;' % \
                 (table_tag, row_class, url, (cl.is_popup and ' onclick=&quot;opener.dismissRelatedLookupPopup(window, %s); return false;&quot;' % result_id or ''), conditional_escape(result_repr), table_tag))
         else:</diff>
      <filename>django/contrib/admin/templatetags/admin_list.py</filename>
    </modified>
    <modified>
      <diff>@@ -96,7 +96,7 @@ def validate(cls, model):
     # list_select_related = False
     # save_as = False
     # save_on_top = False
-    for attr in ('list_select_related', 'save_as', 'save_on_top'):
+    for attr in ('save_as', 'save_on_top'):
         if not isinstance(getattr(cls, attr), bool):
             raise ImproperlyConfigured(&quot;'%s.%s' should be a boolean.&quot;
                     % (cls.__name__, attr))</diff>
      <filename>django/contrib/admin/validation.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,6 +1,7 @@
 from django.contrib.admin.filterspecs import FilterSpec
 from django.contrib.admin.options import IncorrectLookupParameters
 from django.contrib.admin.util import quote
+from django.contrib.admin.models import PRIMARY_KEY_URL_SEPARATOR
 from django.core.paginator import Paginator, InvalidPage
 from django.db import models
 from django.db.models.query import QuerySet
@@ -53,7 +54,7 @@ class ChangeList(object):
             self.page_num = 0
         self.show_all = ALL_VAR in request.GET
         self.is_popup = IS_POPUP_VAR in request.GET
-        self.to_field = request.GET.get(TO_FIELD_VAR)
+        self.to_field = request.GET.getlist(TO_FIELD_VAR)
         self.params = dict(request.GET.items())
         if PAGE_VAR in self.params:
             del self.params[PAGE_VAR]
@@ -68,7 +69,7 @@ class ChangeList(object):
         self.get_results(request)
         self.title = (self.is_popup and ugettext('Select %s') % force_unicode(self.opts.verbose_name) or ugettext('Select %s to change') % force_unicode(self.opts.verbose_name))
         self.filter_specs, self.has_filters = self.get_filters(request)
-        self.pk_attname = self.lookup_opts.pk.attname
+        self.pk = self.lookup_opts.pk
 
     def get_filters(self, request):
         filter_specs = []
@@ -135,7 +136,7 @@ class ChangeList(object):
         # options, then check the object's default ordering. If neither of
         # those exist, order descending by ID by default. Finally, look for
         # manually-specified ordering from the query string.
-        ordering = self.model_admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name]
+        ordering = self.model_admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.names[0]]
 
         if ordering[0].startswith('-'):
             order_field, order_type = ordering[0][1:], 'desc'
@@ -197,9 +198,12 @@ class ChangeList(object):
 
         # Use select_related() if one of the list_display options is a field
         # with a relationship.
-        if self.list_select_related:
+        if isinstance(self.list_select_related, (tuple, list)):
+            qs = qs.select_related(*self.list_select_related)
+        elif self.list_select_related:
             qs = qs.select_related()
         else:
+            fields = []
             for field_name in self.list_display:
                 try:
                     f = self.lookup_opts.get_field(field_name)
@@ -207,9 +211,10 @@ class ChangeList(object):
                     pass
                 else:
                     if isinstance(f.rel, models.ManyToOneRel):
-                        qs = qs.select_related()
-                        break
-
+                        fields.append(f.name)
+            if fields:
+                qs = qs.select_related(*fields)
+        
         # Set ordering.
         if self.order_field:
             qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))
@@ -243,4 +248,4 @@ class ChangeList(object):
         return qs
 
     def url_for_result(self, result):
-        return &quot;%s/&quot; % quote(getattr(result, self.pk_attname))
+        return &quot;%s/&quot; % PRIMARY_KEY_URL_SEPARATOR.join([quote(force_unicode(getattr(result, pk))) for pk in self.pk.attnames])</diff>
      <filename>django/contrib/admin/views/main.py</filename>
    </modified>
    <modified>
      <diff>@@ -8,6 +8,7 @@ from django.utils import dateformat
 from django.utils.text import capfirst
 from django.utils.translation import get_date_formats
 from django.utils.encoding import smart_unicode, smart_str, iri_to_uri
+from django.contrib.admin.models import PRIMARY_KEY_URL_SEPARATOR
 from django.utils.safestring import mark_safe
 from django.db.models.query import QuerySet
 
@@ -105,7 +106,7 @@ class EasyInstance(object):
         return self.instance._get_pk_val()
 
     def url(self):
-        return mark_safe('%s%s/%s/objects/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, iri_to_uri(self.pk())))
+        return mark_safe('%s%s/%s/objects/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, PRIMARY_KEY_URL_SEPARATOR.join(iri_to_uri(self.pk()))))
 
     def fields(self):
         &quot;&quot;&quot;</diff>
      <filename>django/contrib/databrowse/datastructures.py</filename>
    </modified>
    <modified>
      <diff>@@ -156,6 +156,191 @@ def sql_all(app, style):
     &quot;Returns a list of CREATE TABLE SQL, initial-data inserts, and CREATE INDEX SQL for the given module.&quot;
     return sql_create(app, style) + sql_custom(app, style) + sql_indexes(app, style)
 
+def sql_model_create(model, style, known_models=set()):
+    &quot;&quot;&quot;
+    Returns the SQL required to create a single model, as a tuple of:
+        (list_of_sql, pending_references_dict)
+    &quot;&quot;&quot;
+    from django.db import connection, models
+
+    opts = model._meta
+    final_output = []
+    table_output = []
+    pending_references = {}
+    qn = connection.ops.quote_name
+    inline_references = connection.features.inline_fk_references
+    for f in opts.local_fields:
+        col_type = f.db_type()
+        tablespace = f.db_tablespace or opts.db_tablespace
+        if col_type is None:
+            # Skip ManyToManyFields, because they're not represented as
+            # database columns in this table.
+            continue
+        # Make the definition (e.g. 'foo VARCHAR(30)') for this field.
+        field_output = [style.SQL_FIELD(qn(f.column)),
+            style.SQL_COLTYPE(col_type)]
+        field_output.append(style.SQL_KEYWORD('%sNULL' % (not f.null and 'NOT ' or '')))
+        # Why do you have to check f.primary_key also?
+        if f.unique and not f.primary_key:
+            field_output.append(style.SQL_KEYWORD('UNIQUE'))
+        if tablespace and connection.features.supports_tablespaces and f.unique:
+            # We must specify the index tablespace inline, because we
+            # won't be generating a CREATE INDEX statement for this field.
+            field_output.append(connection.ops.tablespace_sql(tablespace, inline=True))
+        if f.rel:
+            if inline_references and f.rel.to in known_models:
+                field_output.append(style.SQL_KEYWORD('REFERENCES') + ' ' + \
+                    style.SQL_TABLE(qn(f.rel.to._meta.db_table)) + ' (' + \
+                    style.SQL_FIELD(qn(f.rel.to._meta.get_field(f.rel.field_name).column)) + ')' +
+                    connection.ops.deferrable_sql()
+                )
+            else:
+                # We haven't yet created the table to which this field
+                # is related, so save it for later.
+                pr = pending_references.setdefault(f.rel.to, []).append((model, f))
+        table_output.append(' '.join(field_output))
+    if opts.order_with_respect_to:
+        table_output.append(style.SQL_FIELD(qn('_order')) + ' ' + \
+            style.SQL_COLTYPE(models.IntegerField().db_type()) + ' ' + \
+            style.SQL_KEYWORD('NULL'))
+    # Handle primary keys last to support multiples
+    table_output.append(style.SQL_KEYWORD('PRIMARY KEY') + ' (%s)' % \
+        &quot;, &quot;.join([style.SQL_FIELD(qn(f.column)) for f in opts.pks]))
+    for field_constraints in opts.unique_together:
+        table_output.append(style.SQL_KEYWORD('UNIQUE') + ' (%s)' % \
+            &quot;, &quot;.join([style.SQL_FIELD(qn(opts.get_field(f).column)) for f in field_constraints]))
+
+    full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' (']
+    for i, line in enumerate(table_output): # Combine and add commas.
+        full_statement.append('    %s%s' % (line, i &lt; len(table_output)-1 and ',' or ''))
+    full_statement.append(')')
+    if opts.db_tablespace and connection.features.supports_tablespaces:
+        full_statement.append(connection.ops.tablespace_sql(opts.db_tablespace))
+    full_statement.append(';')
+    final_output.append('\n'.join(full_statement))
+
+    if opts.has_auto_field:
+        # Add any extra SQL needed to support auto-incrementing primary keys.
+        auto_column = opts.auto_field.db_column or opts.auto_field.name
+        autoinc_sql = connection.ops.autoinc_sql(opts.db_table, auto_column)
+        if autoinc_sql:
+            for stmt in autoinc_sql:
+                final_output.append(stmt)
+
+    return final_output, pending_references
+
+def sql_for_pending_references(model, style, pending_references):
+    &quot;&quot;&quot;
+    Returns any ALTER TABLE statements to add constraints after the fact.
+    &quot;&quot;&quot;
+    from django.db import connection
+    from django.db.backends.util import truncate_name
+
+    qn = connection.ops.quote_name
+    final_output = []
+    if connection.features.supports_constraints:
+        opts = model._meta
+        if model in pending_references:
+            for rel_class, f in pending_references[model]:
+                rel_opts = rel_class._meta
+                r_table = rel_opts.db_table
+                r_col = f.column
+                table = opts.db_table
+                col = opts.get_field(f.rel.field_name).column
+                # For MySQL, r_name must be unique in the first 64 characters.
+                # So we are careful with character usage here.
+                r_name = '%s_refs_%s_%x' % (r_col, col, abs(hash((r_table, table))))
+                final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % \
+                    (qn(r_table), truncate_name(r_name, connection.ops.max_name_length()),
+                    qn(r_col), qn(table), qn(col),
+                    connection.ops.deferrable_sql()))
+            del pending_references[model]
+    return final_output
+
+def many_to_many_sql_for_model(model, style):
+    from django.db import connection, models
+    from django.contrib.contenttypes import generic
+    from django.db.backends.util import truncate_name
+
+    opts = model._meta
+    final_output = []
+    qn = connection.ops.quote_name
+    inline_references = connection.features.inline_fk_references
+    for f in opts.local_many_to_many:
+        if f.creates_table:
+            tablespace = f.db_tablespace or opts.db_tablespace
+            if tablespace and connection.features.supports_tablespaces: 
+                tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True)
+            else:
+                tablespace_sql = ''
+            table_output = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + \
+                style.SQL_TABLE(qn(f.m2m_db_table())) + ' (']
+            table_output.append('    %s %s %s%s,' %
+                (style.SQL_FIELD(qn('id')),
+                style.SQL_COLTYPE(models.AutoField(primary_key=True).db_type()),
+                style.SQL_KEYWORD('NOT NULL PRIMARY KEY'),
+                tablespace_sql))
+            if inline_references:
+                # TODO:
+                deferred = []
+                table_output.append('    %s %s %s %s (%s)%s,' %
+                    (style.SQL_FIELD(qn(f.m2m_column_name())),
+                    style.SQL_COLTYPE(models.ForeignKey(model).db_type()),
+                    style.SQL_KEYWORD('NOT NULL REFERENCES'),
+                    style.SQL_TABLE(qn(opts.db_table)),
+                    style.SQL_FIELD(qn(opts.pk.column)),
+                    connection.ops.deferrable_sql()))
+                table_output.append('    %s %s %s %s (%s)%s,' %
+                    (style.SQL_FIELD(qn(f.m2m_reverse_name())),
+                    style.SQL_COLTYPE(models.ForeignKey(f.rel.to).db_type()),
+                    style.SQL_KEYWORD('NOT NULL REFERENCES'),
+                    style.SQL_TABLE(qn(f.rel.to._meta.db_table)),
+                    style.SQL_FIELD(qn(f.rel.to._meta.pk.column)),
+                    connection.ops.deferrable_sql()))
+            else:
+                table_output.append('    %s %s %s,' %
+                    (style.SQL_FIELD(qn(f.m2m_column_name())),
+                    style.SQL_COLTYPE(models.ForeignKey(model).db_type()),
+                    style.SQL_KEYWORD('NOT NULL')))
+                table_output.append('    %s %s %s,' %
+                    (style.SQL_FIELD(qn(f.m2m_reverse_name())),
+                    style.SQL_COLTYPE(models.ForeignKey(f.rel.to).db_type()),
+                    style.SQL_KEYWORD('NOT NULL')))
+                deferred = [
+                    (f.m2m_db_table(), f.m2m_column_name(), opts.db_table,
+                        opts.pk.column),
+                    ( f.m2m_db_table(), f.m2m_reverse_name(),
+                        f.rel.to._meta.db_table, f.rel.to._meta.pk.column)
+                    ]
+            table_output.append('    %s (%s, %s)%s' %
+                (style.SQL_KEYWORD('UNIQUE'),
+                style.SQL_FIELD(qn(f.m2m_column_name())),
+                style.SQL_FIELD(qn(f.m2m_reverse_name())),
+                tablespace_sql))
+            table_output.append(')')
+            if opts.db_tablespace and connection.features.supports_tablespaces:
+                # f.db_tablespace is only for indices, so ignore its value here.
+                table_output.append(connection.ops.tablespace_sql(opts.db_tablespace))
+            table_output.append(';')
+            final_output.append('\n'.join(table_output))
+
+            for r_table, r_col, table, col in deferred:
+                r_name = '%s_refs_%s_%x' % (r_col, col,
+                        abs(hash((r_table, table))))
+                final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % 
+                (qn(r_table),
+                truncate_name(r_name, connection.ops.max_name_length()),
+                qn(r_col), qn(table), qn(col),
+                connection.ops.deferrable_sql()))
+
+            # Add any extra SQL needed to support auto-incrementing PKs
+            autoinc_sql = connection.ops.autoinc_sql(f.m2m_db_table(), 'id')
+            if autoinc_sql:
+                for stmt in autoinc_sql:
+                    final_output.append(stmt)
+
+    return final_output
+
 def custom_sql_for_model(model, style):
     from django.db import models
     from django.conf import settings</diff>
      <filename>django/core/management/sql.py</filename>
    </modified>
    <modified>
      <diff>@@ -23,6 +23,19 @@ def get_validation_errors(outfile, app=None):
     from django.db.models.loading import get_app_errors
     from django.db.models.fields.related import RelatedObject
 
+    def _check_field_tuple(opts, the_tuple, var_name):
+        &quot;&quot;&quot;Verifies that all fields in opts.var_name are valid.&quot;&quot;&quot;
+        for field_name in the_tuple:
+            try:
+                f = opts.get_field(field_name, many_to_many=True)
+            except models.FieldDoesNotExist:
+                e.add(opts, '&quot;%s&quot; refers to %s, a field that doesn\'t exist. Check your syntax.' % (var_name, field_name))
+            else:
+                if isinstance(f.rel, models.ManyToManyRel):
+                    e.add(opts, '&quot;%s&quot; refers to %s. ManyToManyFields are not supported in unique_together.' % (var_name, f.name))
+                if f not in opts.local_fields:
+                    e.add(opts, '&quot;%s&quot; refers to %s. This is not in the same model as the unique_together statement.' % (var_name, f.name))
+
     e = ModelErrorCollection(outfile)
 
     for (app_name, error) in get_app_errors().items():
@@ -33,7 +46,7 @@ def get_validation_errors(outfile, app=None):
 
         # Do field-specific validation.
         for f in opts.local_fields:
-            if f.name == 'id' and not f.primary_key and opts.pk.name == 'id':
+            if f.name == 'id' and not f.primary_key and 'id' not in [f.name for f in opts.pks]:
                 e.add(opts, '&quot;%s&quot;: You can\'t use &quot;id&quot; as a field name, because each model automatically gets an &quot;id&quot; field if none of the fields have primary_key=True. You need to either remove/rename your &quot;id&quot; field or add primary_key=True to a field.' % f.name)
             if f.name.endswith('_'):
                 e.add(opts, '&quot;%s&quot;: Field names cannot end with underscores, because this would lead to ambiguous queryset filters.' % f.name)
@@ -207,15 +220,9 @@ def get_validation_errors(outfile, app=None):
 
         # Check unique_together.
         for ut in opts.unique_together:
-            for field_name in ut:
-                try:
-                    f = opts.get_field(field_name, many_to_many=True)
-                except models.FieldDoesNotExist:
-                    e.add(opts, '&quot;unique_together&quot; refers to %s, a field that doesn\'t exist. Check your syntax.' % field_name)
-                else:
-                    if isinstance(f.rel, models.ManyToManyRel):
-                        e.add(opts, '&quot;unique_together&quot; refers to %s. ManyToManyFields are not supported in unique_together.' % f.name)
-                    if f not in opts.local_fields:
-                        e.add(opts, '&quot;unique_together&quot; refers to %s. This is not in the same model as the unique_together statement.' % f.name)
+            _check_field_tuple(opts, ut, 'unique_together')
+
+        # Check primary_key.
+        _check_field_tuple(opts, opts.primary_key, 'primary_key')
 
     return len(e.errors)</diff>
      <filename>django/core/management/validation.py</filename>
    </modified>
    <modified>
      <diff>@@ -40,7 +40,8 @@ class Serializer(base.Serializer):
     def handle_fk_field(self, obj, field):
         related = getattr(obj, field.name)
         if related is not None:
-            if field.rel.field_name == related._meta.pk.name:
+            # TODO: can we remove the field_name part?
+            if field.rel.field_name in related._meta.pk.names:
                 # Related to remote object via primary key
                 related = related._get_pk_val()
             else:</diff>
      <filename>django/core/serializers/python.py</filename>
    </modified>
    <modified>
      <diff>@@ -83,7 +83,8 @@ class Serializer(base.Serializer):
         self._start_relational_field(field)
         related = getattr(obj, field.name)
         if related is not None:
-            if field.rel.field_name == related._meta.pk.name:
+            # TODO: can we remove the field_name part?
+            if field.rel.field_name in related._meta.pk.names:
                 # Related to remote object via primary key
                 related = related._get_pk_val()
             else:</diff>
      <filename>django/core/serializers/xml_serializer.py</filename>
    </modified>
    <modified>
      <diff>@@ -48,9 +48,7 @@ class BaseDatabaseCreation(object):
             field_output = [style.SQL_FIELD(qn(f.column)),
                 style.SQL_COLTYPE(col_type)]
             field_output.append(style.SQL_KEYWORD('%sNULL' % (not f.null and 'NOT ' or '')))
-            if f.primary_key:
-                field_output.append(style.SQL_KEYWORD('PRIMARY KEY'))
-            elif f.unique:
+            if f.unique and not f.primary_key:
                 field_output.append(style.SQL_KEYWORD('UNIQUE'))
             if tablespace and f.unique:
                 # We must specify the index tablespace inline, because we
@@ -67,6 +65,8 @@ class BaseDatabaseCreation(object):
             table_output.append(style.SQL_FIELD(qn('_order')) + ' ' + \
                 style.SQL_COLTYPE(models.IntegerField().db_type()) + ' ' + \
                 style.SQL_KEYWORD('NULL'))
+        table_output.append(style.SQL_KEYWORD('PRIMARY KEY') + ' (%s)' % \
+            &quot;, &quot;.join([style.SQL_FIELD(qn(f.column)) for f in opts.pks]))
         for field_constraints in opts.unique_together:
             table_output.append(style.SQL_KEYWORD('UNIQUE') + ' (%s)' % \
                 &quot;, &quot;.join([style.SQL_FIELD(qn(opts.get_field(f).column)) for f in field_constraints]))</diff>
      <filename>django/db/backends/creation.py</filename>
    </modified>
    <modified>
      <diff>@@ -289,12 +289,29 @@ class Model(object):
     def _get_pk_val(self, meta=None):
         if not meta:
             meta = self._meta
-        return getattr(self, meta.pk.attname)
-
-    def _set_pk_val(self, value):
-        return setattr(self, self._meta.pk.attname, value)
+        if len(meta.pks) == 1:
+            return getattr(self, meta.pk.attname)
+        return self._get_pk_vals()
+    
+    def _get_pk_vals(self, meta=None):
+        if not meta:
+            meta = self._meta
+        return [getattr(self, f.attname) for f in meta.pks]
+
+    def _set_pk_val(self, values):
+        # Not quite sure why anyone would use a dictionary here,
+        # but it's supported to keep it in line with filter()
+        if isinstance(values, dict):
+            for key, val in value.iteritems():
+                setattr(self, key, val)
+        else:
+            if not hasattr(values, '__iter__') and len(self._meta.pks) == 1:
+                values = [values,]
+            for field_name, idx in izip(self._meta.pk.attnames, xrange(len(self._meta.pks))):
+                setattr(self, field_name, values[idx])
 
     pk = property(_get_pk_val, _set_pk_val)
+    pks = property(_get_pk_vals, _set_pk_val)
 
     def save(self, force_insert=False, force_update=False):
         &quot;&quot;&quot;
@@ -335,12 +352,18 @@ class Model(object):
         # that might have come from the parent class - we just save the
         # attributes we have been given to the class we have been given.
         if not raw:
+            # TODO: GAHHHHHHH
             for parent, field in meta.parents.items():
                 # At this point, parent's primary key field may be unknown
                 # (for example, from administration form which doesn't fill
                 # this field). If so, fill it.
-                if getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
-                    setattr(self, parent._meta.pk.attname, getattr(self, field.attname))
+
+                # TODO: test that this is good enough
+                if getattr(self, parent._meta.pks[0].attname) is None and getattr(self, field.attname) is None:
+                # if getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
+                    for f in parent._meta.pks:
+                        setattr(self, f.attname, getattr(self, field.attname))
+                    # setattr(self, parent._meta.pk.attname, getattr(self, field.attname))
 
                 self.save_base(raw, parent)
                 setattr(self, field.attname, self._get_pk_val(parent._meta))
@@ -348,8 +371,17 @@ class Model(object):
         non_pks = [f for f in meta.local_fields if not f.primary_key]
 
         # First, try an UPDATE. If that doesn't update anything, do an INSERT.
+
         pk_val = self._get_pk_val(meta)
-        pk_set = pk_val is not None
+        pk_set = bool(pk_val)
+        if pk_val:
+            if not hasattr(pk_val, '__iter__'):
+                pk_val = [pk_val]
+            for pk in pk_val:
+                if not pk:
+                    pk_set = False
+                    break
+
         record_exists = True
         manager = cls._default_manager
         if pk_set:
@@ -386,7 +418,13 @@ class Model(object):
                 result = manager._insert([(meta.pk, connection.ops.pk_default_value())], return_id=update_pk, raw_values=True)
 
             if update_pk:
-                setattr(self, meta.pk.attname, result)
+                # Find the AutoField
+                # TODO: this code is repeated in subqueries.py -- needs changed
+                attname = meta.pks[0].attname
+                for pk in meta.pks:
+                    if isinstance(pk, AutoField):
+                        attname = pk.attname
+                setattr(self, attname, result)
         transaction.commit_unless_managed()
 
         if signal:</diff>
      <filename>django/db/models/base.py</filename>
    </modified>
    <modified>
      <diff>@@ -334,7 +334,7 @@ class Field(object):
 class AutoField(Field):
     empty_strings_allowed = False
     def __init__(self, *args, **kwargs):
-        assert kwargs.get('primary_key', False) is True, &quot;%ss must have primary_key=True.&quot; % self.__class__.__name__
+        # assert kwargs.get('primary_key', False) is True, &quot;%ss must have primary_key=True.&quot; % self.__class__.__name__
         kwargs['blank'] = True
         Field.__init__(self, *args, **kwargs)
 </diff>
      <filename>django/db/models/fields/__init__.py</filename>
    </modified>
    <modified>
      <diff>@@ -23,6 +23,39 @@ DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering',
                  'order_with_respect_to', 'app_label', 'db_tablespace',
                  'abstract')
 
+class CompositePrimaryKey(list):
+    def __eq__(self, field_name):
+        return field_name in self.names
+    
+    def __repr__(self):
+        return '&lt;%s: %s&gt;' % (self.__class__.__name__, list.__repr__(self))
+
+    def __str__(self):
+        return self.name
+    
+    def __getattr__(self, key, value=None):
+        _reserved = ('names', 'attnames', 'append')
+        if key not in _reserved:
+            if len(self) == 1:
+                return getattr(self[0], key, value)
+            raise AttributeError, &quot;'%s' is not accessible on '%s' objects&quot; % (key, self.__class__.__name__,)
+        return list.__getattr__(self, key, value)
+    
+    def names(self):
+        return [f.name for f in self]
+    names = property(names)
+
+    def attnames(self):
+        return [f.attname for f in self]
+    attnames = property(attnames)
+
+    def append(self, field):
+        # Ensure the field is also marked with the primary_key attribute.
+        field.primary_key = True
+
+        if field not in self:
+            list.append(self, field)
+
 class Options(object):
     def __init__(self, meta, app_label=None):
         self.local_fields, self.local_many_to_many = [], []
@@ -40,6 +73,7 @@ class Options(object):
         self.admin = None
         self.meta = meta
         self.pk = None
+        self._primary_key = []
         self.has_auto_field, self.auto_field = False, None
         self.one_to_one_field = None
         self.abstract = False
@@ -59,10 +93,13 @@ class Options(object):
         self.object_name = cls.__name__
         self.module_name = self.object_name.lower()
         self.verbose_name = get_verbose_name(self.object_name)
-
+        self.pk = CompositePrimaryKey()
+        
         # Next, apply any overridden values from 'class Meta'.
         if self.meta:
             meta_attrs = self.meta.__dict__.copy()
+            # We have to delay setup of this because self.fields isn't populated yet
+            self._primary_key = meta_attrs.pop('primary_key', [])
             for name in self.meta.__dict__:
                 # Ignore any private attributes that Django doesn't care about.
                 # NOTE: We can't modify a dictionary's contents while looping
@@ -99,7 +136,6 @@ class Options(object):
             self.db_table = &quot;%s_%s&quot; % (self.app_label, self.module_name)
             self.db_table = truncate_name(self.db_table, connection.ops.max_name_length())
 
-
     def _prepare(self, model):
         if self.order_with_respect_to:
             self.order_with_respect_to = self.get_field(self.order_with_respect_to)
@@ -107,7 +143,13 @@ class Options(object):
         else:
             self.order_with_respect_to = None
 
-        if self.pk is None:
+
+        # Now we initialize the primary key.
+        for field_name in self._primary_key:
+            self.pk.append(self.get_field(field_name))
+        del self._primary_key
+
+        if not self.pk:
             if self.parents:
                 # Promote the first parent link in lieu of adding yet another
                 # field.
@@ -160,10 +202,19 @@ class Options(object):
         self.virtual_fields.append(field)
 
     def setup_pk(self, field):
-        if not self.pk and field.primary_key:
-            self.pk = field
+        if field.primary_key and field not in self.pk:
+            self.pk.append(field)
             field.serialize = False
 
+    def pks(self):
+        return tuple(self.pk)
+        self.primary_key = lambda x: x.pk.primary_key_names
+    pks = property(pks)
+    
+    def primary_key(self):
+        return self.pk.names
+    primary_key = property(primary_key)
+
     def __repr__(self):
         return '&lt;Options for %s&gt;' % self.object_name
 </diff>
      <filename>django/db/models/options.py</filename>
    </modified>
    <modified>
      <diff>@@ -488,7 +488,6 @@ class BaseQuery(object):
             table_alias = start_alias
         else:
             table_alias = self.tables[0]
-        root_pk = opts.pk.column
         seen = {None: table_alias}
         qn = self.quote_name_unless_alias
         qn2 = self.connection.ops.quote_name
@@ -498,7 +497,7 @@ class BaseQuery(object):
                 alias = seen[model]
             except KeyError:
                 alias = self.join((table_alias, model._meta.db_table,
-                        root_pk, model._meta.pk.column))
+                   opts.pk.column, model._meta.pk.column))
                 seen[model] = alias
             if as_pairs:
                 result.append((alias, field.column))
@@ -1127,6 +1126,26 @@ class BaseQuery(object):
         alias = self.get_initial_alias()
         allow_many = trim or not negate
 
+        # Ugly hack to replace any &quot;pk&quot; names with their actual names
+        # We could put this in setup_joins to magically handle pk, but we run
+        # into the issue of separating it into many filters, and we need the value
+        # available.
+        if parts[-1] == 'pk':
+            if not hasattr(value, '__iter__'):
+                value = [value]
+            parts = parts[:-1]
+            for part in parts:
+                if hasattr(opts.get_field_by_name(part)[0], 'rel'):
+                    opts = opts.get_field_by_name(part)[0].rel.to._meta
+                else:
+                    opts = opts.get_field_by_name(part)[0].opts
+            self.where.start_subtree(connector)
+            for pk, value in zip(opts.pk.names, value):
+                filter_expr = &quot;%s%s%s&quot; % (LOOKUP_SEP.join(parts + [pk]), LOOKUP_SEP, lookup_type), value
+                self.add_filter(filter_expr, AND, negate, trim, can_reuse, process_extras)
+            self.where.end_subtree()
+            return
+        
         try:
             field, target, opts, join_list, last, extra_filters = self.setup_joins(
                     parts, opts, alias, True, allow_many, can_reuse=can_reuse,
@@ -1295,6 +1314,7 @@ class BaseQuery(object):
         dupe_set = set()
         exclusions = set()
         extra_filters = []
+
         for pos, name in enumerate(names):
             try:
                 exclusions.add(int_alias)
@@ -1302,8 +1322,6 @@ class BaseQuery(object):
                 pass
             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)</diff>
      <filename>django/db/models/sql/query.py</filename>
    </modified>
    <modified>
      <diff>@@ -7,6 +7,7 @@ from django.db.models.sql.constants import *
 from django.db.models.sql.datastructures import Date
 from django.db.models.sql.query import Query
 from django.db.models.sql.where import AND
+from django.db.models.fields import AutoField
 
 __all__ = ['DeleteQuery', 'UpdateQuery', 'InsertQuery', 'DateQuery',
         'CountQuery']
@@ -308,8 +309,14 @@ class InsertQuery(Query):
     def execute_sql(self, return_id=False):
         cursor = super(InsertQuery, self).execute_sql(None)
         if return_id:
+            # Find the AutoField
+            meta = self.model._meta
+            column = meta.pks[0].column
+            for pk in meta.pks:
+                if isinstance(pk, AutoField):
+                    column = pk.column
             return self.connection.ops.last_insert_id(cursor,
-                    self.model._meta.db_table, self.model._meta.pk.column)
+                    meta.db_table, column)
 
     def insert_values(self, insert_values, raw_values=False):
         &quot;&quot;&quot;</diff>
      <filename>django/db/models/sql/subqueries.py</filename>
    </modified>
    <modified>
      <diff>@@ -44,7 +44,7 @@ class WhereNode(tree.Node):
 
         alias, col, field, lookup_type, value = data
         try:
-            if field:
+            if isinstance(field, Field):
                 params = field.get_db_prep_lookup(lookup_type, value)
                 db_type = field.db_type()
             else:</diff>
      <filename>django/db/models/sql/where.py</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>9ecc2bd086f419a87039257a11aa26f69449340a</id>
    </parent>
  </parents>
  <author>
    <name>David Cramer</name>
    <email>dcramer@david-cramers-macbook.local</email>
  </author>
  <url>http://github.com/dcramer/django-compositepks/commit/f93d45285d0689005009c55a65484ba4a43435be</url>
  <id>f93d45285d0689005009c55a65484ba4a43435be</id>
  <committed-date>2008-11-16T15:50:07-08:00</committed-date>
  <authored-date>2008-11-16T15:50:07-08:00</authored-date>
  <message>Initial working patch</message>
  <tree>82cb014ef30f4dc26a18c7bf9f14997ce99e41cb</tree>
  <committer>
    <name>David Cramer</name>
    <email>dcramer@david-cramers-macbook.local</email>
  </committer>
</commit>
