@@ -616,6 +616,23 @@ class Meta:
616616 ]
617617
618618
619+ # Only define nulls_distinct model for Django 5.0+
620+ if django_version >= (5 , 0 ):
621+ class UniqueConstraintNullsDistinctModel (models .Model ):
622+ name = models .CharField (max_length = 100 )
623+ code = models .CharField (max_length = 100 , null = True )
624+ category = models .CharField (max_length = 100 , null = True )
625+
626+ class Meta :
627+ constraints = [
628+ models .UniqueConstraint (
629+ name = 'unique_code_category_nulls_not_distinct' ,
630+ fields = ('code' , 'category' ),
631+ nulls_distinct = False ,
632+ ),
633+ ]
634+
635+
619636class UniqueConstraintCustomMessageCodeModel (models .Model ):
620637 username = models .CharField (max_length = 32 )
621638 company_id = models .IntegerField ()
@@ -1063,3 +1080,118 @@ def test_equality_operator(self):
10631080 assert validator == validator2
10641081 validator2 .date_field = "bar2"
10651082 assert validator != validator2
1083+
1084+
1085+ # Tests for `nulls_distinct` option (Django 5.0+)
1086+ # -----------------------------------------------
1087+
1088+ @pytest .mark .skipif (
1089+ django_version < (5 , 0 ),
1090+ reason = "nulls_distinct requires Django 5.0+"
1091+ )
1092+ class TestUniqueConstraintNullsDistinct (TestCase ):
1093+ """
1094+ Tests for UniqueConstraint with nulls_distinct=False option.
1095+ When nulls_distinct=False, NULL values should be treated as equal
1096+ for uniqueness validation.
1097+ """
1098+
1099+ def setUp (self ):
1100+ from tests .test_validators import UniqueConstraintNullsDistinctModel
1101+
1102+ class UniqueConstraintNullsDistinctSerializer (serializers .ModelSerializer ):
1103+ class Meta :
1104+ model = UniqueConstraintNullsDistinctModel
1105+ fields = ('name' , 'code' , 'category' )
1106+
1107+ self .serializer_class = UniqueConstraintNullsDistinctSerializer
1108+
1109+ def test_nulls_distinct_false_validates_null_as_duplicate (self ):
1110+ """
1111+ When nulls_distinct=False, creating a second record with NULL values
1112+ in the constrained fields should fail validation.
1113+ """
1114+ from tests .test_validators import UniqueConstraintNullsDistinctModel
1115+
1116+ # Create first record with NULL values
1117+ UniqueConstraintNullsDistinctModel .objects .create (
1118+ name = 'First' ,
1119+ code = None ,
1120+ category = None
1121+ )
1122+
1123+ # Attempt to create second record with same NULL values
1124+ serializer = self .serializer_class (data = {
1125+ 'name' : 'Second' ,
1126+ 'code' : None ,
1127+ 'category' : None
1128+ })
1129+
1130+ # Should fail validation because nulls_distinct=False
1131+ assert not serializer .is_valid ()
1132+
1133+ def test_nulls_distinct_false_allows_different_non_null_values (self ):
1134+ """
1135+ Non-NULL values should still work normally with uniqueness validation.
1136+ """
1137+ from tests .test_validators import UniqueConstraintNullsDistinctModel
1138+
1139+ # Create first record with non-NULL values
1140+ UniqueConstraintNullsDistinctModel .objects .create (
1141+ name = 'First' ,
1142+ code = 'A' ,
1143+ category = 'X'
1144+ )
1145+
1146+ # Create second record with different values - should pass
1147+ serializer = self .serializer_class (data = {
1148+ 'name' : 'Second' ,
1149+ 'code' : 'B' ,
1150+ 'category' : 'Y'
1151+ })
1152+ assert serializer .is_valid (), serializer .errors
1153+
1154+ def test_nulls_distinct_false_rejects_duplicate_non_null_values (self ):
1155+ """
1156+ Duplicate non-NULL values should still fail validation.
1157+ """
1158+ from tests .test_validators import UniqueConstraintNullsDistinctModel
1159+
1160+ # Create first record
1161+ UniqueConstraintNullsDistinctModel .objects .create (
1162+ name = 'First' ,
1163+ code = 'A' ,
1164+ category = 'X'
1165+ )
1166+
1167+ # Attempt to create duplicate - should fail
1168+ serializer = self .serializer_class (data = {
1169+ 'name' : 'Second' ,
1170+ 'code' : 'A' ,
1171+ 'category' : 'X'
1172+ })
1173+ assert not serializer .is_valid ()
1174+
1175+ def test_unique_together_validator_nulls_distinct_equality (self ):
1176+ """
1177+ Test that UniqueTogetherValidator equality considers nulls_distinct.
1178+ """
1179+ mock_queryset = MagicMock ()
1180+ validator1 = UniqueTogetherValidator (
1181+ queryset = mock_queryset ,
1182+ fields = ('a' , 'b' ),
1183+ nulls_distinct = False
1184+ )
1185+ validator2 = UniqueTogetherValidator (
1186+ queryset = mock_queryset ,
1187+ fields = ('a' , 'b' ),
1188+ nulls_distinct = False
1189+ )
1190+ validator3 = UniqueTogetherValidator (
1191+ queryset = mock_queryset ,
1192+ fields = ('a' , 'b' ),
1193+ nulls_distinct = True
1194+ )
1195+
1196+ assert validator1 == validator2
1197+ assert validator1 != validator3
0 commit comments