diff --git a/django/db/migrations/operations/base.py b/django/db/migrations/operations/base.py index 3bd9546bd7..61e3b61f43 100644 --- a/django/db/migrations/operations/base.py +++ b/django/db/migrations/operations/base.py @@ -146,6 +146,9 @@ class Operation: return router.allow_migrate_model(connection_alias, model) + def reduce_related(self, operation, app_label): + return None + def reduce(self, operation, app_label): """ Return either a list of operations the actual operation should be diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py index 9aad9c809e..586a92b2e0 100644 --- a/django/db/migrations/operations/models.py +++ b/django/db/migrations/operations/models.py @@ -134,6 +134,43 @@ class CreateModel(ModelOperation): return True return False + def reduce_related(self, operation, app_label): + if isinstance(operation, RenameModel): + impacted_fields = [ + (_, field) + for _, field in self.fields + if field.remote_field + and field.remote_field.model + == f"{app_label}.{operation.old_name_lower}" + ] + if len(impacted_fields) == 0: + return [self] + + not_impacted_fields = [ + (_, field) + for (_, field) in self.fields + if (_, field) not in impacted_fields + ] + + fixed_fields = [] + + for _, impacted_field in impacted_fields: + name, path, args, kwargs = impacted_field.deconstruct() + kwargs["to"] = f"{app_label}.{operation.new_name_lower}" + impacted_field = impacted_field.__class__(*args, **kwargs) + fixed_fields.append((_, impacted_field)) + + return [ + CreateModel( + name=self.name, + fields=not_impacted_fields + fixed_fields, + options=self.options, + bases=self.bases, + managers=self.managers, + ), + ] + return super().reduce_related(operation, app_label) + def reduce(self, operation, app_label): if ( isinstance(operation, DeleteModel) diff --git a/django/db/migrations/optimizer.py b/django/db/migrations/optimizer.py index 7e5dea2377..9c82cb9c3c 100644 --- a/django/db/migrations/optimizer.py +++ b/django/db/migrations/optimizer.py @@ -46,6 +46,7 @@ class MigrationOptimizer: for j, other in enumerate(operations[i + 1 :]): result = operation.reduce(other, app_label) if isinstance(result, list): + new_reduced_operations = [] in_between = operations[i + 1 : i + j + 1] if right: new_operations.extend(in_between) @@ -59,8 +60,13 @@ class MigrationOptimizer: # Otherwise keep trying. new_operations.append(operation) break - new_operations.extend(operations[i + j + 2 :]) - return new_operations + + for _, op in enumerate(new_operations): + new_reduced_operations.extend( + op.reduce_related(other, app_label) or [op] + ) + new_reduced_operations.extend(operations[i + j + 2 :]) + return new_reduced_operations elif not result: # Can't perform a right reduction. right = False diff --git a/tests/migrations/test_optimizer.py b/tests/migrations/test_optimizer.py index 2acbc7f09f..f017441f00 100644 --- a/tests/migrations/test_optimizer.py +++ b/tests/migrations/test_optimizer.py @@ -649,6 +649,35 @@ class OptimizerTests(SimpleTestCase): ], ) + def test_rename_model_referenced_by_fk(self): + self.assertOptimizesTo( + [ + migrations.CreateModel("Author", []), + migrations.CreateModel( + "Book", + [ + ( + "author", + models.ForeignKey("migrations.author", models.CASCADE), + ), + ], + ), + migrations.RenameModel("Author", "Person"), + ], + [ + migrations.CreateModel("Person", []), + migrations.CreateModel( + "Book", + [ + ( + "author", + models.ForeignKey("migrations.person", models.CASCADE), + ), + ], + ), + ], + ) + def test_create_model_alter_field(self): """ AlterField should optimize into CreateModel.