Loading django/core/management/commands/migrate.py +9 −3 Original line number Diff line number Diff line Loading @@ -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): Loading django/db/migrations/executor.py +29 −8 Original line number Diff line number Diff line from django.db import migrations from .loader import MigrationLoader from .recorder import MigrationRecorder Loading Loading @@ -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) Loading @@ -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) Loading @@ -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 tests/migrations/test_executor.py +52 −13 Original line number Diff line number Diff line Loading @@ -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). Loading @@ -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 Loading @@ -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): Loading @@ -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. Loading @@ -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): Loading Loading @@ -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") Loading
django/core/management/commands/migrate.py +9 −3 Original line number Diff line number Diff line Loading @@ -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): Loading
django/db/migrations/executor.py +29 −8 Original line number Diff line number Diff line from django.db import migrations from .loader import MigrationLoader from .recorder import MigrationRecorder Loading Loading @@ -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) Loading @@ -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) Loading @@ -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
tests/migrations/test_executor.py +52 −13 Original line number Diff line number Diff line Loading @@ -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). Loading @@ -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 Loading @@ -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): Loading @@ -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. Loading @@ -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): Loading Loading @@ -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")