From 02ae5fd31a56ffb42feadb49c1f3870ba0a24869 Mon Sep 17 00:00:00 2001
From: Attila Tovt <uran198@gmail.com>
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 <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)