diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py index 16cadc7a2fb..8019bd72dc8 100644 --- a/django/db/migrations/operations/models.py +++ b/django/db/migrations/operations/models.py @@ -777,7 +777,10 @@ class AddIndex(IndexOperation): def state_forwards(self, app_label, state): model_state = state.models[app_label, self.model_name_lower] - model_state.options[self.option_name].append(self.index) + indexes = list(model_state.options[self.option_name]) + indexes.append(self.index.clone()) + model_state.options[self.option_name] = indexes + state.reload_model(app_label, self.model_name_lower, delay=True) def database_forwards(self, app_label, schema_editor, from_state, to_state): model = to_state.apps.get_model(app_label, self.model_name) @@ -821,6 +824,7 @@ class RemoveIndex(IndexOperation): model_state = state.models[app_label, self.model_name_lower] indexes = model_state.options[self.option_name] model_state.options[self.option_name] = [idx for idx in indexes if idx.name != self.name] + state.reload_model(app_label, self.model_name_lower, delay=True) def database_forwards(self, app_label, schema_editor, from_state, to_state): model = from_state.apps.get_model(app_label, self.model_name) diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index e2253d74c4a..0ba8eea89f0 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -584,6 +584,9 @@ class ModelState(object): app_label=self.app_label, name=self.name, fields=list(self.fields), + # Since options are shallow-copied here, operations such as + # AddIndex must replace their option (e.g 'indexes') rather + # than mutating it. options=dict(self.options), bases=self.bases, managers=list(self.managers), diff --git a/docs/releases/1.11.1.txt b/docs/releases/1.11.1.txt index 1d0b70f76b9..b396ec70ae6 100644 --- a/docs/releases/1.11.1.txt +++ b/docs/releases/1.11.1.txt @@ -69,3 +69,6 @@ Bugfixes * Fixed crash in ``BaseGeometryWidget.get_context()`` when overriding existing ``attrs`` (:ticket:`28105`). + +* Prevented ``AddIndex`` and ``RemoveIndex`` from mutating model state + (:ticket:`28043`). diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index e1daabe8b52..dd8aff4ca87 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -1491,6 +1491,29 @@ class OperationTests(OperationTestBase): self.unapply_operations("test_rmin", project_state, operations=operations) self.assertIndexExists("test_rmin_pony", ["pink", "weight"]) + def test_add_index_state_forwards(self): + project_state = self.set_up_test_model('test_adinsf') + index = models.Index(fields=['pink'], name='test_adinsf_pony_pink_idx') + old_model = project_state.apps.get_model('test_adinsf', 'Pony') + new_state = project_state.clone() + + operation = migrations.AddIndex('Pony', index) + operation.state_forwards('test_adinsf', new_state) + new_model = new_state.apps.get_model('test_adinsf', 'Pony') + self.assertIsNot(old_model, new_model) + + def test_remove_index_state_forwards(self): + project_state = self.set_up_test_model('test_rminsf') + index = models.Index(fields=['pink'], name='test_rminsf_pony_pink_idx') + migrations.AddIndex('Pony', index).state_forwards('test_rminsf', project_state) + old_model = project_state.apps.get_model('test_rminsf', 'Pony') + new_state = project_state.clone() + + operation = migrations.RemoveIndex('Pony', 'test_rminsf_pony_pink_idx') + operation.state_forwards('test_rminsf', new_state) + new_model = new_state.apps.get_model('test_rminsf', 'Pony') + self.assertIsNot(old_model, new_model) + def test_alter_field_with_index(self): """ Test AlterField operation with an index to ensure indexes created via