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.
 
 
 
 

366 lines
15 KiB

  1. from __future__ import unicode_literals
  2. from django.db.migrations import (
  3. AddField, AlterField, AlterIndexTogether, AlterModelTable,
  4. AlterOrderWithRespectTo, AlterUniqueTogether, CreateModel, DeleteModel,
  5. RemoveField, RenameField, RenameModel,
  6. )
  7. from django.utils import six
  8. class MigrationOptimizer(object):
  9. """
  10. Powers the optimization process, where you provide a list of Operations
  11. and you are returned a list of equal or shorter length - operations
  12. are merged into one if possible.
  13. For example, a CreateModel and an AddField can be optimized into a
  14. new CreateModel, and CreateModel and DeleteModel can be optimized into
  15. nothing.
  16. """
  17. def __init__(self):
  18. self.model_level_operations = (
  19. CreateModel,
  20. AlterModelTable,
  21. AlterUniqueTogether,
  22. AlterIndexTogether,
  23. AlterOrderWithRespectTo,
  24. )
  25. self.field_level_operations = (
  26. AddField,
  27. AlterField,
  28. )
  29. self.reduce_methods = {
  30. # (model operation, model operation)
  31. (CreateModel, DeleteModel): self.reduce_create_model_delete_model,
  32. (CreateModel, RenameModel): self.reduce_create_model_rename_model,
  33. (RenameModel, RenameModel): self.reduce_rename_model_rename_model,
  34. (AlterIndexTogether, AlterIndexTogether): self.reduce_alter_model_alter_model,
  35. (AlterModelTable, AlterModelTable): self.reduce_alter_model_alter_model,
  36. (AlterOrderWithRespectTo, AlterOrderWithRespectTo): self.reduce_alter_model_alter_model,
  37. (AlterUniqueTogether, AlterUniqueTogether): self.reduce_alter_model_alter_model,
  38. (AlterIndexTogether, DeleteModel): self.reduce_alter_model_delete_model,
  39. (AlterModelTable, DeleteModel): self.reduce_alter_model_delete_model,
  40. (AlterOrderWithRespectTo, DeleteModel): self.reduce_alter_model_delete_model,
  41. (AlterUniqueTogether, DeleteModel): self.reduce_alter_model_delete_model,
  42. # (model operation, field operation)
  43. (CreateModel, AddField): self.reduce_create_model_add_field,
  44. (CreateModel, AlterField): self.reduce_create_model_alter_field,
  45. (CreateModel, RemoveField): self.reduce_create_model_remove_field,
  46. (CreateModel, RenameField): self.reduce_create_model_rename_field,
  47. (AlterIndexTogether, AddField): self.reduce_alter_model_addalterremove_field,
  48. (AlterIndexTogether, AlterField): self.reduce_alter_model_addalterremove_field,
  49. (AlterIndexTogether, RemoveField): self.reduce_alter_model_addalterremove_field,
  50. (AlterOrderWithRespectTo, AddField): self.reduce_alter_model_addalterremove_field,
  51. (AlterOrderWithRespectTo, AlterField): self.reduce_alter_model_addalterremove_field,
  52. (AlterOrderWithRespectTo, RemoveField): self.reduce_alter_model_addalterremove_field,
  53. (AlterUniqueTogether, AddField): self.reduce_alter_model_addalterremove_field,
  54. (AlterUniqueTogether, AlterField): self.reduce_alter_model_addalterremove_field,
  55. (AlterUniqueTogether, RemoveField): self.reduce_alter_model_addalterremove_field,
  56. (AlterIndexTogether, RenameField): self.reduce_alter_model_rename_field,
  57. (AlterOrderWithRespectTo, RenameField): self.reduce_alter_model_rename_field,
  58. (AlterUniqueTogether, RenameField): self.reduce_alter_model_rename_field,
  59. # (field operation, field operation)
  60. (AddField, AlterField): self.reduce_add_field_alter_field,
  61. (AddField, RemoveField): self.reduce_add_field_remove_field,
  62. (AddField, RenameField): self.reduce_add_field_rename_field,
  63. (AlterField, RemoveField): self.reduce_alter_field_remove_field,
  64. (AlterField, RenameField): self.reduce_alter_field_rename_field,
  65. (RenameField, RenameField): self.reduce_rename_field_rename_field,
  66. }
  67. def optimize(self, operations, app_label=None):
  68. """
  69. Main optimization entry point. Pass in a list of Operation instances,
  70. get out a new list of Operation instances.
  71. Unfortunately, due to the scope of the optimization (two combinable
  72. operations might be separated by several hundred others), this can't be
  73. done as a peephole optimization with checks/output implemented on
  74. the Operations themselves; instead, the optimizer looks at each
  75. individual operation and scans forwards in the list to see if there
  76. are any matches, stopping at boundaries - operations which can't
  77. be optimized over (RunSQL, operations on the same field/model, etc.)
  78. The inner loop is run until the starting list is the same as the result
  79. list, and then the result is returned. This means that operation
  80. optimization must be stable and always return an equal or shorter list.
  81. The app_label argument is optional, but if you pass it you'll get more
  82. efficient optimization.
  83. """
  84. # Internal tracking variable for test assertions about # of loops
  85. self._iterations = 0
  86. while True:
  87. result = self.optimize_inner(operations, app_label)
  88. self._iterations += 1
  89. if result == operations:
  90. return result
  91. operations = result
  92. def optimize_inner(self, operations, app_label=None):
  93. """
  94. Inner optimization loop.
  95. """
  96. new_operations = []
  97. for i, operation in enumerate(operations):
  98. # Compare it to each operation after it
  99. for j, other in enumerate(operations[i + 1:]):
  100. result = self.reduce(operation, other, operations[i + 1:i + j + 1])
  101. if result is not None:
  102. # Optimize! Add result, then remaining others, then return
  103. new_operations.extend(result)
  104. new_operations.extend(operations[i + 1:i + 1 + j])
  105. new_operations.extend(operations[i + j + 2:])
  106. return new_operations
  107. if not self.can_optimize_through(operation, other, app_label):
  108. new_operations.append(operation)
  109. break
  110. else:
  111. new_operations.append(operation)
  112. return new_operations
  113. # REDUCTION
  114. def reduce(self, operation, other, in_between=None):
  115. """
  116. Either returns a list of zero, one or two operations,
  117. or None, meaning this pair cannot be optimized.
  118. """
  119. method = self.reduce_methods.get((type(operation), type(other)))
  120. if method:
  121. return method(operation, other, in_between or [])
  122. return None
  123. def model_to_key(self, model):
  124. """
  125. Takes either a model class or a "appname.ModelName" string
  126. and returns (appname, modelname)
  127. """
  128. if isinstance(model, six.string_types):
  129. return model.split(".", 1)
  130. else:
  131. return (
  132. model._meta.app_label,
  133. model._meta.object_name,
  134. )
  135. # REDUCE METHODS: (MODEL OPERATION, MODEL OPERATION)
  136. def reduce_create_model_delete_model(self, operation, other, in_between):
  137. """
  138. Folds a CreateModel and a DeleteModel into nothing.
  139. """
  140. if (operation.name_lower == other.name_lower and
  141. not operation.options.get("proxy", False)):
  142. return []
  143. def reduce_create_model_rename_model(self, operation, other, in_between):
  144. """
  145. Folds a model rename into its create
  146. """
  147. if operation.name_lower == other.old_name_lower:
  148. return [
  149. CreateModel(
  150. other.new_name,
  151. fields=operation.fields,
  152. options=operation.options,
  153. bases=operation.bases,
  154. managers=operation.managers,
  155. )
  156. ]
  157. def reduce_rename_model_rename_model(self, operation, other, in_between):
  158. """
  159. Folds a model rename into another one
  160. """
  161. if operation.new_name_lower == other.old_name_lower:
  162. return [
  163. RenameModel(
  164. operation.old_name,
  165. other.new_name,
  166. )
  167. ]
  168. def reduce_alter_model_alter_model(self, operation, other, in_between):
  169. """
  170. Folds two AlterModelTable, AlterFooTogether, or AlterOrderWithRespectTo
  171. operations into the latter.
  172. """
  173. if operation.name_lower == other.name_lower:
  174. return [other]
  175. def reduce_alter_model_delete_model(self, operation, other, in_between):
  176. """
  177. Folds an AlterModelSomething and a DeleteModel into just delete.
  178. """
  179. if operation.name_lower == other.name_lower:
  180. return [other]
  181. # REDUCE METHODS: (MODEL OPERATION, FIELD OPERATION)
  182. def reduce_create_model_add_field(self, operation, other, in_between):
  183. if operation.name_lower == other.model_name_lower:
  184. # Don't allow optimizations of FKs through models they reference
  185. if hasattr(other.field, "remote_field") and other.field.remote_field:
  186. for between in in_between:
  187. # Check that it doesn't point to the model
  188. app_label, object_name = self.model_to_key(other.field.remote_field.model)
  189. if between.references_model(object_name, app_label):
  190. return None
  191. # Check that it's not through the model
  192. if getattr(other.field.remote_field, "through", None):
  193. app_label, object_name = self.model_to_key(other.field.remote_field.through)
  194. if between.references_model(object_name, app_label):
  195. return None
  196. # OK, that's fine
  197. return [
  198. CreateModel(
  199. operation.name,
  200. fields=operation.fields + [(other.name, other.field)],
  201. options=operation.options,
  202. bases=operation.bases,
  203. managers=operation.managers,
  204. )
  205. ]
  206. def reduce_create_model_alter_field(self, operation, other, in_between):
  207. if operation.name_lower == other.model_name_lower:
  208. return [
  209. CreateModel(
  210. operation.name,
  211. fields=[
  212. (n, other.field if n == other.name else v)
  213. for n, v in operation.fields
  214. ],
  215. options=operation.options,
  216. bases=operation.bases,
  217. managers=operation.managers,
  218. )
  219. ]
  220. def reduce_create_model_remove_field(self, operation, other, in_between):
  221. if operation.name_lower == other.model_name_lower:
  222. return [
  223. CreateModel(
  224. operation.name,
  225. fields=[
  226. (n, v)
  227. for n, v in operation.fields
  228. if n.lower() != other.name_lower
  229. ],
  230. options=operation.options,
  231. bases=operation.bases,
  232. managers=operation.managers,
  233. )
  234. ]
  235. def reduce_create_model_rename_field(self, operation, other, in_between):
  236. if operation.name_lower == other.model_name_lower:
  237. return [
  238. CreateModel(
  239. operation.name,
  240. fields=[
  241. (other.new_name if n == other.old_name else n, v)
  242. for n, v in operation.fields
  243. ],
  244. options=operation.options,
  245. bases=operation.bases,
  246. managers=operation.managers,
  247. )
  248. ]
  249. def reduce_alter_model_addalterremove_field(self, operation, other, in_between):
  250. if (operation.name_lower == other.model_name_lower and
  251. not operation.references_field(other.model_name, other.name)):
  252. return [other, operation]
  253. def reduce_alter_model_rename_field(self, operation, other, in_between):
  254. if (operation.name_lower == other.model_name_lower and
  255. not operation.references_field(other.model_name, other.old_name)):
  256. return [other, operation]
  257. # REDUCE METHODS: (FIELD OPERATION, FIELD OPERATION)
  258. def reduce_add_field_alter_field(self, operation, other, in_between):
  259. if (operation.model_name_lower == other.model_name_lower and
  260. operation.name_lower == other.name_lower):
  261. return [
  262. AddField(
  263. model_name=operation.model_name,
  264. name=operation.name,
  265. field=other.field,
  266. )
  267. ]
  268. def reduce_add_field_remove_field(self, operation, other, in_between):
  269. if (operation.model_name_lower == other.model_name_lower and
  270. operation.name_lower == other.name_lower):
  271. return []
  272. def reduce_add_field_rename_field(self, operation, other, in_between):
  273. if (operation.model_name_lower == other.model_name_lower and
  274. operation.name_lower == other.old_name_lower):
  275. return [
  276. AddField(
  277. model_name=operation.model_name,
  278. name=other.new_name,
  279. field=operation.field,
  280. )
  281. ]
  282. def reduce_alter_field_remove_field(self, operation, other, in_between):
  283. if (operation.model_name_lower == other.model_name_lower and
  284. operation.name_lower == other.name_lower):
  285. return [other]
  286. def reduce_alter_field_rename_field(self, operation, other, in_between):
  287. if (operation.model_name_lower == other.model_name_lower and
  288. operation.name_lower == other.old_name_lower):
  289. return [
  290. other,
  291. AlterField(
  292. model_name=operation.model_name,
  293. name=other.new_name,
  294. field=operation.field,
  295. ),
  296. ]
  297. def reduce_rename_field_rename_field(self, operation, other, in_between):
  298. if (operation.model_name_lower == other.model_name_lower and
  299. operation.new_name_lower == other.old_name_lower):
  300. return [
  301. RenameField(
  302. operation.model_name,
  303. operation.old_name,
  304. other.new_name,
  305. ),
  306. ]
  307. # THROUGH CHECKS
  308. def can_optimize_through(self, operation, other, app_label=None):
  309. """
  310. Returns True if it's possible to optimize 'operation' with something
  311. the other side of 'other'. This is possible if, for example, they
  312. affect different models.
  313. """
  314. # If it's a model level operation, let it through if there's
  315. # nothing that looks like a reference to us in 'other'.
  316. if isinstance(operation, self.model_level_operations):
  317. if not other.references_model(operation.name, app_label):
  318. return True
  319. # If it's field level, only let it through things that don't reference
  320. # the field (which includes not referencing the model)
  321. if isinstance(operation, self.field_level_operations):
  322. if not other.references_field(operation.model_name, operation.name, app_label):
  323. return True
  324. return False