Files
wehub-resource-sync a7d6d88f6f
CI / changes (push) Has been cancelled
CI / cd libs/checkpoint (push) Has been cancelled
CI / cd libs/checkpoint-conformance (push) Has been cancelled
CI / cd libs/checkpoint-postgres (push) Has been cancelled
CI / cd libs/checkpoint-sqlite (push) Has been cancelled
CI / cd libs/cli (push) Has been cancelled
CI / cd libs/prebuilt (push) Has been cancelled
CI / cd libs/sdk-py (push) Has been cancelled
CI / cd libs/langgraph (push) Has been cancelled
CI / Check SDK methods matching (push) Has been cancelled
CI / Check CLI schema hasn't changed #3.13 (push) Has been cancelled
CI / CLI integration test (push) Has been cancelled
CI / sdk-py integration test (push) Has been cancelled
CI / CI Success (push) Has been cancelled
baseline / benchmark (push) Has been cancelled
Deploy Redirects to GitHub Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:37:18 +08:00

1232 lines
46 KiB
Python

import os
import re
import tempfile
import uuid
from collections.abc import Generator, Iterable
from contextlib import contextmanager
from typing import Any, Literal, cast
import pytest
from langchain_core.embeddings import Embeddings
from langgraph.store.base import (
GetOp,
Item,
ListNamespacesOp,
MatchCondition,
PutOp,
SearchOp,
)
from langgraph.store.sqlite import SqliteStore
from langgraph.store.sqlite.base import SqliteIndexConfig
# Local embeddings implementation for testing vector search
class CharacterEmbeddings(Embeddings):
"""Simple character-frequency based embeddings using random projections."""
def __init__(self, dims: int = 50, seed: int = 42):
"""Initialize with embedding dimensions and random seed."""
import math
import random
from collections import defaultdict
self._rng = random.Random(seed)
self.dims = dims
# Create projection vector for each character lazily
self._char_projections: dict[str, list[float]] = defaultdict(
lambda: [
self._rng.gauss(0, 1 / math.sqrt(self.dims)) for _ in range(self.dims)
]
)
def _embed_one(self, text: str) -> list[float]:
"""Embed a single text."""
import math
from collections import Counter
counts = Counter(text)
total = sum(counts.values())
if total == 0:
return [0.0] * self.dims
embedding = [0.0] * self.dims
for char, count in counts.items():
weight = count / total
char_proj = self._char_projections[char]
for i, proj in enumerate(char_proj):
embedding[i] += weight * proj
norm = math.sqrt(sum(x * x for x in embedding))
if norm > 0:
embedding = [x / norm for x in embedding]
return embedding
def embed_documents(self, texts: list[str]) -> list[list[float]]:
"""Embed a list of documents."""
return [self._embed_one(text) for text in texts]
def embed_query(self, text: str) -> list[float]:
"""Embed a query string."""
return self._embed_one(text)
def __eq__(self, other: Any) -> bool:
return isinstance(other, CharacterEmbeddings) and self.dims == other.dims
@pytest.fixture(scope="function", params=["memory", "file"])
def store(request: Any) -> Generator[SqliteStore, None, None]:
"""Create a SqliteStore for testing."""
if request.param == "memory":
# In-memory store
with SqliteStore.from_conn_string(":memory:") as store:
store.setup()
yield store
else:
# Temporary file store
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_file.close()
try:
with SqliteStore.from_conn_string(temp_file.name) as store:
store.setup()
yield store
finally:
os.unlink(temp_file.name)
@pytest.fixture(scope="function")
def fake_embeddings() -> CharacterEmbeddings:
"""Create fake embeddings for testing."""
return CharacterEmbeddings(dims=500)
# Define vector types and distance types for parametrized tests
VECTOR_TYPES = ["cosine"] # SQLite only supports cosine similarity
@contextmanager
def create_vector_store(
fake_embeddings: CharacterEmbeddings,
text_fields: list[str] | None = None,
distance_type: str = "cosine",
conn_type: Literal["memory", "file"] = "memory",
) -> Generator[SqliteStore, None, None]:
"""Create a SqliteStore with vector search enabled."""
index_config: SqliteIndexConfig = {
"dims": fake_embeddings.dims,
"embed": fake_embeddings,
"text_fields": text_fields,
"distance_type": distance_type, # This is for API consistency but SQLite only supports cosine
}
if conn_type == "memory":
conn_str = ":memory:"
else:
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_file.close()
conn_str = temp_file.name
try:
with SqliteStore.from_conn_string(conn_str, index=index_config) as store:
store.setup()
yield store
finally:
if conn_type == "file":
os.unlink(conn_str)
def test_batch_order(store: SqliteStore) -> None:
# Setup test data
store.put(("test", "foo"), "key1", {"data": "value1"})
store.put(("test", "bar"), "key2", {"data": "value2"})
ops = [
GetOp(namespace=("test", "foo"), key="key1"),
PutOp(namespace=("test", "bar"), key="key2", value={"data": "value2"}),
SearchOp(
namespace_prefix=("test",), filter={"data": "value1"}, limit=10, offset=0
),
ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0),
GetOp(namespace=("test",), key="key3"),
]
results = store.batch(
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
)
assert len(results) == 5
assert isinstance(results[0], Item)
assert isinstance(results[0].value, dict)
assert results[0].value == {"data": "value1"}
assert results[0].key == "key1"
assert results[0].namespace == ("test", "foo")
assert results[1] is None # Put operation returns None
assert isinstance(results[2], list)
assert len(results[2]) == 1
assert results[2][0].key == "key1"
assert results[2][0].value == {"data": "value1"}
assert isinstance(results[3], list)
assert len(results[3]) > 0 # Should contain at least our test namespaces
assert ("test", "foo") in results[3]
assert ("test", "bar") in results[3]
assert results[4] is None # Non-existent key returns None
# Test reordered operations
ops_reordered = [
SearchOp(namespace_prefix=("test",), filter=None, limit=5, offset=0),
GetOp(namespace=("test", "bar"), key="key2"),
ListNamespacesOp(match_conditions=None, max_depth=None, limit=5, offset=0),
PutOp(namespace=("test",), key="key3", value={"data": "value3"}),
GetOp(namespace=("test", "foo"), key="key1"),
]
results_reordered = store.batch(
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops_reordered)
)
assert len(results_reordered) == 5
assert isinstance(results_reordered[0], list)
assert len(results_reordered[0]) >= 2 # Should find at least our two test items
assert isinstance(results_reordered[1], Item)
assert results_reordered[1].value == {"data": "value2"}
assert results_reordered[1].key == "key2"
assert results_reordered[1].namespace == ("test", "bar")
assert isinstance(results_reordered[2], list)
assert len(results_reordered[2]) > 0
assert results_reordered[3] is None # Put operation returns None
assert isinstance(results_reordered[4], Item)
assert results_reordered[4].value == {"data": "value1"}
assert results_reordered[4].key == "key1"
assert results_reordered[4].namespace == ("test", "foo")
# Verify the put worked
item3 = store.get(("test",), "key3")
assert item3 is not None
assert item3.value == {"data": "value3"}
def test_batch_get_ops(store: SqliteStore) -> None:
# Setup test data
store.put(("test",), "key1", {"data": "value1"})
store.put(("test",), "key2", {"data": "value2"})
ops = [
GetOp(namespace=("test",), key="key1"),
GetOp(namespace=("test",), key="key2"),
GetOp(namespace=("test",), key="key3"), # Non-existent key
]
results = store.batch(ops)
assert len(results) == 3
assert results[0] is not None
assert results[1] is not None
assert results[2] is None
assert results[0].key == "key1"
assert results[1].key == "key2"
def test_batch_put_ops(store: SqliteStore) -> None:
ops = [
PutOp(namespace=("test",), key="key1", value={"data": "value1"}),
PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
PutOp(namespace=("test",), key="key3", value=None), # Delete operation
]
results = store.batch(ops)
assert len(results) == 3
assert all(result is None for result in results)
# Verify the puts worked
item1 = store.get(("test",), "key1")
item2 = store.get(("test",), "key2")
item3 = store.get(("test",), "key3")
assert item1 and item1.value == {"data": "value1"}
assert item2 and item2.value == {"data": "value2"}
assert item3 is None
def test_batch_search_ops(store: SqliteStore) -> None:
# Setup test data
test_data = [
(("test", "foo"), "key1", {"data": "value1", "tag": "a"}),
(("test", "bar"), "key2", {"data": "value2", "tag": "a"}),
(("test", "baz"), "key3", {"data": "value3", "tag": "b"}),
]
for namespace, key, value in test_data:
store.put(namespace, key, value)
ops = [
SearchOp(namespace_prefix=("test",), filter={"tag": "a"}, limit=10, offset=0),
SearchOp(namespace_prefix=("test",), filter=None, limit=2, offset=0),
SearchOp(namespace_prefix=("test", "foo"), filter=None, limit=10, offset=0),
]
results = store.batch(ops)
assert len(results) == 3
# First search should find items with tag "a"
assert len(results[0]) == 2
assert all(item.value["tag"] == "a" for item in results[0])
# Second search should return first 2 items
assert len(results[1]) == 2
# Third search should only find items in test/foo namespace
assert len(results[2]) == 1
assert results[2][0].namespace == ("test", "foo")
def test_batch_list_namespaces_ops(store: SqliteStore) -> None:
# Setup test data with various namespaces
test_data = [
(("test", "documents", "public"), "doc1", {"content": "public doc"}),
(("test", "documents", "private"), "doc2", {"content": "private doc"}),
(("test", "images", "public"), "img1", {"content": "public image"}),
(("prod", "documents", "public"), "doc3", {"content": "prod doc"}),
]
for namespace, key, value in test_data:
store.put(namespace, key, value)
ops = [
ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0),
ListNamespacesOp(match_conditions=None, max_depth=2, limit=10, offset=0),
ListNamespacesOp(
match_conditions=tuple([MatchCondition("suffix", ("public",))]),
max_depth=None,
limit=10,
offset=0,
),
]
results = store.batch(
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
)
assert len(results) == 3
# First operation should list all namespaces
assert len(results[0]) == len(test_data)
# Second operation should only return namespaces up to depth 2
assert all(len(ns) <= 2 for ns in results[1])
# Third operation should only return namespaces ending with "public"
assert all(ns[-1] == "public" for ns in results[2])
class TestSqliteStore:
def test_basic_store_ops(self) -> None:
with SqliteStore.from_conn_string(":memory:") as store:
store.setup()
namespace = ("test", "documents")
item_id = "doc1"
item_value = {"title": "Test Document", "content": "Hello, World!"}
store.put(namespace, item_id, item_value)
item = store.get(namespace, item_id)
assert item
assert item.namespace == namespace
assert item.key == item_id
assert item.value == item_value
# Test update
# Small delay to ensure the updated timestamp is different
import time
time.sleep(0.01)
updated_value = {"title": "Updated Document", "content": "Hello, Updated!"}
store.put(namespace, item_id, updated_value)
updated_item = store.get(namespace, item_id)
assert updated_item.value == updated_value
# Don't check timestamps because SQLite execution might be too fast
# assert updated_item.updated_at > item.updated_at
# Test get from non-existent namespace
different_namespace = ("test", "other_documents")
item_in_different_namespace = store.get(different_namespace, item_id)
assert item_in_different_namespace is None
# Test delete
store.delete(namespace, item_id)
deleted_item = store.get(namespace, item_id)
assert deleted_item is None
def test_list_namespaces(self) -> None:
with SqliteStore.from_conn_string(":memory:") as store:
store.setup()
# Create test data with various namespaces
test_namespaces = [
("test", "documents", "public"),
("test", "documents", "private"),
("test", "images", "public"),
("test", "images", "private"),
("prod", "documents", "public"),
("prod", "documents", "private"),
]
# Insert test data
for namespace in test_namespaces:
store.put(namespace, "dummy", {"content": "dummy"})
# Test listing with various filters
all_namespaces = store.list_namespaces()
assert len(all_namespaces) == len(test_namespaces)
# Test prefix filtering
test_prefix_namespaces = store.list_namespaces(prefix=["test"])
assert len(test_prefix_namespaces) == 4
assert all(ns[0] == "test" for ns in test_prefix_namespaces)
# Test suffix filtering
public_namespaces = store.list_namespaces(suffix=["public"])
assert len(public_namespaces) == 3
assert all(ns[-1] == "public" for ns in public_namespaces)
# Test max depth
depth_2_namespaces = store.list_namespaces(max_depth=2)
assert all(len(ns) <= 2 for ns in depth_2_namespaces)
# Test pagination
paginated_namespaces = store.list_namespaces(limit=3)
assert len(paginated_namespaces) == 3
# Cleanup
for namespace in test_namespaces:
store.delete(namespace, "dummy")
def test_search(self) -> None:
with SqliteStore.from_conn_string(":memory:") as store:
store.setup()
# Create test data
test_data = [
(
("test", "docs"),
"doc1",
{"title": "First Doc", "author": "Alice", "tags": ["important"]},
),
(
("test", "docs"),
"doc2",
{"title": "Second Doc", "author": "Bob", "tags": ["draft"]},
),
(
("test", "images"),
"img1",
{"title": "Image 1", "author": "Alice", "tags": ["final"]},
),
]
for namespace, key, value in test_data:
store.put(namespace, key, value)
# Test basic search
all_items = store.search(["test"])
assert len(all_items) == 3
# Test namespace filtering
docs_items = store.search(["test", "docs"])
assert len(docs_items) == 2
assert all(item.namespace == ("test", "docs") for item in docs_items)
# Test value filtering
alice_items = store.search(["test"], filter={"author": "Alice"})
assert len(alice_items) == 2
assert all(item.value["author"] == "Alice" for item in alice_items)
# Test pagination
paginated_items = store.search(["test"], limit=2)
assert len(paginated_items) == 2
offset_items = store.search(["test"], offset=2)
assert len(offset_items) == 1
# Cleanup
for namespace, key, _ in test_data:
store.delete(namespace, key)
def test_vector_store_initialization(fake_embeddings: CharacterEmbeddings) -> None:
"""Test store initialization with embedding config."""
# Basic initialization
with create_vector_store(fake_embeddings) as store:
assert store.index_config is not None
assert store.embeddings == fake_embeddings
assert store.index_config["dims"] == fake_embeddings.dims
assert store.index_config.get("text_fields") is None
# With text fields specified
text_fields = ["content", "title"]
with create_vector_store(fake_embeddings, text_fields=text_fields) as store:
assert store.index_config is not None
assert store.embeddings == fake_embeddings
assert store.index_config["dims"] == fake_embeddings.dims
assert store.index_config["text_fields"] == text_fields
# Ensure store setup properly creates the vector tables
with create_vector_store(fake_embeddings) as store:
# Check if vector tables exist
cursor = store.conn.cursor()
cursor.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%vector%'"
)
tables = cursor.fetchall()
assert len(tables) >= 1, "Vector tables were not created"
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
@pytest.mark.parametrize("conn_type", ["memory", "file"])
def test_vector_insert_with_auto_embedding(
fake_embeddings: CharacterEmbeddings,
distance_type: str,
conn_type: Literal["memory", "file"],
) -> None:
"""Test inserting items that get auto-embedded."""
with create_vector_store(
fake_embeddings, distance_type=distance_type, conn_type=conn_type
) as store:
docs = [
("doc1", {"text": "short text"}),
("doc2", {"text": "longer text document"}),
("doc3", {"text": "longest text document here"}),
("doc4", {"description": "text in description field"}),
("doc5", {"content": "text in content field"}),
("doc6", {"body": "text in body field"}),
]
for key, value in docs:
store.put(("test",), key, value)
results = store.search(("test",), query="long text")
assert len(results) > 0
doc_order = [r.key for r in results]
assert "doc2" in doc_order
assert "doc3" in doc_order
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
@pytest.mark.parametrize("conn_type", ["memory", "file"])
def test_vector_update_with_embedding(
fake_embeddings: CharacterEmbeddings,
distance_type: str,
conn_type: Literal["memory", "file"],
) -> None:
"""Test that updating items properly updates their embeddings."""
with create_vector_store(
fake_embeddings, distance_type=distance_type, conn_type=conn_type
) as store:
store.put(("test",), "doc1", {"text": "zany zebra Xerxes"})
store.put(("test",), "doc2", {"text": "something about dogs"})
store.put(("test",), "doc3", {"text": "text about birds"})
results_initial = store.search(("test",), query="Zany Xerxes")
assert len(results_initial) > 0
assert results_initial[0].key == "doc1"
initial_score = results_initial[0].score
store.put(("test",), "doc1", {"text": "new text about dogs"})
results_after = store.search(("test",), query="Zany Xerxes")
after_score = next((r.score for r in results_after if r.key == "doc1"), 0.0)
assert after_score < initial_score
results_new = store.search(("test",), query="new text about dogs")
for r in results_new:
if r.key == "doc1":
assert r.score > after_score
# Don't index this one
store.put(("test",), "doc4", {"text": "new text about dogs"}, index=False)
results_new = store.search(("test",), query="new text about dogs", limit=3)
assert not any(r.key == "doc4" for r in results_new)
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
def test_vector_search_with_filters(
fake_embeddings: CharacterEmbeddings,
distance_type: str,
) -> None:
"""Test combining vector search with filters."""
with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
# Insert test documents
docs = [
("doc1", {"text": "red apple", "color": "red", "score": 4.5}),
("doc2", {"text": "red car", "color": "red", "score": 3.0}),
("doc3", {"text": "green apple", "color": "green", "score": 4.0}),
("doc4", {"text": "blue car", "color": "blue", "score": 3.5}),
]
for key, value in docs:
store.put(("test",), key, value)
results = store.search(("test",), query="apple", filter={"color": "red"})
# Check ordering and score - verify "doc1" is first result
assert len(results) == 2
assert results[0].key == "doc1"
results = store.search(("test",), query="car", filter={"color": "red"})
# Check ordering - verify "doc2" is first result
assert len(results) > 0
assert results[0].key == "doc2"
results = store.search(
("test",), query="bbbbluuu", filter={"score": {"$gt": 3.2}}
)
# There should be 3 documents with score > 3.2
assert len(results) == 3
# Check that the blue car is the most similar to "bbbbluuu" query
assert results[0].key == "doc4" # The blue car should be the most relevant
# Verify remaining docs are ordered by appropriate similarity
high_score_keys = [r.key for r in results]
assert "doc1" in high_score_keys # score 4.5
assert "doc3" in high_score_keys # score 4.0
# Multiple filters
results = store.search(
("test",), query="apple", filter={"score": {"$gte": 4.0}, "color": "green"}
)
# Check that doc3 is the top result
assert len(results) > 0
assert results[0].key == "doc3"
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
def test_vector_search_pagination(
fake_embeddings: CharacterEmbeddings,
distance_type: str,
) -> None:
"""Test pagination with vector search."""
with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
# Insert multiple similar documents
for i in range(5):
store.put(("test",), f"doc{i}", {"text": f"test document number {i}"})
# Test with different page sizes
results_page1 = store.search(("test",), query="test", limit=2)
results_page2 = store.search(("test",), query="test", limit=2, offset=2)
assert len(results_page1) == 2
assert len(results_page2) == 2
# Make sure different pages have different results
assert results_page1[0].key != results_page2[0].key
assert results_page1[1].key != results_page2[0].key
assert results_page1[0].key != results_page2[1].key
assert results_page1[1].key != results_page2[1].key
# Check scores are in descending order within each page
assert results_page1[0].score >= results_page1[1].score
assert results_page2[0].score >= results_page2[1].score
# First page results should have higher scores than second page
all_results = store.search(("test",), query="test", limit=10)
assert len(all_results) == 5
assert (
all_results[0].score >= all_results[2].score
) # First page vs second page start
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
def test_vector_search_edge_cases(
fake_embeddings: CharacterEmbeddings,
distance_type: str,
) -> None:
"""Test edge cases in vector search."""
with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
store.put(("test",), "doc1", {"text": "test document"})
results = store.search(("test",), query="")
assert len(results) == 1
results = store.search(("test",), query=None)
assert len(results) == 1
long_query = "test " * 100
results = store.search(("test",), query=long_query)
assert len(results) == 1
special_query = "test!@#$%^&*()"
results = store.search(("test",), query=special_query)
assert len(results) == 1
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
def test_embed_with_path(
fake_embeddings: CharacterEmbeddings,
distance_type: str,
) -> None:
"""Test vector search with specific text fields in SQLite store."""
with create_vector_store(
fake_embeddings,
text_fields=["key0", "key1", "key3"],
distance_type=distance_type,
) as store:
# This will have 2 vectors representing it
doc1 = {
# Omit key0 - check it doesn't raise an error
"key1": "xxx",
"key2": "yyy",
"key3": "zzz",
}
# This will have 3 vectors representing it
doc2 = {
"key0": "uuu",
"key1": "vvv",
"key2": "www",
"key3": "xxx",
}
store.put(("test",), "doc1", doc1)
store.put(("test",), "doc2", doc2)
# doc2.key3 and doc1.key1 both would have the highest score
results = store.search(("test",), query="xxx")
assert len(results) == 2
assert results[0].key != results[1].key
assert results[0].score > 0.9
assert results[1].score > 0.9
# ~Only match doc2
results = store.search(("test",), query="uuu")
assert len(results) == 2
assert results[0].key != results[1].key
assert results[0].key == "doc2"
assert results[0].score > results[1].score
# ~Only match doc1
results = store.search(("test",), query="zzz")
assert len(results) == 2
assert results[0].key != results[1].key
assert results[0].key == "doc1"
assert results[0].score > results[1].score
# Un-indexed - will have low results for both, Not zero (because we're projecting)
# but less than the above.
results = store.search(("test",), query="www")
assert len(results) == 2
assert results[0].key != results[1].key
assert results[0].score < 0.9
assert results[1].score < 0.9
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
def test_embed_with_path_operation_config(
fake_embeddings: CharacterEmbeddings,
distance_type: str,
) -> None:
"""Test operation-level field configuration for vector search."""
with create_vector_store(
fake_embeddings, text_fields=["key17"], distance_type=distance_type
) as store:
doc3 = {
"key0": "aaa",
"key1": "bbb",
"key2": "ccc",
"key3": "ddd",
}
doc4 = {
"key0": "eee",
"key1": "bbb", # Same as doc3.key1
"key2": "fff",
"key3": "ggg",
}
store.put(("test",), "doc3", doc3, index=["key0", "key1"])
store.put(("test",), "doc4", doc4, index=["key1", "key3"])
results = store.search(("test",), query="aaa")
assert len(results) == 2
assert results[0].key == "doc3"
assert len(set(r.key for r in results)) == 2
assert results[0].score > results[1].score
results = store.search(("test",), query="ggg")
assert len(results) == 2
assert results[0].key == "doc4"
assert results[0].score > results[1].score
results = store.search(("test",), query="bbb")
assert len(results) == 2
assert results[0].key != results[1].key
assert abs(results[0].score - results[1].score) < 0.1 # Similar scores
results = store.search(("test",), query="ccc")
assert len(results) == 2
assert all(
r.score < 0.9 for r in results
) # Unindexed field should have low scores
# Test index=False behavior
doc5 = {
"key0": "hhh",
"key1": "iii",
}
store.put(("test",), "doc5", doc5, index=False)
results = store.search(("test",))
assert len(results) == 3
assert any(r.key == "doc5" for r in results)
# Helper functions for vector similarity calculations
def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
"""
Compute cosine similarity between a vector X and a matrix Y.
Lazy import numpy for efficiency.
"""
similarities = []
for y in Y:
dot_product = sum(a * b for a, b in zip(X, y, strict=False))
norm1 = sum(a * a for a in X) ** 0.5
norm2 = sum(a * a for a in y) ** 0.5
similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
similarities.append(similarity)
return similarities
@pytest.mark.parametrize("query", ["aaa", "bbb", "ccc", "abcd", "poisson"])
@pytest.mark.parametrize("conn_type", ["memory", "file"])
def test_scores(
fake_embeddings: CharacterEmbeddings,
query: str,
conn_type: Literal["memory", "file"],
) -> None:
"""Test operation-level field configuration for vector search."""
with create_vector_store(
fake_embeddings,
text_fields=["key0"],
distance_type="cosine",
conn_type=conn_type,
) as store:
doc = {
"key0": "aaa",
}
store.put(("test",), "doc", doc, index=["key0", "key1"])
results = store.search((), query=query)
vec0 = fake_embeddings.embed_query(doc["key0"])
vec1 = fake_embeddings.embed_query(query)
# SQLite uses cosine similarity by default
similarities = _cosine_similarity(vec1, [vec0])
assert len(results) == 1
assert results[0].score == pytest.approx(similarities[0], abs=1e-3)
def test_nonnull_migrations() -> None:
"""Test that all migration statements are non-null."""
_leading_comment_remover = re.compile(r"^/\*.*?\*/")
for migration in SqliteStore.MIGRATIONS:
statement = _leading_comment_remover.sub("", migration).split()[0]
assert statement.strip(), f"Empty migration statement found: {migration}"
def test_basic_store_operations(
fake_embeddings: CharacterEmbeddings,
) -> None:
"""Test basic store operations with SQLite store."""
with create_vector_store(
fake_embeddings, text_fields=["key0", "key1", "key3"]
) as store:
uid = uuid.uuid4().hex
namespace = (uid, "test", "documents")
item_id = "doc1"
item_value = {"title": "Test Document", "content": "Hello, World!"}
results = store.search((uid,))
assert len(results) == 0
store.put(namespace, item_id, item_value)
item = store.get(namespace, item_id)
assert item is not None
assert item.namespace == namespace
assert item.key == item_id
assert item.value == item_value
assert item.created_at is not None
assert item.updated_at is not None
updated_value = {
"title": "Updated Test Document",
"content": "Hello, LangGraph!",
}
store.put(namespace, item_id, updated_value)
updated_item = store.get(namespace, item_id)
assert updated_item is not None
assert updated_item.value == updated_value
assert updated_item.updated_at >= item.updated_at
different_namespace = (uid, "test", "other_documents")
item_in_different_namespace = store.get(different_namespace, item_id)
assert item_in_different_namespace is None
new_item_id = "doc2"
new_item_value = {"title": "Another Document", "content": "Greetings!"}
store.put(namespace, new_item_id, new_item_value)
items = store.search((uid, "test"), limit=10)
assert len(items) == 2
assert any(item.key == item_id for item in items)
assert any(item.key == new_item_id for item in items)
namespaces = store.list_namespaces(prefix=(uid, "test"))
assert (uid, "test", "documents") in namespaces
store.delete(namespace, item_id)
store.delete(namespace, new_item_id)
deleted_item = store.get(namespace, item_id)
assert deleted_item is None
deleted_item = store.get(namespace, new_item_id)
assert deleted_item is None
empty_search_results = store.search((uid, "test"), limit=10)
assert len(empty_search_results) == 0
def test_list_namespaces_operations(
fake_embeddings: CharacterEmbeddings,
) -> None:
"""Test list namespaces functionality with various filters."""
with create_vector_store(
fake_embeddings, text_fields=["key0", "key1", "key3"]
) as store:
test_pref = str(uuid.uuid4())
test_namespaces = [
(test_pref, "test", "documents", "public", test_pref),
(test_pref, "test", "documents", "private", test_pref),
(test_pref, "test", "images", "public", test_pref),
(test_pref, "test", "images", "private", test_pref),
(test_pref, "prod", "documents", "public", test_pref),
(test_pref, "prod", "documents", "some", "nesting", "public", test_pref),
(test_pref, "prod", "documents", "private", test_pref),
]
# Add test data
for namespace in test_namespaces:
store.put(namespace, "dummy", {"content": "dummy"})
# Test prefix filtering
prefix_result = store.list_namespaces(prefix=(test_pref, "test"))
assert len(prefix_result) == 4
assert all(ns[1] == "test" for ns in prefix_result)
# Test specific prefix
specific_prefix_result = store.list_namespaces(
prefix=(test_pref, "test", "documents")
)
assert len(specific_prefix_result) == 2
assert all(ns[1:3] == ("test", "documents") for ns in specific_prefix_result)
# Test suffix filtering
suffix_result = store.list_namespaces(suffix=("public", test_pref))
assert len(suffix_result) == 4
assert all(ns[-2] == "public" for ns in suffix_result)
# Test combined prefix and suffix
prefix_suffix_result = store.list_namespaces(
prefix=(test_pref, "test"), suffix=("public", test_pref)
)
assert len(prefix_suffix_result) == 2
assert all(
ns[1] == "test" and ns[-2] == "public" for ns in prefix_suffix_result
)
# Test wildcard in prefix
wildcard_prefix_result = store.list_namespaces(
prefix=(test_pref, "*", "documents")
)
assert len(wildcard_prefix_result) == 5
assert all(ns[2] == "documents" for ns in wildcard_prefix_result)
# Test wildcard in suffix
wildcard_suffix_result = store.list_namespaces(
suffix=("*", "public", test_pref)
)
assert len(wildcard_suffix_result) == 4
assert all(ns[-2] == "public" for ns in wildcard_suffix_result)
wildcard_single = store.list_namespaces(
suffix=("some", "*", "public", test_pref)
)
assert len(wildcard_single) == 1
assert wildcard_single[0] == (
test_pref,
"prod",
"documents",
"some",
"nesting",
"public",
test_pref,
)
# Test max depth
max_depth_result = store.list_namespaces(max_depth=3)
assert all(len(ns) <= 3 for ns in max_depth_result)
max_depth_result = store.list_namespaces(
max_depth=4, prefix=(test_pref, "*", "documents")
)
assert len(set(res for res in max_depth_result)) == len(max_depth_result) == 5
# Test pagination
limit_result = store.list_namespaces(prefix=(test_pref,), limit=3)
assert len(limit_result) == 3
offset_result = store.list_namespaces(prefix=(test_pref,), offset=3)
assert len(offset_result) == len(test_namespaces) - 3
empty_prefix_result = store.list_namespaces(prefix=(test_pref,))
assert len(empty_prefix_result) == len(test_namespaces)
assert set(empty_prefix_result) == set(test_namespaces)
# Clean up
for namespace in test_namespaces:
store.delete(namespace, "dummy")
def test_search_items(
fake_embeddings: CharacterEmbeddings,
) -> None:
"""Test search_items functionality by calling store methods directly."""
base = "test_search_items"
test_namespaces = [
(base, "documents", "user1"),
(base, "documents", "user2"),
(base, "reports", "department1"),
(base, "reports", "department2"),
]
test_items = [
{"title": "Doc 1", "author": "John Doe", "tags": ["important"]},
{"title": "Doc 2", "author": "Jane Smith", "tags": ["draft"]},
{"title": "Report A", "author": "John Doe", "tags": ["final"]},
{"title": "Report B", "author": "Alice Johnson", "tags": ["draft"]},
]
with create_vector_store(
fake_embeddings, text_fields=["key0", "key1", "key3"]
) as store:
# Insert test data
for ns, item in zip(test_namespaces, test_items, strict=False):
key = f"item_{ns[-1]}"
store.put(ns, key, item)
# 1. Search documents
docs = store.search((base, "documents"))
assert len(docs) == 2
assert all(item.namespace[1] == "documents" for item in docs)
# 2. Search reports
reports = store.search((base, "reports"))
assert len(reports) == 2
assert all(item.namespace[1] == "reports" for item in reports)
# 3. Pagination
first_page = store.search((base,), limit=2, offset=0)
second_page = store.search((base,), limit=2, offset=2)
assert len(first_page) == 2
assert len(second_page) == 2
keys_page1 = {item.key for item in first_page}
keys_page2 = {item.key for item in second_page}
assert keys_page1.isdisjoint(keys_page2)
all_items = store.search((base,))
assert len(all_items) == 4
john_items = store.search((base,), filter={"author": "John Doe"})
assert len(john_items) == 2
assert all(item.value["author"] == "John Doe" for item in john_items)
draft_items = store.search((base,), filter={"tags": ["draft"]})
assert len(draft_items) == 2
assert all("draft" in item.value["tags"] for item in draft_items)
for ns in test_namespaces:
key = f"item_{ns[-1]}"
store.delete(ns, key)
def test_sql_injection_vulnerability(store: SqliteStore) -> None:
"""Test that SQL injection via malicious filter keys is prevented."""
# Add public and private documents
store.put(("docs",), "public", {"access": "public", "data": "public info"})
store.put(
("docs",), "private", {"access": "private", "data": "secret", "password": "123"}
)
# Normal query - returns 1 public document
normal = store.search(("docs",), filter={"access": "public"})
assert len(normal) == 1
assert normal[0].value["access"] == "public"
# SQL injection attempt via malicious key should raise ValueError
malicious_key = "access') = 'public' OR '1'='1' OR json_extract(value, '$."
with pytest.raises(ValueError, match="Invalid filter key"):
store.search(("docs",), filter={malicious_key: "dummy"})
def test_sql_injection_filter_values(store: SqliteStore) -> None:
"""Test that SQL injection via malicious filter values is properly escaped."""
# Setup: Create documents with different access levels
store.put(("docs",), "doc1", {"access": "public", "title": "Public Document"})
store.put(("docs",), "doc2", {"access": "private", "title": "Private Document"})
store.put(("docs",), "doc3", {"access": "secret", "title": "Secret Document"})
# Test 1: Basic SQL injection attempt with single quote
malicious_value = "public' OR '1'='1"
results = store.search(("docs",), filter={"access": malicious_value})
# Should return 0 results because the malicious value is escaped and won't match anything
assert len(results) == 0, "SQL injection via string value should be blocked"
# Test 2: SQL injection with comment
malicious_value = "public'; --"
results = store.search(("docs",), filter={"access": malicious_value})
assert len(results) == 0, "SQL comment injection should be blocked"
# Test 3: UNION injection attempt
malicious_value = "public' UNION SELECT * FROM store --"
results = store.search(("docs",), filter={"access": malicious_value})
assert len(results) == 0, "UNION injection should be blocked"
# Test 4: Parameterized queries handle strings with null bytes and SQL injection attempts safely
malicious_value = "public\x00' OR '1'='1"
results = store.search(("docs",), filter={"access": malicious_value})
assert len(results) == 0, (
"Parameterized queries treat injection attempts as literal strings"
)
# Test 5: Multiple single quotes
malicious_value = "''''"
results = store.search(("docs",), filter={"access": malicious_value})
assert len(results) == 0, "Multiple quotes should be handled safely"
# Test 6: Legitimate value with single quote should work
store.put(("docs",), "doc4", {"title": "O'Brien's Document", "access": "public"})
results = store.search(("docs",), filter={"title": "O'Brien's Document"})
assert len(results) == 1, "Legitimate single quotes should work"
assert results[0].value["title"] == "O'Brien's Document"
# Test 7: Unicode characters with injection attempt
malicious_value = "public' OR 'א'='א"
results = store.search(("docs",), filter={"access": malicious_value})
assert len(results) == 0, "Unicode-based injection should be blocked"
def test_numeric_filter_safety(store: SqliteStore) -> None:
"""Test that numeric filter values are handled safely."""
# Setup: Create documents with numeric fields
store.put(("items",), "item1", {"price": 10, "quantity": 5})
store.put(("items",), "item2", {"price": 20, "quantity": 3})
store.put(("items",), "item3", {"price": 30, "quantity": 1})
# Test 1: Normal numeric comparison
results = store.search(("items",), filter={"price": {"$gt": 15}})
assert len(results) == 2
assert all(r.value["price"] > 15 for r in results)
# Test 2: Special float values (infinity)
results = store.search(("items",), filter={"price": {"$lt": float("inf")}})
assert len(results) == 3, "All finite values should be less than infinity"
# Test 3: Special float values (negative infinity)
results = store.search(("items",), filter={"price": {"$gt": float("-inf")}})
assert len(results) == 3, (
"All finite values should be greater than negative infinity"
)
# Test 4: NaN handling - NaN comparisons should not cause errors
try:
results = store.search(("items",), filter={"price": {"$eq": float("nan")}})
# NaN never equals anything, including itself, so should return 0 results
assert len(results) == 0
except Exception as e:
pytest.fail(f"NaN handling should not raise exception: {e}")
# Test 5: Very large numbers
results = store.search(("items",), filter={"price": {"$lt": 10**100}})
assert len(results) == 3, "Very large numbers should be handled safely"
# Test 6: Negative numbers
store.put(("items",), "item4", {"price": -10, "quantity": 0})
results = store.search(("items",), filter={"price": {"$lt": 0}})
assert len(results) == 1
assert results[0].key == "item4"
def test_boolean_filter_safety(store: SqliteStore) -> None:
"""Test that boolean filter values are handled safely."""
store.put(("flags",), "flag1", {"active": True, "name": "Feature A"})
store.put(("flags",), "flag2", {"active": False, "name": "Feature B"})
store.put(("flags",), "flag3", {"active": True, "name": "Feature C"})
# Test boolean filters
results = store.search(("flags",), filter={"active": True})
assert len(results) == 2
assert all(r.value["active"] is True for r in results)
results = store.search(("flags",), filter={"active": False})
assert len(results) == 1
assert results[0].value["active"] is False
def test_filter_keys_with_hyphens_and_digits(store: SqliteStore) -> None:
"""Keys with hyphens or leading digits should be queryable via filters.
Current unquoted JSON path construction (e.g., '$.access-level' or '$.123abc')
is not valid JSON1 syntax, so this test will catch regressions in path handling.
"""
# Documents with top-level and nested keys requiring bracket-quoted JSON paths
store.put(
("docs",),
"hyphen",
{"access-level": "public", "user": {"access-level": "nested"}},
)
store.put(("docs",), "digit", {"123abc": "ok", "user": {"123abc": "ok2"}})
# Top-level hyphenated key
results = store.search(("docs",), filter={"access-level": "public"})
assert [r.key for r in results] == ["hyphen"]
# Nested hyphenated key via dotted path
results = store.search(("docs",), filter={"user.access-level": "nested"})
assert [r.key for r in results] == ["hyphen"]
# Top-level digit-starting key
results = store.search(("docs",), filter={"123abc": "ok"})
assert [r.key for r in results] == ["digit"]
# Nested digit-starting key via dotted path
results = store.search(("docs",), filter={"user.123abc": "ok2"})
assert [r.key for r in results] == ["digit"]
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
def test_non_ascii(
fake_embeddings: CharacterEmbeddings,
distance_type: str,
) -> None:
"""Test support for non-ascii characters"""
with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
store.put(
("user_123", "memories"), "2", {"text": "これは日本語です"}
) # Japanese
store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
result1 = store.search(("user_123", "memories"), query="这是中文")
result2 = store.search(("user_123", "memories"), query="これは日本語です")
result3 = store.search(("user_123", "memories"), query="이건 한국어야")
result4 = store.search(("user_123", "memories"), query="Это русский")
result5 = store.search(("user_123", "memories"), query="यह रूसी है")
assert result1[0].key == "1"
assert result2[0].key == "2"
assert result3[0].key == "3"
assert result4[0].key == "4"
assert result5[0].key == "5"