diff --git a/django/contrib/postgres/constraints.py b/django/contrib/postgres/constraints.py index 8b76de3c420..1caf432d16d 100644 --- a/django/contrib/postgres/constraints.py +++ b/django/contrib/postgres/constraints.py @@ -51,8 +51,6 @@ class ExclusionConstraint(BaseConstraint): raise ValueError("The expressions must be a list of 2-tuples.") if not isinstance(condition, (type(None), Q)): raise ValueError("ExclusionConstraint.condition must be a Q instance.") - if condition and deferrable: - raise ValueError("ExclusionConstraint with conditions cannot be deferred.") if not isinstance(deferrable, (type(None), Deferrable)): raise ValueError( "ExclusionConstraint.deferrable must be a Deferrable instance." diff --git a/tests/postgres_tests/test_constraints.py b/tests/postgres_tests/test_constraints.py index 844c04cd6d8..5084f116ab4 100644 --- a/tests/postgres_tests/test_constraints.py +++ b/tests/postgres_tests/test_constraints.py @@ -312,16 +312,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase): deferrable="invalid", ) - def test_deferrable_with_condition(self): - msg = "ExclusionConstraint with conditions cannot be deferred." - with self.assertRaisesMessage(ValueError, msg): - ExclusionConstraint( - name="exclude_invalid_condition", - expressions=[(F("datespan"), RangeOperators.OVERLAPS)], - condition=Q(cancelled=False), - deferrable=Deferrable.DEFERRED, - ) - def test_invalid_include_type(self): msg = "ExclusionConstraint.include must be a list or tuple." with self.assertRaisesMessage(ValueError, msg): @@ -912,6 +902,39 @@ class ExclusionConstraintTests(PostgreSQLTestCase): RangesModel.objects.create(ints=(10, 19)) RangesModel.objects.create(ints=(51, 60)) + def test_range_adjacent_initially_deferred_with_condition(self): + constraint_name = "ints_adjacent_deferred_with_condition" + self.assertNotIn( + constraint_name, self.get_constraints(RangesModel._meta.db_table) + ) + constraint = ExclusionConstraint( + name=constraint_name, + expressions=[("ints", RangeOperators.ADJACENT_TO)], + condition=Q(ints__lt=(100, 200)), + 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)) + RangesModel.objects.create(ints=(20, 50)) + adjacent_range = RangesModel.objects.create(ints=(10, 20)) + # Constraint behavior can be changed with SET CONSTRAINTS. + with self.assertRaises(IntegrityError): + with transaction.atomic(), connection.cursor() as cursor: + quoted_name = connection.ops.quote_name(constraint_name) + cursor.execute(f"SET CONSTRAINTS {quoted_name} IMMEDIATE") + # Remove adjacent range before the end of transaction. + adjacent_range.delete() + RangesModel.objects.create(ints=(10, 19)) + RangesModel.objects.create(ints=(51, 60)) + # Add adjacent range that doesn't match the condition. + RangesModel.objects.create(ints=(200, 500)) + adjacent_range = RangesModel.objects.create(ints=(100, 200)) + # Constraint behavior can be changed with SET CONSTRAINTS. + with transaction.atomic(), connection.cursor() as cursor: + quoted_name = connection.ops.quote_name(constraint_name) + cursor.execute(f"SET CONSTRAINTS {quoted_name} IMMEDIATE") + def test_range_adjacent_gist_include(self): constraint_name = "ints_adjacent_gist_include" self.assertNotIn(