test_optimizer.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. # -*- coding: utf-8 -*-
  2. from django.db import migrations, models
  3. from django.db.migrations import operations
  4. from django.db.migrations.optimizer import MigrationOptimizer
  5. from django.test import SimpleTestCase
  6. from .models import CustomModelBase, EmptyManager
  7. class OptimizerTests(SimpleTestCase):
  8. """
  9. Tests the migration autodetector.
  10. """
  11. def optimize(self, operations):
  12. """
  13. Handy shortcut for getting results + number of loops
  14. """
  15. optimizer = MigrationOptimizer()
  16. return optimizer.optimize(operations), optimizer._iterations
  17. def assertOptimizesTo(self, operations, expected, exact=None, less_than=None):
  18. result, iterations = self.optimize(operations)
  19. result = [repr(f.deconstruct()) for f in result]
  20. expected = [repr(f.deconstruct()) for f in expected]
  21. self.assertEqual(expected, result)
  22. if exact is not None and iterations != exact:
  23. raise self.failureException(
  24. "Optimization did not take exactly %s iterations (it took %s)" % (exact, iterations)
  25. )
  26. if less_than is not None and iterations >= less_than:
  27. raise self.failureException(
  28. "Optimization did not take less than %s iterations (it took %s)" % (less_than, iterations)
  29. )
  30. def assertDoesNotOptimize(self, operations):
  31. self.assertOptimizesTo(operations, operations)
  32. def test_single(self):
  33. """
  34. Tests that the optimizer does nothing on a single operation,
  35. and that it does it in just one pass.
  36. """
  37. self.assertOptimizesTo(
  38. [migrations.DeleteModel("Foo")],
  39. [migrations.DeleteModel("Foo")],
  40. exact=1,
  41. )
  42. def test_create_delete_model(self):
  43. """
  44. CreateModel and DeleteModel should collapse into nothing.
  45. """
  46. self.assertOptimizesTo(
  47. [
  48. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  49. migrations.DeleteModel("Foo"),
  50. ],
  51. [],
  52. )
  53. def test_create_rename_model(self):
  54. """
  55. CreateModel should absorb RenameModels.
  56. """
  57. managers = [('objects', EmptyManager())]
  58. self.assertOptimizesTo(
  59. [
  60. migrations.CreateModel(
  61. name="Foo",
  62. fields=[("name", models.CharField(max_length=255))],
  63. options={'verbose_name': 'Foo'},
  64. bases=(CustomModelBase),
  65. managers=managers,
  66. ),
  67. migrations.RenameModel("Foo", "Bar"),
  68. ],
  69. [
  70. migrations.CreateModel(
  71. "Bar",
  72. [("name", models.CharField(max_length=255))],
  73. options={'verbose_name': 'Foo'},
  74. bases=(CustomModelBase),
  75. managers=managers,
  76. )
  77. ],
  78. )
  79. def test_rename_model_self(self):
  80. """
  81. RenameModels should absorb themselves.
  82. """
  83. self.assertOptimizesTo(
  84. [
  85. migrations.RenameModel("Foo", "Baa"),
  86. migrations.RenameModel("Baa", "Bar"),
  87. ],
  88. [
  89. migrations.RenameModel("Foo", "Bar"),
  90. ],
  91. )
  92. def _test_create_alter_foo_delete_model(self, alter_foo):
  93. """
  94. CreateModel, AlterModelTable, AlterUniqueTogether/AlterIndexTogether/
  95. AlterOrderWithRespectTo, and DeleteModel should collapse into nothing.
  96. """
  97. self.assertOptimizesTo(
  98. [
  99. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  100. migrations.AlterModelTable("Foo", "woohoo"),
  101. alter_foo,
  102. migrations.DeleteModel("Foo"),
  103. ],
  104. [],
  105. )
  106. def test_create_alter_unique_delete_model(self):
  107. self._test_create_alter_foo_delete_model(migrations.AlterUniqueTogether("Foo", [["a", "b"]]))
  108. def test_create_alter_index_delete_model(self):
  109. self._test_create_alter_foo_delete_model(migrations.AlterIndexTogether("Foo", [["a", "b"]]))
  110. def test_create_alter_owrt_delete_model(self):
  111. self._test_create_alter_foo_delete_model(migrations.AlterOrderWithRespectTo("Foo", "a"))
  112. def _test_alter_alter_model(self, alter_foo, alter_bar):
  113. """
  114. Two AlterUniqueTogether/AlterIndexTogether/AlterOrderWithRespectTo
  115. should collapse into the second.
  116. """
  117. self.assertOptimizesTo(
  118. [
  119. alter_foo,
  120. alter_bar,
  121. ],
  122. [
  123. alter_bar,
  124. ],
  125. )
  126. def test_alter_alter_table_model(self):
  127. self._test_alter_alter_model(
  128. migrations.AlterModelTable("Foo", "a"),
  129. migrations.AlterModelTable("Foo", "b"),
  130. )
  131. def test_alter_alter_unique_model(self):
  132. self._test_alter_alter_model(
  133. migrations.AlterUniqueTogether("Foo", [["a", "b"]]),
  134. migrations.AlterUniqueTogether("Foo", [["a", "c"]]),
  135. )
  136. def test_alter_alter_index_model(self):
  137. self._test_alter_alter_model(
  138. migrations.AlterIndexTogether("Foo", [["a", "b"]]),
  139. migrations.AlterIndexTogether("Foo", [["a", "c"]]),
  140. )
  141. def test_alter_alter_owrt_model(self):
  142. self._test_alter_alter_model(
  143. migrations.AlterOrderWithRespectTo("Foo", "a"),
  144. migrations.AlterOrderWithRespectTo("Foo", "b"),
  145. )
  146. def test_optimize_through_create(self):
  147. """
  148. We should be able to optimize away create/delete through a create or delete
  149. of a different model, but only if the create operation does not mention the model
  150. at all.
  151. """
  152. # These should work
  153. self.assertOptimizesTo(
  154. [
  155. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  156. migrations.CreateModel("Bar", [("size", models.IntegerField())]),
  157. migrations.DeleteModel("Foo"),
  158. ],
  159. [
  160. migrations.CreateModel("Bar", [("size", models.IntegerField())]),
  161. ],
  162. )
  163. self.assertOptimizesTo(
  164. [
  165. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  166. migrations.CreateModel("Bar", [("size", models.IntegerField())]),
  167. migrations.DeleteModel("Bar"),
  168. migrations.DeleteModel("Foo"),
  169. ],
  170. [],
  171. )
  172. self.assertOptimizesTo(
  173. [
  174. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  175. migrations.CreateModel("Bar", [("size", models.IntegerField())]),
  176. migrations.DeleteModel("Foo"),
  177. migrations.DeleteModel("Bar"),
  178. ],
  179. [],
  180. )
  181. # This should not work - FK should block it
  182. self.assertOptimizesTo(
  183. [
  184. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  185. migrations.CreateModel("Bar", [("other", models.ForeignKey("testapp.Foo", models.CASCADE))]),
  186. migrations.DeleteModel("Foo"),
  187. ],
  188. [
  189. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  190. migrations.CreateModel("Bar", [("other", models.ForeignKey("testapp.Foo", models.CASCADE))]),
  191. migrations.DeleteModel("Foo"),
  192. ],
  193. )
  194. # This should not work - bases should block it
  195. self.assertOptimizesTo(
  196. [
  197. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  198. migrations.CreateModel("Bar", [("size", models.IntegerField())], bases=("testapp.Foo", )),
  199. migrations.DeleteModel("Foo"),
  200. ],
  201. [
  202. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  203. migrations.CreateModel("Bar", [("size", models.IntegerField())], bases=("testapp.Foo", )),
  204. migrations.DeleteModel("Foo"),
  205. ],
  206. )
  207. def test_create_model_add_field(self):
  208. """
  209. AddField should optimize into CreateModel.
  210. """
  211. managers = [('objects', EmptyManager())]
  212. self.assertOptimizesTo(
  213. [
  214. migrations.CreateModel(
  215. name="Foo",
  216. fields=[("name", models.CharField(max_length=255))],
  217. options={'verbose_name': 'Foo'},
  218. bases=(CustomModelBase),
  219. managers=managers,
  220. ),
  221. migrations.AddField("Foo", "age", models.IntegerField()),
  222. ],
  223. [
  224. migrations.CreateModel(
  225. name="Foo",
  226. fields=[
  227. ("name", models.CharField(max_length=255)),
  228. ("age", models.IntegerField()),
  229. ],
  230. options={'verbose_name': 'Foo'},
  231. bases=(CustomModelBase),
  232. managers=managers,
  233. ),
  234. ],
  235. )
  236. def test_create_model_add_field_not_through_fk(self):
  237. """
  238. AddField should NOT optimize into CreateModel if it's an FK to a model
  239. that's between them.
  240. """
  241. self.assertOptimizesTo(
  242. [
  243. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  244. migrations.CreateModel("Link", [("url", models.TextField())]),
  245. migrations.AddField("Foo", "link", models.ForeignKey("migrations.Link", models.CASCADE)),
  246. ],
  247. [
  248. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  249. migrations.CreateModel("Link", [("url", models.TextField())]),
  250. migrations.AddField("Foo", "link", models.ForeignKey("migrations.Link", models.CASCADE)),
  251. ],
  252. )
  253. def test_create_model_add_field_not_through_m2m_through(self):
  254. """
  255. AddField should NOT optimize into CreateModel if it's an M2M using a
  256. through that's created between them.
  257. """
  258. # Note: The middle model is not actually a valid through model,
  259. # but that doesn't matter, as we never render it.
  260. self.assertOptimizesTo(
  261. [
  262. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  263. migrations.CreateModel("LinkThrough", []),
  264. migrations.AddField(
  265. "Foo", "link", models.ManyToManyField("migrations.Link", through="migrations.LinkThrough")
  266. ),
  267. ],
  268. [
  269. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  270. migrations.CreateModel("LinkThrough", []),
  271. migrations.AddField(
  272. "Foo", "link", models.ManyToManyField("migrations.Link", through="migrations.LinkThrough")
  273. ),
  274. ],
  275. )
  276. def test_create_model_alter_field(self):
  277. """
  278. AlterField should optimize into CreateModel.
  279. """
  280. managers = [('objects', EmptyManager())]
  281. self.assertOptimizesTo(
  282. [
  283. migrations.CreateModel(
  284. name="Foo",
  285. fields=[("name", models.CharField(max_length=255))],
  286. options={'verbose_name': 'Foo'},
  287. bases=(CustomModelBase),
  288. managers=managers,
  289. ),
  290. migrations.AlterField("Foo", "name", models.IntegerField()),
  291. ],
  292. [
  293. migrations.CreateModel(
  294. name="Foo",
  295. fields=[
  296. ("name", models.IntegerField()),
  297. ],
  298. options={'verbose_name': 'Foo'},
  299. bases=(CustomModelBase),
  300. managers=managers,
  301. ),
  302. ],
  303. )
  304. def test_create_model_rename_field(self):
  305. """
  306. RenameField should optimize into CreateModel.
  307. """
  308. managers = [('objects', EmptyManager())]
  309. self.assertOptimizesTo(
  310. [
  311. migrations.CreateModel(
  312. name="Foo",
  313. fields=[("name", models.CharField(max_length=255))],
  314. options={'verbose_name': 'Foo'},
  315. bases=(CustomModelBase),
  316. managers=managers,
  317. ),
  318. migrations.RenameField("Foo", "name", "title"),
  319. ],
  320. [
  321. migrations.CreateModel(
  322. name="Foo",
  323. fields=[
  324. ("title", models.CharField(max_length=255)),
  325. ],
  326. options={'verbose_name': 'Foo'},
  327. bases=(CustomModelBase),
  328. managers=managers,
  329. ),
  330. ],
  331. )
  332. def test_add_field_rename_field(self):
  333. """
  334. RenameField should optimize into AddField
  335. """
  336. self.assertOptimizesTo(
  337. [
  338. migrations.AddField("Foo", "name", models.CharField(max_length=255)),
  339. migrations.RenameField("Foo", "name", "title"),
  340. ],
  341. [
  342. migrations.AddField("Foo", "title", models.CharField(max_length=255)),
  343. ],
  344. )
  345. def test_alter_field_rename_field(self):
  346. """
  347. RenameField should optimize to the other side of AlterField,
  348. and into itself.
  349. """
  350. self.assertOptimizesTo(
  351. [
  352. migrations.AlterField("Foo", "name", models.CharField(max_length=255)),
  353. migrations.RenameField("Foo", "name", "title"),
  354. migrations.RenameField("Foo", "title", "nom"),
  355. ],
  356. [
  357. migrations.RenameField("Foo", "name", "nom"),
  358. migrations.AlterField("Foo", "nom", models.CharField(max_length=255)),
  359. ],
  360. )
  361. def test_create_model_remove_field(self):
  362. """
  363. RemoveField should optimize into CreateModel.
  364. """
  365. managers = [('objects', EmptyManager())]
  366. self.assertOptimizesTo(
  367. [
  368. migrations.CreateModel(
  369. name="Foo",
  370. fields=[
  371. ("name", models.CharField(max_length=255)),
  372. ("age", models.IntegerField()),
  373. ],
  374. options={'verbose_name': 'Foo'},
  375. bases=(CustomModelBase),
  376. managers=managers,
  377. ),
  378. migrations.RemoveField("Foo", "age"),
  379. ],
  380. [
  381. migrations.CreateModel(
  382. name="Foo",
  383. fields=[
  384. ("name", models.CharField(max_length=255)),
  385. ],
  386. options={'verbose_name': 'Foo'},
  387. bases=(CustomModelBase),
  388. managers=managers,
  389. ),
  390. ],
  391. )
  392. def test_add_field_alter_field(self):
  393. """
  394. AlterField should optimize into AddField.
  395. """
  396. self.assertOptimizesTo(
  397. [
  398. migrations.AddField("Foo", "age", models.IntegerField()),
  399. migrations.AlterField("Foo", "age", models.FloatField(default=2.4)),
  400. ],
  401. [
  402. migrations.AddField("Foo", name="age", field=models.FloatField(default=2.4)),
  403. ],
  404. )
  405. def test_add_field_delete_field(self):
  406. """
  407. RemoveField should cancel AddField
  408. """
  409. self.assertOptimizesTo(
  410. [
  411. migrations.AddField("Foo", "age", models.IntegerField()),
  412. migrations.RemoveField("Foo", "age"),
  413. ],
  414. [],
  415. )
  416. def test_alter_field_delete_field(self):
  417. """
  418. RemoveField should absorb AlterField
  419. """
  420. self.assertOptimizesTo(
  421. [
  422. migrations.AlterField("Foo", "age", models.IntegerField()),
  423. migrations.RemoveField("Foo", "age"),
  424. ],
  425. [
  426. migrations.RemoveField("Foo", "age"),
  427. ],
  428. )
  429. def _test_create_alter_foo_field(self, alter):
  430. """
  431. CreateModel, AlterFooTogether/AlterOrderWithRespectTo followed by an
  432. add/alter/rename field should optimize to CreateModel and the Alter*
  433. """
  434. # AddField
  435. self.assertOptimizesTo(
  436. [
  437. migrations.CreateModel("Foo", [
  438. ("a", models.IntegerField()),
  439. ("b", models.IntegerField()),
  440. ]),
  441. alter,
  442. migrations.AddField("Foo", "c", models.IntegerField()),
  443. ],
  444. [
  445. migrations.CreateModel("Foo", [
  446. ("a", models.IntegerField()),
  447. ("b", models.IntegerField()),
  448. ("c", models.IntegerField()),
  449. ]),
  450. alter,
  451. ],
  452. )
  453. # AlterField
  454. self.assertDoesNotOptimize(
  455. [
  456. migrations.CreateModel("Foo", [
  457. ("a", models.IntegerField()),
  458. ("b", models.IntegerField()),
  459. ]),
  460. alter,
  461. migrations.AlterField("Foo", "b", models.CharField(max_length=255)),
  462. ],
  463. )
  464. self.assertOptimizesTo(
  465. [
  466. migrations.CreateModel("Foo", [
  467. ("a", models.IntegerField()),
  468. ("b", models.IntegerField()),
  469. ("c", models.IntegerField()),
  470. ]),
  471. alter,
  472. migrations.AlterField("Foo", "c", models.CharField(max_length=255)),
  473. ],
  474. [
  475. migrations.CreateModel("Foo", [
  476. ("a", models.IntegerField()),
  477. ("b", models.IntegerField()),
  478. ("c", models.CharField(max_length=255)),
  479. ]),
  480. alter,
  481. ],
  482. )
  483. # RenameField
  484. self.assertDoesNotOptimize(
  485. [
  486. migrations.CreateModel("Foo", [
  487. ("a", models.IntegerField()),
  488. ("b", models.IntegerField()),
  489. ]),
  490. alter,
  491. migrations.RenameField("Foo", "b", "c"),
  492. ],
  493. )
  494. self.assertOptimizesTo(
  495. [
  496. migrations.CreateModel("Foo", [
  497. ("a", models.IntegerField()),
  498. ("b", models.IntegerField()),
  499. ]),
  500. alter,
  501. migrations.RenameField("Foo", "b", "x"),
  502. migrations.RenameField("Foo", "x", "c"),
  503. ],
  504. [
  505. migrations.CreateModel("Foo", [
  506. ("a", models.IntegerField()),
  507. ("b", models.IntegerField()),
  508. ]),
  509. alter,
  510. migrations.RenameField("Foo", "b", "c"),
  511. ],
  512. )
  513. self.assertOptimizesTo(
  514. [
  515. migrations.CreateModel("Foo", [
  516. ("a", models.IntegerField()),
  517. ("b", models.IntegerField()),
  518. ("c", models.IntegerField()),
  519. ]),
  520. alter,
  521. migrations.RenameField("Foo", "c", "d"),
  522. ],
  523. [
  524. migrations.CreateModel("Foo", [
  525. ("a", models.IntegerField()),
  526. ("b", models.IntegerField()),
  527. ("d", models.IntegerField()),
  528. ]),
  529. alter,
  530. ],
  531. )
  532. # RemoveField
  533. self.assertDoesNotOptimize(
  534. [
  535. migrations.CreateModel("Foo", [
  536. ("a", models.IntegerField()),
  537. ("b", models.IntegerField()),
  538. ]),
  539. alter,
  540. migrations.RemoveField("Foo", "b"),
  541. ],
  542. )
  543. self.assertOptimizesTo(
  544. [
  545. migrations.CreateModel("Foo", [
  546. ("a", models.IntegerField()),
  547. ("b", models.IntegerField()),
  548. ("c", models.IntegerField()),
  549. ]),
  550. alter,
  551. migrations.RemoveField("Foo", "c"),
  552. ],
  553. [
  554. migrations.CreateModel("Foo", [
  555. ("a", models.IntegerField()),
  556. ("b", models.IntegerField()),
  557. ]),
  558. alter,
  559. ],
  560. )
  561. def test_create_alter_unique_field(self):
  562. self._test_create_alter_foo_field(migrations.AlterUniqueTogether("Foo", [["a", "b"]]))
  563. def test_create_alter_index_field(self):
  564. self._test_create_alter_foo_field(migrations.AlterIndexTogether("Foo", [["a", "b"]]))
  565. def test_create_alter_owrt_field(self):
  566. self._test_create_alter_foo_field(migrations.AlterOrderWithRespectTo("Foo", "b"))
  567. def test_optimize_through_fields(self):
  568. """
  569. Checks that field-level through checking is working.
  570. This should manage to collapse model Foo to nonexistence,
  571. and model Bar to a single IntegerField called "width".
  572. """
  573. self.assertOptimizesTo(
  574. [
  575. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  576. migrations.CreateModel("Bar", [("size", models.IntegerField())]),
  577. migrations.AddField("Foo", "age", models.IntegerField()),
  578. migrations.AddField("Bar", "width", models.IntegerField()),
  579. migrations.AlterField("Foo", "age", models.IntegerField()),
  580. migrations.RenameField("Bar", "size", "dimensions"),
  581. migrations.RemoveField("Foo", "age"),
  582. migrations.RenameModel("Foo", "Phou"),
  583. migrations.RemoveField("Bar", "dimensions"),
  584. migrations.RenameModel("Phou", "Fou"),
  585. migrations.DeleteModel("Fou"),
  586. ],
  587. [
  588. migrations.CreateModel("Bar", [("width", models.IntegerField())]),
  589. ],
  590. )
  591. def test_optimize_elidable_operation(self):
  592. elidable_operation = operations.base.Operation()
  593. elidable_operation.elidable = True
  594. self.assertOptimizesTo(
  595. [
  596. elidable_operation,
  597. migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
  598. elidable_operation,
  599. migrations.CreateModel("Bar", [("size", models.IntegerField())]),
  600. elidable_operation,
  601. migrations.RenameModel("Foo", "Phou"),
  602. migrations.DeleteModel("Bar"),
  603. elidable_operation,
  604. ],
  605. [
  606. migrations.CreateModel("Phou", [("name", models.CharField(max_length=255))]),
  607. ],
  608. )