Opened 4 months ago

Last modified 3 months ago

#28405 new Bug

CICharField on a ModelFormSet doesn't catch unique constraint violations with different capitalization

Reported by: Jon Dufresne Owned by:
Component: contrib.postgres Version: 1.11
Severity: Normal Keywords:
Cc: Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

When CICharField (new in 1.11) with unique=True is used on a ModelFormSet, values differing only by case are not reported as a validation error. As the form appears to be valid, saving the form will result in a database IntegrityError.

Will attach example test.

For example:

class MyModel(models.Model):
    name = CICharField(max_length=100, unique=True)

MyModelFormset = modelformset_factory(MyModel, fields=['name'])

data = {
    'form-TOTAL_FORMS': 2,
    'form-INITIAL_FORMS': 0,
    'form-0-name': 'frank',
    'form-1-name': 'FRANK',
}
formset = MyModelFormSet(data)

Trying to save the formset, which reports no validation errors, results in an exception:

Traceback (most recent call last):
  File "/home/jon/devel/django/tests/postgres_tests/test_citext.py", line 59, in test_duplicate_formset_mixed_case
    formset.save()
  File "/home/jon/devel/django/django/forms/models.py", line 671, in save
    return self.save_existing_objects(commit) + self.save_new_objects(commit)
  File "/home/jon/devel/django/django/forms/models.py", line 806, in save_new_objects
    self.new_objects.append(self.save_new(form, commit=commit))
  File "/home/jon/devel/django/django/forms/models.py", line 647, in save_new
    return form.save(commit=commit)
  File "/home/jon/devel/django/django/forms/models.py", line 457, in save
    self.instance.save()
  File "/home/jon/devel/django/django/db/models/base.py", line 711, in save
    force_update=force_update, update_fields=update_fields)
  File "/home/jon/devel/django/django/db/models/base.py", line 741, in save_base
    updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
  File "/home/jon/devel/django/django/db/models/base.py", line 825, in _save_table
    result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
  File "/home/jon/devel/django/django/db/models/base.py", line 863, in _do_insert
    using=using, raw=raw)
  File "/home/jon/devel/django/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/home/jon/devel/django/django/db/models/query.py", line 1047, in _insert
    return query.get_compiler(using=using).execute_sql(return_id)
  File "/home/jon/devel/django/django/db/models/sql/compiler.py", line 1202, in execute_sql
    cursor.execute(sql, params)
  File "/home/jon/devel/django/django/db/backends/utils.py", line 62, in execute
    return self.cursor.execute(sql, params)
  File "/home/jon/devel/django/django/db/utils.py", line 89, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/home/jon/devel/django/django/db/backends/utils.py", line 62, in execute
    return self.cursor.execute(sql, params)
django.db.utils.IntegrityError: duplicate key value violates unique constraint "postgres_tests_citestmodel2_name_key"
DETAIL:  Key (name)=(FRANK) already exists.

Attachments (1)

test.diff (3.0 KB) - added by Jon Dufresne 4 months ago.
An example unit test

Download all attachments as: .zip

Change History (4)

Changed 4 months ago by Jon Dufresne

Attachment: test.diff added

An example unit test

comment:1 Changed 4 months ago by Tim Graham

Triage Stage: UnreviewedAccepted

A related issue (case-insensitive collations on MySQL causing this problem) is #23964.

comment:2 Changed 3 months ago by Jon Dufresne

As a potential solution, what do others think about catching the database's IntegrityError during ModelForm.save() to report unique constraint violations in model forms? The database is already optimized for robust checking of unique constraints, why repeat that code at the application layer? It produces subtle differences and edge cases. This solution could potentially deprecate the large amounts of Django code used to check if a submitted form violates unique constraints and rely on the basic database feature instead. All builtin datbase backends support unique constraints. If we rely on the database, we wouldn't need to figure out how the database is comparing rows (case sensitive vs case insensitive), it'll tell us.

Something to note, this would mean that not all validation occurs before the ModelForm.save() and that the ModelForm.save() can be unsuccessful and could add errors to the form.

(Usual backward compatibility concerns apply.)

comment:3 Changed 3 months ago by Tim Graham

I think it'd be difficult to translate a database's exception into the corresponding (translated) Django message.

Note: See TracTickets for help on using tickets.
Back to Top