mirror of https://github.com/django/django.git
Refs #32943 -- Added support for covering exclusion constraints using SP-GiST indexes on PostgreSQL 14+.
This commit is contained in:
parent
e76f9d5b44
commit
c2f6c05c4c
|
@ -45,10 +45,6 @@ class ExclusionConstraint(BaseConstraint):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'ExclusionConstraint.include must be a list or tuple.'
|
'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)):
|
if not isinstance(opclasses, (list, tuple)):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'ExclusionConstraint.opclasses must be a list or tuple.'
|
'ExclusionConstraint.opclasses must be a list or tuple.'
|
||||||
|
@ -124,9 +120,23 @@ class ExclusionConstraint(BaseConstraint):
|
||||||
)
|
)
|
||||||
|
|
||||||
def check_supported(self, schema_editor):
|
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(
|
raise NotSupportedError(
|
||||||
'Covering exclusion constraints require PostgreSQL 12+.'
|
'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 using an SP-GiST index '
|
||||||
|
'require PostgreSQL 14+.'
|
||||||
)
|
)
|
||||||
|
|
||||||
def deconstruct(self):
|
def deconstruct(self):
|
||||||
|
|
|
@ -115,7 +115,13 @@ used for queries that select only included fields
|
||||||
(:attr:`~ExclusionConstraint.include`) and filter only by indexed fields
|
(:attr:`~ExclusionConstraint.include`) and filter only by indexed fields
|
||||||
(:attr:`~ExclusionConstraint.expressions`).
|
(: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``
|
``opclasses``
|
||||||
-------------
|
-------------
|
||||||
|
|
|
@ -71,6 +71,10 @@ Minor features
|
||||||
* :class:`~django.contrib.postgres.indexes.SpGistIndex` now supports covering
|
* :class:`~django.contrib.postgres.indexes.SpGistIndex` now supports covering
|
||||||
indexes on PostgreSQL 14+.
|
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`
|
:mod:`django.contrib.redirects`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -271,16 +271,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
|
||||||
include='invalid',
|
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):
|
def test_invalid_opclasses_type(self):
|
||||||
msg = 'ExclusionConstraint.opclasses must be a list or tuple.'
|
msg = 'ExclusionConstraint.opclasses must be a list or tuple.'
|
||||||
with self.assertRaisesMessage(ValueError, msg):
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
|
@ -709,14 +699,33 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
|
||||||
RangesModel.objects.create(ints=(51, 60))
|
RangesModel.objects.create(ints=(51, 60))
|
||||||
|
|
||||||
@skipUnlessDBFeature('supports_covering_gist_indexes')
|
@skipUnlessDBFeature('supports_covering_gist_indexes')
|
||||||
def test_range_adjacent_include(self):
|
def test_range_adjacent_gist_include(self):
|
||||||
constraint_name = 'ints_adjacent_include'
|
constraint_name = 'ints_adjacent_gist_include'
|
||||||
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
||||||
constraint = ExclusionConstraint(
|
constraint = ExclusionConstraint(
|
||||||
name=constraint_name,
|
name=constraint_name,
|
||||||
expressions=[('ints', RangeOperators.ADJACENT_TO)],
|
expressions=[('ints', RangeOperators.ADJACENT_TO)],
|
||||||
include=['decimals', 'ints'],
|
|
||||||
index_type='gist',
|
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:
|
with connection.schema_editor() as editor:
|
||||||
editor.add_constraint(RangesModel, constraint)
|
editor.add_constraint(RangesModel, constraint)
|
||||||
|
@ -728,12 +737,28 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
|
||||||
RangesModel.objects.create(ints=(51, 60))
|
RangesModel.objects.create(ints=(51, 60))
|
||||||
|
|
||||||
@skipUnlessDBFeature('supports_covering_gist_indexes')
|
@skipUnlessDBFeature('supports_covering_gist_indexes')
|
||||||
def test_range_adjacent_include_condition(self):
|
def test_range_adjacent_gist_include_condition(self):
|
||||||
constraint_name = 'ints_adjacent_include_condition'
|
constraint_name = 'ints_adjacent_gist_include_condition'
|
||||||
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
||||||
constraint = ExclusionConstraint(
|
constraint = ExclusionConstraint(
|
||||||
name=constraint_name,
|
name=constraint_name,
|
||||||
expressions=[('ints', RangeOperators.ADJACENT_TO)],
|
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'],
|
include=['decimals'],
|
||||||
condition=Q(id__gte=100),
|
condition=Q(id__gte=100),
|
||||||
)
|
)
|
||||||
|
@ -742,12 +767,13 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
|
||||||
self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
||||||
|
|
||||||
@skipUnlessDBFeature('supports_covering_gist_indexes')
|
@skipUnlessDBFeature('supports_covering_gist_indexes')
|
||||||
def test_range_adjacent_include_deferrable(self):
|
def test_range_adjacent_gist_include_deferrable(self):
|
||||||
constraint_name = 'ints_adjacent_include_deferrable'
|
constraint_name = 'ints_adjacent_gist_include_deferrable'
|
||||||
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
||||||
constraint = ExclusionConstraint(
|
constraint = ExclusionConstraint(
|
||||||
name=constraint_name,
|
name=constraint_name,
|
||||||
expressions=[('ints', RangeOperators.ADJACENT_TO)],
|
expressions=[('ints', RangeOperators.ADJACENT_TO)],
|
||||||
|
index_type='gist',
|
||||||
include=['decimals'],
|
include=['decimals'],
|
||||||
deferrable=Deferrable.DEFERRED,
|
deferrable=Deferrable.DEFERRED,
|
||||||
)
|
)
|
||||||
|
@ -755,14 +781,33 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
|
||||||
editor.add_constraint(RangesModel, constraint)
|
editor.add_constraint(RangesModel, constraint)
|
||||||
self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
||||||
|
|
||||||
def test_include_not_supported(self):
|
@skipUnlessDBFeature('supports_covering_spgist_indexes')
|
||||||
constraint_name = 'ints_adjacent_include_not_supported'
|
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(
|
constraint = ExclusionConstraint(
|
||||||
name=constraint_name,
|
name=constraint_name,
|
||||||
expressions=[('ints', RangeOperators.ADJACENT_TO)],
|
expressions=[('ints', RangeOperators.ADJACENT_TO)],
|
||||||
|
index_type='spgist',
|
||||||
|
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))
|
||||||
|
|
||||||
|
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'],
|
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 connection.schema_editor() as editor:
|
||||||
with mock.patch(
|
with mock.patch(
|
||||||
'django.db.backends.postgresql.features.DatabaseFeatures.supports_covering_gist_indexes',
|
'django.db.backends.postgresql.features.DatabaseFeatures.supports_covering_gist_indexes',
|
||||||
|
@ -771,6 +816,27 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
|
||||||
with self.assertRaisesMessage(NotSupportedError, msg):
|
with self.assertRaisesMessage(NotSupportedError, msg):
|
||||||
editor.add_constraint(RangesModel, constraint)
|
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):
|
def test_range_adjacent_opclasses(self):
|
||||||
constraint_name = 'ints_adjacent_opclasses'
|
constraint_name = 'ints_adjacent_opclasses'
|
||||||
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
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))
|
self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
||||||
|
|
||||||
@skipUnlessDBFeature('supports_covering_gist_indexes')
|
@skipUnlessDBFeature('supports_covering_gist_indexes')
|
||||||
def test_range_adjacent_opclasses_include(self):
|
def test_range_adjacent_gist_opclasses_include(self):
|
||||||
constraint_name = 'ints_adjacent_opclasses_include'
|
constraint_name = 'ints_adjacent_gist_opclasses_include'
|
||||||
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
||||||
constraint = ExclusionConstraint(
|
constraint = ExclusionConstraint(
|
||||||
name=constraint_name,
|
name=constraint_name,
|
||||||
expressions=[('ints', RangeOperators.ADJACENT_TO)],
|
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'],
|
opclasses=['range_ops'],
|
||||||
include=['decimals'],
|
include=['decimals'],
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue