mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-09-12 04:01:35 +00:00
This branch was forked from an earlier commit of feat/vector-store-iter-all
(#1316), before that PR fixed a false-positive/silent-truncation bug in
QdrantStore.iter_all(): an empty scroll page with a still-advancing cursor
(e.g. a window landing entirely on tombstoned points) was treated as the
end of the collection instead of continuing. Syncing qdrant_store.py,
vector_store.py, and their tests to #1316's current tip (fa967983) so this
branch doesn't reintroduce the already-fixed bug once merged. Content-only
sync of the 4 shared files (verified via diff against origin/feat/vector-store-iter-all)
rather than a full branch merge, to avoid pulling in unrelated main drift
that has landed on that branch since this one diverged.
161 lines
5.1 KiB
Python
161 lines
5.1 KiB
Python
"""Tests for QdrantStore.iter_all() cursor enumeration.
|
|
|
|
Qdrant is not installed in this environment, so these drive the real
|
|
QdrantStore against a MagicMock standing in for the qdrant_client, following
|
|
the pattern already used for qdrant in test_backend_metadata_filtering.py.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from semantica.utils.exceptions import ProcessingError
|
|
from semantica.vector_store.qdrant_store import QdrantStore
|
|
|
|
|
|
def _record(point_id, payload=None, vector=None):
|
|
"""Build a stand-in for a qdrant_client Record."""
|
|
rec = MagicMock()
|
|
rec.id = point_id
|
|
rec.payload = payload
|
|
rec.vector = vector
|
|
return rec
|
|
|
|
|
|
def _store_with_scroll(*pages):
|
|
"""QdrantStore whose client.scroll() returns the given (records, cursor) pages."""
|
|
store = QdrantStore()
|
|
store.client = MagicMock()
|
|
store.client.scroll.side_effect = list(pages)
|
|
store.collection = MagicMock()
|
|
store.collection.collection_name = "test_collection"
|
|
return store
|
|
|
|
|
|
@patch("semantica.vector_store.qdrant_store.QDRANT_AVAILABLE", True)
|
|
def test_iter_all_threads_cursor_across_pages():
|
|
"""The next call continues from the previous page's cursor."""
|
|
store = _store_with_scroll(
|
|
([_record(1), _record(2)], "cursor-1"),
|
|
([_record(3)], None),
|
|
)
|
|
|
|
result = list(store.iter_all(batch_size=2))
|
|
|
|
assert [item["id"] for item in result] == ["1", "2", "3"]
|
|
calls = store.client.scroll.call_args_list
|
|
assert len(calls) == 2
|
|
assert calls[0][1]["offset"] is None
|
|
assert calls[0][1]["limit"] == 2
|
|
assert calls[1][1]["offset"] == "cursor-1"
|
|
|
|
|
|
@patch("semantica.vector_store.qdrant_store.QDRANT_AVAILABLE", True)
|
|
def test_iter_all_yields_final_page_that_reports_no_next_cursor():
|
|
"""Records and a null cursor can arrive together; those records must still
|
|
be yielded or every scan loses its tail."""
|
|
store = _store_with_scroll(([_record(1), _record(2)], None))
|
|
|
|
result = list(store.iter_all(batch_size=10))
|
|
|
|
assert [item["id"] for item in result] == ["1", "2"]
|
|
assert store.client.scroll.call_count == 1
|
|
|
|
|
|
@patch("semantica.vector_store.qdrant_store.QDRANT_AVAILABLE", True)
|
|
def test_iter_all_converts_records_to_the_shared_result_shape():
|
|
store = _store_with_scroll(
|
|
([_record(7, payload={"tag": "x"}, vector=[0.1, 0.2, 0.3])], None),
|
|
)
|
|
|
|
item = list(store.iter_all())[0]
|
|
|
|
assert item["id"] == "7"
|
|
assert item["metadata"] == {"tag": "x"}
|
|
np.testing.assert_allclose(item["vector"], np.array([0.1, 0.2, 0.3]))
|
|
|
|
|
|
@patch("semantica.vector_store.qdrant_store.QDRANT_AVAILABLE", True)
|
|
def test_iter_all_handles_missing_payload_and_vector():
|
|
store = _store_with_scroll(([_record(1, payload=None, vector=None)], None))
|
|
|
|
item = list(store.iter_all())[0]
|
|
|
|
assert item["metadata"] == {}
|
|
assert item["vector"] is None
|
|
|
|
|
|
@patch("semantica.vector_store.qdrant_store.QDRANT_AVAILABLE", True)
|
|
def test_iter_all_empty_collection_yields_nothing():
|
|
store = _store_with_scroll(([], None))
|
|
|
|
assert list(store.iter_all()) == []
|
|
|
|
|
|
@patch("semantica.vector_store.qdrant_store.QDRANT_AVAILABLE", True)
|
|
def test_iter_all_continues_past_empty_page_with_advancing_cursor():
|
|
store = _store_with_scroll(
|
|
([], "cursor-1"),
|
|
([_record(1)], None),
|
|
)
|
|
|
|
result = list(store.iter_all())
|
|
|
|
assert [item["id"] for item in result] == ["1"]
|
|
assert store.client.scroll.call_count == 2
|
|
|
|
|
|
@patch("semantica.vector_store.qdrant_store.QDRANT_AVAILABLE", True)
|
|
def test_iter_all_raises_when_cursor_stops_advancing():
|
|
store = _store_with_scroll(
|
|
([], "stuck-cursor"),
|
|
([], "stuck-cursor"),
|
|
)
|
|
|
|
with pytest.raises(ProcessingError, match="stopped advancing"):
|
|
list(store.iter_all())
|
|
|
|
|
|
@patch("semantica.vector_store.qdrant_store.QDRANT_AVAILABLE", True)
|
|
def test_iter_all_raises_when_collection_not_initialized():
|
|
"""Must fail loudly: an empty scan reads the same as an empty source."""
|
|
store = QdrantStore()
|
|
|
|
with pytest.raises(ProcessingError, match="Collection not initialized"):
|
|
list(store.iter_all())
|
|
|
|
|
|
@patch("semantica.vector_store.qdrant_store.QDRANT_AVAILABLE", False)
|
|
def test_iter_all_raises_when_qdrant_unavailable():
|
|
store = QdrantStore()
|
|
store.client = MagicMock()
|
|
store.collection = MagicMock()
|
|
|
|
with pytest.raises(ProcessingError):
|
|
list(store.iter_all())
|
|
|
|
|
|
@patch("semantica.vector_store.qdrant_store.QDRANT_AVAILABLE", True)
|
|
def test_iter_all_propagates_scroll_errors():
|
|
store = QdrantStore()
|
|
store.client = MagicMock()
|
|
store.client.scroll.side_effect = RuntimeError("connection reset")
|
|
store.collection = MagicMock()
|
|
store.collection.collection_name = "test_collection"
|
|
|
|
with pytest.raises(RuntimeError, match="connection reset"):
|
|
list(store.iter_all())
|
|
|
|
|
|
@patch("semantica.vector_store.qdrant_store.QDRANT_AVAILABLE", True)
|
|
def test_iter_all_requests_payload_and_vectors():
|
|
store = _store_with_scroll(([], None))
|
|
|
|
list(store.iter_all())
|
|
|
|
kwargs = store.client.scroll.call_args[1]
|
|
assert kwargs["with_payload"] is True
|
|
assert kwargs["with_vectors"] is True
|
|
assert kwargs["collection_name"] == "test_collection"
|