summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Sanders <shang.xiao.sanders@gmail.com>2024-08-05 08:22:29 +0200
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2024-08-05 17:42:19 +0200
commit333cfab512689490ea8a6a95598a94d77816fa2d (patch)
tree2783f13b4fb53f4c2b88f62a4a09715ceaa5d98e
parente88ef6a27d125da95e530be20b879fb4f540f7f6 (diff)
[5.0.x] Fixed #35638 -- Updated validate_constraints to consider db_default.
Backport of 509763c79952cde02d9f5b584af4278bdbed77b2 from main.
-rw-r--r--django/db/models/expressions.py34
-rw-r--r--django/db/models/fields/__init__.py12
-rw-r--r--docs/releases/5.0.8.txt4
-rw-r--r--tests/constraints/models.py7
-rw-r--r--tests/constraints/tests.py71
-rw-r--r--tests/postgres_tests/migrations/0002_create_test_models.py2
-rw-r--r--tests/postgres_tests/models.py2
-rw-r--r--tests/postgres_tests/test_constraints.py9
-rw-r--r--tests/validation/models.py2
-rw-r--r--tests/validation/test_unique.py14
10 files changed, 144 insertions, 13 deletions
diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
index 4c1f570766..e2b0e93328 100644
--- a/django/db/models/expressions.py
+++ b/django/db/models/expressions.py
@@ -1153,9 +1153,41 @@ class Star(Expression):
class DatabaseDefault(Expression):
- """Placeholder expression for the database default in an insert query."""
+ """
+ Expression to use DEFAULT keyword during insert otherwise the underlying expression.
+ """
+
+ def __init__(self, expression, output_field=None):
+ super().__init__(output_field)
+ self.expression = expression
+
+ def get_source_expressions(self):
+ return [self.expression]
+
+ def set_source_expressions(self, exprs):
+ (self.expression,) = exprs
+
+ def resolve_expression(
+ self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
+ ):
+ resolved_expression = self.expression.resolve_expression(
+ query=query,
+ allow_joins=allow_joins,
+ reuse=reuse,
+ summarize=summarize,
+ for_save=for_save,
+ )
+ # Defaults used outside an INSERT context should resolve to their
+ # underlying expression.
+ if not for_save:
+ return resolved_expression
+ return DatabaseDefault(
+ resolved_expression, output_field=self._output_field_or_none
+ )
def as_sql(self, compiler, connection):
+ if not connection.features.supports_default_keyword_in_insert:
+ return compiler.compile(self.expression)
return "DEFAULT", []
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 26cb1bc485..81e32f2d15 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -982,13 +982,7 @@ class Field(RegisterLookupMixin):
def pre_save(self, model_instance, add):
"""Return field's value just before saving."""
- value = getattr(model_instance, self.attname)
- if not connection.features.supports_default_keyword_in_insert:
- from django.db.models.expressions import DatabaseDefault
-
- if isinstance(value, DatabaseDefault):
- return self._db_default_expression
- return value
+ return getattr(model_instance, self.attname)
def get_prep_value(self, value):
"""Perform preliminary non-db specific value checks and conversions."""
@@ -1030,7 +1024,9 @@ class Field(RegisterLookupMixin):
if self.db_default is not NOT_PROVIDED:
from django.db.models.expressions import DatabaseDefault
- return DatabaseDefault
+ return lambda: DatabaseDefault(
+ self._db_default_expression, output_field=self
+ )
if (
not self.empty_strings_allowed
diff --git a/docs/releases/5.0.8.txt b/docs/releases/5.0.8.txt
index 31de9985c4..5cc3faec98 100644
--- a/docs/releases/5.0.8.txt
+++ b/docs/releases/5.0.8.txt
@@ -28,3 +28,7 @@ Bugfixes
* Fixed a bug in Django 5.0 that caused a system check crash when
``ModelAdmin.date_hierarchy`` was a ``GeneratedField`` with an
``output_field`` of ``DateField`` or ``DateTimeField`` (:ticket:`35628`).
+
+* Fixed a bug in Django 5.0 which caused constraint validation to either crash
+ or incorrectly raise validation errors for constraints referring to fields
+ using ``Field.db_default`` (:ticket:`35638`).
diff --git a/tests/constraints/models.py b/tests/constraints/models.py
index 3ea5cf2323..c336e110da 100644
--- a/tests/constraints/models.py
+++ b/tests/constraints/models.py
@@ -128,3 +128,10 @@ class JSONFieldModel(models.Model):
class Meta:
required_db_features = {"supports_json_field"}
+
+
+class ModelWithDatabaseDefault(models.Model):
+ field = models.CharField(max_length=255)
+ field_with_db_default = models.CharField(
+ max_length=255, db_default=models.Value("field_with_db_default")
+ )
diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py
index 67d00b67ce..2df3706995 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
+from django.db.models.functions import Abs, Lower, Upper
from django.db.transaction import atomic
from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature
from django.test.utils import ignore_warnings
@@ -14,6 +14,7 @@ from .models import (
ChildModel,
ChildUniqueConstraintProduct,
JSONFieldModel,
+ ModelWithDatabaseDefault,
Product,
UniqueConstraintConditionProduct,
UniqueConstraintDeferrable,
@@ -365,6 +366,47 @@ class CheckConstraintTests(TestCase):
constraint_with_pk.validate(ChildModel, ChildModel(id=1, age=1))
constraint_with_pk.validate(ChildModel, ChildModel(pk=1, age=1), exclude={"pk"})
+ @skipUnlessDBFeature("supports_json_field")
+ def test_validate_jsonfield_exact(self):
+ data = {"release": "5.0.2", "version": "stable"}
+ json_exact_constraint = models.CheckConstraint(
+ check=models.Q(data__version="stable"),
+ name="only_stable_version",
+ )
+ json_exact_constraint.validate(JSONFieldModel, JSONFieldModel(data=data))
+
+ data = {"release": "5.0.2", "version": "not stable"}
+ msg = f"Constraint “{json_exact_constraint.name}” is violated."
+ with self.assertRaisesMessage(ValidationError, msg):
+ json_exact_constraint.validate(JSONFieldModel, JSONFieldModel(data=data))
+
+ def test_database_default(self):
+ models.CheckConstraint(
+ check=models.Q(field_with_db_default="field_with_db_default"),
+ name="check_field_with_db_default",
+ ).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())
+
+ # Ensure that a check also does not silently pass with either
+ # FieldError or DatabaseError when checking with a db_default.
+ with self.assertRaises(ValidationError):
+ models.CheckConstraint(
+ check=models.Q(
+ field_with_db_default="field_with_db_default", field="field"
+ ),
+ name="check_field_with_db_default_2",
+ ).validate(
+ ModelWithDatabaseDefault, ModelWithDatabaseDefault(field="not-field")
+ )
+
+ with self.assertRaises(ValidationError):
+ models.CheckConstraint(
+ check=models.Q(field_with_db_default="field_with_db_default"),
+ name="check_field_with_db_default",
+ ).validate(
+ ModelWithDatabaseDefault,
+ ModelWithDatabaseDefault(field_with_db_default="other value"),
+ )
+
class UniqueConstraintTests(TestCase):
@classmethod
@@ -1220,3 +1262,30 @@ class UniqueConstraintTests(TestCase):
msg = "A unique constraint must be named."
with self.assertRaisesMessage(ValueError, msg):
models.UniqueConstraint(fields=["field"])
+
+ def test_database_default(self):
+ models.UniqueConstraint(
+ fields=["field_with_db_default"], name="unique_field_with_db_default"
+ ).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())
+ models.UniqueConstraint(
+ Upper("field_with_db_default"),
+ name="unique_field_with_db_default_expression",
+ ).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())
+
+ ModelWithDatabaseDefault.objects.create()
+
+ msg = (
+ "Model with database default with this Field with db default already "
+ "exists."
+ )
+ with self.assertRaisesMessage(ValidationError, msg):
+ models.UniqueConstraint(
+ fields=["field_with_db_default"], name="unique_field_with_db_default"
+ ).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())
+
+ msg = "Constraint “unique_field_with_db_default_expression” is violated."
+ with self.assertRaisesMessage(ValidationError, msg):
+ models.UniqueConstraint(
+ Upper("field_with_db_default"),
+ name="unique_field_with_db_default_expression",
+ ).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())
diff --git a/tests/postgres_tests/migrations/0002_create_test_models.py b/tests/postgres_tests/migrations/0002_create_test_models.py
index 011a0d729b..d5cae460d2 100644
--- a/tests/postgres_tests/migrations/0002_create_test_models.py
+++ b/tests/postgres_tests/migrations/0002_create_test_models.py
@@ -454,7 +454,7 @@ class Migration(migrations.Migration):
primary_key=True,
),
),
- ("ints", IntegerRangeField(null=True, blank=True)),
+ ("ints", IntegerRangeField(null=True, blank=True, db_default=(5, 10))),
("bigints", BigIntegerRangeField(null=True, blank=True)),
("decimals", DecimalRangeField(null=True, blank=True)),
("timestamps", DateTimeRangeField(null=True, blank=True)),
diff --git a/tests/postgres_tests/models.py b/tests/postgres_tests/models.py
index 05f2732fb8..0e81eea7ec 100644
--- a/tests/postgres_tests/models.py
+++ b/tests/postgres_tests/models.py
@@ -141,7 +141,7 @@ class LineSavedSearch(PostgreSQLModel):
class RangesModel(PostgreSQLModel):
- ints = IntegerRangeField(blank=True, null=True)
+ ints = IntegerRangeField(blank=True, null=True, db_default=(5, 10))
bigints = BigIntegerRangeField(blank=True, null=True)
decimals = DecimalRangeField(blank=True, null=True)
timestamps = DateTimeRangeField(blank=True, null=True)
diff --git a/tests/postgres_tests/test_constraints.py b/tests/postgres_tests/test_constraints.py
index e5a8e9dbe9..928947e225 100644
--- a/tests/postgres_tests/test_constraints.py
+++ b/tests/postgres_tests/test_constraints.py
@@ -1169,3 +1169,12 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
constraint_name,
self.get_constraints(ModelWithExclusionConstraint._meta.db_table),
)
+
+ def test_database_default(self):
+ constraint = ExclusionConstraint(
+ name="ints_equal", expressions=[("ints", RangeOperators.EQUAL)]
+ )
+ RangesModel.objects.create()
+ msg = "Constraint “ints_equal” is violated."
+ with self.assertRaisesMessage(ValidationError, msg):
+ constraint.validate(RangesModel, RangesModel())
diff --git a/tests/validation/models.py b/tests/validation/models.py
index 612a8dd63a..a54d72fb0e 100644
--- a/tests/validation/models.py
+++ b/tests/validation/models.py
@@ -48,7 +48,7 @@ class ModelToValidate(models.Model):
class UniqueFieldsModel(models.Model):
unique_charfield = models.CharField(max_length=100, unique=True)
- unique_integerfield = models.IntegerField(unique=True)
+ unique_integerfield = models.IntegerField(unique=True, db_default=42)
non_unique_field = models.IntegerField()
diff --git a/tests/validation/test_unique.py b/tests/validation/test_unique.py
index 4a8b3894f0..36ee6e9da0 100644
--- a/tests/validation/test_unique.py
+++ b/tests/validation/test_unique.py
@@ -146,6 +146,20 @@ class PerformUniqueChecksTest(TestCase):
mtv = ModelToValidate(number=10, name="Some Name")
mtv.full_clean()
+ def test_unique_db_default(self):
+ UniqueFieldsModel.objects.create(unique_charfield="foo", non_unique_field=42)
+ um = UniqueFieldsModel(unique_charfield="bar", non_unique_field=42)
+ with self.assertRaises(ValidationError) as cm:
+ um.full_clean()
+ self.assertEqual(
+ cm.exception.message_dict,
+ {
+ "unique_integerfield": [
+ "Unique fields model with this Unique integerfield already exists."
+ ]
+ },
+ )
+
def test_unique_for_date(self):
Post.objects.create(
title="Django 1.0 is released",