|
3 | 3 | from django.apps.registry import apps as global_apps
|
4 | 4 | from django.db import migrations
|
5 | 5 |
|
| 6 | +from .exceptions import InvalidMigrationPlan |
6 | 7 | from .loader import MigrationLoader
|
7 | 8 | from .recorder import MigrationRecorder
|
8 | 9 | from .state import ProjectState
|
@@ -71,46 +72,95 @@ def migrate(self, targets, plan=None, fake=False, fake_initial=False):
|
71 | 72 | """
|
72 | 73 | if plan is None:
|
73 | 74 | plan = self.migration_plan(targets)
|
74 |
| - migrations_to_run = {m[0] for m in plan} |
75 | 75 | # Create the forwards plan Django would follow on an empty database
|
76 | 76 | full_plan = self.migration_plan(self.loader.graph.leaf_nodes(), clean_start=True)
|
77 |
| - # Holds all states right before a migration is applied |
78 |
| - # if the migration is being run. |
| 77 | + |
| 78 | + all_forwards = all(not backwards for mig, backwards in plan) |
| 79 | + all_backwards = all(backwards for mig, backwards in plan) |
| 80 | + |
| 81 | + if not plan: |
| 82 | + pass # Nothing to do for an empty plan |
| 83 | + elif all_forwards == all_backwards: |
| 84 | + # This should only happen if there's a mixed plan |
| 85 | + raise InvalidMigrationPlan( |
| 86 | + "Migration plans with both forwards and backwards migrations " |
| 87 | + "are not supported. Please split your migration process into " |
| 88 | + "separate plans of only forwards OR backwards migrations.", |
| 89 | + plan |
| 90 | + ) |
| 91 | + elif all_forwards: |
| 92 | + self._migrate_all_forwards(plan, full_plan, fake=fake, fake_initial=fake_initial) |
| 93 | + else: |
| 94 | + # No need to check for `elif all_backwards` here, as that condition |
| 95 | + # would always evaluate to true. |
| 96 | + self._migrate_all_backwards(plan, full_plan, fake=fake) |
| 97 | + |
| 98 | + self.check_replacements() |
| 99 | + |
| 100 | + def _migrate_all_forwards(self, plan, full_plan, fake, fake_initial): |
| 101 | + """ |
| 102 | + Take a list of 2-tuples of the form (migration instance, False) and |
| 103 | + apply them in the order they occur in the full_plan. |
| 104 | + """ |
| 105 | + migrations_to_run = {m[0] for m in plan} |
| 106 | + state = ProjectState(real_apps=list(self.loader.unmigrated_apps)) |
| 107 | + for migration, _ in full_plan: |
| 108 | + if not migrations_to_run: |
| 109 | + # We remove every migration that we applied from this set so |
| 110 | + # that we can bail out once the last migration has been applied |
| 111 | + # and don't always run until the very end of the migration |
| 112 | + # process. |
| 113 | + break |
| 114 | + if migration in migrations_to_run: |
| 115 | + if 'apps' not in state.__dict__: |
| 116 | + if self.progress_callback: |
| 117 | + self.progress_callback("render_start") |
| 118 | + state.apps # Render all -- performance critical |
| 119 | + if self.progress_callback: |
| 120 | + self.progress_callback("render_success") |
| 121 | + state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial) |
| 122 | + migrations_to_run.remove(migration) |
| 123 | + else: |
| 124 | + migration.mutate_state(state, preserve=False) |
| 125 | + |
| 126 | + def _migrate_all_backwards(self, plan, full_plan, fake): |
| 127 | + """ |
| 128 | + Take a list of 2-tuples of the form (migration instance, True) and |
| 129 | + unapply them in reverse order they occur in the full_plan. |
| 130 | +
|
| 131 | + Since unapplying a migration requires the project state prior to that |
| 132 | + migration, Django will compute the migration states before each of them |
| 133 | + in a first run over the plan and then unapply them in a second run over |
| 134 | + the plan. |
| 135 | + """ |
| 136 | + migrations_to_run = {m[0] for m in plan} |
| 137 | + # Holds all migration states prior to the migrations being unapplied |
79 | 138 | states = {}
|
80 | 139 | state = ProjectState(real_apps=list(self.loader.unmigrated_apps))
|
81 | 140 | if self.progress_callback:
|
82 | 141 | self.progress_callback("render_start")
|
83 |
| - # Phase 1 -- Store all project states of migrations right before they |
84 |
| - # are applied. The first migration that will be applied in phase 2 will |
85 |
| - # trigger the rendering of the initial project state. From this time on |
86 |
| - # models will be recursively reloaded as explained in |
87 |
| - # `django.db.migrations.state.get_related_models_recursive()`. |
88 | 142 | for migration, _ in full_plan:
|
89 | 143 | if not migrations_to_run:
|
90 |
| - # We remove every migration whose state was already computed |
91 |
| - # from the set below (`migrations_to_run.remove(migration)`). |
92 |
| - # If no states for migrations must be computed, we can exit |
93 |
| - # this loop. Migrations that occur after the latest migration |
94 |
| - # that is about to be applied would only trigger unneeded |
95 |
| - # mutate_state() calls. |
| 144 | + # We remove every migration that we applied from this set so |
| 145 | + # that we can bail out once the last migration has been applied |
| 146 | + # and don't always run until the very end of the migration |
| 147 | + # process. |
96 | 148 | break
|
97 |
| - do_run = migration in migrations_to_run |
98 |
| - if do_run: |
| 149 | + if migration in migrations_to_run: |
99 | 150 | if 'apps' not in state.__dict__:
|
100 |
| - state.apps # Render all real_apps -- performance critical |
101 |
| - states[migration] = state.clone() |
| 151 | + state.apps # Render all -- performance critical |
| 152 | + # The state before this migration |
| 153 | + states[migration] = state |
| 154 | + # The old state keeps as-is, we continue with the new state |
| 155 | + state = migration.mutate_state(state, preserve=True) |
102 | 156 | migrations_to_run.remove(migration)
|
103 |
| - # Only preserve the state if the migration is being run later |
104 |
| - state = migration.mutate_state(state, preserve=do_run) |
| 157 | + else: |
| 158 | + migration.mutate_state(state, preserve=False) |
105 | 159 | if self.progress_callback:
|
106 | 160 | self.progress_callback("render_success")
|
107 |
| - # Phase 2 -- Run the migrations |
108 |
| - for migration, backwards in plan: |
109 |
| - if not backwards: |
110 |
| - self.apply_migration(states[migration], migration, fake=fake, fake_initial=fake_initial) |
111 |
| - else: |
112 |
| - self.unapply_migration(states[migration], migration, fake=fake) |
113 |
| - self.check_replacements() |
| 161 | + |
| 162 | + for migration, _ in plan: |
| 163 | + self.unapply_migration(states[migration], migration, fake=fake) |
114 | 164 |
|
115 | 165 | def collect_sql(self, plan):
|
116 | 166 | """
|
|
0 commit comments