Skip to content

Commit 333cfab

Browse files
shangxiaosarahboyce
authored andcommitted
[5.0.x] Fixed #35638 -- Updated validate_constraints to consider db_default.
Backport of 509763c from main.
1 parent e88ef6a commit 333cfab

File tree

10 files changed

+144
-13
lines changed

10 files changed

+144
-13
lines changed

django/db/models/expressions.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1153,9 +1153,41 @@ def as_sql(self, compiler, connection):
11531153

11541154

11551155
class DatabaseDefault(Expression):
1156-
"""Placeholder expression for the database default in an insert query."""
1156+
"""
1157+
Expression to use DEFAULT keyword during insert otherwise the underlying expression.
1158+
"""
1159+
1160+
def __init__(self, expression, output_field=None):
1161+
super().__init__(output_field)
1162+
self.expression = expression
1163+
1164+
def get_source_expressions(self):
1165+
return [self.expression]
1166+
1167+
def set_source_expressions(self, exprs):
1168+
(self.expression,) = exprs
1169+
1170+
def resolve_expression(
1171+
self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
1172+
):
1173+
resolved_expression = self.expression.resolve_expression(
1174+
query=query,
1175+
allow_joins=allow_joins,
1176+
reuse=reuse,
1177+
summarize=summarize,
1178+
for_save=for_save,
1179+
)
1180+
# Defaults used outside an INSERT context should resolve to their
1181+
# underlying expression.
1182+
if not for_save:
1183+
return resolved_expression
1184+
return DatabaseDefault(
1185+
resolved_expression, output_field=self._output_field_or_none
1186+
)
11571187

11581188
def as_sql(self, compiler, connection):
1189+
if not connection.features.supports_default_keyword_in_insert:
1190+
return compiler.compile(self.expression)
11591191
return "DEFAULT", []
11601192

11611193

django/db/models/fields/__init__.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -982,13 +982,7 @@ def get_internal_type(self):
982982

983983
def pre_save(self, model_instance, add):
984984
"""Return field's value just before saving."""
985-
value = getattr(model_instance, self.attname)
986-
if not connection.features.supports_default_keyword_in_insert:
987-
from django.db.models.expressions import DatabaseDefault
988-
989-
if isinstance(value, DatabaseDefault):
990-
return self._db_default_expression
991-
return value
985+
return getattr(model_instance, self.attname)
992986

993987
def get_prep_value(self, value):
994988
"""Perform preliminary non-db specific value checks and conversions."""
@@ -1030,7 +1024,9 @@ def _get_default(self):
10301024
if self.db_default is not NOT_PROVIDED:
10311025
from django.db.models.expressions import DatabaseDefault
10321026

1033-
return DatabaseDefault
1027+
return lambda: DatabaseDefault(
1028+
self._db_default_expression, output_field=self
1029+
)
10341030

10351031
if (
10361032
not self.empty_strings_allowed

docs/releases/5.0.8.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,7 @@ Bugfixes
2828
* Fixed a bug in Django 5.0 that caused a system check crash when
2929
``ModelAdmin.date_hierarchy`` was a ``GeneratedField`` with an
3030
``output_field`` of ``DateField`` or ``DateTimeField`` (:ticket:`35628`).
31+
32+
* Fixed a bug in Django 5.0 which caused constraint validation to either crash
33+
or incorrectly raise validation errors for constraints referring to fields
34+
using ``Field.db_default`` (:ticket:`35638`).

tests/constraints/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,10 @@ class JSONFieldModel(models.Model):
128128

129129
class Meta:
130130
required_db_features = {"supports_json_field"}
131+
132+
133+
class ModelWithDatabaseDefault(models.Model):
134+
field = models.CharField(max_length=255)
135+
field_with_db_default = models.CharField(
136+
max_length=255, db_default=models.Value("field_with_db_default")
137+
)

tests/constraints/tests.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.db import IntegrityError, connection, models
55
from django.db.models import F
66
from django.db.models.constraints import BaseConstraint, UniqueConstraint
7-
from django.db.models.functions import Abs, Lower
7+
from django.db.models.functions import Abs, Lower, Upper
88
from django.db.transaction import atomic
99
from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature
1010
from django.test.utils import ignore_warnings
@@ -14,6 +14,7 @@
1414
ChildModel,
1515
ChildUniqueConstraintProduct,
1616
JSONFieldModel,
17+
ModelWithDatabaseDefault,
1718
Product,
1819
UniqueConstraintConditionProduct,
1920
UniqueConstraintDeferrable,
@@ -365,6 +366,47 @@ def test_validate_pk_field(self):
365366
constraint_with_pk.validate(ChildModel, ChildModel(id=1, age=1))
366367
constraint_with_pk.validate(ChildModel, ChildModel(pk=1, age=1), exclude={"pk"})
367368

369+
@skipUnlessDBFeature("supports_json_field")
370+
def test_validate_jsonfield_exact(self):
371+
data = {"release": "5.0.2", "version": "stable"}
372+
json_exact_constraint = models.CheckConstraint(
373+
check=models.Q(data__version="stable"),
374+
name="only_stable_version",
375+
)
376+
json_exact_constraint.validate(JSONFieldModel, JSONFieldModel(data=data))
377+
378+
data = {"release": "5.0.2", "version": "not stable"}
379+
msg = f"Constraint “{json_exact_constraint.name}” is violated."
380+
with self.assertRaisesMessage(ValidationError, msg):
381+
json_exact_constraint.validate(JSONFieldModel, JSONFieldModel(data=data))
382+
383+
def test_database_default(self):
384+
models.CheckConstraint(
385+
check=models.Q(field_with_db_default="field_with_db_default"),
386+
name="check_field_with_db_default",
387+
).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())
388+
389+
# Ensure that a check also does not silently pass with either
390+
# FieldError or DatabaseError when checking with a db_default.
391+
with self.assertRaises(ValidationError):
392+
models.CheckConstraint(
393+
check=models.Q(
394+
field_with_db_default="field_with_db_default", field="field"
395+
),
396+
name="check_field_with_db_default_2",
397+
).validate(
398+
ModelWithDatabaseDefault, ModelWithDatabaseDefault(field="not-field")
399+
)
400+
401+
with self.assertRaises(ValidationError):
402+
models.CheckConstraint(
403+
check=models.Q(field_with_db_default="field_with_db_default"),
404+
name="check_field_with_db_default",
405+
).validate(
406+
ModelWithDatabaseDefault,
407+
ModelWithDatabaseDefault(field_with_db_default="other value"),
408+
)
409+
368410

369411
class UniqueConstraintTests(TestCase):
370412
@classmethod
@@ -1220,3 +1262,30 @@ def test_requires_name(self):
12201262
msg = "A unique constraint must be named."
12211263
with self.assertRaisesMessage(ValueError, msg):
12221264
models.UniqueConstraint(fields=["field"])
1265+
1266+
def test_database_default(self):
1267+
models.UniqueConstraint(
1268+
fields=["field_with_db_default"], name="unique_field_with_db_default"
1269+
).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())
1270+
models.UniqueConstraint(
1271+
Upper("field_with_db_default"),
1272+
name="unique_field_with_db_default_expression",
1273+
).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())
1274+
1275+
ModelWithDatabaseDefault.objects.create()
1276+
1277+
msg = (
1278+
"Model with database default with this Field with db default already "
1279+
"exists."
1280+
)
1281+
with self.assertRaisesMessage(ValidationError, msg):
1282+
models.UniqueConstraint(
1283+
fields=["field_with_db_default"], name="unique_field_with_db_default"
1284+
).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())
1285+
1286+
msg = "Constraint “unique_field_with_db_default_expression” is violated."
1287+
with self.assertRaisesMessage(ValidationError, msg):
1288+
models.UniqueConstraint(
1289+
Upper("field_with_db_default"),
1290+
name="unique_field_with_db_default_expression",
1291+
).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault())

tests/postgres_tests/migrations/0002_create_test_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ class Migration(migrations.Migration):
454454
primary_key=True,
455455
),
456456
),
457-
("ints", IntegerRangeField(null=True, blank=True)),
457+
("ints", IntegerRangeField(null=True, blank=True, db_default=(5, 10))),
458458
("bigints", BigIntegerRangeField(null=True, blank=True)),
459459
("decimals", DecimalRangeField(null=True, blank=True)),
460460
("timestamps", DateTimeRangeField(null=True, blank=True)),

tests/postgres_tests/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ class LineSavedSearch(PostgreSQLModel):
141141

142142

143143
class RangesModel(PostgreSQLModel):
144-
ints = IntegerRangeField(blank=True, null=True)
144+
ints = IntegerRangeField(blank=True, null=True, db_default=(5, 10))
145145
bigints = BigIntegerRangeField(blank=True, null=True)
146146
decimals = DecimalRangeField(blank=True, null=True)
147147
timestamps = DateTimeRangeField(blank=True, null=True)

tests/postgres_tests/test_constraints.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,3 +1169,12 @@ class Meta:
11691169
constraint_name,
11701170
self.get_constraints(ModelWithExclusionConstraint._meta.db_table),
11711171
)
1172+
1173+
def test_database_default(self):
1174+
constraint = ExclusionConstraint(
1175+
name="ints_equal", expressions=[("ints", RangeOperators.EQUAL)]
1176+
)
1177+
RangesModel.objects.create()
1178+
msg = "Constraint “ints_equal” is violated."
1179+
with self.assertRaisesMessage(ValidationError, msg):
1180+
constraint.validate(RangesModel, RangesModel())

tests/validation/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def clean(self):
4848

4949
class UniqueFieldsModel(models.Model):
5050
unique_charfield = models.CharField(max_length=100, unique=True)
51-
unique_integerfield = models.IntegerField(unique=True)
51+
unique_integerfield = models.IntegerField(unique=True, db_default=42)
5252
non_unique_field = models.IntegerField()
5353

5454

tests/validation/test_unique.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,20 @@ def test_primary_key_unique_check_not_performed_when_not_adding(self):
146146
mtv = ModelToValidate(number=10, name="Some Name")
147147
mtv.full_clean()
148148

149+
def test_unique_db_default(self):
150+
UniqueFieldsModel.objects.create(unique_charfield="foo", non_unique_field=42)
151+
um = UniqueFieldsModel(unique_charfield="bar", non_unique_field=42)
152+
with self.assertRaises(ValidationError) as cm:
153+
um.full_clean()
154+
self.assertEqual(
155+
cm.exception.message_dict,
156+
{
157+
"unique_integerfield": [
158+
"Unique fields model with this Unique integerfield already exists."
159+
]
160+
},
161+
)
162+
149163
def test_unique_for_date(self):
150164
Post.objects.create(
151165
title="Django 1.0 is released",

0 commit comments

Comments
 (0)