1
0
mirror of https://github.com/django/django.git synced 2025-03-31 19:46:42 +00:00

Fixed #36198 -- Implemented unresolved transform expression replacement.

This allows the proper resolving of F("field__transform") when
performing constraint validation.

Thanks Tom Hall for the report and Sarah for the test.
This commit is contained in:
Simon Charette 2025-02-18 12:43:38 -05:00 committed by Mariusz Felisiak
parent ff3aaf036f
commit fc30355107
4 changed files with 71 additions and 1 deletions

View File

@ -902,7 +902,24 @@ class F(Combinable):
return query.resolve_ref(self.name, allow_joins, reuse, summarize) return query.resolve_ref(self.name, allow_joins, reuse, summarize)
def replace_expressions(self, replacements): def replace_expressions(self, replacements):
return replacements.get(self, self) if (replacement := replacements.get(self)) is not None:
return replacement
field_name, *transforms = self.name.split(LOOKUP_SEP)
# Avoid unnecessarily looking up replacements with field_name again as
# in the vast majority of cases F instances won't be composed of any
# lookups.
if not transforms:
return self
if (
replacement := replacements.get(F(field_name))
) is None or replacement._output_field_or_none is None:
return self
for transform in transforms:
transform_class = replacement.get_transform(transform)
if transform_class is None:
return self
replacement = transform_class(replacement)
return replacement
def asc(self, **kwargs): def asc(self, **kwargs):
return OrderBy(self, **kwargs) return OrderBy(self, **kwargs)

View File

@ -73,6 +73,7 @@ class UniqueConstraintProduct(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
color = models.CharField(max_length=32, null=True) color = models.CharField(max_length=32, null=True)
age = models.IntegerField(null=True) age = models.IntegerField(null=True)
updated = models.DateTimeField(null=True)
class Meta: class Meta:
constraints = [ constraints = [

View File

@ -1,3 +1,4 @@
from datetime import datetime, timedelta
from unittest import mock from unittest import mock
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -1030,6 +1031,23 @@ class UniqueConstraintTests(TestCase):
exclude={"name"}, exclude={"name"},
) )
def test_validate_field_transform(self):
updated_date = datetime(2005, 7, 26)
UniqueConstraintProduct.objects.create(name="p1", updated=updated_date)
constraint = models.UniqueConstraint(
models.F("updated__date"), name="date_created_unique"
)
msg = "Constraint “date_created_unique” is violated."
with self.assertRaisesMessage(ValidationError, msg):
constraint.validate(
UniqueConstraintProduct,
UniqueConstraintProduct(updated=updated_date),
)
constraint.validate(
UniqueConstraintProduct,
UniqueConstraintProduct(updated=updated_date + timedelta(days=1)),
)
def test_validate_ordered_expression(self): def test_validate_ordered_expression(self):
constraint = models.UniqueConstraint( constraint = models.UniqueConstraint(
Lower("name").desc(), name="name_lower_uniq_desc" Lower("name").desc(), name="name_lower_uniq_desc"

View File

@ -58,10 +58,12 @@ from django.db.models.expressions import (
from django.db.models.functions import ( from django.db.models.functions import (
Coalesce, Coalesce,
Concat, Concat,
ExtractDay,
Left, Left,
Length, Length,
Lower, Lower,
Substr, Substr,
TruncDate,
Upper, Upper,
) )
from django.db.models.sql import constants from django.db.models.sql import constants
@ -1330,6 +1332,38 @@ class FTests(SimpleTestCase):
with self.assertRaisesMessage(TypeError, msg): with self.assertRaisesMessage(TypeError, msg):
"" in F("name") "" in F("name")
def test_replace_expressions_transform(self):
replacements = {F("timestamp"): Value(None)}
transform_ref = F("timestamp__date")
self.assertIs(transform_ref.replace_expressions(replacements), transform_ref)
invalid_transform_ref = F("timestamp__invalid")
self.assertIs(
invalid_transform_ref.replace_expressions(replacements),
invalid_transform_ref,
)
replacements = {F("timestamp"): Value(datetime.datetime(2025, 3, 1, 14, 10))}
self.assertEqual(
F("timestamp__date").replace_expressions(replacements),
TruncDate(Value(datetime.datetime(2025, 3, 1, 14, 10))),
)
self.assertEqual(
F("timestamp__date__day").replace_expressions(replacements),
ExtractDay(TruncDate(Value(datetime.datetime(2025, 3, 1, 14, 10)))),
)
invalid_nested_transform_ref = F("timestamp__date__invalid")
self.assertIs(
invalid_nested_transform_ref.replace_expressions(replacements),
invalid_nested_transform_ref,
)
# `replacements` is not unnecessarily looked up a second time for
# transform-less field references as it's the case the vast majority of
# the time.
mock_replacements = mock.Mock()
mock_replacements.get.return_value = None
field_ref = F("name")
self.assertIs(field_ref.replace_expressions(mock_replacements), field_ref)
mock_replacements.get.assert_called_once_with(field_ref)
class ExpressionsTests(TestCase): class ExpressionsTests(TestCase):
def test_F_reuse(self): def test_F_reuse(self):