Opened 8 years ago

Closed 8 years ago

#26963 closed Cleanup/optimization (invalid)

Improve error message when trying to insert a value that overflows DecimalField

Reported by: Floris den Hengst Owned by: nobody
Component: Database layer (models, ORM) Version: dev
Severity: Normal Keywords: DecimalField, ValidationError, quantize
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

DecimalField accepts max_digit parameter that specifies the amount of digit positions used by the DecimalField.

Consider the following model that allows for only one digit before the decimal separator:

from django.db.models import Model
class MyModel(Model):
    my_field = DecimalField(max_digits=2, decimal_places=1)

A value of Decimal("10.1") is not supported, as it requires 3 decimal places, however its input is accepted until the value is being inserted in the database.

This line in django.db.backends.utils.format_number fails when any insertion or update is attempted (e.g. using save() , bulk_create etc. ), because quantize raises an InvalidOperation when there are not enough positions to fit the rounded value (according to Decimal.quantize docs).

The error message is not very informative (0).

This commit contains a test that triggers the error. Note that overflow might also happen because of rounding during the quantization (e.g. values of 9.99999 could result in 10.0 during quantization, depending on the Context used).
For a copy of the commit see (1).

(0) Example error message

Traceback (most recent call last):
  File "/path_to_project/file.py", line 123, in my_failing_function
    overflowing_instance.save()
  File "/path_to_django/db/models/base.py", line 796, in save
    force_update=force_update, update_fields=update_fields)
  File "/path_to_django/db/models/base.py", line 824, in save_base
    updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
  File "/path_to_django/db/models/base.py", line 908, in _save_table
    result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
  File "/path_to_django/db/models/base.py", line 947, in _do_insert
    using=using, raw=raw)
  File "/path_to_django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/path_to_django/db/models/query.py", line 1046, in _insert
    return query.get_compiler(using=using).execute_sql(return_id)
  File "/path_to_django/db/models/sql/compiler.py", line 1053, in execute_sql
    for sql, params in self.as_sql():
  File "/path_to_django/db/models/sql/compiler.py", line 1006, in as_sql
    for obj in self.query.objs
  File "/path_to_django/db/models/sql/compiler.py", line 1006, in <listcomp>
    for obj in self.query.objs
  File "/path_to_django/db/models/sql/compiler.py", line 1005, in <listcomp>
    [self.prepare_value(field, self.pre_save_val(field, obj)) for field in fields]
  File "/path_to_django/db/models/sql/compiler.py", line 945, in prepare_value
    value = field.get_db_prep_save(value, connection=self.connection)
  File "/path_to_django/db/models/fields/__init__.py", line 1587, in get_db_prep_save
    return connection.ops.adapt_decimalfield_value(self.to_python(value), self.max_digits, self.decimal_places)
  File "/path_to_django/db/backends/base/operations.py", line 495, in adapt_decimalfield_value
    return utils.format_number(value, max_digits, decimal_places)
  File "/path_to_django/db/backends/utils.py", line 204, in format_number
    value = value.quantize(decimal.Decimal(".1") ** decimal_places, context=context)
decimal.InvalidOperation: [<class 'decimal.InvalidOperation'>]

(1)Test that triggers the error (add to django.tests.model_fields.test_decimalfield.DecimalFieldTests)

    def test_save_with_max_digits_overflow(self):
        """
        Ensure overflowing decimals yield a meaningful error.
        """
        overflowing_value = Decimal(10 ** 6)
        expected_message = "Not enough digit positions in field 'd' to represent {}".format(overflowing_value) # some meaningful error message
        overflowing_instance = Foo(a='a', d=overflowing_value)
        with self.assertRaisesMessage(ValidationError, # some meaningful error
            expected_message):
            overflowing_instance.save()

Change History (1)

comment:1 by Tim Graham, 8 years ago

Resolution: invalid
Status: newclosed

If you want a nice error message, you need to run validation before calling save: overflowing_instance.full_clean().

Doing so in your test raises ValidationError: {'d': ['Ensure that there are no more than 5 digits in total.']}.

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