Fixed #23410 -- Avoided unnecessary rollbacks in related apps when migrating backwards.

This commit is contained in:
Carl Meyer 2014-11-17 10:13:47 -07:00
parent e7b9a58b08
commit ab2819aa7b
4 changed files with 114 additions and 11 deletions

View File

@ -36,12 +36,17 @@ class MigrationExecutor(object):
# If the migration is already applied, do backwards mode, # If the migration is already applied, do backwards mode,
# otherwise do forwards mode. # otherwise do forwards mode.
elif target in applied: elif target in applied:
backwards_plan = self.loader.graph.backwards_plan(target)[:-1] # Don't migrate backwards all the way to the target node (that
# We only do this if the migration is not the most recent one # may roll back dependencies in other apps that don't need to
# in its app - that is, another migration with the same app # be rolled back); instead roll back through target's immediate
# label is in the backwards plan # child(ren) in the same app, and no further.
if any(node[0] == target[0] for node in backwards_plan): next_in_app = sorted(
for migration in backwards_plan: n for n in
self.loader.graph.dependents.get(target, set())
if n[0] == target[0]
)
for node in next_in_app:
for migration in self.loader.graph.backwards_plan(node):
if migration in applied: if migration in applied:
plan.append((self.loader.graph.nodes[migration], True)) plan.append((self.loader.graph.nodes[migration], True))
applied.remove(migration) applied.remove(migration)

View File

@ -737,10 +737,11 @@ The behavior of this command changes depending on the arguments provided:
* ``<app_label>``: The specified app has its migrations run, up to the most * ``<app_label>``: The specified app has its migrations run, up to the most
recent migration. This may involve running other apps' migrations too, due recent migration. This may involve running other apps' migrations too, due
to dependencies. to dependencies.
* ``<app_label> <migrationname>``: Brings the database schema to a state where it * ``<app_label> <migrationname>``: Brings the database schema to a state where
would have just run the given migration, but no further - this may involve the named migration is applied, but no later migrations in the same app are
unapplying migrations if you have previously migrated past the named applied. This may involve unapplying migrations if you have previously
migration. Use the name ``zero`` to unapply all migrations for an app. migrated past the named migration. Use the name ``zero`` to unapply all
migrations for an app.
Unlike ``syncdb``, this command does not prompt you to create a superuser if Unlike ``syncdb``, this command does not prompt you to create a superuser if
one doesn't exist (assuming you are using :mod:`django.contrib.auth`). Use one doesn't exist (assuming you are using :mod:`django.contrib.auth`). Use

View File

@ -68,3 +68,6 @@ Bugfixes
* Made :class:`~django.db.migrations.operations.RenameModel` reversible * Made :class:`~django.db.migrations.operations.RenameModel` reversible
(:ticket:`22248`) (:ticket:`22248`)
* Avoided unnecessary rollbacks of migrations from other apps when migrating
backwards (:ticket:`23410`).

View File

@ -1,6 +1,7 @@
from django.db import connection from django.db import connection
from django.db.migrations.executor import MigrationExecutor from django.db.migrations.executor import MigrationExecutor
from django.test import modify_settings, override_settings from django.db.migrations.graph import MigrationGraph
from django.test import modify_settings, override_settings, TestCase
from django.apps.registry import apps as global_apps from django.apps.registry import apps as global_apps
from .test_base import MigrationTestBase from .test_base import MigrationTestBase
@ -231,3 +232,96 @@ class ExecutorTests(MigrationTestBase):
executor.migrate([("migrations", None)]) executor.migrate([("migrations", None)])
self.assertTableNotExists("migrations_author") self.assertTableNotExists("migrations_author")
self.assertTableNotExists("migrations_tribble") self.assertTableNotExists("migrations_tribble")
class FakeLoader(object):
def __init__(self, graph, applied):
self.graph = graph
self.applied_migrations = applied
class FakeMigration(object):
"""Really all we need is any object with a debug-useful repr."""
def __init__(self, name):
self.name = name
def __repr__(self):
return 'M<%s>' % self.name
class ExecutorUnitTests(TestCase):
"""(More) isolated unit tests for executor methods."""
def test_minimize_rollbacks(self):
"""
Minimize unnecessary rollbacks in connected apps.
When you say "./manage.py migrate appA 0001", rather than migrating to
just after appA-0001 in the linearized migration plan (which could roll
back migrations in other apps that depend on appA 0001, but don't need
to be rolled back since we're not rolling back appA 0001), we migrate
to just before appA-0002.
"""
a1_impl = FakeMigration('a1')
a1 = ('a', '1')
a2_impl = FakeMigration('a2')
a2 = ('a', '2')
b1_impl = FakeMigration('b1')
b1 = ('b', '1')
graph = MigrationGraph()
graph.add_node(a1, a1_impl)
graph.add_node(a2, a2_impl)
graph.add_node(b1, b1_impl)
graph.add_dependency(None, b1, a1)
graph.add_dependency(None, a2, a1)
executor = MigrationExecutor(None)
executor.loader = FakeLoader(graph, {a1, b1, a2})
plan = executor.migration_plan({a1})
self.assertEqual(plan, [(a2_impl, True)])
def test_minimize_rollbacks_branchy(self):
"""
Minimize rollbacks when target has multiple in-app children.
a: 1 <---- 3 <--\
\ \- 2 <--- 4
\ \
b: \- 1 <--- 2
"""
a1_impl = FakeMigration('a1')
a1 = ('a', '1')
a2_impl = FakeMigration('a2')
a2 = ('a', '2')
a3_impl = FakeMigration('a3')
a3 = ('a', '3')
a4_impl = FakeMigration('a4')
a4 = ('a', '4')
b1_impl = FakeMigration('b1')
b1 = ('b', '1')
b2_impl = FakeMigration('b2')
b2 = ('b', '2')
graph = MigrationGraph()
graph.add_node(a1, a1_impl)
graph.add_node(a2, a2_impl)
graph.add_node(a3, a3_impl)
graph.add_node(a4, a4_impl)
graph.add_node(b1, b1_impl)
graph.add_node(b2, b2_impl)
graph.add_dependency(None, a2, a1)
graph.add_dependency(None, a3, a1)
graph.add_dependency(None, a4, a2)
graph.add_dependency(None, a4, a3)
graph.add_dependency(None, b2, b1)
graph.add_dependency(None, b1, a1)
graph.add_dependency(None, b2, a2)
executor = MigrationExecutor(None)
executor.loader = FakeLoader(graph, {a1, b1, a2, b2, a3, a4})
plan = executor.migration_plan({a1})
should_be_rolled_back = [b2_impl, a4_impl, a2_impl, a3_impl]
exp = [(m, True) for m in should_be_rolled_back]
self.assertEqual(plan, exp)