|
1 | 1 | import datetime
|
2 | 2 | from unittest import mock
|
3 | 3 |
|
4 |
| -from django.db import IntegrityError, connection, transaction |
| 4 | +from django.db import ( |
| 5 | + IntegrityError, NotSupportedError, connection, transaction, |
| 6 | +) |
5 | 7 | from django.db.models import CheckConstraint, Deferrable, F, Func, Q
|
| 8 | +from django.test import skipUnlessDBFeature |
6 | 9 | from django.utils import timezone
|
7 | 10 |
|
8 | 11 | from . import PostgreSQLTestCase
|
@@ -146,6 +149,25 @@ def test_deferrable_with_condition(self):
|
146 | 149 | deferrable=Deferrable.DEFERRED,
|
147 | 150 | )
|
148 | 151 |
|
| 152 | + def test_invalid_include_type(self): |
| 153 | + msg = 'ExclusionConstraint.include must be a list or tuple.' |
| 154 | + with self.assertRaisesMessage(ValueError, msg): |
| 155 | + ExclusionConstraint( |
| 156 | + name='exclude_invalid_include', |
| 157 | + expressions=[(F('datespan'), RangeOperators.OVERLAPS)], |
| 158 | + include='invalid', |
| 159 | + ) |
| 160 | + |
| 161 | + def test_invalid_include_index_type(self): |
| 162 | + msg = 'Covering exclusion constraints only support GiST indexes.' |
| 163 | + with self.assertRaisesMessage(ValueError, msg): |
| 164 | + ExclusionConstraint( |
| 165 | + name='exclude_invalid_index_type', |
| 166 | + expressions=[(F('datespan'), RangeOperators.OVERLAPS)], |
| 167 | + include=['cancelled'], |
| 168 | + index_type='spgist', |
| 169 | + ) |
| 170 | + |
149 | 171 | def test_repr(self):
|
150 | 172 | constraint = ExclusionConstraint(
|
151 | 173 | name='exclude_overlapping',
|
@@ -180,6 +202,16 @@ def test_repr(self):
|
180 | 202 | "<ExclusionConstraint: index_type=GIST, expressions=["
|
181 | 203 | "(F(datespan), '-|-')], deferrable=Deferrable.IMMEDIATE>",
|
182 | 204 | )
|
| 205 | + constraint = ExclusionConstraint( |
| 206 | + name='exclude_overlapping', |
| 207 | + expressions=[(F('datespan'), RangeOperators.ADJACENT_TO)], |
| 208 | + include=['cancelled', 'room'], |
| 209 | + ) |
| 210 | + self.assertEqual( |
| 211 | + repr(constraint), |
| 212 | + "<ExclusionConstraint: index_type=GIST, expressions=[" |
| 213 | + "(F(datespan), '-|-')], include=('cancelled', 'room')>", |
| 214 | + ) |
183 | 215 |
|
184 | 216 | def test_eq(self):
|
185 | 217 | constraint_1 = ExclusionConstraint(
|
@@ -218,14 +250,33 @@ def test_eq(self):
|
218 | 250 | ],
|
219 | 251 | deferrable=Deferrable.IMMEDIATE,
|
220 | 252 | )
|
| 253 | + constraint_6 = ExclusionConstraint( |
| 254 | + name='exclude_overlapping', |
| 255 | + expressions=[ |
| 256 | + ('datespan', RangeOperators.OVERLAPS), |
| 257 | + ('room', RangeOperators.EQUAL), |
| 258 | + ], |
| 259 | + deferrable=Deferrable.IMMEDIATE, |
| 260 | + include=['cancelled'], |
| 261 | + ) |
| 262 | + constraint_7 = ExclusionConstraint( |
| 263 | + name='exclude_overlapping', |
| 264 | + expressions=[ |
| 265 | + ('datespan', RangeOperators.OVERLAPS), |
| 266 | + ('room', RangeOperators.EQUAL), |
| 267 | + ], |
| 268 | + include=['cancelled'], |
| 269 | + ) |
221 | 270 | self.assertEqual(constraint_1, constraint_1)
|
222 | 271 | self.assertEqual(constraint_1, mock.ANY)
|
223 | 272 | self.assertNotEqual(constraint_1, constraint_2)
|
224 | 273 | self.assertNotEqual(constraint_1, constraint_3)
|
225 | 274 | self.assertNotEqual(constraint_1, constraint_4)
|
226 | 275 | self.assertNotEqual(constraint_2, constraint_3)
|
227 | 276 | self.assertNotEqual(constraint_2, constraint_4)
|
| 277 | + self.assertNotEqual(constraint_2, constraint_7) |
228 | 278 | self.assertNotEqual(constraint_4, constraint_5)
|
| 279 | + self.assertNotEqual(constraint_5, constraint_6) |
229 | 280 | self.assertNotEqual(constraint_1, object())
|
230 | 281 |
|
231 | 282 | def test_deconstruct(self):
|
@@ -286,6 +337,21 @@ def test_deconstruct_deferrable(self):
|
286 | 337 | 'deferrable': Deferrable.DEFERRED,
|
287 | 338 | })
|
288 | 339 |
|
| 340 | + def test_deconstruct_include(self): |
| 341 | + constraint = ExclusionConstraint( |
| 342 | + name='exclude_overlapping', |
| 343 | + expressions=[('datespan', RangeOperators.OVERLAPS)], |
| 344 | + include=['cancelled', 'room'], |
| 345 | + ) |
| 346 | + path, args, kwargs = constraint.deconstruct() |
| 347 | + self.assertEqual(path, 'django.contrib.postgres.constraints.ExclusionConstraint') |
| 348 | + self.assertEqual(args, ()) |
| 349 | + self.assertEqual(kwargs, { |
| 350 | + 'name': 'exclude_overlapping', |
| 351 | + 'expressions': [('datespan', RangeOperators.OVERLAPS)], |
| 352 | + 'include': ('cancelled', 'room'), |
| 353 | + }) |
| 354 | + |
289 | 355 | def _test_range_overlaps(self, constraint):
|
290 | 356 | # Create exclusion constraint.
|
291 | 357 | self.assertNotIn(constraint.name, self.get_constraints(HotelReservation._meta.db_table))
|
@@ -417,3 +483,66 @@ def test_range_adjacent_initially_deferred(self):
|
417 | 483 | adjacent_range.delete()
|
418 | 484 | RangesModel.objects.create(ints=(10, 19))
|
419 | 485 | RangesModel.objects.create(ints=(51, 60))
|
| 486 | + |
| 487 | + @skipUnlessDBFeature('supports_covering_gist_indexes') |
| 488 | + def test_range_adjacent_include(self): |
| 489 | + constraint_name = 'ints_adjacent_include' |
| 490 | + self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table)) |
| 491 | + constraint = ExclusionConstraint( |
| 492 | + name=constraint_name, |
| 493 | + expressions=[('ints', RangeOperators.ADJACENT_TO)], |
| 494 | + include=['decimals', 'ints'], |
| 495 | + index_type='gist', |
| 496 | + ) |
| 497 | + with connection.schema_editor() as editor: |
| 498 | + editor.add_constraint(RangesModel, constraint) |
| 499 | + self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table)) |
| 500 | + RangesModel.objects.create(ints=(20, 50)) |
| 501 | + with self.assertRaises(IntegrityError), transaction.atomic(): |
| 502 | + RangesModel.objects.create(ints=(10, 20)) |
| 503 | + RangesModel.objects.create(ints=(10, 19)) |
| 504 | + RangesModel.objects.create(ints=(51, 60)) |
| 505 | + |
| 506 | + @skipUnlessDBFeature('supports_covering_gist_indexes') |
| 507 | + def test_range_adjacent_include_condition(self): |
| 508 | + constraint_name = 'ints_adjacent_include_condition' |
| 509 | + self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table)) |
| 510 | + constraint = ExclusionConstraint( |
| 511 | + name=constraint_name, |
| 512 | + expressions=[('ints', RangeOperators.ADJACENT_TO)], |
| 513 | + include=['decimals'], |
| 514 | + condition=Q(id__gte=100), |
| 515 | + ) |
| 516 | + with connection.schema_editor() as editor: |
| 517 | + editor.add_constraint(RangesModel, constraint) |
| 518 | + self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table)) |
| 519 | + |
| 520 | + @skipUnlessDBFeature('supports_covering_gist_indexes') |
| 521 | + def test_range_adjacent_include_deferrable(self): |
| 522 | + constraint_name = 'ints_adjacent_include_deferrable' |
| 523 | + self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table)) |
| 524 | + constraint = ExclusionConstraint( |
| 525 | + name=constraint_name, |
| 526 | + expressions=[('ints', RangeOperators.ADJACENT_TO)], |
| 527 | + include=['decimals'], |
| 528 | + deferrable=Deferrable.DEFERRED, |
| 529 | + ) |
| 530 | + with connection.schema_editor() as editor: |
| 531 | + editor.add_constraint(RangesModel, constraint) |
| 532 | + self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table)) |
| 533 | + |
| 534 | + def test_include_not_supported(self): |
| 535 | + constraint_name = 'ints_adjacent_include_not_supported' |
| 536 | + constraint = ExclusionConstraint( |
| 537 | + name=constraint_name, |
| 538 | + expressions=[('ints', RangeOperators.ADJACENT_TO)], |
| 539 | + include=['id'], |
| 540 | + ) |
| 541 | + msg = 'Covering exclusion constraints requires PostgreSQL 12+.' |
| 542 | + with connection.schema_editor() as editor: |
| 543 | + with mock.patch( |
| 544 | + 'django.db.backends.postgresql.features.DatabaseFeatures.supports_covering_gist_indexes', |
| 545 | + False, |
| 546 | + ): |
| 547 | + with self.assertRaisesMessage(NotSupportedError, msg): |
| 548 | + editor.add_constraint(RangesModel, constraint) |
0 commit comments