Fixed #35575 -- Added support for constraint validation on GeneratedFields.

This commit is contained in:
Mark Gensler 2024-07-18 08:38:06 +01:00 committed by Sarah Boyce
parent f883bef054
commit 228128618b
7 changed files with 273 additions and 44 deletions

View file

@ -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(