1. import datetime
    
  2. from unittest import skipUnless
    
  3. 
    
  4. from django.conf import settings
    
  5. from django.db import connection
    
  6. from django.db.models import CASCADE, ForeignKey, Index, Q
    
  7. from django.db.models.functions import Lower
    
  8. from django.test import (
    
  9.     TestCase,
    
  10.     TransactionTestCase,
    
  11.     skipIfDBFeature,
    
  12.     skipUnlessDBFeature,
    
  13. )
    
  14. from django.test.utils import override_settings
    
  15. from django.utils import timezone
    
  16. 
    
  17. from .models import (
    
  18.     Article,
    
  19.     ArticleTranslation,
    
  20.     IndexedArticle2,
    
  21.     IndexTogetherSingleList,
    
  22. )
    
  23. 
    
  24. 
    
  25. class SchemaIndexesTests(TestCase):
    
  26.     """
    
  27.     Test index handling by the db.backends.schema infrastructure.
    
  28.     """
    
  29. 
    
  30.     def test_index_name_hash(self):
    
  31.         """
    
  32.         Index names should be deterministic.
    
  33.         """
    
  34.         editor = connection.schema_editor()
    
  35.         index_name = editor._create_index_name(
    
  36.             table_name=Article._meta.db_table,
    
  37.             column_names=("c1",),
    
  38.             suffix="123",
    
  39.         )
    
  40.         self.assertEqual(index_name, "indexes_article_c1_a52bd80b123")
    
  41. 
    
  42.     def test_index_name(self):
    
  43.         """
    
  44.         Index names on the built-in database backends::
    
  45.             * Are truncated as needed.
    
  46.             * Include all the column names.
    
  47.             * Include a deterministic hash.
    
  48.         """
    
  49.         long_name = "l%sng" % ("o" * 100)
    
  50.         editor = connection.schema_editor()
    
  51.         index_name = editor._create_index_name(
    
  52.             table_name=Article._meta.db_table,
    
  53.             column_names=("c1", "c2", long_name),
    
  54.             suffix="ix",
    
  55.         )
    
  56.         expected = {
    
  57.             "mysql": "indexes_article_c1_c2_looooooooooooooooooo_255179b2ix",
    
  58.             "oracle": "indexes_a_c1_c2_loo_255179b2ix",
    
  59.             "postgresql": "indexes_article_c1_c2_loooooooooooooooooo_255179b2ix",
    
  60.             "sqlite": "indexes_article_c1_c2_l%sng_255179b2ix" % ("o" * 100),
    
  61.         }
    
  62.         if connection.vendor not in expected:
    
  63.             self.skipTest(
    
  64.                 "This test is only supported on the built-in database backends."
    
  65.             )
    
  66.         self.assertEqual(index_name, expected[connection.vendor])
    
  67. 
    
  68.     def test_index_together(self):
    
  69.         editor = connection.schema_editor()
    
  70.         index_sql = [str(statement) for statement in editor._model_indexes_sql(Article)]
    
  71.         self.assertEqual(len(index_sql), 1)
    
  72.         # Ensure the index name is properly quoted
    
  73.         self.assertIn(
    
  74.             connection.ops.quote_name(
    
  75.                 editor._create_index_name(
    
  76.                     Article._meta.db_table, ["headline", "pub_date"], suffix="_idx"
    
  77.                 )
    
  78.             ),
    
  79.             index_sql[0],
    
  80.         )
    
  81. 
    
  82.     def test_index_together_single_list(self):
    
  83.         # Test for using index_together with a single list (#22172)
    
  84.         index_sql = connection.schema_editor()._model_indexes_sql(
    
  85.             IndexTogetherSingleList
    
  86.         )
    
  87.         self.assertEqual(len(index_sql), 1)
    
  88. 
    
  89.     def test_columns_list_sql(self):
    
  90.         index = Index(fields=["headline"], name="whitespace_idx")
    
  91.         editor = connection.schema_editor()
    
  92.         self.assertIn(
    
  93.             "(%s)" % editor.quote_name("headline"),
    
  94.             str(index.create_sql(Article, editor)),
    
  95.         )
    
  96. 
    
  97.     @skipUnlessDBFeature("supports_index_column_ordering")
    
  98.     def test_descending_columns_list_sql(self):
    
  99.         index = Index(fields=["-headline"], name="whitespace_idx")
    
  100.         editor = connection.schema_editor()
    
  101.         self.assertIn(
    
  102.             "(%s DESC)" % editor.quote_name("headline"),
    
  103.             str(index.create_sql(Article, editor)),
    
  104.         )
    
  105. 
    
  106. 
    
  107. class SchemaIndexesNotPostgreSQLTests(TransactionTestCase):
    
  108.     available_apps = ["indexes"]
    
  109. 
    
  110.     def test_create_index_ignores_opclasses(self):
    
  111.         index = Index(
    
  112.             name="test_ops_class",
    
  113.             fields=["headline"],
    
  114.             opclasses=["varchar_pattern_ops"],
    
  115.         )
    
  116.         with connection.schema_editor() as editor:
    
  117.             # This would error if opclasses weren't ignored.
    
  118.             editor.add_index(IndexedArticle2, index)
    
  119. 
    
  120. 
    
  121. # The `condition` parameter is ignored by databases that don't support partial
    
  122. # indexes.
    
  123. @skipIfDBFeature("supports_partial_indexes")
    
  124. class PartialIndexConditionIgnoredTests(TransactionTestCase):
    
  125.     available_apps = ["indexes"]
    
  126. 
    
  127.     def test_condition_ignored(self):
    
  128.         index = Index(
    
  129.             name="test_condition_ignored",
    
  130.             fields=["published"],
    
  131.             condition=Q(published=True),
    
  132.         )
    
  133.         with connection.schema_editor() as editor:
    
  134.             # This would error if condition weren't ignored.
    
  135.             editor.add_index(Article, index)
    
  136. 
    
  137.         self.assertNotIn(
    
  138.             "WHERE %s" % editor.quote_name("published"),
    
  139.             str(index.create_sql(Article, editor)),
    
  140.         )
    
  141. 
    
  142. 
    
  143. @skipUnless(connection.vendor == "postgresql", "PostgreSQL tests")
    
  144. class SchemaIndexesPostgreSQLTests(TransactionTestCase):
    
  145.     available_apps = ["indexes"]
    
  146.     get_opclass_query = """
    
  147.         SELECT opcname, c.relname FROM pg_opclass AS oc
    
  148.         JOIN pg_index as i on oc.oid = ANY(i.indclass)
    
  149.         JOIN pg_class as c on c.oid = i.indexrelid
    
  150.         WHERE c.relname = '%s'
    
  151.     """
    
  152. 
    
  153.     def test_text_indexes(self):
    
  154.         """Test creation of PostgreSQL-specific text indexes (#12234)"""
    
  155.         from .models import IndexedArticle
    
  156. 
    
  157.         index_sql = [
    
  158.             str(statement)
    
  159.             for statement in connection.schema_editor()._model_indexes_sql(
    
  160.                 IndexedArticle
    
  161.             )
    
  162.         ]
    
  163.         self.assertEqual(len(index_sql), 5)
    
  164.         self.assertIn('("headline" varchar_pattern_ops)', index_sql[1])
    
  165.         self.assertIn('("body" text_pattern_ops)', index_sql[3])
    
  166.         # unique=True and db_index=True should only create the varchar-specific
    
  167.         # index (#19441).
    
  168.         self.assertIn('("slug" varchar_pattern_ops)', index_sql[4])
    
  169. 
    
  170.     def test_virtual_relation_indexes(self):
    
  171.         """Test indexes are not created for related objects"""
    
  172.         index_sql = connection.schema_editor()._model_indexes_sql(Article)
    
  173.         self.assertEqual(len(index_sql), 1)
    
  174. 
    
  175.     def test_ops_class(self):
    
  176.         index = Index(
    
  177.             name="test_ops_class",
    
  178.             fields=["headline"],
    
  179.             opclasses=["varchar_pattern_ops"],
    
  180.         )
    
  181.         with connection.schema_editor() as editor:
    
  182.             editor.add_index(IndexedArticle2, index)
    
  183.         with editor.connection.cursor() as cursor:
    
  184.             cursor.execute(self.get_opclass_query % "test_ops_class")
    
  185.             self.assertEqual(
    
  186.                 cursor.fetchall(), [("varchar_pattern_ops", "test_ops_class")]
    
  187.             )
    
  188. 
    
  189.     def test_ops_class_multiple_columns(self):
    
  190.         index = Index(
    
  191.             name="test_ops_class_multiple",
    
  192.             fields=["headline", "body"],
    
  193.             opclasses=["varchar_pattern_ops", "text_pattern_ops"],
    
  194.         )
    
  195.         with connection.schema_editor() as editor:
    
  196.             editor.add_index(IndexedArticle2, index)
    
  197.         with editor.connection.cursor() as cursor:
    
  198.             cursor.execute(self.get_opclass_query % "test_ops_class_multiple")
    
  199.             expected_ops_classes = (
    
  200.                 ("varchar_pattern_ops", "test_ops_class_multiple"),
    
  201.                 ("text_pattern_ops", "test_ops_class_multiple"),
    
  202.             )
    
  203.             self.assertCountEqual(cursor.fetchall(), expected_ops_classes)
    
  204. 
    
  205.     def test_ops_class_partial(self):
    
  206.         index = Index(
    
  207.             name="test_ops_class_partial",
    
  208.             fields=["body"],
    
  209.             opclasses=["text_pattern_ops"],
    
  210.             condition=Q(headline__contains="China"),
    
  211.         )
    
  212.         with connection.schema_editor() as editor:
    
  213.             editor.add_index(IndexedArticle2, index)
    
  214.         with editor.connection.cursor() as cursor:
    
  215.             cursor.execute(self.get_opclass_query % "test_ops_class_partial")
    
  216.             self.assertCountEqual(
    
  217.                 cursor.fetchall(), [("text_pattern_ops", "test_ops_class_partial")]
    
  218.             )
    
  219. 
    
  220.     def test_ops_class_partial_tablespace(self):
    
  221.         indexname = "test_ops_class_tblspace"
    
  222.         index = Index(
    
  223.             name=indexname,
    
  224.             fields=["body"],
    
  225.             opclasses=["text_pattern_ops"],
    
  226.             condition=Q(headline__contains="China"),
    
  227.             db_tablespace="pg_default",
    
  228.         )
    
  229.         with connection.schema_editor() as editor:
    
  230.             editor.add_index(IndexedArticle2, index)
    
  231.             self.assertIn(
    
  232.                 'TABLESPACE "pg_default" ',
    
  233.                 str(index.create_sql(IndexedArticle2, editor)),
    
  234.             )
    
  235.         with editor.connection.cursor() as cursor:
    
  236.             cursor.execute(self.get_opclass_query % indexname)
    
  237.             self.assertCountEqual(cursor.fetchall(), [("text_pattern_ops", indexname)])
    
  238. 
    
  239.     def test_ops_class_descending(self):
    
  240.         indexname = "test_ops_class_ordered"
    
  241.         index = Index(
    
  242.             name=indexname,
    
  243.             fields=["-body"],
    
  244.             opclasses=["text_pattern_ops"],
    
  245.         )
    
  246.         with connection.schema_editor() as editor:
    
  247.             editor.add_index(IndexedArticle2, index)
    
  248.         with editor.connection.cursor() as cursor:
    
  249.             cursor.execute(self.get_opclass_query % indexname)
    
  250.             self.assertCountEqual(cursor.fetchall(), [("text_pattern_ops", indexname)])
    
  251. 
    
  252.     def test_ops_class_descending_partial(self):
    
  253.         indexname = "test_ops_class_ordered_partial"
    
  254.         index = Index(
    
  255.             name=indexname,
    
  256.             fields=["-body"],
    
  257.             opclasses=["text_pattern_ops"],
    
  258.             condition=Q(headline__contains="China"),
    
  259.         )
    
  260.         with connection.schema_editor() as editor:
    
  261.             editor.add_index(IndexedArticle2, index)
    
  262.         with editor.connection.cursor() as cursor:
    
  263.             cursor.execute(self.get_opclass_query % indexname)
    
  264.             self.assertCountEqual(cursor.fetchall(), [("text_pattern_ops", indexname)])
    
  265. 
    
  266.     @skipUnlessDBFeature("supports_covering_indexes")
    
  267.     def test_ops_class_include(self):
    
  268.         index_name = "test_ops_class_include"
    
  269.         index = Index(
    
  270.             name=index_name,
    
  271.             fields=["body"],
    
  272.             opclasses=["text_pattern_ops"],
    
  273.             include=["headline"],
    
  274.         )
    
  275.         with connection.schema_editor() as editor:
    
  276.             editor.add_index(IndexedArticle2, index)
    
  277.         with editor.connection.cursor() as cursor:
    
  278.             cursor.execute(self.get_opclass_query % index_name)
    
  279.             self.assertCountEqual(cursor.fetchall(), [("text_pattern_ops", index_name)])
    
  280. 
    
  281.     @skipUnlessDBFeature("supports_covering_indexes")
    
  282.     def test_ops_class_include_tablespace(self):
    
  283.         index_name = "test_ops_class_include_tblspace"
    
  284.         index = Index(
    
  285.             name=index_name,
    
  286.             fields=["body"],
    
  287.             opclasses=["text_pattern_ops"],
    
  288.             include=["headline"],
    
  289.             db_tablespace="pg_default",
    
  290.         )
    
  291.         with connection.schema_editor() as editor:
    
  292.             editor.add_index(IndexedArticle2, index)
    
  293.             self.assertIn(
    
  294.                 'TABLESPACE "pg_default"',
    
  295.                 str(index.create_sql(IndexedArticle2, editor)),
    
  296.             )
    
  297.         with editor.connection.cursor() as cursor:
    
  298.             cursor.execute(self.get_opclass_query % index_name)
    
  299.             self.assertCountEqual(cursor.fetchall(), [("text_pattern_ops", index_name)])
    
  300. 
    
  301.     def test_ops_class_columns_lists_sql(self):
    
  302.         index = Index(
    
  303.             fields=["headline"],
    
  304.             name="whitespace_idx",
    
  305.             opclasses=["text_pattern_ops"],
    
  306.         )
    
  307.         with connection.schema_editor() as editor:
    
  308.             self.assertIn(
    
  309.                 "(%s text_pattern_ops)" % editor.quote_name("headline"),
    
  310.                 str(index.create_sql(Article, editor)),
    
  311.             )
    
  312. 
    
  313.     def test_ops_class_descending_columns_list_sql(self):
    
  314.         index = Index(
    
  315.             fields=["-headline"],
    
  316.             name="whitespace_idx",
    
  317.             opclasses=["text_pattern_ops"],
    
  318.         )
    
  319.         with connection.schema_editor() as editor:
    
  320.             self.assertIn(
    
  321.                 "(%s text_pattern_ops DESC)" % editor.quote_name("headline"),
    
  322.                 str(index.create_sql(Article, editor)),
    
  323.             )
    
  324. 
    
  325. 
    
  326. @skipUnless(connection.vendor == "mysql", "MySQL tests")
    
  327. class SchemaIndexesMySQLTests(TransactionTestCase):
    
  328.     available_apps = ["indexes"]
    
  329. 
    
  330.     def test_no_index_for_foreignkey(self):
    
  331.         """
    
  332.         MySQL on InnoDB already creates indexes automatically for foreign keys.
    
  333.         (#14180). An index should be created if db_constraint=False (#26171).
    
  334.         """
    
  335.         with connection.cursor() as cursor:
    
  336.             storage = connection.introspection.get_storage_engine(
    
  337.                 cursor,
    
  338.                 ArticleTranslation._meta.db_table,
    
  339.             )
    
  340.         if storage != "InnoDB":
    
  341.             self.skipTest("This test only applies to the InnoDB storage engine")
    
  342.         index_sql = [
    
  343.             str(statement)
    
  344.             for statement in connection.schema_editor()._model_indexes_sql(
    
  345.                 ArticleTranslation
    
  346.             )
    
  347.         ]
    
  348.         self.assertEqual(
    
  349.             index_sql,
    
  350.             [
    
  351.                 "CREATE INDEX "
    
  352.                 "`indexes_articletranslation_article_no_constraint_id_d6c0806b` "
    
  353.                 "ON `indexes_articletranslation` (`article_no_constraint_id`)"
    
  354.             ],
    
  355.         )
    
  356. 
    
  357.         # The index also shouldn't be created if the ForeignKey is added after
    
  358.         # the model was created.
    
  359.         field_created = False
    
  360.         try:
    
  361.             with connection.schema_editor() as editor:
    
  362.                 new_field = ForeignKey(Article, CASCADE)
    
  363.                 new_field.set_attributes_from_name("new_foreign_key")
    
  364.                 editor.add_field(ArticleTranslation, new_field)
    
  365.                 field_created = True
    
  366.                 # No deferred SQL. The FK constraint is included in the
    
  367.                 # statement to add the field.
    
  368.                 self.assertFalse(editor.deferred_sql)
    
  369.         finally:
    
  370.             if field_created:
    
  371.                 with connection.schema_editor() as editor:
    
  372.                     editor.remove_field(ArticleTranslation, new_field)
    
  373. 
    
  374. 
    
  375. @skipUnlessDBFeature("supports_partial_indexes")
    
  376. # SQLite doesn't support timezone-aware datetimes when USE_TZ is False.
    
  377. @override_settings(USE_TZ=True)
    
  378. class PartialIndexTests(TransactionTestCase):
    
  379.     # Schema editor is used to create the index to test that it works.
    
  380.     available_apps = ["indexes"]
    
  381. 
    
  382.     def test_partial_index(self):
    
  383.         with connection.schema_editor() as editor:
    
  384.             index = Index(
    
  385.                 name="recent_article_idx",
    
  386.                 fields=["pub_date"],
    
  387.                 condition=Q(
    
  388.                     pub_date__gt=datetime.datetime(
    
  389.                         year=2015,
    
  390.                         month=1,
    
  391.                         day=1,
    
  392.                         # PostgreSQL would otherwise complain about the lookup
    
  393.                         # being converted to a mutable function (by removing
    
  394.                         # the timezone in the cast) which is forbidden.
    
  395.                         tzinfo=timezone.get_current_timezone(),
    
  396.                     ),
    
  397.                 ),
    
  398.             )
    
  399.             self.assertIn(
    
  400.                 "WHERE %s" % editor.quote_name("pub_date"),
    
  401.                 str(index.create_sql(Article, schema_editor=editor)),
    
  402.             )
    
  403.             editor.add_index(index=index, model=Article)
    
  404.             with connection.cursor() as cursor:
    
  405.                 self.assertIn(
    
  406.                     index.name,
    
  407.                     connection.introspection.get_constraints(
    
  408.                         cursor=cursor,
    
  409.                         table_name=Article._meta.db_table,
    
  410.                     ),
    
  411.                 )
    
  412.             editor.remove_index(index=index, model=Article)
    
  413. 
    
  414.     def test_integer_restriction_partial(self):
    
  415.         with connection.schema_editor() as editor:
    
  416.             index = Index(
    
  417.                 name="recent_article_idx",
    
  418.                 fields=["id"],
    
  419.                 condition=Q(pk__gt=1),
    
  420.             )
    
  421.             self.assertIn(
    
  422.                 "WHERE %s" % editor.quote_name("id"),
    
  423.                 str(index.create_sql(Article, schema_editor=editor)),
    
  424.             )
    
  425.             editor.add_index(index=index, model=Article)
    
  426.             with connection.cursor() as cursor:
    
  427.                 self.assertIn(
    
  428.                     index.name,
    
  429.                     connection.introspection.get_constraints(
    
  430.                         cursor=cursor,
    
  431.                         table_name=Article._meta.db_table,
    
  432.                     ),
    
  433.                 )
    
  434.             editor.remove_index(index=index, model=Article)
    
  435. 
    
  436.     def test_boolean_restriction_partial(self):
    
  437.         with connection.schema_editor() as editor:
    
  438.             index = Index(
    
  439.                 name="published_index",
    
  440.                 fields=["published"],
    
  441.                 condition=Q(published=True),
    
  442.             )
    
  443.             self.assertIn(
    
  444.                 "WHERE %s" % editor.quote_name("published"),
    
  445.                 str(index.create_sql(Article, schema_editor=editor)),
    
  446.             )
    
  447.             editor.add_index(index=index, model=Article)
    
  448.             with connection.cursor() as cursor:
    
  449.                 self.assertIn(
    
  450.                     index.name,
    
  451.                     connection.introspection.get_constraints(
    
  452.                         cursor=cursor,
    
  453.                         table_name=Article._meta.db_table,
    
  454.                     ),
    
  455.                 )
    
  456.             editor.remove_index(index=index, model=Article)
    
  457. 
    
  458.     @skipUnlessDBFeature("supports_functions_in_partial_indexes")
    
  459.     def test_multiple_conditions(self):
    
  460.         with connection.schema_editor() as editor:
    
  461.             index = Index(
    
  462.                 name="recent_article_idx",
    
  463.                 fields=["pub_date", "headline"],
    
  464.                 condition=(
    
  465.                     Q(
    
  466.                         pub_date__gt=datetime.datetime(
    
  467.                             year=2015,
    
  468.                             month=1,
    
  469.                             day=1,
    
  470.                             tzinfo=timezone.get_current_timezone(),
    
  471.                         )
    
  472.                     )
    
  473.                     & Q(headline__contains="China")
    
  474.                 ),
    
  475.             )
    
  476.             sql = str(index.create_sql(Article, schema_editor=editor))
    
  477.             where = sql.find("WHERE")
    
  478.             self.assertIn("WHERE (%s" % editor.quote_name("pub_date"), sql)
    
  479.             # Because each backend has different syntax for the operators,
    
  480.             # check ONLY the occurrence of headline in the SQL.
    
  481.             self.assertGreater(sql.rfind("headline"), where)
    
  482.             editor.add_index(index=index, model=Article)
    
  483.             with connection.cursor() as cursor:
    
  484.                 self.assertIn(
    
  485.                     index.name,
    
  486.                     connection.introspection.get_constraints(
    
  487.                         cursor=cursor,
    
  488.                         table_name=Article._meta.db_table,
    
  489.                     ),
    
  490.                 )
    
  491.             editor.remove_index(index=index, model=Article)
    
  492. 
    
  493.     def test_is_null_condition(self):
    
  494.         with connection.schema_editor() as editor:
    
  495.             index = Index(
    
  496.                 name="recent_article_idx",
    
  497.                 fields=["pub_date"],
    
  498.                 condition=Q(pub_date__isnull=False),
    
  499.             )
    
  500.             self.assertIn(
    
  501.                 "WHERE %s IS NOT NULL" % editor.quote_name("pub_date"),
    
  502.                 str(index.create_sql(Article, schema_editor=editor)),
    
  503.             )
    
  504.             editor.add_index(index=index, model=Article)
    
  505.             with connection.cursor() as cursor:
    
  506.                 self.assertIn(
    
  507.                     index.name,
    
  508.                     connection.introspection.get_constraints(
    
  509.                         cursor=cursor,
    
  510.                         table_name=Article._meta.db_table,
    
  511.                     ),
    
  512.                 )
    
  513.             editor.remove_index(index=index, model=Article)
    
  514. 
    
  515.     @skipUnlessDBFeature("supports_expression_indexes")
    
  516.     def test_partial_func_index(self):
    
  517.         index_name = "partial_func_idx"
    
  518.         index = Index(
    
  519.             Lower("headline").desc(),
    
  520.             name=index_name,
    
  521.             condition=Q(pub_date__isnull=False),
    
  522.         )
    
  523.         with connection.schema_editor() as editor:
    
  524.             editor.add_index(index=index, model=Article)
    
  525.             sql = index.create_sql(Article, schema_editor=editor)
    
  526.         table = Article._meta.db_table
    
  527.         self.assertIs(sql.references_column(table, "headline"), True)
    
  528.         sql = str(sql)
    
  529.         self.assertIn("LOWER(%s)" % editor.quote_name("headline"), sql)
    
  530.         self.assertIn(
    
  531.             "WHERE %s IS NOT NULL" % editor.quote_name("pub_date"),
    
  532.             sql,
    
  533.         )
    
  534.         self.assertGreater(sql.find("WHERE"), sql.find("LOWER"))
    
  535.         with connection.cursor() as cursor:
    
  536.             constraints = connection.introspection.get_constraints(
    
  537.                 cursor=cursor,
    
  538.                 table_name=table,
    
  539.             )
    
  540.         self.assertIn(index_name, constraints)
    
  541.         if connection.features.supports_index_column_ordering:
    
  542.             self.assertEqual(constraints[index_name]["orders"], ["DESC"])
    
  543.         with connection.schema_editor() as editor:
    
  544.             editor.remove_index(Article, index)
    
  545.         with connection.cursor() as cursor:
    
  546.             self.assertNotIn(
    
  547.                 index_name,
    
  548.                 connection.introspection.get_constraints(
    
  549.                     cursor=cursor,
    
  550.                     table_name=table,
    
  551.                 ),
    
  552.             )
    
  553. 
    
  554. 
    
  555. @skipUnlessDBFeature("supports_covering_indexes")
    
  556. class CoveringIndexTests(TransactionTestCase):
    
  557.     available_apps = ["indexes"]
    
  558. 
    
  559.     def test_covering_index(self):
    
  560.         index = Index(
    
  561.             name="covering_headline_idx",
    
  562.             fields=["headline"],
    
  563.             include=["pub_date", "published"],
    
  564.         )
    
  565.         with connection.schema_editor() as editor:
    
  566.             self.assertIn(
    
  567.                 "(%s) INCLUDE (%s, %s)"
    
  568.                 % (
    
  569.                     editor.quote_name("headline"),
    
  570.                     editor.quote_name("pub_date"),
    
  571.                     editor.quote_name("published"),
    
  572.                 ),
    
  573.                 str(index.create_sql(Article, editor)),
    
  574.             )
    
  575.             editor.add_index(Article, index)
    
  576.             with connection.cursor() as cursor:
    
  577.                 constraints = connection.introspection.get_constraints(
    
  578.                     cursor=cursor,
    
  579.                     table_name=Article._meta.db_table,
    
  580.                 )
    
  581.                 self.assertIn(index.name, constraints)
    
  582.                 self.assertEqual(
    
  583.                     constraints[index.name]["columns"],
    
  584.                     ["headline", "pub_date", "published"],
    
  585.                 )
    
  586.             editor.remove_index(Article, index)
    
  587.             with connection.cursor() as cursor:
    
  588.                 self.assertNotIn(
    
  589.                     index.name,
    
  590.                     connection.introspection.get_constraints(
    
  591.                         cursor=cursor,
    
  592.                         table_name=Article._meta.db_table,
    
  593.                     ),
    
  594.                 )
    
  595. 
    
  596.     def test_covering_partial_index(self):
    
  597.         index = Index(
    
  598.             name="covering_partial_headline_idx",
    
  599.             fields=["headline"],
    
  600.             include=["pub_date"],
    
  601.             condition=Q(pub_date__isnull=False),
    
  602.         )
    
  603.         with connection.schema_editor() as editor:
    
  604.             extra_sql = ""
    
  605.             if settings.DEFAULT_INDEX_TABLESPACE:
    
  606.                 extra_sql = "TABLESPACE %s " % editor.quote_name(
    
  607.                     settings.DEFAULT_INDEX_TABLESPACE
    
  608.                 )
    
  609.             self.assertIn(
    
  610.                 "(%s) INCLUDE (%s) %sWHERE %s "
    
  611.                 % (
    
  612.                     editor.quote_name("headline"),
    
  613.                     editor.quote_name("pub_date"),
    
  614.                     extra_sql,
    
  615.                     editor.quote_name("pub_date"),
    
  616.                 ),
    
  617.                 str(index.create_sql(Article, editor)),
    
  618.             )
    
  619.             editor.add_index(Article, index)
    
  620.             with connection.cursor() as cursor:
    
  621.                 constraints = connection.introspection.get_constraints(
    
  622.                     cursor=cursor,
    
  623.                     table_name=Article._meta.db_table,
    
  624.                 )
    
  625.                 self.assertIn(index.name, constraints)
    
  626.                 self.assertEqual(
    
  627.                     constraints[index.name]["columns"],
    
  628.                     ["headline", "pub_date"],
    
  629.                 )
    
  630.             editor.remove_index(Article, index)
    
  631.             with connection.cursor() as cursor:
    
  632.                 self.assertNotIn(
    
  633.                     index.name,
    
  634.                     connection.introspection.get_constraints(
    
  635.                         cursor=cursor,
    
  636.                         table_name=Article._meta.db_table,
    
  637.                     ),
    
  638.                 )
    
  639. 
    
  640.     @skipUnlessDBFeature("supports_expression_indexes")
    
  641.     def test_covering_func_index(self):
    
  642.         index_name = "covering_func_headline_idx"
    
  643.         index = Index(Lower("headline"), name=index_name, include=["pub_date"])
    
  644.         with connection.schema_editor() as editor:
    
  645.             editor.add_index(index=index, model=Article)
    
  646.             sql = index.create_sql(Article, schema_editor=editor)
    
  647.         table = Article._meta.db_table
    
  648.         self.assertIs(sql.references_column(table, "headline"), True)
    
  649.         sql = str(sql)
    
  650.         self.assertIn("LOWER(%s)" % editor.quote_name("headline"), sql)
    
  651.         self.assertIn("INCLUDE (%s)" % editor.quote_name("pub_date"), sql)
    
  652.         self.assertGreater(sql.find("INCLUDE"), sql.find("LOWER"))
    
  653.         with connection.cursor() as cursor:
    
  654.             constraints = connection.introspection.get_constraints(
    
  655.                 cursor=cursor,
    
  656.                 table_name=table,
    
  657.             )
    
  658.         self.assertIn(index_name, constraints)
    
  659.         self.assertIn("pub_date", constraints[index_name]["columns"])
    
  660.         with connection.schema_editor() as editor:
    
  661.             editor.remove_index(Article, index)
    
  662.         with connection.cursor() as cursor:
    
  663.             self.assertNotIn(
    
  664.                 index_name,
    
  665.                 connection.introspection.get_constraints(
    
  666.                     cursor=cursor,
    
  667.                     table_name=table,
    
  668.                 ),
    
  669.             )
    
  670. 
    
  671. 
    
  672. @skipIfDBFeature("supports_covering_indexes")
    
  673. class CoveringIndexIgnoredTests(TransactionTestCase):
    
  674.     available_apps = ["indexes"]
    
  675. 
    
  676.     def test_covering_ignored(self):
    
  677.         index = Index(
    
  678.             name="test_covering_ignored",
    
  679.             fields=["headline"],
    
  680.             include=["pub_date"],
    
  681.         )
    
  682.         with connection.schema_editor() as editor:
    
  683.             editor.add_index(Article, index)
    
  684.         self.assertNotIn(
    
  685.             "INCLUDE (%s)" % editor.quote_name("headline"),
    
  686.             str(index.create_sql(Article, editor)),
    
  687.         )