mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[5.0.x] Fixed #35594 -- Added unique nulls distinct validation for expressions.
Thanks Mark Gensler for the report.
Backport of adc0b6aac3 from main.
			
			
This commit is contained in:
		
				
					committed by
					
						 Sarah Boyce
						Sarah Boyce
					
				
			
			
				
	
			
			
			
						parent
						
							c30669821b
						
					
				
				
					commit
					fe9bf0cef5
				
			| @@ -6,7 +6,7 @@ from django.core.exceptions import FieldError, ValidationError | |||||||
| from django.db import connections | from django.db import connections | ||||||
| from django.db.models.expressions import Exists, ExpressionList, F, OrderBy | from django.db.models.expressions import Exists, ExpressionList, F, OrderBy | ||||||
| from django.db.models.indexes import IndexExpression | from django.db.models.indexes import IndexExpression | ||||||
| from django.db.models.lookups import Exact | from django.db.models.lookups import Exact, IsNull | ||||||
| from django.db.models.query_utils import Q | from django.db.models.query_utils import Q | ||||||
| from django.db.models.sql.query import Query | from django.db.models.sql.query import Query | ||||||
| from django.db.utils import DEFAULT_DB_ALIAS | from django.db.utils import DEFAULT_DB_ALIAS | ||||||
| @@ -427,13 +427,17 @@ class UniqueConstraint(BaseConstraint): | |||||||
|                     meta=model._meta, exclude=exclude |                     meta=model._meta, exclude=exclude | ||||||
|                 ).items() |                 ).items() | ||||||
|             } |             } | ||||||
|             expressions = [] |             filters = [] | ||||||
|             for expr in self.expressions: |             for expr in self.expressions: | ||||||
|                 # Ignore ordering. |                 # Ignore ordering. | ||||||
|                 if isinstance(expr, OrderBy): |                 if isinstance(expr, OrderBy): | ||||||
|                     expr = expr.expression |                     expr = expr.expression | ||||||
|                 expressions.append(Exact(expr, expr.replace_expressions(replacements))) |                 rhs = expr.replace_expressions(replacements) | ||||||
|             queryset = queryset.filter(*expressions) |                 condition = Exact(expr, rhs) | ||||||
|  |                 if self.nulls_distinct is False: | ||||||
|  |                     condition = Q(condition) | Q(IsNull(expr, True), IsNull(rhs, True)) | ||||||
|  |                 filters.append(condition) | ||||||
|  |             queryset = queryset.filter(*filters) | ||||||
|         model_class_pk = instance._get_pk_val(model._meta) |         model_class_pk = instance._get_pk_val(model._meta) | ||||||
|         if not instance._state.adding and model_class_pk is not None: |         if not instance._state.adding and model_class_pk is not None: | ||||||
|             queryset = queryset.exclude(pk=model_class_pk) |             queryset = queryset.exclude(pk=model_class_pk) | ||||||
|   | |||||||
| @@ -9,4 +9,5 @@ Django 5.0.8 fixes several bugs in 5.0.7. | |||||||
| Bugfixes | Bugfixes | ||||||
| ======== | ======== | ||||||
|  |  | ||||||
| * ... | * Added missing validation for ``UniqueConstraint(nulls_distinct=False)`` when | ||||||
|  |   using ``*expressions`` (:ticket:`35594`). | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError | |||||||
| from django.db import IntegrityError, connection, models | from django.db import IntegrityError, connection, models | ||||||
| from django.db.models import F | from django.db.models import F | ||||||
| from django.db.models.constraints import BaseConstraint, UniqueConstraint | from django.db.models.constraints import BaseConstraint, UniqueConstraint | ||||||
| from django.db.models.functions import Lower | from django.db.models.functions import Abs, Lower | ||||||
| from django.db.transaction import atomic | from django.db.transaction import atomic | ||||||
| from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature | from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature | ||||||
| from django.test.utils import ignore_warnings | from django.test.utils import ignore_warnings | ||||||
| @@ -1039,6 +1039,19 @@ class UniqueConstraintTests(TestCase): | |||||||
|         is_not_null_constraint.validate(Product, Product(price=4, discounted_price=3)) |         is_not_null_constraint.validate(Product, Product(price=4, discounted_price=3)) | ||||||
|         is_not_null_constraint.validate(Product, Product(price=2, discounted_price=1)) |         is_not_null_constraint.validate(Product, Product(price=2, discounted_price=1)) | ||||||
|  |  | ||||||
|  |     def test_validate_nulls_distinct_expressions(self): | ||||||
|  |         Product.objects.create(price=42) | ||||||
|  |         constraint = models.UniqueConstraint( | ||||||
|  |             Abs("price"), | ||||||
|  |             nulls_distinct=False, | ||||||
|  |             name="uniq_prices_nulls_distinct", | ||||||
|  |         ) | ||||||
|  |         constraint.validate(Product, Product(price=None)) | ||||||
|  |         Product.objects.create(price=None) | ||||||
|  |         msg = f"Constraint “{constraint.name}” is violated." | ||||||
|  |         with self.assertRaisesMessage(ValidationError, msg): | ||||||
|  |             constraint.validate(Product, Product(price=None)) | ||||||
|  |  | ||||||
|     def test_name(self): |     def test_name(self): | ||||||
|         constraints = get_constraints(UniqueConstraintProduct._meta.db_table) |         constraints = get_constraints(UniqueConstraintProduct._meta.db_table) | ||||||
|         expected_name = "name_color_uniq" |         expected_name = "name_color_uniq" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user