Repository navigation
Description
upsert() silently discards document.id when a condition is passed, and then overwrites it.
What happens
The same instance produces two different outcomes depending on whether a condition is given:
x = A(id=7, name="seven")
A.upsert(x) # -> [7] x.id == 7 (id honored)
y = A(id=7, name="seven")
A.upsert(y, A.name == "nope") # -> [8] y.id == 8 (id discarded)With no condition, upsert() serializes with force_dict=False, producing a TinyDB Document that carries doc_id=7, so the insert lands at id 7. With a condition, it serializes with force_dict=True, which drops the id, so the insert gets a fresh one — and document.id is then overwritten with that fresh value, so even the evidence that the caller asked for id 7 disappears.
Why it matters
Setting id explicitly means "put it here" — that is the entire point of the field. Silently ignoring it and then destroying the record of the request is the failure mode this project otherwise works hard to eliminate. The docstring covers the condition's id ("the insert does not adopt the condition's id value") but says nothing about the document's own id being dropped.
Options
- Honor
document.idin both branches — consistent, and matches whatinsert()does. - Raise when a condition is passed alongside a set
idand nothing matches.
Either way it needs documenting; this is exactly the kind of sharp edge the CRUD tour is otherwise good at calling out.
Notes
TinyDB 4.9.0 rewrote the internals this path runs through (Table.upsert() now checks for an empty update() return instead of catching KeyError), but the divergence lives in tinydantic's own force_dict choice, so upstream neither fixes nor worsens it.
Reproduction
from tinydb import TinyDB
from tinydb.storages import MemoryStorage
from tinydantic import TinydanticModel
db = TinyDB(storage=MemoryStorage)
class Book(TinydanticModel, database=db):
title: str
no_cond = Book(id=7, title="a")
print("no condition: ", Book.upsert(no_cond), "instance id:", no_cond.id)
with_cond = Book(id=7, title="b")
print("with condition:", Book.upsert(with_cond, Book.title == "nope"),
"instance id:", with_cond.id)Run with python repro.py.
Actual output:
no condition: [7] instance id: 7
with condition: [8] instance id: 8
Expected: the second call honors id=7 the way the first does — or refuses the call — instead of inserting at 8 and overwriting with_cond.id with the id the caller did not ask for.
Environment: tinydantic 0.4.0 at 4d373d1, Python 3.10.20, TinyDB 4.9.0, pydantic 2.13.4, macOS. Run 2026-08-13.
Reproduced at e7d0ed0 on 2026-08-12. Originally finding M7 of the 2026-08-06 internal review.
Assisted-by: Claude:claude-opus-5
Activity
docs(agents): require AI attribution on agent-assisted issues
docs(agents): require AI attribution on agent-assisted issues
docs(agents): require AI attribution on agent-assisted issues
fix(model): honor document.id in upsert()'s no-match insert