summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorHannes Ljungberg <hannes@5monkeys.se>2019-10-31 13:33:53 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-06-04 12:26:22 +0200
commit8c7992f658e1125f8dc20b19206e8bbbe4187372 (patch)
treeba09524a9a3d27bb33f22dc92dec4b60a80f37e1 /tests
parentf997b5e6ae85e2df2342b1a7812fe8130206c957 (diff)
Fixed #30913 -- Added support for covering indexes on PostgreSQL 11+.
Diffstat (limited to 'tests')
-rw-r--r--tests/constraints/models.py17
-rw-r--r--tests/constraints/tests.py61
-rw-r--r--tests/indexes/tests.py121
-rw-r--r--tests/invalid_models_tests/test_models.py159
-rw-r--r--tests/migrations/test_operations.py124
-rw-r--r--tests/model_indexes/tests.py38
-rw-r--r--tests/postgres_tests/test_indexes.py29
-rw-r--r--tests/schema/tests.py1
8 files changed, 548 insertions, 2 deletions
diff --git a/tests/constraints/models.py b/tests/constraints/models.py
index 3d091f6ccf..947d1b4ff1 100644
--- a/tests/constraints/models.py
+++ b/tests/constraints/models.py
@@ -81,6 +81,23 @@ class UniqueConstraintDeferrable(models.Model):
]
+class UniqueConstraintInclude(models.Model):
+ name = models.CharField(max_length=255)
+ color = models.CharField(max_length=32, null=True)
+
+ class Meta:
+ required_db_features = {
+ 'supports_table_check_constraints',
+ }
+ constraints = [
+ models.UniqueConstraint(
+ fields=['name'],
+ name='name_include_color_uniq',
+ include=['color'],
+ ),
+ ]
+
+
class AbstractModel(models.Model):
age = models.IntegerField()
diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py
index 8eb62a940d..02320e30b1 100644
--- a/tests/constraints/tests.py
+++ b/tests/constraints/tests.py
@@ -8,7 +8,8 @@ from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
from .models import (
ChildModel, Product, UniqueConstraintConditionProduct,
- UniqueConstraintDeferrable, UniqueConstraintProduct,
+ UniqueConstraintDeferrable, UniqueConstraintInclude,
+ UniqueConstraintProduct,
)
@@ -181,6 +182,20 @@ class UniqueConstraintTests(TestCase):
self.assertEqual(constraint_1, constraint_1)
self.assertNotEqual(constraint_1, constraint_2)
+ def test_eq_with_include(self):
+ constraint_1 = models.UniqueConstraint(
+ fields=['foo', 'bar'],
+ name='include',
+ include=['baz_1'],
+ )
+ constraint_2 = models.UniqueConstraint(
+ fields=['foo', 'bar'],
+ name='include',
+ include=['baz_2'],
+ )
+ self.assertEqual(constraint_1, constraint_1)
+ self.assertNotEqual(constraint_1, constraint_2)
+
def test_repr(self):
fields = ['foo', 'bar']
name = 'unique_fields'
@@ -214,6 +229,18 @@ class UniqueConstraintTests(TestCase):
"deferrable=Deferrable.IMMEDIATE>",
)
+ def test_repr_with_include(self):
+ constraint = models.UniqueConstraint(
+ fields=['foo', 'bar'],
+ name='include_fields',
+ include=['baz_1', 'baz_2'],
+ )
+ self.assertEqual(
+ repr(constraint),
+ "<UniqueConstraint: fields=('foo', 'bar') name='include_fields' "
+ "include=('baz_1', 'baz_2')>",
+ )
+
def test_deconstruction(self):
fields = ['foo', 'bar']
name = 'unique_fields'
@@ -250,6 +277,20 @@ class UniqueConstraintTests(TestCase):
'deferrable': models.Deferrable.DEFERRED,
})
+ def test_deconstruction_with_include(self):
+ fields = ['foo', 'bar']
+ name = 'unique_fields'
+ include = ['baz_1', 'baz_2']
+ constraint = models.UniqueConstraint(fields=fields, name=name, include=include)
+ path, args, kwargs = constraint.deconstruct()
+ self.assertEqual(path, 'django.db.models.UniqueConstraint')
+ self.assertEqual(args, ())
+ self.assertEqual(kwargs, {
+ 'fields': tuple(fields),
+ 'name': name,
+ 'include': tuple(include),
+ })
+
def test_database_constraint(self):
with self.assertRaises(IntegrityError):
UniqueConstraintProduct.objects.create(name=self.p1.name, color=self.p1.color)
@@ -333,3 +374,21 @@ class UniqueConstraintTests(TestCase):
name='name_invalid',
deferrable='invalid',
)
+
+ @skipUnlessDBFeature(
+ 'supports_table_check_constraints',
+ 'supports_covering_indexes',
+ )
+ def test_include_database_constraint(self):
+ UniqueConstraintInclude.objects.create(name='p1', color='red')
+ with self.assertRaises(IntegrityError):
+ UniqueConstraintInclude.objects.create(name='p1', color='blue')
+
+ def test_invalid_include_argument(self):
+ msg = 'UniqueConstraint.include must be a list or tuple.'
+ with self.assertRaisesMessage(ValueError, msg):
+ models.UniqueConstraint(
+ name='uniq_include',
+ fields=['field'],
+ include='other',
+ )
diff --git a/tests/indexes/tests.py b/tests/indexes/tests.py
index 274ee54a37..3c4541f684 100644
--- a/tests/indexes/tests.py
+++ b/tests/indexes/tests.py
@@ -236,6 +236,41 @@ class SchemaIndexesPostgreSQLTests(TransactionTestCase):
cursor.execute(self.get_opclass_query % indexname)
self.assertCountEqual(cursor.fetchall(), [('text_pattern_ops', indexname)])
+ @skipUnlessDBFeature('supports_covering_indexes')
+ def test_ops_class_include(self):
+ index_name = 'test_ops_class_include'
+ index = Index(
+ name=index_name,
+ fields=['body'],
+ opclasses=['text_pattern_ops'],
+ include=['headline'],
+ )
+ with connection.schema_editor() as editor:
+ editor.add_index(IndexedArticle2, index)
+ with editor.connection.cursor() as cursor:
+ cursor.execute(self.get_opclass_query % index_name)
+ self.assertCountEqual(cursor.fetchall(), [('text_pattern_ops', index_name)])
+
+ @skipUnlessDBFeature('supports_covering_indexes')
+ def test_ops_class_include_tablespace(self):
+ index_name = 'test_ops_class_include_tblspace'
+ index = Index(
+ name=index_name,
+ fields=['body'],
+ opclasses=['text_pattern_ops'],
+ include=['headline'],
+ db_tablespace='pg_default',
+ )
+ with connection.schema_editor() as editor:
+ editor.add_index(IndexedArticle2, index)
+ self.assertIn(
+ 'TABLESPACE "pg_default"',
+ str(index.create_sql(IndexedArticle2, editor)),
+ )
+ with editor.connection.cursor() as cursor:
+ cursor.execute(self.get_opclass_query % index_name)
+ self.assertCountEqual(cursor.fetchall(), [('text_pattern_ops', index_name)])
+
def test_ops_class_columns_lists_sql(self):
index = Index(
fields=['headline'],
@@ -417,3 +452,89 @@ class PartialIndexTests(TransactionTestCase):
cursor=cursor, table_name=Article._meta.db_table,
))
editor.remove_index(index=index, model=Article)
+
+
+@skipUnlessDBFeature('supports_covering_indexes')
+class CoveringIndexTests(TransactionTestCase):
+ available_apps = ['indexes']
+
+ def test_covering_index(self):
+ index = Index(
+ name='covering_headline_idx',
+ fields=['headline'],
+ include=['pub_date', 'published'],
+ )
+ with connection.schema_editor() as editor:
+ self.assertIn(
+ '(%s) INCLUDE (%s, %s)' % (
+ editor.quote_name('headline'),
+ editor.quote_name('pub_date'),
+ editor.quote_name('published'),
+ ),
+ str(index.create_sql(Article, editor)),
+ )
+ editor.add_index(Article, index)
+ with connection.cursor() as cursor:
+ constraints = connection.introspection.get_constraints(
+ cursor=cursor, table_name=Article._meta.db_table,
+ )
+ self.assertIn(index.name, constraints)
+ self.assertEqual(
+ constraints[index.name]['columns'],
+ ['headline', 'pub_date', 'published'],
+ )
+ editor.remove_index(Article, index)
+ with connection.cursor() as cursor:
+ self.assertNotIn(index.name, connection.introspection.get_constraints(
+ cursor=cursor, table_name=Article._meta.db_table,
+ ))
+
+ def test_covering_partial_index(self):
+ index = Index(
+ name='covering_partial_headline_idx',
+ fields=['headline'],
+ include=['pub_date'],
+ condition=Q(pub_date__isnull=False),
+ )
+ with connection.schema_editor() as editor:
+ self.assertIn(
+ '(%s) INCLUDE (%s) WHERE %s ' % (
+ editor.quote_name('headline'),
+ editor.quote_name('pub_date'),
+ editor.quote_name('pub_date'),
+ ),
+ str(index.create_sql(Article, editor)),
+ )
+ editor.add_index(Article, index)
+ with connection.cursor() as cursor:
+ constraints = connection.introspection.get_constraints(
+ cursor=cursor, table_name=Article._meta.db_table,
+ )
+ self.assertIn(index.name, constraints)
+ self.assertEqual(
+ constraints[index.name]['columns'],
+ ['headline', 'pub_date'],
+ )
+ editor.remove_index(Article, index)
+ with connection.cursor() as cursor:
+ self.assertNotIn(index.name, connection.introspection.get_constraints(
+ cursor=cursor, table_name=Article._meta.db_table,
+ ))
+
+
+@skipIfDBFeature('supports_covering_indexes')
+class CoveringIndexIgnoredTests(TransactionTestCase):
+ available_apps = ['indexes']
+
+ def test_covering_ignored(self):
+ index = Index(
+ name='test_covering_ignored',
+ fields=['headline'],
+ include=['pub_date'],
+ )
+ with connection.schema_editor() as editor:
+ editor.add_index(Article, index)
+ self.assertNotIn(
+ 'INCLUDE (%s)' % editor.quote_name('headline'),
+ str(index.create_sql(Article, editor)),
+ )
diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py
index 7847ee8486..374eff6028 100644
--- a/tests/invalid_models_tests/test_models.py
+++ b/tests/invalid_models_tests/test_models.py
@@ -375,6 +375,78 @@ class IndexesTests(TestCase):
self.assertEqual(Model.check(databases=self.databases), [])
+ def test_index_include_pointing_to_missing_field(self):
+ class Model(models.Model):
+ class Meta:
+ indexes = [
+ models.Index(fields=['id'], include=['missing_field'], name='name'),
+ ]
+
+ self.assertEqual(Model.check(databases=self.databases), [
+ Error(
+ "'indexes' refers to the nonexistent field 'missing_field'.",
+ obj=Model,
+ id='models.E012',
+ ),
+ ])
+
+ def test_index_include_pointing_to_m2m_field(self):
+ class Model(models.Model):
+ m2m = models.ManyToManyField('self')
+
+ class Meta:
+ indexes = [models.Index(fields=['id'], include=['m2m'], name='name')]
+
+ self.assertEqual(Model.check(databases=self.databases), [
+ Error(
+ "'indexes' refers to a ManyToManyField 'm2m', but "
+ "ManyToManyFields are not permitted in 'indexes'.",
+ obj=Model,
+ id='models.E013',
+ ),
+ ])
+
+ def test_index_include_pointing_to_non_local_field(self):
+ class Parent(models.Model):
+ field1 = models.IntegerField()
+
+ class Child(Parent):
+ field2 = models.IntegerField()
+
+ class Meta:
+ indexes = [
+ models.Index(fields=['field2'], include=['field1'], name='name'),
+ ]
+
+ self.assertEqual(Child.check(databases=self.databases), [
+ Error(
+ "'indexes' refers to field 'field1' which is not local to "
+ "model 'Child'.",
+ hint='This issue may be caused by multi-table inheritance.',
+ obj=Child,
+ id='models.E016',
+ ),
+ ])
+
+ def test_index_include_pointing_to_fk(self):
+ class Target(models.Model):
+ pass
+
+ class Model(models.Model):
+ fk_1 = models.ForeignKey(Target, models.CASCADE, related_name='target_1')
+ fk_2 = models.ForeignKey(Target, models.CASCADE, related_name='target_2')
+
+ class Meta:
+ constraints = [
+ models.Index(
+ fields=['id'],
+ include=['fk_1_id', 'fk_2'],
+ name='name',
+ ),
+ ]
+
+ self.assertEqual(Model.check(databases=self.databases), [])
+
@isolate_apps('invalid_models_tests')
class FieldNamesTests(TestCase):
@@ -1568,3 +1640,90 @@ class ConstraintsTests(TestCase):
]
self.assertEqual(Model.check(databases=self.databases), [])
+
+ def test_unique_constraint_include_pointing_to_missing_field(self):
+ class Model(models.Model):
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=['id'],
+ include=['missing_field'],
+ name='name',
+ ),
+ ]
+
+ self.assertEqual(Model.check(databases=self.databases), [
+ Error(
+ "'constraints' refers to the nonexistent field "
+ "'missing_field'.",
+ obj=Model,
+ id='models.E012',
+ ),
+ ])
+
+ def test_unique_constraint_include_pointing_to_m2m_field(self):
+ class Model(models.Model):
+ m2m = models.ManyToManyField('self')
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=['id'],
+ include=['m2m'],
+ name='name',
+ ),
+ ]
+
+ self.assertEqual(Model.check(databases=self.databases), [
+ Error(
+ "'constraints' refers to a ManyToManyField 'm2m', but "
+ "ManyToManyFields are not permitted in 'constraints'.",
+ obj=Model,
+ id='models.E013',
+ ),
+ ])
+
+ def test_unique_constraint_include_pointing_to_non_local_field(self):
+ class Parent(models.Model):
+ field1 = models.IntegerField()
+
+ class Child(Parent):
+ field2 = models.IntegerField()
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=['field2'],
+ include=['field1'],
+ name='name',
+ ),
+ ]
+
+ self.assertEqual(Child.check(databases=self.databases), [
+ Error(
+ "'constraints' refers to field 'field1' which is not local to "
+ "model 'Child'.",
+ hint='This issue may be caused by multi-table inheritance.',
+ obj=Child,
+ id='models.E016',
+ ),
+ ])
+
+ def test_unique_constraint_include_pointing_to_fk(self):
+ class Target(models.Model):
+ pass
+
+ class Model(models.Model):
+ fk_1 = models.ForeignKey(Target, models.CASCADE, related_name='target_1')
+ fk_2 = models.ForeignKey(Target, models.CASCADE, related_name='target_2')
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=['id'],
+ include=['fk_1_id', 'fk_2'],
+ name='name',
+ ),
+ ]
+
+ self.assertEqual(Model.check(databases=self.databases), [])
diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
index 855a052058..588186fae4 100644
--- a/tests/migrations/test_operations.py
+++ b/tests/migrations/test_operations.py
@@ -448,6 +448,48 @@ class OperationTests(OperationTestBase):
[deferred_unique_constraint],
)
+ @skipUnlessDBFeature('supports_covering_indexes')
+ def test_create_model_with_covering_unique_constraint(self):
+ covering_unique_constraint = models.UniqueConstraint(
+ fields=['pink'],
+ include=['weight'],
+ name='test_constraint_pony_pink_covering_weight',
+ )
+ operation = migrations.CreateModel(
+ 'Pony',
+ [
+ ('id', models.AutoField(primary_key=True)),
+ ('pink', models.IntegerField(default=3)),
+ ('weight', models.FloatField()),
+ ],
+ options={'constraints': [covering_unique_constraint]},
+ )
+ project_state = ProjectState()
+ new_state = project_state.clone()
+ operation.state_forwards('test_crmo', new_state)
+ self.assertEqual(len(new_state.models['test_crmo', 'pony'].options['constraints']), 1)
+ self.assertTableNotExists('test_crmo_pony')
+ # Create table.
+ with connection.schema_editor() as editor:
+ operation.database_forwards('test_crmo', editor, project_state, new_state)
+ self.assertTableExists('test_crmo_pony')
+ Pony = new_state.apps.get_model('test_crmo', 'Pony')
+ Pony.objects.create(pink=1, weight=4.0)
+ with self.assertRaises(IntegrityError):
+ Pony.objects.create(pink=1, weight=7.0)
+ # Reversal.
+ with connection.schema_editor() as editor:
+ operation.database_backwards('test_crmo', editor, new_state, project_state)
+ self.assertTableNotExists('test_crmo_pony')
+ # Deconstruction.
+ definition = operation.deconstruct()
+ self.assertEqual(definition[0], 'CreateModel')
+ self.assertEqual(definition[1], [])
+ self.assertEqual(
+ definition[2]['options']['constraints'],
+ [covering_unique_constraint],
+ )
+
def test_create_model_managers(self):
"""
The managers on a model are set.
@@ -2236,6 +2278,88 @@ class OperationTests(OperationTestBase):
'name': 'deferred_pink_constraint_rm',
})
+ def test_add_covering_unique_constraint(self):
+ app_label = 'test_addcovering_uc'
+ project_state = self.set_up_test_model(app_label)
+ covering_unique_constraint = models.UniqueConstraint(
+ fields=['pink'],
+ name='covering_pink_constraint_add',
+ include=['weight'],
+ )
+ operation = migrations.AddConstraint('Pony', covering_unique_constraint)
+ self.assertEqual(
+ operation.describe(),
+ 'Create constraint covering_pink_constraint_add on model Pony',
+ )
+ # Add constraint.
+ new_state = project_state.clone()
+ operation.state_forwards(app_label, new_state)
+ self.assertEqual(len(new_state.models[app_label, 'pony'].options['constraints']), 1)
+ Pony = new_state.apps.get_model(app_label, 'Pony')
+ self.assertEqual(len(Pony._meta.constraints), 1)
+ with connection.schema_editor() as editor:
+ operation.database_forwards(app_label, editor, project_state, new_state)
+ Pony.objects.create(pink=1, weight=4.0)
+ if connection.features.supports_covering_indexes:
+ with self.assertRaises(IntegrityError):
+ Pony.objects.create(pink=1, weight=4.0)
+ else:
+ Pony.objects.create(pink=1, weight=4.0)
+ # Reversal.
+ with connection.schema_editor() as editor:
+ operation.database_backwards(app_label, editor, new_state, project_state)
+ # Constraint doesn't work.
+ Pony.objects.create(pink=1, weight=4.0)
+ # Deconstruction.
+ definition = operation.deconstruct()
+ self.assertEqual(definition[0], 'AddConstraint')
+ self.assertEqual(definition[1], [])
+ self.assertEqual(
+ definition[2],
+ {'model_name': 'Pony', 'constraint': covering_unique_constraint},
+ )
+
+ def test_remove_covering_unique_constraint(self):
+ app_label = 'test_removecovering_uc'
+ covering_unique_constraint = models.UniqueConstraint(
+ fields=['pink'],
+ name='covering_pink_constraint_rm',
+ include=['weight'],
+ )
+ project_state = self.set_up_test_model(app_label, constraints=[covering_unique_constraint])
+ operation = migrations.RemoveConstraint('Pony', covering_unique_constraint.name)
+ self.assertEqual(
+ operation.describe(),
+ 'Remove constraint covering_pink_constraint_rm from model Pony',
+ )
+ # Remove constraint.
+ new_state = project_state.clone()
+ operation.state_forwards(app_label, new_state)
+ self.assertEqual(len(new_state.models[app_label, 'pony'].options['constraints']), 0)
+ Pony = new_state.apps.get_model(app_label, 'Pony')
+ self.assertEqual(len(Pony._meta.constraints), 0)
+ with connection.schema_editor() as editor:
+ operation.database_forwards(app_label, editor, project_state, new_state)
+ # Constraint doesn't work.
+ Pony.objects.create(pink=1, weight=4.0)
+ Pony.objects.create(pink=1, weight=4.0).delete()
+ # Reversal.
+ with connection.schema_editor() as editor:
+ operation.database_backwards(app_label, editor, new_state, project_state)
+ if connection.features.supports_covering_indexes:
+ with self.assertRaises(IntegrityError):
+ Pony.objects.create(pink=1, weight=4.0)
+ else:
+ Pony.objects.create(pink=1, weight=4.0)
+ # Deconstruction.
+ definition = operation.deconstruct()
+ self.assertEqual(definition[0], 'RemoveConstraint')
+ self.assertEqual(definition[1], [])
+ self.assertEqual(definition[2], {
+ 'model_name': 'Pony',
+ 'name': 'covering_pink_constraint_rm',
+ })
+
def test_alter_model_options(self):
"""
Tests the AlterModelOptions operation.
diff --git a/tests/model_indexes/tests.py b/tests/model_indexes/tests.py
index 15ec702937..ff3c6c73c7 100644
--- a/tests/model_indexes/tests.py
+++ b/tests/model_indexes/tests.py
@@ -17,9 +17,18 @@ class SimpleIndexesTests(SimpleTestCase):
index = models.Index(fields=['title'])
multi_col_index = models.Index(fields=['title', 'author'])
partial_index = models.Index(fields=['title'], name='long_books_idx', condition=models.Q(pages__gt=400))
+ covering_index = models.Index(
+ fields=['title'],
+ name='include_idx',
+ include=['author', 'pages'],
+ )
self.assertEqual(repr(index), "<Index: fields='title'>")
self.assertEqual(repr(multi_col_index), "<Index: fields='title, author'>")
self.assertEqual(repr(partial_index), "<Index: fields='title', condition=(AND: ('pages__gt', 400))>")
+ self.assertEqual(
+ repr(covering_index),
+ "<Index: fields='title', include='author, pages'>",
+ )
def test_eq(self):
index = models.Index(fields=['title'])
@@ -65,6 +74,16 @@ class SimpleIndexesTests(SimpleTestCase):
with self.assertRaisesMessage(ValueError, 'Index.condition must be a Q instance.'):
models.Index(condition='invalid', name='long_book_idx')
+ def test_include_requires_list_or_tuple(self):
+ msg = 'Index.include must be a list or tuple.'
+ with self.assertRaisesMessage(ValueError, msg):
+ models.Index(name='test_include', fields=['field'], include='other')
+
+ def test_include_requires_index_name(self):
+ msg = 'A covering index must be named.'
+ with self.assertRaisesMessage(ValueError, msg):
+ models.Index(fields=['field'], include=['other'])
+
def test_name_auto_generation(self):
index = models.Index(fields=['author'])
index.set_name_with_model(Book)
@@ -128,6 +147,25 @@ class SimpleIndexesTests(SimpleTestCase):
}
)
+ def test_deconstruct_with_include(self):
+ index = models.Index(
+ name='book_include_idx',
+ fields=['title'],
+ include=['author'],
+ )
+ index.set_name_with_model(Book)
+ path, args, kwargs = index.deconstruct()
+ self.assertEqual(path, 'django.db.models.Index')
+ self.assertEqual(args, ())
+ self.assertEqual(
+ kwargs,
+ {
+ 'fields': ['title'],
+ 'name': 'model_index_title_196f42_idx',
+ 'include': ('author',),
+ },
+ )
+
def test_clone(self):
index = models.Index(fields=['title'])
new_index = index.clone()
diff --git a/tests/postgres_tests/test_indexes.py b/tests/postgres_tests/test_indexes.py
index 0440beda5d..b9888f4843 100644
--- a/tests/postgres_tests/test_indexes.py
+++ b/tests/postgres_tests/test_indexes.py
@@ -11,7 +11,7 @@ from django.test import skipUnlessDBFeature
from django.test.utils import register_lookup
from . import PostgreSQLSimpleTestCase, PostgreSQLTestCase
-from .models import CharFieldModel, IntegerArrayModel
+from .models import CharFieldModel, IntegerArrayModel, Scene
class IndexTestMixin:
@@ -373,6 +373,33 @@ class SchemaTests(PostgreSQLTestCase):
editor.remove_index(CharFieldModel, index)
self.assertNotIn(index_name, self.get_constraints(CharFieldModel._meta.db_table))
+ @skipUnlessDBFeature('supports_covering_gist_indexes')
+ def test_gist_include(self):
+ index_name = 'scene_gist_include_setting'
+ index = GistIndex(name=index_name, fields=['scene'], include=['setting'])
+ with connection.schema_editor() as editor:
+ editor.add_index(Scene, index)
+ constraints = self.get_constraints(Scene._meta.db_table)
+ self.assertIn(index_name, constraints)
+ self.assertEqual(constraints[index_name]['type'], GistIndex.suffix)
+ self.assertEqual(constraints[index_name]['columns'], ['scene', 'setting'])
+ with connection.schema_editor() as editor:
+ editor.remove_index(Scene, index)
+ self.assertNotIn(index_name, self.get_constraints(Scene._meta.db_table))
+
+ def test_gist_include_not_supported(self):
+ index_name = 'gist_include_exception'
+ index = GistIndex(fields=['scene'], name=index_name, include=['setting'])
+ msg = 'Covering GiST indexes requires PostgreSQL 12+.'
+ with self.assertRaisesMessage(NotSupportedError, msg):
+ with mock.patch(
+ 'django.db.backends.postgresql.features.DatabaseFeatures.supports_covering_gist_indexes',
+ False,
+ ):
+ with connection.schema_editor() as editor:
+ editor.add_index(Scene, index)
+ self.assertNotIn(index_name, self.get_constraints(Scene._meta.db_table))
+
def test_hash_index(self):
# Ensure the table is there and doesn't have an index.
self.assertNotIn('field', self.get_constraints(CharFieldModel._meta.db_table))
diff --git a/tests/schema/tests.py b/tests/schema/tests.py
index 2942b90130..5868d520e8 100644
--- a/tests/schema/tests.py
+++ b/tests/schema/tests.py
@@ -2587,6 +2587,7 @@ class SchemaTests(TransactionTestCase):
"columns": editor.quote_name(column),
"extra": "",
"condition": "",
+ "include": "",
}
)
self.assertIn(expected_constraint_name, self.get_constraints(model._meta.db_table))