from django.db import migrations, models from django.db.migrations import operations from django.db.migrations.optimizer import MigrationOptimizer from django.db.migrations.serializer import serializer_factory from django.test import SimpleTestCase from .models import EmptyManager, UnicodeModel class OptimizerTests(SimpleTestCase): """ Tests the migration autodetector. """ def optimize(self, operations, app_label): """ Handy shortcut for getting results + number of loops """ optimizer = MigrationOptimizer() return optimizer.optimize(operations, app_label), optimizer._iterations def serialize(self, value): return serializer_factory(value).serialize()[0] def assertOptimizesTo(self, operations, expected, exact=None, less_than=None, app_label=None): result, iterations = self.optimize(operations, app_label or 'migrations') result = [self.serialize(f) for f in result] expected = [self.serialize(f) for f in expected] self.assertEqual(expected, result) if exact is not None and iterations != exact: raise self.failureException( "Optimization did not take exactly %s iterations (it took %s)" % (exact, iterations) ) if less_than is not None and iterations >= less_than: raise self.failureException( "Optimization did not take less than %s iterations (it took %s)" % (less_than, iterations) ) def assertDoesNotOptimize(self, operations, **kwargs): self.assertOptimizesTo(operations, operations, **kwargs) def test_none_app_label(self): optimizer = MigrationOptimizer() with self.assertRaisesMessage(TypeError, 'app_label must be a str'): optimizer.optimize([], None) def test_single(self): """ The optimizer does nothing on a single operation, and that it does it in just one pass. """ self.assertOptimizesTo( [migrations.DeleteModel("Foo")], [migrations.DeleteModel("Foo")], exact=1, ) def test_create_delete_model(self): """ CreateModel and DeleteModel should collapse into nothing. """ self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.DeleteModel("Foo"), ], [], ) def test_create_rename_model(self): """ CreateModel should absorb RenameModels. """ managers = [('objects', EmptyManager())] self.assertOptimizesTo( [ migrations.CreateModel( name="Foo", fields=[("name", models.CharField(max_length=255))], options={'verbose_name': 'Foo'}, bases=(UnicodeModel,), managers=managers, ), migrations.RenameModel("Foo", "Bar"), ], [ migrations.CreateModel( "Bar", [("name", models.CharField(max_length=255))], options={'verbose_name': 'Foo'}, bases=(UnicodeModel,), managers=managers, ) ], ) def test_rename_model_self(self): """ RenameModels should absorb themselves. """ self.assertOptimizesTo( [ migrations.RenameModel("Foo", "Baa"), migrations.RenameModel("Baa", "Bar"), ], [ migrations.RenameModel("Foo", "Bar"), ], ) def test_create_alter_model_options(self): self.assertOptimizesTo( [ migrations.CreateModel('Foo', fields=[]), migrations.AlterModelOptions(name='Foo', options={'verbose_name_plural': 'Foozes'}), ], [ migrations.CreateModel('Foo', fields=[], options={'verbose_name_plural': 'Foozes'}), ] ) def _test_create_alter_foo_delete_model(self, alter_foo): """ CreateModel, AlterModelTable, AlterUniqueTogether/AlterIndexTogether/ AlterOrderWithRespectTo, and DeleteModel should collapse into nothing. """ self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.AlterModelTable("Foo", "woohoo"), alter_foo, migrations.DeleteModel("Foo"), ], [], ) def test_create_alter_unique_delete_model(self): self._test_create_alter_foo_delete_model(migrations.AlterUniqueTogether("Foo", [["a", "b"]])) def test_create_alter_index_delete_model(self): self._test_create_alter_foo_delete_model(migrations.AlterIndexTogether("Foo", [["a", "b"]])) def test_create_alter_owrt_delete_model(self): self._test_create_alter_foo_delete_model(migrations.AlterOrderWithRespectTo("Foo", "a")) def _test_alter_alter_model(self, alter_foo, alter_bar): """ Two AlterUniqueTogether/AlterIndexTogether/AlterOrderWithRespectTo should collapse into the second. """ self.assertOptimizesTo( [ alter_foo, alter_bar, ], [ alter_bar, ], ) def test_alter_alter_table_model(self): self._test_alter_alter_model( migrations.AlterModelTable("Foo", "a"), migrations.AlterModelTable("Foo", "b"), ) def test_alter_alter_unique_model(self): self._test_alter_alter_model( migrations.AlterUniqueTogether("Foo", [["a", "b"]]), migrations.AlterUniqueTogether("Foo", [["a", "c"]]), ) def test_alter_alter_index_model(self): self._test_alter_alter_model( migrations.AlterIndexTogether("Foo", [["a", "b"]]), migrations.AlterIndexTogether("Foo", [["a", "c"]]), ) def test_alter_alter_owrt_model(self): self._test_alter_alter_model( migrations.AlterOrderWithRespectTo("Foo", "a"), migrations.AlterOrderWithRespectTo("Foo", "b"), ) def test_optimize_through_create(self): """ We should be able to optimize away create/delete through a create or delete of a different model, but only if the create operation does not mention the model at all. """ # These should work self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())]), migrations.DeleteModel("Foo"), ], [ migrations.CreateModel("Bar", [("size", models.IntegerField())]), ], ) self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())]), migrations.DeleteModel("Bar"), migrations.DeleteModel("Foo"), ], [], ) self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())]), migrations.DeleteModel("Foo"), migrations.DeleteModel("Bar"), ], [], ) # Operations should be optimized if the FK references a model from the # other app. self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("other", models.ForeignKey("testapp.Foo", models.CASCADE))]), migrations.DeleteModel("Foo"), ], [ migrations.CreateModel("Bar", [("other", models.ForeignKey("testapp.Foo", models.CASCADE))]), ], app_label="otherapp", ) # But it shouldn't work if a FK references a model with the same # app_label. self.assertDoesNotOptimize( [ migrations.CreateModel('Foo', [('name', models.CharField(max_length=255))]), migrations.CreateModel('Bar', [('other', models.ForeignKey('Foo', models.CASCADE))]), migrations.DeleteModel('Foo'), ], ) self.assertDoesNotOptimize( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("other", models.ForeignKey("testapp.Foo", models.CASCADE))]), migrations.DeleteModel("Foo"), ], app_label="testapp", ) # This should not work - bases should block it self.assertDoesNotOptimize( [ migrations.CreateModel('Foo', [('name', models.CharField(max_length=255))]), migrations.CreateModel('Bar', [('size', models.IntegerField())], bases=('Foo',)), migrations.DeleteModel('Foo'), ], ) self.assertDoesNotOptimize( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())], bases=("testapp.Foo",)), migrations.DeleteModel("Foo"), ], app_label='testapp', ) # The same operations should be optimized if app_label and none of # bases belong to that app. self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())], bases=("testapp.Foo",)), migrations.DeleteModel("Foo"), ], [ migrations.CreateModel("Bar", [("size", models.IntegerField())], bases=("testapp.Foo",)), ], app_label="otherapp", ) # But it shouldn't work if some of bases belongs to the specified app. self.assertDoesNotOptimize( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())], bases=("testapp.Foo",)), migrations.DeleteModel("Foo"), ], app_label="testapp", ) self.assertOptimizesTo( [ migrations.CreateModel('Book', [('name', models.CharField(max_length=255))]), migrations.CreateModel('Person', [('name', models.CharField(max_length=255))]), migrations.AddField('book', 'author', models.ForeignKey('test_app.Person', models.CASCADE)), migrations.CreateModel('Review', [('book', models.ForeignKey('test_app.Book', models.CASCADE))]), migrations.CreateModel('Reviewer', [('name', models.CharField(max_length=255))]), migrations.AddField('review', 'reviewer', models.ForeignKey('test_app.Reviewer', models.CASCADE)), migrations.RemoveField('book', 'author'), migrations.DeleteModel('Person'), ], [ migrations.CreateModel('Book', [('name', models.CharField(max_length=255))]), migrations.CreateModel('Reviewer', [('name', models.CharField(max_length=255))]), migrations.CreateModel('Review', [ ('book', models.ForeignKey('test_app.Book', models.CASCADE)), ('reviewer', models.ForeignKey('test_app.Reviewer', models.CASCADE)), ]), ], app_label='test_app', ) def test_create_model_add_field(self): """ AddField should optimize into CreateModel. """ managers = [('objects', EmptyManager())] self.assertOptimizesTo( [ migrations.CreateModel( name="Foo", fields=[("name", models.CharField(max_length=255))], options={'verbose_name': 'Foo'}, bases=(UnicodeModel,), managers=managers, ), migrations.AddField("Foo", "age", models.IntegerField()), ], [ migrations.CreateModel( name="Foo", fields=[ ("name", models.CharField(max_length=255)), ("age", models.IntegerField()), ], options={'verbose_name': 'Foo'}, bases=(UnicodeModel,), managers=managers, ), ], ) def test_create_model_reordering(self): """ AddField optimizes into CreateModel if it's a FK to a model that's between them (and there's no FK in the other direction), by changing the order of the CreateModel operations. """ self.assertOptimizesTo( [ migrations.CreateModel('Foo', [('name', models.CharField(max_length=255))]), migrations.CreateModel('Link', [('url', models.TextField())]), migrations.AddField('Foo', 'link', models.ForeignKey('migrations.Link', models.CASCADE)), ], [ migrations.CreateModel('Link', [('url', models.TextField())]), migrations.CreateModel('Foo', [ ('name', models.CharField(max_length=255)), ('link', models.ForeignKey('migrations.Link', models.CASCADE)) ]), ], ) def test_create_model_reordering_circular_fk(self): """ CreateModel reordering behavior doesn't result in an infinite loop if there are FKs in both directions. """ self.assertOptimizesTo( [ migrations.CreateModel('Bar', [('url', models.TextField())]), migrations.CreateModel('Foo', [('name', models.CharField(max_length=255))]), migrations.AddField('Bar', 'foo_fk', models.ForeignKey('migrations.Foo', models.CASCADE)), migrations.AddField('Foo', 'bar_fk', models.ForeignKey('migrations.Bar', models.CASCADE)), ], [ migrations.CreateModel('Foo', [('name', models.CharField(max_length=255))]), migrations.CreateModel('Bar', [ ('url', models.TextField()), ('foo_fk', models.ForeignKey('migrations.Foo', models.CASCADE)), ]), migrations.AddField('Foo', 'bar_fk', models.ForeignKey('migrations.Bar', models.CASCADE)), ], ) def test_create_model_no_reordering_for_unrelated_fk(self): """ CreateModel order remains unchanged if the later AddField operation isn't a FK between them. """ self.assertDoesNotOptimize( [ migrations.CreateModel('Foo', [('name', models.CharField(max_length=255))]), migrations.CreateModel('Link', [('url', models.TextField())]), migrations.AddField('Other', 'link', models.ForeignKey('migrations.Link', models.CASCADE)), ], ) def test_create_model_no_reordering_of_inherited_model(self): """ A CreateModel that inherits from another isn't reordered to avoid moving it earlier than its parent CreateModel operation. """ self.assertOptimizesTo( [ migrations.CreateModel('Other', [('foo', models.CharField(max_length=255))]), migrations.CreateModel('ParentModel', [('bar', models.CharField(max_length=255))]), migrations.CreateModel( 'ChildModel', [('baz', models.CharField(max_length=255))], bases=('migrations.parentmodel',), ), migrations.AddField('Other', 'fk', models.ForeignKey('migrations.ChildModel', models.CASCADE)), ], [ migrations.CreateModel('ParentModel', [('bar', models.CharField(max_length=255))]), migrations.CreateModel( 'ChildModel', [('baz', models.CharField(max_length=255))], bases=('migrations.parentmodel',), ), migrations.CreateModel( 'Other', [ ('foo', models.CharField(max_length=255)), ('fk', models.ForeignKey('migrations.ChildModel', models.CASCADE)), ] ), ], ) def test_create_model_add_field_not_through_m2m_through(self): """ AddField should NOT optimize into CreateModel if it's an M2M using a through that's created between them. """ self.assertDoesNotOptimize( [ migrations.CreateModel('Employee', []), migrations.CreateModel('Employer', []), migrations.CreateModel('Employment', [ ('employee', models.ForeignKey('migrations.Employee', models.CASCADE)), ('employment', models.ForeignKey('migrations.Employer', models.CASCADE)), ]), migrations.AddField( 'Employer', 'employees', models.ManyToManyField( 'migrations.Employee', through='migrations.Employment', ) ), ], ) def test_create_model_alter_field(self): """ AlterField should optimize into CreateModel. """ managers = [('objects', EmptyManager())] self.assertOptimizesTo( [ migrations.CreateModel( name="Foo", fields=[("name", models.CharField(max_length=255))], options={'verbose_name': 'Foo'}, bases=(UnicodeModel,), managers=managers, ), migrations.AlterField("Foo", "name", models.IntegerField()), ], [ migrations.CreateModel( name="Foo", fields=[ ("name", models.IntegerField()), ], options={'verbose_name': 'Foo'}, bases=(UnicodeModel,), managers=managers, ), ], ) def test_create_model_rename_field(self): """ RenameField should optimize into CreateModel. """ managers = [('objects', EmptyManager())] self.assertOptimizesTo( [ migrations.CreateModel( name="Foo", fields=[("name", models.CharField(max_length=255))], options={'verbose_name': 'Foo'}, bases=(UnicodeModel,), managers=managers, ), migrations.RenameField("Foo", "name", "title"), ], [ migrations.CreateModel( name="Foo", fields=[ ("title", models.CharField(max_length=255)), ], options={'verbose_name': 'Foo'}, bases=(UnicodeModel,), managers=managers, ), ], ) def test_add_field_rename_field(self): """ RenameField should optimize into AddField """ self.assertOptimizesTo( [ migrations.AddField("Foo", "name", models.CharField(max_length=255)), migrations.RenameField("Foo", "name", "title"), ], [ migrations.AddField("Foo", "title", models.CharField(max_length=255)), ], ) def test_alter_field_rename_field(self): """ RenameField should optimize to the other side of AlterField, and into itself. """ self.assertOptimizesTo( [ migrations.AlterField("Foo", "name", models.CharField(max_length=255)), migrations.RenameField("Foo", "name", "title"), migrations.RenameField("Foo", "title", "nom"), ], [ migrations.RenameField("Foo", "name", "nom"), migrations.AlterField("Foo", "nom", models.CharField(max_length=255)), ], ) def test_create_model_remove_field(self): """ RemoveField should optimize into CreateModel. """ managers = [('objects', EmptyManager())] self.assertOptimizesTo( [ migrations.CreateModel( name="Foo", fields=[ ("name", models.CharField(max_length=255)), ("age", models.IntegerField()), ], options={'verbose_name': 'Foo'}, bases=(UnicodeModel,), managers=managers, ), migrations.RemoveField("Foo", "age"), ], [ migrations.CreateModel( name="Foo", fields=[ ("name", models.CharField(max_length=255)), ], options={'verbose_name': 'Foo'}, bases=(UnicodeModel,), managers=managers, ), ], ) def test_add_field_alter_field(self): """ AlterField should optimize into AddField. """ self.assertOptimizesTo( [ migrations.AddField("Foo", "age", models.IntegerField()), migrations.AlterField("Foo", "age", models.FloatField(default=2.4)), ], [ migrations.AddField("Foo", name="age", field=models.FloatField(default=2.4)), ], ) def test_add_field_delete_field(self): """ RemoveField should cancel AddField """ self.assertOptimizesTo( [ migrations.AddField("Foo", "age", models.IntegerField()), migrations.RemoveField("Foo", "age"), ], [], ) def test_alter_field_delete_field(self): """ RemoveField should absorb AlterField """ self.assertOptimizesTo( [ migrations.AlterField("Foo", "age", models.IntegerField()), migrations.RemoveField("Foo", "age"), ], [ migrations.RemoveField("Foo", "age"), ], ) def _test_create_alter_foo_field(self, alter): """ CreateModel, AlterFooTogether/AlterOrderWithRespectTo followed by an 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( [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), ("b", models.IntegerField()), ]), alter, migrations.AddField("Foo", "c", models.IntegerField()), ], [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), ("b", models.IntegerField()), ("c", models.IntegerField()), ], options=options), ], ) # AlterField self.assertOptimizesTo( [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), ("b", models.IntegerField()), ]), 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( [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), ("b", models.IntegerField()), ("c", models.IntegerField()), ]), alter, migrations.AlterField("Foo", "c", models.CharField(max_length=255)), ], [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), ("b", models.IntegerField()), ("c", models.CharField(max_length=255)), ], options=options), ], ) # RenameField 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()), ("b", models.IntegerField()), ]), alter, migrations.RenameField("Foo", "b", "c"), ], [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), ("c", models.IntegerField()), ], options=renamed_options), ], ) self.assertOptimizesTo( [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), ("b", models.IntegerField()), ]), alter, migrations.RenameField("Foo", "b", "x"), migrations.RenameField("Foo", "x", "c"), ], [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), ("c", models.IntegerField()), ], options=renamed_options), ], ) self.assertOptimizesTo( [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), ("b", models.IntegerField()), ("c", models.IntegerField()), ]), alter, migrations.RenameField("Foo", "c", "d"), ], [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), ("b", models.IntegerField()), ("d", models.IntegerField()), ], options=options), ], ) # RemoveField 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()), ("b", models.IntegerField()), ]), alter, migrations.RemoveField("Foo", "b"), ], [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), ], options=removed_options), ] ) self.assertOptimizesTo( [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), ("b", models.IntegerField()), ("c", models.IntegerField()), ]), alter, migrations.RemoveField("Foo", "c"), ], [ migrations.CreateModel("Foo", [ ("a", models.IntegerField()), ("b", models.IntegerField()), ], options=options), ], ) def test_create_alter_unique_field(self): self._test_create_alter_foo_field(migrations.AlterUniqueTogether("Foo", [["a", "b"]])) def test_create_alter_index_field(self): self._test_create_alter_foo_field(migrations.AlterIndexTogether("Foo", [["a", "b"]])) def test_create_alter_owrt_field(self): self._test_create_alter_foo_field(migrations.AlterOrderWithRespectTo("Foo", "b")) def test_optimize_through_fields(self): """ field-level through checking is working. This should manage to collapse model Foo to nonexistence, and model Bar to a single IntegerField called "width". """ self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())]), migrations.AddField("Foo", "age", models.IntegerField()), migrations.AddField("Bar", "width", models.IntegerField()), migrations.AlterField("Foo", "age", models.IntegerField()), migrations.RenameField("Bar", "size", "dimensions"), migrations.RemoveField("Foo", "age"), migrations.RenameModel("Foo", "Phou"), migrations.RemoveField("Bar", "dimensions"), migrations.RenameModel("Phou", "Fou"), migrations.DeleteModel("Fou"), ], [ migrations.CreateModel("Bar", [("width", models.IntegerField())]), ], ) def test_optimize_elidable_operation(self): elidable_operation = operations.base.Operation() elidable_operation.elidable = True self.assertOptimizesTo( [ elidable_operation, migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), elidable_operation, migrations.CreateModel("Bar", [("size", models.IntegerField())]), elidable_operation, migrations.RenameModel("Foo", "Phou"), migrations.DeleteModel("Bar"), elidable_operation, ], [ migrations.CreateModel("Phou", [("name", models.CharField(max_length=255))]), ], )