Fixed #25850 -- Made migrate/makemigrations error on inconsistent history.
This commit is contained in:
parent
6448873197
commit
02ae5fd31a
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue