test_indexes.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  1. from unittest import mock
  2. from django.contrib.postgres.indexes import (
  3. BloomIndex,
  4. BrinIndex,
  5. BTreeIndex,
  6. GinIndex,
  7. GistIndex,
  8. HashIndex,
  9. OpClass,
  10. PostgresIndex,
  11. SpGistIndex,
  12. )
  13. from django.db import NotSupportedError, connection
  14. from django.db.models import CharField, F, Index, Q
  15. from django.db.models.functions import Cast, Collate, Length, Lower
  16. from django.test import skipUnlessDBFeature
  17. from django.test.utils import register_lookup
  18. from . import PostgreSQLSimpleTestCase, PostgreSQLTestCase
  19. from .fields import SearchVector, SearchVectorField
  20. from .models import CharFieldModel, IntegerArrayModel, Scene, TextFieldModel
  21. class IndexTestMixin:
  22. def test_name_auto_generation(self):
  23. index = self.index_class(fields=["field"])
  24. index.set_name_with_model(CharFieldModel)
  25. self.assertRegex(
  26. index.name, r"postgres_te_field_[0-9a-f]{6}_%s" % self.index_class.suffix
  27. )
  28. def test_deconstruction_no_customization(self):
  29. index = self.index_class(
  30. fields=["title"], name="test_title_%s" % self.index_class.suffix
  31. )
  32. path, args, kwargs = index.deconstruct()
  33. self.assertEqual(
  34. path, "django.contrib.postgres.indexes.%s" % self.index_class.__name__
  35. )
  36. self.assertEqual(args, ())
  37. self.assertEqual(
  38. kwargs,
  39. {"fields": ["title"], "name": "test_title_%s" % self.index_class.suffix},
  40. )
  41. def test_deconstruction_with_expressions_no_customization(self):
  42. name = f"test_title_{self.index_class.suffix}"
  43. index = self.index_class(Lower("title"), name=name)
  44. path, args, kwargs = index.deconstruct()
  45. self.assertEqual(
  46. path,
  47. f"django.contrib.postgres.indexes.{self.index_class.__name__}",
  48. )
  49. self.assertEqual(args, (Lower("title"),))
  50. self.assertEqual(kwargs, {"name": name})
  51. class BloomIndexTests(IndexTestMixin, PostgreSQLSimpleTestCase):
  52. index_class = BloomIndex
  53. def test_suffix(self):
  54. self.assertEqual(BloomIndex.suffix, "bloom")
  55. def test_deconstruction(self):
  56. index = BloomIndex(fields=["title"], name="test_bloom", length=80, columns=[4])
  57. path, args, kwargs = index.deconstruct()
  58. self.assertEqual(path, "django.contrib.postgres.indexes.BloomIndex")
  59. self.assertEqual(args, ())
  60. self.assertEqual(
  61. kwargs,
  62. {
  63. "fields": ["title"],
  64. "name": "test_bloom",
  65. "length": 80,
  66. "columns": [4],
  67. },
  68. )
  69. def test_invalid_fields(self):
  70. msg = "Bloom indexes support a maximum of 32 fields."
  71. with self.assertRaisesMessage(ValueError, msg):
  72. BloomIndex(fields=["title"] * 33, name="test_bloom")
  73. def test_invalid_columns(self):
  74. msg = "BloomIndex.columns must be a list or tuple."
  75. with self.assertRaisesMessage(ValueError, msg):
  76. BloomIndex(fields=["title"], name="test_bloom", columns="x")
  77. msg = "BloomIndex.columns cannot have more values than fields."
  78. with self.assertRaisesMessage(ValueError, msg):
  79. BloomIndex(fields=["title"], name="test_bloom", columns=[4, 3])
  80. def test_invalid_columns_value(self):
  81. msg = "BloomIndex.columns must contain integers from 1 to 4095."
  82. for length in (0, 4096):
  83. with self.subTest(length), self.assertRaisesMessage(ValueError, msg):
  84. BloomIndex(fields=["title"], name="test_bloom", columns=[length])
  85. def test_invalid_length(self):
  86. msg = "BloomIndex.length must be None or an integer from 1 to 4096."
  87. for length in (0, 4097):
  88. with self.subTest(length), self.assertRaisesMessage(ValueError, msg):
  89. BloomIndex(fields=["title"], name="test_bloom", length=length)
  90. class BrinIndexTests(IndexTestMixin, PostgreSQLSimpleTestCase):
  91. index_class = BrinIndex
  92. def test_suffix(self):
  93. self.assertEqual(BrinIndex.suffix, "brin")
  94. def test_deconstruction(self):
  95. index = BrinIndex(
  96. fields=["title"],
  97. name="test_title_brin",
  98. autosummarize=True,
  99. pages_per_range=16,
  100. )
  101. path, args, kwargs = index.deconstruct()
  102. self.assertEqual(path, "django.contrib.postgres.indexes.BrinIndex")
  103. self.assertEqual(args, ())
  104. self.assertEqual(
  105. kwargs,
  106. {
  107. "fields": ["title"],
  108. "name": "test_title_brin",
  109. "autosummarize": True,
  110. "pages_per_range": 16,
  111. },
  112. )
  113. def test_invalid_pages_per_range(self):
  114. with self.assertRaisesMessage(
  115. ValueError, "pages_per_range must be None or a positive integer"
  116. ):
  117. BrinIndex(fields=["title"], name="test_title_brin", pages_per_range=0)
  118. class BTreeIndexTests(IndexTestMixin, PostgreSQLSimpleTestCase):
  119. index_class = BTreeIndex
  120. def test_suffix(self):
  121. self.assertEqual(BTreeIndex.suffix, "btree")
  122. def test_deconstruction(self):
  123. index = BTreeIndex(fields=["title"], name="test_title_btree")
  124. path, args, kwargs = index.deconstruct()
  125. self.assertEqual(path, "django.contrib.postgres.indexes.BTreeIndex")
  126. self.assertEqual(args, ())
  127. self.assertEqual(kwargs, {"fields": ["title"], "name": "test_title_btree"})
  128. index = BTreeIndex(
  129. fields=["title"],
  130. name="test_title_btree",
  131. fillfactor=80,
  132. deduplicate_items=False,
  133. )
  134. path, args, kwargs = index.deconstruct()
  135. self.assertEqual(path, "django.contrib.postgres.indexes.BTreeIndex")
  136. self.assertEqual(args, ())
  137. self.assertEqual(
  138. kwargs,
  139. {
  140. "fields": ["title"],
  141. "name": "test_title_btree",
  142. "fillfactor": 80,
  143. "deduplicate_items": False,
  144. },
  145. )
  146. class GinIndexTests(IndexTestMixin, PostgreSQLSimpleTestCase):
  147. index_class = GinIndex
  148. def test_suffix(self):
  149. self.assertEqual(GinIndex.suffix, "gin")
  150. def test_deconstruction(self):
  151. index = GinIndex(
  152. fields=["title"],
  153. name="test_title_gin",
  154. fastupdate=True,
  155. gin_pending_list_limit=128,
  156. )
  157. path, args, kwargs = index.deconstruct()
  158. self.assertEqual(path, "django.contrib.postgres.indexes.GinIndex")
  159. self.assertEqual(args, ())
  160. self.assertEqual(
  161. kwargs,
  162. {
  163. "fields": ["title"],
  164. "name": "test_title_gin",
  165. "fastupdate": True,
  166. "gin_pending_list_limit": 128,
  167. },
  168. )
  169. class GistIndexTests(IndexTestMixin, PostgreSQLSimpleTestCase):
  170. index_class = GistIndex
  171. def test_suffix(self):
  172. self.assertEqual(GistIndex.suffix, "gist")
  173. def test_deconstruction(self):
  174. index = GistIndex(
  175. fields=["title"], name="test_title_gist", buffering=False, fillfactor=80
  176. )
  177. path, args, kwargs = index.deconstruct()
  178. self.assertEqual(path, "django.contrib.postgres.indexes.GistIndex")
  179. self.assertEqual(args, ())
  180. self.assertEqual(
  181. kwargs,
  182. {
  183. "fields": ["title"],
  184. "name": "test_title_gist",
  185. "buffering": False,
  186. "fillfactor": 80,
  187. },
  188. )
  189. class HashIndexTests(IndexTestMixin, PostgreSQLSimpleTestCase):
  190. index_class = HashIndex
  191. def test_suffix(self):
  192. self.assertEqual(HashIndex.suffix, "hash")
  193. def test_deconstruction(self):
  194. index = HashIndex(fields=["title"], name="test_title_hash", fillfactor=80)
  195. path, args, kwargs = index.deconstruct()
  196. self.assertEqual(path, "django.contrib.postgres.indexes.HashIndex")
  197. self.assertEqual(args, ())
  198. self.assertEqual(
  199. kwargs, {"fields": ["title"], "name": "test_title_hash", "fillfactor": 80}
  200. )
  201. class SpGistIndexTests(IndexTestMixin, PostgreSQLSimpleTestCase):
  202. index_class = SpGistIndex
  203. def test_suffix(self):
  204. self.assertEqual(SpGistIndex.suffix, "spgist")
  205. def test_deconstruction(self):
  206. index = SpGistIndex(fields=["title"], name="test_title_spgist", fillfactor=80)
  207. path, args, kwargs = index.deconstruct()
  208. self.assertEqual(path, "django.contrib.postgres.indexes.SpGistIndex")
  209. self.assertEqual(args, ())
  210. self.assertEqual(
  211. kwargs, {"fields": ["title"], "name": "test_title_spgist", "fillfactor": 80}
  212. )
  213. class SchemaTests(PostgreSQLTestCase):
  214. get_opclass_query = """
  215. SELECT opcname, c.relname FROM pg_opclass AS oc
  216. JOIN pg_index as i on oc.oid = ANY(i.indclass)
  217. JOIN pg_class as c on c.oid = i.indexrelid
  218. WHERE c.relname = %s
  219. """
  220. def get_constraints(self, table):
  221. """
  222. Get the indexes on the table using a new cursor.
  223. """
  224. with connection.cursor() as cursor:
  225. return connection.introspection.get_constraints(cursor, table)
  226. def test_gin_index(self):
  227. # Ensure the table is there and doesn't have an index.
  228. self.assertNotIn(
  229. "field", self.get_constraints(IntegerArrayModel._meta.db_table)
  230. )
  231. # Add the index
  232. index_name = "integer_array_model_field_gin"
  233. index = GinIndex(fields=["field"], name=index_name)
  234. with connection.schema_editor() as editor:
  235. editor.add_index(IntegerArrayModel, index)
  236. constraints = self.get_constraints(IntegerArrayModel._meta.db_table)
  237. # Check gin index was added
  238. self.assertEqual(constraints[index_name]["type"], GinIndex.suffix)
  239. # Drop the index
  240. with connection.schema_editor() as editor:
  241. editor.remove_index(IntegerArrayModel, index)
  242. self.assertNotIn(
  243. index_name, self.get_constraints(IntegerArrayModel._meta.db_table)
  244. )
  245. def test_gin_fastupdate(self):
  246. index_name = "integer_array_gin_fastupdate"
  247. index = GinIndex(fields=["field"], name=index_name, fastupdate=False)
  248. with connection.schema_editor() as editor:
  249. editor.add_index(IntegerArrayModel, index)
  250. constraints = self.get_constraints(IntegerArrayModel._meta.db_table)
  251. self.assertEqual(constraints[index_name]["type"], "gin")
  252. self.assertEqual(constraints[index_name]["options"], ["fastupdate=off"])
  253. with connection.schema_editor() as editor:
  254. editor.remove_index(IntegerArrayModel, index)
  255. self.assertNotIn(
  256. index_name, self.get_constraints(IntegerArrayModel._meta.db_table)
  257. )
  258. def test_partial_gin_index(self):
  259. with register_lookup(CharField, Length):
  260. index_name = "char_field_gin_partial_idx"
  261. index = GinIndex(
  262. fields=["field"], name=index_name, condition=Q(field__length=40)
  263. )
  264. with connection.schema_editor() as editor:
  265. editor.add_index(CharFieldModel, index)
  266. constraints = self.get_constraints(CharFieldModel._meta.db_table)
  267. self.assertEqual(constraints[index_name]["type"], "gin")
  268. with connection.schema_editor() as editor:
  269. editor.remove_index(CharFieldModel, index)
  270. self.assertNotIn(
  271. index_name, self.get_constraints(CharFieldModel._meta.db_table)
  272. )
  273. def test_partial_gin_index_with_tablespace(self):
  274. with register_lookup(CharField, Length):
  275. index_name = "char_field_gin_partial_idx"
  276. index = GinIndex(
  277. fields=["field"],
  278. name=index_name,
  279. condition=Q(field__length=40),
  280. db_tablespace="pg_default",
  281. )
  282. with connection.schema_editor() as editor:
  283. editor.add_index(CharFieldModel, index)
  284. self.assertIn(
  285. 'TABLESPACE "pg_default" ',
  286. str(index.create_sql(CharFieldModel, editor)),
  287. )
  288. constraints = self.get_constraints(CharFieldModel._meta.db_table)
  289. self.assertEqual(constraints[index_name]["type"], "gin")
  290. with connection.schema_editor() as editor:
  291. editor.remove_index(CharFieldModel, index)
  292. self.assertNotIn(
  293. index_name, self.get_constraints(CharFieldModel._meta.db_table)
  294. )
  295. def test_gin_parameters(self):
  296. index_name = "integer_array_gin_params"
  297. index = GinIndex(
  298. fields=["field"],
  299. name=index_name,
  300. fastupdate=True,
  301. gin_pending_list_limit=64,
  302. db_tablespace="pg_default",
  303. )
  304. with connection.schema_editor() as editor:
  305. editor.add_index(IntegerArrayModel, index)
  306. self.assertIn(
  307. ") WITH (gin_pending_list_limit = 64, fastupdate = on) TABLESPACE",
  308. str(index.create_sql(IntegerArrayModel, editor)),
  309. )
  310. constraints = self.get_constraints(IntegerArrayModel._meta.db_table)
  311. self.assertEqual(constraints[index_name]["type"], "gin")
  312. self.assertEqual(
  313. constraints[index_name]["options"],
  314. ["gin_pending_list_limit=64", "fastupdate=on"],
  315. )
  316. with connection.schema_editor() as editor:
  317. editor.remove_index(IntegerArrayModel, index)
  318. self.assertNotIn(
  319. index_name, self.get_constraints(IntegerArrayModel._meta.db_table)
  320. )
  321. def test_trigram_op_class_gin_index(self):
  322. index_name = "trigram_op_class_gin"
  323. index = GinIndex(OpClass(F("scene"), name="gin_trgm_ops"), name=index_name)
  324. with connection.schema_editor() as editor:
  325. editor.add_index(Scene, index)
  326. with editor.connection.cursor() as cursor:
  327. cursor.execute(self.get_opclass_query, [index_name])
  328. self.assertCountEqual(cursor.fetchall(), [("gin_trgm_ops", index_name)])
  329. constraints = self.get_constraints(Scene._meta.db_table)
  330. self.assertIn(index_name, constraints)
  331. self.assertIn(constraints[index_name]["type"], GinIndex.suffix)
  332. with connection.schema_editor() as editor:
  333. editor.remove_index(Scene, index)
  334. self.assertNotIn(index_name, self.get_constraints(Scene._meta.db_table))
  335. def test_cast_search_vector_gin_index(self):
  336. index_name = "cast_search_vector_gin"
  337. index = GinIndex(Cast("field", SearchVectorField()), name=index_name)
  338. with connection.schema_editor() as editor:
  339. editor.add_index(TextFieldModel, index)
  340. sql = index.create_sql(TextFieldModel, editor)
  341. table = TextFieldModel._meta.db_table
  342. constraints = self.get_constraints(table)
  343. self.assertIn(index_name, constraints)
  344. self.assertIn(constraints[index_name]["type"], GinIndex.suffix)
  345. self.assertIs(sql.references_column(table, "field"), True)
  346. self.assertIn("::tsvector", str(sql))
  347. with connection.schema_editor() as editor:
  348. editor.remove_index(TextFieldModel, index)
  349. self.assertNotIn(index_name, self.get_constraints(table))
  350. def test_bloom_index(self):
  351. index_name = "char_field_model_field_bloom"
  352. index = BloomIndex(fields=["field"], name=index_name)
  353. with connection.schema_editor() as editor:
  354. editor.add_index(CharFieldModel, index)
  355. constraints = self.get_constraints(CharFieldModel._meta.db_table)
  356. self.assertEqual(constraints[index_name]["type"], BloomIndex.suffix)
  357. with connection.schema_editor() as editor:
  358. editor.remove_index(CharFieldModel, index)
  359. self.assertNotIn(
  360. index_name, self.get_constraints(CharFieldModel._meta.db_table)
  361. )
  362. def test_bloom_parameters(self):
  363. index_name = "char_field_model_field_bloom_params"
  364. index = BloomIndex(fields=["field"], name=index_name, length=512, columns=[3])
  365. with connection.schema_editor() as editor:
  366. editor.add_index(CharFieldModel, index)
  367. constraints = self.get_constraints(CharFieldModel._meta.db_table)
  368. self.assertEqual(constraints[index_name]["type"], BloomIndex.suffix)
  369. self.assertEqual(constraints[index_name]["options"], ["length=512", "col1=3"])
  370. with connection.schema_editor() as editor:
  371. editor.remove_index(CharFieldModel, index)
  372. self.assertNotIn(
  373. index_name, self.get_constraints(CharFieldModel._meta.db_table)
  374. )
  375. def test_brin_index(self):
  376. index_name = "char_field_model_field_brin"
  377. index = BrinIndex(fields=["field"], name=index_name, pages_per_range=4)
  378. with connection.schema_editor() as editor:
  379. editor.add_index(CharFieldModel, index)
  380. constraints = self.get_constraints(CharFieldModel._meta.db_table)
  381. self.assertEqual(constraints[index_name]["type"], BrinIndex.suffix)
  382. self.assertEqual(constraints[index_name]["options"], ["pages_per_range=4"])
  383. with connection.schema_editor() as editor:
  384. editor.remove_index(CharFieldModel, index)
  385. self.assertNotIn(
  386. index_name, self.get_constraints(CharFieldModel._meta.db_table)
  387. )
  388. def test_brin_parameters(self):
  389. index_name = "char_field_brin_params"
  390. index = BrinIndex(fields=["field"], name=index_name, autosummarize=True)
  391. with connection.schema_editor() as editor:
  392. editor.add_index(CharFieldModel, index)
  393. constraints = self.get_constraints(CharFieldModel._meta.db_table)
  394. self.assertEqual(constraints[index_name]["type"], BrinIndex.suffix)
  395. self.assertEqual(constraints[index_name]["options"], ["autosummarize=on"])
  396. with connection.schema_editor() as editor:
  397. editor.remove_index(CharFieldModel, index)
  398. self.assertNotIn(
  399. index_name, self.get_constraints(CharFieldModel._meta.db_table)
  400. )
  401. def test_btree_index(self):
  402. # Ensure the table is there and doesn't have an index.
  403. self.assertNotIn("field", self.get_constraints(CharFieldModel._meta.db_table))
  404. # Add the index.
  405. index_name = "char_field_model_field_btree"
  406. index = BTreeIndex(fields=["field"], name=index_name)
  407. with connection.schema_editor() as editor:
  408. editor.add_index(CharFieldModel, index)
  409. constraints = self.get_constraints(CharFieldModel._meta.db_table)
  410. # The index was added.
  411. self.assertEqual(constraints[index_name]["type"], BTreeIndex.suffix)
  412. # Drop the index.
  413. with connection.schema_editor() as editor:
  414. editor.remove_index(CharFieldModel, index)
  415. self.assertNotIn(
  416. index_name, self.get_constraints(CharFieldModel._meta.db_table)
  417. )
  418. def test_btree_parameters(self):
  419. index_name = "integer_array_btree_parameters"
  420. index = BTreeIndex(
  421. fields=["field"], name=index_name, fillfactor=80, deduplicate_items=False
  422. )
  423. with connection.schema_editor() as editor:
  424. editor.add_index(CharFieldModel, index)
  425. constraints = self.get_constraints(CharFieldModel._meta.db_table)
  426. self.assertEqual(constraints[index_name]["type"], BTreeIndex.suffix)
  427. self.assertEqual(
  428. constraints[index_name]["options"],
  429. ["fillfactor=80", "deduplicate_items=off"],
  430. )
  431. with connection.schema_editor() as editor:
  432. editor.remove_index(CharFieldModel, index)
  433. self.assertNotIn(
  434. index_name, self.get_constraints(CharFieldModel._meta.db_table)
  435. )
  436. def test_gist_index(self):
  437. # Ensure the table is there and doesn't have an index.
  438. self.assertNotIn("field", self.get_constraints(CharFieldModel._meta.db_table))
  439. # Add the index.
  440. index_name = "char_field_model_field_gist"
  441. index = GistIndex(fields=["field"], name=index_name)
  442. with connection.schema_editor() as editor:
  443. editor.add_index(CharFieldModel, index)
  444. constraints = self.get_constraints(CharFieldModel._meta.db_table)
  445. # The index was added.
  446. self.assertEqual(constraints[index_name]["type"], GistIndex.suffix)
  447. # Drop the index.
  448. with connection.schema_editor() as editor:
  449. editor.remove_index(CharFieldModel, index)
  450. self.assertNotIn(
  451. index_name, self.get_constraints(CharFieldModel._meta.db_table)
  452. )
  453. def test_gist_parameters(self):
  454. index_name = "integer_array_gist_buffering"
  455. index = GistIndex(
  456. fields=["field"], name=index_name, buffering=True, fillfactor=80
  457. )
  458. with connection.schema_editor() as editor:
  459. editor.add_index(CharFieldModel, index)
  460. constraints = self.get_constraints(CharFieldModel._meta.db_table)
  461. self.assertEqual(constraints[index_name]["type"], GistIndex.suffix)
  462. self.assertEqual(
  463. constraints[index_name]["options"], ["buffering=on", "fillfactor=80"]
  464. )
  465. with connection.schema_editor() as editor:
  466. editor.remove_index(CharFieldModel, index)
  467. self.assertNotIn(
  468. index_name, self.get_constraints(CharFieldModel._meta.db_table)
  469. )
  470. def test_gist_include(self):
  471. index_name = "scene_gist_include_setting"
  472. index = GistIndex(name=index_name, fields=["scene"], include=["setting"])
  473. with connection.schema_editor() as editor:
  474. editor.add_index(Scene, index)
  475. constraints = self.get_constraints(Scene._meta.db_table)
  476. self.assertIn(index_name, constraints)
  477. self.assertEqual(constraints[index_name]["type"], GistIndex.suffix)
  478. self.assertEqual(constraints[index_name]["columns"], ["scene", "setting"])
  479. with connection.schema_editor() as editor:
  480. editor.remove_index(Scene, index)
  481. self.assertNotIn(index_name, self.get_constraints(Scene._meta.db_table))
  482. def test_tsvector_op_class_gist_index(self):
  483. index_name = "tsvector_op_class_gist"
  484. index = GistIndex(
  485. OpClass(
  486. SearchVector("scene", "setting", config="english"),
  487. name="tsvector_ops",
  488. ),
  489. name=index_name,
  490. )
  491. with connection.schema_editor() as editor:
  492. editor.add_index(Scene, index)
  493. sql = index.create_sql(Scene, editor)
  494. table = Scene._meta.db_table
  495. constraints = self.get_constraints(table)
  496. self.assertIn(index_name, constraints)
  497. self.assertIn(constraints[index_name]["type"], GistIndex.suffix)
  498. self.assertIs(sql.references_column(table, "scene"), True)
  499. self.assertIs(sql.references_column(table, "setting"), True)
  500. with connection.schema_editor() as editor:
  501. editor.remove_index(Scene, index)
  502. self.assertNotIn(index_name, self.get_constraints(table))
  503. def test_search_vector(self):
  504. """SearchVector generates IMMUTABLE SQL in order to be indexable."""
  505. index_name = "test_search_vector"
  506. index = Index(SearchVector("id", "scene", config="english"), name=index_name)
  507. # Indexed function must be IMMUTABLE.
  508. with connection.schema_editor() as editor:
  509. editor.add_index(Scene, index)
  510. constraints = self.get_constraints(Scene._meta.db_table)
  511. self.assertIn(index_name, constraints)
  512. self.assertIs(constraints[index_name]["index"], True)
  513. with connection.schema_editor() as editor:
  514. editor.remove_index(Scene, index)
  515. self.assertNotIn(index_name, self.get_constraints(Scene._meta.db_table))
  516. def test_hash_index(self):
  517. # Ensure the table is there and doesn't have an index.
  518. self.assertNotIn("field", self.get_constraints(CharFieldModel._meta.db_table))
  519. # Add the index.
  520. index_name = "char_field_model_field_hash"
  521. index = HashIndex(fields=["field"], name=index_name)
  522. with connection.schema_editor() as editor:
  523. editor.add_index(CharFieldModel, index)
  524. constraints = self.get_constraints(CharFieldModel._meta.db_table)
  525. # The index was added.
  526. self.assertEqual(constraints[index_name]["type"], HashIndex.suffix)
  527. # Drop the index.
  528. with connection.schema_editor() as editor:
  529. editor.remove_index(CharFieldModel, index)
  530. self.assertNotIn(
  531. index_name, self.get_constraints(CharFieldModel._meta.db_table)
  532. )
  533. def test_hash_parameters(self):
  534. index_name = "integer_array_hash_fillfactor"
  535. index = HashIndex(fields=["field"], name=index_name, fillfactor=80)
  536. with connection.schema_editor() as editor:
  537. editor.add_index(CharFieldModel, index)
  538. constraints = self.get_constraints(CharFieldModel._meta.db_table)
  539. self.assertEqual(constraints[index_name]["type"], HashIndex.suffix)
  540. self.assertEqual(constraints[index_name]["options"], ["fillfactor=80"])
  541. with connection.schema_editor() as editor:
  542. editor.remove_index(CharFieldModel, index)
  543. self.assertNotIn(
  544. index_name, self.get_constraints(CharFieldModel._meta.db_table)
  545. )
  546. def test_spgist_index(self):
  547. # Ensure the table is there and doesn't have an index.
  548. self.assertNotIn("field", self.get_constraints(TextFieldModel._meta.db_table))
  549. # Add the index.
  550. index_name = "text_field_model_field_spgist"
  551. index = SpGistIndex(fields=["field"], name=index_name)
  552. with connection.schema_editor() as editor:
  553. editor.add_index(TextFieldModel, index)
  554. constraints = self.get_constraints(TextFieldModel._meta.db_table)
  555. # The index was added.
  556. self.assertEqual(constraints[index_name]["type"], SpGistIndex.suffix)
  557. # Drop the index.
  558. with connection.schema_editor() as editor:
  559. editor.remove_index(TextFieldModel, index)
  560. self.assertNotIn(
  561. index_name, self.get_constraints(TextFieldModel._meta.db_table)
  562. )
  563. def test_spgist_parameters(self):
  564. index_name = "text_field_model_spgist_fillfactor"
  565. index = SpGistIndex(fields=["field"], name=index_name, fillfactor=80)
  566. with connection.schema_editor() as editor:
  567. editor.add_index(TextFieldModel, index)
  568. constraints = self.get_constraints(TextFieldModel._meta.db_table)
  569. self.assertEqual(constraints[index_name]["type"], SpGistIndex.suffix)
  570. self.assertEqual(constraints[index_name]["options"], ["fillfactor=80"])
  571. with connection.schema_editor() as editor:
  572. editor.remove_index(TextFieldModel, index)
  573. self.assertNotIn(
  574. index_name, self.get_constraints(TextFieldModel._meta.db_table)
  575. )
  576. @skipUnlessDBFeature("supports_covering_spgist_indexes")
  577. def test_spgist_include(self):
  578. index_name = "scene_spgist_include_setting"
  579. index = SpGistIndex(name=index_name, fields=["scene"], include=["setting"])
  580. with connection.schema_editor() as editor:
  581. editor.add_index(Scene, index)
  582. constraints = self.get_constraints(Scene._meta.db_table)
  583. self.assertIn(index_name, constraints)
  584. self.assertEqual(constraints[index_name]["type"], SpGistIndex.suffix)
  585. self.assertEqual(constraints[index_name]["columns"], ["scene", "setting"])
  586. with connection.schema_editor() as editor:
  587. editor.remove_index(Scene, index)
  588. self.assertNotIn(index_name, self.get_constraints(Scene._meta.db_table))
  589. def test_spgist_include_not_supported(self):
  590. index_name = "spgist_include_exception"
  591. index = SpGistIndex(fields=["scene"], name=index_name, include=["setting"])
  592. msg = "Covering SP-GiST indexes require PostgreSQL 14+."
  593. with self.assertRaisesMessage(NotSupportedError, msg):
  594. with mock.patch(
  595. "django.db.backends.postgresql.features.DatabaseFeatures."
  596. "supports_covering_spgist_indexes",
  597. False,
  598. ):
  599. with connection.schema_editor() as editor:
  600. editor.add_index(Scene, index)
  601. self.assertNotIn(index_name, self.get_constraints(Scene._meta.db_table))
  602. def test_custom_suffix(self):
  603. class CustomSuffixIndex(PostgresIndex):
  604. suffix = "sfx"
  605. def create_sql(self, model, schema_editor, using="gin", **kwargs):
  606. return super().create_sql(model, schema_editor, using=using, **kwargs)
  607. index = CustomSuffixIndex(fields=["field"], name="custom_suffix_idx")
  608. self.assertEqual(index.suffix, "sfx")
  609. with connection.schema_editor() as editor:
  610. self.assertIn(
  611. " USING gin ",
  612. str(index.create_sql(CharFieldModel, editor)),
  613. )
  614. def test_op_class(self):
  615. index_name = "test_op_class"
  616. index = Index(
  617. OpClass(Lower("field"), name="text_pattern_ops"),
  618. name=index_name,
  619. )
  620. with connection.schema_editor() as editor:
  621. editor.add_index(TextFieldModel, index)
  622. with editor.connection.cursor() as cursor:
  623. cursor.execute(self.get_opclass_query, [index_name])
  624. self.assertCountEqual(cursor.fetchall(), [("text_pattern_ops", index_name)])
  625. def test_op_class_descending_collation(self):
  626. collation = connection.features.test_collations.get("non_default")
  627. if not collation:
  628. self.skipTest("This backend does not support case-insensitive collations.")
  629. index_name = "test_op_class_descending_collation"
  630. index = Index(
  631. Collate(
  632. OpClass(Lower("field"), name="text_pattern_ops").desc(nulls_last=True),
  633. collation=collation,
  634. ),
  635. name=index_name,
  636. )
  637. with connection.schema_editor() as editor:
  638. editor.add_index(TextFieldModel, index)
  639. self.assertIn(
  640. "COLLATE %s" % editor.quote_name(collation),
  641. str(index.create_sql(TextFieldModel, editor)),
  642. )
  643. with editor.connection.cursor() as cursor:
  644. cursor.execute(self.get_opclass_query, [index_name])
  645. self.assertCountEqual(cursor.fetchall(), [("text_pattern_ops", index_name)])
  646. table = TextFieldModel._meta.db_table
  647. constraints = self.get_constraints(table)
  648. self.assertIn(index_name, constraints)
  649. self.assertEqual(constraints[index_name]["orders"], ["DESC"])
  650. with connection.schema_editor() as editor:
  651. editor.remove_index(TextFieldModel, index)
  652. self.assertNotIn(index_name, self.get_constraints(table))
  653. def test_op_class_descending_partial(self):
  654. index_name = "test_op_class_descending_partial"
  655. index = Index(
  656. OpClass(Lower("field"), name="text_pattern_ops").desc(),
  657. name=index_name,
  658. condition=Q(field__contains="China"),
  659. )
  660. with connection.schema_editor() as editor:
  661. editor.add_index(TextFieldModel, index)
  662. with editor.connection.cursor() as cursor:
  663. cursor.execute(self.get_opclass_query, [index_name])
  664. self.assertCountEqual(cursor.fetchall(), [("text_pattern_ops", index_name)])
  665. constraints = self.get_constraints(TextFieldModel._meta.db_table)
  666. self.assertIn(index_name, constraints)
  667. self.assertEqual(constraints[index_name]["orders"], ["DESC"])
  668. def test_op_class_descending_partial_tablespace(self):
  669. index_name = "test_op_class_descending_partial_tablespace"
  670. index = Index(
  671. OpClass(Lower("field").desc(), name="text_pattern_ops"),
  672. name=index_name,
  673. condition=Q(field__contains="China"),
  674. db_tablespace="pg_default",
  675. )
  676. with connection.schema_editor() as editor:
  677. editor.add_index(TextFieldModel, index)
  678. self.assertIn(
  679. 'TABLESPACE "pg_default" ',
  680. str(index.create_sql(TextFieldModel, editor)),
  681. )
  682. with editor.connection.cursor() as cursor:
  683. cursor.execute(self.get_opclass_query, [index_name])
  684. self.assertCountEqual(cursor.fetchall(), [("text_pattern_ops", index_name)])
  685. constraints = self.get_constraints(TextFieldModel._meta.db_table)
  686. self.assertIn(index_name, constraints)
  687. self.assertEqual(constraints[index_name]["orders"], ["DESC"])