fix(memory): paginate delete_all across vector store pages (#6636)

Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
tomatotomata
2026-08-01 00:20:37 +05:30
committed by GitHub
co-authored by kartik-mem0
parent 29fa41558c
commit 54328ffd97
4 changed files with 118 additions and 24 deletions
+37 -2
View File
@@ -91,7 +91,7 @@ def test_sync_delete_failure_does_not_trigger_decay_usage_notice(monkeypatch):
def test_sync_delete_all_decay_usage_runs_after_success(monkeypatch):
memory = make_sync_memory()
memories = [SimpleNamespace(id="memory-1"), SimpleNamespace(id="memory-2")]
memory.vector_store.list.return_value = (memories, None)
memory.vector_store.list.side_effect = [(memories, None), ([], None)]
decay_notice = MagicMock()
first_run_notice = MagicMock()
detect_decay = MagicMock(return_value=("delete_all", "bulk_delete", None, 2))
@@ -117,6 +117,21 @@ def test_sync_delete_all_decay_usage_runs_after_success(monkeypatch):
first_run_notice.assert_not_called()
def test_sync_delete_all_stops_when_vector_store_repeats_batch(monkeypatch):
memory = make_sync_memory()
memories = [SimpleNamespace(id="memory-1"), SimpleNamespace(id="memory-2")]
memory.vector_store.list.side_effect = [(memories, None), (memories, None)]
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main, "detect_decay_usage_from_delete_all", MagicMock(return_value=None))
monkeypatch.setattr(memory_main, "display_first_run_notice", MagicMock())
result = Memory.delete_all(memory, user_id="u1")
assert result == {"message": "Memories deleted successfully!"}
assert memory.vector_store.list.call_count == 2
assert memory._delete_memory.call_count == 2
def test_sync_delete_all_zero_deletes_uses_first_run_notice(monkeypatch):
memory = make_sync_memory()
memory.vector_store.list.return_value = ([], None)
@@ -189,7 +204,7 @@ async def test_async_delete_failure_does_not_trigger_decay_usage_notice(monkeypa
async def test_async_delete_all_decay_usage_runs_after_success(monkeypatch):
memory = make_async_memory()
memories = [SimpleNamespace(id="memory-1"), SimpleNamespace(id="memory-2")]
memory.vector_store.list.return_value = (memories, None)
memory.vector_store.list.side_effect = [(memories, None), ([], None)]
decay_notice = AsyncMock()
first_run_notice = AsyncMock()
detect_decay = MagicMock(return_value=("delete_all", "bulk_delete", None, 2))
@@ -213,3 +228,23 @@ async def test_async_delete_all_decay_usage_runs_after_success(monkeypatch):
2,
)
first_run_notice.assert_not_awaited()
@pytest.mark.asyncio
async def test_async_delete_all_stops_when_vector_store_repeats_batch(monkeypatch):
memory = make_async_memory()
memories = [SimpleNamespace(id="memory-1"), SimpleNamespace(id="memory-2")]
memory.vector_store.list.side_effect = [
(memories, None),
(memories, None),
RuntimeError("delete_all should have stopped before a third list() call"),
]
monkeypatch.setattr(memory_main, "capture_event", MagicMock())
monkeypatch.setattr(memory_main, "detect_decay_usage_from_delete_all", MagicMock(return_value=None))
monkeypatch.setattr(memory_main, "display_first_run_notice_async", AsyncMock())
result = await AsyncMemory.delete_all(memory, user_id="u1")
assert result == {"message": "Memories deleted successfully!"}
assert memory.vector_store.list.call_count == 2
assert memory._delete_memory.await_count == 2
+24 -3
View File
@@ -280,19 +280,36 @@ def test_delete(memory_instance):
def test_delete_all(memory_instance):
mock_memories = [Mock(id="1"), Mock(id="2")]
memory_instance.vector_store.list = Mock(return_value=(mock_memories, None))
memory_instance.vector_store.list = Mock(side_effect=[(mock_memories, None), ([], None)])
memory_instance.vector_store.reset = Mock()
memory_instance._delete_memory = Mock()
result = memory_instance.delete_all(user_id="test_user")
assert memory_instance._delete_memory.call_count == 2
memory_instance.vector_store.list.assert_called_with(
filters={"user_id": "test_user"}, top_k=1000
)
# Ensure the collection is NOT dropped — only matched memories should be removed
memory_instance.vector_store.reset.assert_not_called()
assert result["message"] == "Memories deleted successfully!"
def test_delete_all_paginates_beyond_vector_store_page_size(memory_instance):
first_batch = [Mock(id=str(index)) for index in range(1000)]
second_batch = [Mock(id="1000")]
memory_instance.vector_store.list = Mock(
side_effect=[(first_batch, None), (second_batch, None), ([], None)]
)
memory_instance._delete_memory = Mock()
memory_instance.delete_all(user_id="test_user")
assert memory_instance._delete_memory.call_count == 1001
assert memory_instance.vector_store.list.call_count == 3
def test_get_all(memory_instance):
mock_memories = [Mock(id="1", payload={"data": "Memory 1", "user_id": "test_user"})]
memory_instance.vector_store.list = Mock(return_value=(mock_memories, None))
@@ -431,7 +448,9 @@ class TestEntityIdValidation:
memory_instance.delete_all(user_id=" alice ")
memory_instance.vector_store.list.assert_called_once_with(filters={"user_id": "alice"})
memory_instance.vector_store.list.assert_called_once_with(
filters={"user_id": "alice"}, top_k=1000
)
def test_validate_coerces_non_string_entity_id(self):
"""Integer (and other non-string) ids are coerced to str, not crashed on."""
@@ -444,7 +463,9 @@ class TestEntityIdValidation:
memory_instance.delete_all(user_id=42)
memory_instance.vector_store.list.assert_called_once_with(filters={"user_id": "42"})
memory_instance.vector_store.list.assert_called_once_with(
filters={"user_id": "42"}, top_k=1000
)
def test_get_all_coerces_integer_user_id(self, memory_instance):
"""get_all should accept an integer user_id in filters and scope by its str form."""
+6 -2
View File
@@ -983,7 +983,7 @@ async def test_async_delete_all_continues_on_partial_failure(mock_sqlite, mock_l
mem3.id = "mem-3"
mem3.payload = {"data": "three", "created_at": "2024-01-01T00:00:00+00:00", "actor_id": None, "role": None}
mock_vector_store.list.return_value = ([mem1, mem2, mem3],)
mock_vector_store.list.side_effect = [([mem1, mem2, mem3],), ([],)]
def _get_side_effect(vector_id):
if vector_id == "mem-2":
@@ -999,6 +999,10 @@ async def test_async_delete_all_continues_on_partial_failure(mock_sqlite, mock_l
assert result == {"message": "Memories deleted successfully!"}
assert mock_vector_store.delete.call_count == 2
assert mock_vector_store.list.call_args_list[0].kwargs == {
"filters": {"user_id": "test-user"},
"top_k": 1000,
}
@patch('mem0.utils.factory.EmbedderFactory.create')
@@ -1473,7 +1477,7 @@ class TestAsyncDeleteAllEntityRace:
mem_b = MagicMock()
mem_b.id = "mem-b"
mem_b.payload = {"data": "Alice works at Acme", "user_id": "alice"}
mock_vector_store.list.return_value = ([mem_a, mem_b],)
mock_vector_store.list.side_effect = [([mem_a, mem_b],), ([],)]
mock_vector_store.get.side_effect = lambda vector_id: {"mem-a": mem_a, "mem-b": mem_b}[vector_id]
mock_vector_factory.return_value = mock_vector_store