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.

squashmigrations.py 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. from django.apps import apps
  2. from django.conf import settings
  3. from django.core.management.base import BaseCommand, CommandError
  4. from django.db import DEFAULT_DB_ALIAS, connections, migrations
  5. from django.db.migrations.loader import AmbiguityError, MigrationLoader
  6. from django.db.migrations.migration import SwappableTuple
  7. from django.db.migrations.optimizer import MigrationOptimizer
  8. from django.db.migrations.writer import MigrationWriter
  9. from django.utils.version import get_docs_version
  10. class Command(BaseCommand):
  11. help = "Squashes an existing set of migrations (from first until specified) into a single new one."
  12. def add_arguments(self, parser):
  13. parser.add_argument(
  14. 'app_label',
  15. help='App label of the application to squash migrations for.',
  16. )
  17. parser.add_argument(
  18. 'start_migration_name', nargs='?',
  19. help='Migrations will be squashed starting from and including this migration.',
  20. )
  21. parser.add_argument(
  22. 'migration_name',
  23. help='Migrations will be squashed until and including this migration.',
  24. )
  25. parser.add_argument(
  26. '--no-optimize', action='store_true',
  27. help='Do not try to optimize the squashed operations.',
  28. )
  29. parser.add_argument(
  30. '--noinput', '--no-input', action='store_false', dest='interactive',
  31. help='Tells Django to NOT prompt the user for input of any kind.',
  32. )
  33. parser.add_argument(
  34. '--squashed-name',
  35. help='Sets the name of the new squashed migration.',
  36. )
  37. parser.add_argument(
  38. '--no-header', action='store_false', dest='include_header',
  39. help='Do not add a header comment to the new squashed migration.',
  40. )
  41. def handle(self, **options):
  42. self.verbosity = options['verbosity']
  43. self.interactive = options['interactive']
  44. app_label = options['app_label']
  45. start_migration_name = options['start_migration_name']
  46. migration_name = options['migration_name']
  47. no_optimize = options['no_optimize']
  48. squashed_name = options['squashed_name']
  49. include_header = options['include_header']
  50. # Validate app_label.
  51. try:
  52. apps.get_app_config(app_label)
  53. except LookupError as err:
  54. raise CommandError(str(err))
  55. # Load the current graph state, check the app and migration they asked for exists
  56. loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])
  57. if app_label not in loader.migrated_apps:
  58. raise CommandError(
  59. "App '%s' does not have migrations (so squashmigrations on "
  60. "it makes no sense)" % app_label
  61. )
  62. migration = self.find_migration(loader, app_label, migration_name)
  63. # Work out the list of predecessor migrations
  64. migrations_to_squash = [
  65. loader.get_migration(al, mn)
  66. for al, mn in loader.graph.forwards_plan((migration.app_label, migration.name))
  67. if al == migration.app_label
  68. ]
  69. if start_migration_name:
  70. start_migration = self.find_migration(loader, app_label, start_migration_name)
  71. start = loader.get_migration(start_migration.app_label, start_migration.name)
  72. try:
  73. start_index = migrations_to_squash.index(start)
  74. migrations_to_squash = migrations_to_squash[start_index:]
  75. except ValueError:
  76. raise CommandError(
  77. "The migration '%s' cannot be found. Maybe it comes after "
  78. "the migration '%s'?\n"
  79. "Have a look at:\n"
  80. " python manage.py showmigrations %s\n"
  81. "to debug this issue." % (start_migration, migration, app_label)
  82. )
  83. # Tell them what we're doing and optionally ask if we should proceed
  84. if self.verbosity > 0 or self.interactive:
  85. self.stdout.write(self.style.MIGRATE_HEADING("Will squash the following migrations:"))
  86. for migration in migrations_to_squash:
  87. self.stdout.write(" - %s" % migration.name)
  88. if self.interactive:
  89. answer = None
  90. while not answer or answer not in "yn":
  91. answer = input("Do you wish to proceed? [yN] ")
  92. if not answer:
  93. answer = "n"
  94. break
  95. else:
  96. answer = answer[0].lower()
  97. if answer != "y":
  98. return
  99. # Load the operations from all those migrations and concat together,
  100. # along with collecting external dependencies and detecting
  101. # double-squashing
  102. operations = []
  103. dependencies = set()
  104. # We need to take all dependencies from the first migration in the list
  105. # as it may be 0002 depending on 0001
  106. first_migration = True
  107. for smigration in migrations_to_squash:
  108. if smigration.replaces:
  109. raise CommandError(
  110. "You cannot squash squashed migrations! Please transition "
  111. "it to a normal migration first: "
  112. "https://docs.djangoproject.com/en/%s/topics/migrations/#squashing-migrations" % get_docs_version()
  113. )
  114. operations.extend(smigration.operations)
  115. for dependency in smigration.dependencies:
  116. if isinstance(dependency, SwappableTuple):
  117. if settings.AUTH_USER_MODEL == dependency.setting:
  118. dependencies.add(("__setting__", "AUTH_USER_MODEL"))
  119. else:
  120. dependencies.add(dependency)
  121. elif dependency[0] != smigration.app_label or first_migration:
  122. dependencies.add(dependency)
  123. first_migration = False
  124. if no_optimize:
  125. if self.verbosity > 0:
  126. self.stdout.write(self.style.MIGRATE_HEADING("(Skipping optimization.)"))
  127. new_operations = operations
  128. else:
  129. if self.verbosity > 0:
  130. self.stdout.write(self.style.MIGRATE_HEADING("Optimizing..."))
  131. optimizer = MigrationOptimizer()
  132. new_operations = optimizer.optimize(operations, migration.app_label)
  133. if self.verbosity > 0:
  134. if len(new_operations) == len(operations):
  135. self.stdout.write(" No optimizations possible.")
  136. else:
  137. self.stdout.write(
  138. " Optimized from %s operations to %s operations." %
  139. (len(operations), len(new_operations))
  140. )
  141. # Work out the value of replaces (any squashed ones we're re-squashing)
  142. # need to feed their replaces into ours
  143. replaces = []
  144. for migration in migrations_to_squash:
  145. if migration.replaces:
  146. replaces.extend(migration.replaces)
  147. else:
  148. replaces.append((migration.app_label, migration.name))
  149. # Make a new migration with those operations
  150. subclass = type("Migration", (migrations.Migration,), {
  151. "dependencies": dependencies,
  152. "operations": new_operations,
  153. "replaces": replaces,
  154. })
  155. if start_migration_name:
  156. if squashed_name:
  157. # Use the name from --squashed-name.
  158. prefix, _ = start_migration.name.split('_', 1)
  159. name = '%s_%s' % (prefix, squashed_name)
  160. else:
  161. # Generate a name.
  162. name = '%s_squashed_%s' % (start_migration.name, migration.name)
  163. new_migration = subclass(name, app_label)
  164. else:
  165. name = '0001_%s' % (squashed_name or 'squashed_%s' % migration.name)
  166. new_migration = subclass(name, app_label)
  167. new_migration.initial = True
  168. # Write out the new migration file
  169. writer = MigrationWriter(new_migration, include_header)
  170. with open(writer.path, "w", encoding='utf-8') as fh:
  171. fh.write(writer.as_string())
  172. if self.verbosity > 0:
  173. self.stdout.write(self.style.MIGRATE_HEADING("Created new squashed migration %s" % writer.path))
  174. self.stdout.write(" You should commit this migration but leave the old ones in place;")
  175. self.stdout.write(" the new migration will be used for new installs. Once you are sure")
  176. self.stdout.write(" all instances of the codebase have applied the migrations you squashed,")
  177. self.stdout.write(" you can delete them.")
  178. if writer.needs_manual_porting:
  179. self.stdout.write(self.style.MIGRATE_HEADING("Manual porting required"))
  180. self.stdout.write(" Your migrations contained functions that must be manually copied over,")
  181. self.stdout.write(" as we could not safely copy their implementation.")
  182. self.stdout.write(" See the comment at the top of the squashed migration for details.")
  183. def find_migration(self, loader, app_label, name):
  184. try:
  185. return loader.get_migration_by_prefix(app_label, name)
  186. except AmbiguityError:
  187. raise CommandError(
  188. "More than one migration matches '%s' in app '%s'. Please be "
  189. "more specific." % (name, app_label)
  190. )
  191. except KeyError:
  192. raise CommandError(
  193. "Cannot find a migration matching '%s' from app '%s'." %
  194. (name, app_label)
  195. )