summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
Diffstat (limited to 'django')
-rw-r--r--django/db/migrations/exceptions.py4
-rw-r--r--django/db/migrations/executor.py104
2 files changed, 81 insertions, 27 deletions
diff --git a/django/db/migrations/exceptions.py b/django/db/migrations/exceptions.py
index 8b60a035fe..1e8549a504 100644
--- a/django/db/migrations/exceptions.py
+++ b/django/db/migrations/exceptions.py
@@ -58,3 +58,7 @@ class NodeNotFoundError(LookupError):
class MigrationSchemaMissing(DatabaseError):
pass
+
+
+class InvalidMigrationPlan(ValueError):
+ pass
diff --git a/django/db/migrations/executor.py b/django/db/migrations/executor.py
index c25aee6193..3779b626ec 100644
--- a/django/db/migrations/executor.py
+++ b/django/db/migrations/executor.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.apps.registry import apps as global_apps
from django.db import migrations
+from .exceptions import InvalidMigrationPlan
from .loader import MigrationLoader
from .recorder import MigrationRecorder
from .state import ProjectState
@@ -71,46 +72,95 @@ class MigrationExecutor(object):
"""
if plan is None:
plan = self.migration_plan(targets)
- 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)
- # Holds all states right before a migration is applied
- # if the migration is being run.
+
+ all_forwards = all(not backwards for mig, backwards in plan)
+ all_backwards = all(backwards for mig, backwards in plan)
+
+ if not plan:
+ pass # Nothing to do for an empty plan
+ elif all_forwards == all_backwards:
+ # This should only happen if there's a mixed plan
+ raise InvalidMigrationPlan(
+ "Migration plans with both forwards and backwards migrations "
+ "are not supported. Please split your migration process into "
+ "separate plans of only forwards OR backwards migrations.",
+ plan
+ )
+ elif all_forwards:
+ self._migrate_all_forwards(plan, full_plan, fake=fake, fake_initial=fake_initial)
+ else:
+ # No need to check for `elif all_backwards` here, as that condition
+ # would always evaluate to true.
+ self._migrate_all_backwards(plan, full_plan, fake=fake)
+
+ self.check_replacements()
+
+ def _migrate_all_forwards(self, plan, full_plan, fake, fake_initial):
+ """
+ Take a list of 2-tuples of the form (migration instance, False) and
+ apply them in the order they occur in the full_plan.
+ """
+ migrations_to_run = {m[0] for m in plan}
+ state = ProjectState(real_apps=list(self.loader.unmigrated_apps))
+ for migration, _ in full_plan:
+ if not migrations_to_run:
+ # We remove every migration that we applied from this set so
+ # that we can bail out once the last migration has been applied
+ # and don't always run until the very end of the migration
+ # process.
+ break
+ if migration in migrations_to_run:
+ if 'apps' not in state.__dict__:
+ if self.progress_callback:
+ self.progress_callback("render_start")
+ state.apps # Render all -- performance critical
+ if self.progress_callback:
+ self.progress_callback("render_success")
+ state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial)
+ migrations_to_run.remove(migration)
+ else:
+ migration.mutate_state(state, preserve=False)
+
+ def _migrate_all_backwards(self, plan, full_plan, fake):
+ """
+ Take a list of 2-tuples of the form (migration instance, True) and
+ unapply them in reverse order they occur in the full_plan.
+
+ Since unapplying a migration requires the project state prior to that
+ migration, Django will compute the migration states before each of them
+ in a first run over the plan and then unapply them in a second run over
+ the plan.
+ """
+ migrations_to_run = {m[0] for m in plan}
+ # Holds all migration states prior to the migrations being unapplied
states = {}
state = ProjectState(real_apps=list(self.loader.unmigrated_apps))
if self.progress_callback:
self.progress_callback("render_start")
- # 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()`.
for migration, _ in full_plan:
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.
+ # We remove every migration that we applied from this set so
+ # that we can bail out once the last migration has been applied
+ # and don't always run until the very end of the migration
+ # process.
break
- do_run = migration in migrations_to_run
- if do_run:
+ if migration in migrations_to_run:
if 'apps' not in state.__dict__:
- state.apps # Render all real_apps -- performance critical
- states[migration] = state.clone()
+ state.apps # Render all -- performance critical
+ # The state before this migration
+ states[migration] = state
+ # The old state keeps as-is, we continue with the new state
+ state = migration.mutate_state(state, preserve=True)
migrations_to_run.remove(migration)
- # Only preserve the state if the migration is being run later
- state = migration.mutate_state(state, preserve=do_run)
+ else:
+ migration.mutate_state(state, preserve=False)
if self.progress_callback:
self.progress_callback("render_success")
- # Phase 2 -- Run the migrations
- for migration, backwards in plan:
- if not backwards:
- self.apply_migration(states[migration], migration, fake=fake, fake_initial=fake_initial)
- else:
- self.unapply_migration(states[migration], migration, fake=fake)
- self.check_replacements()
+
+ for migration, _ in plan:
+ self.unapply_migration(states[migration], migration, fake=fake)
def collect_sql(self, plan):
"""