From b074091892890b0bc31f805e0e62425bb9e813ca Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Wed, 24 Jun 2026 23:50:06 +0200 Subject: [PATCH] fix: deep-copy cache entries so mutating search results cannot corrupt the cache `Table.search` stored and returned shallow copies of the document list, meaning the `Document` objects inside the cache and the caller's list were the same objects. Mutating a nested mutable value (e.g. appending to a list field) inside a returned document therefore silently corrupted the cached results, causing subsequent searches to return stale/wrong data. Switch to `copy.deepcopy` when writing to and reading from `_query_cache` so that cached entries and returned results are fully independent. Fixes #516 --- tests/test_tinydb.py | 21 +++++++++++++++++++++ tinydb/table.py | 8 +++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/tests/test_tinydb.py b/tests/test_tinydb.py index 762a460b..5e36e7cd 100644 --- a/tests/test_tinydb.py +++ b/tests/test_tinydb.py @@ -677,6 +677,27 @@ def test_query_cache(): assert db.search(query) == [{'name': 'foo', 'value': 42}] +def test_query_cache_not_corrupted_by_mutation(): + """Mutating nested values of a search result must not pollute the cache.""" + db = TinyDB(storage=MemoryStorage) + db.insert({'name': 'Alice', 'tags': ['a', 'b']}) + + query = where('name') == 'Alice' + + results1 = db.search(query) + assert results1 == [{'name': 'Alice', 'tags': ['a', 'b']}] + + # Mutate both a top-level value and a nested mutable inside the document + results1[0]['name'] = 'MUTATED' + results1[0]['tags'].append('c') + + # A subsequent search hits the cache; it must return the original data + results2 = db.search(query) + assert results2 == [{'name': 'Alice', 'tags': ['a', 'b']}], ( + 'query cache was corrupted by mutation of a previously returned document' + ) + + def test_tinydb_is_iterable(db: TinyDB): assert [r for r in db] == db.all() diff --git a/tinydb/table.py b/tinydb/table.py index 17a0fb60..1e46f745 100644 --- a/tinydb/table.py +++ b/tinydb/table.py @@ -4,6 +4,7 @@ """ from collections.abc import Callable, Iterable, Iterator, Mapping +from copy import deepcopy from typing import ( NoReturn, Optional, @@ -244,7 +245,7 @@ def search(self, cond: QueryLike) -> list[Document]: # query cached_results = self._query_cache.get(cond) if cached_results is not None: - return cached_results[:] + return deepcopy(cached_results) # Perform the search by applying the query to all documents. # Then, only if the document matches the query, convert it @@ -271,8 +272,9 @@ def search(self, cond: QueryLike) -> list[Document]: is_cacheable: Callable[[], bool] = getattr(cond, 'is_cacheable', lambda: True) if is_cacheable(): - # Update the query cache - self._query_cache[cond] = docs[:] + # Update the query cache with a deep copy so that callers + # mutating returned documents cannot corrupt cached results. + self._query_cache[cond] = deepcopy(docs) return docs