diff options
| author | Mark Gensler <mark.gensler@protonmail.com> | 2024-07-18 08:38:06 +0100 |
|---|---|---|
| committer | Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> | 2024-08-12 13:45:57 +0200 |
| commit | 228128618bd895ecad235d2215f4ad4e3232595d (patch) | |
| tree | e0e73d595dce097d69909bee06c86019c61dc20b /tests/constraints | |
| parent | f883bef05457a5a49eb31109429fc01737f82532 (diff) | |
Fixed #35575 -- Added support for constraint validation on GeneratedFields.
Diffstat (limited to 'tests/constraints')
| -rw-r--r-- | tests/constraints/models.py | 41 | ||||
| -rw-r--r-- | tests/constraints/tests.py | 111 |
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( |
