浏览代码

Fixed #34149 -- Allowed adding deferrable conditional exclusion constraints on PostgreSQL.

Márton Salomváry 2 年之前
父节点
当前提交
d6cbf39a1b
共有 2 个文件被更改,包括 33 次插入12 次删除
  1. 0 2
      django/contrib/postgres/constraints.py
  2. 33 10
      tests/postgres_tests/test_constraints.py

+ 0 - 2
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."

+ 33 - 10
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(