Opened 9 years ago
Closed 9 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()
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.']}.