summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Pope <nick@nickpope.me.uk>2021-05-28 23:56:23 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2021-10-01 13:11:37 +0200
commitc2f6c05c4cc73e831b7e852eb58bd6d7a83fa46c (patch)
treed88631488b481873d6b0b3096454f6d4e9e0a071
parente76f9d5b443845639262e18d9020ef4b070f1c7d (diff)
Refs #32943 -- Added support for covering exclusion constraints using SP-GiST indexes on PostgreSQL 14+.
-rw-r--r--django/contrib/postgres/constraints.py22
-rw-r--r--docs/ref/contrib/postgres/constraints.txt8
-rw-r--r--docs/releases/4.1.txt4
-rw-r--r--tests/postgres_tests/test_constraints.py126
4 files changed, 131 insertions, 29 deletions
diff --git a/django/contrib/postgres/constraints.py b/django/contrib/postgres/constraints.py
index a670628d8a..3b12098fe9 100644
--- a/django/contrib/postgres/constraints.py
+++ b/django/contrib/postgres/constraints.py
@@ -45,10 +45,6 @@ class ExclusionConstraint(BaseConstraint):
raise ValueError(
'ExclusionConstraint.include must be a list or tuple.'
)
- if include and index_type and index_type.lower() != 'gist':
- raise ValueError(
- 'Covering exclusion constraints only support GiST indexes.'
- )
if not isinstance(opclasses, (list, tuple)):
raise ValueError(
'ExclusionConstraint.opclasses must be a list or tuple.'
@@ -124,9 +120,23 @@ class ExclusionConstraint(BaseConstraint):
)
def check_supported(self, schema_editor):
- if self.include and not schema_editor.connection.features.supports_covering_gist_indexes:
+ if (
+ self.include and
+ self.index_type.lower() == 'gist' and
+ not schema_editor.connection.features.supports_covering_gist_indexes
+ ):
+ raise NotSupportedError(
+ 'Covering exclusion constraints using a GiST index require '
+ 'PostgreSQL 12+.'
+ )
+ if (
+ self.include and
+ self.index_type.lower() == 'spgist' and
+ not schema_editor.connection.features.supports_covering_spgist_indexes
+ ):
raise NotSupportedError(
- 'Covering exclusion constraints require PostgreSQL 12+.'
+ 'Covering exclusion constraints using an SP-GiST index '
+ 'require PostgreSQL 14+.'
)
def deconstruct(self):
diff --git a/docs/ref/contrib/postgres/constraints.txt b/docs/ref/contrib/postgres/constraints.txt
index cb4347dd95..be06b907ff 100644
--- a/docs/ref/contrib/postgres/constraints.txt
+++ b/docs/ref/contrib/postgres/constraints.txt
@@ -115,7 +115,13 @@ used for queries that select only included fields
(:attr:`~ExclusionConstraint.include`) and filter only by indexed fields
(:attr:`~ExclusionConstraint.expressions`).
-``include`` is supported only for GiST indexes on PostgreSQL 12+.
+``include`` is supported for GiST indexes on PostgreSQL 12+ and SP-GiST
+indexes on PostgreSQL 14+.
+
+.. versionchanged:: 4.1
+
+ Support for covering exclusion constraints using SP-GiST indexes on
+ PostgreSQL 14+ was added.
``opclasses``
-------------
diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index da805df315..e74e3e07f1 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -71,6 +71,10 @@ Minor features
* :class:`~django.contrib.postgres.indexes.SpGistIndex` now supports covering
indexes on PostgreSQL 14+.
+* :class:`~django.contrib.postgres.constraints.ExclusionConstraint` now
+ supports covering exclusion constraints using SP-GiST indexes on PostgreSQL
+ 14+.
+
:mod:`django.contrib.redirects`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/postgres_tests/test_constraints.py b/tests/postgres_tests/test_constraints.py
index dfe63be61b..bf0b57488a 100644
--- a/tests/postgres_tests/test_constraints.py
+++ b/tests/postgres_tests/test_constraints.py
@@ -271,16 +271,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
include='invalid',
)
- def test_invalid_include_index_type(self):
- msg = 'Covering exclusion constraints only support GiST indexes.'
- with self.assertRaisesMessage(ValueError, msg):
- ExclusionConstraint(
- name='exclude_invalid_index_type',
- expressions=[(F('datespan'), RangeOperators.OVERLAPS)],
- include=['cancelled'],
- index_type='spgist',
- )
-
def test_invalid_opclasses_type(self):
msg = 'ExclusionConstraint.opclasses must be a list or tuple.'
with self.assertRaisesMessage(ValueError, msg):
@@ -709,14 +699,33 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
RangesModel.objects.create(ints=(51, 60))
@skipUnlessDBFeature('supports_covering_gist_indexes')
- def test_range_adjacent_include(self):
- constraint_name = 'ints_adjacent_include'
+ def test_range_adjacent_gist_include(self):
+ constraint_name = 'ints_adjacent_gist_include'
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
constraint = ExclusionConstraint(
name=constraint_name,
expressions=[('ints', RangeOperators.ADJACENT_TO)],
- include=['decimals', 'ints'],
index_type='gist',
+ include=['decimals', 'ints'],
+ )
+ with connection.schema_editor() as editor:
+ editor.add_constraint(RangesModel, constraint)
+ self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
+ RangesModel.objects.create(ints=(20, 50))
+ with self.assertRaises(IntegrityError), transaction.atomic():
+ RangesModel.objects.create(ints=(10, 20))
+ RangesModel.objects.create(ints=(10, 19))
+ RangesModel.objects.create(ints=(51, 60))
+
+ @skipUnlessDBFeature('supports_covering_spgist_indexes')
+ def test_range_adjacent_spgist_include(self):
+ constraint_name = 'ints_adjacent_spgist_include'
+ self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
+ constraint = ExclusionConstraint(
+ name=constraint_name,
+ expressions=[('ints', RangeOperators.ADJACENT_TO)],
+ index_type='spgist',
+ include=['decimals', 'ints'],
)
with connection.schema_editor() as editor:
editor.add_constraint(RangesModel, constraint)
@@ -728,12 +737,28 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
RangesModel.objects.create(ints=(51, 60))
@skipUnlessDBFeature('supports_covering_gist_indexes')
- def test_range_adjacent_include_condition(self):
- constraint_name = 'ints_adjacent_include_condition'
+ def test_range_adjacent_gist_include_condition(self):
+ constraint_name = 'ints_adjacent_gist_include_condition'
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
constraint = ExclusionConstraint(
name=constraint_name,
expressions=[('ints', RangeOperators.ADJACENT_TO)],
+ index_type='gist',
+ include=['decimals'],
+ condition=Q(id__gte=100),
+ )
+ with connection.schema_editor() as editor:
+ editor.add_constraint(RangesModel, constraint)
+ self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
+
+ @skipUnlessDBFeature('supports_covering_spgist_indexes')
+ def test_range_adjacent_spgist_include_condition(self):
+ constraint_name = 'ints_adjacent_spgist_include_condition'
+ self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
+ constraint = ExclusionConstraint(
+ name=constraint_name,
+ expressions=[('ints', RangeOperators.ADJACENT_TO)],
+ index_type='spgist',
include=['decimals'],
condition=Q(id__gte=100),
)
@@ -742,12 +767,28 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
@skipUnlessDBFeature('supports_covering_gist_indexes')
- def test_range_adjacent_include_deferrable(self):
- constraint_name = 'ints_adjacent_include_deferrable'
+ def test_range_adjacent_gist_include_deferrable(self):
+ constraint_name = 'ints_adjacent_gist_include_deferrable'
+ self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
+ constraint = ExclusionConstraint(
+ name=constraint_name,
+ expressions=[('ints', RangeOperators.ADJACENT_TO)],
+ index_type='gist',
+ include=['decimals'],
+ deferrable=Deferrable.DEFERRED,
+ )
+ with connection.schema_editor() as editor:
+ editor.add_constraint(RangesModel, constraint)
+ self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
+
+ @skipUnlessDBFeature('supports_covering_spgist_indexes')
+ def test_range_adjacent_spgist_include_deferrable(self):
+ constraint_name = 'ints_adjacent_spgist_include_deferrable'
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
constraint = ExclusionConstraint(
name=constraint_name,
expressions=[('ints', RangeOperators.ADJACENT_TO)],
+ index_type='spgist',
include=['decimals'],
deferrable=Deferrable.DEFERRED,
)
@@ -755,14 +796,18 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
editor.add_constraint(RangesModel, constraint)
self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
- def test_include_not_supported(self):
- constraint_name = 'ints_adjacent_include_not_supported'
+ def test_gist_include_not_supported(self):
+ constraint_name = 'ints_adjacent_gist_include_not_supported'
constraint = ExclusionConstraint(
name=constraint_name,
expressions=[('ints', RangeOperators.ADJACENT_TO)],
+ index_type='gist',
include=['id'],
)
- msg = 'Covering exclusion constraints require PostgreSQL 12+.'
+ msg = (
+ 'Covering exclusion constraints using a GiST index require '
+ 'PostgreSQL 12+.'
+ )
with connection.schema_editor() as editor:
with mock.patch(
'django.db.backends.postgresql.features.DatabaseFeatures.supports_covering_gist_indexes',
@@ -771,6 +816,27 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
with self.assertRaisesMessage(NotSupportedError, msg):
editor.add_constraint(RangesModel, constraint)
+ def test_spgist_include_not_supported(self):
+ constraint_name = 'ints_adjacent_spgist_include_not_supported'
+ constraint = ExclusionConstraint(
+ name=constraint_name,
+ expressions=[('ints', RangeOperators.ADJACENT_TO)],
+ index_type='spgist',
+ include=['id'],
+ )
+ msg = (
+ 'Covering exclusion constraints using an SP-GiST index require '
+ 'PostgreSQL 14+.'
+ )
+ with connection.schema_editor() as editor:
+ with mock.patch(
+ 'django.db.backends.postgresql.features.DatabaseFeatures.'
+ 'supports_covering_spgist_indexes',
+ False,
+ ):
+ with self.assertRaisesMessage(NotSupportedError, msg):
+ editor.add_constraint(RangesModel, constraint)
+
def test_range_adjacent_opclasses(self):
constraint_name = 'ints_adjacent_opclasses'
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
@@ -819,12 +885,28 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
@skipUnlessDBFeature('supports_covering_gist_indexes')
- def test_range_adjacent_opclasses_include(self):
- constraint_name = 'ints_adjacent_opclasses_include'
+ def test_range_adjacent_gist_opclasses_include(self):
+ constraint_name = 'ints_adjacent_gist_opclasses_include'
+ self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
+ constraint = ExclusionConstraint(
+ name=constraint_name,
+ expressions=[('ints', RangeOperators.ADJACENT_TO)],
+ index_type='gist',
+ opclasses=['range_ops'],
+ include=['decimals'],
+ )
+ with connection.schema_editor() as editor:
+ editor.add_constraint(RangesModel, constraint)
+ self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
+
+ @skipUnlessDBFeature('supports_covering_spgist_indexes')
+ def test_range_adjacent_spgist_opclasses_include(self):
+ constraint_name = 'ints_adjacent_spgist_opclasses_include'
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
constraint = ExclusionConstraint(
name=constraint_name,
expressions=[('ints', RangeOperators.ADJACENT_TO)],
+ index_type='spgist',
opclasses=['range_ops'],
include=['decimals'],
)