You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

312 lines
14 KiB

  1. from __future__ import unicode_literals
  2. from django.apps.registry import apps as global_apps
  3. from django.db import migrations
  4. from .exceptions import InvalidMigrationPlan
  5. from .loader import MigrationLoader
  6. from .recorder import MigrationRecorder
  7. from .state import ProjectState
  8. class MigrationExecutor(object):
  9. """
  10. End-to-end migration execution - loads migrations, and runs them
  11. up or down to a specified set of targets.
  12. """
  13. def __init__(self, connection, progress_callback=None):
  14. self.connection = connection
  15. self.loader = MigrationLoader(self.connection)
  16. self.recorder = MigrationRecorder(self.connection)
  17. self.progress_callback = progress_callback
  18. def migration_plan(self, targets, clean_start=False):
  19. """
  20. Given a set of targets, returns a list of (Migration instance, backwards?).
  21. """
  22. plan = []
  23. if clean_start:
  24. applied = set()
  25. else:
  26. applied = set(self.loader.applied_migrations)
  27. for target in targets:
  28. # If the target is (app_label, None), that means unmigrate everything
  29. if target[1] is None:
  30. for root in self.loader.graph.root_nodes():
  31. if root[0] == target[0]:
  32. for migration in self.loader.graph.backwards_plan(root):
  33. if migration in applied:
  34. plan.append((self.loader.graph.nodes[migration], True))
  35. applied.remove(migration)
  36. # If the migration is already applied, do backwards mode,
  37. # otherwise do forwards mode.
  38. elif target in applied:
  39. # Don't migrate backwards all the way to the target node (that
  40. # may roll back dependencies in other apps that don't need to
  41. # be rolled back); instead roll back through target's immediate
  42. # child(ren) in the same app, and no further.
  43. next_in_app = sorted(
  44. n for n in
  45. self.loader.graph.node_map[target].children
  46. if n[0] == target[0]
  47. )
  48. for node in next_in_app:
  49. for migration in self.loader.graph.backwards_plan(node):
  50. if migration in applied:
  51. plan.append((self.loader.graph.nodes[migration], True))
  52. applied.remove(migration)
  53. else:
  54. for migration in self.loader.graph.forwards_plan(target):
  55. if migration not in applied:
  56. plan.append((self.loader.graph.nodes[migration], False))
  57. applied.add(migration)
  58. return plan
  59. def migrate(self, targets, plan=None, fake=False, fake_initial=False):
  60. """
  61. Migrates the database up to the given targets.
  62. Django first needs to create all project states before a migration is
  63. (un)applied and in a second step run all the database operations.
  64. """
  65. if plan is None:
  66. plan = self.migration_plan(targets)
  67. # Create the forwards plan Django would follow on an empty database
  68. full_plan = self.migration_plan(self.loader.graph.leaf_nodes(), clean_start=True)
  69. all_forwards = all(not backwards for mig, backwards in plan)
  70. all_backwards = all(backwards for mig, backwards in plan)
  71. if not plan:
  72. pass # Nothing to do for an empty plan
  73. elif all_forwards == all_backwards:
  74. # This should only happen if there's a mixed plan
  75. raise InvalidMigrationPlan(
  76. "Migration plans with both forwards and backwards migrations "
  77. "are not supported. Please split your migration process into "
  78. "separate plans of only forwards OR backwards migrations.",
  79. plan
  80. )
  81. elif all_forwards:
  82. self._migrate_all_forwards(plan, full_plan, fake=fake, fake_initial=fake_initial)
  83. else:
  84. # No need to check for `elif all_backwards` here, as that condition
  85. # would always evaluate to true.
  86. self._migrate_all_backwards(plan, full_plan, fake=fake)
  87. self.check_replacements()
  88. def _migrate_all_forwards(self, plan, full_plan, fake, fake_initial):
  89. """
  90. Take a list of 2-tuples of the form (migration instance, False) and
  91. apply them in the order they occur in the full_plan.
  92. """
  93. migrations_to_run = {m[0] for m in plan}
  94. state = ProjectState(real_apps=list(self.loader.unmigrated_apps))
  95. for migration, _ in full_plan:
  96. if not migrations_to_run:
  97. # We remove every migration that we applied from this set so
  98. # that we can bail out once the last migration has been applied
  99. # and don't always run until the very end of the migration
  100. # process.
  101. break
  102. if migration in migrations_to_run:
  103. if 'apps' not in state.__dict__:
  104. if self.progress_callback:
  105. self.progress_callback("render_start")
  106. state.apps # Render all -- performance critical
  107. if self.progress_callback:
  108. self.progress_callback("render_success")
  109. state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial)
  110. migrations_to_run.remove(migration)
  111. else:
  112. migration.mutate_state(state, preserve=False)
  113. def _migrate_all_backwards(self, plan, full_plan, fake):
  114. """
  115. Take a list of 2-tuples of the form (migration instance, True) and
  116. unapply them in reverse order they occur in the full_plan.
  117. Since unapplying a migration requires the project state prior to that
  118. migration, Django will compute the migration states before each of them
  119. in a first run over the plan and then unapply them in a second run over
  120. the plan.
  121. """
  122. migrations_to_run = {m[0] for m in plan}
  123. # Holds all migration states prior to the migrations being unapplied
  124. states = {}
  125. state = ProjectState(real_apps=list(self.loader.unmigrated_apps))
  126. if self.progress_callback:
  127. self.progress_callback("render_start")
  128. for migration, _ in full_plan:
  129. if not migrations_to_run:
  130. # We remove every migration that we applied from this set so
  131. # that we can bail out once the last migration has been applied
  132. # and don't always run until the very end of the migration
  133. # process.
  134. break
  135. if migration in migrations_to_run:
  136. if 'apps' not in state.__dict__:
  137. state.apps # Render all -- performance critical
  138. # The state before this migration
  139. states[migration] = state
  140. # The old state keeps as-is, we continue with the new state
  141. state = migration.mutate_state(state, preserve=True)
  142. migrations_to_run.remove(migration)
  143. else:
  144. migration.mutate_state(state, preserve=False)
  145. if self.progress_callback:
  146. self.progress_callback("render_success")
  147. for migration, _ in plan:
  148. self.unapply_migration(states[migration], migration, fake=fake)
  149. def collect_sql(self, plan):
  150. """
  151. Takes a migration plan and returns a list of collected SQL
  152. statements that represent the best-efforts version of that plan.
  153. """
  154. statements = []
  155. state = None
  156. for migration, backwards in plan:
  157. with self.connection.schema_editor(collect_sql=True) as schema_editor:
  158. if state is None:
  159. state = self.loader.project_state((migration.app_label, migration.name), at_end=False)
  160. if not backwards:
  161. state = migration.apply(state, schema_editor, collect_sql=True)
  162. else:
  163. state = migration.unapply(state, schema_editor, collect_sql=True)
  164. statements.extend(schema_editor.collected_sql)
  165. return statements
  166. def apply_migration(self, state, migration, fake=False, fake_initial=False):
  167. """
  168. Runs a migration forwards.
  169. """
  170. if self.progress_callback:
  171. self.progress_callback("apply_start", migration, fake)
  172. if not fake:
  173. if fake_initial:
  174. # Test to see if this is an already-applied initial migration
  175. applied, state = self.detect_soft_applied(state, migration)
  176. if applied:
  177. fake = True
  178. if not fake:
  179. # Alright, do it normally
  180. with self.connection.schema_editor() as schema_editor:
  181. state = migration.apply(state, schema_editor)
  182. # For replacement migrations, record individual statuses
  183. if migration.replaces:
  184. for app_label, name in migration.replaces:
  185. self.recorder.record_applied(app_label, name)
  186. else:
  187. self.recorder.record_applied(migration.app_label, migration.name)
  188. # Report progress
  189. if self.progress_callback:
  190. self.progress_callback("apply_success", migration, fake)
  191. return state
  192. def unapply_migration(self, state, migration, fake=False):
  193. """
  194. Runs a migration backwards.
  195. """
  196. if self.progress_callback:
  197. self.progress_callback("unapply_start", migration, fake)
  198. if not fake:
  199. with self.connection.schema_editor() as schema_editor:
  200. state = migration.unapply(state, schema_editor)
  201. # For replacement migrations, record individual statuses
  202. if migration.replaces:
  203. for app_label, name in migration.replaces:
  204. self.recorder.record_unapplied(app_label, name)
  205. else:
  206. self.recorder.record_unapplied(migration.app_label, migration.name)
  207. # Report progress
  208. if self.progress_callback:
  209. self.progress_callback("unapply_success", migration, fake)
  210. return state
  211. def check_replacements(self):
  212. """
  213. Mark replacement migrations applied if their replaced set all are.
  214. We do this unconditionally on every migrate, rather than just when
  215. migrations are applied or unapplied, so as to correctly handle the case
  216. when a new squash migration is pushed to a deployment that already had
  217. all its replaced migrations applied. In this case no new migration will
  218. be applied, but we still want to correctly maintain the applied state
  219. of the squash migration.
  220. """
  221. applied = self.recorder.applied_migrations()
  222. for key, migration in self.loader.replacements.items():
  223. all_applied = all(m in applied for m in migration.replaces)
  224. if all_applied and key not in applied:
  225. self.recorder.record_applied(*key)
  226. def detect_soft_applied(self, project_state, migration):
  227. """
  228. Tests whether a migration has been implicitly applied - that the
  229. tables or columns it would create exist. This is intended only for use
  230. on initial migrations (as it only looks for CreateModel and AddField).
  231. """
  232. if migration.initial is None:
  233. # Bail if the migration isn't the first one in its app
  234. if any(app == migration.app_label for app, name in migration.dependencies):
  235. return False, project_state
  236. elif migration.initial is False:
  237. # Bail if it's NOT an initial migration
  238. return False, project_state
  239. if project_state is None:
  240. after_state = self.loader.project_state((migration.app_label, migration.name), at_end=True)
  241. else:
  242. after_state = migration.mutate_state(project_state)
  243. apps = after_state.apps
  244. found_create_model_migration = False
  245. found_add_field_migration = False
  246. existing_table_names = self.connection.introspection.table_names(self.connection.cursor())
  247. # Make sure all create model and add field operations are done
  248. for operation in migration.operations:
  249. if isinstance(operation, migrations.CreateModel):
  250. model = apps.get_model(migration.app_label, operation.name)
  251. if model._meta.swapped:
  252. # We have to fetch the model to test with from the
  253. # main app cache, as it's not a direct dependency.
  254. model = global_apps.get_model(model._meta.swapped)
  255. if model._meta.proxy or not model._meta.managed:
  256. continue
  257. if model._meta.db_table not in existing_table_names:
  258. return False, project_state
  259. found_create_model_migration = True
  260. elif isinstance(operation, migrations.AddField):
  261. model = apps.get_model(migration.app_label, operation.model_name)
  262. if model._meta.swapped:
  263. # We have to fetch the model to test with from the
  264. # main app cache, as it's not a direct dependency.
  265. model = global_apps.get_model(model._meta.swapped)
  266. if model._meta.proxy or not model._meta.managed:
  267. continue
  268. table = model._meta.db_table
  269. field = model._meta.get_field(operation.name)
  270. # Handle implicit many-to-many tables created by AddField.
  271. if field.many_to_many:
  272. if field.remote_field.through._meta.db_table not in existing_table_names:
  273. return False, project_state
  274. else:
  275. found_add_field_migration = True
  276. continue
  277. column_names = [
  278. column.name for column in
  279. self.connection.introspection.get_table_description(self.connection.cursor(), table)
  280. ]
  281. if field.column not in column_names:
  282. return False, project_state
  283. found_add_field_migration = True
  284. # If we get this far and we found at least one CreateModel or AddField migration,
  285. # the migration is considered implicitly applied.
  286. return (found_create_model_migration or found_add_field_migration), after_state