1
0
mirror of https://github.com/django/django.git synced 2025-04-01 03:56:42 +00:00

[5.1.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-13 22:18:21 -04:00 committed by Sarah Boyce
parent a2791f5ea2
commit 4d8e574379
3 changed files with 24 additions and 6 deletions

View File

@ -8,7 +8,7 @@ from django.db import connections
from django.db.models.constants import LOOKUP_SEP from django.db.models.constants import LOOKUP_SEP
from django.db.models.expressions import Exists, ExpressionList, F, RawSQL from django.db.models.expressions import Exists, ExpressionList, F, RawSQL
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
@ -642,12 +642,16 @@ 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:
if hasattr(expr, "get_expression_for_validation"): if hasattr(expr, "get_expression_for_validation"):
expr = expr.get_expression_for_validation() expr = expr.get_expression_for_validation()
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)

View File

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

View File

@ -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
@ -1070,6 +1070,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"