Skip to content

docs: document that search() results share structure with the query cache - #617

Merged
msiemens merged 2 commits into
msiemens:masterfrom
gaoflow:fix/query-cache-deepcopy
Aug 8, 2026
Merged

docs: document that search() results share structure with the query cache#617
msiemens merged 2 commits into
msiemens:masterfrom
gaoflow:fix/query-cache-deepcopy

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Update — converted to a docs-only PR

Per discussion below: a deep copy costs 150-4000x on the read hot path, and there's no fix that's both cheap and complete (a shallow top-level copy stops attribute reassignment but not nested mutation). Reverted the code change; the fix is now a docstring note on Table.search() plus a test that documents the actual sharing behavior (list is fresh, documents inside it are the cached objects).

Original problem (#516)

Table.search stored and returned shallow copies of the document list, so a returned Document is the same object the cache holds. Mutating a key or a nested value in a returned document corrupts what a later identical search returns:

from tinydb import TinyDB, Query
from tinydb.storages import MemoryStorage

db = TinyDB(storage=MemoryStorage)
db.insert({'name': 'Alice', 'tags': ['a', 'b']})

Q = Query()
results = db.search(Q.name == 'Alice')
results[0]['tags'].append('c')          # mutate the returned document

print(db.search(Q.name == 'Alice'))     # prints [{'name': 'Alice', 'tags': ['a', 'b', 'c']}]

Tests

test_query_cache_documents_are_shared_with_cache documents the boundary: mutating the returned list is safe, mutating a document inside it is not. 219/219 pass.

@msiemens

msiemens commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Thanks for looking into this! Unfortunately, this PR has a major flaw: it significantly degrades performance for all users of the cache:

Scenario master PR #617
cache hit, 10 docs matched 0.4 µs 62 µs (~150× slower)
cache hit, 1000 docs matched 1.6 µs 6,458 µs (~4,000× slower)
cache miss, 1000 docs matched 502 µs 6,994 µs (~14× slower)

Ultimately, thinking about this again, I think there is no fix for #516 that does not introduce a performance penalty (deep copy) or break the API contract (make all results immutable). Given that my main issue was that the described behavior is unexpected, maybe it would be better to just properly document it. What do you think?

@gaoflow

gaoflow commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Agreed, deep-copy isn't worth it here — the read path is the hot path and a 150-4000x hit for something meant to be a fast lookup is a bad trade for an edge case (mutating a returned result before the next identical search). I don't see a fix that's both cheap and complete: shallow-copying the top-level dict on read would stop attribute-reassignment corruption cheaply but not nested-value mutation, which is a half-fix that could still surprise someone. Happy to convert this into a docs-only PR noting that search() results share structure with the cache and must be treated as read-only — let me know if you'd rather write that note yourself.

@msiemens

msiemens commented Aug 6, 2026

Copy link
Copy Markdown
Owner

A docs-only PR is fine with me

…ache

Table.search() results are read from and written to the internal
cache as a shallow list copy, so a returned document is the same
object the cache holds. Mutating one (a key or a nested value)
corrupts what a later identical search returns.

msiemens#516 asked for a fix rather than a doc note; per the discussion
there, a deep copy costs 150-4000x on the read hot path, so this
documents the caveat instead.

Fixes msiemens#516
@gaoflow
gaoflow force-pushed the fix/query-cache-deepcopy branch from b074091 to 1e8b70f Compare August 6, 2026 23:21
@gaoflow gaoflow changed the title fix: deep-copy cache entries so mutating search results cannot corrupt the cache docs: document that search() results share structure with the query cache Aug 6, 2026
@gaoflow

gaoflow commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Done — reverted the code change, kept a docstring note on Table.search() plus a test documenting the actual sharing boundary (fresh list, shared documents). 219/219 pass. Updated the PR title/body.

@msiemens

msiemens commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Thanks for reworking this! Could you also add a brief note to docs/usage.rst (section "Query Caching")? I assume most users will not regularly go into the API docs

@gaoflow

gaoflow commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Done — added a note to the "Query Caching" section of docs/usage.rst, mirroring the search() docstring: cached searches return a fresh list, but the documents inside it are the cached objects, so treat them as read-only.

@msiemens
msiemens merged commit 8d3f0c7 into msiemens:master Aug 8, 2026
31 checks passed
@msiemens

msiemens commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Thanks, @gaoflow!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants