浏览代码

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.
Mark Gensler 9 月之前
父节点
当前提交
1005c2abd1
共有 4 个文件被更改,包括 126 次插入2 次删除
  1. 1 1
      django/db/models/base.py
  2. 4 1
      docs/releases/5.0.7.txt
  3. 76 0
      tests/model_fields/models.py
  4. 45 0
      tests/model_fields/test_generatedfield.py

+ 1 - 1
django/db/models/base.py

@@ -1340,7 +1340,7 @@ class Model(AltersData, metaclass=ModelBase):
         field_map = {
             field.name: Value(getattr(self, field.attname), field)
             for field in meta.local_concrete_fields
-            if field.name not in exclude
+            if field.name not in exclude and not field.generated
         }
         if "pk" not in exclude:
             field_map["pk"] = Value(self.pk, meta.pk)

+ 4 - 1
docs/releases/5.0.7.txt

@@ -10,4 +10,7 @@ issues with severity "low", and several bugs in 5.0.6.
 Bugfixes
 ========
 
-* ...
+* Fixed a bug in Django 5.0 that caused a crash of ``Model.full_clean()`` on
+  unsaved model instances with a ``GeneratedField`` and certain defined
+  :attr:`Meta.constraints <django.db.models.Options.constraints>`
+  (:ticket:`35560`).

+ 76 - 0
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"
+            ),
+        ]

+ 45 - 0
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