From 78f2cf86598c55a0606a49f703ffe4b951c27197 Mon Sep 17 00:00:00 2001
From: Andrea Zanotto <andrea.zanotto@zoppas.com>
Date: Wed, 2 Apr 2025 10:49:27 +0200
Subject: [PATCH 1/3] TestUniqueConstraintValidation failing on foreign key
 field.

---
 tests/test_validators.py | 22 +++++++++++++++-------
 1 file changed, 15 insertions(+), 7 deletions(-)

diff --git a/tests/test_validators.py b/tests/test_validators.py
index 29b097ef39..b0d71b91ff 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -517,11 +517,15 @@ def filter(self, **kwargs):
         assert queryset.called_with == {'race_name': 'bar', 'position': 1}
 
 
+class FancyConditionModel(models.Model):
+    id = models.IntegerField(primary_key=True)
+
+
 class UniqueConstraintModel(models.Model):
     race_name = models.CharField(max_length=100)
     position = models.IntegerField()
     global_id = models.IntegerField()
-    fancy_conditions = models.IntegerField()
+    fancy_conditions = models.ForeignKey(FancyConditionModel, on_delete=models.CASCADE)
 
     class Meta:
         constraints = [
@@ -578,23 +582,24 @@ class Meta:
 
 class TestUniqueConstraintValidation(TestCase):
     def setUp(self):
+        fancy_model_condition = FancyConditionModel.objects.create(id=1)
         self.instance = UniqueConstraintModel.objects.create(
             race_name='example',
             position=1,
             global_id=1,
-            fancy_conditions=1
+            fancy_conditions=fancy_model_condition
         )
         UniqueConstraintModel.objects.create(
             race_name='example',
             position=2,
             global_id=2,
-            fancy_conditions=1
+            fancy_conditions=fancy_model_condition
         )
         UniqueConstraintModel.objects.create(
             race_name='other',
             position=1,
             global_id=3,
-            fancy_conditions=1
+            fancy_conditions=fancy_model_condition
         )
 
     def test_repr(self):
@@ -618,24 +623,27 @@ def test_unique_together_condition(self):
         Fields used in UniqueConstraint's condition must be included
         into queryset existence check
         """
+        fancy_model_condition_9 = FancyConditionModel.objects.create(id=9)
+        fancy_model_condition_10 = FancyConditionModel.objects.create(id=10)
+        fancy_model_condition_11 = FancyConditionModel.objects.create(id=11)
         UniqueConstraintModel.objects.create(
             race_name='condition',
             position=1,
             global_id=10,
-            fancy_conditions=10,
+            fancy_conditions=fancy_model_condition_10,
         )
         serializer = UniqueConstraintSerializer(data={
             'race_name': 'condition',
             'position': 1,
             'global_id': 11,
-            'fancy_conditions': 9,
+            'fancy_conditions': fancy_model_condition_9,
         })
         assert serializer.is_valid()
         serializer = UniqueConstraintSerializer(data={
             'race_name': 'condition',
             'position': 1,
             'global_id': 11,
-            'fancy_conditions': 11,
+            'fancy_conditions': fancy_model_condition_11,
         })
         assert not serializer.is_valid()
 

From 68af9408b7ca4dc64444c940ea99a1749c6a8a0a Mon Sep 17 00:00:00 2001
From: Andrea Zanotto <andrea.zanotto@zoppas.com>
Date: Tue, 8 Apr 2025 09:45:45 +0200
Subject: [PATCH 2/3] Revert "TestUniqueConstraintValidation failing on foreign
 key field."

This reverts commit 78f2cf86598c55a0606a49f703ffe4b951c27197.
---
 tests/test_validators.py | 22 +++++++---------------
 1 file changed, 7 insertions(+), 15 deletions(-)

diff --git a/tests/test_validators.py b/tests/test_validators.py
index b0d71b91ff..29b097ef39 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -517,15 +517,11 @@ def filter(self, **kwargs):
         assert queryset.called_with == {'race_name': 'bar', 'position': 1}
 
 
-class FancyConditionModel(models.Model):
-    id = models.IntegerField(primary_key=True)
-
-
 class UniqueConstraintModel(models.Model):
     race_name = models.CharField(max_length=100)
     position = models.IntegerField()
     global_id = models.IntegerField()
-    fancy_conditions = models.ForeignKey(FancyConditionModel, on_delete=models.CASCADE)
+    fancy_conditions = models.IntegerField()
 
     class Meta:
         constraints = [
@@ -582,24 +578,23 @@ class Meta:
 
 class TestUniqueConstraintValidation(TestCase):
     def setUp(self):
-        fancy_model_condition = FancyConditionModel.objects.create(id=1)
         self.instance = UniqueConstraintModel.objects.create(
             race_name='example',
             position=1,
             global_id=1,
-            fancy_conditions=fancy_model_condition
+            fancy_conditions=1
         )
         UniqueConstraintModel.objects.create(
             race_name='example',
             position=2,
             global_id=2,
-            fancy_conditions=fancy_model_condition
+            fancy_conditions=1
         )
         UniqueConstraintModel.objects.create(
             race_name='other',
             position=1,
             global_id=3,
-            fancy_conditions=fancy_model_condition
+            fancy_conditions=1
         )
 
     def test_repr(self):
@@ -623,27 +618,24 @@ def test_unique_together_condition(self):
         Fields used in UniqueConstraint's condition must be included
         into queryset existence check
         """
-        fancy_model_condition_9 = FancyConditionModel.objects.create(id=9)
-        fancy_model_condition_10 = FancyConditionModel.objects.create(id=10)
-        fancy_model_condition_11 = FancyConditionModel.objects.create(id=11)
         UniqueConstraintModel.objects.create(
             race_name='condition',
             position=1,
             global_id=10,
-            fancy_conditions=fancy_model_condition_10,
+            fancy_conditions=10,
         )
         serializer = UniqueConstraintSerializer(data={
             'race_name': 'condition',
             'position': 1,
             'global_id': 11,
-            'fancy_conditions': fancy_model_condition_9,
+            'fancy_conditions': 9,
         })
         assert serializer.is_valid()
         serializer = UniqueConstraintSerializer(data={
             'race_name': 'condition',
             'position': 1,
             'global_id': 11,
-            'fancy_conditions': fancy_model_condition_11,
+            'fancy_conditions': 11,
         })
         assert not serializer.is_valid()
 

From 4920fd5338bdc8f2c7904d8d78a819a6282bf3c3 Mon Sep 17 00:00:00 2001
From: Andrea Zanotto <andrea.zanotto@zoppas.com>
Date: Tue, 8 Apr 2025 09:49:19 +0200
Subject: [PATCH 3/3] Add TestUniqueConstraintForeignKeyValidation test failing
 on foreign key field.

---
 tests/test_validators.py | 157 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 157 insertions(+)

diff --git a/tests/test_validators.py b/tests/test_validators.py
index 29b097ef39..e40ed5dc82 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -552,6 +552,45 @@ class Meta:
         ]
 
 
+class FancyConditionModel(models.Model):
+    id = models.IntegerField(primary_key=True)
+
+
+class UniqueConstraintForeignKeyModel(models.Model):
+    race_name = models.CharField(max_length=100)
+    position = models.IntegerField()
+    global_id = models.IntegerField()
+    fancy_conditions = models.ForeignKey(FancyConditionModel, on_delete=models.CASCADE)
+
+    class Meta:
+        constraints = [
+            models.UniqueConstraint(
+                name="unique_constraint_foreign_key_model_global_id_uniq",
+                fields=('global_id',),
+            ),
+            models.UniqueConstraint(
+                name="unique_constraint_foreign_key_model_fancy_1_uniq",
+                fields=('fancy_conditions',),
+                condition=models.Q(global_id__lte=1)
+            ),
+            models.UniqueConstraint(
+                name="unique_constraint_foreign_key_model_fancy_3_uniq",
+                fields=('fancy_conditions',),
+                condition=models.Q(global_id__gte=3)
+            ),
+            models.UniqueConstraint(
+                name="unique_constraint_foreign_key_model_together_uniq",
+                fields=('race_name', 'position'),
+                condition=models.Q(race_name='example'),
+            ),
+            models.UniqueConstraint(
+                name='unique_constraint_foreign_key_model_together_uniq2',
+                fields=('race_name', 'position'),
+                condition=models.Q(fancy_conditions__gte=10),
+            ),
+        ]
+
+
 class UniqueConstraintNullableModel(models.Model):
     title = models.CharField(max_length=100)
     age = models.IntegerField(null=True)
@@ -570,6 +609,12 @@ class Meta:
         fields = '__all__'
 
 
+class UniqueConstraintForeignKeySerializer(serializers.ModelSerializer):
+    class Meta:
+        model = UniqueConstraintForeignKeyModel
+        fields = '__all__'
+
+
 class UniqueConstraintNullableSerializer(serializers.ModelSerializer):
     class Meta:
         model = UniqueConstraintNullableModel
@@ -684,6 +729,118 @@ def test_nullable_unique_constraint_fields_are_not_required(self):
         self.assertIsInstance(result, UniqueConstraintNullableModel)
 
 
+class TestUniqueConstraintForeignKeyValidation(TestCase):
+    def setUp(self):
+        fancy_model_condition = FancyConditionModel.objects.create(id=1)
+        self.instance = UniqueConstraintForeignKeyModel.objects.create(
+            race_name='example',
+            position=1,
+            global_id=1,
+            fancy_conditions=fancy_model_condition
+        )
+        UniqueConstraintForeignKeyModel.objects.create(
+            race_name='example',
+            position=2,
+            global_id=2,
+            fancy_conditions=fancy_model_condition
+        )
+        UniqueConstraintForeignKeyModel.objects.create(
+            race_name='other',
+            position=1,
+            global_id=3,
+            fancy_conditions=fancy_model_condition
+        )
+
+    def test_repr(self):
+        serializer = UniqueConstraintForeignKeySerializer()
+        # the order of validators isn't deterministic so delete
+        # fancy_conditions field that has two of them
+        del serializer.fields['fancy_conditions']
+        expected = dedent(r"""
+            UniqueConstraintForeignKeySerializer\(\):
+                id = IntegerField\(label='ID', read_only=True\)
+                race_name = CharField\(max_length=100, required=True\)
+                position = IntegerField\(.*required=True\)
+                global_id = IntegerField\(.*validators=\[<UniqueValidator\(queryset=UniqueConstraintForeignKeyModel.objects.all\(\)\)>\]\)
+                class Meta:
+                    validators = \[<UniqueTogetherValidator\(queryset=UniqueConstraintForeignKeyModel.objects.all\(\), fields=\('race_name', 'position'\), condition=<Q: \(AND: \('race_name', 'example'\)\)>\)>\]
+        """)
+        assert re.search(expected, repr(serializer)) is not None
+
+    def test_unique_together_condition(self):
+        """
+        Fields used in UniqueConstraint's condition must be included
+        into queryset existence check
+        """
+        fancy_model_condition_9 = FancyConditionModel.objects.create(id=9)
+        fancy_model_condition_10 = FancyConditionModel.objects.create(id=10)
+        fancy_model_condition_11 = FancyConditionModel.objects.create(id=11)
+        UniqueConstraintForeignKeyModel.objects.create(
+            race_name='condition',
+            position=1,
+            global_id=10,
+            fancy_conditions=fancy_model_condition_10,
+        )
+        serializer = UniqueConstraintForeignKeySerializer(data={
+            'race_name': 'condition',
+            'position': 1,
+            'global_id': 11,
+            'fancy_conditions': fancy_model_condition_9,
+        })
+        assert serializer.is_valid()
+        serializer = UniqueConstraintForeignKeySerializer(data={
+            'race_name': 'condition',
+            'position': 1,
+            'global_id': 11,
+            'fancy_conditions': fancy_model_condition_11,
+        })
+        assert not serializer.is_valid()
+
+    def test_unique_together_condition_fields_required(self):
+        """
+        Fields used in UniqueConstraint's condition must be present in serializer
+        """
+        serializer = UniqueConstraintForeignKeySerializer(data={
+            'race_name': 'condition',
+            'position': 1,
+            'global_id': 11,
+        })
+        assert not serializer.is_valid()
+        assert serializer.errors == {'fancy_conditions': ['This field is required.']}
+
+        class NoFieldsSerializer(serializers.ModelSerializer):
+            class Meta:
+                model = UniqueConstraintForeignKeyModel
+                fields = ('race_name', 'position', 'global_id')
+
+        serializer = NoFieldsSerializer()
+        assert len(serializer.validators) == 1
+
+    def test_single_field_uniq_validators(self):
+        """
+        UniqueConstraint with single field must be transformed into
+        field's UniqueValidator
+        """
+        # Django 5 includes Max and Min values validators for IntegerField
+        extra_validators_qty = 2 if django_version[0] >= 5 else 0
+        serializer = UniqueConstraintForeignKeySerializer()
+        assert len(serializer.validators) == 2
+        validators = serializer.fields['global_id'].validators
+        assert len(validators) == 1 + extra_validators_qty
+        assert validators[0].queryset == UniqueConstraintForeignKeyModel.objects
+
+        validators = serializer.fields['fancy_conditions'].validators
+        assert len(validators) == 2 + extra_validators_qty
+        ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators if hasattr(v, "queryset")}
+        assert ids_in_qs == {frozenset([1]), frozenset([3])}
+
+    def test_nullable_unique_constraint_fields_are_not_required(self):
+        serializer = UniqueConstraintNullableSerializer(data={'title': 'Bob'})
+        self.assertTrue(serializer.is_valid(), serializer.errors)
+        result = serializer.save()
+        self.assertIsInstance(result, UniqueConstraintNullableModel)
+
+
 # Tests for `UniqueForDateValidator`
 # ----------------------------------