Fixed #25850 -- Made migrate/makemigrations error on inconsistent history.

This commit is contained in:
Attila Tovt 2016-04-02 14:46:59 +02:00 committed by Tim Graham
parent 6448873197
commit 02ae5fd31a
7 changed files with 85 additions and 2 deletions

View File

@ -5,6 +5,7 @@ from itertools import takewhile
from django.apps import apps from django.apps import apps
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db import connections
from django.db.migrations import Migration from django.db.migrations import Migration
from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.autodetector import MigrationAutodetector
from django.db.migrations.loader import MigrationLoader from django.db.migrations.loader import MigrationLoader
@ -75,6 +76,10 @@ class Command(BaseCommand):
# the loader doesn't try to resolve replaced migrations from DB. # the loader doesn't try to resolve replaced migrations from DB.
loader = MigrationLoader(None, ignore_no_migrations=True) loader = MigrationLoader(None, ignore_no_migrations=True)
# Raise an error if any migrations are applied before their dependencies.
for db in connections:
loader.check_consistent_history(connections[db])
# Before anything else, see if there's conflicting apps and drop out # Before anything else, see if there's conflicting apps and drop out
# hard if there are any and they don't want to merge # hard if there are any and they don't want to merge
conflicts = loader.detect_conflicts() conflicts = loader.detect_conflicts()

View File

@ -65,6 +65,9 @@ class Command(BaseCommand):
# Work out which apps have migrations and which do not # Work out which apps have migrations and which do not
executor = MigrationExecutor(connection, self.migration_progress_callback) executor = MigrationExecutor(connection, self.migration_progress_callback)
# Raise an error if any migrations are applied before their dependencies.
executor.loader.check_consistent_history(connection)
# Before anything else, see if there's conflicting apps and drop out # Before anything else, see if there's conflicting apps and drop out
# hard if there are any # hard if there are any
conflicts = executor.loader.detect_conflicts() conflicts = executor.loader.detect_conflicts()

View File

@ -25,6 +25,13 @@ class CircularDependencyError(Exception):
pass pass
class InconsistentMigrationHistory(Exception):
"""
Raised when an applied migration has some of its dependencies not applied.
"""
pass
class InvalidBasesError(ValueError): class InvalidBasesError(ValueError):
""" """
Raised when a model's base classes can't be resolved. Raised when a model's base classes can't be resolved.

View File

@ -10,7 +10,10 @@ from django.db.migrations.graph import MigrationGraph
from django.db.migrations.recorder import MigrationRecorder from django.db.migrations.recorder import MigrationRecorder
from django.utils import six from django.utils import six
from .exceptions import AmbiguityError, BadMigrationError, NodeNotFoundError from .exceptions import (
AmbiguityError, BadMigrationError, InconsistentMigrationHistory,
NodeNotFoundError,
)
MIGRATIONS_MODULE_NAME = 'migrations' MIGRATIONS_MODULE_NAME = 'migrations'
@ -318,6 +321,25 @@ class MigrationLoader(object):
# "child" is not in there. # "child" is not in there.
_reraise_missing_dependency(migration, child, e) _reraise_missing_dependency(migration, child, e)
def check_consistent_history(self, connection):
"""
Raise InconsistentMigrationHistory if any applied migrations have
unapplied dependencies.
"""
recorder = MigrationRecorder(connection)
applied = recorder.applied_migrations()
for migration in applied:
# If the migration is unknown, skip it.
if migration not in self.graph.nodes:
continue
for parent in self.graph.node_map[migration].parents:
if parent not in applied:
raise InconsistentMigrationHistory(
"Migration {}.{} is applied before its dependency {}.{}".format(
migration[0], migration[1], parent[0], parent[1],
)
)
def detect_conflicts(self): def detect_conflicts(self):
""" """
Looks through the loaded graph and detects any conflicts - apps Looks through the loaded graph and detects any conflicts - apps

View File

@ -323,6 +323,10 @@ Migrations
* Added support for :ref:`non-atomic migrations <non-atomic-migrations>` by * Added support for :ref:`non-atomic migrations <non-atomic-migrations>` by
setting the ``atomic`` attribute on a ``Migration``. setting the ``atomic`` attribute on a ``Migration``.
* The ``migrate`` and ``makemigrations`` commands now check for a consistent
migration history. If they find some unapplied dependencies of an applied
migration, ``InconsistentMigrationHistory`` is raised.
Models Models
~~~~~~ ~~~~~~

View File

@ -8,6 +8,7 @@ import os
from django.apps import apps from django.apps import apps
from django.core.management import CommandError, call_command from django.core.management import CommandError, call_command
from django.db import DatabaseError, connection, connections, models from django.db import DatabaseError, connection, connections, models
from django.db.migrations.exceptions import InconsistentMigrationHistory
from django.db.migrations.recorder import MigrationRecorder from django.db.migrations.recorder import MigrationRecorder
from django.test import ignore_warnings, mock, override_settings from django.test import ignore_warnings, mock, override_settings
from django.utils import six from django.utils import six
@ -462,6 +463,20 @@ class MigrateTests(MigrationTestBase):
) )
# No changes were actually applied so there is nothing to rollback # No changes were actually applied so there is nothing to rollback
@override_settings(MIGRATION_MODULES={'migrations': 'migrations.test_migrations'})
def test_migrate_inconsistent_history(self):
"""
Running migrate with some migrations applied before their dependencies
should not be allowed.
"""
recorder = MigrationRecorder(connection)
recorder.record_applied("migrations", "0002_second")
msg = "Migration migrations.0002_second is applied before its dependency migrations.0001_initial"
with self.assertRaisesMessage(InconsistentMigrationHistory, msg):
call_command("migrate")
applied_migrations = recorder.applied_migrations()
self.assertNotIn(("migrations", "0001_initial"), applied_migrations)
class MakeMigrationsTests(MigrationTestBase): class MakeMigrationsTests(MigrationTestBase):
""" """
@ -1055,6 +1070,18 @@ class MakeMigrationsTests(MigrationTestBase):
call_command("makemigrations", "migrations", stdout=out) call_command("makemigrations", "migrations", stdout=out)
self.assertIn(os.path.join(migration_dir, '0001_initial.py'), out.getvalue()) self.assertIn(os.path.join(migration_dir, '0001_initial.py'), out.getvalue())
def test_makemigrations_inconsistent_history(self):
"""
makemigrations should raise InconsistentMigrationHistory exception if
there are some migrations applied before their dependencies.
"""
recorder = MigrationRecorder(connection)
recorder.record_applied('migrations', '0002_second')
msg = "Migration migrations.0002_second is applied before its dependency migrations.0001_initial"
with self.temporary_migration_module(module="migrations.test_migrations"):
with self.assertRaisesMessage(InconsistentMigrationHistory, msg):
call_command("makemigrations")
class SquashMigrationsTests(MigrationTestBase): class SquashMigrationsTests(MigrationTestBase):
""" """

View File

@ -3,7 +3,9 @@ from __future__ import unicode_literals
from unittest import skipIf from unittest import skipIf
from django.db import ConnectionHandler, connection, connections from django.db import ConnectionHandler, connection, connections
from django.db.migrations.exceptions import AmbiguityError, NodeNotFoundError from django.db.migrations.exceptions import (
AmbiguityError, InconsistentMigrationHistory, NodeNotFoundError,
)
from django.db.migrations.loader import MigrationLoader from django.db.migrations.loader import MigrationLoader
from django.db.migrations.recorder import MigrationRecorder from django.db.migrations.recorder import MigrationRecorder
from django.test import TestCase, modify_settings, override_settings from django.test import TestCase, modify_settings, override_settings
@ -382,3 +384,16 @@ class LoaderTests(TestCase):
recorder.record_applied("migrations", "7_auto") recorder.record_applied("migrations", "7_auto")
loader.build_graph() loader.build_graph()
self.assertEqual(num_nodes(), 0) self.assertEqual(num_nodes(), 0)
@override_settings(
MIGRATION_MODULES={'migrations': 'migrations.test_migrations'},
INSTALLED_APPS=['migrations'],
)
def test_check_consistent_history(self):
loader = MigrationLoader(connection=None)
loader.check_consistent_history(connection)
recorder = MigrationRecorder(connection)
recorder.record_applied('migrations', '0002_second')
msg = "Migration migrations.0002_second is applied before its dependency migrations.0001_initial"
with self.assertRaisesMessage(InconsistentMigrationHistory, msg):
loader.check_consistent_history(connection)