tests.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. import datetime
  2. from unittest import skipIf, skipUnless
  3. from django.db import connection
  4. from django.db.models import CASCADE, ForeignKey, Index, Q
  5. from django.test import (
  6. TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature,
  7. )
  8. from django.test.utils import override_settings
  9. from django.utils import timezone
  10. from .models import (
  11. Article, ArticleTranslation, IndexedArticle2, IndexTogetherSingleList,
  12. )
  13. class SchemaIndexesTests(TestCase):
  14. """
  15. Test index handling by the db.backends.schema infrastructure.
  16. """
  17. def test_index_name_hash(self):
  18. """
  19. Index names should be deterministic.
  20. """
  21. editor = connection.schema_editor()
  22. index_name = editor._create_index_name(
  23. table_name=Article._meta.db_table,
  24. column_names=("c1",),
  25. suffix="123",
  26. )
  27. self.assertEqual(index_name, "indexes_article_c1_a52bd80b123")
  28. def test_index_name(self):
  29. """
  30. Index names on the built-in database backends::
  31. * Are truncated as needed.
  32. * Include all the column names.
  33. * Include a deterministic hash.
  34. """
  35. long_name = 'l%sng' % ('o' * 100)
  36. editor = connection.schema_editor()
  37. index_name = editor._create_index_name(
  38. table_name=Article._meta.db_table,
  39. column_names=('c1', 'c2', long_name),
  40. suffix='ix',
  41. )
  42. expected = {
  43. 'mysql': 'indexes_article_c1_c2_looooooooooooooooooo_255179b2ix',
  44. 'oracle': 'indexes_a_c1_c2_loo_255179b2ix',
  45. 'postgresql': 'indexes_article_c1_c2_loooooooooooooooooo_255179b2ix',
  46. 'sqlite': 'indexes_article_c1_c2_l%sng_255179b2ix' % ('o' * 100),
  47. }
  48. if connection.vendor not in expected:
  49. self.skipTest('This test is only supported on the built-in database backends.')
  50. self.assertEqual(index_name, expected[connection.vendor])
  51. def test_index_together(self):
  52. editor = connection.schema_editor()
  53. index_sql = [str(statement) for statement in editor._model_indexes_sql(Article)]
  54. self.assertEqual(len(index_sql), 1)
  55. # Ensure the index name is properly quoted
  56. self.assertIn(
  57. connection.ops.quote_name(
  58. editor._create_index_name(Article._meta.db_table, ['headline', 'pub_date'], suffix='_idx')
  59. ),
  60. index_sql[0]
  61. )
  62. def test_index_together_single_list(self):
  63. # Test for using index_together with a single list (#22172)
  64. index_sql = connection.schema_editor()._model_indexes_sql(IndexTogetherSingleList)
  65. self.assertEqual(len(index_sql), 1)
  66. def test_columns_list_sql(self):
  67. index = Index(fields=['headline'], name='whitespace_idx')
  68. editor = connection.schema_editor()
  69. self.assertIn(
  70. '(%s)' % editor.quote_name('headline'),
  71. str(index.create_sql(Article, editor)),
  72. )
  73. def test_descending_columns_list_sql(self):
  74. index = Index(fields=['-headline'], name='whitespace_idx')
  75. editor = connection.schema_editor()
  76. self.assertIn(
  77. '(%s DESC)' % editor.quote_name('headline'),
  78. str(index.create_sql(Article, editor)),
  79. )
  80. @skipIf(connection.vendor == 'postgresql', 'opclasses are PostgreSQL only')
  81. class SchemaIndexesNotPostgreSQLTests(TransactionTestCase):
  82. available_apps = ['indexes']
  83. def test_create_index_ignores_opclasses(self):
  84. index = Index(
  85. name='test_ops_class',
  86. fields=['headline'],
  87. opclasses=['varchar_pattern_ops'],
  88. )
  89. with connection.schema_editor() as editor:
  90. # This would error if opclasses weren't ignored.
  91. editor.add_index(IndexedArticle2, index)
  92. # The `condition` parameter is ignored by databases that don't support partial
  93. # indexes.
  94. @skipIfDBFeature('supports_partial_indexes')
  95. class PartialIndexConditionIgnoredTests(TransactionTestCase):
  96. available_apps = ['indexes']
  97. def test_condition_ignored(self):
  98. index = Index(
  99. name='test_condition_ignored',
  100. fields=['published'],
  101. condition=Q(published=True),
  102. )
  103. with connection.schema_editor() as editor:
  104. # This would error if condition weren't ignored.
  105. editor.add_index(Article, index)
  106. self.assertNotIn(
  107. 'WHERE %s' % editor.quote_name('published'),
  108. str(index.create_sql(Article, editor))
  109. )
  110. @skipUnless(connection.vendor == 'postgresql', 'PostgreSQL tests')
  111. class SchemaIndexesPostgreSQLTests(TransactionTestCase):
  112. available_apps = ['indexes']
  113. get_opclass_query = '''
  114. SELECT opcname, c.relname FROM pg_opclass AS oc
  115. JOIN pg_index as i on oc.oid = ANY(i.indclass)
  116. JOIN pg_class as c on c.oid = i.indexrelid
  117. WHERE c.relname = '%s'
  118. '''
  119. def test_text_indexes(self):
  120. """Test creation of PostgreSQL-specific text indexes (#12234)"""
  121. from .models import IndexedArticle
  122. index_sql = [str(statement) for statement in connection.schema_editor()._model_indexes_sql(IndexedArticle)]
  123. self.assertEqual(len(index_sql), 5)
  124. self.assertIn('("headline" varchar_pattern_ops)', index_sql[1])
  125. self.assertIn('("body" text_pattern_ops)', index_sql[3])
  126. # unique=True and db_index=True should only create the varchar-specific
  127. # index (#19441).
  128. self.assertIn('("slug" varchar_pattern_ops)', index_sql[4])
  129. def test_virtual_relation_indexes(self):
  130. """Test indexes are not created for related objects"""
  131. index_sql = connection.schema_editor()._model_indexes_sql(Article)
  132. self.assertEqual(len(index_sql), 1)
  133. def test_ops_class(self):
  134. index = Index(
  135. name='test_ops_class',
  136. fields=['headline'],
  137. opclasses=['varchar_pattern_ops'],
  138. )
  139. with connection.schema_editor() as editor:
  140. editor.add_index(IndexedArticle2, index)
  141. with editor.connection.cursor() as cursor:
  142. cursor.execute(self.get_opclass_query % 'test_ops_class')
  143. self.assertEqual(cursor.fetchall(), [('varchar_pattern_ops', 'test_ops_class')])
  144. def test_ops_class_multiple_columns(self):
  145. index = Index(
  146. name='test_ops_class_multiple',
  147. fields=['headline', 'body'],
  148. opclasses=['varchar_pattern_ops', 'text_pattern_ops'],
  149. )
  150. with connection.schema_editor() as editor:
  151. editor.add_index(IndexedArticle2, index)
  152. with editor.connection.cursor() as cursor:
  153. cursor.execute(self.get_opclass_query % 'test_ops_class_multiple')
  154. expected_ops_classes = (
  155. ('varchar_pattern_ops', 'test_ops_class_multiple'),
  156. ('text_pattern_ops', 'test_ops_class_multiple'),
  157. )
  158. self.assertCountEqual(cursor.fetchall(), expected_ops_classes)
  159. def test_ops_class_partial(self):
  160. index = Index(
  161. name='test_ops_class_partial',
  162. fields=['body'],
  163. opclasses=['text_pattern_ops'],
  164. condition=Q(headline__contains='China'),
  165. )
  166. with connection.schema_editor() as editor:
  167. editor.add_index(IndexedArticle2, index)
  168. with editor.connection.cursor() as cursor:
  169. cursor.execute(self.get_opclass_query % 'test_ops_class_partial')
  170. self.assertCountEqual(cursor.fetchall(), [('text_pattern_ops', 'test_ops_class_partial')])
  171. def test_ops_class_partial_tablespace(self):
  172. indexname = 'test_ops_class_tblspace'
  173. index = Index(
  174. name=indexname,
  175. fields=['body'],
  176. opclasses=['text_pattern_ops'],
  177. condition=Q(headline__contains='China'),
  178. db_tablespace='pg_default',
  179. )
  180. with connection.schema_editor() as editor:
  181. editor.add_index(IndexedArticle2, index)
  182. self.assertIn('TABLESPACE "pg_default" ', str(index.create_sql(IndexedArticle2, editor)))
  183. with editor.connection.cursor() as cursor:
  184. cursor.execute(self.get_opclass_query % indexname)
  185. self.assertCountEqual(cursor.fetchall(), [('text_pattern_ops', indexname)])
  186. def test_ops_class_descending(self):
  187. indexname = 'test_ops_class_ordered'
  188. index = Index(
  189. name=indexname,
  190. fields=['-body'],
  191. opclasses=['text_pattern_ops'],
  192. )
  193. with connection.schema_editor() as editor:
  194. editor.add_index(IndexedArticle2, index)
  195. with editor.connection.cursor() as cursor:
  196. cursor.execute(self.get_opclass_query % indexname)
  197. self.assertCountEqual(cursor.fetchall(), [('text_pattern_ops', indexname)])
  198. def test_ops_class_descending_partial(self):
  199. indexname = 'test_ops_class_ordered_partial'
  200. index = Index(
  201. name=indexname,
  202. fields=['-body'],
  203. opclasses=['text_pattern_ops'],
  204. condition=Q(headline__contains='China'),
  205. )
  206. with connection.schema_editor() as editor:
  207. editor.add_index(IndexedArticle2, index)
  208. with editor.connection.cursor() as cursor:
  209. cursor.execute(self.get_opclass_query % indexname)
  210. self.assertCountEqual(cursor.fetchall(), [('text_pattern_ops', indexname)])
  211. @skipUnlessDBFeature('supports_covering_indexes')
  212. def test_ops_class_include(self):
  213. index_name = 'test_ops_class_include'
  214. index = Index(
  215. name=index_name,
  216. fields=['body'],
  217. opclasses=['text_pattern_ops'],
  218. include=['headline'],
  219. )
  220. with connection.schema_editor() as editor:
  221. editor.add_index(IndexedArticle2, index)
  222. with editor.connection.cursor() as cursor:
  223. cursor.execute(self.get_opclass_query % index_name)
  224. self.assertCountEqual(cursor.fetchall(), [('text_pattern_ops', index_name)])
  225. @skipUnlessDBFeature('supports_covering_indexes')
  226. def test_ops_class_include_tablespace(self):
  227. index_name = 'test_ops_class_include_tblspace'
  228. index = Index(
  229. name=index_name,
  230. fields=['body'],
  231. opclasses=['text_pattern_ops'],
  232. include=['headline'],
  233. db_tablespace='pg_default',
  234. )
  235. with connection.schema_editor() as editor:
  236. editor.add_index(IndexedArticle2, index)
  237. self.assertIn(
  238. 'TABLESPACE "pg_default"',
  239. str(index.create_sql(IndexedArticle2, editor)),
  240. )
  241. with editor.connection.cursor() as cursor:
  242. cursor.execute(self.get_opclass_query % index_name)
  243. self.assertCountEqual(cursor.fetchall(), [('text_pattern_ops', index_name)])
  244. def test_ops_class_columns_lists_sql(self):
  245. index = Index(
  246. fields=['headline'],
  247. name='whitespace_idx',
  248. opclasses=['text_pattern_ops'],
  249. )
  250. with connection.schema_editor() as editor:
  251. self.assertIn(
  252. '(%s text_pattern_ops)' % editor.quote_name('headline'),
  253. str(index.create_sql(Article, editor)),
  254. )
  255. def test_ops_class_descending_columns_list_sql(self):
  256. index = Index(
  257. fields=['-headline'],
  258. name='whitespace_idx',
  259. opclasses=['text_pattern_ops'],
  260. )
  261. with connection.schema_editor() as editor:
  262. self.assertIn(
  263. '(%s text_pattern_ops DESC)' % editor.quote_name('headline'),
  264. str(index.create_sql(Article, editor)),
  265. )
  266. @skipUnless(connection.vendor == 'mysql', 'MySQL tests')
  267. class SchemaIndexesMySQLTests(TransactionTestCase):
  268. available_apps = ['indexes']
  269. def test_no_index_for_foreignkey(self):
  270. """
  271. MySQL on InnoDB already creates indexes automatically for foreign keys.
  272. (#14180). An index should be created if db_constraint=False (#26171).
  273. """
  274. with connection.cursor() as cursor:
  275. storage = connection.introspection.get_storage_engine(
  276. cursor, ArticleTranslation._meta.db_table,
  277. )
  278. if storage != "InnoDB":
  279. self.skip("This test only applies to the InnoDB storage engine")
  280. index_sql = [str(statement) for statement in connection.schema_editor()._model_indexes_sql(ArticleTranslation)]
  281. self.assertEqual(index_sql, [
  282. 'CREATE INDEX `indexes_articletranslation_article_no_constraint_id_d6c0806b` '
  283. 'ON `indexes_articletranslation` (`article_no_constraint_id`)'
  284. ])
  285. # The index also shouldn't be created if the ForeignKey is added after
  286. # the model was created.
  287. field_created = False
  288. try:
  289. with connection.schema_editor() as editor:
  290. new_field = ForeignKey(Article, CASCADE)
  291. new_field.set_attributes_from_name('new_foreign_key')
  292. editor.add_field(ArticleTranslation, new_field)
  293. field_created = True
  294. # No deferred SQL. The FK constraint is included in the
  295. # statement to add the field.
  296. self.assertFalse(editor.deferred_sql)
  297. finally:
  298. if field_created:
  299. with connection.schema_editor() as editor:
  300. editor.remove_field(ArticleTranslation, new_field)
  301. @skipUnlessDBFeature('supports_partial_indexes')
  302. # SQLite doesn't support timezone-aware datetimes when USE_TZ is False.
  303. @override_settings(USE_TZ=True)
  304. class PartialIndexTests(TransactionTestCase):
  305. # Schema editor is used to create the index to test that it works.
  306. available_apps = ['indexes']
  307. def test_partial_index(self):
  308. with connection.schema_editor() as editor:
  309. index = Index(
  310. name='recent_article_idx',
  311. fields=['pub_date'],
  312. condition=Q(
  313. pub_date__gt=datetime.datetime(
  314. year=2015, month=1, day=1,
  315. # PostgreSQL would otherwise complain about the lookup
  316. # being converted to a mutable function (by removing
  317. # the timezone in the cast) which is forbidden.
  318. tzinfo=timezone.get_current_timezone(),
  319. ),
  320. )
  321. )
  322. self.assertIn(
  323. 'WHERE %s' % editor.quote_name('pub_date'),
  324. str(index.create_sql(Article, schema_editor=editor))
  325. )
  326. editor.add_index(index=index, model=Article)
  327. with connection.cursor() as cursor:
  328. self.assertIn(index.name, connection.introspection.get_constraints(
  329. cursor=cursor, table_name=Article._meta.db_table,
  330. ))
  331. editor.remove_index(index=index, model=Article)
  332. def test_integer_restriction_partial(self):
  333. with connection.schema_editor() as editor:
  334. index = Index(
  335. name='recent_article_idx',
  336. fields=['id'],
  337. condition=Q(pk__gt=1),
  338. )
  339. self.assertIn(
  340. 'WHERE %s' % editor.quote_name('id'),
  341. str(index.create_sql(Article, schema_editor=editor))
  342. )
  343. editor.add_index(index=index, model=Article)
  344. with connection.cursor() as cursor:
  345. self.assertIn(index.name, connection.introspection.get_constraints(
  346. cursor=cursor, table_name=Article._meta.db_table,
  347. ))
  348. editor.remove_index(index=index, model=Article)
  349. def test_boolean_restriction_partial(self):
  350. with connection.schema_editor() as editor:
  351. index = Index(
  352. name='published_index',
  353. fields=['published'],
  354. condition=Q(published=True),
  355. )
  356. self.assertIn(
  357. 'WHERE %s' % editor.quote_name('published'),
  358. str(index.create_sql(Article, schema_editor=editor))
  359. )
  360. editor.add_index(index=index, model=Article)
  361. with connection.cursor() as cursor:
  362. self.assertIn(index.name, connection.introspection.get_constraints(
  363. cursor=cursor, table_name=Article._meta.db_table,
  364. ))
  365. editor.remove_index(index=index, model=Article)
  366. @skipUnlessDBFeature('supports_functions_in_partial_indexes')
  367. def test_multiple_conditions(self):
  368. with connection.schema_editor() as editor:
  369. index = Index(
  370. name='recent_article_idx',
  371. fields=['pub_date', 'headline'],
  372. condition=(
  373. Q(pub_date__gt=datetime.datetime(
  374. year=2015,
  375. month=1,
  376. day=1,
  377. tzinfo=timezone.get_current_timezone(),
  378. )) & Q(headline__contains='China')
  379. ),
  380. )
  381. sql = str(index.create_sql(Article, schema_editor=editor))
  382. where = sql.find('WHERE')
  383. self.assertIn(
  384. 'WHERE (%s' % editor.quote_name('pub_date'),
  385. sql
  386. )
  387. # Because each backend has different syntax for the operators,
  388. # check ONLY the occurrence of headline in the SQL.
  389. self.assertGreater(sql.rfind('headline'), where)
  390. editor.add_index(index=index, model=Article)
  391. with connection.cursor() as cursor:
  392. self.assertIn(index.name, connection.introspection.get_constraints(
  393. cursor=cursor, table_name=Article._meta.db_table,
  394. ))
  395. editor.remove_index(index=index, model=Article)
  396. def test_is_null_condition(self):
  397. with connection.schema_editor() as editor:
  398. index = Index(
  399. name='recent_article_idx',
  400. fields=['pub_date'],
  401. condition=Q(pub_date__isnull=False),
  402. )
  403. self.assertIn(
  404. 'WHERE %s IS NOT NULL' % editor.quote_name('pub_date'),
  405. str(index.create_sql(Article, schema_editor=editor))
  406. )
  407. editor.add_index(index=index, model=Article)
  408. with connection.cursor() as cursor:
  409. self.assertIn(index.name, connection.introspection.get_constraints(
  410. cursor=cursor, table_name=Article._meta.db_table,
  411. ))
  412. editor.remove_index(index=index, model=Article)
  413. @skipUnlessDBFeature('supports_covering_indexes')
  414. class CoveringIndexTests(TransactionTestCase):
  415. available_apps = ['indexes']
  416. def test_covering_index(self):
  417. index = Index(
  418. name='covering_headline_idx',
  419. fields=['headline'],
  420. include=['pub_date', 'published'],
  421. )
  422. with connection.schema_editor() as editor:
  423. self.assertIn(
  424. '(%s) INCLUDE (%s, %s)' % (
  425. editor.quote_name('headline'),
  426. editor.quote_name('pub_date'),
  427. editor.quote_name('published'),
  428. ),
  429. str(index.create_sql(Article, editor)),
  430. )
  431. editor.add_index(Article, index)
  432. with connection.cursor() as cursor:
  433. constraints = connection.introspection.get_constraints(
  434. cursor=cursor, table_name=Article._meta.db_table,
  435. )
  436. self.assertIn(index.name, constraints)
  437. self.assertEqual(
  438. constraints[index.name]['columns'],
  439. ['headline', 'pub_date', 'published'],
  440. )
  441. editor.remove_index(Article, index)
  442. with connection.cursor() as cursor:
  443. self.assertNotIn(index.name, connection.introspection.get_constraints(
  444. cursor=cursor, table_name=Article._meta.db_table,
  445. ))
  446. def test_covering_partial_index(self):
  447. index = Index(
  448. name='covering_partial_headline_idx',
  449. fields=['headline'],
  450. include=['pub_date'],
  451. condition=Q(pub_date__isnull=False),
  452. )
  453. with connection.schema_editor() as editor:
  454. self.assertIn(
  455. '(%s) INCLUDE (%s) WHERE %s ' % (
  456. editor.quote_name('headline'),
  457. editor.quote_name('pub_date'),
  458. editor.quote_name('pub_date'),
  459. ),
  460. str(index.create_sql(Article, editor)),
  461. )
  462. editor.add_index(Article, index)
  463. with connection.cursor() as cursor:
  464. constraints = connection.introspection.get_constraints(
  465. cursor=cursor, table_name=Article._meta.db_table,
  466. )
  467. self.assertIn(index.name, constraints)
  468. self.assertEqual(
  469. constraints[index.name]['columns'],
  470. ['headline', 'pub_date'],
  471. )
  472. editor.remove_index(Article, index)
  473. with connection.cursor() as cursor:
  474. self.assertNotIn(index.name, connection.introspection.get_constraints(
  475. cursor=cursor, table_name=Article._meta.db_table,
  476. ))
  477. @skipIfDBFeature('supports_covering_indexes')
  478. class CoveringIndexIgnoredTests(TransactionTestCase):
  479. available_apps = ['indexes']
  480. def test_covering_ignored(self):
  481. index = Index(
  482. name='test_covering_ignored',
  483. fields=['headline'],
  484. include=['pub_date'],
  485. )
  486. with connection.schema_editor() as editor:
  487. editor.add_index(Article, index)
  488. self.assertNotIn(
  489. 'INCLUDE (%s)' % editor.quote_name('headline'),
  490. str(index.create_sql(Article, editor)),
  491. )