2014-05-06 01:50:51 +08:00
|
|
|
from __future__ import unicode_literals
|
|
|
|
|
2014-07-26 00:35:25 +08:00
|
|
|
from django.apps.registry import apps as global_apps
|
2015-01-28 20:35:27 +08:00
|
|
|
from django.db import migrations
|
|
|
|
|
2013-05-31 01:08:58 +08:00
|
|
|
from .loader import MigrationLoader
|
|
|
|
from .recorder import MigrationRecorder
|
2015-01-12 03:13:31 +08:00
|
|
|
from .state import ProjectState
|
2013-05-31 01:08:58 +08:00
|
|
|
|
|
|
|
|
|
|
|
class MigrationExecutor(object):
|
|
|
|
"""
|
|
|
|
End-to-end migration execution - loads migrations, and runs them
|
|
|
|
up or down to a specified set of targets.
|
|
|
|
"""
|
|
|
|
|
2013-06-08 01:47:17 +08:00
|
|
|
def __init__(self, connection, progress_callback=None):
|
2013-05-31 01:08:58 +08:00
|
|
|
self.connection = connection
|
|
|
|
self.loader = MigrationLoader(self.connection)
|
|
|
|
self.recorder = MigrationRecorder(self.connection)
|
2013-06-08 01:47:17 +08:00
|
|
|
self.progress_callback = progress_callback
|
2013-05-31 01:08:58 +08:00
|
|
|
|
2015-01-12 03:13:31 +08:00
|
|
|
def migration_plan(self, targets, clean_start=False):
|
2013-05-31 01:08:58 +08:00
|
|
|
"""
|
|
|
|
Given a set of targets, returns a list of (Migration instance, backwards?).
|
|
|
|
"""
|
|
|
|
plan = []
|
2015-01-12 03:13:31 +08:00
|
|
|
if clean_start:
|
|
|
|
applied = set()
|
|
|
|
else:
|
|
|
|
applied = set(self.loader.applied_migrations)
|
2013-05-31 01:08:58 +08:00
|
|
|
for target in targets:
|
2013-12-28 16:53:02 +08:00
|
|
|
# If the target is (app_label, None), that means unmigrate everything
|
2013-07-23 02:43:58 +08:00
|
|
|
if target[1] is None:
|
|
|
|
for root in self.loader.graph.root_nodes():
|
|
|
|
if root[0] == target[0]:
|
|
|
|
for migration in self.loader.graph.backwards_plan(root):
|
|
|
|
if migration in applied:
|
|
|
|
plan.append((self.loader.graph.nodes[migration], True))
|
|
|
|
applied.remove(migration)
|
2013-05-31 01:08:58 +08:00
|
|
|
# If the migration is already applied, do backwards mode,
|
|
|
|
# otherwise do forwards mode.
|
2013-07-23 02:43:58 +08:00
|
|
|
elif target in applied:
|
2014-11-18 01:13:47 +08:00
|
|
|
# Don't migrate backwards all the way to the target node (that
|
|
|
|
# may roll back dependencies in other apps that don't need to
|
|
|
|
# be rolled back); instead roll back through target's immediate
|
|
|
|
# child(ren) in the same app, and no further.
|
|
|
|
next_in_app = sorted(
|
|
|
|
n for n in
|
2015-02-20 12:43:45 +08:00
|
|
|
self.loader.graph.node_map[target].children
|
2014-11-18 01:13:47 +08:00
|
|
|
if n[0] == target[0]
|
|
|
|
)
|
|
|
|
for node in next_in_app:
|
|
|
|
for migration in self.loader.graph.backwards_plan(node):
|
2013-08-11 22:28:51 +08:00
|
|
|
if migration in applied:
|
|
|
|
plan.append((self.loader.graph.nodes[migration], True))
|
|
|
|
applied.remove(migration)
|
2013-05-31 01:08:58 +08:00
|
|
|
else:
|
|
|
|
for migration in self.loader.graph.forwards_plan(target):
|
|
|
|
if migration not in applied:
|
|
|
|
plan.append((self.loader.graph.nodes[migration], False))
|
|
|
|
applied.add(migration)
|
|
|
|
return plan
|
|
|
|
|
2015-02-12 19:48:28 +08:00
|
|
|
def migrate(self, targets, plan=None, fake=False, fake_initial=False):
|
2013-05-31 01:08:58 +08:00
|
|
|
"""
|
|
|
|
Migrates the database up to the given targets.
|
2015-01-12 03:13:31 +08:00
|
|
|
|
|
|
|
Django first needs to create all project states before a migration is
|
|
|
|
(un)applied and in a second step run all the database operations.
|
2013-05-31 01:08:58 +08:00
|
|
|
"""
|
2013-06-08 01:47:17 +08:00
|
|
|
if plan is None:
|
|
|
|
plan = self.migration_plan(targets)
|
2015-01-12 03:13:31 +08:00
|
|
|
migrations_to_run = {m[0] for m in plan}
|
|
|
|
# Create the forwards plan Django would follow on an empty database
|
|
|
|
full_plan = self.migration_plan(self.loader.graph.leaf_nodes(), clean_start=True)
|
2015-04-01 05:35:41 +08:00
|
|
|
# Holds all states right before a migration is applied
|
2015-01-12 03:13:31 +08:00
|
|
|
# if the migration is being run.
|
|
|
|
states = {}
|
|
|
|
state = ProjectState(real_apps=list(self.loader.unmigrated_apps))
|
2015-01-12 04:07:45 +08:00
|
|
|
if self.progress_callback:
|
|
|
|
self.progress_callback("render_start")
|
2015-04-01 05:35:41 +08:00
|
|
|
# Phase 1 -- Store all project states of migrations right before they
|
|
|
|
# are applied. The first migration that will be applied in phase 2 will
|
|
|
|
# trigger the rendering of the initial project state. From this time on
|
|
|
|
# models will be recursively reloaded as explained in
|
|
|
|
# `django.db.migrations.state.get_related_models_recursive()`.
|
2015-01-12 03:13:31 +08:00
|
|
|
for migration, _ in full_plan:
|
2015-04-01 05:35:48 +08:00
|
|
|
if not migrations_to_run:
|
|
|
|
# We remove every migration whose state was already computed
|
|
|
|
# from the set below (`migrations_to_run.remove(migration)`).
|
|
|
|
# If no states for migrations must be computed, we can exit
|
|
|
|
# this loop. Migrations that occur after the latest migration
|
|
|
|
# that is about to be applied would only trigger unneeded
|
|
|
|
# mutate_state() calls.
|
|
|
|
break
|
2015-04-01 05:35:41 +08:00
|
|
|
do_run = migration in migrations_to_run
|
|
|
|
if do_run:
|
|
|
|
if 'apps' not in state.__dict__:
|
|
|
|
state.apps # Render all real_apps -- performance critical
|
2015-01-12 03:13:31 +08:00
|
|
|
states[migration] = state.clone()
|
2015-04-01 05:35:48 +08:00
|
|
|
migrations_to_run.remove(migration)
|
2015-04-01 05:35:41 +08:00
|
|
|
# Only preserve the state if the migration is being run later
|
|
|
|
state = migration.mutate_state(state, preserve=do_run)
|
|
|
|
if self.progress_callback:
|
|
|
|
self.progress_callback("render_success")
|
2015-01-12 03:13:31 +08:00
|
|
|
# Phase 2 -- Run the migrations
|
2013-05-31 01:08:58 +08:00
|
|
|
for migration, backwards in plan:
|
|
|
|
if not backwards:
|
2015-02-12 19:48:28 +08:00
|
|
|
self.apply_migration(states[migration], migration, fake=fake, fake_initial=fake_initial)
|
2013-05-31 01:08:58 +08:00
|
|
|
else:
|
2015-01-12 03:13:31 +08:00
|
|
|
self.unapply_migration(states[migration], migration, fake=fake)
|
2013-05-31 01:08:58 +08:00
|
|
|
|
2013-09-07 04:27:51 +08:00
|
|
|
def collect_sql(self, plan):
|
|
|
|
"""
|
|
|
|
Takes a migration plan and returns a list of collected SQL
|
|
|
|
statements that represent the best-efforts version of that plan.
|
|
|
|
"""
|
|
|
|
statements = []
|
2014-10-27 01:30:58 +08:00
|
|
|
state = None
|
2013-09-07 04:27:51 +08:00
|
|
|
for migration, backwards in plan:
|
|
|
|
with self.connection.schema_editor(collect_sql=True) as schema_editor:
|
2014-10-27 01:30:58 +08:00
|
|
|
if state is None:
|
|
|
|
state = self.loader.project_state((migration.app_label, migration.name), at_end=False)
|
2013-09-07 04:27:51 +08:00
|
|
|
if not backwards:
|
2014-10-27 01:30:58 +08:00
|
|
|
state = migration.apply(state, schema_editor, collect_sql=True)
|
2013-09-07 04:27:51 +08:00
|
|
|
else:
|
2014-10-27 01:30:58 +08:00
|
|
|
state = migration.unapply(state, schema_editor, collect_sql=True)
|
2013-11-28 00:29:37 +08:00
|
|
|
statements.extend(schema_editor.collected_sql)
|
2013-09-07 04:27:51 +08:00
|
|
|
return statements
|
|
|
|
|
2015-02-12 19:48:28 +08:00
|
|
|
def apply_migration(self, state, migration, fake=False, fake_initial=False):
|
2013-05-31 01:08:58 +08:00
|
|
|
"""
|
|
|
|
Runs a migration forwards.
|
|
|
|
"""
|
2013-06-08 01:47:17 +08:00
|
|
|
if self.progress_callback:
|
2013-10-30 23:17:28 +08:00
|
|
|
self.progress_callback("apply_start", migration, fake)
|
2013-07-26 23:47:00 +08:00
|
|
|
if not fake:
|
2015-02-12 19:48:28 +08:00
|
|
|
if fake_initial:
|
|
|
|
# Test to see if this is an already-applied initial migration
|
|
|
|
applied, state = self.detect_soft_applied(state, migration)
|
|
|
|
if applied:
|
|
|
|
fake = True
|
|
|
|
if not fake:
|
2013-10-30 23:17:28 +08:00
|
|
|
# Alright, do it normally
|
|
|
|
with self.connection.schema_editor() as schema_editor:
|
2014-11-06 03:53:39 +08:00
|
|
|
state = migration.apply(state, schema_editor)
|
2013-10-24 05:56:54 +08:00
|
|
|
# For replacement migrations, record individual statuses
|
|
|
|
if migration.replaces:
|
|
|
|
for app_label, name in migration.replaces:
|
|
|
|
self.recorder.record_applied(app_label, name)
|
|
|
|
else:
|
|
|
|
self.recorder.record_applied(migration.app_label, migration.name)
|
2013-10-30 23:17:28 +08:00
|
|
|
# Report progress
|
2013-06-08 01:47:17 +08:00
|
|
|
if self.progress_callback:
|
2013-10-30 23:17:28 +08:00
|
|
|
self.progress_callback("apply_success", migration, fake)
|
2014-10-27 01:30:58 +08:00
|
|
|
return state
|
2013-05-31 01:08:58 +08:00
|
|
|
|
2014-10-27 01:30:58 +08:00
|
|
|
def unapply_migration(self, state, migration, fake=False):
|
2013-05-31 01:08:58 +08:00
|
|
|
"""
|
|
|
|
Runs a migration backwards.
|
|
|
|
"""
|
2013-06-08 01:47:17 +08:00
|
|
|
if self.progress_callback:
|
2013-10-30 23:17:28 +08:00
|
|
|
self.progress_callback("unapply_start", migration, fake)
|
2013-07-26 23:47:00 +08:00
|
|
|
if not fake:
|
|
|
|
with self.connection.schema_editor() as schema_editor:
|
2014-11-06 03:53:39 +08:00
|
|
|
state = migration.unapply(state, schema_editor)
|
2013-10-24 05:56:54 +08:00
|
|
|
# For replacement migrations, record individual statuses
|
|
|
|
if migration.replaces:
|
|
|
|
for app_label, name in migration.replaces:
|
|
|
|
self.recorder.record_unapplied(app_label, name)
|
|
|
|
else:
|
|
|
|
self.recorder.record_unapplied(migration.app_label, migration.name)
|
|
|
|
# Report progress
|
2013-06-08 01:47:17 +08:00
|
|
|
if self.progress_callback:
|
2013-10-30 23:17:28 +08:00
|
|
|
self.progress_callback("unapply_success", migration, fake)
|
2014-10-27 01:30:58 +08:00
|
|
|
return state
|
2013-10-30 23:17:28 +08:00
|
|
|
|
2014-10-27 01:30:58 +08:00
|
|
|
def detect_soft_applied(self, project_state, migration):
|
2013-10-30 23:17:28 +08:00
|
|
|
"""
|
2014-03-02 22:25:53 +08:00
|
|
|
Tests whether a migration has been implicitly applied - that the
|
2013-10-30 23:17:28 +08:00
|
|
|
tables it would create exist. This is intended only for use
|
|
|
|
on initial migrations (as it only looks for CreateModel).
|
|
|
|
"""
|
2014-07-30 00:24:07 +08:00
|
|
|
# Bail if the migration isn't the first one in its app
|
|
|
|
if [name for app, name in migration.dependencies if app == migration.app_label]:
|
2014-11-06 03:53:39 +08:00
|
|
|
return False, project_state
|
|
|
|
if project_state is None:
|
|
|
|
after_state = self.loader.project_state((migration.app_label, migration.name), at_end=True)
|
|
|
|
else:
|
|
|
|
after_state = migration.mutate_state(project_state)
|
|
|
|
apps = after_state.apps
|
|
|
|
found_create_migration = False
|
2014-07-30 00:24:07 +08:00
|
|
|
# Make sure all create model are done
|
2013-10-30 23:17:28 +08:00
|
|
|
for operation in migration.operations:
|
|
|
|
if isinstance(operation, migrations.CreateModel):
|
2013-12-24 19:25:17 +08:00
|
|
|
model = apps.get_model(migration.app_label, operation.name)
|
2014-07-26 00:35:25 +08:00
|
|
|
if model._meta.swapped:
|
|
|
|
# We have to fetch the model to test with from the
|
|
|
|
# main app cache, as it's not a direct dependency.
|
|
|
|
model = global_apps.get_model(model._meta.swapped)
|
2014-09-21 06:29:13 +08:00
|
|
|
if model._meta.db_table not in self.connection.introspection.table_names(self.connection.cursor()):
|
2014-11-06 03:53:39 +08:00
|
|
|
return False, project_state
|
2014-06-23 09:29:27 +08:00
|
|
|
found_create_migration = True
|
|
|
|
# If we get this far and we found at least one CreateModel migration,
|
|
|
|
# the migration is considered implicitly applied.
|
2014-11-06 03:53:39 +08:00
|
|
|
return found_create_migration, after_state
|