diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py index 549b2bfc62..8ae877b5fe 100644 --- a/django/db/migrations/operations/models.py +++ b/django/db/migrations/operations/models.py @@ -142,6 +142,17 @@ class CreateModel(ModelOperation): managers=self.managers, ), ] + elif isinstance(operation, FieldRelatedOptionOperation) and self.name_lower == operation.name_lower: + option_value = getattr(operation, operation.option_name) + return [ + CreateModel( + self.name, + fields=self.fields, + options={**self.options, **{operation.option_name: option_value}}, + bases=self.bases, + managers=self.managers, + ), + ] elif isinstance(operation, FieldOperation) and self.name_lower == operation.model_name_lower: if isinstance(operation, AddField): return [ @@ -167,6 +178,18 @@ class CreateModel(ModelOperation): ), ] elif isinstance(operation, RemoveField): + options = self.options.copy() + for option_name in ('unique_together', 'index_together'): + option = options.pop(option_name, None) + if option: + option = set(filter(bool, ( + tuple(f for f in fields if f != operation.name_lower) for fields in option + ))) + if option: + options[option_name] = option + order_with_respect_to = options.get('order_with_respect_to') + if order_with_respect_to == operation.name_lower: + del options['order_with_respect_to'] return [ CreateModel( self.name, @@ -175,12 +198,23 @@ class CreateModel(ModelOperation): for n, v in self.fields if n.lower() != operation.name_lower ], - options=self.options, + options=options, bases=self.bases, managers=self.managers, ), ] elif isinstance(operation, RenameField): + options = self.options.copy() + for option_name in ('unique_together', 'index_together'): + option = options.get(option_name) + if option: + options[option_name] = { + tuple(operation.new_name if f == operation.old_name else f for f in fields) + for fields in option + } + order_with_respect_to = options.get('order_with_respect_to') + if order_with_respect_to == operation.old_name: + options['order_with_respect_to'] = operation.new_name return [ CreateModel( self.name, @@ -188,7 +222,7 @@ class CreateModel(ModelOperation): (operation.new_name if n == operation.old_name else n, v) for n, v in self.fields ], - options=self.options, + options=options, bases=self.bases, managers=self.managers, ), @@ -568,6 +602,8 @@ class AlterIndexTogether(FieldRelatedOptionOperation): class AlterOrderWithRespectTo(FieldRelatedOptionOperation): """Represent a change with the order_with_respect_to option.""" + option_name = 'order_with_respect_to' + def __init__(self, name, order_with_respect_to): self.order_with_respect_to = order_with_respect_to super().__init__(name) diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index b181b084be..02b95b5458 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -2075,8 +2075,10 @@ class AutodetectorTests(TestCase): changes = self.get_changes([], [self.book, self.author_with_book_order_wrt]) # Right number/type of migrations? self.assertNumberMigrations(changes, 'testapp', 1) - self.assertOperationTypes(changes, 'testapp', 0, ["CreateModel", "AlterOrderWithRespectTo"]) - self.assertOperationAttributes(changes, 'testapp', 0, 1, name="author", order_with_respect_to="book") + self.assertOperationTypes(changes, 'testapp', 0, ["CreateModel"]) + self.assertOperationAttributes( + changes, 'testapp', 0, 0, name="Author", options={'order_with_respect_to': 'book'} + ) self.assertNotIn("_order", [name for name, field in changes['testapp'][0].operations[0].fields]) def test_alter_model_managers(self): diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index 3c42755917..3408f2fefa 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -1335,7 +1335,7 @@ class SquashMigrationsTests(MigrationTestBase): out = io.StringIO() with self.temporary_migration_module(module="migrations.test_migrations"): call_command("squashmigrations", "migrations", "0002", interactive=False, verbosity=1, stdout=out) - self.assertIn("Optimized from 8 operations to 4 operations.", out.getvalue()) + self.assertIn("Optimized from 8 operations to 2 operations.", out.getvalue()) def test_ticket_23799_squashmigrations_no_optimize(self): """ diff --git a/tests/migrations/test_optimizer.py b/tests/migrations/test_optimizer.py index b841b531b1..b2c7b062c6 100644 --- a/tests/migrations/test_optimizer.py +++ b/tests/migrations/test_optimizer.py @@ -594,8 +594,11 @@ class OptimizerTests(SimpleTestCase): def _test_create_alter_foo_field(self, alter): """ CreateModel, AlterFooTogether/AlterOrderWithRespectTo followed by an - add/alter/rename field should optimize to CreateModel and the Alter* + add/alter/rename field should optimize to CreateModel with options. """ + option_value = getattr(alter, alter.option_name) + options = {alter.option_name: option_value} + # AddField self.assertOptimizesTo( [ @@ -611,13 +614,12 @@ class OptimizerTests(SimpleTestCase): ("a", models.IntegerField()), ("b", models.IntegerField()), ("c", models.IntegerField()), - ]), - alter, + ], options=options), ], ) # AlterField - self.assertDoesNotOptimize( + self.assertOptimizesTo( [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), @@ -626,6 +628,12 @@ class OptimizerTests(SimpleTestCase): alter, migrations.AlterField("Foo", "b", models.CharField(max_length=255)), ], + [ + migrations.CreateModel("Foo", [ + ("a", models.IntegerField()), + ("b", models.CharField(max_length=255)), + ], options=options), + ], ) self.assertOptimizesTo( @@ -643,13 +651,20 @@ class OptimizerTests(SimpleTestCase): ("a", models.IntegerField()), ("b", models.IntegerField()), ("c", models.CharField(max_length=255)), - ]), - alter, + ], options=options), ], ) # RenameField - self.assertDoesNotOptimize( + if isinstance(option_value, str): + renamed_options = {alter.option_name: 'c'} + else: + renamed_options = { + alter.option_name: { + tuple('c' if value == 'b' else value for value in item) for item in option_value + } + } + self.assertOptimizesTo( [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), @@ -658,6 +673,12 @@ class OptimizerTests(SimpleTestCase): alter, migrations.RenameField("Foo", "b", "c"), ], + [ + migrations.CreateModel("Foo", [ + ("a", models.IntegerField()), + ("c", models.IntegerField()), + ], options=renamed_options), + ], ) self.assertOptimizesTo( @@ -673,10 +694,8 @@ class OptimizerTests(SimpleTestCase): [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), - ("b", models.IntegerField()), - ]), - alter, - migrations.RenameField("Foo", "b", "c"), + ("c", models.IntegerField()), + ], options=renamed_options), ], ) @@ -695,13 +714,20 @@ class OptimizerTests(SimpleTestCase): ("a", models.IntegerField()), ("b", models.IntegerField()), ("d", models.IntegerField()), - ]), - alter, + ], options=options), ], ) # RemoveField - self.assertDoesNotOptimize( + if isinstance(option_value, str): + removed_options = None + else: + removed_options = { + alter.option_name: { + tuple(value for value in item if value != 'b') for item in option_value + } + } + self.assertOptimizesTo( [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), @@ -710,9 +736,14 @@ class OptimizerTests(SimpleTestCase): alter, migrations.RemoveField("Foo", "b"), ], + [ + migrations.CreateModel("Foo", [ + ("a", models.IntegerField()), + ], options=removed_options), + ] ) - self.assertDoesNotOptimize( + self.assertOptimizesTo( [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), @@ -722,6 +753,12 @@ class OptimizerTests(SimpleTestCase): alter, migrations.RemoveField("Foo", "c"), ], + [ + migrations.CreateModel("Foo", [ + ("a", models.IntegerField()), + ("b", models.IntegerField()), + ], options=options), + ], ) def test_create_alter_unique_field(self):