tests.py 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231
  1. from unittest import mock
  2. from django.core.exceptions import ValidationError
  3. from django.db import IntegrityError, connection, models
  4. from django.db.models import F
  5. from django.db.models.constraints import BaseConstraint, UniqueConstraint
  6. from django.db.models.functions import Lower
  7. from django.db.transaction import atomic
  8. from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature
  9. from django.test.utils import ignore_warnings
  10. from django.utils.deprecation import RemovedInDjango60Warning
  11. from .models import (
  12. ChildModel,
  13. ChildUniqueConstraintProduct,
  14. JSONFieldModel,
  15. Product,
  16. UniqueConstraintConditionProduct,
  17. UniqueConstraintDeferrable,
  18. UniqueConstraintInclude,
  19. UniqueConstraintProduct,
  20. )
  21. def get_constraints(table):
  22. with connection.cursor() as cursor:
  23. return connection.introspection.get_constraints(cursor, table)
  24. class BaseConstraintTests(SimpleTestCase):
  25. def test_constraint_sql(self):
  26. c = BaseConstraint(name="name")
  27. msg = "This method must be implemented by a subclass."
  28. with self.assertRaisesMessage(NotImplementedError, msg):
  29. c.constraint_sql(None, None)
  30. def test_contains_expressions(self):
  31. c = BaseConstraint(name="name")
  32. self.assertIs(c.contains_expressions, False)
  33. def test_create_sql(self):
  34. c = BaseConstraint(name="name")
  35. msg = "This method must be implemented by a subclass."
  36. with self.assertRaisesMessage(NotImplementedError, msg):
  37. c.create_sql(None, None)
  38. def test_remove_sql(self):
  39. c = BaseConstraint(name="name")
  40. msg = "This method must be implemented by a subclass."
  41. with self.assertRaisesMessage(NotImplementedError, msg):
  42. c.remove_sql(None, None)
  43. def test_validate(self):
  44. c = BaseConstraint(name="name")
  45. msg = "This method must be implemented by a subclass."
  46. with self.assertRaisesMessage(NotImplementedError, msg):
  47. c.validate(None, None)
  48. def test_default_violation_error_message(self):
  49. c = BaseConstraint(name="name")
  50. self.assertEqual(
  51. c.get_violation_error_message(), "Constraint “name” is violated."
  52. )
  53. def test_custom_violation_error_message(self):
  54. c = BaseConstraint(
  55. name="base_name", violation_error_message="custom %(name)s message"
  56. )
  57. self.assertEqual(c.get_violation_error_message(), "custom base_name message")
  58. def test_custom_violation_error_message_clone(self):
  59. constraint = BaseConstraint(
  60. name="base_name",
  61. violation_error_message="custom %(name)s message",
  62. ).clone()
  63. self.assertEqual(
  64. constraint.get_violation_error_message(),
  65. "custom base_name message",
  66. )
  67. def test_custom_violation_code_message(self):
  68. c = BaseConstraint(name="base_name", violation_error_code="custom_code")
  69. self.assertEqual(c.violation_error_code, "custom_code")
  70. def test_deconstruction(self):
  71. constraint = BaseConstraint(
  72. name="base_name",
  73. violation_error_message="custom %(name)s message",
  74. violation_error_code="custom_code",
  75. )
  76. path, args, kwargs = constraint.deconstruct()
  77. self.assertEqual(path, "django.db.models.BaseConstraint")
  78. self.assertEqual(args, ())
  79. self.assertEqual(
  80. kwargs,
  81. {
  82. "name": "base_name",
  83. "violation_error_message": "custom %(name)s message",
  84. "violation_error_code": "custom_code",
  85. },
  86. )
  87. def test_deprecation(self):
  88. msg = "Passing positional arguments to BaseConstraint is deprecated."
  89. with self.assertRaisesMessage(RemovedInDjango60Warning, msg):
  90. BaseConstraint("name", "violation error message")
  91. def test_name_required(self):
  92. msg = (
  93. "BaseConstraint.__init__() missing 1 required keyword-only argument: 'name'"
  94. )
  95. with self.assertRaisesMessage(TypeError, msg):
  96. BaseConstraint()
  97. @ignore_warnings(category=RemovedInDjango60Warning)
  98. def test_positional_arguments(self):
  99. c = BaseConstraint("name", "custom %(name)s message")
  100. self.assertEqual(c.get_violation_error_message(), "custom name message")
  101. class CheckConstraintTests(TestCase):
  102. def test_eq(self):
  103. check1 = models.Q(price__gt=models.F("discounted_price"))
  104. check2 = models.Q(price__lt=models.F("discounted_price"))
  105. self.assertEqual(
  106. models.CheckConstraint(condition=check1, name="price"),
  107. models.CheckConstraint(condition=check1, name="price"),
  108. )
  109. self.assertEqual(
  110. models.CheckConstraint(condition=check1, name="price"), mock.ANY
  111. )
  112. self.assertNotEqual(
  113. models.CheckConstraint(condition=check1, name="price"),
  114. models.CheckConstraint(condition=check1, name="price2"),
  115. )
  116. self.assertNotEqual(
  117. models.CheckConstraint(condition=check1, name="price"),
  118. models.CheckConstraint(condition=check2, name="price"),
  119. )
  120. self.assertNotEqual(models.CheckConstraint(condition=check1, name="price"), 1)
  121. self.assertNotEqual(
  122. models.CheckConstraint(condition=check1, name="price"),
  123. models.CheckConstraint(
  124. condition=check1, name="price", violation_error_message="custom error"
  125. ),
  126. )
  127. self.assertNotEqual(
  128. models.CheckConstraint(
  129. condition=check1, name="price", violation_error_message="custom error"
  130. ),
  131. models.CheckConstraint(
  132. condition=check1,
  133. name="price",
  134. violation_error_message="other custom error",
  135. ),
  136. )
  137. self.assertEqual(
  138. models.CheckConstraint(
  139. condition=check1, name="price", violation_error_message="custom error"
  140. ),
  141. models.CheckConstraint(
  142. condition=check1, name="price", violation_error_message="custom error"
  143. ),
  144. )
  145. self.assertNotEqual(
  146. models.CheckConstraint(condition=check1, name="price"),
  147. models.CheckConstraint(
  148. condition=check1, name="price", violation_error_code="custom_code"
  149. ),
  150. )
  151. self.assertEqual(
  152. models.CheckConstraint(
  153. condition=check1, name="price", violation_error_code="custom_code"
  154. ),
  155. models.CheckConstraint(
  156. condition=check1, name="price", violation_error_code="custom_code"
  157. ),
  158. )
  159. def test_repr(self):
  160. constraint = models.CheckConstraint(
  161. condition=models.Q(price__gt=models.F("discounted_price")),
  162. name="price_gt_discounted_price",
  163. )
  164. self.assertEqual(
  165. repr(constraint),
  166. "<CheckConstraint: condition=(AND: ('price__gt', F(discounted_price))) "
  167. "name='price_gt_discounted_price'>",
  168. )
  169. def test_repr_with_violation_error_message(self):
  170. constraint = models.CheckConstraint(
  171. condition=models.Q(price__lt=1),
  172. name="price_lt_one",
  173. violation_error_message="More than 1",
  174. )
  175. self.assertEqual(
  176. repr(constraint),
  177. "<CheckConstraint: condition=(AND: ('price__lt', 1)) name='price_lt_one' "
  178. "violation_error_message='More than 1'>",
  179. )
  180. def test_repr_with_violation_error_code(self):
  181. constraint = models.CheckConstraint(
  182. condition=models.Q(price__lt=1),
  183. name="price_lt_one",
  184. violation_error_code="more_than_one",
  185. )
  186. self.assertEqual(
  187. repr(constraint),
  188. "<CheckConstraint: condition=(AND: ('price__lt', 1)) name='price_lt_one' "
  189. "violation_error_code='more_than_one'>",
  190. )
  191. def test_invalid_check_types(self):
  192. msg = "CheckConstraint.condition must be a Q instance or boolean expression."
  193. with self.assertRaisesMessage(TypeError, msg):
  194. models.CheckConstraint(condition=models.F("discounted_price"), name="check")
  195. def test_deconstruction(self):
  196. check = models.Q(price__gt=models.F("discounted_price"))
  197. name = "price_gt_discounted_price"
  198. constraint = models.CheckConstraint(condition=check, name=name)
  199. path, args, kwargs = constraint.deconstruct()
  200. self.assertEqual(path, "django.db.models.CheckConstraint")
  201. self.assertEqual(args, ())
  202. self.assertEqual(kwargs, {"condition": check, "name": name})
  203. @skipUnlessDBFeature("supports_table_check_constraints")
  204. def test_database_constraint(self):
  205. Product.objects.create(price=10, discounted_price=5)
  206. with self.assertRaises(IntegrityError):
  207. Product.objects.create(price=10, discounted_price=20)
  208. @skipUnlessDBFeature("supports_table_check_constraints")
  209. def test_database_constraint_unicode(self):
  210. Product.objects.create(price=10, discounted_price=5, unit="μg/mL")
  211. with self.assertRaises(IntegrityError):
  212. Product.objects.create(price=10, discounted_price=7, unit="l")
  213. @skipUnlessDBFeature(
  214. "supports_table_check_constraints", "can_introspect_check_constraints"
  215. )
  216. def test_name(self):
  217. constraints = get_constraints(Product._meta.db_table)
  218. for expected_name in (
  219. "price_gt_discounted_price",
  220. "constraints_product_price_gt_0",
  221. ):
  222. with self.subTest(expected_name):
  223. self.assertIn(expected_name, constraints)
  224. @skipUnlessDBFeature(
  225. "supports_table_check_constraints", "can_introspect_check_constraints"
  226. )
  227. def test_abstract_name(self):
  228. constraints = get_constraints(ChildModel._meta.db_table)
  229. self.assertIn("constraints_childmodel_adult", constraints)
  230. def test_validate(self):
  231. check = models.Q(price__gt=models.F("discounted_price"))
  232. constraint = models.CheckConstraint(condition=check, name="price")
  233. # Invalid product.
  234. invalid_product = Product(price=10, discounted_price=42)
  235. with self.assertRaises(ValidationError):
  236. constraint.validate(Product, invalid_product)
  237. with self.assertRaises(ValidationError):
  238. constraint.validate(Product, invalid_product, exclude={"unit"})
  239. # Fields used by the check constraint are excluded.
  240. constraint.validate(Product, invalid_product, exclude={"price"})
  241. constraint.validate(Product, invalid_product, exclude={"discounted_price"})
  242. constraint.validate(
  243. Product,
  244. invalid_product,
  245. exclude={"discounted_price", "price"},
  246. )
  247. # Valid product.
  248. constraint.validate(Product, Product(price=10, discounted_price=5))
  249. def test_validate_custom_error(self):
  250. check = models.Q(price__gt=models.F("discounted_price"))
  251. constraint = models.CheckConstraint(
  252. condition=check,
  253. name="price",
  254. violation_error_message="discount is fake",
  255. violation_error_code="fake_discount",
  256. )
  257. # Invalid product.
  258. invalid_product = Product(price=10, discounted_price=42)
  259. msg = "discount is fake"
  260. with self.assertRaisesMessage(ValidationError, msg) as cm:
  261. constraint.validate(Product, invalid_product)
  262. self.assertEqual(cm.exception.code, "fake_discount")
  263. def test_validate_boolean_expressions(self):
  264. constraint = models.CheckConstraint(
  265. condition=models.expressions.ExpressionWrapper(
  266. models.Q(price__gt=500) | models.Q(price__lt=500),
  267. output_field=models.BooleanField(),
  268. ),
  269. name="price_neq_500_wrap",
  270. )
  271. msg = f"Constraint “{constraint.name}” is violated."
  272. with self.assertRaisesMessage(ValidationError, msg):
  273. constraint.validate(Product, Product(price=500, discounted_price=5))
  274. constraint.validate(Product, Product(price=501, discounted_price=5))
  275. constraint.validate(Product, Product(price=499, discounted_price=5))
  276. def test_validate_rawsql_expressions_noop(self):
  277. constraint = models.CheckConstraint(
  278. condition=models.expressions.RawSQL(
  279. "price < %s OR price > %s",
  280. (500, 500),
  281. output_field=models.BooleanField(),
  282. ),
  283. name="price_neq_500_raw",
  284. )
  285. # RawSQL can not be checked and is always considered valid.
  286. constraint.validate(Product, Product(price=500, discounted_price=5))
  287. constraint.validate(Product, Product(price=501, discounted_price=5))
  288. constraint.validate(Product, Product(price=499, discounted_price=5))
  289. @skipUnlessDBFeature("supports_comparing_boolean_expr")
  290. def test_validate_nullable_field_with_none(self):
  291. # Nullable fields should be considered valid on None values.
  292. constraint = models.CheckConstraint(
  293. condition=models.Q(price__gte=0),
  294. name="positive_price",
  295. )
  296. constraint.validate(Product, Product())
  297. @skipIfDBFeature("supports_comparing_boolean_expr")
  298. def test_validate_nullable_field_with_isnull(self):
  299. constraint = models.CheckConstraint(
  300. condition=models.Q(price__gte=0) | models.Q(price__isnull=True),
  301. name="positive_price",
  302. )
  303. constraint.validate(Product, Product())
  304. @skipUnlessDBFeature("supports_json_field")
  305. def test_validate_nullable_jsonfield(self):
  306. is_null_constraint = models.CheckConstraint(
  307. condition=models.Q(data__isnull=True),
  308. name="nullable_data",
  309. )
  310. is_not_null_constraint = models.CheckConstraint(
  311. condition=models.Q(data__isnull=False),
  312. name="nullable_data",
  313. )
  314. is_null_constraint.validate(JSONFieldModel, JSONFieldModel(data=None))
  315. msg = f"Constraint “{is_null_constraint.name}” is violated."
  316. with self.assertRaisesMessage(ValidationError, msg):
  317. is_null_constraint.validate(JSONFieldModel, JSONFieldModel(data={}))
  318. msg = f"Constraint “{is_not_null_constraint.name}” is violated."
  319. with self.assertRaisesMessage(ValidationError, msg):
  320. is_not_null_constraint.validate(JSONFieldModel, JSONFieldModel(data=None))
  321. is_not_null_constraint.validate(JSONFieldModel, JSONFieldModel(data={}))
  322. def test_validate_pk_field(self):
  323. constraint_with_pk = models.CheckConstraint(
  324. condition=~models.Q(pk=models.F("age")),
  325. name="pk_not_age_check",
  326. )
  327. constraint_with_pk.validate(ChildModel, ChildModel(pk=1, age=2))
  328. msg = f"Constraint “{constraint_with_pk.name}” is violated."
  329. with self.assertRaisesMessage(ValidationError, msg):
  330. constraint_with_pk.validate(ChildModel, ChildModel(pk=1, age=1))
  331. with self.assertRaisesMessage(ValidationError, msg):
  332. constraint_with_pk.validate(ChildModel, ChildModel(id=1, age=1))
  333. constraint_with_pk.validate(ChildModel, ChildModel(pk=1, age=1), exclude={"pk"})
  334. @skipUnlessDBFeature("supports_json_field")
  335. def test_validate_jsonfield_exact(self):
  336. data = {"release": "5.0.2", "version": "stable"}
  337. json_exact_constraint = models.CheckConstraint(
  338. condition=models.Q(data__version="stable"),
  339. name="only_stable_version",
  340. )
  341. json_exact_constraint.validate(JSONFieldModel, JSONFieldModel(data=data))
  342. data = {"release": "5.0.2", "version": "not stable"}
  343. msg = f"Constraint “{json_exact_constraint.name}” is violated."
  344. with self.assertRaisesMessage(ValidationError, msg):
  345. json_exact_constraint.validate(JSONFieldModel, JSONFieldModel(data=data))
  346. def test_check_deprecation(self):
  347. msg = "CheckConstraint.check is deprecated in favor of `.condition`."
  348. condition = models.Q(foo="bar")
  349. with self.assertWarnsRegex(RemovedInDjango60Warning, msg):
  350. constraint = models.CheckConstraint(name="constraint", check=condition)
  351. with self.assertWarnsRegex(RemovedInDjango60Warning, msg):
  352. self.assertIs(constraint.check, condition)
  353. other_condition = models.Q(something="else")
  354. with self.assertWarnsRegex(RemovedInDjango60Warning, msg):
  355. constraint.check = other_condition
  356. with self.assertWarnsRegex(RemovedInDjango60Warning, msg):
  357. self.assertIs(constraint.check, other_condition)
  358. class UniqueConstraintTests(TestCase):
  359. @classmethod
  360. def setUpTestData(cls):
  361. cls.p1 = UniqueConstraintProduct.objects.create(name="p1", color="red")
  362. cls.p2 = UniqueConstraintProduct.objects.create(name="p2")
  363. def test_eq(self):
  364. self.assertEqual(
  365. models.UniqueConstraint(fields=["foo", "bar"], name="unique"),
  366. models.UniqueConstraint(fields=["foo", "bar"], name="unique"),
  367. )
  368. self.assertEqual(
  369. models.UniqueConstraint(fields=["foo", "bar"], name="unique"),
  370. mock.ANY,
  371. )
  372. self.assertNotEqual(
  373. models.UniqueConstraint(fields=["foo", "bar"], name="unique"),
  374. models.UniqueConstraint(fields=["foo", "bar"], name="unique2"),
  375. )
  376. self.assertNotEqual(
  377. models.UniqueConstraint(fields=["foo", "bar"], name="unique"),
  378. models.UniqueConstraint(fields=["foo", "baz"], name="unique"),
  379. )
  380. self.assertNotEqual(
  381. models.UniqueConstraint(fields=["foo", "bar"], name="unique"), 1
  382. )
  383. self.assertNotEqual(
  384. models.UniqueConstraint(fields=["foo", "bar"], name="unique"),
  385. models.UniqueConstraint(
  386. fields=["foo", "bar"],
  387. name="unique",
  388. violation_error_message="custom error",
  389. ),
  390. )
  391. self.assertNotEqual(
  392. models.UniqueConstraint(
  393. fields=["foo", "bar"],
  394. name="unique",
  395. violation_error_message="custom error",
  396. ),
  397. models.UniqueConstraint(
  398. fields=["foo", "bar"],
  399. name="unique",
  400. violation_error_message="other custom error",
  401. ),
  402. )
  403. self.assertEqual(
  404. models.UniqueConstraint(
  405. fields=["foo", "bar"],
  406. name="unique",
  407. violation_error_message="custom error",
  408. ),
  409. models.UniqueConstraint(
  410. fields=["foo", "bar"],
  411. name="unique",
  412. violation_error_message="custom error",
  413. ),
  414. )
  415. self.assertNotEqual(
  416. models.UniqueConstraint(
  417. fields=["foo", "bar"],
  418. name="unique",
  419. violation_error_code="custom_error",
  420. ),
  421. models.UniqueConstraint(
  422. fields=["foo", "bar"],
  423. name="unique",
  424. violation_error_code="other_custom_error",
  425. ),
  426. )
  427. self.assertEqual(
  428. models.UniqueConstraint(
  429. fields=["foo", "bar"],
  430. name="unique",
  431. violation_error_code="custom_error",
  432. ),
  433. models.UniqueConstraint(
  434. fields=["foo", "bar"],
  435. name="unique",
  436. violation_error_code="custom_error",
  437. ),
  438. )
  439. def test_eq_with_condition(self):
  440. self.assertEqual(
  441. models.UniqueConstraint(
  442. fields=["foo", "bar"],
  443. name="unique",
  444. condition=models.Q(foo=models.F("bar")),
  445. ),
  446. models.UniqueConstraint(
  447. fields=["foo", "bar"],
  448. name="unique",
  449. condition=models.Q(foo=models.F("bar")),
  450. ),
  451. )
  452. self.assertNotEqual(
  453. models.UniqueConstraint(
  454. fields=["foo", "bar"],
  455. name="unique",
  456. condition=models.Q(foo=models.F("bar")),
  457. ),
  458. models.UniqueConstraint(
  459. fields=["foo", "bar"],
  460. name="unique",
  461. condition=models.Q(foo=models.F("baz")),
  462. ),
  463. )
  464. def test_eq_with_deferrable(self):
  465. constraint_1 = models.UniqueConstraint(
  466. fields=["foo", "bar"],
  467. name="unique",
  468. deferrable=models.Deferrable.DEFERRED,
  469. )
  470. constraint_2 = models.UniqueConstraint(
  471. fields=["foo", "bar"],
  472. name="unique",
  473. deferrable=models.Deferrable.IMMEDIATE,
  474. )
  475. self.assertEqual(constraint_1, constraint_1)
  476. self.assertNotEqual(constraint_1, constraint_2)
  477. def test_eq_with_include(self):
  478. constraint_1 = models.UniqueConstraint(
  479. fields=["foo", "bar"],
  480. name="include",
  481. include=["baz_1"],
  482. )
  483. constraint_2 = models.UniqueConstraint(
  484. fields=["foo", "bar"],
  485. name="include",
  486. include=["baz_2"],
  487. )
  488. self.assertEqual(constraint_1, constraint_1)
  489. self.assertNotEqual(constraint_1, constraint_2)
  490. def test_eq_with_opclasses(self):
  491. constraint_1 = models.UniqueConstraint(
  492. fields=["foo", "bar"],
  493. name="opclasses",
  494. opclasses=["text_pattern_ops", "varchar_pattern_ops"],
  495. )
  496. constraint_2 = models.UniqueConstraint(
  497. fields=["foo", "bar"],
  498. name="opclasses",
  499. opclasses=["varchar_pattern_ops", "text_pattern_ops"],
  500. )
  501. self.assertEqual(constraint_1, constraint_1)
  502. self.assertNotEqual(constraint_1, constraint_2)
  503. def test_eq_with_expressions(self):
  504. constraint = models.UniqueConstraint(
  505. Lower("title"),
  506. F("author"),
  507. name="book_func_uq",
  508. )
  509. same_constraint = models.UniqueConstraint(
  510. Lower("title"),
  511. "author",
  512. name="book_func_uq",
  513. )
  514. another_constraint = models.UniqueConstraint(
  515. Lower("title"),
  516. name="book_func_uq",
  517. )
  518. self.assertEqual(constraint, same_constraint)
  519. self.assertEqual(constraint, mock.ANY)
  520. self.assertNotEqual(constraint, another_constraint)
  521. def test_eq_with_nulls_distinct(self):
  522. constraint_1 = models.UniqueConstraint(
  523. Lower("title"),
  524. nulls_distinct=False,
  525. name="book_func_nulls_distinct_uq",
  526. )
  527. constraint_2 = models.UniqueConstraint(
  528. Lower("title"),
  529. nulls_distinct=True,
  530. name="book_func_nulls_distinct_uq",
  531. )
  532. constraint_3 = models.UniqueConstraint(
  533. Lower("title"),
  534. name="book_func_nulls_distinct_uq",
  535. )
  536. self.assertEqual(constraint_1, constraint_1)
  537. self.assertEqual(constraint_1, mock.ANY)
  538. self.assertNotEqual(constraint_1, constraint_2)
  539. self.assertNotEqual(constraint_1, constraint_3)
  540. self.assertNotEqual(constraint_2, constraint_3)
  541. def test_repr(self):
  542. fields = ["foo", "bar"]
  543. name = "unique_fields"
  544. constraint = models.UniqueConstraint(fields=fields, name=name)
  545. self.assertEqual(
  546. repr(constraint),
  547. "<UniqueConstraint: fields=('foo', 'bar') name='unique_fields'>",
  548. )
  549. def test_repr_with_condition(self):
  550. constraint = models.UniqueConstraint(
  551. fields=["foo", "bar"],
  552. name="unique_fields",
  553. condition=models.Q(foo=models.F("bar")),
  554. )
  555. self.assertEqual(
  556. repr(constraint),
  557. "<UniqueConstraint: fields=('foo', 'bar') name='unique_fields' "
  558. "condition=(AND: ('foo', F(bar)))>",
  559. )
  560. def test_repr_with_deferrable(self):
  561. constraint = models.UniqueConstraint(
  562. fields=["foo", "bar"],
  563. name="unique_fields",
  564. deferrable=models.Deferrable.IMMEDIATE,
  565. )
  566. self.assertEqual(
  567. repr(constraint),
  568. "<UniqueConstraint: fields=('foo', 'bar') name='unique_fields' "
  569. "deferrable=Deferrable.IMMEDIATE>",
  570. )
  571. def test_repr_with_include(self):
  572. constraint = models.UniqueConstraint(
  573. fields=["foo", "bar"],
  574. name="include_fields",
  575. include=["baz_1", "baz_2"],
  576. )
  577. self.assertEqual(
  578. repr(constraint),
  579. "<UniqueConstraint: fields=('foo', 'bar') name='include_fields' "
  580. "include=('baz_1', 'baz_2')>",
  581. )
  582. def test_repr_with_opclasses(self):
  583. constraint = models.UniqueConstraint(
  584. fields=["foo", "bar"],
  585. name="opclasses_fields",
  586. opclasses=["text_pattern_ops", "varchar_pattern_ops"],
  587. )
  588. self.assertEqual(
  589. repr(constraint),
  590. "<UniqueConstraint: fields=('foo', 'bar') name='opclasses_fields' "
  591. "opclasses=['text_pattern_ops', 'varchar_pattern_ops']>",
  592. )
  593. def test_repr_with_nulls_distinct(self):
  594. constraint = models.UniqueConstraint(
  595. fields=["foo", "bar"],
  596. name="nulls_distinct_fields",
  597. nulls_distinct=False,
  598. )
  599. self.assertEqual(
  600. repr(constraint),
  601. "<UniqueConstraint: fields=('foo', 'bar') name='nulls_distinct_fields' "
  602. "nulls_distinct=False>",
  603. )
  604. def test_repr_with_expressions(self):
  605. constraint = models.UniqueConstraint(
  606. Lower("title"),
  607. F("author"),
  608. name="book_func_uq",
  609. )
  610. self.assertEqual(
  611. repr(constraint),
  612. "<UniqueConstraint: expressions=(Lower(F(title)), F(author)) "
  613. "name='book_func_uq'>",
  614. )
  615. def test_repr_with_violation_error_message(self):
  616. constraint = models.UniqueConstraint(
  617. models.F("baz__lower"),
  618. name="unique_lower_baz",
  619. violation_error_message="BAZ",
  620. )
  621. self.assertEqual(
  622. repr(constraint),
  623. (
  624. "<UniqueConstraint: expressions=(F(baz__lower),) "
  625. "name='unique_lower_baz' violation_error_message='BAZ'>"
  626. ),
  627. )
  628. def test_repr_with_violation_error_code(self):
  629. constraint = models.UniqueConstraint(
  630. models.F("baz__lower"),
  631. name="unique_lower_baz",
  632. violation_error_code="baz",
  633. )
  634. self.assertEqual(
  635. repr(constraint),
  636. (
  637. "<UniqueConstraint: expressions=(F(baz__lower),) "
  638. "name='unique_lower_baz' violation_error_code='baz'>"
  639. ),
  640. )
  641. def test_deconstruction(self):
  642. fields = ["foo", "bar"]
  643. name = "unique_fields"
  644. constraint = models.UniqueConstraint(fields=fields, name=name)
  645. path, args, kwargs = constraint.deconstruct()
  646. self.assertEqual(path, "django.db.models.UniqueConstraint")
  647. self.assertEqual(args, ())
  648. self.assertEqual(kwargs, {"fields": tuple(fields), "name": name})
  649. def test_deconstruction_with_condition(self):
  650. fields = ["foo", "bar"]
  651. name = "unique_fields"
  652. condition = models.Q(foo=models.F("bar"))
  653. constraint = models.UniqueConstraint(
  654. fields=fields, name=name, condition=condition
  655. )
  656. path, args, kwargs = constraint.deconstruct()
  657. self.assertEqual(path, "django.db.models.UniqueConstraint")
  658. self.assertEqual(args, ())
  659. self.assertEqual(
  660. kwargs, {"fields": tuple(fields), "name": name, "condition": condition}
  661. )
  662. def test_deconstruction_with_deferrable(self):
  663. fields = ["foo"]
  664. name = "unique_fields"
  665. constraint = models.UniqueConstraint(
  666. fields=fields,
  667. name=name,
  668. deferrable=models.Deferrable.DEFERRED,
  669. )
  670. path, args, kwargs = constraint.deconstruct()
  671. self.assertEqual(path, "django.db.models.UniqueConstraint")
  672. self.assertEqual(args, ())
  673. self.assertEqual(
  674. kwargs,
  675. {
  676. "fields": tuple(fields),
  677. "name": name,
  678. "deferrable": models.Deferrable.DEFERRED,
  679. },
  680. )
  681. def test_deconstruction_with_include(self):
  682. fields = ["foo", "bar"]
  683. name = "unique_fields"
  684. include = ["baz_1", "baz_2"]
  685. constraint = models.UniqueConstraint(fields=fields, name=name, include=include)
  686. path, args, kwargs = constraint.deconstruct()
  687. self.assertEqual(path, "django.db.models.UniqueConstraint")
  688. self.assertEqual(args, ())
  689. self.assertEqual(
  690. kwargs,
  691. {
  692. "fields": tuple(fields),
  693. "name": name,
  694. "include": tuple(include),
  695. },
  696. )
  697. def test_deconstruction_with_opclasses(self):
  698. fields = ["foo", "bar"]
  699. name = "unique_fields"
  700. opclasses = ["varchar_pattern_ops", "text_pattern_ops"]
  701. constraint = models.UniqueConstraint(
  702. fields=fields, name=name, opclasses=opclasses
  703. )
  704. path, args, kwargs = constraint.deconstruct()
  705. self.assertEqual(path, "django.db.models.UniqueConstraint")
  706. self.assertEqual(args, ())
  707. self.assertEqual(
  708. kwargs,
  709. {
  710. "fields": tuple(fields),
  711. "name": name,
  712. "opclasses": opclasses,
  713. },
  714. )
  715. def test_deconstruction_with_nulls_distinct(self):
  716. fields = ["foo", "bar"]
  717. name = "unique_fields"
  718. constraint = models.UniqueConstraint(
  719. fields=fields, name=name, nulls_distinct=True
  720. )
  721. path, args, kwargs = constraint.deconstruct()
  722. self.assertEqual(path, "django.db.models.UniqueConstraint")
  723. self.assertEqual(args, ())
  724. self.assertEqual(
  725. kwargs,
  726. {
  727. "fields": tuple(fields),
  728. "name": name,
  729. "nulls_distinct": True,
  730. },
  731. )
  732. def test_deconstruction_with_expressions(self):
  733. name = "unique_fields"
  734. constraint = models.UniqueConstraint(Lower("title"), name=name)
  735. path, args, kwargs = constraint.deconstruct()
  736. self.assertEqual(path, "django.db.models.UniqueConstraint")
  737. self.assertEqual(args, (Lower("title"),))
  738. self.assertEqual(kwargs, {"name": name})
  739. def test_database_constraint(self):
  740. with self.assertRaises(IntegrityError):
  741. UniqueConstraintProduct.objects.create(
  742. name=self.p1.name, color=self.p1.color
  743. )
  744. @skipUnlessDBFeature("supports_partial_indexes")
  745. def test_database_constraint_with_condition(self):
  746. UniqueConstraintConditionProduct.objects.create(name="p1")
  747. UniqueConstraintConditionProduct.objects.create(name="p2")
  748. with self.assertRaises(IntegrityError):
  749. UniqueConstraintConditionProduct.objects.create(name="p1")
  750. def test_model_validation(self):
  751. msg = "Unique constraint product with this Name and Color already exists."
  752. with self.assertRaisesMessage(ValidationError, msg):
  753. UniqueConstraintProduct(
  754. name=self.p1.name, color=self.p1.color
  755. ).validate_constraints()
  756. @skipUnlessDBFeature("supports_partial_indexes")
  757. def test_model_validation_with_condition(self):
  758. """
  759. Partial unique constraints are not ignored by
  760. Model.validate_constraints().
  761. """
  762. obj1 = UniqueConstraintConditionProduct.objects.create(name="p1", color="red")
  763. obj2 = UniqueConstraintConditionProduct.objects.create(name="p2")
  764. UniqueConstraintConditionProduct(
  765. name=obj1.name, color="blue"
  766. ).validate_constraints()
  767. msg = "Constraint “name_without_color_uniq” is violated."
  768. with self.assertRaisesMessage(ValidationError, msg):
  769. UniqueConstraintConditionProduct(name=obj2.name).validate_constraints()
  770. def test_model_validation_constraint_no_code_error(self):
  771. class ValidateNoCodeErrorConstraint(UniqueConstraint):
  772. def validate(self, model, instance, **kwargs):
  773. raise ValidationError({"name": ValidationError("Already exists.")})
  774. class NoCodeErrorConstraintModel(models.Model):
  775. name = models.CharField(max_length=255)
  776. class Meta:
  777. constraints = [
  778. ValidateNoCodeErrorConstraint(
  779. Lower("name"),
  780. name="custom_validate_no_code_error",
  781. )
  782. ]
  783. msg = "{'name': ['Already exists.']}"
  784. with self.assertRaisesMessage(ValidationError, msg):
  785. NoCodeErrorConstraintModel(name="test").validate_constraints()
  786. def test_validate(self):
  787. constraint = UniqueConstraintProduct._meta.constraints[0]
  788. msg = "Unique constraint product with this Name and Color already exists."
  789. non_unique_product = UniqueConstraintProduct(
  790. name=self.p1.name, color=self.p1.color
  791. )
  792. with self.assertRaisesMessage(ValidationError, msg) as cm:
  793. constraint.validate(UniqueConstraintProduct, non_unique_product)
  794. self.assertEqual(cm.exception.code, "unique_together")
  795. # Null values are ignored.
  796. constraint.validate(
  797. UniqueConstraintProduct,
  798. UniqueConstraintProduct(name=self.p2.name, color=None),
  799. )
  800. # Existing instances have their existing row excluded.
  801. constraint.validate(UniqueConstraintProduct, self.p1)
  802. # Unique fields are excluded.
  803. constraint.validate(
  804. UniqueConstraintProduct,
  805. non_unique_product,
  806. exclude={"name"},
  807. )
  808. constraint.validate(
  809. UniqueConstraintProduct,
  810. non_unique_product,
  811. exclude={"color"},
  812. )
  813. constraint.validate(
  814. UniqueConstraintProduct,
  815. non_unique_product,
  816. exclude={"name", "color"},
  817. )
  818. # Validation on a child instance.
  819. with self.assertRaisesMessage(ValidationError, msg):
  820. constraint.validate(
  821. UniqueConstraintProduct,
  822. ChildUniqueConstraintProduct(name=self.p1.name, color=self.p1.color),
  823. )
  824. @skipUnlessDBFeature("supports_partial_indexes")
  825. def test_validate_condition(self):
  826. p1 = UniqueConstraintConditionProduct.objects.create(name="p1")
  827. constraint = UniqueConstraintConditionProduct._meta.constraints[0]
  828. msg = "Constraint “name_without_color_uniq” is violated."
  829. with self.assertRaisesMessage(ValidationError, msg):
  830. constraint.validate(
  831. UniqueConstraintConditionProduct,
  832. UniqueConstraintConditionProduct(name=p1.name, color=None),
  833. )
  834. # Values not matching condition are ignored.
  835. constraint.validate(
  836. UniqueConstraintConditionProduct,
  837. UniqueConstraintConditionProduct(name=p1.name, color="anything-but-none"),
  838. )
  839. # Existing instances have their existing row excluded.
  840. constraint.validate(UniqueConstraintConditionProduct, p1)
  841. # Unique field is excluded.
  842. constraint.validate(
  843. UniqueConstraintConditionProduct,
  844. UniqueConstraintConditionProduct(name=p1.name, color=None),
  845. exclude={"name"},
  846. )
  847. @skipUnlessDBFeature("supports_partial_indexes")
  848. def test_validate_conditon_custom_error(self):
  849. p1 = UniqueConstraintConditionProduct.objects.create(name="p1")
  850. constraint = models.UniqueConstraint(
  851. fields=["name"],
  852. name="name_without_color_uniq",
  853. condition=models.Q(color__isnull=True),
  854. violation_error_code="custom_code",
  855. violation_error_message="Custom message",
  856. )
  857. msg = "Custom message"
  858. with self.assertRaisesMessage(ValidationError, msg) as cm:
  859. constraint.validate(
  860. UniqueConstraintConditionProduct,
  861. UniqueConstraintConditionProduct(name=p1.name, color=None),
  862. )
  863. self.assertEqual(cm.exception.code, "custom_code")
  864. def test_validate_expression(self):
  865. constraint = models.UniqueConstraint(Lower("name"), name="name_lower_uniq")
  866. msg = "Constraint “name_lower_uniq” is violated."
  867. with self.assertRaisesMessage(ValidationError, msg):
  868. constraint.validate(
  869. UniqueConstraintProduct,
  870. UniqueConstraintProduct(name=self.p1.name.upper()),
  871. )
  872. constraint.validate(
  873. UniqueConstraintProduct,
  874. UniqueConstraintProduct(name="another-name"),
  875. )
  876. # Existing instances have their existing row excluded.
  877. constraint.validate(UniqueConstraintProduct, self.p1)
  878. # Unique field is excluded.
  879. constraint.validate(
  880. UniqueConstraintProduct,
  881. UniqueConstraintProduct(name=self.p1.name.upper()),
  882. exclude={"name"},
  883. )
  884. def test_validate_ordered_expression(self):
  885. constraint = models.UniqueConstraint(
  886. Lower("name").desc(), name="name_lower_uniq_desc"
  887. )
  888. msg = "Constraint “name_lower_uniq_desc” is violated."
  889. with self.assertRaisesMessage(ValidationError, msg):
  890. constraint.validate(
  891. UniqueConstraintProduct,
  892. UniqueConstraintProduct(name=self.p1.name.upper()),
  893. )
  894. constraint.validate(
  895. UniqueConstraintProduct,
  896. UniqueConstraintProduct(name="another-name"),
  897. )
  898. # Existing instances have their existing row excluded.
  899. constraint.validate(UniqueConstraintProduct, self.p1)
  900. # Unique field is excluded.
  901. constraint.validate(
  902. UniqueConstraintProduct,
  903. UniqueConstraintProduct(name=self.p1.name.upper()),
  904. exclude={"name"},
  905. )
  906. def test_validate_expression_condition(self):
  907. constraint = models.UniqueConstraint(
  908. Lower("name"),
  909. name="name_lower_without_color_uniq",
  910. condition=models.Q(color__isnull=True),
  911. )
  912. non_unique_product = UniqueConstraintProduct(name=self.p2.name.upper())
  913. msg = "Constraint “name_lower_without_color_uniq” is violated."
  914. with self.assertRaisesMessage(ValidationError, msg):
  915. constraint.validate(UniqueConstraintProduct, non_unique_product)
  916. # Values not matching condition are ignored.
  917. constraint.validate(
  918. UniqueConstraintProduct,
  919. UniqueConstraintProduct(name=self.p1.name, color=self.p1.color),
  920. )
  921. # Existing instances have their existing row excluded.
  922. constraint.validate(UniqueConstraintProduct, self.p2)
  923. # Unique field is excluded.
  924. constraint.validate(
  925. UniqueConstraintProduct,
  926. non_unique_product,
  927. exclude={"name"},
  928. )
  929. # Field from a condition is excluded.
  930. constraint.validate(
  931. UniqueConstraintProduct,
  932. non_unique_product,
  933. exclude={"color"},
  934. )
  935. def test_validate_expression_str(self):
  936. constraint = models.UniqueConstraint("name", name="name_uniq")
  937. msg = "Constraint “name_uniq” is violated."
  938. with self.assertRaisesMessage(ValidationError, msg):
  939. constraint.validate(
  940. UniqueConstraintProduct,
  941. UniqueConstraintProduct(name=self.p1.name),
  942. )
  943. constraint.validate(
  944. UniqueConstraintProduct,
  945. UniqueConstraintProduct(name=self.p1.name),
  946. exclude={"name"},
  947. )
  948. @skipUnlessDBFeature("supports_table_check_constraints")
  949. def test_validate_nullable_textfield_with_isnull_true(self):
  950. is_null_constraint = models.UniqueConstraint(
  951. "price",
  952. "discounted_price",
  953. condition=models.Q(unit__isnull=True),
  954. name="uniq_prices_no_unit",
  955. )
  956. is_not_null_constraint = models.UniqueConstraint(
  957. "price",
  958. "discounted_price",
  959. condition=models.Q(unit__isnull=False),
  960. name="uniq_prices_unit",
  961. )
  962. Product.objects.create(price=2, discounted_price=1)
  963. Product.objects.create(price=4, discounted_price=3, unit="ng/mL")
  964. msg = "Constraint “uniq_prices_no_unit” is violated."
  965. with self.assertRaisesMessage(ValidationError, msg):
  966. is_null_constraint.validate(
  967. Product, Product(price=2, discounted_price=1, unit=None)
  968. )
  969. is_null_constraint.validate(
  970. Product, Product(price=2, discounted_price=1, unit="ng/mL")
  971. )
  972. is_null_constraint.validate(Product, Product(price=4, discounted_price=3))
  973. msg = "Constraint “uniq_prices_unit” is violated."
  974. with self.assertRaisesMessage(ValidationError, msg):
  975. is_not_null_constraint.validate(
  976. Product,
  977. Product(price=4, discounted_price=3, unit="μg/mL"),
  978. )
  979. is_not_null_constraint.validate(Product, Product(price=4, discounted_price=3))
  980. is_not_null_constraint.validate(Product, Product(price=2, discounted_price=1))
  981. def test_name(self):
  982. constraints = get_constraints(UniqueConstraintProduct._meta.db_table)
  983. expected_name = "name_color_uniq"
  984. self.assertIn(expected_name, constraints)
  985. def test_condition_must_be_q(self):
  986. with self.assertRaisesMessage(
  987. ValueError, "UniqueConstraint.condition must be a Q instance."
  988. ):
  989. models.UniqueConstraint(name="uniq", fields=["name"], condition="invalid")
  990. @skipUnlessDBFeature("supports_deferrable_unique_constraints")
  991. def test_initially_deferred_database_constraint(self):
  992. obj_1 = UniqueConstraintDeferrable.objects.create(name="p1", shelf="front")
  993. obj_2 = UniqueConstraintDeferrable.objects.create(name="p2", shelf="back")
  994. def swap():
  995. obj_1.name, obj_2.name = obj_2.name, obj_1.name
  996. obj_1.save()
  997. obj_2.save()
  998. swap()
  999. # Behavior can be changed with SET CONSTRAINTS.
  1000. with self.assertRaises(IntegrityError):
  1001. with atomic(), connection.cursor() as cursor:
  1002. constraint_name = connection.ops.quote_name("name_init_deferred_uniq")
  1003. cursor.execute("SET CONSTRAINTS %s IMMEDIATE" % constraint_name)
  1004. swap()
  1005. @skipUnlessDBFeature("supports_deferrable_unique_constraints")
  1006. def test_initially_immediate_database_constraint(self):
  1007. obj_1 = UniqueConstraintDeferrable.objects.create(name="p1", shelf="front")
  1008. obj_2 = UniqueConstraintDeferrable.objects.create(name="p2", shelf="back")
  1009. obj_1.shelf, obj_2.shelf = obj_2.shelf, obj_1.shelf
  1010. with self.assertRaises(IntegrityError), atomic():
  1011. obj_1.save()
  1012. # Behavior can be changed with SET CONSTRAINTS.
  1013. with connection.cursor() as cursor:
  1014. constraint_name = connection.ops.quote_name("sheld_init_immediate_uniq")
  1015. cursor.execute("SET CONSTRAINTS %s DEFERRED" % constraint_name)
  1016. obj_1.save()
  1017. obj_2.save()
  1018. def test_deferrable_with_condition(self):
  1019. message = "UniqueConstraint with conditions cannot be deferred."
  1020. with self.assertRaisesMessage(ValueError, message):
  1021. models.UniqueConstraint(
  1022. fields=["name"],
  1023. name="name_without_color_unique",
  1024. condition=models.Q(color__isnull=True),
  1025. deferrable=models.Deferrable.DEFERRED,
  1026. )
  1027. def test_deferrable_with_include(self):
  1028. message = "UniqueConstraint with include fields cannot be deferred."
  1029. with self.assertRaisesMessage(ValueError, message):
  1030. models.UniqueConstraint(
  1031. fields=["name"],
  1032. name="name_inc_color_color_unique",
  1033. include=["color"],
  1034. deferrable=models.Deferrable.DEFERRED,
  1035. )
  1036. def test_deferrable_with_opclasses(self):
  1037. message = "UniqueConstraint with opclasses cannot be deferred."
  1038. with self.assertRaisesMessage(ValueError, message):
  1039. models.UniqueConstraint(
  1040. fields=["name"],
  1041. name="name_text_pattern_ops_unique",
  1042. opclasses=["text_pattern_ops"],
  1043. deferrable=models.Deferrable.DEFERRED,
  1044. )
  1045. def test_deferrable_with_expressions(self):
  1046. message = "UniqueConstraint with expressions cannot be deferred."
  1047. with self.assertRaisesMessage(ValueError, message):
  1048. models.UniqueConstraint(
  1049. Lower("name"),
  1050. name="deferred_expression_unique",
  1051. deferrable=models.Deferrable.DEFERRED,
  1052. )
  1053. def test_invalid_defer_argument(self):
  1054. message = "UniqueConstraint.deferrable must be a Deferrable instance."
  1055. with self.assertRaisesMessage(TypeError, message):
  1056. models.UniqueConstraint(
  1057. fields=["name"],
  1058. name="name_invalid",
  1059. deferrable="invalid",
  1060. )
  1061. @skipUnlessDBFeature(
  1062. "supports_table_check_constraints",
  1063. "supports_covering_indexes",
  1064. )
  1065. def test_include_database_constraint(self):
  1066. UniqueConstraintInclude.objects.create(name="p1", color="red")
  1067. with self.assertRaises(IntegrityError):
  1068. UniqueConstraintInclude.objects.create(name="p1", color="blue")
  1069. def test_invalid_include_argument(self):
  1070. msg = "UniqueConstraint.include must be a list or tuple."
  1071. with self.assertRaisesMessage(TypeError, msg):
  1072. models.UniqueConstraint(
  1073. name="uniq_include",
  1074. fields=["field"],
  1075. include="other",
  1076. )
  1077. def test_invalid_opclasses_argument(self):
  1078. msg = "UniqueConstraint.opclasses must be a list or tuple."
  1079. with self.assertRaisesMessage(TypeError, msg):
  1080. models.UniqueConstraint(
  1081. name="uniq_opclasses",
  1082. fields=["field"],
  1083. opclasses="jsonb_path_ops",
  1084. )
  1085. def test_invalid_nulls_distinct_argument(self):
  1086. msg = "UniqueConstraint.nulls_distinct must be a bool."
  1087. with self.assertRaisesMessage(TypeError, msg):
  1088. models.UniqueConstraint(
  1089. name="uniq_opclasses", fields=["field"], nulls_distinct="NULLS DISTINCT"
  1090. )
  1091. def test_opclasses_and_fields_same_length(self):
  1092. msg = (
  1093. "UniqueConstraint.fields and UniqueConstraint.opclasses must have "
  1094. "the same number of elements."
  1095. )
  1096. with self.assertRaisesMessage(ValueError, msg):
  1097. models.UniqueConstraint(
  1098. name="uniq_opclasses",
  1099. fields=["field"],
  1100. opclasses=["foo", "bar"],
  1101. )
  1102. def test_requires_field_or_expression(self):
  1103. msg = (
  1104. "At least one field or expression is required to define a unique "
  1105. "constraint."
  1106. )
  1107. with self.assertRaisesMessage(ValueError, msg):
  1108. models.UniqueConstraint(name="name")
  1109. def test_expressions_and_fields_mutually_exclusive(self):
  1110. msg = "UniqueConstraint.fields and expressions are mutually exclusive."
  1111. with self.assertRaisesMessage(ValueError, msg):
  1112. models.UniqueConstraint(Lower("field_1"), fields=["field_2"], name="name")
  1113. def test_expressions_with_opclasses(self):
  1114. msg = (
  1115. "UniqueConstraint.opclasses cannot be used with expressions. Use "
  1116. "django.contrib.postgres.indexes.OpClass() instead."
  1117. )
  1118. with self.assertRaisesMessage(ValueError, msg):
  1119. models.UniqueConstraint(
  1120. Lower("field"),
  1121. name="test_func_opclass",
  1122. opclasses=["jsonb_path_ops"],
  1123. )
  1124. def test_requires_name(self):
  1125. msg = "A unique constraint must be named."
  1126. with self.assertRaisesMessage(ValueError, msg):
  1127. models.UniqueConstraint(fields=["field"])