diff options
| author | Ian Foote <python@ian.feete.org> | 2020-04-12 11:43:16 +0100 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2020-05-01 09:08:36 +0200 |
| commit | b4068bc65636cca6c2905aa8c40bea69bb0e4245 (patch) | |
| tree | 578a412d31851d728f303652619056e031d4219d /tests/postgres_tests/test_constraints.py | |
| parent | 5d2f5dd4cc5234a97d69af8740ba862c2051fd43 (diff) | |
Fixed #31455 -- Added support for deferrable exclusion constraints on PostgreSQL.
Diffstat (limited to 'tests/postgres_tests/test_constraints.py')
| -rw-r--r-- | tests/postgres_tests/test_constraints.py | 88 |
1 files changed, 87 insertions, 1 deletions
diff --git a/tests/postgres_tests/test_constraints.py b/tests/postgres_tests/test_constraints.py index ccdd7b818d..79c24c2d75 100644 --- a/tests/postgres_tests/test_constraints.py +++ b/tests/postgres_tests/test_constraints.py @@ -2,7 +2,7 @@ import datetime from unittest import mock from django.db import IntegrityError, connection, transaction -from django.db.models import CheckConstraint, F, Func, Q +from django.db.models import CheckConstraint, Deferrable, F, Func, Q from django.utils import timezone from . import PostgreSQLTestCase @@ -127,6 +127,25 @@ class ExclusionConstraintTests(PostgreSQLTestCase): expressions=empty_expressions, ) + def test_invalid_deferrable(self): + msg = 'ExclusionConstraint.deferrable must be a Deferrable instance.' + with self.assertRaisesMessage(ValueError, msg): + ExclusionConstraint( + name='exclude_invalid_deferrable', + expressions=[(F('datespan'), RangeOperators.OVERLAPS)], + 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_repr(self): constraint = ExclusionConstraint( name='exclude_overlapping', @@ -151,6 +170,16 @@ class ExclusionConstraintTests(PostgreSQLTestCase): "<ExclusionConstraint: index_type=SPGiST, expressions=[" "(F(datespan), '-|-')], condition=(AND: ('cancelled', False))>", ) + constraint = ExclusionConstraint( + name='exclude_overlapping', + expressions=[(F('datespan'), RangeOperators.ADJACENT_TO)], + deferrable=Deferrable.IMMEDIATE, + ) + self.assertEqual( + repr(constraint), + "<ExclusionConstraint: index_type=GIST, expressions=[" + "(F(datespan), '-|-')], deferrable=Deferrable.IMMEDIATE>", + ) def test_eq(self): constraint_1 = ExclusionConstraint( @@ -173,11 +202,30 @@ class ExclusionConstraintTests(PostgreSQLTestCase): expressions=[('datespan', RangeOperators.OVERLAPS)], condition=Q(cancelled=False), ) + constraint_4 = ExclusionConstraint( + name='exclude_overlapping', + expressions=[ + ('datespan', RangeOperators.OVERLAPS), + ('room', RangeOperators.EQUAL), + ], + deferrable=Deferrable.DEFERRED, + ) + constraint_5 = ExclusionConstraint( + name='exclude_overlapping', + expressions=[ + ('datespan', RangeOperators.OVERLAPS), + ('room', RangeOperators.EQUAL), + ], + deferrable=Deferrable.IMMEDIATE, + ) self.assertEqual(constraint_1, constraint_1) self.assertEqual(constraint_1, mock.ANY) self.assertNotEqual(constraint_1, constraint_2) self.assertNotEqual(constraint_1, constraint_3) + self.assertNotEqual(constraint_1, constraint_4) self.assertNotEqual(constraint_2, constraint_3) + self.assertNotEqual(constraint_2, constraint_4) + self.assertNotEqual(constraint_4, constraint_5) self.assertNotEqual(constraint_1, object()) def test_deconstruct(self): @@ -223,6 +271,21 @@ class ExclusionConstraintTests(PostgreSQLTestCase): 'condition': Q(cancelled=False), }) + def test_deconstruct_deferrable(self): + constraint = ExclusionConstraint( + name='exclude_overlapping', + expressions=[('datespan', RangeOperators.OVERLAPS)], + deferrable=Deferrable.DEFERRED, + ) + 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)], + 'deferrable': Deferrable.DEFERRED, + }) + def _test_range_overlaps(self, constraint): # Create exclusion constraint. self.assertNotIn(constraint.name, self.get_constraints(HotelReservation._meta.db_table)) @@ -327,3 +390,26 @@ class ExclusionConstraintTests(PostgreSQLTestCase): RangesModel.objects.create(ints=(10, 20)) RangesModel.objects.create(ints=(10, 19)) RangesModel.objects.create(ints=(51, 60)) + + def test_range_adjacent_initially_deferred(self): + constraint_name = 'ints_adjacent_deferred' + self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table)) + constraint = ExclusionConstraint( + name=constraint_name, + expressions=[('ints', RangeOperators.ADJACENT_TO)], + 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('SET CONSTRAINTS %s IMMEDIATE' % quoted_name) + # Remove adjacent range before the end of transaction. + adjacent_range.delete() + RangesModel.objects.create(ints=(10, 19)) + RangesModel.objects.create(ints=(51, 60)) |
