From d3c6e488e1910426fb9ac1f6e46c9d9be7a3d6e8 Mon Sep 17 00:00:00 2001 From: Br1an67 <932039080@qq.com> Date: Sun, 1 Mar 2026 14:51:58 +0800 Subject: [PATCH 1/3] Fix auto_now field not updated with save(update_fields=[...]) When calling model.save(update_fields=['field']), fields with auto_now=True were not included in the update, so their timestamps were not refreshed. Now auto_now fields are automatically appended to update_fields before executing the update. --- tests/test_update.py | 22 ++++++++++++++++++++++ tortoise/models.py | 6 ++++++ 2 files changed, 28 insertions(+) diff --git a/tests/test_update.py b/tests/test_update.py index 106f6c1b4..d2baf5f50 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -165,6 +165,28 @@ async def test_update_auto_now(db): assert obj1.updated_at.date() == updated_at.date() +@pytest.mark.asyncio +async def test_update_auto_now_with_update_fields(db): + tournament = await Tournament.create(name="1") + event = await Event.create(name="original", tournament=tournament) + original_modified = event.modified + + # Set modified to the past so we can detect if it gets updated + past = timezone.now() - timedelta(days=1) + await Event.filter(pk=event.pk).update(modified=past) + + event = await Event.get(pk=event.pk) + assert event.modified.date() == past.date() + + # Update only name with update_fields; auto_now field should also be updated + event.name = "updated" + await event.save(update_fields=["name"]) + + event = await Event.get(pk=event.pk) + assert event.name == "updated" + assert event.modified.date() == timezone.now().date() + + @pytest.mark.asyncio async def test_update_relation(db): tournament_first = await Tournament.create(name="1") diff --git a/tortoise/models.py b/tortoise/models.py index 71fa3c43d..a7e15f0ec 100644 --- a/tortoise/models.py +++ b/tortoise/models.py @@ -1149,6 +1149,12 @@ async def save( raise IncompleteInstanceError( f"{self.__class__.__name__} is a partial model, can only be saved with the relevant update_field provided" ) + if update_fields: + update_fields = list(update_fields) + for field_name, field_obj in self._meta.fields_map.items(): + if field_name not in update_fields and getattr(field_obj, "auto_now", False): + update_fields.append(field_name) + await self._pre_save(db, update_fields) if force_create: From 8107991f804f49b028d4b216ee7e9e1eb04a3b35 Mon Sep 17 00:00:00 2001 From: Br1an67 <932039080@qq.com> Date: Sun, 1 Mar 2026 15:25:12 +0800 Subject: [PATCH 2/3] fix: also handle auto_now in bulk_update() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add auto_now fields to BulkUpdateQuery so bulk_update() automatically includes auto_now fields, consistent with Django's behavior. QuerySet.update() intentionally does not handle auto_now — it is a raw SQL operation where the caller provides explicit values, matching Django's documented behavior. --- tests/test_update.py | 30 ++++++++++++++++++++++++++++++ tortoise/queryset.py | 6 +++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/test_update.py b/tests/test_update.py index d2baf5f50..95c0e130b 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -187,6 +187,36 @@ async def test_update_auto_now_with_update_fields(db): assert event.modified.date() == timezone.now().date() +@pytest.mark.asyncio +async def test_bulk_update_auto_now(db): + tournament = await Tournament.create(name="1") + event1 = await Event.create(name="original1", tournament=tournament) + event2 = await Event.create(name="original2", tournament=tournament) + + # Set modified to the past so we can detect if it gets updated + past = timezone.now() - timedelta(days=1) + await Event.filter(pk__in=[event1.pk, event2.pk]).update(modified=past) + + event1 = await Event.get(pk=event1.pk) + event2 = await Event.get(pk=event2.pk) + assert event1.modified.date() == past.date() + assert event2.modified.date() == past.date() + + # bulk_update only name; auto_now field should also be updated + event1.name = "updated1" + event2.name = "updated2" + await Event.filter(pk__in=[event1.pk, event2.pk]).bulk_update( + [event1, event2], fields=["name"] + ) + + event1 = await Event.get(pk=event1.pk) + event2 = await Event.get(pk=event2.pk) + assert event1.name == "updated1" + assert event2.name == "updated2" + assert event1.modified.date() == timezone.now().date() + assert event2.modified.date() == timezone.now().date() + + @pytest.mark.asyncio async def test_update_relation(db): tournament_first = await Tournament.create(name="1") diff --git a/tortoise/queryset.py b/tortoise/queryset.py index 0cfd2fc52..b4ddd5842 100644 --- a/tortoise/queryset.py +++ b/tortoise/queryset.py @@ -1933,7 +1933,11 @@ def __init__( limit=limit, orderings=orderings, ) - self.fields = fields + fields_list = list(fields) + for field_name, field_obj in model._meta.fields_map.items(): + if field_name not in fields_list and getattr(field_obj, "auto_now", False): + fields_list.append(field_name) + self.fields = fields_list self._objects = objects self._batch_size = batch_size self._queries: list[QueryBuilder] = [] From 50120a2791691fc6a83569c708518abf1bf966ff Mon Sep 17 00:00:00 2001 From: Br1an67 <932039080@qq.com> Date: Tue, 3 Mar 2026 23:06:07 +0800 Subject: [PATCH 3/3] fix: auto-update auto_now fields in queryset.update() Add auto_now field injection to UpdateQuery so that QuerySet.update() automatically sets auto_now fields to the current time when they are not explicitly specified. Add test_queryset_update_auto_now to verify the behavior. --- tests/test_update.py | 20 ++++++++++++++++++++ tortoise/queryset.py | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/tests/test_update.py b/tests/test_update.py index 95c0e130b..8cdfb455f 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -217,6 +217,26 @@ async def test_bulk_update_auto_now(db): assert event2.modified.date() == timezone.now().date() +@pytest.mark.asyncio +async def test_queryset_update_auto_now(db): + tournament = await Tournament.create(name="1") + event = await Event.create(name="original", tournament=tournament) + + # Set modified to the past (explicit modified= won't be overridden by auto_now) + past = timezone.now() - timedelta(days=1) + await Event.filter(pk=event.pk).update(modified=past) + + event = await Event.get(pk=event.pk) + assert event.modified.date() == past.date() + + # queryset.update() should auto-include auto_now fields + await Event.filter(pk=event.pk).update(name="updated") + + event = await Event.get(pk=event.pk) + assert event.name == "updated" + assert event.modified.date() == timezone.now().date() + + @pytest.mark.asyncio async def test_update_relation(db): tournament_first = await Tournament.create(name="1") diff --git a/tortoise/queryset.py b/tortoise/queryset.py index b4ddd5842..0cc796c4f 100644 --- a/tortoise/queryset.py +++ b/tortoise/queryset.py @@ -1285,6 +1285,12 @@ def __init__( orderings: list[tuple[str, str]], ) -> None: super().__init__(model) + # Inject auto_now fields into update_kwargs if not already specified + from tortoise import timezone + + for field_name, field_obj in model._meta.fields_map.items(): + if field_name not in update_kwargs and getattr(field_obj, "auto_now", False): + update_kwargs[field_name] = timezone.now() self.update_kwargs = update_kwargs self._q_objects = q_objects self._annotations = annotations