Browse Source

Fixed #33335 -- Made model validation ignore functional unique constraints.

Regression in 3aa545281e0c0f9fac93753e3769df9e0334dbaa.

Thanks Hervé Le Roy for the report.
Hannes Ljungberg 3 years ago
parent
commit
1eaf38fa87

+ 5 - 1
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

+ 6 - 4
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``
 ===================

+ 11 - 0
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'),
+        ]

+ 13 - 1
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",