diff --git a/django/db/migrations/executor.py b/django/db/migrations/executor.py index 3779b626ec..b1a8383100 100644 --- a/django/db/migrations/executor.py +++ b/django/db/migrations/executor.py @@ -265,6 +265,7 @@ class MigrationExecutor(object): apps = after_state.apps found_create_model_migration = False found_add_field_migration = False + existing_table_names = self.connection.introspection.table_names(self.connection.cursor()) # Make sure all create model and add field operations are done for operation in migration.operations: if isinstance(operation, migrations.CreateModel): @@ -275,7 +276,7 @@ class MigrationExecutor(object): model = global_apps.get_model(model._meta.swapped) if model._meta.proxy or not model._meta.managed: continue - if model._meta.db_table not in self.connection.introspection.table_names(self.connection.cursor()): + if model._meta.db_table not in existing_table_names: return False, project_state found_create_model_migration = True elif isinstance(operation, migrations.AddField): @@ -288,9 +289,21 @@ class MigrationExecutor(object): continue table = model._meta.db_table - db_field = model._meta.get_field(operation.name).column - fields = self.connection.introspection.get_table_description(self.connection.cursor(), table) - if db_field not in (f.name for f in fields): + field = model._meta.get_field(operation.name) + + # Handle implicit many-to-many tables created by AddField. + if field.many_to_many: + if field.remote_field.through._meta.db_table not in existing_table_names: + return False, project_state + else: + found_add_field_migration = True + continue + + column_names = [ + column.name for column in + self.connection.introspection.get_table_description(self.connection.cursor(), table) + ] + if field.column not in column_names: return False, project_state found_add_field_migration = True # If we get this far and we found at least one CreateModel or AddField migration, diff --git a/docs/releases/1.9.1.txt b/docs/releases/1.9.1.txt index 905f207d6a..8180a2805f 100644 --- a/docs/releases/1.9.1.txt +++ b/docs/releases/1.9.1.txt @@ -58,3 +58,6 @@ Bugfixes behind ``AppRegistryNotReady`` when starting ``runserver`` (:ticket:`25510`). This regression appeared in 1.8.5 as a side effect of fixing :ticket:`24704` and by mistake the fix wasn't applied to the ``stable/1.9.x`` branch. + +* Fixed ``migrate --fake-initial`` detection of many-to-many tables + (:ticket:`25922`). diff --git a/tests/migrations/test_add_many_to_many_field_initial/0001_initial.py b/tests/migrations/test_add_many_to_many_field_initial/0001_initial.py new file mode 100644 index 0000000000..b50b3c68b8 --- /dev/null +++ b/tests/migrations/test_add_many_to_many_field_initial/0001_initial.py @@ -0,0 +1,31 @@ +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Project', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='Task', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.AddField( + model_name='project', + name='tasks', + field=models.ManyToManyField(to='Task'), + ), + ] diff --git a/tests/migrations/test_add_many_to_many_field_initial/0002_initial.py b/tests/migrations/test_add_many_to_many_field_initial/0002_initial.py new file mode 100644 index 0000000000..65739a5bb4 --- /dev/null +++ b/tests/migrations/test_add_many_to_many_field_initial/0002_initial.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("migrations", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='projects', + field=models.ManyToManyField(to='Project'), + ), + ] diff --git a/tests/migrations/test_add_many_to_many_field_initial/__init__.py b/tests/migrations/test_add_many_to_many_field_initial/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/migrations/test_executor.py b/tests/migrations/test_executor.py index 32cdc5e09c..1bbed48e3c 100644 --- a/tests/migrations/test_executor.py +++ b/tests/migrations/test_executor.py @@ -301,6 +301,65 @@ class ExecutorTests(MigrationTestBase): self.assertTableNotExists("migrations_author") self.assertTableNotExists("migrations_tribble") + @override_settings( + MIGRATION_MODULES={ + "migrations": "migrations.test_add_many_to_many_field_initial", + }, + ) + def test_detect_soft_applied_add_field_manytomanyfield(self): + """ + executor.detect_soft_applied() detects ManyToManyField tables from an + AddField operation. This checks the case of AddField in a migration + with other operations (0001) and the case of AddField in its own + migration (0002). + """ + tables = [ + # from 0001 + "migrations_project", + "migrations_task", + "migrations_project_tasks", + # from 0002 + "migrations_task_projects", + ] + executor = MigrationExecutor(connection) + # Create the tables for 0001 but make it look like the migration hasn't + # been applied. + executor.migrate([("migrations", "0001_initial")]) + executor.migrate([("migrations", None)], fake=True) + for table in tables[:3]: + self.assertTableExists(table) + # Table detection sees 0001 is applied but not 0002. + migration = executor.loader.get_migration("migrations", "0001_initial") + self.assertEqual(executor.detect_soft_applied(None, migration)[0], True) + migration = executor.loader.get_migration("migrations", "0002_initial") + self.assertEqual(executor.detect_soft_applied(None, migration)[0], False) + + # Create the tables for both migrations but make it look like neither + # has been applied. + executor.loader.build_graph() + executor.migrate([("migrations", "0001_initial")], fake=True) + executor.migrate([("migrations", "0002_initial")]) + executor.loader.build_graph() + executor.migrate([("migrations", None)], fake=True) + # Table detection sees 0002 is applied. + migration = executor.loader.get_migration("migrations", "0002_initial") + self.assertEqual(executor.detect_soft_applied(None, migration)[0], True) + + # Leave the tables for 0001 except the many-to-many table. That missing + # table should cause detect_soft_applied() to return False. + with connection.schema_editor() as editor: + for table in tables[2:]: + editor.execute(editor.sql_delete_table % {"table": table}) + migration = executor.loader.get_migration("migrations", "0001_initial") + self.assertEqual(executor.detect_soft_applied(None, migration)[0], False) + + # Cleanup by removing the remaining tables. + with connection.schema_editor() as editor: + for table in tables[:2]: + editor.execute(editor.sql_delete_table % {"table": table}) + for table in tables: + self.assertTableNotExists(table) + @override_settings( INSTALLED_APPS=[ "migrations.migrations_test_apps.lookuperror_a",