2019-09-03 02:09:31 +00:00
|
|
|
from unittest import mock
|
|
|
|
|
2018-08-06 02:30:44 +00:00
|
|
|
from django.core.exceptions import ValidationError
|
2018-08-06 01:06:52 +00:00
|
|
|
from django.db import IntegrityError, connection, models
|
2021-02-06 19:45:54 +00:00
|
|
|
from django.db.models import F
|
2023-02-08 15:38:55 +00:00
|
|
|
from django.db.models.constraints import BaseConstraint, UniqueConstraint
|
2024-07-18 07:38:06 +00:00
|
|
|
from django.db.models.functions import Abs, Lower, Sqrt, Upper
|
2018-08-27 02:25:06 +00:00
|
|
|
from django.db.transaction import atomic
|
2022-09-08 14:02:58 +00:00
|
|
|
from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature
|
2023-02-20 13:28:21 +00:00
|
|
|
from django.test.utils import ignore_warnings
|
|
|
|
from django.utils.deprecation import RemovedInDjango60Warning
|
2016-11-05 13:12:12 +00:00
|
|
|
|
2020-04-17 05:45:52 +00:00
|
|
|
from .models import (
|
|
|
|
ChildModel,
|
2022-01-31 15:04:13 +00:00
|
|
|
ChildUniqueConstraintProduct,
|
2024-07-18 07:38:06 +00:00
|
|
|
GeneratedFieldStoredProduct,
|
|
|
|
GeneratedFieldVirtualProduct,
|
2023-08-03 00:47:49 +00:00
|
|
|
JSONFieldModel,
|
2024-08-05 06:22:29 +00:00
|
|
|
ModelWithDatabaseDefault,
|
2020-04-17 05:45:52 +00:00
|
|
|
Product,
|
|
|
|
UniqueConstraintConditionProduct,
|
2019-10-31 12:33:53 +00:00
|
|
|
UniqueConstraintDeferrable,
|
|
|
|
UniqueConstraintInclude,
|
|
|
|
UniqueConstraintProduct,
|
2020-04-17 05:45:52 +00:00
|
|
|
)
|
2016-11-05 13:12:12 +00:00
|
|
|
|
|
|
|
|
2018-08-06 01:06:52 +00:00
|
|
|
def get_constraints(table):
|
|
|
|
with connection.cursor() as cursor:
|
|
|
|
return connection.introspection.get_constraints(cursor, table)
|
|
|
|
|
|
|
|
|
2018-08-06 02:12:27 +00:00
|
|
|
class BaseConstraintTests(SimpleTestCase):
|
|
|
|
def test_constraint_sql(self):
|
2023-02-20 13:28:21 +00:00
|
|
|
c = BaseConstraint(name="name")
|
2018-08-06 02:12:27 +00:00
|
|
|
msg = "This method must be implemented by a subclass."
|
|
|
|
with self.assertRaisesMessage(NotImplementedError, msg):
|
|
|
|
c.constraint_sql(None, None)
|
|
|
|
|
2021-02-06 19:45:54 +00:00
|
|
|
def test_contains_expressions(self):
|
2023-02-20 13:28:21 +00:00
|
|
|
c = BaseConstraint(name="name")
|
2021-02-06 19:45:54 +00:00
|
|
|
self.assertIs(c.contains_expressions, False)
|
|
|
|
|
2019-01-01 14:39:58 +00:00
|
|
|
def test_create_sql(self):
|
2023-02-20 13:28:21 +00:00
|
|
|
c = BaseConstraint(name="name")
|
2019-01-01 14:39:58 +00:00
|
|
|
msg = "This method must be implemented by a subclass."
|
|
|
|
with self.assertRaisesMessage(NotImplementedError, msg):
|
|
|
|
c.create_sql(None, None)
|
|
|
|
|
|
|
|
def test_remove_sql(self):
|
2023-02-20 13:28:21 +00:00
|
|
|
c = BaseConstraint(name="name")
|
2019-01-01 14:39:58 +00:00
|
|
|
msg = "This method must be implemented by a subclass."
|
|
|
|
with self.assertRaisesMessage(NotImplementedError, msg):
|
|
|
|
c.remove_sql(None, None)
|
|
|
|
|
2022-01-31 15:04:13 +00:00
|
|
|
def test_validate(self):
|
2023-02-20 13:28:21 +00:00
|
|
|
c = BaseConstraint(name="name")
|
2022-01-31 15:04:13 +00:00
|
|
|
msg = "This method must be implemented by a subclass."
|
|
|
|
with self.assertRaisesMessage(NotImplementedError, msg):
|
|
|
|
c.validate(None, None)
|
|
|
|
|
|
|
|
def test_default_violation_error_message(self):
|
2023-02-20 13:28:21 +00:00
|
|
|
c = BaseConstraint(name="name")
|
2022-01-31 15:04:13 +00:00
|
|
|
self.assertEqual(
|
|
|
|
c.get_violation_error_message(), "Constraint “name” is violated."
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_custom_violation_error_message(self):
|
|
|
|
c = BaseConstraint(
|
2023-02-20 13:28:21 +00:00
|
|
|
name="base_name", violation_error_message="custom %(name)s message"
|
2022-01-31 15:04:13 +00:00
|
|
|
)
|
|
|
|
self.assertEqual(c.get_violation_error_message(), "custom base_name message")
|
|
|
|
|
2022-07-07 02:26:49 +00:00
|
|
|
def test_custom_violation_error_message_clone(self):
|
|
|
|
constraint = BaseConstraint(
|
2023-02-20 13:28:21 +00:00
|
|
|
name="base_name",
|
2022-07-07 02:26:49 +00:00
|
|
|
violation_error_message="custom %(name)s message",
|
|
|
|
).clone()
|
|
|
|
self.assertEqual(
|
|
|
|
constraint.get_violation_error_message(),
|
|
|
|
"custom base_name message",
|
|
|
|
)
|
|
|
|
|
2023-02-14 20:06:45 +00:00
|
|
|
def test_custom_violation_code_message(self):
|
|
|
|
c = BaseConstraint(name="base_name", violation_error_code="custom_code")
|
|
|
|
self.assertEqual(c.violation_error_code, "custom_code")
|
|
|
|
|
2022-07-07 02:26:49 +00:00
|
|
|
def test_deconstruction(self):
|
|
|
|
constraint = BaseConstraint(
|
2023-02-20 13:28:21 +00:00
|
|
|
name="base_name",
|
2022-07-07 02:26:49 +00:00
|
|
|
violation_error_message="custom %(name)s message",
|
2023-02-14 20:06:45 +00:00
|
|
|
violation_error_code="custom_code",
|
2022-07-07 02:26:49 +00:00
|
|
|
)
|
|
|
|
path, args, kwargs = constraint.deconstruct()
|
|
|
|
self.assertEqual(path, "django.db.models.BaseConstraint")
|
|
|
|
self.assertEqual(args, ())
|
|
|
|
self.assertEqual(
|
|
|
|
kwargs,
|
2023-02-14 20:06:45 +00:00
|
|
|
{
|
|
|
|
"name": "base_name",
|
|
|
|
"violation_error_message": "custom %(name)s message",
|
|
|
|
"violation_error_code": "custom_code",
|
|
|
|
},
|
2022-07-07 02:26:49 +00:00
|
|
|
)
|
|
|
|
|
2023-02-20 13:28:21 +00:00
|
|
|
def test_deprecation(self):
|
|
|
|
msg = "Passing positional arguments to BaseConstraint is deprecated."
|
|
|
|
with self.assertRaisesMessage(RemovedInDjango60Warning, msg):
|
|
|
|
BaseConstraint("name", "violation error message")
|
|
|
|
|
|
|
|
def test_name_required(self):
|
|
|
|
msg = (
|
|
|
|
"BaseConstraint.__init__() missing 1 required keyword-only argument: 'name'"
|
|
|
|
)
|
|
|
|
with self.assertRaisesMessage(TypeError, msg):
|
|
|
|
BaseConstraint()
|
|
|
|
|
|
|
|
@ignore_warnings(category=RemovedInDjango60Warning)
|
|
|
|
def test_positional_arguments(self):
|
|
|
|
c = BaseConstraint("name", "custom %(name)s message")
|
|
|
|
self.assertEqual(c.get_violation_error_message(), "custom name message")
|
|
|
|
|
2018-08-06 02:12:27 +00:00
|
|
|
|
2016-11-05 13:12:12 +00:00
|
|
|
class CheckConstraintTests(TestCase):
|
2019-01-11 22:42:47 +00:00
|
|
|
def test_eq(self):
|
|
|
|
check1 = models.Q(price__gt=models.F("discounted_price"))
|
|
|
|
check2 = models.Q(price__lt=models.F("discounted_price"))
|
|
|
|
self.assertEqual(
|
2024-02-26 05:14:26 +00:00
|
|
|
models.CheckConstraint(condition=check1, name="price"),
|
|
|
|
models.CheckConstraint(condition=check1, name="price"),
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
models.CheckConstraint(condition=check1, name="price"), mock.ANY
|
2019-01-11 22:42:47 +00:00
|
|
|
)
|
|
|
|
self.assertNotEqual(
|
2024-02-26 05:14:26 +00:00
|
|
|
models.CheckConstraint(condition=check1, name="price"),
|
|
|
|
models.CheckConstraint(condition=check1, name="price2"),
|
2019-01-11 22:42:47 +00:00
|
|
|
)
|
|
|
|
self.assertNotEqual(
|
2024-02-26 05:14:26 +00:00
|
|
|
models.CheckConstraint(condition=check1, name="price"),
|
|
|
|
models.CheckConstraint(condition=check2, name="price"),
|
2019-01-11 22:42:47 +00:00
|
|
|
)
|
2024-02-26 05:14:26 +00:00
|
|
|
self.assertNotEqual(models.CheckConstraint(condition=check1, name="price"), 1)
|
2022-07-07 02:26:49 +00:00
|
|
|
self.assertNotEqual(
|
2024-02-26 05:14:26 +00:00
|
|
|
models.CheckConstraint(condition=check1, name="price"),
|
2022-07-07 02:26:49 +00:00
|
|
|
models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=check1, name="price", violation_error_message="custom error"
|
2022-07-07 02:26:49 +00:00
|
|
|
),
|
|
|
|
)
|
|
|
|
self.assertNotEqual(
|
|
|
|
models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=check1, name="price", violation_error_message="custom error"
|
2022-07-07 02:26:49 +00:00
|
|
|
),
|
|
|
|
models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=check1,
|
|
|
|
name="price",
|
|
|
|
violation_error_message="other custom error",
|
2022-07-07 02:26:49 +00:00
|
|
|
),
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=check1, name="price", violation_error_message="custom error"
|
2022-07-07 02:26:49 +00:00
|
|
|
),
|
|
|
|
models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=check1, name="price", violation_error_message="custom error"
|
2022-07-07 02:26:49 +00:00
|
|
|
),
|
|
|
|
)
|
2023-02-14 20:06:45 +00:00
|
|
|
self.assertNotEqual(
|
2024-02-26 05:14:26 +00:00
|
|
|
models.CheckConstraint(condition=check1, name="price"),
|
2023-02-14 20:06:45 +00:00
|
|
|
models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=check1, name="price", violation_error_code="custom_code"
|
2023-02-14 20:06:45 +00:00
|
|
|
),
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=check1, name="price", violation_error_code="custom_code"
|
2023-02-14 20:06:45 +00:00
|
|
|
),
|
|
|
|
models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=check1, name="price", violation_error_code="custom_code"
|
2023-02-14 20:06:45 +00:00
|
|
|
),
|
|
|
|
)
|
2019-01-11 22:42:47 +00:00
|
|
|
|
2016-11-05 13:12:12 +00:00
|
|
|
def test_repr(self):
|
2021-02-19 11:16:24 +00:00
|
|
|
constraint = models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=models.Q(price__gt=models.F("discounted_price")),
|
2021-02-19 11:16:24 +00:00
|
|
|
name="price_gt_discounted_price",
|
|
|
|
)
|
2016-11-05 13:12:12 +00:00
|
|
|
self.assertEqual(
|
2018-08-06 02:15:10 +00:00
|
|
|
repr(constraint),
|
2024-02-26 05:14:26 +00:00
|
|
|
"<CheckConstraint: condition=(AND: ('price__gt', F(discounted_price))) "
|
2021-02-19 11:16:24 +00:00
|
|
|
"name='price_gt_discounted_price'>",
|
2016-11-05 13:12:12 +00:00
|
|
|
)
|
|
|
|
|
2023-02-22 20:04:05 +00:00
|
|
|
def test_repr_with_violation_error_message(self):
|
|
|
|
constraint = models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=models.Q(price__lt=1),
|
2023-02-22 20:04:05 +00:00
|
|
|
name="price_lt_one",
|
|
|
|
violation_error_message="More than 1",
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
repr(constraint),
|
2024-02-26 05:14:26 +00:00
|
|
|
"<CheckConstraint: condition=(AND: ('price__lt', 1)) name='price_lt_one' "
|
2023-02-22 20:04:05 +00:00
|
|
|
"violation_error_message='More than 1'>",
|
|
|
|
)
|
|
|
|
|
2023-02-14 20:06:45 +00:00
|
|
|
def test_repr_with_violation_error_code(self):
|
|
|
|
constraint = models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=models.Q(price__lt=1),
|
2023-02-14 20:06:45 +00:00
|
|
|
name="price_lt_one",
|
|
|
|
violation_error_code="more_than_one",
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
repr(constraint),
|
2024-02-26 05:14:26 +00:00
|
|
|
"<CheckConstraint: condition=(AND: ('price__lt', 1)) name='price_lt_one' "
|
2023-02-14 20:06:45 +00:00
|
|
|
"violation_error_code='more_than_one'>",
|
|
|
|
)
|
|
|
|
|
2019-11-15 05:08:36 +00:00
|
|
|
def test_invalid_check_types(self):
|
2024-02-26 05:14:26 +00:00
|
|
|
msg = "CheckConstraint.condition must be a Q instance or boolean expression."
|
2019-11-15 05:08:36 +00:00
|
|
|
with self.assertRaisesMessage(TypeError, msg):
|
2024-02-26 05:14:26 +00:00
|
|
|
models.CheckConstraint(condition=models.F("discounted_price"), name="check")
|
2019-11-15 05:08:36 +00:00
|
|
|
|
2016-11-05 13:12:12 +00:00
|
|
|
def test_deconstruction(self):
|
2018-08-06 02:15:10 +00:00
|
|
|
check = models.Q(price__gt=models.F("discounted_price"))
|
2016-11-05 13:12:12 +00:00
|
|
|
name = "price_gt_discounted_price"
|
2024-02-26 05:14:26 +00:00
|
|
|
constraint = models.CheckConstraint(condition=check, name=name)
|
2018-08-06 02:15:10 +00:00
|
|
|
path, args, kwargs = constraint.deconstruct()
|
2016-11-05 13:12:12 +00:00
|
|
|
self.assertEqual(path, "django.db.models.CheckConstraint")
|
|
|
|
self.assertEqual(args, ())
|
2024-02-26 05:14:26 +00:00
|
|
|
self.assertEqual(kwargs, {"condition": check, "name": name})
|
2016-11-05 13:12:12 +00:00
|
|
|
|
|
|
|
@skipUnlessDBFeature("supports_table_check_constraints")
|
2018-09-25 15:14:45 +00:00
|
|
|
def test_database_constraint(self):
|
2019-08-10 06:41:18 +00:00
|
|
|
Product.objects.create(price=10, discounted_price=5)
|
2016-11-05 13:12:12 +00:00
|
|
|
with self.assertRaises(IntegrityError):
|
2019-08-10 06:41:18 +00:00
|
|
|
Product.objects.create(price=10, discounted_price=20)
|
2018-08-06 01:06:52 +00:00
|
|
|
|
2020-07-27 04:39:02 +00:00
|
|
|
@skipUnlessDBFeature("supports_table_check_constraints")
|
|
|
|
def test_database_constraint_unicode(self):
|
|
|
|
Product.objects.create(price=10, discounted_price=5, unit="μg/mL")
|
|
|
|
with self.assertRaises(IntegrityError):
|
|
|
|
Product.objects.create(price=10, discounted_price=7, unit="l")
|
|
|
|
|
2019-07-13 23:24:35 +00:00
|
|
|
@skipUnlessDBFeature(
|
|
|
|
"supports_table_check_constraints", "can_introspect_check_constraints"
|
|
|
|
)
|
2018-08-06 01:06:52 +00:00
|
|
|
def test_name(self):
|
|
|
|
constraints = get_constraints(Product._meta.db_table)
|
2019-07-05 12:15:41 +00:00
|
|
|
for expected_name in (
|
|
|
|
"price_gt_discounted_price",
|
|
|
|
"constraints_product_price_gt_0",
|
|
|
|
):
|
|
|
|
with self.subTest(expected_name):
|
|
|
|
self.assertIn(expected_name, constraints)
|
|
|
|
|
2019-07-13 23:24:35 +00:00
|
|
|
@skipUnlessDBFeature(
|
|
|
|
"supports_table_check_constraints", "can_introspect_check_constraints"
|
|
|
|
)
|
2019-07-05 12:15:41 +00:00
|
|
|
def test_abstract_name(self):
|
|
|
|
constraints = get_constraints(ChildModel._meta.db_table)
|
|
|
|
self.assertIn("constraints_childmodel_adult", constraints)
|
2018-08-06 02:30:44 +00:00
|
|
|
|
2022-01-31 15:04:13 +00:00
|
|
|
def test_validate(self):
|
|
|
|
check = models.Q(price__gt=models.F("discounted_price"))
|
2024-02-26 05:14:26 +00:00
|
|
|
constraint = models.CheckConstraint(condition=check, name="price")
|
2022-01-31 15:04:13 +00:00
|
|
|
# Invalid product.
|
|
|
|
invalid_product = Product(price=10, discounted_price=42)
|
|
|
|
with self.assertRaises(ValidationError):
|
|
|
|
constraint.validate(Product, invalid_product)
|
|
|
|
with self.assertRaises(ValidationError):
|
|
|
|
constraint.validate(Product, invalid_product, exclude={"unit"})
|
|
|
|
# Fields used by the check constraint are excluded.
|
|
|
|
constraint.validate(Product, invalid_product, exclude={"price"})
|
|
|
|
constraint.validate(Product, invalid_product, exclude={"discounted_price"})
|
|
|
|
constraint.validate(
|
|
|
|
Product,
|
|
|
|
invalid_product,
|
|
|
|
exclude={"discounted_price", "price"},
|
|
|
|
)
|
|
|
|
# Valid product.
|
|
|
|
constraint.validate(Product, Product(price=10, discounted_price=5))
|
|
|
|
|
2023-02-14 20:06:45 +00:00
|
|
|
def test_validate_custom_error(self):
|
|
|
|
check = models.Q(price__gt=models.F("discounted_price"))
|
|
|
|
constraint = models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=check,
|
2023-02-14 20:06:45 +00:00
|
|
|
name="price",
|
|
|
|
violation_error_message="discount is fake",
|
|
|
|
violation_error_code="fake_discount",
|
|
|
|
)
|
|
|
|
# Invalid product.
|
|
|
|
invalid_product = Product(price=10, discounted_price=42)
|
|
|
|
msg = "discount is fake"
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg) as cm:
|
|
|
|
constraint.validate(Product, invalid_product)
|
|
|
|
self.assertEqual(cm.exception.code, "fake_discount")
|
|
|
|
|
2022-01-31 15:04:13 +00:00
|
|
|
def test_validate_boolean_expressions(self):
|
|
|
|
constraint = models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=models.expressions.ExpressionWrapper(
|
2022-01-31 15:04:13 +00:00
|
|
|
models.Q(price__gt=500) | models.Q(price__lt=500),
|
|
|
|
output_field=models.BooleanField(),
|
|
|
|
),
|
|
|
|
name="price_neq_500_wrap",
|
|
|
|
)
|
|
|
|
msg = f"Constraint “{constraint.name}” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint.validate(Product, Product(price=500, discounted_price=5))
|
|
|
|
constraint.validate(Product, Product(price=501, discounted_price=5))
|
|
|
|
constraint.validate(Product, Product(price=499, discounted_price=5))
|
|
|
|
|
|
|
|
def test_validate_rawsql_expressions_noop(self):
|
|
|
|
constraint = models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=models.expressions.RawSQL(
|
2022-01-31 15:04:13 +00:00
|
|
|
"price < %s OR price > %s",
|
|
|
|
(500, 500),
|
|
|
|
output_field=models.BooleanField(),
|
|
|
|
),
|
|
|
|
name="price_neq_500_raw",
|
|
|
|
)
|
|
|
|
# RawSQL can not be checked and is always considered valid.
|
|
|
|
constraint.validate(Product, Product(price=500, discounted_price=5))
|
|
|
|
constraint.validate(Product, Product(price=501, discounted_price=5))
|
|
|
|
constraint.validate(Product, Product(price=499, discounted_price=5))
|
|
|
|
|
2022-09-08 14:02:58 +00:00
|
|
|
@skipUnlessDBFeature("supports_comparing_boolean_expr")
|
|
|
|
def test_validate_nullable_field_with_none(self):
|
|
|
|
# Nullable fields should be considered valid on None values.
|
|
|
|
constraint = models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=models.Q(price__gte=0),
|
2022-09-08 14:02:58 +00:00
|
|
|
name="positive_price",
|
|
|
|
)
|
|
|
|
constraint.validate(Product, Product())
|
|
|
|
|
|
|
|
@skipIfDBFeature("supports_comparing_boolean_expr")
|
|
|
|
def test_validate_nullable_field_with_isnull(self):
|
|
|
|
constraint = models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=models.Q(price__gte=0) | models.Q(price__isnull=True),
|
2022-09-08 14:02:58 +00:00
|
|
|
name="positive_price",
|
|
|
|
)
|
|
|
|
constraint.validate(Product, Product())
|
|
|
|
|
2023-08-03 00:47:49 +00:00
|
|
|
@skipUnlessDBFeature("supports_json_field")
|
|
|
|
def test_validate_nullable_jsonfield(self):
|
|
|
|
is_null_constraint = models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=models.Q(data__isnull=True),
|
2023-08-03 00:47:49 +00:00
|
|
|
name="nullable_data",
|
|
|
|
)
|
|
|
|
is_not_null_constraint = models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=models.Q(data__isnull=False),
|
2023-08-03 00:47:49 +00:00
|
|
|
name="nullable_data",
|
|
|
|
)
|
|
|
|
is_null_constraint.validate(JSONFieldModel, JSONFieldModel(data=None))
|
|
|
|
msg = f"Constraint “{is_null_constraint.name}” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
is_null_constraint.validate(JSONFieldModel, JSONFieldModel(data={}))
|
|
|
|
msg = f"Constraint “{is_not_null_constraint.name}” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
is_not_null_constraint.validate(JSONFieldModel, JSONFieldModel(data=None))
|
|
|
|
is_not_null_constraint.validate(JSONFieldModel, JSONFieldModel(data={}))
|
|
|
|
|
2023-08-10 14:08:11 +00:00
|
|
|
def test_validate_pk_field(self):
|
|
|
|
constraint_with_pk = models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=~models.Q(pk=models.F("age")),
|
2023-08-10 14:08:11 +00:00
|
|
|
name="pk_not_age_check",
|
|
|
|
)
|
|
|
|
constraint_with_pk.validate(ChildModel, ChildModel(pk=1, age=2))
|
|
|
|
msg = f"Constraint “{constraint_with_pk.name}” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint_with_pk.validate(ChildModel, ChildModel(pk=1, age=1))
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint_with_pk.validate(ChildModel, ChildModel(id=1, age=1))
|
|
|
|
constraint_with_pk.validate(ChildModel, ChildModel(pk=1, age=1), exclude={"pk"})
|
|
|
|
|
2024-02-14 17:34:00 +00:00
|
|
|
@skipUnlessDBFeature("supports_json_field")
|
|
|
|
def test_validate_jsonfield_exact(self):
|
|
|
|
data = {"release": "5.0.2", "version": "stable"}
|
|
|
|
json_exact_constraint = models.CheckConstraint(
|
2024-02-26 05:14:26 +00:00
|
|
|
condition=models.Q(data__version="stable"),
|
2024-02-14 17:34:00 +00:00
|
|
|
name="only_stable_version",
|
|
|
|
)
|
|
|
|
json_exact_constraint.validate(JSONFieldModel, JSONFieldModel(data=data))
|
|
|
|
|
|
|
|
data = {"release": "5.0.2", "version": "not stable"}
|
|
|
|
msg = f"Constraint “{json_exact_constraint.name}” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
json_exact_constraint.validate(JSONFieldModel, JSONFieldModel(data=data))
|
|
|
|
|
2024-07-18 07:38:06 +00:00
|
|
|
@skipUnlessDBFeature("supports_stored_generated_columns")
|
|
|
|
def test_validate_generated_field_stored(self):
|
|
|
|
self.assertGeneratedFieldIsValidated(model=GeneratedFieldStoredProduct)
|
|
|
|
|
|
|
|
@skipUnlessDBFeature("supports_virtual_generated_columns")
|
|
|
|
def test_validate_generated_field_virtual(self):
|
|
|
|
self.assertGeneratedFieldIsValidated(model=GeneratedFieldVirtualProduct)
|
|
|
|
|
|
|
|
def assertGeneratedFieldIsValidated(self, model):
|
|
|
|
constraint = models.CheckConstraint(
|
|
|
|
condition=models.Q(rebate__range=(0, 100)), name="bounded_rebate"
|
|
|
|
)
|
|
|
|
constraint.validate(model, model(price=50, discounted_price=20))
|
|
|
|
|
|
|
|
invalid_product = model(price=1200, discounted_price=500)
|
|
|
|
msg = f"Constraint “{constraint.name}” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint.validate(model, invalid_product)
|
|
|
|
|
|
|
|
# Excluding referenced or generated fields should skip validation.
|
|
|
|
constraint.validate(model, invalid_product, exclude={"price"})
|
|
|
|
constraint.validate(model, invalid_product, exclude={"rebate"})
|
|
|
|
|
2024-02-26 05:14:26 +00:00
|
|
|
def test_check_deprecation(self):
|
|
|
|
msg = "CheckConstraint.check is deprecated in favor of `.condition`."
|
|
|
|
condition = models.Q(foo="bar")
|
2024-08-09 17:03:24 +00:00
|
|
|
with self.assertWarnsMessage(RemovedInDjango60Warning, msg) as ctx:
|
2024-02-26 05:14:26 +00:00
|
|
|
constraint = models.CheckConstraint(name="constraint", check=condition)
|
2024-08-09 17:03:24 +00:00
|
|
|
self.assertEqual(ctx.filename, __file__)
|
|
|
|
with self.assertWarnsMessage(RemovedInDjango60Warning, msg) as ctx:
|
2024-02-26 05:14:26 +00:00
|
|
|
self.assertIs(constraint.check, condition)
|
2024-08-09 17:03:24 +00:00
|
|
|
self.assertEqual(ctx.filename, __file__)
|
2024-02-26 05:14:26 +00:00
|
|
|
other_condition = models.Q(something="else")
|
2024-08-09 17:03:24 +00:00
|
|
|
with self.assertWarnsMessage(RemovedInDjango60Warning, msg) as ctx:
|
2024-02-26 05:14:26 +00:00
|
|
|
constraint.check = other_condition
|
2024-08-09 17:03:24 +00:00
|
|
|
self.assertEqual(ctx.filename, __file__)
|
|
|
|
with self.assertWarnsMessage(RemovedInDjango60Warning, msg) as ctx:
|
2024-02-26 05:14:26 +00:00
|
|
|
self.assertIs(constraint.check, other_condition)
|
2024-08-09 17:03:24 +00:00
|
|
|
self.assertEqual(ctx.filename, __file__)
|
2024-02-26 05:14:26 +00:00
|
|
|
|
2024-08-05 06:22:29 +00:00
|
|
|
def test_database_default(self):
|
|
|
|
models.CheckConstraint(
|
|
|
|
condition=models.Q(field_with_db_default="field_with_db_default"),
|
|
|
|
name="check_field_with_db_default",
|
|
|
|
).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())
|
|
|
|
|
|
|
|
# Ensure that a check also does not silently pass with either
|
|
|
|
# FieldError or DatabaseError when checking with a db_default.
|
|
|
|
with self.assertRaises(ValidationError):
|
|
|
|
models.CheckConstraint(
|
|
|
|
condition=models.Q(
|
|
|
|
field_with_db_default="field_with_db_default", field="field"
|
|
|
|
),
|
|
|
|
name="check_field_with_db_default_2",
|
|
|
|
).validate(
|
|
|
|
ModelWithDatabaseDefault, ModelWithDatabaseDefault(field="not-field")
|
|
|
|
)
|
|
|
|
|
|
|
|
with self.assertRaises(ValidationError):
|
|
|
|
models.CheckConstraint(
|
|
|
|
condition=models.Q(field_with_db_default="field_with_db_default"),
|
|
|
|
name="check_field_with_db_default",
|
|
|
|
).validate(
|
|
|
|
ModelWithDatabaseDefault,
|
|
|
|
ModelWithDatabaseDefault(field_with_db_default="other value"),
|
|
|
|
)
|
|
|
|
|
2018-08-06 02:30:44 +00:00
|
|
|
|
|
|
|
class UniqueConstraintTests(TestCase):
|
|
|
|
@classmethod
|
|
|
|
def setUpTestData(cls):
|
2022-01-31 15:04:13 +00:00
|
|
|
cls.p1 = UniqueConstraintProduct.objects.create(name="p1", color="red")
|
|
|
|
cls.p2 = UniqueConstraintProduct.objects.create(name="p2")
|
2018-08-06 02:30:44 +00:00
|
|
|
|
2019-01-11 22:42:47 +00:00
|
|
|
def test_eq(self):
|
|
|
|
self.assertEqual(
|
|
|
|
models.UniqueConstraint(fields=["foo", "bar"], name="unique"),
|
|
|
|
models.UniqueConstraint(fields=["foo", "bar"], name="unique"),
|
|
|
|
)
|
2019-09-03 02:09:31 +00:00
|
|
|
self.assertEqual(
|
|
|
|
models.UniqueConstraint(fields=["foo", "bar"], name="unique"),
|
|
|
|
mock.ANY,
|
|
|
|
)
|
2019-01-11 22:42:47 +00:00
|
|
|
self.assertNotEqual(
|
|
|
|
models.UniqueConstraint(fields=["foo", "bar"], name="unique"),
|
|
|
|
models.UniqueConstraint(fields=["foo", "bar"], name="unique2"),
|
2022-02-03 19:24:19 +00:00
|
|
|
)
|
2019-01-11 22:42:47 +00:00
|
|
|
self.assertNotEqual(
|
|
|
|
models.UniqueConstraint(fields=["foo", "bar"], name="unique"),
|
|
|
|
models.UniqueConstraint(fields=["foo", "baz"], name="unique"),
|
|
|
|
)
|
|
|
|
self.assertNotEqual(
|
|
|
|
models.UniqueConstraint(fields=["foo", "bar"], name="unique"), 1
|
|
|
|
)
|
2022-07-07 02:26:49 +00:00
|
|
|
self.assertNotEqual(
|
|
|
|
models.UniqueConstraint(fields=["foo", "bar"], name="unique"),
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
violation_error_message="custom error",
|
|
|
|
),
|
|
|
|
)
|
|
|
|
self.assertNotEqual(
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
violation_error_message="custom error",
|
|
|
|
),
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
violation_error_message="other custom error",
|
|
|
|
),
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
violation_error_message="custom error",
|
|
|
|
),
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
violation_error_message="custom error",
|
|
|
|
),
|
|
|
|
)
|
2023-02-14 20:06:45 +00:00
|
|
|
self.assertNotEqual(
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
violation_error_code="custom_error",
|
|
|
|
),
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
violation_error_code="other_custom_error",
|
|
|
|
),
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
violation_error_code="custom_error",
|
|
|
|
),
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
violation_error_code="custom_error",
|
|
|
|
),
|
|
|
|
)
|
2019-01-11 22:42:47 +00:00
|
|
|
|
2018-12-27 19:21:59 +00:00
|
|
|
def test_eq_with_condition(self):
|
|
|
|
self.assertEqual(
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
condition=models.Q(foo=models.F("bar")),
|
|
|
|
),
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
condition=models.Q(foo=models.F("bar")),
|
2022-02-03 19:24:19 +00:00
|
|
|
),
|
2018-12-27 19:21:59 +00:00
|
|
|
)
|
|
|
|
self.assertNotEqual(
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
condition=models.Q(foo=models.F("bar")),
|
|
|
|
),
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
condition=models.Q(foo=models.F("baz")),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
2018-08-27 02:25:06 +00:00
|
|
|
def test_eq_with_deferrable(self):
|
|
|
|
constraint_1 = models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
deferrable=models.Deferrable.DEFERRED,
|
|
|
|
)
|
|
|
|
constraint_2 = models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique",
|
|
|
|
deferrable=models.Deferrable.IMMEDIATE,
|
|
|
|
)
|
|
|
|
self.assertEqual(constraint_1, constraint_1)
|
|
|
|
self.assertNotEqual(constraint_1, constraint_2)
|
|
|
|
|
2019-10-31 12:33:53 +00:00
|
|
|
def test_eq_with_include(self):
|
|
|
|
constraint_1 = models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="include",
|
|
|
|
include=["baz_1"],
|
|
|
|
)
|
|
|
|
constraint_2 = models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="include",
|
|
|
|
include=["baz_2"],
|
|
|
|
)
|
|
|
|
self.assertEqual(constraint_1, constraint_1)
|
|
|
|
self.assertNotEqual(constraint_1, constraint_2)
|
|
|
|
|
2020-06-11 19:37:12 +00:00
|
|
|
def test_eq_with_opclasses(self):
|
|
|
|
constraint_1 = models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="opclasses",
|
|
|
|
opclasses=["text_pattern_ops", "varchar_pattern_ops"],
|
|
|
|
)
|
|
|
|
constraint_2 = models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="opclasses",
|
|
|
|
opclasses=["varchar_pattern_ops", "text_pattern_ops"],
|
|
|
|
)
|
|
|
|
self.assertEqual(constraint_1, constraint_1)
|
|
|
|
self.assertNotEqual(constraint_1, constraint_2)
|
|
|
|
|
2021-02-06 19:45:54 +00:00
|
|
|
def test_eq_with_expressions(self):
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
Lower("title"),
|
|
|
|
F("author"),
|
|
|
|
name="book_func_uq",
|
|
|
|
)
|
|
|
|
same_constraint = models.UniqueConstraint(
|
|
|
|
Lower("title"),
|
|
|
|
"author",
|
|
|
|
name="book_func_uq",
|
|
|
|
)
|
|
|
|
another_constraint = models.UniqueConstraint(
|
|
|
|
Lower("title"),
|
|
|
|
name="book_func_uq",
|
|
|
|
)
|
|
|
|
self.assertEqual(constraint, same_constraint)
|
|
|
|
self.assertEqual(constraint, mock.ANY)
|
|
|
|
self.assertNotEqual(constraint, another_constraint)
|
|
|
|
|
2023-07-07 23:43:51 +00:00
|
|
|
def test_eq_with_nulls_distinct(self):
|
|
|
|
constraint_1 = models.UniqueConstraint(
|
|
|
|
Lower("title"),
|
|
|
|
nulls_distinct=False,
|
|
|
|
name="book_func_nulls_distinct_uq",
|
|
|
|
)
|
|
|
|
constraint_2 = models.UniqueConstraint(
|
|
|
|
Lower("title"),
|
|
|
|
nulls_distinct=True,
|
|
|
|
name="book_func_nulls_distinct_uq",
|
|
|
|
)
|
|
|
|
constraint_3 = models.UniqueConstraint(
|
|
|
|
Lower("title"),
|
|
|
|
name="book_func_nulls_distinct_uq",
|
|
|
|
)
|
|
|
|
self.assertEqual(constraint_1, constraint_1)
|
|
|
|
self.assertEqual(constraint_1, mock.ANY)
|
|
|
|
self.assertNotEqual(constraint_1, constraint_2)
|
|
|
|
self.assertNotEqual(constraint_1, constraint_3)
|
|
|
|
self.assertNotEqual(constraint_2, constraint_3)
|
|
|
|
|
2018-08-06 02:30:44 +00:00
|
|
|
def test_repr(self):
|
|
|
|
fields = ["foo", "bar"]
|
|
|
|
name = "unique_fields"
|
|
|
|
constraint = models.UniqueConstraint(fields=fields, name=name)
|
|
|
|
self.assertEqual(
|
|
|
|
repr(constraint),
|
|
|
|
"<UniqueConstraint: fields=('foo', 'bar') name='unique_fields'>",
|
|
|
|
)
|
|
|
|
|
2018-12-27 19:21:59 +00:00
|
|
|
def test_repr_with_condition(self):
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique_fields",
|
|
|
|
condition=models.Q(foo=models.F("bar")),
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
repr(constraint),
|
|
|
|
"<UniqueConstraint: fields=('foo', 'bar') name='unique_fields' "
|
|
|
|
"condition=(AND: ('foo', F(bar)))>",
|
|
|
|
)
|
|
|
|
|
2018-08-27 02:25:06 +00:00
|
|
|
def test_repr_with_deferrable(self):
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="unique_fields",
|
|
|
|
deferrable=models.Deferrable.IMMEDIATE,
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
repr(constraint),
|
|
|
|
"<UniqueConstraint: fields=('foo', 'bar') name='unique_fields' "
|
|
|
|
"deferrable=Deferrable.IMMEDIATE>",
|
|
|
|
)
|
|
|
|
|
2019-10-31 12:33:53 +00:00
|
|
|
def test_repr_with_include(self):
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="include_fields",
|
|
|
|
include=["baz_1", "baz_2"],
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
repr(constraint),
|
|
|
|
"<UniqueConstraint: fields=('foo', 'bar') name='include_fields' "
|
|
|
|
"include=('baz_1', 'baz_2')>",
|
|
|
|
)
|
|
|
|
|
2020-06-11 19:37:12 +00:00
|
|
|
def test_repr_with_opclasses(self):
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="opclasses_fields",
|
|
|
|
opclasses=["text_pattern_ops", "varchar_pattern_ops"],
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
repr(constraint),
|
|
|
|
"<UniqueConstraint: fields=('foo', 'bar') name='opclasses_fields' "
|
|
|
|
"opclasses=['text_pattern_ops', 'varchar_pattern_ops']>",
|
|
|
|
)
|
|
|
|
|
2023-07-07 23:43:51 +00:00
|
|
|
def test_repr_with_nulls_distinct(self):
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
fields=["foo", "bar"],
|
|
|
|
name="nulls_distinct_fields",
|
|
|
|
nulls_distinct=False,
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
repr(constraint),
|
|
|
|
"<UniqueConstraint: fields=('foo', 'bar') name='nulls_distinct_fields' "
|
|
|
|
"nulls_distinct=False>",
|
|
|
|
)
|
|
|
|
|
2021-02-06 19:45:54 +00:00
|
|
|
def test_repr_with_expressions(self):
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
Lower("title"),
|
|
|
|
F("author"),
|
|
|
|
name="book_func_uq",
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
repr(constraint),
|
|
|
|
"<UniqueConstraint: expressions=(Lower(F(title)), F(author)) "
|
|
|
|
"name='book_func_uq'>",
|
|
|
|
)
|
|
|
|
|
2023-02-22 20:04:05 +00:00
|
|
|
def test_repr_with_violation_error_message(self):
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
models.F("baz__lower"),
|
|
|
|
name="unique_lower_baz",
|
|
|
|
violation_error_message="BAZ",
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
repr(constraint),
|
|
|
|
(
|
|
|
|
"<UniqueConstraint: expressions=(F(baz__lower),) "
|
|
|
|
"name='unique_lower_baz' violation_error_message='BAZ'>"
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
2023-02-14 20:06:45 +00:00
|
|
|
def test_repr_with_violation_error_code(self):
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
models.F("baz__lower"),
|
|
|
|
name="unique_lower_baz",
|
|
|
|
violation_error_code="baz",
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
repr(constraint),
|
|
|
|
(
|
|
|
|
"<UniqueConstraint: expressions=(F(baz__lower),) "
|
|
|
|
"name='unique_lower_baz' violation_error_code='baz'>"
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
2018-08-06 02:30:44 +00:00
|
|
|
def test_deconstruction(self):
|
|
|
|
fields = ["foo", "bar"]
|
|
|
|
name = "unique_fields"
|
2019-01-10 19:06:17 +00:00
|
|
|
constraint = models.UniqueConstraint(fields=fields, name=name)
|
|
|
|
path, args, kwargs = constraint.deconstruct()
|
2018-08-06 02:30:44 +00:00
|
|
|
self.assertEqual(path, "django.db.models.UniqueConstraint")
|
|
|
|
self.assertEqual(args, ())
|
|
|
|
self.assertEqual(kwargs, {"fields": tuple(fields), "name": name})
|
|
|
|
|
2018-12-27 19:21:59 +00:00
|
|
|
def test_deconstruction_with_condition(self):
|
|
|
|
fields = ["foo", "bar"]
|
|
|
|
name = "unique_fields"
|
|
|
|
condition = models.Q(foo=models.F("bar"))
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
fields=fields, name=name, condition=condition
|
|
|
|
)
|
|
|
|
path, args, kwargs = constraint.deconstruct()
|
|
|
|
self.assertEqual(path, "django.db.models.UniqueConstraint")
|
|
|
|
self.assertEqual(args, ())
|
|
|
|
self.assertEqual(
|
|
|
|
kwargs, {"fields": tuple(fields), "name": name, "condition": condition}
|
|
|
|
)
|
|
|
|
|
2018-08-27 02:25:06 +00:00
|
|
|
def test_deconstruction_with_deferrable(self):
|
|
|
|
fields = ["foo"]
|
|
|
|
name = "unique_fields"
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
fields=fields,
|
|
|
|
name=name,
|
|
|
|
deferrable=models.Deferrable.DEFERRED,
|
|
|
|
)
|
|
|
|
path, args, kwargs = constraint.deconstruct()
|
|
|
|
self.assertEqual(path, "django.db.models.UniqueConstraint")
|
|
|
|
self.assertEqual(args, ())
|
|
|
|
self.assertEqual(
|
|
|
|
kwargs,
|
|
|
|
{
|
|
|
|
"fields": tuple(fields),
|
|
|
|
"name": name,
|
|
|
|
"deferrable": models.Deferrable.DEFERRED,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
2019-10-31 12:33:53 +00:00
|
|
|
def test_deconstruction_with_include(self):
|
|
|
|
fields = ["foo", "bar"]
|
|
|
|
name = "unique_fields"
|
|
|
|
include = ["baz_1", "baz_2"]
|
|
|
|
constraint = models.UniqueConstraint(fields=fields, name=name, include=include)
|
|
|
|
path, args, kwargs = constraint.deconstruct()
|
|
|
|
self.assertEqual(path, "django.db.models.UniqueConstraint")
|
|
|
|
self.assertEqual(args, ())
|
|
|
|
self.assertEqual(
|
|
|
|
kwargs,
|
|
|
|
{
|
|
|
|
"fields": tuple(fields),
|
|
|
|
"name": name,
|
|
|
|
"include": tuple(include),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
2020-06-11 19:37:12 +00:00
|
|
|
def test_deconstruction_with_opclasses(self):
|
|
|
|
fields = ["foo", "bar"]
|
|
|
|
name = "unique_fields"
|
|
|
|
opclasses = ["varchar_pattern_ops", "text_pattern_ops"]
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
fields=fields, name=name, opclasses=opclasses
|
|
|
|
)
|
|
|
|
path, args, kwargs = constraint.deconstruct()
|
|
|
|
self.assertEqual(path, "django.db.models.UniqueConstraint")
|
|
|
|
self.assertEqual(args, ())
|
|
|
|
self.assertEqual(
|
|
|
|
kwargs,
|
|
|
|
{
|
|
|
|
"fields": tuple(fields),
|
|
|
|
"name": name,
|
|
|
|
"opclasses": opclasses,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
2023-07-07 23:43:51 +00:00
|
|
|
def test_deconstruction_with_nulls_distinct(self):
|
|
|
|
fields = ["foo", "bar"]
|
|
|
|
name = "unique_fields"
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
fields=fields, name=name, nulls_distinct=True
|
|
|
|
)
|
|
|
|
path, args, kwargs = constraint.deconstruct()
|
|
|
|
self.assertEqual(path, "django.db.models.UniqueConstraint")
|
|
|
|
self.assertEqual(args, ())
|
|
|
|
self.assertEqual(
|
|
|
|
kwargs,
|
|
|
|
{
|
|
|
|
"fields": tuple(fields),
|
|
|
|
"name": name,
|
|
|
|
"nulls_distinct": True,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
2021-02-06 19:45:54 +00:00
|
|
|
def test_deconstruction_with_expressions(self):
|
|
|
|
name = "unique_fields"
|
|
|
|
constraint = models.UniqueConstraint(Lower("title"), name=name)
|
|
|
|
path, args, kwargs = constraint.deconstruct()
|
|
|
|
self.assertEqual(path, "django.db.models.UniqueConstraint")
|
|
|
|
self.assertEqual(args, (Lower("title"),))
|
|
|
|
self.assertEqual(kwargs, {"name": name})
|
|
|
|
|
2018-08-06 02:30:44 +00:00
|
|
|
def test_database_constraint(self):
|
|
|
|
with self.assertRaises(IntegrityError):
|
2019-08-10 06:41:18 +00:00
|
|
|
UniqueConstraintProduct.objects.create(
|
|
|
|
name=self.p1.name, color=self.p1.color
|
|
|
|
)
|
2018-08-06 02:30:44 +00:00
|
|
|
|
2020-04-17 05:45:52 +00:00
|
|
|
@skipUnlessDBFeature("supports_partial_indexes")
|
|
|
|
def test_database_constraint_with_condition(self):
|
|
|
|
UniqueConstraintConditionProduct.objects.create(name="p1")
|
|
|
|
UniqueConstraintConditionProduct.objects.create(name="p2")
|
|
|
|
with self.assertRaises(IntegrityError):
|
|
|
|
UniqueConstraintConditionProduct.objects.create(name="p1")
|
|
|
|
|
2018-08-06 02:30:44 +00:00
|
|
|
def test_model_validation(self):
|
2019-08-10 06:41:18 +00:00
|
|
|
msg = "Unique constraint product with this Name and Color already exists."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
UniqueConstraintProduct(
|
|
|
|
name=self.p1.name, color=self.p1.color
|
2022-01-31 15:04:13 +00:00
|
|
|
).validate_constraints()
|
2018-12-27 19:21:59 +00:00
|
|
|
|
2020-04-17 05:45:52 +00:00
|
|
|
@skipUnlessDBFeature("supports_partial_indexes")
|
2018-12-27 19:21:59 +00:00
|
|
|
def test_model_validation_with_condition(self):
|
2022-01-31 15:04:13 +00:00
|
|
|
"""
|
|
|
|
Partial unique constraints are not ignored by
|
|
|
|
Model.validate_constraints().
|
|
|
|
"""
|
2020-04-17 05:45:52 +00:00
|
|
|
obj1 = UniqueConstraintConditionProduct.objects.create(name="p1", color="red")
|
|
|
|
obj2 = UniqueConstraintConditionProduct.objects.create(name="p2")
|
2022-01-31 15:04:13 +00:00
|
|
|
UniqueConstraintConditionProduct(
|
|
|
|
name=obj1.name, color="blue"
|
|
|
|
).validate_constraints()
|
|
|
|
msg = "Constraint “name_without_color_uniq” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
UniqueConstraintConditionProduct(name=obj2.name).validate_constraints()
|
|
|
|
|
2023-02-08 15:38:55 +00:00
|
|
|
def test_model_validation_constraint_no_code_error(self):
|
|
|
|
class ValidateNoCodeErrorConstraint(UniqueConstraint):
|
|
|
|
def validate(self, model, instance, **kwargs):
|
|
|
|
raise ValidationError({"name": ValidationError("Already exists.")})
|
|
|
|
|
|
|
|
class NoCodeErrorConstraintModel(models.Model):
|
|
|
|
name = models.CharField(max_length=255)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
constraints = [
|
|
|
|
ValidateNoCodeErrorConstraint(
|
|
|
|
Lower("name"),
|
|
|
|
name="custom_validate_no_code_error",
|
|
|
|
)
|
|
|
|
]
|
|
|
|
|
|
|
|
msg = "{'name': ['Already exists.']}"
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
NoCodeErrorConstraintModel(name="test").validate_constraints()
|
|
|
|
|
2022-01-31 15:04:13 +00:00
|
|
|
def test_validate(self):
|
|
|
|
constraint = UniqueConstraintProduct._meta.constraints[0]
|
|
|
|
msg = "Unique constraint product with this Name and Color already exists."
|
|
|
|
non_unique_product = UniqueConstraintProduct(
|
|
|
|
name=self.p1.name, color=self.p1.color
|
|
|
|
)
|
2023-02-14 20:06:45 +00:00
|
|
|
with self.assertRaisesMessage(ValidationError, msg) as cm:
|
2022-01-31 15:04:13 +00:00
|
|
|
constraint.validate(UniqueConstraintProduct, non_unique_product)
|
2023-02-14 20:06:45 +00:00
|
|
|
self.assertEqual(cm.exception.code, "unique_together")
|
2022-01-31 15:04:13 +00:00
|
|
|
# Null values are ignored.
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
UniqueConstraintProduct(name=self.p2.name, color=None),
|
|
|
|
)
|
|
|
|
# Existing instances have their existing row excluded.
|
|
|
|
constraint.validate(UniqueConstraintProduct, self.p1)
|
|
|
|
# Unique fields are excluded.
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
non_unique_product,
|
|
|
|
exclude={"name"},
|
|
|
|
)
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
non_unique_product,
|
|
|
|
exclude={"color"},
|
|
|
|
)
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
non_unique_product,
|
|
|
|
exclude={"name", "color"},
|
|
|
|
)
|
|
|
|
# Validation on a child instance.
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
ChildUniqueConstraintProduct(name=self.p1.name, color=self.p1.color),
|
|
|
|
)
|
|
|
|
|
2024-07-26 15:01:48 +00:00
|
|
|
@skipUnlessDBFeature("supports_table_check_constraints")
|
2024-07-14 02:09:50 +00:00
|
|
|
def test_validate_fields_unattached(self):
|
|
|
|
Product.objects.create(price=42)
|
|
|
|
constraint = models.UniqueConstraint(fields=["price"], name="uniq_prices")
|
|
|
|
msg = "Product with this Price already exists."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint.validate(Product, Product(price=42))
|
|
|
|
|
2022-01-31 15:04:13 +00:00
|
|
|
@skipUnlessDBFeature("supports_partial_indexes")
|
|
|
|
def test_validate_condition(self):
|
|
|
|
p1 = UniqueConstraintConditionProduct.objects.create(name="p1")
|
|
|
|
constraint = UniqueConstraintConditionProduct._meta.constraints[0]
|
|
|
|
msg = "Constraint “name_without_color_uniq” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintConditionProduct,
|
|
|
|
UniqueConstraintConditionProduct(name=p1.name, color=None),
|
|
|
|
)
|
|
|
|
# Values not matching condition are ignored.
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintConditionProduct,
|
|
|
|
UniqueConstraintConditionProduct(name=p1.name, color="anything-but-none"),
|
|
|
|
)
|
|
|
|
# Existing instances have their existing row excluded.
|
|
|
|
constraint.validate(UniqueConstraintConditionProduct, p1)
|
|
|
|
# Unique field is excluded.
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintConditionProduct,
|
|
|
|
UniqueConstraintConditionProduct(name=p1.name, color=None),
|
|
|
|
exclude={"name"},
|
|
|
|
)
|
|
|
|
|
2023-02-14 20:06:45 +00:00
|
|
|
@skipUnlessDBFeature("supports_partial_indexes")
|
2024-07-14 02:09:50 +00:00
|
|
|
def test_validate_condition_custom_error(self):
|
2023-02-14 20:06:45 +00:00
|
|
|
p1 = UniqueConstraintConditionProduct.objects.create(name="p1")
|
2023-02-24 08:02:45 +00:00
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
fields=["name"],
|
|
|
|
name="name_without_color_uniq",
|
|
|
|
condition=models.Q(color__isnull=True),
|
|
|
|
violation_error_code="custom_code",
|
|
|
|
violation_error_message="Custom message",
|
|
|
|
)
|
2023-02-14 20:06:45 +00:00
|
|
|
msg = "Custom message"
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg) as cm:
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintConditionProduct,
|
|
|
|
UniqueConstraintConditionProduct(name=p1.name, color=None),
|
|
|
|
)
|
|
|
|
self.assertEqual(cm.exception.code, "custom_code")
|
|
|
|
|
2022-01-31 15:04:13 +00:00
|
|
|
def test_validate_expression(self):
|
|
|
|
constraint = models.UniqueConstraint(Lower("name"), name="name_lower_uniq")
|
|
|
|
msg = "Constraint “name_lower_uniq” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
UniqueConstraintProduct(name=self.p1.name.upper()),
|
|
|
|
)
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
UniqueConstraintProduct(name="another-name"),
|
|
|
|
)
|
|
|
|
# Existing instances have their existing row excluded.
|
|
|
|
constraint.validate(UniqueConstraintProduct, self.p1)
|
|
|
|
# Unique field is excluded.
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
UniqueConstraintProduct(name=self.p1.name.upper()),
|
|
|
|
exclude={"name"},
|
|
|
|
)
|
|
|
|
|
2023-01-26 08:31:40 +00:00
|
|
|
def test_validate_ordered_expression(self):
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
Lower("name").desc(), name="name_lower_uniq_desc"
|
|
|
|
)
|
|
|
|
msg = "Constraint “name_lower_uniq_desc” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
UniqueConstraintProduct(name=self.p1.name.upper()),
|
|
|
|
)
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
UniqueConstraintProduct(name="another-name"),
|
|
|
|
)
|
|
|
|
# Existing instances have their existing row excluded.
|
|
|
|
constraint.validate(UniqueConstraintProduct, self.p1)
|
|
|
|
# Unique field is excluded.
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
UniqueConstraintProduct(name=self.p1.name.upper()),
|
|
|
|
exclude={"name"},
|
|
|
|
)
|
|
|
|
|
2022-01-31 15:04:13 +00:00
|
|
|
def test_validate_expression_condition(self):
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
Lower("name"),
|
|
|
|
name="name_lower_without_color_uniq",
|
|
|
|
condition=models.Q(color__isnull=True),
|
|
|
|
)
|
|
|
|
non_unique_product = UniqueConstraintProduct(name=self.p2.name.upper())
|
|
|
|
msg = "Constraint “name_lower_without_color_uniq” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint.validate(UniqueConstraintProduct, non_unique_product)
|
|
|
|
# Values not matching condition are ignored.
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
UniqueConstraintProduct(name=self.p1.name, color=self.p1.color),
|
|
|
|
)
|
|
|
|
# Existing instances have their existing row excluded.
|
|
|
|
constraint.validate(UniqueConstraintProduct, self.p2)
|
|
|
|
# Unique field is excluded.
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
non_unique_product,
|
|
|
|
exclude={"name"},
|
|
|
|
)
|
|
|
|
# Field from a condition is excluded.
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
non_unique_product,
|
|
|
|
exclude={"color"},
|
|
|
|
)
|
2018-08-06 02:30:44 +00:00
|
|
|
|
2022-08-09 04:08:48 +00:00
|
|
|
def test_validate_expression_str(self):
|
|
|
|
constraint = models.UniqueConstraint("name", name="name_uniq")
|
|
|
|
msg = "Constraint “name_uniq” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
UniqueConstraintProduct(name=self.p1.name),
|
|
|
|
)
|
|
|
|
constraint.validate(
|
|
|
|
UniqueConstraintProduct,
|
|
|
|
UniqueConstraintProduct(name=self.p1.name),
|
|
|
|
exclude={"name"},
|
|
|
|
)
|
|
|
|
|
2024-07-18 07:38:06 +00:00
|
|
|
@skipUnlessDBFeature("supports_stored_generated_columns")
|
|
|
|
def test_validate_expression_generated_field_stored(self):
|
|
|
|
self.assertGeneratedFieldWithExpressionIsValidated(
|
|
|
|
model=GeneratedFieldStoredProduct
|
|
|
|
)
|
|
|
|
|
|
|
|
@skipUnlessDBFeature("supports_virtual_generated_columns")
|
|
|
|
def test_validate_expression_generated_field_virtual(self):
|
|
|
|
self.assertGeneratedFieldWithExpressionIsValidated(
|
|
|
|
model=GeneratedFieldVirtualProduct
|
|
|
|
)
|
|
|
|
|
|
|
|
def assertGeneratedFieldWithExpressionIsValidated(self, model):
|
|
|
|
constraint = UniqueConstraint(Sqrt("rebate"), name="unique_rebate_sqrt")
|
|
|
|
model.objects.create(price=100, discounted_price=84)
|
|
|
|
|
|
|
|
valid_product = model(price=100, discounted_price=75)
|
|
|
|
constraint.validate(model, valid_product)
|
|
|
|
|
|
|
|
invalid_product = model(price=20, discounted_price=4)
|
|
|
|
with self.assertRaisesMessage(
|
|
|
|
ValidationError, f"Constraint “{constraint.name}” is violated."
|
|
|
|
):
|
|
|
|
constraint.validate(model, invalid_product)
|
|
|
|
|
|
|
|
# Excluding referenced or generated fields should skip validation.
|
|
|
|
constraint.validate(model, invalid_product, exclude={"rebate"})
|
|
|
|
constraint.validate(model, invalid_product, exclude={"price"})
|
|
|
|
|
|
|
|
@skipUnlessDBFeature("supports_stored_generated_columns")
|
|
|
|
def test_validate_fields_generated_field_stored(self):
|
|
|
|
self.assertGeneratedFieldWithFieldsIsValidated(
|
|
|
|
model=GeneratedFieldStoredProduct
|
|
|
|
)
|
|
|
|
|
|
|
|
@skipUnlessDBFeature("supports_virtual_generated_columns")
|
|
|
|
def test_validate_fields_generated_field_virtual(self):
|
|
|
|
self.assertGeneratedFieldWithFieldsIsValidated(
|
|
|
|
model=GeneratedFieldVirtualProduct
|
|
|
|
)
|
|
|
|
|
|
|
|
def assertGeneratedFieldWithFieldsIsValidated(self, model):
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
fields=["lower_name"], name="lower_name_unique"
|
|
|
|
)
|
|
|
|
model.objects.create(name="Box")
|
|
|
|
constraint.validate(model, model(name="Case"))
|
|
|
|
|
|
|
|
invalid_product = model(name="BOX")
|
|
|
|
msg = str(invalid_product.unique_error_message(model, ["lower_name"]))
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint.validate(model, invalid_product)
|
|
|
|
|
|
|
|
# Excluding referenced or generated fields should skip validation.
|
|
|
|
constraint.validate(model, invalid_product, exclude={"lower_name"})
|
|
|
|
constraint.validate(model, invalid_product, exclude={"name"})
|
|
|
|
|
|
|
|
@skipUnlessDBFeature("supports_stored_generated_columns")
|
|
|
|
def test_validate_fields_generated_field_stored_nulls_distinct(self):
|
|
|
|
self.assertGeneratedFieldNullsDistinctIsValidated(
|
|
|
|
model=GeneratedFieldStoredProduct
|
|
|
|
)
|
|
|
|
|
|
|
|
@skipUnlessDBFeature("supports_virtual_generated_columns")
|
|
|
|
def test_validate_fields_generated_field_virtual_nulls_distinct(self):
|
|
|
|
self.assertGeneratedFieldNullsDistinctIsValidated(
|
|
|
|
model=GeneratedFieldVirtualProduct
|
|
|
|
)
|
|
|
|
|
|
|
|
def assertGeneratedFieldNullsDistinctIsValidated(self, model):
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
fields=["lower_name"],
|
|
|
|
name="lower_name_unique_nulls_distinct",
|
|
|
|
nulls_distinct=False,
|
|
|
|
)
|
|
|
|
model.objects.create(name=None)
|
|
|
|
valid_product = model(name="Box")
|
|
|
|
constraint.validate(model, valid_product)
|
|
|
|
|
|
|
|
invalid_product = model(name=None)
|
|
|
|
msg = str(invalid_product.unique_error_message(model, ["lower_name"]))
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint.validate(model, invalid_product)
|
|
|
|
|
2023-10-12 18:01:44 +00:00
|
|
|
@skipUnlessDBFeature("supports_table_check_constraints")
|
2023-09-22 04:01:11 +00:00
|
|
|
def test_validate_nullable_textfield_with_isnull_true(self):
|
|
|
|
is_null_constraint = models.UniqueConstraint(
|
|
|
|
"price",
|
|
|
|
"discounted_price",
|
|
|
|
condition=models.Q(unit__isnull=True),
|
|
|
|
name="uniq_prices_no_unit",
|
|
|
|
)
|
|
|
|
is_not_null_constraint = models.UniqueConstraint(
|
|
|
|
"price",
|
|
|
|
"discounted_price",
|
|
|
|
condition=models.Q(unit__isnull=False),
|
|
|
|
name="uniq_prices_unit",
|
|
|
|
)
|
|
|
|
|
|
|
|
Product.objects.create(price=2, discounted_price=1)
|
|
|
|
Product.objects.create(price=4, discounted_price=3, unit="ng/mL")
|
|
|
|
|
|
|
|
msg = "Constraint “uniq_prices_no_unit” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
is_null_constraint.validate(
|
|
|
|
Product, Product(price=2, discounted_price=1, unit=None)
|
|
|
|
)
|
|
|
|
is_null_constraint.validate(
|
|
|
|
Product, Product(price=2, discounted_price=1, unit="ng/mL")
|
|
|
|
)
|
|
|
|
is_null_constraint.validate(Product, Product(price=4, discounted_price=3))
|
|
|
|
|
|
|
|
msg = "Constraint “uniq_prices_unit” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
is_not_null_constraint.validate(
|
|
|
|
Product,
|
|
|
|
Product(price=4, discounted_price=3, unit="μg/mL"),
|
|
|
|
)
|
|
|
|
is_not_null_constraint.validate(Product, Product(price=4, discounted_price=3))
|
|
|
|
is_not_null_constraint.validate(Product, Product(price=2, discounted_price=1))
|
|
|
|
|
2024-07-26 15:01:48 +00:00
|
|
|
@skipUnlessDBFeature("supports_table_check_constraints")
|
2024-07-14 02:15:21 +00:00
|
|
|
def test_validate_nulls_distinct_fields(self):
|
|
|
|
Product.objects.create(price=42)
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
fields=["price"],
|
|
|
|
nulls_distinct=False,
|
|
|
|
name="uniq_prices_nulls_distinct",
|
|
|
|
)
|
|
|
|
constraint.validate(Product, Product(price=None))
|
|
|
|
Product.objects.create(price=None)
|
|
|
|
msg = "Product with this Price already exists."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint.validate(Product, Product(price=None))
|
|
|
|
|
2024-07-26 15:01:48 +00:00
|
|
|
@skipUnlessDBFeature("supports_table_check_constraints")
|
2024-07-14 02:18:21 +00:00
|
|
|
def test_validate_nulls_distinct_expressions(self):
|
|
|
|
Product.objects.create(price=42)
|
|
|
|
constraint = models.UniqueConstraint(
|
|
|
|
Abs("price"),
|
|
|
|
nulls_distinct=False,
|
|
|
|
name="uniq_prices_nulls_distinct",
|
|
|
|
)
|
|
|
|
constraint.validate(Product, Product(price=None))
|
|
|
|
Product.objects.create(price=None)
|
|
|
|
msg = f"Constraint “{constraint.name}” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
constraint.validate(Product, Product(price=None))
|
|
|
|
|
2018-08-06 02:30:44 +00:00
|
|
|
def test_name(self):
|
2019-08-10 06:41:18 +00:00
|
|
|
constraints = get_constraints(UniqueConstraintProduct._meta.db_table)
|
2018-12-27 19:21:59 +00:00
|
|
|
expected_name = "name_color_uniq"
|
2018-08-06 02:30:44 +00:00
|
|
|
self.assertIn(expected_name, constraints)
|
2018-12-27 19:21:59 +00:00
|
|
|
|
|
|
|
def test_condition_must_be_q(self):
|
|
|
|
with self.assertRaisesMessage(
|
|
|
|
ValueError, "UniqueConstraint.condition must be a Q instance."
|
|
|
|
):
|
|
|
|
models.UniqueConstraint(name="uniq", fields=["name"], condition="invalid")
|
2018-08-27 02:25:06 +00:00
|
|
|
|
|
|
|
@skipUnlessDBFeature("supports_deferrable_unique_constraints")
|
|
|
|
def test_initially_deferred_database_constraint(self):
|
|
|
|
obj_1 = UniqueConstraintDeferrable.objects.create(name="p1", shelf="front")
|
|
|
|
obj_2 = UniqueConstraintDeferrable.objects.create(name="p2", shelf="back")
|
|
|
|
|
|
|
|
def swap():
|
|
|
|
obj_1.name, obj_2.name = obj_2.name, obj_1.name
|
|
|
|
obj_1.save()
|
|
|
|
obj_2.save()
|
|
|
|
|
|
|
|
swap()
|
|
|
|
# Behavior can be changed with SET CONSTRAINTS.
|
|
|
|
with self.assertRaises(IntegrityError):
|
|
|
|
with atomic(), connection.cursor() as cursor:
|
|
|
|
constraint_name = connection.ops.quote_name("name_init_deferred_uniq")
|
|
|
|
cursor.execute("SET CONSTRAINTS %s IMMEDIATE" % constraint_name)
|
|
|
|
swap()
|
|
|
|
|
|
|
|
@skipUnlessDBFeature("supports_deferrable_unique_constraints")
|
|
|
|
def test_initially_immediate_database_constraint(self):
|
|
|
|
obj_1 = UniqueConstraintDeferrable.objects.create(name="p1", shelf="front")
|
|
|
|
obj_2 = UniqueConstraintDeferrable.objects.create(name="p2", shelf="back")
|
|
|
|
obj_1.shelf, obj_2.shelf = obj_2.shelf, obj_1.shelf
|
|
|
|
with self.assertRaises(IntegrityError), atomic():
|
|
|
|
obj_1.save()
|
|
|
|
# Behavior can be changed with SET CONSTRAINTS.
|
|
|
|
with connection.cursor() as cursor:
|
|
|
|
constraint_name = connection.ops.quote_name("sheld_init_immediate_uniq")
|
|
|
|
cursor.execute("SET CONSTRAINTS %s DEFERRED" % constraint_name)
|
|
|
|
obj_1.save()
|
|
|
|
obj_2.save()
|
|
|
|
|
|
|
|
def test_deferrable_with_condition(self):
|
|
|
|
message = "UniqueConstraint with conditions cannot be deferred."
|
|
|
|
with self.assertRaisesMessage(ValueError, message):
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["name"],
|
|
|
|
name="name_without_color_unique",
|
|
|
|
condition=models.Q(color__isnull=True),
|
|
|
|
deferrable=models.Deferrable.DEFERRED,
|
|
|
|
)
|
|
|
|
|
2020-06-29 04:41:23 +00:00
|
|
|
def test_deferrable_with_include(self):
|
|
|
|
message = "UniqueConstraint with include fields cannot be deferred."
|
|
|
|
with self.assertRaisesMessage(ValueError, message):
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["name"],
|
|
|
|
name="name_inc_color_color_unique",
|
|
|
|
include=["color"],
|
|
|
|
deferrable=models.Deferrable.DEFERRED,
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_deferrable_with_opclasses(self):
|
|
|
|
message = "UniqueConstraint with opclasses cannot be deferred."
|
|
|
|
with self.assertRaisesMessage(ValueError, message):
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["name"],
|
|
|
|
name="name_text_pattern_ops_unique",
|
|
|
|
opclasses=["text_pattern_ops"],
|
|
|
|
deferrable=models.Deferrable.DEFERRED,
|
|
|
|
)
|
|
|
|
|
2021-02-06 19:45:54 +00:00
|
|
|
def test_deferrable_with_expressions(self):
|
|
|
|
message = "UniqueConstraint with expressions cannot be deferred."
|
|
|
|
with self.assertRaisesMessage(ValueError, message):
|
|
|
|
models.UniqueConstraint(
|
|
|
|
Lower("name"),
|
|
|
|
name="deferred_expression_unique",
|
|
|
|
deferrable=models.Deferrable.DEFERRED,
|
|
|
|
)
|
|
|
|
|
2018-08-27 02:25:06 +00:00
|
|
|
def test_invalid_defer_argument(self):
|
|
|
|
message = "UniqueConstraint.deferrable must be a Deferrable instance."
|
2023-07-09 17:47:21 +00:00
|
|
|
with self.assertRaisesMessage(TypeError, message):
|
2018-08-27 02:25:06 +00:00
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["name"],
|
|
|
|
name="name_invalid",
|
|
|
|
deferrable="invalid",
|
|
|
|
)
|
2019-10-31 12:33:53 +00:00
|
|
|
|
|
|
|
@skipUnlessDBFeature(
|
|
|
|
"supports_table_check_constraints",
|
|
|
|
"supports_covering_indexes",
|
|
|
|
)
|
|
|
|
def test_include_database_constraint(self):
|
|
|
|
UniqueConstraintInclude.objects.create(name="p1", color="red")
|
|
|
|
with self.assertRaises(IntegrityError):
|
|
|
|
UniqueConstraintInclude.objects.create(name="p1", color="blue")
|
|
|
|
|
|
|
|
def test_invalid_include_argument(self):
|
|
|
|
msg = "UniqueConstraint.include must be a list or tuple."
|
2023-07-09 17:47:21 +00:00
|
|
|
with self.assertRaisesMessage(TypeError, msg):
|
2019-10-31 12:33:53 +00:00
|
|
|
models.UniqueConstraint(
|
|
|
|
name="uniq_include",
|
|
|
|
fields=["field"],
|
|
|
|
include="other",
|
|
|
|
)
|
2020-06-11 19:37:12 +00:00
|
|
|
|
|
|
|
def test_invalid_opclasses_argument(self):
|
|
|
|
msg = "UniqueConstraint.opclasses must be a list or tuple."
|
2023-07-09 17:47:21 +00:00
|
|
|
with self.assertRaisesMessage(TypeError, msg):
|
2020-06-11 19:37:12 +00:00
|
|
|
models.UniqueConstraint(
|
|
|
|
name="uniq_opclasses",
|
|
|
|
fields=["field"],
|
|
|
|
opclasses="jsonb_path_ops",
|
|
|
|
)
|
|
|
|
|
2023-07-07 23:43:51 +00:00
|
|
|
def test_invalid_nulls_distinct_argument(self):
|
|
|
|
msg = "UniqueConstraint.nulls_distinct must be a bool."
|
2023-07-09 17:47:21 +00:00
|
|
|
with self.assertRaisesMessage(TypeError, msg):
|
2023-07-07 23:43:51 +00:00
|
|
|
models.UniqueConstraint(
|
|
|
|
name="uniq_opclasses", fields=["field"], nulls_distinct="NULLS DISTINCT"
|
|
|
|
)
|
|
|
|
|
2020-06-11 19:37:12 +00:00
|
|
|
def test_opclasses_and_fields_same_length(self):
|
|
|
|
msg = (
|
|
|
|
"UniqueConstraint.fields and UniqueConstraint.opclasses must have "
|
|
|
|
"the same number of elements."
|
|
|
|
)
|
|
|
|
with self.assertRaisesMessage(ValueError, msg):
|
|
|
|
models.UniqueConstraint(
|
|
|
|
name="uniq_opclasses",
|
|
|
|
fields=["field"],
|
|
|
|
opclasses=["foo", "bar"],
|
|
|
|
)
|
2021-02-06 19:45:54 +00:00
|
|
|
|
|
|
|
def test_requires_field_or_expression(self):
|
|
|
|
msg = (
|
|
|
|
"At least one field or expression is required to define a unique "
|
|
|
|
"constraint."
|
|
|
|
)
|
|
|
|
with self.assertRaisesMessage(ValueError, msg):
|
|
|
|
models.UniqueConstraint(name="name")
|
|
|
|
|
|
|
|
def test_expressions_and_fields_mutually_exclusive(self):
|
|
|
|
msg = "UniqueConstraint.fields and expressions are mutually exclusive."
|
|
|
|
with self.assertRaisesMessage(ValueError, msg):
|
|
|
|
models.UniqueConstraint(Lower("field_1"), fields=["field_2"], name="name")
|
|
|
|
|
|
|
|
def test_expressions_with_opclasses(self):
|
|
|
|
msg = (
|
|
|
|
"UniqueConstraint.opclasses cannot be used with expressions. Use "
|
|
|
|
"django.contrib.postgres.indexes.OpClass() instead."
|
|
|
|
)
|
|
|
|
with self.assertRaisesMessage(ValueError, msg):
|
|
|
|
models.UniqueConstraint(
|
|
|
|
Lower("field"),
|
|
|
|
name="test_func_opclass",
|
|
|
|
opclasses=["jsonb_path_ops"],
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_requires_name(self):
|
|
|
|
msg = "A unique constraint must be named."
|
|
|
|
with self.assertRaisesMessage(ValueError, msg):
|
|
|
|
models.UniqueConstraint(fields=["field"])
|
2024-08-05 06:22:29 +00:00
|
|
|
|
|
|
|
def test_database_default(self):
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["field_with_db_default"], name="unique_field_with_db_default"
|
|
|
|
).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())
|
|
|
|
models.UniqueConstraint(
|
|
|
|
Upper("field_with_db_default"),
|
|
|
|
name="unique_field_with_db_default_expression",
|
|
|
|
).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())
|
|
|
|
|
|
|
|
ModelWithDatabaseDefault.objects.create()
|
|
|
|
|
|
|
|
msg = (
|
|
|
|
"Model with database default with this Field with db default already "
|
|
|
|
"exists."
|
|
|
|
)
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
models.UniqueConstraint(
|
|
|
|
fields=["field_with_db_default"], name="unique_field_with_db_default"
|
|
|
|
).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())
|
|
|
|
|
|
|
|
msg = "Constraint “unique_field_with_db_default_expression” is violated."
|
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
|
models.UniqueConstraint(
|
|
|
|
Upper("field_with_db_default"),
|
|
|
|
name="unique_field_with_db_default_expression",
|
|
|
|
).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())
|