Commit 4285571c authored by Alex Gaynor's avatar Alex Gaynor
Browse files

Fixed #5805 -- it is now possible to specify multi-column indexes. Thanks to...

Fixed #5805 -- it is now possible to specify multi-column indexes. Thanks to jgelens for the original patch.
parent 249c3d73
Loading
Loading
Loading
Loading
+25 −10
Original line number Diff line number Diff line
import collections
import sys

from django.conf import settings
@@ -327,15 +328,29 @@ def get_validation_errors(outfile, app=None):

        # Check unique_together.
        for ut in opts.unique_together:
            for field_name in ut:
            validate_local_fields(e, opts, "unique_together", ut)
        if not isinstance(opts.index_together, collections.Sequence):
            e.add(opts, '"index_together" must a sequence')
        else:
            for it in opts.index_together:
                validate_local_fields(e, opts, "index_together", it)

    return len(e.errors)


def validate_local_fields(e, opts, field_name, fields):
    from django.db import models

    if not isinstance(fields, collections.Sequence):
        e.add(opts, 'all %s elements must be sequences' % field_name)
    else:
        for field in fields:
            try:
                    f = opts.get_field(field_name, many_to_many=True)
                f = opts.get_field(field, many_to_many=True)
            except models.FieldDoesNotExist:
                    e.add(opts, '"unique_together" refers to %s, a field that doesn\'t exist. Check your syntax.' % field_name)
                e.add(opts, '"%s" refers to %s, a field that doesn\'t exist.' % (field_name, field))
            else:
                if isinstance(f.rel, models.ManyToManyRel):
                        e.add(opts, '"unique_together" refers to %s. ManyToManyFields are not supported in unique_together.' % f.name)
                    e.add(opts, '"%s" refers to %s. ManyToManyFields are not supported in %s.' % (field_name, f.name, field_name))
                if f not in opts.local_fields:
                        e.add(opts, '"unique_together" refers to %s. This is not in the same model as the unique_together statement.' % f.name)

    return len(e.errors)
                    e.add(opts, '"%s" refers to %s. This is not in the same model as the %s statement.' % (field_name, f.name, field_name))
+32 −19
Original line number Diff line number Diff line
@@ -177,34 +177,47 @@ class BaseDatabaseCreation(object):
        output = []
        for f in model._meta.local_fields:
            output.extend(self.sql_indexes_for_field(model, f, style))
        for fs in model._meta.index_together:
            fields = [model._meta.get_field_by_name(f)[0] for f in fs]
            output.extend(self.sql_indexes_for_fields(model, fields, style))
        return output

    def sql_indexes_for_field(self, model, f, style):
        """
        Return the CREATE INDEX SQL statements for a single model field.
        """
        from django.db.backends.util import truncate_name

        if f.db_index and not f.unique:
            qn = self.connection.ops.quote_name
            tablespace = f.db_tablespace or model._meta.db_tablespace
            if tablespace:
                tablespace_sql = self.connection.ops.tablespace_sql(tablespace)
                if tablespace_sql:
                    tablespace_sql = ' ' + tablespace_sql
            return self.sql_indexes_for_fields(model, [f], style)
        else:
                tablespace_sql = ''
            i_name = '%s_%s' % (model._meta.db_table, self._digest(f.column))
            output = [style.SQL_KEYWORD('CREATE INDEX') + ' ' +
                style.SQL_TABLE(qn(truncate_name(
                    i_name, self.connection.ops.max_name_length()))) + ' ' +
                style.SQL_KEYWORD('ON') + ' ' +
                style.SQL_TABLE(qn(model._meta.db_table)) + ' ' +
                "(%s)" % style.SQL_FIELD(qn(f.column)) +
                "%s;" % tablespace_sql]
            return []

    def sql_indexes_for_fields(self, model, fields, style):
        from django.db.backends.util import truncate_name

        if len(fields) == 1 and fields[0].db_tablespace:
            tablespace_sql = self.connection.ops.tablespace_sql(fields[0].db_tablespace)
        elif model._meta.db_tablespace:
            tablespace_sql = self.connection.ops.tablespace_sql(model._meta.db_tablespace)
        else:
            output = []
        return output
            tablespace_sql = ""
        if tablespace_sql:
            tablespace_sql = " " + tablespace_sql

        field_names = []
        qn = self.connection.ops.quote_name
        for f in fields:
            field_names.append(style.SQL_FIELD(qn(f.column)))

        index_name = "%s_%s" % (model._meta.db_table, self._digest([f.name for f in fields]))

        return [
            style.SQL_KEYWORD("CREATE INDEX") + " " +
            style.SQL_TABLE(qn(truncate_name(index_name, self.connection.ops.max_name_length()))) + " " +
            style.SQL_KEYWORD("ON") + " " +
            style.SQL_TABLE(qn(model._meta.db_table)) + " " +
            "(%s)" % style.SQL_FIELD(", ".join(field_names)) +
            "%s;" % tablespace_sql,
        ]

    def sql_destroy_model(self, model, references_to_delete, style):
        """
+3 −1
Original line number Diff line number Diff line
@@ -21,7 +21,8 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|
DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
                 'unique_together', 'permissions', 'get_latest_by',
                 'order_with_respect_to', 'app_label', 'db_tablespace',
                 'abstract', 'managed', 'proxy', 'swappable', 'auto_created')
                 'abstract', 'managed', 'proxy', 'swappable', 'auto_created',
                 'index_together')


@python_2_unicode_compatible
@@ -34,6 +35,7 @@ class Options(object):
        self.db_table = ''
        self.ordering = []
        self.unique_together = []
        self.index_together = []
        self.permissions = []
        self.object_name, self.app_label = None, app_label
        self.get_latest_by = None
+15 −0
Original line number Diff line number Diff line
@@ -261,6 +261,21 @@ Django quotes column and table names behind the scenes.
    :class:`~django.db.models.ManyToManyField`, try using a signal or
    an explicit :attr:`through <ManyToManyField.through>` model.

``index_together``

.. versionadded:: 1.5

.. attribute:: Options.index_together

    Sets of field names that, taken together, are indexed::

        index_together = [
            ["pub_date", "deadline"],
        ]

    This list of fields will be indexed together (i.e. the appropriate
    ``CREATE INDEX`` statement will be issued.)

``verbose_name``
----------------

+8 −0
Original line number Diff line number Diff line
@@ -356,6 +356,13 @@ class HardReferenceModel(models.Model):
    m2m_4 = models.ManyToManyField('invalid_models.SwappedModel', related_name='m2m_hardref4')


class BadIndexTogether1(models.Model):
    class Meta:
        index_together = [
            ["field_that_does_not_exist"],
        ]


model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer.
invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer.
invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer.
@@ -470,6 +477,7 @@ invalid_models.hardreferencemodel: 'm2m_3' defines a relation with the model 'in
invalid_models.hardreferencemodel: 'm2m_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL.
invalid_models.badswappablevalue: TEST_SWAPPED_MODEL_BAD_VALUE is not of the form 'app_label.app_name'.
invalid_models.badswappablemodel: Model has been swapped out for 'not_an_app.Target' which has not been installed or is abstract.
invalid_models.badindextogether1: "index_together" refers to field_that_does_not_exist, a field that doesn't exist.
"""

if not connection.features.interprets_empty_strings_as_nulls:
Loading