From 1eaf38fa87384fe26d1abf6e389d6df1600d4d8c Mon Sep 17 00:00:00 2001 From: Hannes Ljungberg Date: Sat, 4 Dec 2021 21:03:38 +0100 Subject: [PATCH] Fixed #33335 -- Made model validation ignore functional unique constraints. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression in 3aa545281e0c0f9fac93753e3769df9e0334dbaa. Thanks Hervé Le Roy for the report. --- django/db/models/options.py | 6 +++++- docs/ref/models/constraints.txt | 10 ++++++---- tests/validation/models.py | 11 +++++++++++ tests/validation/test_unique.py | 14 +++++++++++++- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/django/db/models/options.py b/django/db/models/options.py index 5f0f8f0e5f7..6022099e3e9 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -866,7 +866,11 @@ class Options: return [ constraint 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 diff --git a/docs/ref/models/constraints.txt b/docs/ref/models/constraints.txt index d604ec30d33..17d5c3362bb 100644 --- a/docs/ref/models/constraints.txt +++ b/docs/ref/models/constraints.txt @@ -35,10 +35,12 @@ option. not raise ``ValidationError``\s. Rather you'll get a database integrity error on ``save()``. ``UniqueConstraint``\s without a :attr:`~UniqueConstraint.condition` (i.e. non-partial unique constraints) - are different in this regard, in that they leverage the existing - ``validate_unique()`` logic, and thus enable two-stage validation. In - addition to ``IntegrityError`` on ``save()``, ``ValidationError`` is also - raised during model validation when the ``UniqueConstraint`` is violated. + and :attr:`~UniqueConstraint.expressions` (i.e. non-functional unique + constraints) are different in this regard, in that they leverage the + existing ``validate_unique()`` logic, and thus enable two-stage validation. + In addition to ``IntegrityError`` on ``save()``, ``ValidationError`` is + also raised during model validation when the ``UniqueConstraint`` is + violated. ``CheckConstraint`` =================== diff --git a/tests/validation/models.py b/tests/validation/models.py index 3a5a9cd3549..b2d705aed2d 100644 --- a/tests/validation/models.py +++ b/tests/validation/models.py @@ -2,6 +2,7 @@ from datetime import datetime from django.core.exceptions import ValidationError from django.db import models +from django.db.models.functions import Lower def validate_answer_to_universe(value): @@ -125,3 +126,13 @@ class GenericIPAddressTestModel(models.Model): class GenericIPAddrUnpackUniqueTest(models.Model): 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'), + ] diff --git a/tests/validation/test_unique.py b/tests/validation/test_unique.py index 88eb94a54e8..8bf4ca21227 100644 --- a/tests/validation/test_unique.py +++ b/tests/validation/test_unique.py @@ -8,7 +8,8 @@ from django.test import TestCase from .models import ( 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') ) + def test_func_unique_constraint_ignored(self): + m = UniqueFuncConstraintModel() + self.assertEqual( + m._get_unique_checks(), + ([(UniqueFuncConstraintModel, ('id',))], []), + ) + class PerformUniqueChecksTest(TestCase): 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.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): Post.objects.create( title="Django 1.0 is released", slug="Django 1.0",