From 02ae5fd31a56ffb42feadb49c1f3870ba0a24869 Mon Sep 17 00:00:00 2001 From: Attila Tovt Date: Sat, 2 Apr 2016 14:46:59 +0200 Subject: [PATCH] Fixed #25850 -- Made migrate/makemigrations error on inconsistent history. --- .../management/commands/makemigrations.py | 5 ++++ django/core/management/commands/migrate.py | 3 +++ django/db/migrations/exceptions.py | 7 +++++ django/db/migrations/loader.py | 24 ++++++++++++++++- docs/releases/1.10.txt | 4 +++ tests/migrations/test_commands.py | 27 +++++++++++++++++++ tests/migrations/test_loader.py | 17 +++++++++++- 7 files changed, 85 insertions(+), 2 deletions(-) diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py index 229cb991af..fc691391f5 100644 --- a/django/core/management/commands/makemigrations.py +++ b/django/core/management/commands/makemigrations.py @@ -5,6 +5,7 @@ from itertools import takewhile from django.apps import apps from django.core.management.base import BaseCommand, CommandError +from django.db import connections from django.db.migrations import Migration from django.db.migrations.autodetector import MigrationAutodetector 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. 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 # hard if there are any and they don't want to merge conflicts = loader.detect_conflicts() diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index f78fde3ebb..9c73500d21 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -65,6 +65,9 @@ class Command(BaseCommand): # Work out which apps have migrations and which do not 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 # hard if there are any conflicts = executor.loader.detect_conflicts() diff --git a/django/db/migrations/exceptions.py b/django/db/migrations/exceptions.py index dd3f1be2a6..d9c9b22416 100644 --- a/django/db/migrations/exceptions.py +++ b/django/db/migrations/exceptions.py @@ -25,6 +25,13 @@ class CircularDependencyError(Exception): pass +class InconsistentMigrationHistory(Exception): + """ + Raised when an applied migration has some of its dependencies not applied. + """ + pass + + class InvalidBasesError(ValueError): """ Raised when a model's base classes can't be resolved. diff --git a/django/db/migrations/loader.py b/django/db/migrations/loader.py index 9feedecc88..c9ffbcc495 100644 --- a/django/db/migrations/loader.py +++ b/django/db/migrations/loader.py @@ -10,7 +10,10 @@ from django.db.migrations.graph import MigrationGraph from django.db.migrations.recorder import MigrationRecorder from django.utils import six -from .exceptions import AmbiguityError, BadMigrationError, NodeNotFoundError +from .exceptions import ( + AmbiguityError, BadMigrationError, InconsistentMigrationHistory, + NodeNotFoundError, +) MIGRATIONS_MODULE_NAME = 'migrations' @@ -318,6 +321,25 @@ class MigrationLoader(object): # "child" is not in there. _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): """ Looks through the loaded graph and detects any conflicts - apps diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index 96b9d23f62..2e97ff3d54 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -323,6 +323,10 @@ Migrations * Added support for :ref:`non-atomic migrations ` by 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 ~~~~~~ diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index c7bb2e603c..88aff5d021 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -8,6 +8,7 @@ import os from django.apps import apps from django.core.management import CommandError, call_command from django.db import DatabaseError, connection, connections, models +from django.db.migrations.exceptions import InconsistentMigrationHistory from django.db.migrations.recorder import MigrationRecorder from django.test import ignore_warnings, mock, override_settings from django.utils import six @@ -462,6 +463,20 @@ class MigrateTests(MigrationTestBase): ) # 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): """ @@ -1055,6 +1070,18 @@ class MakeMigrationsTests(MigrationTestBase): call_command("makemigrations", "migrations", stdout=out) 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): """ diff --git a/tests/migrations/test_loader.py b/tests/migrations/test_loader.py index bcb30bffd8..ec9c30adee 100644 --- a/tests/migrations/test_loader.py +++ b/tests/migrations/test_loader.py @@ -3,7 +3,9 @@ from __future__ import unicode_literals from unittest import skipIf 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.recorder import MigrationRecorder from django.test import TestCase, modify_settings, override_settings @@ -382,3 +384,16 @@ class LoaderTests(TestCase): recorder.record_applied("migrations", "7_auto") loader.build_graph() 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)