Pārlūkot izejas kodu

Fixed #25922 -- Fixed migrate --fake-initial detection of many-to-many tables.

Tim Graham 9 gadi atpakaļ
vecāks
revīzija
fa9ce4e9a6

+ 17 - 4
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,

+ 3 - 0
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`).

+ 31 - 0
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'),
+        ),
+    ]

+ 20 - 0
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'),
+        ),
+    ]

+ 0 - 0
tests/migrations/test_add_many_to_many_field_initial/__init__.py


+ 59 - 0
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",