From 8afc27dc6ba24bcb25de0354871f97ce4f21518c Mon Sep 17 00:00:00 2001 From: SeaStar Deng <37767638+DSeaStar@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:43:32 +0000 Subject: [PATCH] Fix inc/dec rejecting deltas outside a field's min_value/max_value $inc/$dec apply a delta, not the stored value, so min/max validation must not run against the increment (see #2339). --- AUTHORS | 1 + docs/changelog.rst | 1 + mongoengine/base/fields.py | 5 ++++- tests/queryset/test_transform.py | 26 ++++++++++++++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 17fae84ea..a5d228c39 100644 --- a/AUTHORS +++ b/AUTHORS @@ -266,3 +266,4 @@ that much better: * Terence Honles (https://github.com/terencehonles) * Sean Bermejo (https://github.com/seanbermejo) * Juan Gutierrez (https://github.com/juannyg) + * DSeaStar (https://github.com/DSeaStar) diff --git a/docs/changelog.rst b/docs/changelog.rst index fab41b7b6..b2d7546a6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,7 @@ Changelog Development =========== - (Fill this out as you fix issues and develop your features). +- Fix inc/dec atomic updates rejecting deltas outside a field's min_value/max_value #2339 - Add a warning that ``mongoengine.org`` is no longer controlled by the MongoEngine project and appears to be an expired domain takeover. - Fix querying GenericReferenceField with __in operator #2886 diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 07962ecde..1b273ba01 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -236,7 +236,10 @@ def _to_mongo_safe_call(self, value, use_db_field=True, fields=None): def prepare_query_value(self, op, value): """Prepare a value that is being used in a query for PyMongo.""" - if op in UPDATE_OPERATORS: + # $inc/$dec apply a delta, not the stored value. Checking min_value + # / max_value against the delta rejects legitimate decrements on + # fields with min_value=0 (see #2339). + if op in UPDATE_OPERATORS and op not in ("inc", "dec"): self.validate(value) return value diff --git a/tests/queryset/test_transform.py b/tests/queryset/test_transform.py index 8cb8ad426..a24107624 100644 --- a/tests/queryset/test_transform.py +++ b/tests/queryset/test_transform.py @@ -83,6 +83,32 @@ class BlogPost(Document): update = transform.update(BlogPost, push_all__tags=["mongo", "db"]) assert update == {"$push": {"tags": {"$each": ["mongo", "db"]}}} + def test_transform_update_inc_dec_ignores_min_max(self): + """inc/dec pass a delta; min_value/max_value apply to stored values (#2339).""" + + class Account(Document): + amount = FloatField(min_value=0, required=True) + count = IntField(min_value=0, max_value=100) + money = DecimalField(min_value=0) + + update = transform.update(Account, dec__amount=10) + assert update == {"$inc": {"amount": -10.0}} + + update = transform.update(Account, inc__count=1) + assert update == {"$inc": {"count": 1}} + + update = transform.update(Account, dec__count=5) + assert update == {"$inc": {"count": -5}} + + update = transform.update(Account, dec__money=3) + assert update == {"$inc": {"money": -3.0}} + + with pytest.raises(ValidationError): + transform.update(Account, set__amount=-1) + + with pytest.raises(ValidationError): + transform.update(Account, set__count=101) + def test_transform_update_no_operator_default_to_set(self): """Ensure the differences in behvaior between 'push' and 'push_all'"""