test_constraints.py 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088
  1. import datetime
  2. from unittest import mock
  3. from django.contrib.postgres.indexes import OpClass
  4. from django.db import (
  5. IntegrityError, NotSupportedError, connection, transaction,
  6. )
  7. from django.db.models import (
  8. CheckConstraint, Deferrable, F, Func, IntegerField, Q, UniqueConstraint,
  9. )
  10. from django.db.models.fields.json import KeyTextTransform
  11. from django.db.models.functions import Cast, Left, Lower
  12. from django.test import ignore_warnings, modify_settings, skipUnlessDBFeature
  13. from django.utils import timezone
  14. from django.utils.deprecation import RemovedInDjango50Warning
  15. from . import PostgreSQLTestCase
  16. from .models import (
  17. HotelReservation, IntegerArrayModel, RangesModel, Room, Scene,
  18. )
  19. try:
  20. from psycopg2.extras import DateRange, NumericRange
  21. from django.contrib.postgres.constraints import ExclusionConstraint
  22. from django.contrib.postgres.fields import (
  23. DateTimeRangeField, RangeBoundary, RangeOperators,
  24. )
  25. except ImportError:
  26. pass
  27. @modify_settings(INSTALLED_APPS={'append': 'django.contrib.postgres'})
  28. class SchemaTests(PostgreSQLTestCase):
  29. get_opclass_query = '''
  30. SELECT opcname, c.relname FROM pg_opclass AS oc
  31. JOIN pg_index as i on oc.oid = ANY(i.indclass)
  32. JOIN pg_class as c on c.oid = i.indexrelid
  33. WHERE c.relname = %s
  34. '''
  35. def get_constraints(self, table):
  36. """Get the constraints on the table using a new cursor."""
  37. with connection.cursor() as cursor:
  38. return connection.introspection.get_constraints(cursor, table)
  39. def test_check_constraint_range_value(self):
  40. constraint_name = 'ints_between'
  41. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  42. constraint = CheckConstraint(
  43. check=Q(ints__contained_by=NumericRange(10, 30)),
  44. name=constraint_name,
  45. )
  46. with connection.schema_editor() as editor:
  47. editor.add_constraint(RangesModel, constraint)
  48. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  49. with self.assertRaises(IntegrityError), transaction.atomic():
  50. RangesModel.objects.create(ints=(20, 50))
  51. RangesModel.objects.create(ints=(10, 30))
  52. def test_check_constraint_daterange_contains(self):
  53. constraint_name = 'dates_contains'
  54. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  55. constraint = CheckConstraint(
  56. check=Q(dates__contains=F('dates_inner')),
  57. name=constraint_name,
  58. )
  59. with connection.schema_editor() as editor:
  60. editor.add_constraint(RangesModel, constraint)
  61. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  62. date_1 = datetime.date(2016, 1, 1)
  63. date_2 = datetime.date(2016, 1, 4)
  64. with self.assertRaises(IntegrityError), transaction.atomic():
  65. RangesModel.objects.create(
  66. dates=(date_1, date_2),
  67. dates_inner=(date_1, date_2.replace(day=5)),
  68. )
  69. RangesModel.objects.create(
  70. dates=(date_1, date_2),
  71. dates_inner=(date_1, date_2),
  72. )
  73. def test_check_constraint_datetimerange_contains(self):
  74. constraint_name = 'timestamps_contains'
  75. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  76. constraint = CheckConstraint(
  77. check=Q(timestamps__contains=F('timestamps_inner')),
  78. name=constraint_name,
  79. )
  80. with connection.schema_editor() as editor:
  81. editor.add_constraint(RangesModel, constraint)
  82. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  83. datetime_1 = datetime.datetime(2016, 1, 1)
  84. datetime_2 = datetime.datetime(2016, 1, 2, 12)
  85. with self.assertRaises(IntegrityError), transaction.atomic():
  86. RangesModel.objects.create(
  87. timestamps=(datetime_1, datetime_2),
  88. timestamps_inner=(datetime_1, datetime_2.replace(hour=13)),
  89. )
  90. RangesModel.objects.create(
  91. timestamps=(datetime_1, datetime_2),
  92. timestamps_inner=(datetime_1, datetime_2),
  93. )
  94. def test_opclass(self):
  95. constraint = UniqueConstraint(
  96. name='test_opclass',
  97. fields=['scene'],
  98. opclasses=['varchar_pattern_ops'],
  99. )
  100. with connection.schema_editor() as editor:
  101. editor.add_constraint(Scene, constraint)
  102. self.assertIn(constraint.name, self.get_constraints(Scene._meta.db_table))
  103. with editor.connection.cursor() as cursor:
  104. cursor.execute(self.get_opclass_query, [constraint.name])
  105. self.assertEqual(
  106. cursor.fetchall(),
  107. [('varchar_pattern_ops', constraint.name)],
  108. )
  109. # Drop the constraint.
  110. with connection.schema_editor() as editor:
  111. editor.remove_constraint(Scene, constraint)
  112. self.assertNotIn(constraint.name, self.get_constraints(Scene._meta.db_table))
  113. def test_opclass_multiple_columns(self):
  114. constraint = UniqueConstraint(
  115. name='test_opclass_multiple',
  116. fields=['scene', 'setting'],
  117. opclasses=['varchar_pattern_ops', 'text_pattern_ops'],
  118. )
  119. with connection.schema_editor() as editor:
  120. editor.add_constraint(Scene, constraint)
  121. with editor.connection.cursor() as cursor:
  122. cursor.execute(self.get_opclass_query, [constraint.name])
  123. expected_opclasses = (
  124. ('varchar_pattern_ops', constraint.name),
  125. ('text_pattern_ops', constraint.name),
  126. )
  127. self.assertCountEqual(cursor.fetchall(), expected_opclasses)
  128. def test_opclass_partial(self):
  129. constraint = UniqueConstraint(
  130. name='test_opclass_partial',
  131. fields=['scene'],
  132. opclasses=['varchar_pattern_ops'],
  133. condition=Q(setting__contains="Sir Bedemir's Castle"),
  134. )
  135. with connection.schema_editor() as editor:
  136. editor.add_constraint(Scene, constraint)
  137. with editor.connection.cursor() as cursor:
  138. cursor.execute(self.get_opclass_query, [constraint.name])
  139. self.assertCountEqual(
  140. cursor.fetchall(),
  141. [('varchar_pattern_ops', constraint.name)],
  142. )
  143. @skipUnlessDBFeature('supports_covering_indexes')
  144. def test_opclass_include(self):
  145. constraint = UniqueConstraint(
  146. name='test_opclass_include',
  147. fields=['scene'],
  148. opclasses=['varchar_pattern_ops'],
  149. include=['setting'],
  150. )
  151. with connection.schema_editor() as editor:
  152. editor.add_constraint(Scene, constraint)
  153. with editor.connection.cursor() as cursor:
  154. cursor.execute(self.get_opclass_query, [constraint.name])
  155. self.assertCountEqual(
  156. cursor.fetchall(),
  157. [('varchar_pattern_ops', constraint.name)],
  158. )
  159. @skipUnlessDBFeature('supports_expression_indexes')
  160. def test_opclass_func(self):
  161. constraint = UniqueConstraint(
  162. OpClass(Lower('scene'), name='text_pattern_ops'),
  163. name='test_opclass_func',
  164. )
  165. with connection.schema_editor() as editor:
  166. editor.add_constraint(Scene, constraint)
  167. constraints = self.get_constraints(Scene._meta.db_table)
  168. self.assertIs(constraints[constraint.name]['unique'], True)
  169. self.assertIn(constraint.name, constraints)
  170. with editor.connection.cursor() as cursor:
  171. cursor.execute(self.get_opclass_query, [constraint.name])
  172. self.assertEqual(
  173. cursor.fetchall(),
  174. [('text_pattern_ops', constraint.name)],
  175. )
  176. Scene.objects.create(scene='Scene 10', setting='The dark forest of Ewing')
  177. with self.assertRaises(IntegrityError), transaction.atomic():
  178. Scene.objects.create(scene='ScEnE 10', setting="Sir Bedemir's Castle")
  179. Scene.objects.create(scene='Scene 5', setting="Sir Bedemir's Castle")
  180. # Drop the constraint.
  181. with connection.schema_editor() as editor:
  182. editor.remove_constraint(Scene, constraint)
  183. self.assertNotIn(constraint.name, self.get_constraints(Scene._meta.db_table))
  184. Scene.objects.create(scene='ScEnE 10', setting="Sir Bedemir's Castle")
  185. @modify_settings(INSTALLED_APPS={'append': 'django.contrib.postgres'})
  186. class ExclusionConstraintTests(PostgreSQLTestCase):
  187. def get_constraints(self, table):
  188. """Get the constraints on the table using a new cursor."""
  189. with connection.cursor() as cursor:
  190. return connection.introspection.get_constraints(cursor, table)
  191. def test_invalid_condition(self):
  192. msg = 'ExclusionConstraint.condition must be a Q instance.'
  193. with self.assertRaisesMessage(ValueError, msg):
  194. ExclusionConstraint(
  195. index_type='GIST',
  196. name='exclude_invalid_condition',
  197. expressions=[(F('datespan'), RangeOperators.OVERLAPS)],
  198. condition=F('invalid'),
  199. )
  200. def test_invalid_index_type(self):
  201. msg = 'Exclusion constraints only support GiST or SP-GiST indexes.'
  202. with self.assertRaisesMessage(ValueError, msg):
  203. ExclusionConstraint(
  204. index_type='gin',
  205. name='exclude_invalid_index_type',
  206. expressions=[(F('datespan'), RangeOperators.OVERLAPS)],
  207. )
  208. def test_invalid_expressions(self):
  209. msg = 'The expressions must be a list of 2-tuples.'
  210. for expressions in (['foo'], [('foo')], [('foo_1', 'foo_2', 'foo_3')]):
  211. with self.subTest(expressions), self.assertRaisesMessage(ValueError, msg):
  212. ExclusionConstraint(
  213. index_type='GIST',
  214. name='exclude_invalid_expressions',
  215. expressions=expressions,
  216. )
  217. def test_empty_expressions(self):
  218. msg = 'At least one expression is required to define an exclusion constraint.'
  219. for empty_expressions in (None, []):
  220. with self.subTest(empty_expressions), self.assertRaisesMessage(ValueError, msg):
  221. ExclusionConstraint(
  222. index_type='GIST',
  223. name='exclude_empty_expressions',
  224. expressions=empty_expressions,
  225. )
  226. def test_invalid_deferrable(self):
  227. msg = 'ExclusionConstraint.deferrable must be a Deferrable instance.'
  228. with self.assertRaisesMessage(ValueError, msg):
  229. ExclusionConstraint(
  230. name='exclude_invalid_deferrable',
  231. expressions=[(F('datespan'), RangeOperators.OVERLAPS)],
  232. deferrable='invalid',
  233. )
  234. def test_deferrable_with_condition(self):
  235. msg = 'ExclusionConstraint with conditions cannot be deferred.'
  236. with self.assertRaisesMessage(ValueError, msg):
  237. ExclusionConstraint(
  238. name='exclude_invalid_condition',
  239. expressions=[(F('datespan'), RangeOperators.OVERLAPS)],
  240. condition=Q(cancelled=False),
  241. deferrable=Deferrable.DEFERRED,
  242. )
  243. def test_invalid_include_type(self):
  244. msg = 'ExclusionConstraint.include must be a list or tuple.'
  245. with self.assertRaisesMessage(ValueError, msg):
  246. ExclusionConstraint(
  247. name='exclude_invalid_include',
  248. expressions=[(F('datespan'), RangeOperators.OVERLAPS)],
  249. include='invalid',
  250. )
  251. @ignore_warnings(category=RemovedInDjango50Warning)
  252. def test_invalid_opclasses_type(self):
  253. msg = 'ExclusionConstraint.opclasses must be a list or tuple.'
  254. with self.assertRaisesMessage(ValueError, msg):
  255. ExclusionConstraint(
  256. name='exclude_invalid_opclasses',
  257. expressions=[(F('datespan'), RangeOperators.OVERLAPS)],
  258. opclasses='invalid',
  259. )
  260. @ignore_warnings(category=RemovedInDjango50Warning)
  261. def test_opclasses_and_expressions_same_length(self):
  262. msg = (
  263. 'ExclusionConstraint.expressions and '
  264. 'ExclusionConstraint.opclasses must have the same number of '
  265. 'elements.'
  266. )
  267. with self.assertRaisesMessage(ValueError, msg):
  268. ExclusionConstraint(
  269. name='exclude_invalid_expressions_opclasses_length',
  270. expressions=[(F('datespan'), RangeOperators.OVERLAPS)],
  271. opclasses=['foo', 'bar'],
  272. )
  273. def test_repr(self):
  274. constraint = ExclusionConstraint(
  275. name='exclude_overlapping',
  276. expressions=[
  277. (F('datespan'), RangeOperators.OVERLAPS),
  278. (F('room'), RangeOperators.EQUAL),
  279. ],
  280. )
  281. self.assertEqual(
  282. repr(constraint),
  283. "<ExclusionConstraint: index_type='GIST' expressions=["
  284. "(F(datespan), '&&'), (F(room), '=')] name='exclude_overlapping'>",
  285. )
  286. constraint = ExclusionConstraint(
  287. name='exclude_overlapping',
  288. expressions=[(F('datespan'), RangeOperators.ADJACENT_TO)],
  289. condition=Q(cancelled=False),
  290. index_type='SPGiST',
  291. )
  292. self.assertEqual(
  293. repr(constraint),
  294. "<ExclusionConstraint: index_type='SPGiST' expressions=["
  295. "(F(datespan), '-|-')] name='exclude_overlapping' "
  296. "condition=(AND: ('cancelled', False))>",
  297. )
  298. constraint = ExclusionConstraint(
  299. name='exclude_overlapping',
  300. expressions=[(F('datespan'), RangeOperators.ADJACENT_TO)],
  301. deferrable=Deferrable.IMMEDIATE,
  302. )
  303. self.assertEqual(
  304. repr(constraint),
  305. "<ExclusionConstraint: index_type='GIST' expressions=["
  306. "(F(datespan), '-|-')] name='exclude_overlapping' "
  307. "deferrable=Deferrable.IMMEDIATE>",
  308. )
  309. constraint = ExclusionConstraint(
  310. name='exclude_overlapping',
  311. expressions=[(F('datespan'), RangeOperators.ADJACENT_TO)],
  312. include=['cancelled', 'room'],
  313. )
  314. self.assertEqual(
  315. repr(constraint),
  316. "<ExclusionConstraint: index_type='GIST' expressions=["
  317. "(F(datespan), '-|-')] name='exclude_overlapping' "
  318. "include=('cancelled', 'room')>",
  319. )
  320. constraint = ExclusionConstraint(
  321. name='exclude_overlapping',
  322. expressions=[
  323. (OpClass('datespan', name='range_ops'), RangeOperators.ADJACENT_TO),
  324. ],
  325. )
  326. self.assertEqual(
  327. repr(constraint),
  328. "<ExclusionConstraint: index_type='GIST' expressions=["
  329. "(OpClass(F(datespan), name=range_ops), '-|-')] "
  330. "name='exclude_overlapping'>",
  331. )
  332. def test_eq(self):
  333. constraint_1 = ExclusionConstraint(
  334. name='exclude_overlapping',
  335. expressions=[
  336. (F('datespan'), RangeOperators.OVERLAPS),
  337. (F('room'), RangeOperators.EQUAL),
  338. ],
  339. condition=Q(cancelled=False),
  340. )
  341. constraint_2 = ExclusionConstraint(
  342. name='exclude_overlapping',
  343. expressions=[
  344. ('datespan', RangeOperators.OVERLAPS),
  345. ('room', RangeOperators.EQUAL),
  346. ],
  347. )
  348. constraint_3 = ExclusionConstraint(
  349. name='exclude_overlapping',
  350. expressions=[('datespan', RangeOperators.OVERLAPS)],
  351. condition=Q(cancelled=False),
  352. )
  353. constraint_4 = ExclusionConstraint(
  354. name='exclude_overlapping',
  355. expressions=[
  356. ('datespan', RangeOperators.OVERLAPS),
  357. ('room', RangeOperators.EQUAL),
  358. ],
  359. deferrable=Deferrable.DEFERRED,
  360. )
  361. constraint_5 = ExclusionConstraint(
  362. name='exclude_overlapping',
  363. expressions=[
  364. ('datespan', RangeOperators.OVERLAPS),
  365. ('room', RangeOperators.EQUAL),
  366. ],
  367. deferrable=Deferrable.IMMEDIATE,
  368. )
  369. constraint_6 = ExclusionConstraint(
  370. name='exclude_overlapping',
  371. expressions=[
  372. ('datespan', RangeOperators.OVERLAPS),
  373. ('room', RangeOperators.EQUAL),
  374. ],
  375. deferrable=Deferrable.IMMEDIATE,
  376. include=['cancelled'],
  377. )
  378. constraint_7 = ExclusionConstraint(
  379. name='exclude_overlapping',
  380. expressions=[
  381. ('datespan', RangeOperators.OVERLAPS),
  382. ('room', RangeOperators.EQUAL),
  383. ],
  384. include=['cancelled'],
  385. )
  386. with ignore_warnings(category=RemovedInDjango50Warning):
  387. constraint_8 = ExclusionConstraint(
  388. name='exclude_overlapping',
  389. expressions=[
  390. ('datespan', RangeOperators.OVERLAPS),
  391. ('room', RangeOperators.EQUAL),
  392. ],
  393. include=['cancelled'],
  394. opclasses=['range_ops', 'range_ops']
  395. )
  396. constraint_9 = ExclusionConstraint(
  397. name='exclude_overlapping',
  398. expressions=[
  399. ('datespan', RangeOperators.OVERLAPS),
  400. ('room', RangeOperators.EQUAL),
  401. ],
  402. opclasses=['range_ops', 'range_ops']
  403. )
  404. self.assertNotEqual(constraint_2, constraint_9)
  405. self.assertNotEqual(constraint_7, constraint_8)
  406. self.assertEqual(constraint_1, constraint_1)
  407. self.assertEqual(constraint_1, mock.ANY)
  408. self.assertNotEqual(constraint_1, constraint_2)
  409. self.assertNotEqual(constraint_1, constraint_3)
  410. self.assertNotEqual(constraint_1, constraint_4)
  411. self.assertNotEqual(constraint_2, constraint_3)
  412. self.assertNotEqual(constraint_2, constraint_4)
  413. self.assertNotEqual(constraint_2, constraint_7)
  414. self.assertNotEqual(constraint_4, constraint_5)
  415. self.assertNotEqual(constraint_5, constraint_6)
  416. self.assertNotEqual(constraint_1, object())
  417. def test_deconstruct(self):
  418. constraint = ExclusionConstraint(
  419. name='exclude_overlapping',
  420. expressions=[('datespan', RangeOperators.OVERLAPS), ('room', RangeOperators.EQUAL)],
  421. )
  422. path, args, kwargs = constraint.deconstruct()
  423. self.assertEqual(path, 'django.contrib.postgres.constraints.ExclusionConstraint')
  424. self.assertEqual(args, ())
  425. self.assertEqual(kwargs, {
  426. 'name': 'exclude_overlapping',
  427. 'expressions': [('datespan', RangeOperators.OVERLAPS), ('room', RangeOperators.EQUAL)],
  428. })
  429. def test_deconstruct_index_type(self):
  430. constraint = ExclusionConstraint(
  431. name='exclude_overlapping',
  432. index_type='SPGIST',
  433. expressions=[('datespan', RangeOperators.OVERLAPS), ('room', RangeOperators.EQUAL)],
  434. )
  435. path, args, kwargs = constraint.deconstruct()
  436. self.assertEqual(path, 'django.contrib.postgres.constraints.ExclusionConstraint')
  437. self.assertEqual(args, ())
  438. self.assertEqual(kwargs, {
  439. 'name': 'exclude_overlapping',
  440. 'index_type': 'SPGIST',
  441. 'expressions': [('datespan', RangeOperators.OVERLAPS), ('room', RangeOperators.EQUAL)],
  442. })
  443. def test_deconstruct_condition(self):
  444. constraint = ExclusionConstraint(
  445. name='exclude_overlapping',
  446. expressions=[('datespan', RangeOperators.OVERLAPS), ('room', RangeOperators.EQUAL)],
  447. condition=Q(cancelled=False),
  448. )
  449. path, args, kwargs = constraint.deconstruct()
  450. self.assertEqual(path, 'django.contrib.postgres.constraints.ExclusionConstraint')
  451. self.assertEqual(args, ())
  452. self.assertEqual(kwargs, {
  453. 'name': 'exclude_overlapping',
  454. 'expressions': [('datespan', RangeOperators.OVERLAPS), ('room', RangeOperators.EQUAL)],
  455. 'condition': Q(cancelled=False),
  456. })
  457. def test_deconstruct_deferrable(self):
  458. constraint = ExclusionConstraint(
  459. name='exclude_overlapping',
  460. expressions=[('datespan', RangeOperators.OVERLAPS)],
  461. deferrable=Deferrable.DEFERRED,
  462. )
  463. path, args, kwargs = constraint.deconstruct()
  464. self.assertEqual(path, 'django.contrib.postgres.constraints.ExclusionConstraint')
  465. self.assertEqual(args, ())
  466. self.assertEqual(kwargs, {
  467. 'name': 'exclude_overlapping',
  468. 'expressions': [('datespan', RangeOperators.OVERLAPS)],
  469. 'deferrable': Deferrable.DEFERRED,
  470. })
  471. def test_deconstruct_include(self):
  472. constraint = ExclusionConstraint(
  473. name='exclude_overlapping',
  474. expressions=[('datespan', RangeOperators.OVERLAPS)],
  475. include=['cancelled', 'room'],
  476. )
  477. path, args, kwargs = constraint.deconstruct()
  478. self.assertEqual(path, 'django.contrib.postgres.constraints.ExclusionConstraint')
  479. self.assertEqual(args, ())
  480. self.assertEqual(kwargs, {
  481. 'name': 'exclude_overlapping',
  482. 'expressions': [('datespan', RangeOperators.OVERLAPS)],
  483. 'include': ('cancelled', 'room'),
  484. })
  485. @ignore_warnings(category=RemovedInDjango50Warning)
  486. def test_deconstruct_opclasses(self):
  487. constraint = ExclusionConstraint(
  488. name='exclude_overlapping',
  489. expressions=[('datespan', RangeOperators.OVERLAPS)],
  490. opclasses=['range_ops'],
  491. )
  492. path, args, kwargs = constraint.deconstruct()
  493. self.assertEqual(path, 'django.contrib.postgres.constraints.ExclusionConstraint')
  494. self.assertEqual(args, ())
  495. self.assertEqual(kwargs, {
  496. 'name': 'exclude_overlapping',
  497. 'expressions': [('datespan', RangeOperators.OVERLAPS)],
  498. 'opclasses': ['range_ops'],
  499. })
  500. def _test_range_overlaps(self, constraint):
  501. # Create exclusion constraint.
  502. self.assertNotIn(constraint.name, self.get_constraints(HotelReservation._meta.db_table))
  503. with connection.schema_editor() as editor:
  504. editor.add_constraint(HotelReservation, constraint)
  505. self.assertIn(constraint.name, self.get_constraints(HotelReservation._meta.db_table))
  506. # Add initial reservations.
  507. room101 = Room.objects.create(number=101)
  508. room102 = Room.objects.create(number=102)
  509. datetimes = [
  510. timezone.datetime(2018, 6, 20),
  511. timezone.datetime(2018, 6, 24),
  512. timezone.datetime(2018, 6, 26),
  513. timezone.datetime(2018, 6, 28),
  514. timezone.datetime(2018, 6, 29),
  515. ]
  516. HotelReservation.objects.create(
  517. datespan=DateRange(datetimes[0].date(), datetimes[1].date()),
  518. start=datetimes[0],
  519. end=datetimes[1],
  520. room=room102,
  521. )
  522. HotelReservation.objects.create(
  523. datespan=DateRange(datetimes[1].date(), datetimes[3].date()),
  524. start=datetimes[1],
  525. end=datetimes[3],
  526. room=room102,
  527. )
  528. # Overlap dates.
  529. with self.assertRaises(IntegrityError), transaction.atomic():
  530. reservation = HotelReservation(
  531. datespan=(datetimes[1].date(), datetimes[2].date()),
  532. start=datetimes[1],
  533. end=datetimes[2],
  534. room=room102,
  535. )
  536. reservation.save()
  537. # Valid range.
  538. HotelReservation.objects.bulk_create([
  539. # Other room.
  540. HotelReservation(
  541. datespan=(datetimes[1].date(), datetimes[2].date()),
  542. start=datetimes[1],
  543. end=datetimes[2],
  544. room=room101,
  545. ),
  546. # Cancelled reservation.
  547. HotelReservation(
  548. datespan=(datetimes[1].date(), datetimes[1].date()),
  549. start=datetimes[1],
  550. end=datetimes[2],
  551. room=room102,
  552. cancelled=True,
  553. ),
  554. # Other adjacent dates.
  555. HotelReservation(
  556. datespan=(datetimes[3].date(), datetimes[4].date()),
  557. start=datetimes[3],
  558. end=datetimes[4],
  559. room=room102,
  560. ),
  561. ])
  562. @ignore_warnings(category=RemovedInDjango50Warning)
  563. def test_range_overlaps_custom_opclasses(self):
  564. class TsTzRange(Func):
  565. function = 'TSTZRANGE'
  566. output_field = DateTimeRangeField()
  567. constraint = ExclusionConstraint(
  568. name='exclude_overlapping_reservations_custom',
  569. expressions=[
  570. (TsTzRange('start', 'end', RangeBoundary()), RangeOperators.OVERLAPS),
  571. ('room', RangeOperators.EQUAL)
  572. ],
  573. condition=Q(cancelled=False),
  574. opclasses=['range_ops', 'gist_int4_ops'],
  575. )
  576. self._test_range_overlaps(constraint)
  577. def test_range_overlaps_custom(self):
  578. class TsTzRange(Func):
  579. function = 'TSTZRANGE'
  580. output_field = DateTimeRangeField()
  581. constraint = ExclusionConstraint(
  582. name='exclude_overlapping_reservations_custom_opclass',
  583. expressions=[
  584. (
  585. OpClass(TsTzRange('start', 'end', RangeBoundary()), 'range_ops'),
  586. RangeOperators.OVERLAPS,
  587. ),
  588. (OpClass('room', 'gist_int4_ops'), RangeOperators.EQUAL),
  589. ],
  590. condition=Q(cancelled=False),
  591. )
  592. self._test_range_overlaps(constraint)
  593. def test_range_overlaps(self):
  594. constraint = ExclusionConstraint(
  595. name='exclude_overlapping_reservations',
  596. expressions=[
  597. (F('datespan'), RangeOperators.OVERLAPS),
  598. ('room', RangeOperators.EQUAL)
  599. ],
  600. condition=Q(cancelled=False),
  601. )
  602. self._test_range_overlaps(constraint)
  603. def test_range_adjacent(self):
  604. constraint_name = 'ints_adjacent'
  605. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  606. constraint = ExclusionConstraint(
  607. name=constraint_name,
  608. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  609. )
  610. with connection.schema_editor() as editor:
  611. editor.add_constraint(RangesModel, constraint)
  612. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  613. RangesModel.objects.create(ints=(20, 50))
  614. with self.assertRaises(IntegrityError), transaction.atomic():
  615. RangesModel.objects.create(ints=(10, 20))
  616. RangesModel.objects.create(ints=(10, 19))
  617. RangesModel.objects.create(ints=(51, 60))
  618. # Drop the constraint.
  619. with connection.schema_editor() as editor:
  620. editor.remove_constraint(RangesModel, constraint)
  621. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  622. def test_expressions_with_params(self):
  623. constraint_name = 'scene_left_equal'
  624. self.assertNotIn(constraint_name, self.get_constraints(Scene._meta.db_table))
  625. constraint = ExclusionConstraint(
  626. name=constraint_name,
  627. expressions=[(Left('scene', 4), RangeOperators.EQUAL)],
  628. )
  629. with connection.schema_editor() as editor:
  630. editor.add_constraint(Scene, constraint)
  631. self.assertIn(constraint_name, self.get_constraints(Scene._meta.db_table))
  632. def test_expressions_with_key_transform(self):
  633. constraint_name = 'exclude_overlapping_reservations_smoking'
  634. constraint = ExclusionConstraint(
  635. name=constraint_name,
  636. expressions=[
  637. (F('datespan'), RangeOperators.OVERLAPS),
  638. (KeyTextTransform('smoking', 'requirements'), RangeOperators.EQUAL),
  639. ],
  640. )
  641. with connection.schema_editor() as editor:
  642. editor.add_constraint(HotelReservation, constraint)
  643. self.assertIn(
  644. constraint_name,
  645. self.get_constraints(HotelReservation._meta.db_table),
  646. )
  647. def test_index_transform(self):
  648. constraint_name = 'first_index_equal'
  649. constraint = ExclusionConstraint(
  650. name=constraint_name,
  651. expressions=[('field__0', RangeOperators.EQUAL)],
  652. )
  653. with connection.schema_editor() as editor:
  654. editor.add_constraint(IntegerArrayModel, constraint)
  655. self.assertIn(
  656. constraint_name,
  657. self.get_constraints(IntegerArrayModel._meta.db_table),
  658. )
  659. def test_range_adjacent_initially_deferred(self):
  660. constraint_name = 'ints_adjacent_deferred'
  661. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  662. constraint = ExclusionConstraint(
  663. name=constraint_name,
  664. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  665. deferrable=Deferrable.DEFERRED,
  666. )
  667. with connection.schema_editor() as editor:
  668. editor.add_constraint(RangesModel, constraint)
  669. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  670. RangesModel.objects.create(ints=(20, 50))
  671. adjacent_range = RangesModel.objects.create(ints=(10, 20))
  672. # Constraint behavior can be changed with SET CONSTRAINTS.
  673. with self.assertRaises(IntegrityError):
  674. with transaction.atomic(), connection.cursor() as cursor:
  675. quoted_name = connection.ops.quote_name(constraint_name)
  676. cursor.execute('SET CONSTRAINTS %s IMMEDIATE' % quoted_name)
  677. # Remove adjacent range before the end of transaction.
  678. adjacent_range.delete()
  679. RangesModel.objects.create(ints=(10, 19))
  680. RangesModel.objects.create(ints=(51, 60))
  681. @skipUnlessDBFeature('supports_covering_gist_indexes')
  682. def test_range_adjacent_gist_include(self):
  683. constraint_name = 'ints_adjacent_gist_include'
  684. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  685. constraint = ExclusionConstraint(
  686. name=constraint_name,
  687. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  688. index_type='gist',
  689. include=['decimals', 'ints'],
  690. )
  691. with connection.schema_editor() as editor:
  692. editor.add_constraint(RangesModel, constraint)
  693. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  694. RangesModel.objects.create(ints=(20, 50))
  695. with self.assertRaises(IntegrityError), transaction.atomic():
  696. RangesModel.objects.create(ints=(10, 20))
  697. RangesModel.objects.create(ints=(10, 19))
  698. RangesModel.objects.create(ints=(51, 60))
  699. @skipUnlessDBFeature('supports_covering_spgist_indexes')
  700. def test_range_adjacent_spgist_include(self):
  701. constraint_name = 'ints_adjacent_spgist_include'
  702. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  703. constraint = ExclusionConstraint(
  704. name=constraint_name,
  705. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  706. index_type='spgist',
  707. include=['decimals', 'ints'],
  708. )
  709. with connection.schema_editor() as editor:
  710. editor.add_constraint(RangesModel, constraint)
  711. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  712. RangesModel.objects.create(ints=(20, 50))
  713. with self.assertRaises(IntegrityError), transaction.atomic():
  714. RangesModel.objects.create(ints=(10, 20))
  715. RangesModel.objects.create(ints=(10, 19))
  716. RangesModel.objects.create(ints=(51, 60))
  717. @skipUnlessDBFeature('supports_covering_gist_indexes')
  718. def test_range_adjacent_gist_include_condition(self):
  719. constraint_name = 'ints_adjacent_gist_include_condition'
  720. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  721. constraint = ExclusionConstraint(
  722. name=constraint_name,
  723. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  724. index_type='gist',
  725. include=['decimals'],
  726. condition=Q(id__gte=100),
  727. )
  728. with connection.schema_editor() as editor:
  729. editor.add_constraint(RangesModel, constraint)
  730. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  731. @skipUnlessDBFeature('supports_covering_spgist_indexes')
  732. def test_range_adjacent_spgist_include_condition(self):
  733. constraint_name = 'ints_adjacent_spgist_include_condition'
  734. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  735. constraint = ExclusionConstraint(
  736. name=constraint_name,
  737. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  738. index_type='spgist',
  739. include=['decimals'],
  740. condition=Q(id__gte=100),
  741. )
  742. with connection.schema_editor() as editor:
  743. editor.add_constraint(RangesModel, constraint)
  744. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  745. @skipUnlessDBFeature('supports_covering_gist_indexes')
  746. def test_range_adjacent_gist_include_deferrable(self):
  747. constraint_name = 'ints_adjacent_gist_include_deferrable'
  748. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  749. constraint = ExclusionConstraint(
  750. name=constraint_name,
  751. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  752. index_type='gist',
  753. include=['decimals'],
  754. deferrable=Deferrable.DEFERRED,
  755. )
  756. with connection.schema_editor() as editor:
  757. editor.add_constraint(RangesModel, constraint)
  758. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  759. @skipUnlessDBFeature('supports_covering_spgist_indexes')
  760. def test_range_adjacent_spgist_include_deferrable(self):
  761. constraint_name = 'ints_adjacent_spgist_include_deferrable'
  762. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  763. constraint = ExclusionConstraint(
  764. name=constraint_name,
  765. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  766. index_type='spgist',
  767. include=['decimals'],
  768. deferrable=Deferrable.DEFERRED,
  769. )
  770. with connection.schema_editor() as editor:
  771. editor.add_constraint(RangesModel, constraint)
  772. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  773. def test_gist_include_not_supported(self):
  774. constraint_name = 'ints_adjacent_gist_include_not_supported'
  775. constraint = ExclusionConstraint(
  776. name=constraint_name,
  777. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  778. index_type='gist',
  779. include=['id'],
  780. )
  781. msg = (
  782. 'Covering exclusion constraints using a GiST index require '
  783. 'PostgreSQL 12+.'
  784. )
  785. with connection.schema_editor() as editor:
  786. with mock.patch(
  787. 'django.db.backends.postgresql.features.DatabaseFeatures.supports_covering_gist_indexes',
  788. False,
  789. ):
  790. with self.assertRaisesMessage(NotSupportedError, msg):
  791. editor.add_constraint(RangesModel, constraint)
  792. def test_spgist_include_not_supported(self):
  793. constraint_name = 'ints_adjacent_spgist_include_not_supported'
  794. constraint = ExclusionConstraint(
  795. name=constraint_name,
  796. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  797. index_type='spgist',
  798. include=['id'],
  799. )
  800. msg = (
  801. 'Covering exclusion constraints using an SP-GiST index require '
  802. 'PostgreSQL 14+.'
  803. )
  804. with connection.schema_editor() as editor:
  805. with mock.patch(
  806. 'django.db.backends.postgresql.features.DatabaseFeatures.'
  807. 'supports_covering_spgist_indexes',
  808. False,
  809. ):
  810. with self.assertRaisesMessage(NotSupportedError, msg):
  811. editor.add_constraint(RangesModel, constraint)
  812. def test_range_adjacent_opclass(self):
  813. constraint_name = 'ints_adjacent_opclass'
  814. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  815. constraint = ExclusionConstraint(
  816. name=constraint_name,
  817. expressions=[
  818. (OpClass('ints', name='range_ops'), RangeOperators.ADJACENT_TO),
  819. ],
  820. )
  821. with connection.schema_editor() as editor:
  822. editor.add_constraint(RangesModel, constraint)
  823. constraints = self.get_constraints(RangesModel._meta.db_table)
  824. self.assertIn(constraint_name, constraints)
  825. with editor.connection.cursor() as cursor:
  826. cursor.execute(SchemaTests.get_opclass_query, [constraint_name])
  827. self.assertEqual(
  828. cursor.fetchall(),
  829. [('range_ops', constraint_name)],
  830. )
  831. RangesModel.objects.create(ints=(20, 50))
  832. with self.assertRaises(IntegrityError), transaction.atomic():
  833. RangesModel.objects.create(ints=(10, 20))
  834. RangesModel.objects.create(ints=(10, 19))
  835. RangesModel.objects.create(ints=(51, 60))
  836. # Drop the constraint.
  837. with connection.schema_editor() as editor:
  838. editor.remove_constraint(RangesModel, constraint)
  839. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  840. def test_range_adjacent_opclass_condition(self):
  841. constraint_name = 'ints_adjacent_opclass_condition'
  842. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  843. constraint = ExclusionConstraint(
  844. name=constraint_name,
  845. expressions=[
  846. (OpClass('ints', name='range_ops'), RangeOperators.ADJACENT_TO),
  847. ],
  848. condition=Q(id__gte=100),
  849. )
  850. with connection.schema_editor() as editor:
  851. editor.add_constraint(RangesModel, constraint)
  852. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  853. def test_range_adjacent_opclass_deferrable(self):
  854. constraint_name = 'ints_adjacent_opclass_deferrable'
  855. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  856. constraint = ExclusionConstraint(
  857. name=constraint_name,
  858. expressions=[
  859. (OpClass('ints', name='range_ops'), RangeOperators.ADJACENT_TO),
  860. ],
  861. deferrable=Deferrable.DEFERRED,
  862. )
  863. with connection.schema_editor() as editor:
  864. editor.add_constraint(RangesModel, constraint)
  865. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  866. @skipUnlessDBFeature('supports_covering_gist_indexes')
  867. def test_range_adjacent_gist_opclass_include(self):
  868. constraint_name = 'ints_adjacent_gist_opclass_include'
  869. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  870. constraint = ExclusionConstraint(
  871. name=constraint_name,
  872. expressions=[
  873. (OpClass('ints', name='range_ops'), RangeOperators.ADJACENT_TO),
  874. ],
  875. index_type='gist',
  876. include=['decimals'],
  877. )
  878. with connection.schema_editor() as editor:
  879. editor.add_constraint(RangesModel, constraint)
  880. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  881. @skipUnlessDBFeature('supports_covering_spgist_indexes')
  882. def test_range_adjacent_spgist_opclass_include(self):
  883. constraint_name = 'ints_adjacent_spgist_opclass_include'
  884. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  885. constraint = ExclusionConstraint(
  886. name=constraint_name,
  887. expressions=[
  888. (OpClass('ints', name='range_ops'), RangeOperators.ADJACENT_TO),
  889. ],
  890. index_type='spgist',
  891. include=['decimals'],
  892. )
  893. with connection.schema_editor() as editor:
  894. editor.add_constraint(RangesModel, constraint)
  895. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  896. def test_range_equal_cast(self):
  897. constraint_name = 'exclusion_equal_room_cast'
  898. self.assertNotIn(constraint_name, self.get_constraints(Room._meta.db_table))
  899. constraint = ExclusionConstraint(
  900. name=constraint_name,
  901. expressions=[(Cast('number', IntegerField()), RangeOperators.EQUAL)],
  902. )
  903. with connection.schema_editor() as editor:
  904. editor.add_constraint(Room, constraint)
  905. self.assertIn(constraint_name, self.get_constraints(Room._meta.db_table))
  906. @modify_settings(INSTALLED_APPS={'append': 'django.contrib.postgres'})
  907. class ExclusionConstraintOpclassesDepracationTests(PostgreSQLTestCase):
  908. def get_constraints(self, table):
  909. """Get the constraints on the table using a new cursor."""
  910. with connection.cursor() as cursor:
  911. return connection.introspection.get_constraints(cursor, table)
  912. def test_warning(self):
  913. msg = (
  914. 'The opclasses argument is deprecated in favor of using '
  915. 'django.contrib.postgres.indexes.OpClass in '
  916. 'ExclusionConstraint.expressions.'
  917. )
  918. with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
  919. ExclusionConstraint(
  920. name='exclude_overlapping',
  921. expressions=[(F('datespan'), RangeOperators.ADJACENT_TO)],
  922. opclasses=['range_ops'],
  923. )
  924. @ignore_warnings(category=RemovedInDjango50Warning)
  925. def test_repr(self):
  926. constraint = ExclusionConstraint(
  927. name='exclude_overlapping',
  928. expressions=[(F('datespan'), RangeOperators.ADJACENT_TO)],
  929. opclasses=['range_ops'],
  930. )
  931. self.assertEqual(
  932. repr(constraint),
  933. "<ExclusionConstraint: index_type='GIST' expressions=["
  934. "(F(datespan), '-|-')] name='exclude_overlapping' "
  935. "opclasses=['range_ops']>",
  936. )
  937. @ignore_warnings(category=RemovedInDjango50Warning)
  938. def test_range_adjacent_opclasses(self):
  939. constraint_name = 'ints_adjacent_opclasses'
  940. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  941. constraint = ExclusionConstraint(
  942. name=constraint_name,
  943. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  944. opclasses=['range_ops'],
  945. )
  946. with connection.schema_editor() as editor:
  947. editor.add_constraint(RangesModel, constraint)
  948. constraints = self.get_constraints(RangesModel._meta.db_table)
  949. self.assertIn(constraint_name, constraints)
  950. with editor.connection.cursor() as cursor:
  951. cursor.execute(SchemaTests.get_opclass_query, [constraint.name])
  952. self.assertEqual(
  953. cursor.fetchall(),
  954. [('range_ops', constraint.name)],
  955. )
  956. RangesModel.objects.create(ints=(20, 50))
  957. with self.assertRaises(IntegrityError), transaction.atomic():
  958. RangesModel.objects.create(ints=(10, 20))
  959. RangesModel.objects.create(ints=(10, 19))
  960. RangesModel.objects.create(ints=(51, 60))
  961. # Drop the constraint.
  962. with connection.schema_editor() as editor:
  963. editor.remove_constraint(RangesModel, constraint)
  964. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  965. @ignore_warnings(category=RemovedInDjango50Warning)
  966. def test_range_adjacent_opclasses_condition(self):
  967. constraint_name = 'ints_adjacent_opclasses_condition'
  968. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  969. constraint = ExclusionConstraint(
  970. name=constraint_name,
  971. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  972. opclasses=['range_ops'],
  973. condition=Q(id__gte=100),
  974. )
  975. with connection.schema_editor() as editor:
  976. editor.add_constraint(RangesModel, constraint)
  977. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  978. @ignore_warnings(category=RemovedInDjango50Warning)
  979. def test_range_adjacent_opclasses_deferrable(self):
  980. constraint_name = 'ints_adjacent_opclasses_deferrable'
  981. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  982. constraint = ExclusionConstraint(
  983. name=constraint_name,
  984. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  985. opclasses=['range_ops'],
  986. deferrable=Deferrable.DEFERRED,
  987. )
  988. with connection.schema_editor() as editor:
  989. editor.add_constraint(RangesModel, constraint)
  990. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  991. @ignore_warnings(category=RemovedInDjango50Warning)
  992. @skipUnlessDBFeature('supports_covering_gist_indexes')
  993. def test_range_adjacent_gist_opclasses_include(self):
  994. constraint_name = 'ints_adjacent_gist_opclasses_include'
  995. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  996. constraint = ExclusionConstraint(
  997. name=constraint_name,
  998. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  999. index_type='gist',
  1000. opclasses=['range_ops'],
  1001. include=['decimals'],
  1002. )
  1003. with connection.schema_editor() as editor:
  1004. editor.add_constraint(RangesModel, constraint)
  1005. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  1006. @ignore_warnings(category=RemovedInDjango50Warning)
  1007. @skipUnlessDBFeature('supports_covering_spgist_indexes')
  1008. def test_range_adjacent_spgist_opclasses_include(self):
  1009. constraint_name = 'ints_adjacent_spgist_opclasses_include'
  1010. self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
  1011. constraint = ExclusionConstraint(
  1012. name=constraint_name,
  1013. expressions=[('ints', RangeOperators.ADJACENT_TO)],
  1014. index_type='spgist',
  1015. opclasses=['range_ops'],
  1016. include=['decimals'],
  1017. )
  1018. with connection.schema_editor() as editor:
  1019. editor.add_constraint(RangesModel, constraint)
  1020. self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))