diff --git a/django/db/backends/schema.py b/django/db/backends/schema.py index 21eeefab82..c2503e5f53 100644 --- a/django/db/backends/schema.py +++ b/django/db/backends/schema.py @@ -362,9 +362,9 @@ class BaseDatabaseSchemaEditor(object): return self._alter_many_to_many(model, old_field, new_field, strict) elif old_type is None or new_type is None: raise ValueError("Cannot alter field %s into %s - they are not compatible types (probably means only one is an M2M with implicit through model)" % ( - old_field, - new_field, - )) + old_field, + new_field, + )) # Has unique been removed? if old_field.unique and not new_field.unique: # Find the unique constraint for this field diff --git a/django/db/migrations/operations/__init__.py b/django/db/migrations/operations/__init__.py index 0aa7e2d119..6c2c784635 100644 --- a/django/db/migrations/operations/__init__.py +++ b/django/db/migrations/operations/__init__.py @@ -1,2 +1,2 @@ -from .models import CreateModel, DeleteModel +from .models import CreateModel, DeleteModel, AlterModelTable from .fields import AddField, RemoveField diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py index ef7dafab90..c73ff179d4 100644 --- a/django/db/migrations/operations/models.py +++ b/django/db/migrations/operations/models.py @@ -50,3 +50,31 @@ class DeleteModel(Operation): def describe(self): return "Delete model %s" % (self.name, ) + + +class AlterModelTable(Operation): + """ + Renames a model's table + """ + + def __init__(self, name, table): + self.name = name + self.table = table + + def state_forwards(self, app_label, state): + state.models[app_label, self.name.lower()].options["db_table"] = self.table + + def database_forwards(self, app_label, schema_editor, from_state, to_state): + old_app_cache = from_state.render() + new_app_cache = to_state.render() + schema_editor.alter_db_table( + new_app_cache.get_model(app_label, self.name), + old_app_cache.get_model(app_label, self.name)._meta.db_table, + new_app_cache.get_model(app_label, self.name)._meta.db_table, + ) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + return self.database_forwards(app_label, schema_editor, from_state, to_state) + + def describe(self): + return "Rename table for %s to %s" % (self.name, self.table) diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index 2b9ce21a1d..7a90110127 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -12,6 +12,7 @@ class AutodetectorTests(TestCase): """ author_empty = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True))]) + author_name = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200))]) other_pony = ModelState("otherapp", "Pony", [("id", models.AutoField(primary_key=True))]) other_stable = ModelState("otherapp", "Stable", [("id", models.AutoField(primary_key=True))]) third_thing = ModelState("thirdapp", "Thing", [("id", models.AutoField(primary_key=True))]) @@ -95,3 +96,37 @@ class AutodetectorTests(TestCase): action = migration.operations[0] self.assertEqual(action.__class__.__name__, "DeleteModel") self.assertEqual(action.name, "Author") + + def test_add_field(self): + "Tests autodetection of new fields" + # Make state + before = self.make_project_state([self.author_empty]) + after = self.make_project_state([self.author_name]) + autodetector = MigrationAutodetector(before, after) + changes = autodetector.changes() + # Right number of migrations? + self.assertEqual(len(changes['testapp']), 1) + # Right number of actions? + migration = changes['testapp'][0] + self.assertEqual(len(migration.operations), 1) + # Right action? + action = migration.operations[0] + self.assertEqual(action.__class__.__name__, "AddField") + self.assertEqual(action.name, "name") + + def test_remove_field(self): + "Tests autodetection of removed fields" + # Make state + before = self.make_project_state([self.author_name]) + after = self.make_project_state([self.author_empty]) + autodetector = MigrationAutodetector(before, after) + changes = autodetector.changes() + # Right number of migrations? + self.assertEqual(len(changes['testapp']), 1) + # Right number of actions? + migration = changes['testapp'][0] + self.assertEqual(len(migration.operations), 1) + # Right action? + action = migration.operations[0] + self.assertEqual(action.__class__.__name__, "RemoveField") + self.assertEqual(action.name, "name") diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index fc5aa47faf..50674e84ba 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -114,3 +114,46 @@ class OperationTests(TestCase): with connection.schema_editor() as editor: operation.database_backwards("test_adfl", editor, new_state, project_state) self.assertColumnNotExists("test_adfl_pony", "height") + + def test_remove_field(self): + """ + Tests the RemoveField operation. + """ + project_state = self.set_up_test_model("test_rmfl") + # Test the state alteration + operation = migrations.RemoveField("Pony", "pink") + new_state = project_state.clone() + operation.state_forwards("test_rmfl", new_state) + self.assertEqual(len(new_state.models["test_rmfl", "pony"].fields), 1) + # Test the database alteration + self.assertColumnExists("test_rmfl_pony", "pink") + with connection.schema_editor() as editor: + operation.database_forwards("test_rmfl", editor, project_state, new_state) + self.assertColumnNotExists("test_rmfl_pony", "pink") + # And test reversal + with connection.schema_editor() as editor: + operation.database_backwards("test_rmfl", editor, new_state, project_state) + self.assertColumnExists("test_rmfl_pony", "pink") + + def test_alter_model_table(self): + """ + Tests the AlterModelTable operation. + """ + project_state = self.set_up_test_model("test_almota") + # Test the state alteration + operation = migrations.AlterModelTable("Pony", "test_almota_pony_2") + new_state = project_state.clone() + operation.state_forwards("test_almota", new_state) + self.assertEqual(new_state.models["test_almota", "pony"].options["db_table"], "test_almota_pony_2") + # Test the database alteration + self.assertTableExists("test_almota_pony") + self.assertTableNotExists("test_almota_pony_2") + with connection.schema_editor() as editor: + operation.database_forwards("test_almota", editor, project_state, new_state) + self.assertTableNotExists("test_almota_pony") + self.assertTableExists("test_almota_pony_2") + # And test reversal + with connection.schema_editor() as editor: + operation.database_backwards("test_almota", editor, new_state, project_state) + self.assertTableExists("test_almota_pony") + self.assertTableNotExists("test_almota_pony_2")