Skip to content

Commit

Permalink
Do not exit early when validating partial unique indexes
Browse files Browse the repository at this point in the history
Instead, evaluate the validation of every unique index.
  • Loading branch information
Peter Bex committed Jul 22, 2019
1 parent 2cb9cd5 commit 042819e
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 2 deletions.
22 changes: 20 additions & 2 deletions partial_index/mixins.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections import defaultdict
from django.core.exceptions import ImproperlyConfigured, ValidationError, NON_FIELD_ERRORS
from django.db.models import Q

Expand Down Expand Up @@ -73,6 +74,7 @@ def validate_partial_unique(self):
if unique_idxs:
model_fields = set(f.name for f in self._meta.get_fields(include_parents=True, include_hidden=True))

errors = defaultdict(list)
for idx in unique_idxs:
where = idx.where
if not isinstance(where, Q):
Expand All @@ -88,7 +90,20 @@ def validate_partial_unique(self):
raise RuntimeError('Unable to use ValidatePartialUniqueMixin: expecting to find fields %s on model. ' +
'This is a bug in the PartialIndex definition or the django-partial-index library itself.')

values = {field_name: getattr(self, field_name) for field_name in mentioned_fields}
values = {}
skip = False
for field_name in mentioned_fields:
field_value = getattr(self, field_name)
if field_value is None and field_name in idx.fields:
# Can never be unique if value is NULL. If
# field is non-nullable we'll get a validation
# error from the field validations themselves.
skip = True
else:
values[field_name] = field_value

if skip:
continue

conflict = self.__class__.objects.filter(**values) # Step 1 and 3
conflict = conflict.filter(where) # Step 2
Expand All @@ -100,4 +115,7 @@ def validate_partial_unique(self):
key = idx.fields[0]
else:
key = NON_FIELD_ERRORS
raise PartialUniqueValidationError({key: self.unique_error_message(self.__class__, sorted(idx.fields))})
errors[key].append(self.unique_error_message(self.__class__, sorted(idx.fields)))

if errors:
raise PartialUniqueValidationError(errors)
17 changes: 17 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,20 @@ def test_standard_unique_together_constraints_do_not_block_evaluation_of_partial

with self.assertRaises(IntegrityError):
label.save()

def test_all_partial_constraints_are_included_in_validation_errors(self):
Label.objects.create(label='a', user=self.user1, room=self.room1, uuid='11111111-0000-0000-0000-000000000000', created_at='2019-01-01T11:11:11')

label = Label(label='a', user=self.user1, room=self.room1, uuid='22222222-0000-0000-0000-000000000000', created_at='2019-01-02T22:22:22')
with self.assertRaises(ValidationError) as cm:
label.full_clean()

self.assertSetEqual({NON_FIELD_ERRORS}, set(cm.exception.message_dict.keys()))
self.assertEqual(2, len(cm.exception.error_dict[NON_FIELD_ERRORS]))
self.assertEqual('unique_together', cm.exception.error_dict[NON_FIELD_ERRORS][0].code)
self.assertEqual(['label', 'room'], cm.exception.error_dict[NON_FIELD_ERRORS][0].params['unique_check'])
self.assertEqual('unique_together', cm.exception.error_dict[NON_FIELD_ERRORS][1].code)
self.assertEqual(['label', 'user'], cm.exception.error_dict[NON_FIELD_ERRORS][1].params['unique_check'])

with self.assertRaises(IntegrityError):
label.save()

0 comments on commit 042819e

Please sign in to comment.