1
0
mirror of https://github.com/django/django.git synced 2025-03-09 17:02:43 +00:00

[5.0.x] Fixed #35594 -- Added unique nulls distinct validation for expressions.

Thanks Mark Gensler for the report.

Backport of adc0b6aac3f8a5c96e1ca282bc9f46e28d20281c from main.
This commit is contained in:
Simon Charette 2024-07-14 04:18:21 +02:00 committed by Sarah Boyce
parent c30669821b
commit fe9bf0cef5
3 changed files with 24 additions and 6 deletions

View File

@ -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)

View File

@ -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`).

View File

@ -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"