Commit e9cb333b authored by Andrew Godwin's avatar Andrew Godwin
Browse files

Auto-apply initial migrations if their tables exist already.

parent eafe2791
Loading
Loading
Loading
Loading
+9 −3
Original line number Diff line number Diff line
@@ -127,17 +127,23 @@ class Command(BaseCommand):
        # to do at this point.
        emit_post_migrate_signal(created_models, self.verbosity, self.interactive, connection.alias)

    def migration_progress_callback(self, action, migration):
    def migration_progress_callback(self, action, migration, fake=False):
        if self.verbosity >= 1:
            if action == "apply_start":
                self.stdout.write("  Applying %s..." % migration, ending="")
                self.stdout.flush()
            elif action == "apply_success":
                if fake:
                    self.stdout.write(self.style.MIGRATE_SUCCESS(" FAKED"))
                else:
                    self.stdout.write(self.style.MIGRATE_SUCCESS(" OK"))
            elif action == "unapply_start":
                self.stdout.write("  Unapplying %s..." % migration, ending="")
                self.stdout.flush()
            elif action == "unapply_success":
                if fake:
                    self.stdout.write(self.style.MIGRATE_SUCCESS(" FAKED"))
                else:
                    self.stdout.write(self.style.MIGRATE_SUCCESS(" OK"))

    def sync_apps(self, connection, apps):
+29 −8
Original line number Diff line number Diff line
from django.db import migrations
from .loader import MigrationLoader
from .recorder import MigrationRecorder

@@ -81,8 +82,13 @@ class MigrationExecutor(object):
        Runs a migration forwards.
        """
        if self.progress_callback:
            self.progress_callback("apply_start", migration)
            self.progress_callback("apply_start", migration, fake)
        if not fake:
            # Test to see if this is an already-applied initial migration
            if not migration.dependencies and self.detect_soft_applied(migration):
                fake = True
            else:
                # Alright, do it normally
                with self.connection.schema_editor() as schema_editor:
                    project_state = self.loader.graph.project_state((migration.app_label, migration.name), at_end=False)
                    migration.apply(project_state, schema_editor)
@@ -92,16 +98,16 @@ class MigrationExecutor(object):
                self.recorder.record_applied(app_label, name)
        else:
            self.recorder.record_applied(migration.app_label, migration.name)
        # Report prgress
        # Report progress
        if self.progress_callback:
            self.progress_callback("apply_success", migration)
            self.progress_callback("apply_success", migration, fake)

    def unapply_migration(self, migration, fake=False):
        """
        Runs a migration backwards.
        """
        if self.progress_callback:
            self.progress_callback("unapply_start", migration)
            self.progress_callback("unapply_start", migration, fake)
        if not fake:
            with self.connection.schema_editor() as schema_editor:
                project_state = self.loader.graph.project_state((migration.app_label, migration.name), at_end=False)
@@ -114,4 +120,19 @@ class MigrationExecutor(object):
            self.recorder.record_unapplied(migration.app_label, migration.name)
        # Report progress
        if self.progress_callback:
            self.progress_callback("unapply_success", migration)
            self.progress_callback("unapply_success", migration, fake)

    def detect_soft_applied(self, migration):
        """
        Tests whether a migration has been implicity applied - that the
        tables it would create exist. This is intended only for use
        on initial migrations (as it only looks for CreateModel).
        """
        project_state = self.loader.graph.project_state((migration.app_label, migration.name), at_end=True)
        app_cache = project_state.render()
        for operation in migration.operations:
            if isinstance(operation, migrations.CreateModel):
                model = app_cache.get_model(migration.app_label, operation.name)
                if model._meta.db_table not in self.connection.introspection.get_table_list(self.connection.cursor()):
                    return False
        return True
+52 −13
Original line number Diff line number Diff line
@@ -2,9 +2,10 @@ from django.test import TransactionTestCase
from django.test.utils import override_settings
from django.db import connection
from django.db.migrations.executor import MigrationExecutor
from .test_base import MigrationTestBase


class ExecutorTests(TransactionTestCase):
class ExecutorTests(MigrationTestBase):
    """
    Tests the migration executor (full end-to-end running).

@@ -31,13 +32,13 @@ class ExecutorTests(TransactionTestCase):
            ],
        )
        # Were the tables there before?
        self.assertNotIn("migrations_author", connection.introspection.get_table_list(connection.cursor()))
        self.assertNotIn("migrations_book", connection.introspection.get_table_list(connection.cursor()))
        self.assertTableNotExists("migrations_author")
        self.assertTableNotExists("migrations_book")
        # Alright, let's try running it
        executor.migrate([("migrations", "0002_second")])
        # Are the tables there now?
        self.assertIn("migrations_author", connection.introspection.get_table_list(connection.cursor()))
        self.assertIn("migrations_book", connection.introspection.get_table_list(connection.cursor()))
        self.assertTableExists("migrations_author")
        self.assertTableExists("migrations_book")
        # Rebuild the graph to reflect the new DB state
        executor.loader.build_graph()
        # Alright, let's undo what we did
@@ -51,8 +52,8 @@ class ExecutorTests(TransactionTestCase):
        )
        executor.migrate([("migrations", None)])
        # Are the tables gone?
        self.assertNotIn("migrations_author", connection.introspection.get_table_list(connection.cursor()))
        self.assertNotIn("migrations_book", connection.introspection.get_table_list(connection.cursor()))
        self.assertTableNotExists("migrations_author")
        self.assertTableNotExists("migrations_book")

    @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_squashed"})
    def test_run_with_squashed(self):
@@ -73,13 +74,13 @@ class ExecutorTests(TransactionTestCase):
            ],
        )
        # Were the tables there before?
        self.assertNotIn("migrations_author", connection.introspection.get_table_list(connection.cursor()))
        self.assertNotIn("migrations_book", connection.introspection.get_table_list(connection.cursor()))
        self.assertTableNotExists("migrations_author")
        self.assertTableNotExists("migrations_book")
        # Alright, let's try running it
        executor.migrate([("migrations", "0001_squashed_0002")])
        # Are the tables there now?
        self.assertIn("migrations_author", connection.introspection.get_table_list(connection.cursor()))
        self.assertIn("migrations_book", connection.introspection.get_table_list(connection.cursor()))
        self.assertTableExists("migrations_author")
        self.assertTableExists("migrations_book")
        # Rebuild the graph to reflect the new DB state
        executor.loader.build_graph()
        # Alright, let's undo what we did. Should also just use squashed.
@@ -92,8 +93,8 @@ class ExecutorTests(TransactionTestCase):
        )
        executor.migrate([("migrations", None)])
        # Are the tables gone?
        self.assertNotIn("migrations_author", connection.introspection.get_table_list(connection.cursor()))
        self.assertNotIn("migrations_book", connection.introspection.get_table_list(connection.cursor()))
        self.assertTableNotExists("migrations_author")
        self.assertTableNotExists("migrations_book")

    @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations", "sessions": "migrations.test_migrations_2"})
    def test_empty_plan(self):
@@ -128,3 +129,41 @@ class ExecutorTests(TransactionTestCase):
        self.assertEqual(plan, [])
        # Erase all the fake records
        executor.recorder.flush()


    @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
    def test_soft_apply(self):
        """
        Tests detection of initial migrations already having been applied.
        """
        state = {"faked": None}
        def fake_storer(phase, migration, fake):
            state["faked"] = fake
        executor = MigrationExecutor(connection, progress_callback=fake_storer)
        executor.recorder.flush()
        # Were the tables there before?
        self.assertTableNotExists("migrations_author")
        self.assertTableNotExists("migrations_tribble")
        # Run it normally
        executor.migrate([("migrations", "0001_initial")])
        # Are the tables there now?
        self.assertTableExists("migrations_author")
        self.assertTableExists("migrations_tribble")
        # We shouldn't have faked that one
        self.assertEqual(state["faked"], False)
        # Rebuild the graph to reflect the new DB state
        executor.loader.build_graph()
        # Fake-reverse that
        executor.migrate([("migrations", None)], fake=True)
        # Are the tables still there?
        self.assertTableExists("migrations_author")
        self.assertTableExists("migrations_tribble")
        # Make sure that was faked
        self.assertEqual(state["faked"], True)
        # Finally, migrate forwards; this should fake-apply our initial migration
        executor.migrate([("migrations", "0001_initial")])
        self.assertEqual(state["faked"], True)
        # And migrate back to clean up the database
        executor.migrate([("migrations", None)])
        self.assertTableNotExists("migrations_author")
        self.assertTableNotExists("migrations_tribble")