tortoise / tortoise/tortoise-orm

`bulk_update()` ignores explicitly set values for `DatetimeField(auto_now=True)` and recalculates timestamp per object

Open
#2,133 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
5.6k
Forks
516
Avg merge
2d 21h
Merged PRs (30d)
9

Description

Describe the bug

When using bulk_update() with a DatetimeField(auto_now=True), Tortoise ignores the value already set on the model instance and recalculates a new timestamp, with microsecond difference, for each object.

This makes it impossible to assign a single deterministic timestamp across all rows in a bulk operation, even when the field is explicitly included in fields.

The behavior is surprising because the ORM silently overrides user-provided values.

To Reproduce

from tortoise import Tortoise, fields, models, run_async
from tortoise.timezone import now


class Item(models.Model):
    id = fields.IntField(pk=True)
    value = fields.IntField()
    updated_at = fields.DatetimeField(auto_now=True)


async def reproduce():
    await Tortoise.init(
        db_url="sqlite://:memory:",
        modules={"models": ["__main__"]},
    )
    await Tortoise.generate_schemas()

    i1 = await Item.create(value=1)
    i2 = await Item.create(value=2)

    ts = now()
    items = await Item.all()

    for obj in items:
        obj.value += 1
        obj.updated_at = ts

    await Item.bulk_update(items, fields=["value", "updated_at"])

    i1_after = await Item.get(id=i1.id)
    i2_after = await Item.get(id=i2.id)

    assert i1_after.updated_at > i1.updated_at
    assert i2_after.updated_at > i2.updated_at

    # This should be True
    assert i1_after.updated_at == i2_after.updated_at


run_async(reproduce())

Expected behavior

If a value is already set on the instance and the field is included in fields, bulk_update() should respect that value instead of recomputing it.

This would allow deterministic timestamps in bulk operations.

Additional context

Why this matters?

Bulk updates are typically used for performance-sensitive paths affecting thousands of rows.
In those cases it is common to want a single timestamp representing the update operation.

The current behavior forces users to either:

  • run a second Model.filter(...).update(...) to fix the timestamp, or
  • avoid auto_now=True entirely.

Environment:

  • Tortoise ORM: 1.1.6
  • Python: 3.14

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at Item.bulk_update and trace how fields with auto_now are handled during the reproduced SQLite operation. Verify that an explicitly assigned updated_at is preserved for every object when it is included in fields, and that both rows retain the same timestamp after the bulk update.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, sqlite
Domain
database
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
65/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.