From fe9bf0cef50e8e97e76424df98fd841fdc211e94 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Sun, 14 Jul 2024 04:18:21 +0200 Subject: [PATCH] [5.0.x] Fixed #35594 -- Added unique nulls distinct validation for expressions. Thanks Mark Gensler for the report. Backport of adc0b6aac3f8a5c96e1ca282bc9f46e28d20281c from main. --- django/db/models/constraints.py | 12 ++++++++---- docs/releases/5.0.8.txt | 3 ++- tests/constraints/tests.py | 15 ++++++++++++++- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/django/db/models/constraints.py b/django/db/models/constraints.py index 7e269b2418..ca9eef920b 100644 --- a/django/db/models/constraints.py +++ b/django/db/models/constraints.py @@ -6,7 +6,7 @@ from django.core.exceptions import FieldError, ValidationError from django.db import connections from django.db.models.expressions import Exists, ExpressionList, F, OrderBy 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.sql.query import Query from django.db.utils import DEFAULT_DB_ALIAS @@ -427,13 +427,17 @@ class UniqueConstraint(BaseConstraint): meta=model._meta, exclude=exclude ).items() } - expressions = [] + filters = [] for expr in self.expressions: # Ignore ordering. if isinstance(expr, OrderBy): expr = expr.expression - expressions.append(Exact(expr, expr.replace_expressions(replacements))) - queryset = queryset.filter(*expressions) + rhs = expr.replace_expressions(replacements) + 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) if not instance._state.adding and model_class_pk is not None: queryset = queryset.exclude(pk=model_class_pk) diff --git a/docs/releases/5.0.8.txt b/docs/releases/5.0.8.txt index 1c30ed4766..8e072049b2 100644 --- a/docs/releases/5.0.8.txt +++ b/docs/releases/5.0.8.txt @@ -9,4 +9,5 @@ Django 5.0.8 fixes several bugs in 5.0.7. Bugfixes ======== -* ... +* Added missing validation for ``UniqueConstraint(nulls_distinct=False)`` when + using ``*expressions`` (:ticket:`35594`). diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py index 30b9472c31..b7fe7cc8db 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 Lower +from django.db.models.functions import Abs, Lower from django.db.transaction import atomic from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature 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=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): constraints = get_constraints(UniqueConstraintProduct._meta.db_table) expected_name = "name_color_uniq"