Auto-apply initial migrations if their tables exist already.
This commit is contained in:
parent
eafe279120
commit
e9cb333bc3
|
@ -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."
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue