summaryrefslogtreecommitdiff
path: root/tests/model_fields
diff options
context:
space:
mode:
authorMark Gensler <mark.gensler@protonmail.com>2024-06-25 15:04:48 +0100
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2024-07-04 11:45:15 +0200
commit1005c2abd1ef0c156f449641e38c33e473989d37 (patch)
treedda3a6827743dcffd1bd35a63a259dde4a6c30d3 /tests/model_fields
parent53e674d5744faad61e52d8459c9198b2aa6f63dd (diff)
Fixed #35560 -- Made Model.full_clean() ignore GeneratedFields for constraints.
Accessing generated field values on unsaved models caused a crash when validating CheckConstraints and UniqueConstraints with expressions.
Diffstat (limited to 'tests/model_fields')
-rw-r--r--tests/model_fields/models.py76
-rw-r--r--tests/model_fields/test_generatedfield.py45
2 files changed, 121 insertions, 0 deletions
diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py
index 652c808b40..d9811ba164 100644
--- a/tests/model_fields/models.py
+++ b/tests/model_fields/models.py
@@ -609,3 +609,79 @@ class GeneratedModelNullVirtual(models.Model):
class Meta:
required_db_features = {"supports_virtual_generated_columns"}
+
+
+class GeneratedModelBase(models.Model):
+ a = models.IntegerField()
+ a_squared = models.GeneratedField(
+ expression=F("a") * F("a"),
+ output_field=models.IntegerField(),
+ db_persist=True,
+ )
+
+ class Meta:
+ abstract = True
+
+
+class GeneratedModelVirtualBase(models.Model):
+ a = models.IntegerField()
+ a_squared = models.GeneratedField(
+ expression=F("a") * F("a"),
+ output_field=models.IntegerField(),
+ db_persist=False,
+ )
+
+ class Meta:
+ abstract = True
+
+
+class GeneratedModelCheckConstraint(GeneratedModelBase):
+ class Meta:
+ required_db_features = {
+ "supports_stored_generated_columns",
+ "supports_table_check_constraints",
+ }
+ constraints = [
+ models.CheckConstraint(
+ condition=models.Q(a__gt=0),
+ name="Generated model check constraint a > 0",
+ )
+ ]
+
+
+class GeneratedModelCheckConstraintVirtual(GeneratedModelVirtualBase):
+ class Meta:
+ required_db_features = {
+ "supports_virtual_generated_columns",
+ "supports_table_check_constraints",
+ }
+ constraints = [
+ models.CheckConstraint(
+ condition=models.Q(a__gt=0),
+ name="Generated model check constraint virtual a > 0",
+ )
+ ]
+
+
+class GeneratedModelUniqueConstraint(GeneratedModelBase):
+ class Meta:
+ required_db_features = {
+ "supports_stored_generated_columns",
+ "supports_table_check_constraints",
+ }
+ constraints = [
+ models.UniqueConstraint(F("a"), name="Generated model unique constraint a"),
+ ]
+
+
+class GeneratedModelUniqueConstraintVirtual(GeneratedModelVirtualBase):
+ class Meta:
+ required_db_features = {
+ "supports_virtual_generated_columns",
+ "supports_expression_indexes",
+ }
+ constraints = [
+ models.UniqueConstraint(
+ F("a"), name="Generated model unique constraint virtual a"
+ ),
+ ]
diff --git a/tests/model_fields/test_generatedfield.py b/tests/model_fields/test_generatedfield.py
index 2fbfe3c82a..c185e19d8b 100644
--- a/tests/model_fields/test_generatedfield.py
+++ b/tests/model_fields/test_generatedfield.py
@@ -2,6 +2,7 @@ import uuid
from decimal import Decimal
from django.apps import apps
+from django.core.exceptions import ValidationError
from django.db import IntegrityError, connection
from django.db.models import (
CharField,
@@ -18,6 +19,8 @@ from django.test.utils import isolate_apps
from .models import (
Foo,
GeneratedModel,
+ GeneratedModelCheckConstraint,
+ GeneratedModelCheckConstraintVirtual,
GeneratedModelFieldWithConverters,
GeneratedModelNull,
GeneratedModelNullVirtual,
@@ -25,6 +28,8 @@ from .models import (
GeneratedModelOutputFieldDbCollationVirtual,
GeneratedModelParams,
GeneratedModelParamsVirtual,
+ GeneratedModelUniqueConstraint,
+ GeneratedModelUniqueConstraintVirtual,
GeneratedModelVirtual,
)
@@ -186,6 +191,42 @@ class GeneratedFieldTestMixin:
m = self._refresh_if_needed(m)
self.assertEqual(m.field, 3)
+ @skipUnlessDBFeature("supports_table_check_constraints")
+ def test_full_clean_with_check_constraint(self):
+ model_name = self.check_constraint_model._meta.verbose_name.capitalize()
+
+ m = self.check_constraint_model(a=2)
+ m.full_clean()
+ m.save()
+ m = self._refresh_if_needed(m)
+ self.assertEqual(m.a_squared, 4)
+
+ m = self.check_constraint_model(a=-1)
+ with self.assertRaises(ValidationError) as cm:
+ m.full_clean()
+ self.assertEqual(
+ cm.exception.message_dict,
+ {"__all__": [f"Constraint “{model_name} a > 0” is violated."]},
+ )
+
+ @skipUnlessDBFeature("supports_expression_indexes")
+ def test_full_clean_with_unique_constraint_expression(self):
+ model_name = self.unique_constraint_model._meta.verbose_name.capitalize()
+
+ m = self.unique_constraint_model(a=2)
+ m.full_clean()
+ m.save()
+ m = self._refresh_if_needed(m)
+ self.assertEqual(m.a_squared, 4)
+
+ m = self.unique_constraint_model(a=2)
+ with self.assertRaises(ValidationError) as cm:
+ m.full_clean()
+ self.assertEqual(
+ cm.exception.message_dict,
+ {"__all__": [f"Constraint “{model_name} a” is violated."]},
+ )
+
def test_create(self):
m = self.base_model.objects.create(a=1, b=2)
m = self._refresh_if_needed(m)
@@ -305,6 +346,8 @@ class GeneratedFieldTestMixin:
class StoredGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
base_model = GeneratedModel
nullable_model = GeneratedModelNull
+ check_constraint_model = GeneratedModelCheckConstraint
+ unique_constraint_model = GeneratedModelUniqueConstraint
output_field_db_collation_model = GeneratedModelOutputFieldDbCollation
params_model = GeneratedModelParams
@@ -318,5 +361,7 @@ class StoredGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
class VirtualGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
base_model = GeneratedModelVirtual
nullable_model = GeneratedModelNullVirtual
+ check_constraint_model = GeneratedModelCheckConstraintVirtual
+ unique_constraint_model = GeneratedModelUniqueConstraintVirtual
output_field_db_collation_model = GeneratedModelOutputFieldDbCollationVirtual
params_model = GeneratedModelParamsVirtual