diff --git a/django/contrib/postgres/constraints.py b/django/contrib/postgres/constraints.py index 1caf432d16d..4c739b3fbb3 100644 --- a/django/contrib/postgres/constraints.py +++ b/django/contrib/postgres/constraints.py @@ -1,5 +1,3 @@ -import warnings - from django.contrib.postgres.indexes import OpClass from django.core.exceptions import ValidationError 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.lookups import PostgresOperatorLookup from django.db.models.sql import Query -from django.utils.deprecation import RemovedInDjango50Warning __all__ = ["ExclusionConstraint"] @@ -33,7 +30,6 @@ class ExclusionConstraint(BaseConstraint): condition=None, deferrable=None, include=None, - opclasses=(), violation_error_message=None, ): 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)): 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.index_type = index_type or "GIST" self.condition = condition self.deferrable = deferrable 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) def _get_expressions(self, schema_editor, query): @@ -86,10 +65,6 @@ class ExclusionConstraint(BaseConstraint): for idx, (expression, operator) in enumerate(self.expressions): if isinstance(expression, str): expression = F(expression) - try: - expression = OpClass(expression, self.opclasses[idx]) - except IndexError: - pass expression = ExclusionConstraintExpression(expression, operator=operator) expression.set_wrapper_classes(schema_editor.connection) expressions.append(expression) @@ -161,8 +136,6 @@ class ExclusionConstraint(BaseConstraint): kwargs["deferrable"] = self.deferrable if self.include: kwargs["include"] = self.include - if self.opclasses: - kwargs["opclasses"] = self.opclasses return path, args, kwargs def __eq__(self, other): @@ -174,13 +147,12 @@ class ExclusionConstraint(BaseConstraint): and self.condition == other.condition and self.deferrable == other.deferrable and self.include == other.include - and self.opclasses == other.opclasses and self.violation_error_message == other.violation_error_message ) return super().__eq__(other) 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__, repr(self.index_type), repr(self.expressions), @@ -188,7 +160,6 @@ class ExclusionConstraint(BaseConstraint): "" if self.condition is None else " condition=%s" % self.condition, "" if self.deferrable is None else " deferrable=%r" % self.deferrable, "" 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): diff --git a/docs/ref/contrib/postgres/constraints.txt b/docs/ref/contrib/postgres/constraints.txt index 5c50ddd3a5b..fcf50b8b5f9 100644 --- a/docs/ref/contrib/postgres/constraints.txt +++ b/docs/ref/contrib/postgres/constraints.txt @@ -12,7 +12,7 @@ PostgreSQL supports additional data integrity constraints available from the ``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 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`` for SP-GiST indexes. -``opclasses`` -------------- - -.. attribute:: ExclusionConstraint.opclasses - -The names of the `PostgreSQL operator classes -`_ 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() ` in - :attr:`~ExclusionConstraint.expressions`. - ``violation_error_message`` --------------------------- diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt index 0195e3cf6ee..5668c1a6ad4 100644 --- a/docs/releases/3.2.txt +++ b/docs/releases/3.2.txt @@ -247,8 +247,8 @@ Minor features * The new :attr:`.ExclusionConstraint.include` attribute allows creating covering exclusion constraints on PostgreSQL 12+. -* The new :attr:`.ExclusionConstraint.opclasses` attribute allows setting - PostgreSQL operator classes. +* The new ``ExclusionConstraint.opclasses`` attribute allows setting PostgreSQL + operator classes. * The new :attr:`.JSONBAgg.ordering` attribute determines the ordering of the aggregated elements. diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt index c09973578a9..3b1db9a9ada 100644 --- a/docs/releases/5.0.txt +++ b/docs/releases/5.0.txt @@ -311,3 +311,6 @@ to remove usage of these features. * The ``name`` argument of ``django.utils.functional.cached_property()`` is removed. + +* The ``opclasses`` argument of + ``django.contrib.postgres.constraints.ExclusionConstraint`` is removed. diff --git a/tests/postgres_tests/test_constraints.py b/tests/postgres_tests/test_constraints.py index ad21ffa2b5c..e601f347eb6 100644 --- a/tests/postgres_tests/test_constraints.py +++ b/tests/postgres_tests/test_constraints.py @@ -16,10 +16,9 @@ from django.db.models import ( ) from django.db.models.fields.json import KeyTextTransform 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.utils import timezone -from django.utils.deprecation import RemovedInDjango50Warning from . import PostgreSQLTestCase from .models import HotelReservation, IntegerArrayModel, RangesModel, Room, Scene @@ -328,30 +327,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase): 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): constraint = ExclusionConstraint( name="exclude_overlapping", @@ -466,27 +441,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase): ], 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( name="exclude_overlapping", 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): # Create exclusion constraint. self.assertNotIn( @@ -759,23 +692,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase): 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): class TsTzRange(Func): function = "TSTZRANGE" @@ -1203,137 +1119,3 @@ class ExclusionConstraintTests(PostgreSQLTestCase): constraint_name, 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), - "", - ) - - @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))