Commit ae635cc3 authored by Claude Paroz's avatar Claude Paroz
Browse files

Fixed #24757 -- Recreated MySQL index when needed during combined index removal

Thanks Thomas Recouvreux for the report and Tim Graham for the tests and
review.
parent 0eaef8e5
Loading
Loading
Loading
Loading
+13 −18
Original line number Diff line number Diff line
@@ -315,15 +315,7 @@ class BaseDatabaseSchemaEditor(object):
        news = set(tuple(fields) for fields in new_unique_together)
        # Deleted uniques
        for fields in olds.difference(news):
            columns = [model._meta.get_field(field).column for field in fields]
            constraint_names = self._constraint_names(model, columns, unique=True)
            if len(constraint_names) != 1:
                raise ValueError("Found wrong number (%s) of constraints for %s(%s)" % (
                    len(constraint_names),
                    model._meta.db_table,
                    ", ".join(columns),
                ))
            self.execute(self._delete_constraint_sql(self.sql_delete_unique, model, constraint_names[0]))
            self._delete_composed_index(model, fields, {'unique': True}, self.sql_delete_unique)
        # Created uniques
        for fields in news.difference(olds):
            columns = [model._meta.get_field(field).column for field in fields]
@@ -339,19 +331,22 @@ class BaseDatabaseSchemaEditor(object):
        news = set(tuple(fields) for fields in new_index_together)
        # Deleted indexes
        for fields in olds.difference(news):
            self._delete_composed_index(model, fields, {'index': True}, self.sql_delete_index)
        # Created indexes
        for field_names in news.difference(olds):
            fields = [model._meta.get_field(field) for field in field_names]
            self.execute(self._create_index_sql(model, fields, suffix="_idx"))

    def _delete_composed_index(self, model, fields, constraint_kwargs, sql):
        columns = [model._meta.get_field(field).column for field in fields]
            constraint_names = self._constraint_names(model, list(columns), index=True)
        constraint_names = self._constraint_names(model, columns, **constraint_kwargs)
        if len(constraint_names) != 1:
            raise ValueError("Found wrong number (%s) of constraints for %s(%s)" % (
                len(constraint_names),
                model._meta.db_table,
                ", ".join(columns),
            ))
            self.execute(self._delete_constraint_sql(self.sql_delete_index, model, constraint_names[0]))
        # Created indexes
        for field_names in news.difference(olds):
            fields = [model._meta.get_field(field) for field in field_names]
            self.execute(self._create_index_sql(model, fields, suffix="_idx"))
        self.execute(self._delete_constraint_sql(sql, model, constraint_names[0]))

    def alter_db_table(self, model, old_db_table, new_db_table):
        """
+18 −0
Original line number Diff line number Diff line
@@ -62,6 +62,24 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
                    field.db_index = False
        return super(DatabaseSchemaEditor, self)._model_indexes_sql(model)

    def _delete_composed_index(self, model, fields, *args):
        """
        MySQL can remove an implicit FK index on a field when that field is
        covered by another index like a unique_together. "covered" here means
        that the more complex index starts like the simpler one.
        http://bugs.mysql.com/bug.php?id=37910 / Django ticket #24757
        We check here before removing the [unique|index]_together if we have to
        recreate a FK index.
        """
        first_field = model._meta.get_field(fields[0])
        if first_field.get_internal_type() == 'ForeignKey':
            constraint_names = self._constraint_names(model, fields[0], index=True)
            if not constraint_names:
                self.execute(
                    self._create_index_sql(model, [model._meta.get_field(fields[0])], suffix="")
                )
        return super(DatabaseSchemaEditor, self)._delete_composed_index(model, fields, *args)

    def _alter_column_type_sql(self, table, old_field, new_field, new_type):
        # Keep null property of old field, if it has changed, it will be handled separately
        if old_field.null:
+3 −0
Original line number Diff line number Diff line
@@ -27,3 +27,6 @@ Bugfixes
  :ticket:`24712`).

* Fixed ``isnull`` lookup for ``HStoreField`` (:ticket:`24751`).

* Fixed a MySQL crash when a migration removes a combined index (unique_together
  or index_together) containing a foreign key (:ticket:`24757`).
+36 −0
Original line number Diff line number Diff line
@@ -1084,6 +1084,24 @@ class SchemaTests(TransactionTestCase):
        self.assertRaises(IntegrityError, UniqueTest.objects.create, year=2012, slug="foo")
        UniqueTest.objects.all().delete()

    def test_unique_together_with_fk(self):
        """
        Tests removing and adding unique_together constraints that include
        a foreign key.
        """
        # Create the table
        with connection.schema_editor() as editor:
            editor.create_model(Author)
            editor.create_model(Book)
        # Ensure the fields are unique to begin with
        self.assertEqual(Book._meta.unique_together, ())
        # Add the unique_together constraint
        with connection.schema_editor() as editor:
            editor.alter_unique_together(Book, [], [['author', 'title']])
        # Alter it back
        with connection.schema_editor() as editor:
            editor.alter_unique_together(Book, [['author', 'title']], [])

    def test_index_together(self):
        """
        Tests removing and adding index_together constraints on a model.
@@ -1127,6 +1145,24 @@ class SchemaTests(TransactionTestCase):
            ),
        )

    def test_index_together_with_fk(self):
        """
        Tests removing and adding index_together constraints that include
        a foreign key.
        """
        # Create the table
        with connection.schema_editor() as editor:
            editor.create_model(Author)
            editor.create_model(Book)
        # Ensure the fields are unique to begin with
        self.assertEqual(Book._meta.index_together, ())
        # Add the unique_together constraint
        with connection.schema_editor() as editor:
            editor.alter_index_together(Book, [], [['author', 'title']])
        # Alter it back
        with connection.schema_editor() as editor:
            editor.alter_index_together(Book, [['author', 'title']], [])

    def test_create_index_together(self):
        """
        Tests creating models with index_together already defined