From 95f92069a7a4dc02453bfa5afe60e9e5c647f41c Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Sun, 18 May 2025 23:50:27 -0700 Subject: [PATCH] sqlite: Add test for search with list filters (#4747) --- .../langgraph/store/sqlite/base.py | 5 +- libs/checkpoint-sqlite/pyproject.toml | 2 +- .../tests/test_async_store.py | 60 +++++++++++++++++++ libs/checkpoint-sqlite/tests/test_store.py | 60 +++++++++++++++++++ libs/checkpoint-sqlite/uv.lock | 2 +- libs/langgraph/uv.lock | 2 +- libs/prebuilt/uv.lock | 2 +- 7 files changed, 127 insertions(+), 6 deletions(-) diff --git a/libs/checkpoint-sqlite/langgraph/store/sqlite/base.py b/libs/checkpoint-sqlite/langgraph/store/sqlite/base.py index d9c6b1e22..033434578 100644 --- a/libs/checkpoint-sqlite/langgraph/store/sqlite/base.py +++ b/libs/checkpoint-sqlite/langgraph/store/sqlite/base.py @@ -410,11 +410,12 @@ class BaseSqliteStore: "json_extract(value, '$." + key + "') = " + str(value) ) else: - # For complex objects, use param binding with JSON serialization + # Complex objects (list, dict, …) – compare JSON text filter_conditions.append( "json_extract(value, '$." + key + "') = ?" ) - filter_params.append(orjson.dumps(value)) + # orjson.dumps returns bytes → decode to str so SQLite sees TEXT + filter_params.append(orjson.dumps(value).decode()) # Vector search branch if op.query and self.index_config: diff --git a/libs/checkpoint-sqlite/pyproject.toml b/libs/checkpoint-sqlite/pyproject.toml index 2d3424504..e7de9e46f 100644 --- a/libs/checkpoint-sqlite/pyproject.toml +++ b/libs/checkpoint-sqlite/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "langgraph-checkpoint-sqlite" -version = "2.0.9" +version = "2.0.10" description = "Library with a SQLite implementation of LangGraph checkpoint saver." authors = [] requires-python = ">=3.9" diff --git a/libs/checkpoint-sqlite/tests/test_async_store.py b/libs/checkpoint-sqlite/tests/test_async_store.py index aef9b9ccd..ee83d5c81 100644 --- a/libs/checkpoint-sqlite/tests/test_async_store.py +++ b/libs/checkpoint-sqlite/tests/test_async_store.py @@ -657,3 +657,63 @@ async def test_list_namespaces( # Clean up for namespace in test_namespaces: await store.adelete(namespace, "dummy") + + +async 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"]}, + ] + + async 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): + key = f"item_{ns[-1]}" + await store.aput(ns, key, item) + + # 1. Search documents + docs = await store.asearch((base, "documents")) + assert len(docs) == 2 + assert all(item.namespace[1] == "documents" for item in docs) + + # 2. Search reports + reports = await store.asearch((base, "reports")) + assert len(reports) == 2 + assert all(item.namespace[1] == "reports" for item in reports) + + # 3. Pagination + first_page = await store.asearch((base,), limit=2, offset=0) + second_page = await store.asearch((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 = await store.asearch((base,)) + assert len(all_items) == 4 + + john_items = await store.asearch((base,), filter={"author": "John Doe"}) + assert len(john_items) == 2 + assert all(item.value["author"] == "John Doe" for item in john_items) + + draft_items = await store.asearch((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]}" + await store.adelete(ns, key) diff --git a/libs/checkpoint-sqlite/tests/test_store.py b/libs/checkpoint-sqlite/tests/test_store.py index 119118609..135624dc9 100644 --- a/libs/checkpoint-sqlite/tests/test_store.py +++ b/libs/checkpoint-sqlite/tests/test_store.py @@ -987,3 +987,63 @@ def test_list_namespaces_operations( # 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): + 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) diff --git a/libs/checkpoint-sqlite/uv.lock b/libs/checkpoint-sqlite/uv.lock index 90dbe7059..6c29b5a89 100644 --- a/libs/checkpoint-sqlite/uv.lock +++ b/libs/checkpoint-sqlite/uv.lock @@ -346,7 +346,7 @@ dev = [ [[package]] name = "langgraph-checkpoint-sqlite" -version = "2.0.9" +version = "2.0.10" source = { editable = "." } dependencies = [ { name = "aiosqlite" }, diff --git a/libs/langgraph/uv.lock b/libs/langgraph/uv.lock index 01ddd7ff0..0a17b3ef5 100644 --- a/libs/langgraph/uv.lock +++ b/libs/langgraph/uv.lock @@ -1365,7 +1365,7 @@ dev = [ [[package]] name = "langgraph-checkpoint-sqlite" -version = "2.0.9" +version = "2.0.10" source = { editable = "../checkpoint-sqlite" } dependencies = [ { name = "aiosqlite" }, diff --git a/libs/prebuilt/uv.lock b/libs/prebuilt/uv.lock index 8c9e04c12..08a3546b7 100644 --- a/libs/prebuilt/uv.lock +++ b/libs/prebuilt/uv.lock @@ -430,7 +430,7 @@ dev = [ [[package]] name = "langgraph-checkpoint-sqlite" -version = "2.0.9" +version = "2.0.10" source = { editable = "../checkpoint-sqlite" } dependencies = [ { name = "aiosqlite" },