[4.0.x] Fixed #33335 -- Made model validation ignore functional unique constraints.

Regression in 3aa545281e.

Thanks Hervé Le Roy for the report.

Backport of 1eaf38fa87 from main
This commit is contained in:
Hannes Ljungberg 2021-12-04 21:03:38 +01:00 committed by Mariusz Felisiak
parent 7bde53a7ae
commit fed7f992ac
4 changed files with 35 additions and 6 deletions

View File

@ -870,7 +870,11 @@ class Options:
return [ return [
constraint constraint
for constraint in self.constraints for constraint in self.constraints
if isinstance(constraint, UniqueConstraint) and constraint.condition is None if (
isinstance(constraint, UniqueConstraint) and
constraint.condition is None and
not constraint.contains_expressions
)
] ]
@cached_property @cached_property

View File

@ -35,10 +35,12 @@ option.
not raise ``ValidationError``\s. Rather you'll get a database integrity not raise ``ValidationError``\s. Rather you'll get a database integrity
error on ``save()``. ``UniqueConstraint``\s without a error on ``save()``. ``UniqueConstraint``\s without a
:attr:`~UniqueConstraint.condition` (i.e. non-partial unique constraints) :attr:`~UniqueConstraint.condition` (i.e. non-partial unique constraints)
are different in this regard, in that they leverage the existing and :attr:`~UniqueConstraint.expressions` (i.e. non-functional unique
``validate_unique()`` logic, and thus enable two-stage validation. In constraints) are different in this regard, in that they leverage the
addition to ``IntegrityError`` on ``save()``, ``ValidationError`` is also existing ``validate_unique()`` logic, and thus enable two-stage validation.
raised during model validation when the ``UniqueConstraint`` is violated. In addition to ``IntegrityError`` on ``save()``, ``ValidationError`` is
also raised during model validation when the ``UniqueConstraint`` is
violated.
``CheckConstraint`` ``CheckConstraint``
=================== ===================

View File

@ -2,6 +2,7 @@ from datetime import datetime
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models.functions import Lower
def validate_answer_to_universe(value): def validate_answer_to_universe(value):
@ -125,3 +126,13 @@ class GenericIPAddressTestModel(models.Model):
class GenericIPAddrUnpackUniqueTest(models.Model): class GenericIPAddrUnpackUniqueTest(models.Model):
generic_v4unpack_ip = models.GenericIPAddressField(null=True, blank=True, unique=True, unpack_ipv4=True) generic_v4unpack_ip = models.GenericIPAddressField(null=True, blank=True, unique=True, unpack_ipv4=True)
class UniqueFuncConstraintModel(models.Model):
field = models.CharField(max_length=255)
class Meta:
required_db_features = {'supports_expression_indexes'}
constraints = [
models.UniqueConstraint(Lower('field'), name='func_lower_field_uq'),
]

View File

@ -8,7 +8,8 @@ from django.test import TestCase
from .models import ( from .models import (
CustomPKModel, FlexibleDatePost, ModelToValidate, Post, UniqueErrorsModel, CustomPKModel, FlexibleDatePost, ModelToValidate, Post, UniqueErrorsModel,
UniqueFieldsModel, UniqueForDateModel, UniqueTogetherModel, UniqueFieldsModel, UniqueForDateModel, UniqueFuncConstraintModel,
UniqueTogetherModel,
) )
@ -86,6 +87,13 @@ class GetUniqueCheckTests(unittest.TestCase):
), m._get_unique_checks(exclude='start_date') ), m._get_unique_checks(exclude='start_date')
) )
def test_func_unique_constraint_ignored(self):
m = UniqueFuncConstraintModel()
self.assertEqual(
m._get_unique_checks(),
([(UniqueFuncConstraintModel, ('id',))], []),
)
class PerformUniqueChecksTest(TestCase): class PerformUniqueChecksTest(TestCase):
def test_primary_key_unique_check_not_performed_when_adding_and_pk_not_specified(self): def test_primary_key_unique_check_not_performed_when_adding_and_pk_not_specified(self):
@ -108,6 +116,10 @@ class PerformUniqueChecksTest(TestCase):
mtv = ModelToValidate(number=10, name='Some Name') mtv = ModelToValidate(number=10, name='Some Name')
mtv.full_clean() mtv.full_clean()
def test_func_unique_check_not_performed(self):
with self.assertNumQueries(0):
UniqueFuncConstraintModel(field='some name').full_clean()
def test_unique_for_date(self): def test_unique_for_date(self):
Post.objects.create( Post.objects.create(
title="Django 1.0 is released", slug="Django 1.0", title="Django 1.0 is released", slug="Django 1.0",