summaryrefslogtreecommitdiff
path: root/tests/constraints
diff options
context:
space:
mode:
authorMark Gensler <mark.gensler@protonmail.com>2024-07-18 08:38:06 +0100
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2024-08-12 13:45:57 +0200
commit228128618bd895ecad235d2215f4ad4e3232595d (patch)
treee0e73d595dce097d69909bee06c86019c61dc20b /tests/constraints
parentf883bef05457a5a49eb31109429fc01737f82532 (diff)
Fixed #35575 -- Added support for constraint validation on GeneratedFields.
Diffstat (limited to 'tests/constraints')
-rw-r--r--tests/constraints/models.py41
-rw-r--r--tests/constraints/tests.py111
2 files changed, 151 insertions, 1 deletions
diff --git a/tests/constraints/models.py b/tests/constraints/models.py
index 983d550502..829f671cdd 100644
--- a/tests/constraints/models.py
+++ b/tests/constraints/models.py
@@ -1,4 +1,5 @@
from django.db import models
+from django.db.models.functions import Coalesce, Lower
class Product(models.Model):
@@ -28,6 +29,46 @@ class Product(models.Model):
]
+class GeneratedFieldStoredProduct(models.Model):
+ name = models.CharField(max_length=255, null=True)
+ price = models.IntegerField(null=True)
+ discounted_price = models.IntegerField(null=True)
+ rebate = models.GeneratedField(
+ expression=Coalesce("price", 0)
+ - Coalesce("discounted_price", Coalesce("price", 0)),
+ output_field=models.IntegerField(),
+ db_persist=True,
+ )
+ lower_name = models.GeneratedField(
+ expression=Lower(models.F("name")),
+ output_field=models.CharField(max_length=255, null=True),
+ db_persist=True,
+ )
+
+ class Meta:
+ required_db_features = {"supports_stored_generated_columns"}
+
+
+class GeneratedFieldVirtualProduct(models.Model):
+ name = models.CharField(max_length=255, null=True)
+ price = models.IntegerField(null=True)
+ discounted_price = models.IntegerField(null=True)
+ rebate = models.GeneratedField(
+ expression=Coalesce("price", 0)
+ - Coalesce("discounted_price", Coalesce("price", 0)),
+ output_field=models.IntegerField(),
+ db_persist=False,
+ )
+ lower_name = models.GeneratedField(
+ expression=Lower(models.F("name")),
+ output_field=models.CharField(max_length=255, null=True),
+ db_persist=False,
+ )
+
+ class Meta:
+ required_db_features = {"supports_virtual_generated_columns"}
+
+
class UniqueConstraintProduct(models.Model):
name = models.CharField(max_length=255)
color = models.CharField(max_length=32, null=True)
diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py
index 350f05f2b8..9ca889ca6d 100644
--- a/tests/constraints/tests.py
+++ b/tests/constraints/tests.py
@@ -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(