#34590 closed Bug (fixed)
Querying for decimals larger than max_digits crashes on SQLite
Reported by: | Marc Odermatt | Owned by: | David Sanders |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | 4.2 |
Severity: | Release blocker | Keywords: | |
Cc: | Florian Apolloner | Triage Stage: | Accepted |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
Introduced in: Refs #33308 -- Improved adapting DecimalField values to decimal.
Description: I have DecimalField with max_digits=4
. Previously, querying for something larger (e.g. 12345) would execute the sql and return ObjectNotFound. Now, in 4.2, it throws a decimal.InvalidOperation error, as it tries to quantize the value to have 4 digits.
I understand that it doesn't make sense to query for a larger number, but the error that occurs was pretty confusing to me. Also, it is not as easy to check in my application, because I don't have easy access to the max_digits
parameter of the field.
In my opinion, the backend should either accept larger values and always return "not found", or the error should be more descriptive, so that it can be caught specifically.
Testcase: placed in tests/backends
folder and used for git bisect
import decimal from django.db import models from django.test import TestCase class DecimalModel(models.Model): dec_field = models.DecimalField(decimal_places=0, max_digits=4) class InvalidDecimalQuery(TestCase): def test_invalid_decimal_query(self): try: DecimalModel.objects.get(dec_field='12345') except decimal.InvalidOperation: self.fail("Too large decimal query caused exception.") except DecimalModel.DoesNotExist: pass
Stacktrace:
Traceback (most recent call last): File "lib/python3.10/site-packages/django/db/models/manager.py", line 87, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) File "lib/python3.10/site-packages/django/db/models/query.py", line 633, in get num = len(clone) File "lib/python3.10/site-packages/django/db/models/query.py", line 380, in __len__ self._fetch_all() File "lib/python3.10/site-packages/django/db/models/query.py", line 1881, in _fetch_all self._result_cache = list(self._iterable_class(self)) File "lib/python3.10/site-packages/django/db/models/query.py", line 91, in __iter__ results = compiler.execute_sql( File "lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 1547, in execute_sql sql, params = self.as_sql() File "lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 762, in as_sql self.compile(self.where) if self.where is not None else ("", []) File "lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 544, in compile sql, params = node.as_sql(self, self.connection) File "lib/python3.10/site-packages/django/db/models/sql/where.py", line 145, in as_sql sql, params = compiler.compile(child) File "lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 544, in compile sql, params = node.as_sql(self, self.connection) File "lib/python3.10/site-packages/django/db/models/lookups.py", line 357, in as_sql return super().as_sql(compiler, connection) File "lib/python3.10/site-packages/django/db/models/lookups.py", line 225, in as_sql rhs_sql, rhs_params = self.process_rhs(compiler, connection) File "lib/python3.10/site-packages/django/db/models/lookups.py", line 126, in process_rhs return self.get_db_prep_lookup(value, connection) File "lib/python3.10/site-packages/django/db/models/lookups.py", line 254, in get_db_prep_lookup else [get_db_prep_value(value, connection, prepared=True)], File "lib/python3.10/site-packages/django/db/models/fields/__init__.py", line 1761, in get_db_prep_value return connection.ops.adapt_decimalfield_value( File "lib/python3.10/site-packages/django/db/backends/base/operations.py", line 574, in adapt_decimalfield_value return utils.format_number(value, max_digits, decimal_places) File "lib/python3.10/site-packages/django/db/backends/utils.py", line 304, in format_number value = value.quantize( decimal.InvalidOperation: [<class 'decimal.InvalidOperation'>]
Change History (12)
comment:1 by , 18 months ago
Cc: | added |
---|---|
Severity: | Normal → Release blocker |
Triage Stage: | Unreviewed → Accepted |
comment:2 by , 18 months ago
Summary: | Regression in 4.2 when querying db for too large decimals → Querying for decimals larger than max_digits crashes on SQLite |
---|
follow-up: 4 comment:3 by , 18 months ago
The most straightforward way to address this is likely to have DecimalField.get_db_prep_value
catch decimal.InvalidOperation
and raise EmptyResultSet
instead.
comment:4 by , 18 months ago
Owner: | changed from | to
---|---|
Status: | new → assigned |
Replying to Simon Charette:
The most straightforward way to address this is likely to have
DecimalField.get_db_prep_value
catchdecimal.InvalidOperation
and raiseEmptyResultSet
instead.
Hi Simon 👋 , I want to implement this patch. I will send the pr within 6hrs
comment:5 by , 18 months ago
Mhm this is a tricky one. Raising EmptyResultSet
feels wrong though. There is no reason why this code should not be able to properly prep the value (there will certainly be dragons somewhere down the line but it feels like we are doing one transformation to many). I'd be interested to know what the actual code was that led to this behavior. If you pass .filter(pk='abc')
you also get an exception that this is not a valid integer. Wouldn't form validation where the validator is derived from the model field would have detected that this is out of range already?
follow-up: 7 comment:6 by , 18 months ago
Yup raising EmptyResultSet would break doing DecimalModel.objects.filter(dec_field__lte=Decimal('12345'))
which seems valid if there are values <= 12,345 🤔 (which is currently crashing as per above)
comment:7 by , 18 months ago
Replying to David Sanders:
Yup raising EmptyResultSet would break doing
DecimalModel.objects.filter(dec_field__lte=Decimal('12345'))
which seems valid if there are values <= 12,345 🤔 (which is currently crashing as per above)
(Also just confirming that DecimalModel.objects.filter(dec_field__lte=Decimal('12345'))
did work ok prior to 7990d254b0af158baf827fafbd90fe8e890f23bd)
comment:8 by , 18 months ago
Yeah, so a query for "less than" seems valid to me and tells me our conversion code is not as lenient as it should be. So maybe let's just revert https://github.com/django/django/commit/7990d254b0af158baf827fafbd90fe8e890f23bd locally and rerun the testsuite to maybe see a reason for introducing it? To be honest I am not that sure anymore what I did why for the psycopg3 branch :D
comment:9 by , 18 months ago
You're right, the EmptyResultSet
approach failed to account for lte and friends that would have to be adjusted as well like we did with IntegerField
.
What would be good to confirm if we can't revert this change is what the previous logic did for cases where an extra decimal was provided. Was it a non-match on all backends? Were some of them performing rounding?
For example, given field = DecimalField(max_digits=4, decimal_places=2)
what does filter(Q(field=Decimal('42.425')) | Q(field=Decimal('42.424')) | Q(field=Decimal('42.426')))
yielded on all backends against existing rows values of 42.42
and 42.43
. I agree that we don't want to start erroring out here without deprecation period though so this is more of exploration work but I could see why we could want to error out immediately when providing invalid lookup values.
comment:10 by , 18 months ago
Has patch: | set |
---|---|
Owner: | changed from | to
Confirmed regression with 7990d254b0af158baf827fafbd90fe8e890f23bd
Crashes with sqlite, postgres ok