-
-
Notifications
You must be signed in to change notification settings - Fork 7.1k
Add nulls_distinct support to UniqueTogetherValidator #9866
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds support for Django 5.0's nulls_distinct parameter to the UniqueTogetherValidator class in Django REST Framework. When nulls_distinct=False is set on a UniqueConstraint, the validator now properly treats NULL values as equal for uniqueness checks, preventing integrity errors on databases like Oracle where NULLs can violate unique constraints.
Changes:
- Modified
get_unique_together_constraints()to extract and yield thenulls_distinctattribute from constraints - Updated
UniqueTogetherValidatorto accept and handle thenulls_distinctparameter in validation logic - Added comprehensive tests for the new functionality with Django 5.0+ version guards
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| rest_framework/serializers.py | Updated get_unique_together_constraints() to yield nulls_distinct from constraints and modified call sites to unpack the additional tuple element |
| rest_framework/validators.py | Added nulls_distinct parameter to UniqueTogetherValidator, updated validation logic to skip NULL checks when nulls_distinct=False, and modified __repr__() and __eq__() methods |
| tests/test_validators.py | Added test model with nulls_distinct=False constraint and comprehensive test cases for create operations and validator equality |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| class TestUniqueConstraintNullsDistinct(TestCase): | ||
| """ | ||
| Tests for UniqueConstraint with nulls_distinct=False option. | ||
| When nulls_distinct=False, NULL values should be treated as equal | ||
| for uniqueness validation. | ||
| """ | ||
|
|
||
| def setUp(self): | ||
| from tests.test_validators import UniqueConstraintNullsDistinctModel | ||
|
|
||
| class UniqueConstraintNullsDistinctSerializer(serializers.ModelSerializer): | ||
| class Meta: | ||
| model = UniqueConstraintNullsDistinctModel | ||
| fields = ('name', 'code', 'category') | ||
|
|
||
| self.serializer_class = UniqueConstraintNullsDistinctSerializer | ||
|
|
||
| def test_nulls_distinct_false_validates_null_as_duplicate(self): | ||
| """ | ||
| When nulls_distinct=False, creating a second record with NULL values | ||
| in the constrained fields should fail validation. | ||
| """ | ||
| from tests.test_validators import UniqueConstraintNullsDistinctModel | ||
|
|
||
| # Create first record with NULL values | ||
| UniqueConstraintNullsDistinctModel.objects.create( | ||
| name='First', | ||
| code=None, | ||
| category=None | ||
| ) | ||
|
|
||
| # Attempt to create second record with same NULL values | ||
| serializer = self.serializer_class(data={ | ||
| 'name': 'Second', | ||
| 'code': None, | ||
| 'category': None | ||
| }) | ||
|
|
||
| # Should fail validation because nulls_distinct=False | ||
| assert not serializer.is_valid() | ||
|
|
||
| def test_nulls_distinct_false_allows_different_non_null_values(self): | ||
| """ | ||
| Non-NULL values should still work normally with uniqueness validation. | ||
| """ | ||
| from tests.test_validators import UniqueConstraintNullsDistinctModel | ||
|
|
||
| # Create first record with non-NULL values | ||
| UniqueConstraintNullsDistinctModel.objects.create( | ||
| name='First', | ||
| code='A', | ||
| category='X' | ||
| ) | ||
|
|
||
| # Create second record with different values - should pass | ||
| serializer = self.serializer_class(data={ | ||
| 'name': 'Second', | ||
| 'code': 'B', | ||
| 'category': 'Y' | ||
| }) | ||
| assert serializer.is_valid(), serializer.errors | ||
|
|
||
| def test_nulls_distinct_false_rejects_duplicate_non_null_values(self): | ||
| """ | ||
| Duplicate non-NULL values should still fail validation. | ||
| """ | ||
| from tests.test_validators import UniqueConstraintNullsDistinctModel | ||
|
|
||
| # Create first record | ||
| UniqueConstraintNullsDistinctModel.objects.create( | ||
| name='First', | ||
| code='A', | ||
| category='X' | ||
| ) | ||
|
|
||
| # Attempt to create duplicate - should fail | ||
| serializer = self.serializer_class(data={ | ||
| 'name': 'Second', | ||
| 'code': 'A', | ||
| 'category': 'X' | ||
| }) | ||
| assert not serializer.is_valid() | ||
|
|
||
| def test_unique_together_validator_nulls_distinct_equality(self): | ||
| """ | ||
| Test that UniqueTogetherValidator equality considers nulls_distinct. | ||
| """ | ||
| mock_queryset = MagicMock() | ||
| validator1 = UniqueTogetherValidator( | ||
| queryset=mock_queryset, | ||
| fields=('a', 'b'), | ||
| nulls_distinct=False | ||
| ) | ||
| validator2 = UniqueTogetherValidator( | ||
| queryset=mock_queryset, | ||
| fields=('a', 'b'), | ||
| nulls_distinct=False | ||
| ) | ||
| validator3 = UniqueTogetherValidator( | ||
| queryset=mock_queryset, | ||
| fields=('a', 'b'), | ||
| nulls_distinct=True | ||
| ) | ||
|
|
||
| assert validator1 == validator2 | ||
| assert validator1 != validator3 |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test coverage for nulls_distinct=False is missing some important edge cases. Consider adding tests for:
- Update scenarios where an instance is being modified with NULL values
- Partial NULL scenarios (e.g., one field NULL, another non-NULL like
code=None, category='X') - Mixed update scenarios (updating from non-NULL to NULL values)
These scenarios would help ensure the validator correctly handles the interaction between nulls_distinct=False and the update logic in UniqueTogetherValidator.__call__().
| """ | ||
|
|
||
| def setUp(self): | ||
| from tests.test_validators import UniqueConstraintNullsDistinctModel |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The module 'tests.test_validators' imports itself.
| When nulls_distinct=False, creating a second record with NULL values | ||
| in the constrained fields should fail validation. | ||
| """ | ||
| from tests.test_validators import UniqueConstraintNullsDistinctModel |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The module 'tests.test_validators' imports itself.
| """ | ||
| Non-NULL values should still work normally with uniqueness validation. | ||
| """ | ||
| from tests.test_validators import UniqueConstraintNullsDistinctModel |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The module 'tests.test_validators' imports itself.
| """ | ||
| Duplicate non-NULL values should still fail validation. | ||
| """ | ||
| from tests.test_validators import UniqueConstraintNullsDistinctModel |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The module 'tests.test_validators' imports itself.
Fixes #8409
This PR adds support for Django 5.0's
nulls_distinctoption inUniqueTogetherValidator.Problem
When
nulls_distinct=Falseis set on aUniqueConstraint, DRF's validator still skips validation for NULL values, causing integrity errors on databases like Oracle where NULLs can violate unique constraints.Solution
nulls_distinctfromUniqueConstraintinget_unique_together_constraints()UniqueTogetherValidator.__init__nulls_distinct=False, validate NULL values as potential duplicatesChanges
rest_framework/serializers.py: Updatedget_unique_together_constraints()to yieldnulls_distinctrest_framework/validators.py: Addednulls_distinctparameter toUniqueTogetherValidatortests/test_validators.py: Added tests fornulls_distinct=Falsebehavior (Django 5.0+)Backward Compatibility
nulls_distinct=None(default): Existing behavior preserved (skip NULL validation)getattr()fallback, no breaking changes