summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Charette <charette.s@gmail.com>2024-07-14 04:18:21 +0200
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2024-07-17 13:07:42 +0200
commitfe9bf0cef50e8e97e76424df98fd841fdc211e94 (patch)
treebbf2aaa44a69a990a191e382726233a95bce3d16
parentc30669821bcda112647af4ebf5b09d3607672909 (diff)
[5.0.x] Fixed #35594 -- Added unique nulls distinct validation for expressions.
Thanks Mark Gensler for the report. Backport of adc0b6aac3f8a5c96e1ca282bc9f46e28d20281c from main.
-rw-r--r--django/db/models/constraints.py12
-rw-r--r--docs/releases/5.0.8.txt3
-rw-r--r--tests/constraints/tests.py15
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"