mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +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:
		
				
					committed by
					
						 Mariusz Felisiak
						Mariusz Felisiak
					
				
			
			
				
	
			
			
			
						parent
						
							ff3aaf036f
						
					
				
				
					commit
					fc30355107
				
			| @@ -902,7 +902,24 @@ class F(Combinable): | ||||
|         return query.resolve_ref(self.name, allow_joins, reuse, summarize) | ||||
|  | ||||
|     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): | ||||
|         return OrderBy(self, **kwargs) | ||||
|   | ||||
| @@ -73,6 +73,7 @@ class UniqueConstraintProduct(models.Model): | ||||
|     name = models.CharField(max_length=255) | ||||
|     color = models.CharField(max_length=32, null=True) | ||||
|     age = models.IntegerField(null=True) | ||||
|     updated = models.DateTimeField(null=True) | ||||
|  | ||||
|     class Meta: | ||||
|         constraints = [ | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| from datetime import datetime, timedelta | ||||
| from unittest import mock | ||||
|  | ||||
| from django.core.exceptions import ValidationError | ||||
| @@ -1030,6 +1031,23 @@ class UniqueConstraintTests(TestCase): | ||||
|             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): | ||||
|         constraint = models.UniqueConstraint( | ||||
|             Lower("name").desc(), name="name_lower_uniq_desc" | ||||
|   | ||||
| @@ -58,10 +58,12 @@ from django.db.models.expressions import ( | ||||
| from django.db.models.functions import ( | ||||
|     Coalesce, | ||||
|     Concat, | ||||
|     ExtractDay, | ||||
|     Left, | ||||
|     Length, | ||||
|     Lower, | ||||
|     Substr, | ||||
|     TruncDate, | ||||
|     Upper, | ||||
| ) | ||||
| from django.db.models.sql import constants | ||||
| @@ -1330,6 +1332,38 @@ class FTests(SimpleTestCase): | ||||
|         with self.assertRaisesMessage(TypeError, msg): | ||||
|             "" 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): | ||||
|     def test_F_reuse(self): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user