Repository navigation
Description
Tracks upstream friction in TinyDB. The explanation lives in the docs; this issue is the unit of work.
Explanation: Table.upsert() silently ignores the condition when the document carries a doc_id — the limitation, why it matters to an ODM, the workaround tinydantic carries, and the change that would remove it. That section is written to be read standalone by an upstream maintainer, so it doubles as the ready-to-send proposal if we ever file this.
Reproduction
from tinydb import TinyDB, where
from tinydb.storages import MemoryStorage
from tinydb.table import Document
db = TinyDB(storage=MemoryStorage)
table = db.table("t")
table.insert({"name": "alice"}) # doc_id 1
table.insert({"name": "bob"}) # doc_id 2
# Intent: "update documents matching the condition; if none match,
# insert this document at its doc_id".
ids = table.upsert(Document({"name": "carol"}, doc_id=1), where("name") == "bob")
print("returned ids:", ids)
print("doc 1:", table.get(doc_id=1))
print("doc 2:", table.get(doc_id=2))Run with python repro.py.
Actual output:
returned ids: [1]
doc 1: {'name': 'carol'}
doc 2: {'name': 'bob'}
The condition matched doc 2, but doc 1 was overwritten and doc 2 untouched: upsert() extracts the Document's doc_id and calls Table.update(document, cond, doc_ids=[doc_id]), and update() checks doc_ids before cond, so the condition is never evaluated. Nothing raises or warns. The docstring says a Document with a doc_id makes cond "optional"; in fact it overrides cond entirely.
Expected: either the condition is honored (with the doc_id seeding only the fall-through insert) or the ambiguous call is refused with a ValueError — anything but silently performing a different operation than the one written.
Environment: TinyDB 4.9.0, Python 3.10.20, macOS. Run 2026-08-13 at tinydantic 3d7d09a.
What tinydantic carries today
TinydanticModel.upsert() honors a set document.id on the no-match insert (#120), which requires serializing the model as a Document so the id survives to the insert — and handing that Document plus the user's condition to Table.upsert() would silently turn every condition-upsert into an id-upsert. The fix for #120 therefore inlines upstream's own update-then-insert instead of delegating: Table.update(document, cond) first, and on an empty match Table.insert(document) with the id-bearing Document. Public API throughout, same storage-cycle count as Table.upsert(); no private-API registry entry needed. Verified by the upsert cases in tests/model/test_table_methods.py and tests/model/test_id_queries.py.
Upstream status
Not filed. No draft prepared beyond the limitations-page section linked above.
What would close this
An upstream Table.upsert() that evaluates a supplied cond even when the document carries a doc_id (or rejects the combination), letting tinydantic delegate again — or a maintainer decision to carry the inlined update-then-insert indefinitely.
Assisted-by: Claude:claude-fable-5
Activity
fix(model): honor document.id in upsert()'s no-match insert