Refs #33342 -- Removed ExclusionConstraint.opclasses per deprecation timeline.

This commit is contained in:
Mariusz Felisiak 2023-01-12 13:20:08 +01:00
parent 5c10041f46
commit 23ec318988
5 changed files with 8 additions and 278 deletions

View File

@ -1,5 +1,3 @@
import warnings
from django.contrib.postgres.indexes import OpClass from django.contrib.postgres.indexes import OpClass
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import DEFAULT_DB_ALIAS, NotSupportedError from django.db import DEFAULT_DB_ALIAS, NotSupportedError
@ -9,7 +7,6 @@ from django.db.models.expressions import Exists, ExpressionList
from django.db.models.indexes import IndexExpression from django.db.models.indexes import IndexExpression
from django.db.models.lookups import PostgresOperatorLookup from django.db.models.lookups import PostgresOperatorLookup
from django.db.models.sql import Query from django.db.models.sql import Query
from django.utils.deprecation import RemovedInDjango50Warning
__all__ = ["ExclusionConstraint"] __all__ = ["ExclusionConstraint"]
@ -33,7 +30,6 @@ class ExclusionConstraint(BaseConstraint):
condition=None, condition=None,
deferrable=None, deferrable=None,
include=None, include=None,
opclasses=(),
violation_error_message=None, violation_error_message=None,
): ):
if index_type and index_type.lower() not in {"gist", "spgist"}: if index_type and index_type.lower() not in {"gist", "spgist"}:
@ -57,28 +53,11 @@ class ExclusionConstraint(BaseConstraint):
) )
if not isinstance(include, (type(None), list, tuple)): if not isinstance(include, (type(None), list, tuple)):
raise ValueError("ExclusionConstraint.include must be a list or tuple.") raise ValueError("ExclusionConstraint.include must be a list or tuple.")
if not isinstance(opclasses, (list, tuple)):
raise ValueError("ExclusionConstraint.opclasses must be a list or tuple.")
if opclasses and len(expressions) != len(opclasses):
raise ValueError(
"ExclusionConstraint.expressions and "
"ExclusionConstraint.opclasses must have the same number of "
"elements."
)
self.expressions = expressions self.expressions = expressions
self.index_type = index_type or "GIST" self.index_type = index_type or "GIST"
self.condition = condition self.condition = condition
self.deferrable = deferrable self.deferrable = deferrable
self.include = tuple(include) if include else () self.include = tuple(include) if include else ()
self.opclasses = opclasses
if self.opclasses:
warnings.warn(
"The opclasses argument is deprecated in favor of using "
"django.contrib.postgres.indexes.OpClass in "
"ExclusionConstraint.expressions.",
category=RemovedInDjango50Warning,
stacklevel=2,
)
super().__init__(name=name, violation_error_message=violation_error_message) super().__init__(name=name, violation_error_message=violation_error_message)
def _get_expressions(self, schema_editor, query): def _get_expressions(self, schema_editor, query):
@ -86,10 +65,6 @@ class ExclusionConstraint(BaseConstraint):
for idx, (expression, operator) in enumerate(self.expressions): for idx, (expression, operator) in enumerate(self.expressions):
if isinstance(expression, str): if isinstance(expression, str):
expression = F(expression) expression = F(expression)
try:
expression = OpClass(expression, self.opclasses[idx])
except IndexError:
pass
expression = ExclusionConstraintExpression(expression, operator=operator) expression = ExclusionConstraintExpression(expression, operator=operator)
expression.set_wrapper_classes(schema_editor.connection) expression.set_wrapper_classes(schema_editor.connection)
expressions.append(expression) expressions.append(expression)
@ -161,8 +136,6 @@ class ExclusionConstraint(BaseConstraint):
kwargs["deferrable"] = self.deferrable kwargs["deferrable"] = self.deferrable
if self.include: if self.include:
kwargs["include"] = self.include kwargs["include"] = self.include
if self.opclasses:
kwargs["opclasses"] = self.opclasses
return path, args, kwargs return path, args, kwargs
def __eq__(self, other): def __eq__(self, other):
@ -174,13 +147,12 @@ class ExclusionConstraint(BaseConstraint):
and self.condition == other.condition and self.condition == other.condition
and self.deferrable == other.deferrable and self.deferrable == other.deferrable
and self.include == other.include and self.include == other.include
and self.opclasses == other.opclasses
and self.violation_error_message == other.violation_error_message and self.violation_error_message == other.violation_error_message
) )
return super().__eq__(other) return super().__eq__(other)
def __repr__(self): def __repr__(self):
return "<%s: index_type=%s expressions=%s name=%s%s%s%s%s>" % ( return "<%s: index_type=%s expressions=%s name=%s%s%s%s>" % (
self.__class__.__qualname__, self.__class__.__qualname__,
repr(self.index_type), repr(self.index_type),
repr(self.expressions), repr(self.expressions),
@ -188,7 +160,6 @@ class ExclusionConstraint(BaseConstraint):
"" if self.condition is None else " condition=%s" % self.condition, "" if self.condition is None else " condition=%s" % self.condition,
"" if self.deferrable is None else " deferrable=%r" % self.deferrable, "" if self.deferrable is None else " deferrable=%r" % self.deferrable,
"" if not self.include else " include=%s" % repr(self.include), "" if not self.include else " include=%s" % repr(self.include),
"" if not self.opclasses else " opclasses=%s" % repr(self.opclasses),
) )
def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS): def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS):

View File

@ -12,7 +12,7 @@ PostgreSQL supports additional data integrity constraints available from the
``ExclusionConstraint`` ``ExclusionConstraint``
======================= =======================
.. class:: ExclusionConstraint(*, name, expressions, index_type=None, condition=None, deferrable=None, include=None, opclasses=(), violation_error_message=None) .. class:: ExclusionConstraint(*, name, expressions, index_type=None, condition=None, deferrable=None, include=None, violation_error_message=None)
Creates an exclusion constraint in the database. Internally, PostgreSQL Creates an exclusion constraint in the database. Internally, PostgreSQL
implements exclusion constraints using indexes. The default index type is implements exclusion constraints using indexes. The default index type is
@ -133,32 +133,6 @@ used for queries that select only included fields
``include`` is supported for GiST indexes. PostgreSQL 14+ also supports ``include`` is supported for GiST indexes. PostgreSQL 14+ also supports
``include`` for SP-GiST indexes. ``include`` for SP-GiST indexes.
``opclasses``
-------------
.. attribute:: ExclusionConstraint.opclasses
The names of the `PostgreSQL operator classes
<https://www.postgresql.org/docs/current/indexes-opclass.html>`_ to use for
this constraint. If you require a custom operator class, you must provide one
for each expression in the constraint.
For example::
ExclusionConstraint(
name='exclude_overlapping_opclasses',
expressions=[('circle', RangeOperators.OVERLAPS)],
opclasses=['circle_ops'],
)
creates an exclusion constraint on ``circle`` using ``circle_ops``.
.. deprecated:: 4.1
The ``opclasses`` parameter is deprecated in favor of using
:class:`OpClass() <django.contrib.postgres.indexes.OpClass>` in
:attr:`~ExclusionConstraint.expressions`.
``violation_error_message`` ``violation_error_message``
--------------------------- ---------------------------

View File

@ -247,8 +247,8 @@ Minor features
* The new :attr:`.ExclusionConstraint.include` attribute allows creating * The new :attr:`.ExclusionConstraint.include` attribute allows creating
covering exclusion constraints on PostgreSQL 12+. covering exclusion constraints on PostgreSQL 12+.
* The new :attr:`.ExclusionConstraint.opclasses` attribute allows setting * The new ``ExclusionConstraint.opclasses`` attribute allows setting PostgreSQL
PostgreSQL operator classes. operator classes.
* The new :attr:`.JSONBAgg.ordering` attribute determines the ordering of the * The new :attr:`.JSONBAgg.ordering` attribute determines the ordering of the
aggregated elements. aggregated elements.

View File

@ -311,3 +311,6 @@ to remove usage of these features.
* The ``name`` argument of ``django.utils.functional.cached_property()`` is * The ``name`` argument of ``django.utils.functional.cached_property()`` is
removed. removed.
* The ``opclasses`` argument of
``django.contrib.postgres.constraints.ExclusionConstraint`` is removed.

View File

@ -16,10 +16,9 @@ from django.db.models import (
) )
from django.db.models.fields.json import KeyTextTransform from django.db.models.fields.json import KeyTextTransform
from django.db.models.functions import Cast, Left, Lower from django.db.models.functions import Cast, Left, Lower
from django.test import ignore_warnings, skipUnlessDBFeature from django.test import skipUnlessDBFeature
from django.test.utils import isolate_apps from django.test.utils import isolate_apps
from django.utils import timezone from django.utils import timezone
from django.utils.deprecation import RemovedInDjango50Warning
from . import PostgreSQLTestCase from . import PostgreSQLTestCase
from .models import HotelReservation, IntegerArrayModel, RangesModel, Room, Scene from .models import HotelReservation, IntegerArrayModel, RangesModel, Room, Scene
@ -328,30 +327,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
include="invalid", include="invalid",
) )
@ignore_warnings(category=RemovedInDjango50Warning)
def test_invalid_opclasses_type(self):
msg = "ExclusionConstraint.opclasses must be a list or tuple."
with self.assertRaisesMessage(ValueError, msg):
ExclusionConstraint(
name="exclude_invalid_opclasses",
expressions=[(F("datespan"), RangeOperators.OVERLAPS)],
opclasses="invalid",
)
@ignore_warnings(category=RemovedInDjango50Warning)
def test_opclasses_and_expressions_same_length(self):
msg = (
"ExclusionConstraint.expressions and "
"ExclusionConstraint.opclasses must have the same number of "
"elements."
)
with self.assertRaisesMessage(ValueError, msg):
ExclusionConstraint(
name="exclude_invalid_expressions_opclasses_length",
expressions=[(F("datespan"), RangeOperators.OVERLAPS)],
opclasses=["foo", "bar"],
)
def test_repr(self): def test_repr(self):
constraint = ExclusionConstraint( constraint = ExclusionConstraint(
name="exclude_overlapping", name="exclude_overlapping",
@ -466,27 +441,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
], ],
include=["cancelled"], include=["cancelled"],
) )
with ignore_warnings(category=RemovedInDjango50Warning):
constraint_8 = ExclusionConstraint(
name="exclude_overlapping",
expressions=[
("datespan", RangeOperators.OVERLAPS),
("room", RangeOperators.EQUAL),
],
include=["cancelled"],
opclasses=["range_ops", "range_ops"],
)
constraint_9 = ExclusionConstraint(
name="exclude_overlapping",
expressions=[
("datespan", RangeOperators.OVERLAPS),
("room", RangeOperators.EQUAL),
],
opclasses=["range_ops", "range_ops"],
)
self.assertNotEqual(constraint_2, constraint_9)
self.assertNotEqual(constraint_7, constraint_8)
constraint_10 = ExclusionConstraint( constraint_10 = ExclusionConstraint(
name="exclude_overlapping", name="exclude_overlapping",
expressions=[ expressions=[
@ -636,27 +590,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
}, },
) )
@ignore_warnings(category=RemovedInDjango50Warning)
def test_deconstruct_opclasses(self):
constraint = ExclusionConstraint(
name="exclude_overlapping",
expressions=[("datespan", RangeOperators.OVERLAPS)],
opclasses=["range_ops"],
)
path, args, kwargs = constraint.deconstruct()
self.assertEqual(
path, "django.contrib.postgres.constraints.ExclusionConstraint"
)
self.assertEqual(args, ())
self.assertEqual(
kwargs,
{
"name": "exclude_overlapping",
"expressions": [("datespan", RangeOperators.OVERLAPS)],
"opclasses": ["range_ops"],
},
)
def _test_range_overlaps(self, constraint): def _test_range_overlaps(self, constraint):
# Create exclusion constraint. # Create exclusion constraint.
self.assertNotIn( self.assertNotIn(
@ -759,23 +692,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
exclude={"datespan", "start", "end", "room"}, exclude={"datespan", "start", "end", "room"},
) )
@ignore_warnings(category=RemovedInDjango50Warning)
def test_range_overlaps_custom_opclasses(self):
class TsTzRange(Func):
function = "TSTZRANGE"
output_field = DateTimeRangeField()
constraint = ExclusionConstraint(
name="exclude_overlapping_reservations_custom",
expressions=[
(TsTzRange("start", "end", RangeBoundary()), RangeOperators.OVERLAPS),
("room", RangeOperators.EQUAL),
],
condition=Q(cancelled=False),
opclasses=["range_ops", "gist_int4_ops"],
)
self._test_range_overlaps(constraint)
def test_range_overlaps_custom(self): def test_range_overlaps_custom(self):
class TsTzRange(Func): class TsTzRange(Func):
function = "TSTZRANGE" function = "TSTZRANGE"
@ -1203,137 +1119,3 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
constraint_name, constraint_name,
self.get_constraints(ModelWithExclusionConstraint._meta.db_table), self.get_constraints(ModelWithExclusionConstraint._meta.db_table),
) )
class ExclusionConstraintOpclassesDepracationTests(PostgreSQLTestCase):
def get_constraints(self, table):
"""Get the constraints on the table using a new cursor."""
with connection.cursor() as cursor:
return connection.introspection.get_constraints(cursor, table)
def test_warning(self):
msg = (
"The opclasses argument is deprecated in favor of using "
"django.contrib.postgres.indexes.OpClass in "
"ExclusionConstraint.expressions."
)
with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
ExclusionConstraint(
name="exclude_overlapping",
expressions=[(F("datespan"), RangeOperators.ADJACENT_TO)],
opclasses=["range_ops"],
)
@ignore_warnings(category=RemovedInDjango50Warning)
def test_repr(self):
constraint = ExclusionConstraint(
name="exclude_overlapping",
expressions=[(F("datespan"), RangeOperators.ADJACENT_TO)],
opclasses=["range_ops"],
)
self.assertEqual(
repr(constraint),
"<ExclusionConstraint: index_type='GIST' expressions=["
"(F(datespan), '-|-')] name='exclude_overlapping' "
"opclasses=['range_ops']>",
)
@ignore_warnings(category=RemovedInDjango50Warning)
def test_range_adjacent_opclasses(self):
constraint_name = "ints_adjacent_opclasses"
self.assertNotIn(
constraint_name, self.get_constraints(RangesModel._meta.db_table)
)
constraint = ExclusionConstraint(
name=constraint_name,
expressions=[("ints", RangeOperators.ADJACENT_TO)],
opclasses=["range_ops"],
)
with connection.schema_editor() as editor:
editor.add_constraint(RangesModel, constraint)
constraints = self.get_constraints(RangesModel._meta.db_table)
self.assertIn(constraint_name, constraints)
with editor.connection.cursor() as cursor:
cursor.execute(SchemaTests.get_opclass_query, [constraint.name])
self.assertEqual(
cursor.fetchall(),
[("range_ops", constraint.name)],
)
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))
# Drop the constraint.
with connection.schema_editor() as editor:
editor.remove_constraint(RangesModel, constraint)
self.assertNotIn(
constraint_name, self.get_constraints(RangesModel._meta.db_table)
)
@ignore_warnings(category=RemovedInDjango50Warning)
def test_range_adjacent_opclasses_condition(self):
constraint_name = "ints_adjacent_opclasses_condition"
self.assertNotIn(
constraint_name, self.get_constraints(RangesModel._meta.db_table)
)
constraint = ExclusionConstraint(
name=constraint_name,
expressions=[("ints", RangeOperators.ADJACENT_TO)],
opclasses=["range_ops"],
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))
@ignore_warnings(category=RemovedInDjango50Warning)
def test_range_adjacent_opclasses_deferrable(self):
constraint_name = "ints_adjacent_opclasses_deferrable"
self.assertNotIn(
constraint_name, self.get_constraints(RangesModel._meta.db_table)
)
constraint = ExclusionConstraint(
name=constraint_name,
expressions=[("ints", RangeOperators.ADJACENT_TO)],
opclasses=["range_ops"],
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))
@ignore_warnings(category=RemovedInDjango50Warning)
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))
@ignore_warnings(category=RemovedInDjango50Warning)
@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"],
)
with connection.schema_editor() as editor:
editor.add_constraint(RangesModel, constraint)
self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))