Skip to content

upstream(tinydb): Table.upsert() silently ignores the condition when the document carries a doc_id #148

Open

Description

@cdwilson

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

  1. added
    enhancementNew feature or request
    upstreamTracks a defect or request in an upstream dependency
    ai-assistedCreated or investigated with help from an AI agent
    on Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai-assistedCreated or investigated with help from an AI agentenhancementNew feature or requestupstreamTracks a defect or request in an upstream dependency

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions