Auto-apply initial migrations if their tables exist already.

This commit is contained in:
Andrew Godwin 2013-10-30 15:17:28 +00:00
parent eafe279120
commit e9cb333bc3
3 changed files with 90 additions and 24 deletions

View File

@ -127,18 +127,24 @@ class Command(BaseCommand):
# to do at this point. # to do at this point.
emit_post_migrate_signal(created_models, self.verbosity, self.interactive, connection.alias) 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 self.verbosity >= 1:
if action == "apply_start": if action == "apply_start":
self.stdout.write(" Applying %s..." % migration, ending="") self.stdout.write(" Applying %s..." % migration, ending="")
self.stdout.flush() self.stdout.flush()
elif action == "apply_success": elif action == "apply_success":
self.stdout.write(self.style.MIGRATE_SUCCESS(" OK")) if fake:
self.stdout.write(self.style.MIGRATE_SUCCESS(" FAKED"))
else:
self.stdout.write(self.style.MIGRATE_SUCCESS(" OK"))
elif action == "unapply_start": elif action == "unapply_start":
self.stdout.write(" Unapplying %s..." % migration, ending="") self.stdout.write(" Unapplying %s..." % migration, ending="")
self.stdout.flush() self.stdout.flush()
elif action == "unapply_success": elif action == "unapply_success":
self.stdout.write(self.style.MIGRATE_SUCCESS(" OK")) 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): def sync_apps(self, connection, apps):
"Runs the old syncdb-style operation on a list of apps." "Runs the old syncdb-style operation on a list of apps."

View File

@ -1,3 +1,4 @@
from django.db import migrations
from .loader import MigrationLoader from .loader import MigrationLoader
from .recorder import MigrationRecorder from .recorder import MigrationRecorder
@ -81,27 +82,32 @@ class MigrationExecutor(object):
Runs a migration forwards. Runs a migration forwards.
""" """
if self.progress_callback: if self.progress_callback:
self.progress_callback("apply_start", migration) self.progress_callback("apply_start", migration, fake)
if not fake: if not fake:
with self.connection.schema_editor() as schema_editor: # Test to see if this is an already-applied initial migration
project_state = self.loader.graph.project_state((migration.app_label, migration.name), at_end=False) if not migration.dependencies and self.detect_soft_applied(migration):
migration.apply(project_state, schema_editor) 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)
# For replacement migrations, record individual statuses # For replacement migrations, record individual statuses
if migration.replaces: if migration.replaces:
for app_label, name in migration.replaces: for app_label, name in migration.replaces:
self.recorder.record_applied(app_label, name) self.recorder.record_applied(app_label, name)
else: else:
self.recorder.record_applied(migration.app_label, migration.name) self.recorder.record_applied(migration.app_label, migration.name)
# Report prgress # Report progress
if self.progress_callback: if self.progress_callback:
self.progress_callback("apply_success", migration) self.progress_callback("apply_success", migration, fake)
def unapply_migration(self, migration, fake=False): def unapply_migration(self, migration, fake=False):
""" """
Runs a migration backwards. Runs a migration backwards.
""" """
if self.progress_callback: if self.progress_callback:
self.progress_callback("unapply_start", migration) self.progress_callback("unapply_start", migration, fake)
if not fake: if not fake:
with self.connection.schema_editor() as schema_editor: with self.connection.schema_editor() as schema_editor:
project_state = self.loader.graph.project_state((migration.app_label, migration.name), at_end=False) 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) self.recorder.record_unapplied(migration.app_label, migration.name)
# Report progress # Report progress
if self.progress_callback: 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

View File

@ -2,9 +2,10 @@ from django.test import TransactionTestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.db import connection from django.db import connection
from django.db.migrations.executor import MigrationExecutor 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). Tests the migration executor (full end-to-end running).
@ -31,13 +32,13 @@ class ExecutorTests(TransactionTestCase):
], ],
) )
# Were the tables there before? # Were the tables there before?
self.assertNotIn("migrations_author", connection.introspection.get_table_list(connection.cursor())) self.assertTableNotExists("migrations_author")
self.assertNotIn("migrations_book", connection.introspection.get_table_list(connection.cursor())) self.assertTableNotExists("migrations_book")
# Alright, let's try running it # Alright, let's try running it
executor.migrate([("migrations", "0002_second")]) executor.migrate([("migrations", "0002_second")])
# Are the tables there now? # Are the tables there now?
self.assertIn("migrations_author", connection.introspection.get_table_list(connection.cursor())) self.assertTableExists("migrations_author")
self.assertIn("migrations_book", connection.introspection.get_table_list(connection.cursor())) self.assertTableExists("migrations_book")
# Rebuild the graph to reflect the new DB state # Rebuild the graph to reflect the new DB state
executor.loader.build_graph() executor.loader.build_graph()
# Alright, let's undo what we did # Alright, let's undo what we did
@ -51,8 +52,8 @@ class ExecutorTests(TransactionTestCase):
) )
executor.migrate([("migrations", None)]) executor.migrate([("migrations", None)])
# Are the tables gone? # Are the tables gone?
self.assertNotIn("migrations_author", connection.introspection.get_table_list(connection.cursor())) self.assertTableNotExists("migrations_author")
self.assertNotIn("migrations_book", connection.introspection.get_table_list(connection.cursor())) self.assertTableNotExists("migrations_book")
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_squashed"}) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_squashed"})
def test_run_with_squashed(self): def test_run_with_squashed(self):
@ -73,13 +74,13 @@ class ExecutorTests(TransactionTestCase):
], ],
) )
# Were the tables there before? # Were the tables there before?
self.assertNotIn("migrations_author", connection.introspection.get_table_list(connection.cursor())) self.assertTableNotExists("migrations_author")
self.assertNotIn("migrations_book", connection.introspection.get_table_list(connection.cursor())) self.assertTableNotExists("migrations_book")
# Alright, let's try running it # Alright, let's try running it
executor.migrate([("migrations", "0001_squashed_0002")]) executor.migrate([("migrations", "0001_squashed_0002")])
# Are the tables there now? # Are the tables there now?
self.assertIn("migrations_author", connection.introspection.get_table_list(connection.cursor())) self.assertTableExists("migrations_author")
self.assertIn("migrations_book", connection.introspection.get_table_list(connection.cursor())) self.assertTableExists("migrations_book")
# Rebuild the graph to reflect the new DB state # Rebuild the graph to reflect the new DB state
executor.loader.build_graph() executor.loader.build_graph()
# Alright, let's undo what we did. Should also just use squashed. # Alright, let's undo what we did. Should also just use squashed.
@ -92,8 +93,8 @@ class ExecutorTests(TransactionTestCase):
) )
executor.migrate([("migrations", None)]) executor.migrate([("migrations", None)])
# Are the tables gone? # Are the tables gone?
self.assertNotIn("migrations_author", connection.introspection.get_table_list(connection.cursor())) self.assertTableNotExists("migrations_author")
self.assertNotIn("migrations_book", connection.introspection.get_table_list(connection.cursor())) self.assertTableNotExists("migrations_book")
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations", "sessions": "migrations.test_migrations_2"}) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations", "sessions": "migrations.test_migrations_2"})
def test_empty_plan(self): def test_empty_plan(self):
@ -128,3 +129,41 @@ class ExecutorTests(TransactionTestCase):
self.assertEqual(plan, []) self.assertEqual(plan, [])
# Erase all the fake records # Erase all the fake records
executor.recorder.flush() 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")