Skip to content

Commit

Permalink
Fixed #35594 -- Added unique nulls distinct validation for expressions.
Browse files Browse the repository at this point in the history
Thanks Mark Gensler for the report.
  • Loading branch information
charettes authored and sarahboyce committed Jul 17, 2024
1 parent 1392258 commit adc0b6a
Show file tree
Hide file tree
Showing 3 changed files with 24 additions and 6 deletions.
12 changes: 8 additions & 4 deletions django/db/models/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.db.models.constants import LOOKUP_SEP
from django.db.models.expressions import Exists, ExpressionList, F, RawSQL
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
Expand Down Expand Up @@ -642,12 +642,16 @@ def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS):
meta=model._meta, exclude=exclude
).items()
}
expressions = []
filters = []
for expr in self.expressions:
if hasattr(expr, "get_expression_for_validation"):
expr = expr.get_expression_for_validation()
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)
Expand Down
3 changes: 2 additions & 1 deletion docs/releases/5.0.8.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
15 changes: 14 additions & 1 deletion tests/constraints/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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
Expand Down Expand Up @@ -1070,6 +1070,19 @@ def test_validate_nullable_textfield_with_isnull_true(self):
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"
Expand Down

0 comments on commit adc0b6a

Please sign in to comment.