|
@@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError
|
|
|
from django.db import IntegrityError, connection, models
|
|
|
from django.db.models import F
|
|
|
from django.db.models.constraints import BaseConstraint, UniqueConstraint
|
|
|
-from django.db.models.functions import Abs, Lower, Upper
|
|
|
+from django.db.models.functions import Abs, Lower, Sqrt, Upper
|
|
|
from django.db.transaction import atomic
|
|
|
from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature
|
|
|
from django.test.utils import ignore_warnings
|
|
@@ -13,6 +13,8 @@ from django.utils.deprecation import RemovedInDjango60Warning
|
|
|
from .models import (
|
|
|
ChildModel,
|
|
|
ChildUniqueConstraintProduct,
|
|
|
+ GeneratedFieldStoredProduct,
|
|
|
+ GeneratedFieldVirtualProduct,
|
|
|
JSONFieldModel,
|
|
|
ModelWithDatabaseDefault,
|
|
|
Product,
|
|
@@ -384,6 +386,29 @@ class CheckConstraintTests(TestCase):
|
|
|
with self.assertRaisesMessage(ValidationError, msg):
|
|
|
json_exact_constraint.validate(JSONFieldModel, JSONFieldModel(data=data))
|
|
|
|
|
|
+ @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"})
|
|
|
+
|
|
|
def test_check_deprecation(self):
|
|
|
msg = "CheckConstraint.check is deprecated in favor of `.condition`."
|
|
|
condition = models.Q(foo="bar")
|
|
@@ -1062,6 +1087,90 @@ class UniqueConstraintTests(TestCase):
|
|
|
exclude={"name"},
|
|
|
)
|
|
|
|
|
|
+ @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)
|
|
|
+
|
|
|
@skipUnlessDBFeature("supports_table_check_constraints")
|
|
|
def test_validate_nullable_textfield_with_isnull_true(self):
|
|
|
is_null_constraint = models.UniqueConstraint(
|