1. from unittest import mock
    
  2. 
    
  3. from django.conf import settings
    
  4. from django.db import connection, models
    
  5. from django.db.models.functions import Lower, Upper
    
  6. from django.test import SimpleTestCase, TestCase, override_settings, skipUnlessDBFeature
    
  7. from django.test.utils import isolate_apps
    
  8. 
    
  9. from .models import Book, ChildModel1, ChildModel2
    
  10. 
    
  11. 
    
  12. class SimpleIndexesTests(SimpleTestCase):
    
  13.     def test_suffix(self):
    
  14.         self.assertEqual(models.Index.suffix, "idx")
    
  15. 
    
  16.     def test_repr(self):
    
  17.         index = models.Index(fields=["title"])
    
  18.         named_index = models.Index(fields=["title"], name="title_idx")
    
  19.         multi_col_index = models.Index(fields=["title", "author"])
    
  20.         partial_index = models.Index(
    
  21.             fields=["title"], name="long_books_idx", condition=models.Q(pages__gt=400)
    
  22.         )
    
  23.         covering_index = models.Index(
    
  24.             fields=["title"],
    
  25.             name="include_idx",
    
  26.             include=["author", "pages"],
    
  27.         )
    
  28.         opclasses_index = models.Index(
    
  29.             fields=["headline", "body"],
    
  30.             name="opclasses_idx",
    
  31.             opclasses=["varchar_pattern_ops", "text_pattern_ops"],
    
  32.         )
    
  33.         func_index = models.Index(Lower("title"), "subtitle", name="book_func_idx")
    
  34.         tablespace_index = models.Index(
    
  35.             fields=["title"],
    
  36.             db_tablespace="idx_tbls",
    
  37.             name="book_tablespace_idx",
    
  38.         )
    
  39.         self.assertEqual(repr(index), "<Index: fields=['title']>")
    
  40.         self.assertEqual(
    
  41.             repr(named_index),
    
  42.             "<Index: fields=['title'] name='title_idx'>",
    
  43.         )
    
  44.         self.assertEqual(repr(multi_col_index), "<Index: fields=['title', 'author']>")
    
  45.         self.assertEqual(
    
  46.             repr(partial_index),
    
  47.             "<Index: fields=['title'] name='long_books_idx' "
    
  48.             "condition=(AND: ('pages__gt', 400))>",
    
  49.         )
    
  50.         self.assertEqual(
    
  51.             repr(covering_index),
    
  52.             "<Index: fields=['title'] name='include_idx' "
    
  53.             "include=('author', 'pages')>",
    
  54.         )
    
  55.         self.assertEqual(
    
  56.             repr(opclasses_index),
    
  57.             "<Index: fields=['headline', 'body'] name='opclasses_idx' "
    
  58.             "opclasses=['varchar_pattern_ops', 'text_pattern_ops']>",
    
  59.         )
    
  60.         self.assertEqual(
    
  61.             repr(func_index),
    
  62.             "<Index: expressions=(Lower(F(title)), F(subtitle)) "
    
  63.             "name='book_func_idx'>",
    
  64.         )
    
  65.         self.assertEqual(
    
  66.             repr(tablespace_index),
    
  67.             "<Index: fields=['title'] name='book_tablespace_idx' "
    
  68.             "db_tablespace='idx_tbls'>",
    
  69.         )
    
  70. 
    
  71.     def test_eq(self):
    
  72.         index = models.Index(fields=["title"])
    
  73.         same_index = models.Index(fields=["title"])
    
  74.         another_index = models.Index(fields=["title", "author"])
    
  75.         index.model = Book
    
  76.         same_index.model = Book
    
  77.         another_index.model = Book
    
  78.         self.assertEqual(index, same_index)
    
  79.         self.assertEqual(index, mock.ANY)
    
  80.         self.assertNotEqual(index, another_index)
    
  81. 
    
  82.     def test_eq_func(self):
    
  83.         index = models.Index(Lower("title"), models.F("author"), name="book_func_idx")
    
  84.         same_index = models.Index(Lower("title"), "author", name="book_func_idx")
    
  85.         another_index = models.Index(Lower("title"), name="book_func_idx")
    
  86.         self.assertEqual(index, same_index)
    
  87.         self.assertEqual(index, mock.ANY)
    
  88.         self.assertNotEqual(index, another_index)
    
  89. 
    
  90.     def test_index_fields_type(self):
    
  91.         with self.assertRaisesMessage(
    
  92.             ValueError, "Index.fields must be a list or tuple."
    
  93.         ):
    
  94.             models.Index(fields="title")
    
  95. 
    
  96.     def test_index_fields_strings(self):
    
  97.         msg = "Index.fields must contain only strings with field names."
    
  98.         with self.assertRaisesMessage(ValueError, msg):
    
  99.             models.Index(fields=[models.F("title")])
    
  100. 
    
  101.     def test_fields_tuple(self):
    
  102.         self.assertEqual(models.Index(fields=("title",)).fields, ["title"])
    
  103. 
    
  104.     def test_requires_field_or_expression(self):
    
  105.         msg = "At least one field or expression is required to define an index."
    
  106.         with self.assertRaisesMessage(ValueError, msg):
    
  107.             models.Index()
    
  108. 
    
  109.     def test_expressions_and_fields_mutually_exclusive(self):
    
  110.         msg = "Index.fields and expressions are mutually exclusive."
    
  111.         with self.assertRaisesMessage(ValueError, msg):
    
  112.             models.Index(Upper("foo"), fields=["field"])
    
  113. 
    
  114.     def test_opclasses_requires_index_name(self):
    
  115.         with self.assertRaisesMessage(
    
  116.             ValueError, "An index must be named to use opclasses."
    
  117.         ):
    
  118.             models.Index(opclasses=["jsonb_path_ops"])
    
  119. 
    
  120.     def test_opclasses_requires_list_or_tuple(self):
    
  121.         with self.assertRaisesMessage(
    
  122.             ValueError, "Index.opclasses must be a list or tuple."
    
  123.         ):
    
  124.             models.Index(
    
  125.                 name="test_opclass", fields=["field"], opclasses="jsonb_path_ops"
    
  126.             )
    
  127. 
    
  128.     def test_opclasses_and_fields_same_length(self):
    
  129.         msg = "Index.fields and Index.opclasses must have the same number of elements."
    
  130.         with self.assertRaisesMessage(ValueError, msg):
    
  131.             models.Index(
    
  132.                 name="test_opclass",
    
  133.                 fields=["field", "other"],
    
  134.                 opclasses=["jsonb_path_ops"],
    
  135.             )
    
  136. 
    
  137.     def test_condition_requires_index_name(self):
    
  138.         with self.assertRaisesMessage(
    
  139.             ValueError, "An index must be named to use condition."
    
  140.         ):
    
  141.             models.Index(condition=models.Q(pages__gt=400))
    
  142. 
    
  143.     def test_expressions_requires_index_name(self):
    
  144.         msg = "An index must be named to use expressions."
    
  145.         with self.assertRaisesMessage(ValueError, msg):
    
  146.             models.Index(Lower("field"))
    
  147. 
    
  148.     def test_expressions_with_opclasses(self):
    
  149.         msg = (
    
  150.             "Index.opclasses cannot be used with expressions. Use "
    
  151.             "django.contrib.postgres.indexes.OpClass() instead."
    
  152.         )
    
  153.         with self.assertRaisesMessage(ValueError, msg):
    
  154.             models.Index(
    
  155.                 Lower("field"),
    
  156.                 name="test_func_opclass",
    
  157.                 opclasses=["jsonb_path_ops"],
    
  158.             )
    
  159. 
    
  160.     def test_condition_must_be_q(self):
    
  161.         with self.assertRaisesMessage(
    
  162.             ValueError, "Index.condition must be a Q instance."
    
  163.         ):
    
  164.             models.Index(condition="invalid", name="long_book_idx")
    
  165. 
    
  166.     def test_include_requires_list_or_tuple(self):
    
  167.         msg = "Index.include must be a list or tuple."
    
  168.         with self.assertRaisesMessage(ValueError, msg):
    
  169.             models.Index(name="test_include", fields=["field"], include="other")
    
  170. 
    
  171.     def test_include_requires_index_name(self):
    
  172.         msg = "A covering index must be named."
    
  173.         with self.assertRaisesMessage(ValueError, msg):
    
  174.             models.Index(fields=["field"], include=["other"])
    
  175. 
    
  176.     def test_name_auto_generation(self):
    
  177.         index = models.Index(fields=["author"])
    
  178.         index.set_name_with_model(Book)
    
  179.         self.assertEqual(index.name, "model_index_author_0f5565_idx")
    
  180. 
    
  181.         # '-' for DESC columns should be accounted for in the index name.
    
  182.         index = models.Index(fields=["-author"])
    
  183.         index.set_name_with_model(Book)
    
  184.         self.assertEqual(index.name, "model_index_author_708765_idx")
    
  185. 
    
  186.         # fields may be truncated in the name. db_column is used for naming.
    
  187.         long_field_index = models.Index(fields=["pages"])
    
  188.         long_field_index.set_name_with_model(Book)
    
  189.         self.assertEqual(long_field_index.name, "model_index_page_co_69235a_idx")
    
  190. 
    
  191.         # suffix can't be longer than 3 characters.
    
  192.         long_field_index.suffix = "suff"
    
  193.         msg = (
    
  194.             "Index too long for multiple database support. Is self.suffix "
    
  195.             "longer than 3 characters?"
    
  196.         )
    
  197.         with self.assertRaisesMessage(ValueError, msg):
    
  198.             long_field_index.set_name_with_model(Book)
    
  199. 
    
  200.     @isolate_apps("model_indexes")
    
  201.     def test_name_auto_generation_with_quoted_db_table(self):
    
  202.         class QuotedDbTable(models.Model):
    
  203.             name = models.CharField(max_length=50)
    
  204. 
    
  205.             class Meta:
    
  206.                 db_table = '"t_quoted"'
    
  207. 
    
  208.         index = models.Index(fields=["name"])
    
  209.         index.set_name_with_model(QuotedDbTable)
    
  210.         self.assertEqual(index.name, "t_quoted_name_e4ed1b_idx")
    
  211. 
    
  212.     def test_deconstruction(self):
    
  213.         index = models.Index(fields=["title"], db_tablespace="idx_tbls")
    
  214.         index.set_name_with_model(Book)
    
  215.         path, args, kwargs = index.deconstruct()
    
  216.         self.assertEqual(path, "django.db.models.Index")
    
  217.         self.assertEqual(args, ())
    
  218.         self.assertEqual(
    
  219.             kwargs,
    
  220.             {
    
  221.                 "fields": ["title"],
    
  222.                 "name": "model_index_title_196f42_idx",
    
  223.                 "db_tablespace": "idx_tbls",
    
  224.             },
    
  225.         )
    
  226. 
    
  227.     def test_deconstruct_with_condition(self):
    
  228.         index = models.Index(
    
  229.             name="big_book_index",
    
  230.             fields=["title"],
    
  231.             condition=models.Q(pages__gt=400),
    
  232.         )
    
  233.         index.set_name_with_model(Book)
    
  234.         path, args, kwargs = index.deconstruct()
    
  235.         self.assertEqual(path, "django.db.models.Index")
    
  236.         self.assertEqual(args, ())
    
  237.         self.assertEqual(
    
  238.             kwargs,
    
  239.             {
    
  240.                 "fields": ["title"],
    
  241.                 "name": "model_index_title_196f42_idx",
    
  242.                 "condition": models.Q(pages__gt=400),
    
  243.             },
    
  244.         )
    
  245. 
    
  246.     def test_deconstruct_with_include(self):
    
  247.         index = models.Index(
    
  248.             name="book_include_idx",
    
  249.             fields=["title"],
    
  250.             include=["author"],
    
  251.         )
    
  252.         index.set_name_with_model(Book)
    
  253.         path, args, kwargs = index.deconstruct()
    
  254.         self.assertEqual(path, "django.db.models.Index")
    
  255.         self.assertEqual(args, ())
    
  256.         self.assertEqual(
    
  257.             kwargs,
    
  258.             {
    
  259.                 "fields": ["title"],
    
  260.                 "name": "model_index_title_196f42_idx",
    
  261.                 "include": ("author",),
    
  262.             },
    
  263.         )
    
  264. 
    
  265.     def test_deconstruct_with_expressions(self):
    
  266.         index = models.Index(Upper("title"), name="book_func_idx")
    
  267.         path, args, kwargs = index.deconstruct()
    
  268.         self.assertEqual(path, "django.db.models.Index")
    
  269.         self.assertEqual(args, (Upper("title"),))
    
  270.         self.assertEqual(kwargs, {"name": "book_func_idx"})
    
  271. 
    
  272.     def test_clone(self):
    
  273.         index = models.Index(fields=["title"])
    
  274.         new_index = index.clone()
    
  275.         self.assertIsNot(index, new_index)
    
  276.         self.assertEqual(index.fields, new_index.fields)
    
  277. 
    
  278.     def test_clone_with_expressions(self):
    
  279.         index = models.Index(Upper("title"), name="book_func_idx")
    
  280.         new_index = index.clone()
    
  281.         self.assertIsNot(index, new_index)
    
  282.         self.assertEqual(index.expressions, new_index.expressions)
    
  283. 
    
  284.     def test_name_set(self):
    
  285.         index_names = [index.name for index in Book._meta.indexes]
    
  286.         self.assertCountEqual(
    
  287.             index_names,
    
  288.             [
    
  289.                 "model_index_title_196f42_idx",
    
  290.                 "model_index_isbn_34f975_idx",
    
  291.                 "model_indexes_book_barcode_idx",
    
  292.             ],
    
  293.         )
    
  294. 
    
  295.     def test_abstract_children(self):
    
  296.         index_names = [index.name for index in ChildModel1._meta.indexes]
    
  297.         self.assertEqual(
    
  298.             index_names,
    
  299.             ["model_index_name_440998_idx", "model_indexes_childmodel1_idx"],
    
  300.         )
    
  301.         index_names = [index.name for index in ChildModel2._meta.indexes]
    
  302.         self.assertEqual(
    
  303.             index_names,
    
  304.             ["model_index_name_b6c374_idx", "model_indexes_childmodel2_idx"],
    
  305.         )
    
  306. 
    
  307. 
    
  308. @override_settings(DEFAULT_TABLESPACE=None)
    
  309. class IndexesTests(TestCase):
    
  310.     @skipUnlessDBFeature("supports_tablespaces")
    
  311.     def test_db_tablespace(self):
    
  312.         editor = connection.schema_editor()
    
  313.         # Index with db_tablespace attribute.
    
  314.         for fields in [
    
  315.             # Field with db_tablespace specified on model.
    
  316.             ["shortcut"],
    
  317.             # Field without db_tablespace specified on model.
    
  318.             ["author"],
    
  319.             # Multi-column with db_tablespaces specified on model.
    
  320.             ["shortcut", "isbn"],
    
  321.             # Multi-column without db_tablespace specified on model.
    
  322.             ["title", "author"],
    
  323.         ]:
    
  324.             with self.subTest(fields=fields):
    
  325.                 index = models.Index(fields=fields, db_tablespace="idx_tbls2")
    
  326.                 self.assertIn(
    
  327.                     '"idx_tbls2"', str(index.create_sql(Book, editor)).lower()
    
  328.                 )
    
  329.         # Indexes without db_tablespace attribute.
    
  330.         for fields in [["author"], ["shortcut", "isbn"], ["title", "author"]]:
    
  331.             with self.subTest(fields=fields):
    
  332.                 index = models.Index(fields=fields)
    
  333.                 # The DEFAULT_INDEX_TABLESPACE setting can't be tested because
    
  334.                 # it's evaluated when the model class is defined. As a
    
  335.                 # consequence, @override_settings doesn't work.
    
  336.                 if settings.DEFAULT_INDEX_TABLESPACE:
    
  337.                     self.assertIn(
    
  338.                         '"%s"' % settings.DEFAULT_INDEX_TABLESPACE,
    
  339.                         str(index.create_sql(Book, editor)).lower(),
    
  340.                     )
    
  341.                 else:
    
  342.                     self.assertNotIn("TABLESPACE", str(index.create_sql(Book, editor)))
    
  343.         # Field with db_tablespace specified on the model and an index without
    
  344.         # db_tablespace.
    
  345.         index = models.Index(fields=["shortcut"])
    
  346.         self.assertIn('"idx_tbls"', str(index.create_sql(Book, editor)).lower())
    
  347. 
    
  348.     @skipUnlessDBFeature("supports_tablespaces")
    
  349.     def test_func_with_tablespace(self):
    
  350.         # Functional index with db_tablespace attribute.
    
  351.         index = models.Index(
    
  352.             Lower("shortcut").desc(),
    
  353.             name="functional_tbls",
    
  354.             db_tablespace="idx_tbls2",
    
  355.         )
    
  356.         with connection.schema_editor() as editor:
    
  357.             sql = str(index.create_sql(Book, editor))
    
  358.             self.assertIn(editor.quote_name("idx_tbls2"), sql)
    
  359.         # Functional index without db_tablespace attribute.
    
  360.         index = models.Index(Lower("shortcut").desc(), name="functional_no_tbls")
    
  361.         with connection.schema_editor() as editor:
    
  362.             sql = str(index.create_sql(Book, editor))
    
  363.             # The DEFAULT_INDEX_TABLESPACE setting can't be tested because it's
    
  364.             # evaluated when the model class is defined. As a consequence,
    
  365.             # @override_settings doesn't work.
    
  366.             if settings.DEFAULT_INDEX_TABLESPACE:
    
  367.                 self.assertIn(
    
  368.                     editor.quote_name(settings.DEFAULT_INDEX_TABLESPACE),
    
  369.                     sql,
    
  370.                 )
    
  371.             else:
    
  372.                 self.assertNotIn("TABLESPACE", sql)