summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/db/migrations/executor.py21
-rw-r--r--docs/releases/1.9.1.txt3
-rw-r--r--tests/migrations/test_add_many_to_many_field_initial/0001_initial.py31
-rw-r--r--tests/migrations/test_add_many_to_many_field_initial/0002_initial.py20
-rw-r--r--tests/migrations/test_add_many_to_many_field_initial/__init__.py0
-rw-r--r--tests/migrations/test_executor.py59
6 files changed, 130 insertions, 4 deletions
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
--- /dev/null
+++ b/tests/migrations/test_add_many_to_many_field_initial/__init__.py
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
@@ -302,6 +302,65 @@ class ExecutorTests(MigrationTestBase):
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",
"migrations.migrations_test_apps.lookuperror_b",