tests.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. from __future__ import absolute_import
  2. import datetime
  3. from django.test import TransactionTestCase
  4. from django.utils.unittest import skipUnless
  5. from django.db import connection, DatabaseError, IntegrityError
  6. from django.db.models.fields import IntegerField, TextField, CharField, SlugField
  7. from django.db.models.fields.related import ManyToManyField, ForeignKey
  8. from django.db.transaction import atomic
  9. from .models import Author, AuthorWithM2M, Book, BookWithSlug, BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename, UniqueTest
  10. class SchemaTests(TransactionTestCase):
  11. """
  12. Tests that the schema-alteration code works correctly.
  13. Be aware that these tests are more liable than most to false results,
  14. as sometimes the code to check if a test has worked is almost as complex
  15. as the code it is testing.
  16. """
  17. available_apps = []
  18. models = [Author, AuthorWithM2M, Book, BookWithSlug, BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename, UniqueTest]
  19. no_table_strings = ["no such table", "unknown table", "does not exist"]
  20. # Utility functions
  21. def tearDown(self):
  22. # Delete any tables made for our models
  23. self.delete_tables()
  24. def delete_tables(self):
  25. "Deletes all model tables for our models for a clean test environment"
  26. cursor = connection.cursor()
  27. connection.disable_constraint_checking()
  28. for model in self.models:
  29. # Remove any M2M tables first
  30. for field in model._meta.local_many_to_many:
  31. with atomic():
  32. try:
  33. cursor.execute(connection.schema_editor().sql_delete_table % {
  34. "table": connection.ops.quote_name(field.rel.through._meta.db_table),
  35. })
  36. except DatabaseError as e:
  37. if any([s in str(e).lower() for s in self.no_table_strings]):
  38. pass
  39. else:
  40. raise
  41. # Then remove the main tables
  42. with atomic():
  43. try:
  44. cursor.execute(connection.schema_editor().sql_delete_table % {
  45. "table": connection.ops.quote_name(model._meta.db_table),
  46. })
  47. except DatabaseError as e:
  48. if any([s in str(e).lower() for s in self.no_table_strings]):
  49. pass
  50. else:
  51. raise
  52. connection.enable_constraint_checking()
  53. def column_classes(self, model):
  54. cursor = connection.cursor()
  55. columns = dict(
  56. (d[0], (connection.introspection.get_field_type(d[1], d), d))
  57. for d in connection.introspection.get_table_description(
  58. cursor,
  59. model._meta.db_table,
  60. )
  61. )
  62. # SQLite has a different format for field_type
  63. for name, (type, desc) in columns.items():
  64. if isinstance(type, tuple):
  65. columns[name] = (type[0], desc)
  66. # SQLite also doesn't error properly
  67. if not columns:
  68. raise DatabaseError("Table does not exist (empty pragma)")
  69. return columns
  70. # Tests
  71. def test_creation_deletion(self):
  72. """
  73. Tries creating a model's table, and then deleting it.
  74. """
  75. # Create the table
  76. with connection.schema_editor() as editor:
  77. editor.create_model(Author)
  78. # Check that it's there
  79. list(Author.objects.all())
  80. # Clean up that table
  81. with connection.schema_editor() as editor:
  82. editor.delete_model(Author)
  83. # Check that it's gone
  84. self.assertRaises(
  85. DatabaseError,
  86. lambda: list(Author.objects.all()),
  87. )
  88. @skipUnless(connection.features.supports_foreign_keys, "No FK support")
  89. def test_fk(self):
  90. "Tests that creating tables out of FK order, then repointing, works"
  91. # Create the table
  92. with connection.schema_editor() as editor:
  93. editor.create_model(Book)
  94. editor.create_model(Author)
  95. editor.create_model(Tag)
  96. # Check that initial tables are there
  97. list(Author.objects.all())
  98. list(Book.objects.all())
  99. # Make sure the FK constraint is present
  100. with self.assertRaises(IntegrityError):
  101. Book.objects.create(
  102. author_id = 1,
  103. title = "Much Ado About Foreign Keys",
  104. pub_date = datetime.datetime.now(),
  105. )
  106. # Repoint the FK constraint
  107. new_field = ForeignKey(Tag)
  108. new_field.set_attributes_from_name("author")
  109. with connection.schema_editor() as editor:
  110. editor.alter_field(
  111. Book,
  112. Book._meta.get_field_by_name("author")[0],
  113. new_field,
  114. strict=True,
  115. )
  116. # Make sure the new FK constraint is present
  117. constraints = connection.introspection.get_constraints(connection.cursor(), Book._meta.db_table)
  118. for name, details in constraints.items():
  119. if details['columns'] == ["author_id"] and details['foreign_key']:
  120. self.assertEqual(details['foreign_key'], ('schema_tag', 'id'))
  121. break
  122. else:
  123. self.fail("No FK constraint for author_id found")
  124. def test_add_field(self):
  125. """
  126. Tests adding fields to models
  127. """
  128. # Create the table
  129. with connection.schema_editor() as editor:
  130. editor.create_model(Author)
  131. # Ensure there's no age field
  132. columns = self.column_classes(Author)
  133. self.assertNotIn("age", columns)
  134. # Alter the name field to a TextField
  135. new_field = IntegerField(null=True)
  136. new_field.set_attributes_from_name("age")
  137. with connection.schema_editor() as editor:
  138. editor.add_field(
  139. Author,
  140. new_field,
  141. )
  142. # Ensure the field is right afterwards
  143. columns = self.column_classes(Author)
  144. self.assertEqual(columns['age'][0], "IntegerField")
  145. self.assertEqual(columns['age'][1][6], True)
  146. def test_alter(self):
  147. """
  148. Tests simple altering of fields
  149. """
  150. # Create the table
  151. with connection.schema_editor() as editor:
  152. editor.create_model(Author)
  153. # Ensure the field is right to begin with
  154. columns = self.column_classes(Author)
  155. self.assertEqual(columns['name'][0], "CharField")
  156. self.assertEqual(bool(columns['name'][1][6]), bool(connection.features.interprets_empty_strings_as_nulls))
  157. # Alter the name field to a TextField
  158. new_field = TextField(null=True)
  159. new_field.set_attributes_from_name("name")
  160. with connection.schema_editor() as editor:
  161. editor.alter_field(
  162. Author,
  163. Author._meta.get_field_by_name("name")[0],
  164. new_field,
  165. strict=True,
  166. )
  167. # Ensure the field is right afterwards
  168. columns = self.column_classes(Author)
  169. self.assertEqual(columns['name'][0], "TextField")
  170. self.assertEqual(columns['name'][1][6], True)
  171. # Change nullability again
  172. new_field2 = TextField(null=False)
  173. new_field2.set_attributes_from_name("name")
  174. with connection.schema_editor() as editor:
  175. editor.alter_field(
  176. Author,
  177. new_field,
  178. new_field2,
  179. strict=True,
  180. )
  181. # Ensure the field is right afterwards
  182. columns = self.column_classes(Author)
  183. self.assertEqual(columns['name'][0], "TextField")
  184. self.assertEqual(bool(columns['name'][1][6]), False)
  185. def test_rename(self):
  186. """
  187. Tests simple altering of fields
  188. """
  189. # Create the table
  190. with connection.schema_editor() as editor:
  191. editor.create_model(Author)
  192. # Ensure the field is right to begin with
  193. columns = self.column_classes(Author)
  194. self.assertEqual(columns['name'][0], "CharField")
  195. self.assertNotIn("display_name", columns)
  196. # Alter the name field's name
  197. new_field = CharField(max_length=254)
  198. new_field.set_attributes_from_name("display_name")
  199. with connection.schema_editor() as editor:
  200. editor.alter_field(
  201. Author,
  202. Author._meta.get_field_by_name("name")[0],
  203. new_field,
  204. strict = True,
  205. )
  206. # Ensure the field is right afterwards
  207. columns = self.column_classes(Author)
  208. self.assertEqual(columns['display_name'][0], "CharField")
  209. self.assertNotIn("name", columns)
  210. def test_m2m_create(self):
  211. """
  212. Tests M2M fields on models during creation
  213. """
  214. # Create the tables
  215. with connection.schema_editor() as editor:
  216. editor.create_model(Author)
  217. editor.create_model(TagM2MTest)
  218. editor.create_model(BookWithM2M)
  219. # Ensure there is now an m2m table there
  220. columns = self.column_classes(BookWithM2M._meta.get_field_by_name("tags")[0].rel.through)
  221. self.assertEqual(columns['tagm2mtest_id'][0], "IntegerField")
  222. def test_m2m(self):
  223. """
  224. Tests adding/removing M2M fields on models
  225. """
  226. # Create the tables
  227. with connection.schema_editor() as editor:
  228. editor.create_model(AuthorWithM2M)
  229. editor.create_model(TagM2MTest)
  230. # Create an M2M field
  231. new_field = ManyToManyField("schema.TagM2MTest", related_name="authors")
  232. new_field.contribute_to_class(AuthorWithM2M, "tags")
  233. try:
  234. # Ensure there's no m2m table there
  235. self.assertRaises(DatabaseError, self.column_classes, new_field.rel.through)
  236. # Add the field
  237. with connection.schema_editor() as editor:
  238. editor.add_field(
  239. Author,
  240. new_field,
  241. )
  242. # Ensure there is now an m2m table there
  243. columns = self.column_classes(new_field.rel.through)
  244. self.assertEqual(columns['tagm2mtest_id'][0], "IntegerField")
  245. # Remove the M2M table again
  246. with connection.schema_editor() as editor:
  247. editor.remove_field(
  248. Author,
  249. new_field,
  250. )
  251. # Ensure there's no m2m table there
  252. self.assertRaises(DatabaseError, self.column_classes, new_field.rel.through)
  253. finally:
  254. # Cleanup model states
  255. AuthorWithM2M._meta.local_many_to_many.remove(new_field)
  256. def test_m2m_repoint(self):
  257. """
  258. Tests repointing M2M fields
  259. """
  260. # Create the tables
  261. with connection.schema_editor() as editor:
  262. editor.create_model(Author)
  263. editor.create_model(BookWithM2M)
  264. editor.create_model(TagM2MTest)
  265. editor.create_model(UniqueTest)
  266. # Ensure the M2M exists and points to TagM2MTest
  267. constraints = connection.introspection.get_constraints(connection.cursor(), BookWithM2M._meta.get_field_by_name("tags")[0].rel.through._meta.db_table)
  268. if connection.features.supports_foreign_keys:
  269. for name, details in constraints.items():
  270. if details['columns'] == ["tagm2mtest_id"] and details['foreign_key']:
  271. self.assertEqual(details['foreign_key'], ('schema_tagm2mtest', 'id'))
  272. break
  273. else:
  274. self.fail("No FK constraint for tagm2mtest_id found")
  275. # Repoint the M2M
  276. new_field = ManyToManyField(UniqueTest)
  277. new_field.contribute_to_class(BookWithM2M, "uniques")
  278. try:
  279. with connection.schema_editor() as editor:
  280. editor.alter_field(
  281. Author,
  282. BookWithM2M._meta.get_field_by_name("tags")[0],
  283. new_field,
  284. )
  285. # Ensure old M2M is gone
  286. self.assertRaises(DatabaseError, self.column_classes, BookWithM2M._meta.get_field_by_name("tags")[0].rel.through)
  287. # Ensure the new M2M exists and points to UniqueTest
  288. constraints = connection.introspection.get_constraints(connection.cursor(), new_field.rel.through._meta.db_table)
  289. if connection.features.supports_foreign_keys:
  290. for name, details in constraints.items():
  291. if details['columns'] == ["uniquetest_id"] and details['foreign_key']:
  292. self.assertEqual(details['foreign_key'], ('schema_uniquetest', 'id'))
  293. break
  294. else:
  295. self.fail("No FK constraint for uniquetest_id found")
  296. finally:
  297. # Cleanup model states
  298. BookWithM2M._meta.local_many_to_many.remove(new_field)
  299. del BookWithM2M._meta._m2m_cache
  300. @skipUnless(connection.features.supports_check_constraints, "No check constraints")
  301. def test_check_constraints(self):
  302. """
  303. Tests creating/deleting CHECK constraints
  304. """
  305. # Create the tables
  306. with connection.schema_editor() as editor:
  307. editor.create_model(Author)
  308. # Ensure the constraint exists
  309. constraints = connection.introspection.get_constraints(connection.cursor(), Author._meta.db_table)
  310. for name, details in constraints.items():
  311. if details['columns'] == ["height"] and details['check']:
  312. break
  313. else:
  314. self.fail("No check constraint for height found")
  315. # Alter the column to remove it
  316. new_field = IntegerField(null=True, blank=True)
  317. new_field.set_attributes_from_name("height")
  318. with connection.schema_editor() as editor:
  319. editor.alter_field(
  320. Author,
  321. Author._meta.get_field_by_name("height")[0],
  322. new_field,
  323. strict = True,
  324. )
  325. constraints = connection.introspection.get_constraints(connection.cursor(), Author._meta.db_table)
  326. for name, details in constraints.items():
  327. if details['columns'] == ["height"] and details['check']:
  328. self.fail("Check constraint for height found")
  329. # Alter the column to re-add it
  330. with connection.schema_editor() as editor:
  331. editor.alter_field(
  332. Author,
  333. new_field,
  334. Author._meta.get_field_by_name("height")[0],
  335. strict = True,
  336. )
  337. constraints = connection.introspection.get_constraints(connection.cursor(), Author._meta.db_table)
  338. for name, details in constraints.items():
  339. if details['columns'] == ["height"] and details['check']:
  340. break
  341. else:
  342. self.fail("No check constraint for height found")
  343. def test_unique(self):
  344. """
  345. Tests removing and adding unique constraints to a single column.
  346. """
  347. # Create the table
  348. with connection.schema_editor() as editor:
  349. editor.create_model(Tag)
  350. # Ensure the field is unique to begin with
  351. Tag.objects.create(title="foo", slug="foo")
  352. self.assertRaises(IntegrityError, Tag.objects.create, title="bar", slug="foo")
  353. Tag.objects.all().delete()
  354. # Alter the slug field to be non-unique
  355. new_field = SlugField(unique=False)
  356. new_field.set_attributes_from_name("slug")
  357. with connection.schema_editor() as editor:
  358. editor.alter_field(
  359. Tag,
  360. Tag._meta.get_field_by_name("slug")[0],
  361. new_field,
  362. strict = True,
  363. )
  364. # Ensure the field is no longer unique
  365. Tag.objects.create(title="foo", slug="foo")
  366. Tag.objects.create(title="bar", slug="foo")
  367. Tag.objects.all().delete()
  368. # Alter the slug field to be unique
  369. new_new_field = SlugField(unique=True)
  370. new_new_field.set_attributes_from_name("slug")
  371. with connection.schema_editor() as editor:
  372. editor.alter_field(
  373. Tag,
  374. new_field,
  375. new_new_field,
  376. strict = True,
  377. )
  378. # Ensure the field is unique again
  379. Tag.objects.create(title="foo", slug="foo")
  380. self.assertRaises(IntegrityError, Tag.objects.create, title="bar", slug="foo")
  381. Tag.objects.all().delete()
  382. # Rename the field
  383. new_field = SlugField(unique=False)
  384. new_field.set_attributes_from_name("slug2")
  385. with connection.schema_editor() as editor:
  386. editor.alter_field(
  387. Tag,
  388. Tag._meta.get_field_by_name("slug")[0],
  389. TagUniqueRename._meta.get_field_by_name("slug2")[0],
  390. strict = True,
  391. )
  392. # Ensure the field is still unique
  393. TagUniqueRename.objects.create(title="foo", slug2="foo")
  394. self.assertRaises(IntegrityError, TagUniqueRename.objects.create, title="bar", slug2="foo")
  395. Tag.objects.all().delete()
  396. def test_unique_together(self):
  397. """
  398. Tests removing and adding unique_together constraints on a model.
  399. """
  400. # Create the table
  401. with connection.schema_editor() as editor:
  402. editor.create_model(UniqueTest)
  403. # Ensure the fields are unique to begin with
  404. UniqueTest.objects.create(year=2012, slug="foo")
  405. UniqueTest.objects.create(year=2011, slug="foo")
  406. UniqueTest.objects.create(year=2011, slug="bar")
  407. self.assertRaises(IntegrityError, UniqueTest.objects.create, year=2012, slug="foo")
  408. UniqueTest.objects.all().delete()
  409. # Alter the model to it's non-unique-together companion
  410. with connection.schema_editor() as editor:
  411. editor.alter_unique_together(
  412. UniqueTest,
  413. UniqueTest._meta.unique_together,
  414. [],
  415. )
  416. # Ensure the fields are no longer unique
  417. UniqueTest.objects.create(year=2012, slug="foo")
  418. UniqueTest.objects.create(year=2012, slug="foo")
  419. UniqueTest.objects.all().delete()
  420. # Alter it back
  421. new_new_field = SlugField(unique=True)
  422. new_new_field.set_attributes_from_name("slug")
  423. with connection.schema_editor() as editor:
  424. editor.alter_unique_together(
  425. UniqueTest,
  426. [],
  427. UniqueTest._meta.unique_together,
  428. )
  429. # Ensure the fields are unique again
  430. UniqueTest.objects.create(year=2012, slug="foo")
  431. self.assertRaises(IntegrityError, UniqueTest.objects.create, year=2012, slug="foo")
  432. UniqueTest.objects.all().delete()
  433. def test_index_together(self):
  434. """
  435. Tests removing and adding index_together constraints on a model.
  436. """
  437. # Create the table
  438. with connection.schema_editor() as editor:
  439. editor.create_model(Tag)
  440. # Ensure there's no index on the year/slug columns first
  441. self.assertEqual(
  442. False,
  443. any(
  444. c["index"]
  445. for c in connection.introspection.get_constraints(connection.cursor(), "schema_tag").values()
  446. if c['columns'] == ["slug", "title"]
  447. ),
  448. )
  449. # Alter the model to add an index
  450. with connection.schema_editor() as editor:
  451. editor.alter_index_together(
  452. Tag,
  453. [],
  454. [("slug", "title")],
  455. )
  456. # Ensure there is now an index
  457. self.assertEqual(
  458. True,
  459. any(
  460. c["index"]
  461. for c in connection.introspection.get_constraints(connection.cursor(), "schema_tag").values()
  462. if c['columns'] == ["slug", "title"]
  463. ),
  464. )
  465. # Alter it back
  466. new_new_field = SlugField(unique=True)
  467. new_new_field.set_attributes_from_name("slug")
  468. with connection.schema_editor() as editor:
  469. editor.alter_index_together(
  470. Tag,
  471. [("slug", "title")],
  472. [],
  473. )
  474. # Ensure there's no index
  475. self.assertEqual(
  476. False,
  477. any(
  478. c["index"]
  479. for c in connection.introspection.get_constraints(connection.cursor(), "schema_tag").values()
  480. if c['columns'] == ["slug", "title"]
  481. ),
  482. )
  483. def test_create_index_together(self):
  484. """
  485. Tests creating models with index_together already defined
  486. """
  487. # Create the table
  488. with connection.schema_editor() as editor:
  489. editor.create_model(TagIndexed)
  490. # Ensure there is an index
  491. self.assertEqual(
  492. True,
  493. any(
  494. c["index"]
  495. for c in connection.introspection.get_constraints(connection.cursor(), "schema_tagindexed").values()
  496. if c['columns'] == ["slug", "title"]
  497. ),
  498. )
  499. def test_db_table(self):
  500. """
  501. Tests renaming of the table
  502. """
  503. # Create the table
  504. with connection.schema_editor() as editor:
  505. editor.create_model(Author)
  506. # Ensure the table is there to begin with
  507. columns = self.column_classes(Author)
  508. self.assertEqual(columns['name'][0], "CharField")
  509. # Alter the table
  510. with connection.schema_editor() as editor:
  511. editor.alter_db_table(
  512. Author,
  513. "schema_author",
  514. "schema_otherauthor",
  515. )
  516. # Ensure the table is there afterwards
  517. Author._meta.db_table = "schema_otherauthor"
  518. columns = self.column_classes(Author)
  519. self.assertEqual(columns['name'][0], "CharField")
  520. # Alter the table again
  521. with connection.schema_editor() as editor:
  522. editor.alter_db_table(
  523. Author,
  524. "schema_otherauthor",
  525. "schema_author",
  526. )
  527. # Ensure the table is still there
  528. Author._meta.db_table = "schema_author"
  529. columns = self.column_classes(Author)
  530. self.assertEqual(columns['name'][0], "CharField")
  531. def test_indexes(self):
  532. """
  533. Tests creation/altering of indexes
  534. """
  535. # Create the table
  536. with connection.schema_editor() as editor:
  537. editor.create_model(Author)
  538. editor.create_model(Book)
  539. # Ensure the table is there and has the right index
  540. self.assertIn(
  541. "title",
  542. connection.introspection.get_indexes(connection.cursor(), Book._meta.db_table),
  543. )
  544. # Alter to remove the index
  545. new_field = CharField(max_length=100, db_index=False)
  546. new_field.set_attributes_from_name("title")
  547. with connection.schema_editor() as editor:
  548. editor.alter_field(
  549. Book,
  550. Book._meta.get_field_by_name("title")[0],
  551. new_field,
  552. strict = True,
  553. )
  554. # Ensure the table is there and has no index
  555. self.assertNotIn(
  556. "title",
  557. connection.introspection.get_indexes(connection.cursor(), Book._meta.db_table),
  558. )
  559. # Alter to re-add the index
  560. with connection.schema_editor() as editor:
  561. editor.alter_field(
  562. Book,
  563. new_field,
  564. Book._meta.get_field_by_name("title")[0],
  565. strict = True,
  566. )
  567. # Ensure the table is there and has the index again
  568. self.assertIn(
  569. "title",
  570. connection.introspection.get_indexes(connection.cursor(), Book._meta.db_table),
  571. )
  572. # Add a unique column, verify that creates an implicit index
  573. with connection.schema_editor() as editor:
  574. editor.add_field(
  575. Book,
  576. BookWithSlug._meta.get_field_by_name("slug")[0],
  577. )
  578. self.assertIn(
  579. "slug",
  580. connection.introspection.get_indexes(connection.cursor(), Book._meta.db_table),
  581. )
  582. # Remove the unique, check the index goes with it
  583. new_field2 = CharField(max_length=20, unique=False)
  584. new_field2.set_attributes_from_name("slug")
  585. with connection.schema_editor() as editor:
  586. editor.alter_field(
  587. BookWithSlug,
  588. BookWithSlug._meta.get_field_by_name("slug")[0],
  589. new_field2,
  590. strict = True,
  591. )
  592. self.assertNotIn(
  593. "slug",
  594. connection.introspection.get_indexes(connection.cursor(), Book._meta.db_table),
  595. )
  596. def test_primary_key(self):
  597. """
  598. Tests altering of the primary key
  599. """
  600. # Create the table
  601. with connection.schema_editor() as editor:
  602. editor.create_model(Tag)
  603. # Ensure the table is there and has the right PK
  604. self.assertTrue(
  605. connection.introspection.get_indexes(connection.cursor(), Tag._meta.db_table)['id']['primary_key'],
  606. )
  607. # Alter to change the PK
  608. new_field = SlugField(primary_key=True)
  609. new_field.set_attributes_from_name("slug")
  610. with connection.schema_editor() as editor:
  611. editor.remove_field(Tag, Tag._meta.get_field_by_name("id")[0])
  612. editor.alter_field(
  613. Tag,
  614. Tag._meta.get_field_by_name("slug")[0],
  615. new_field,
  616. )
  617. # Ensure the PK changed
  618. self.assertNotIn(
  619. 'id',
  620. connection.introspection.get_indexes(connection.cursor(), Tag._meta.db_table),
  621. )
  622. self.assertTrue(
  623. connection.introspection.get_indexes(connection.cursor(), Tag._meta.db_table)['slug']['primary_key'],
  624. )