Skip to content

Commit b045e2b

Browse files
committed
fix: ensure version values are strings before Tag comparison (#40)
This fixes issue #40 where numeric version values caused TypeError when compared with RedisVL Tag objects. The Tag type only accepts specific types (str, list, set, tuple, None) for comparison operations. The fix adds string conversion for all version values in the _dump_blobs method before they're used in Tag comparisons: - Added tests to reproduce and verify the issue - Added explicit string conversion using dict comprehension - Verified compatibility with existing functionality This prevents "TypeError: Right side argument passed to operator == with left side argument Tag must be of type..." errors when numeric versions are used.
1 parent 238d0c7 commit b045e2b

File tree

3 files changed

+182
-3
lines changed

3 files changed

+182
-3
lines changed

langgraph/checkpoint/redis/base.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,19 +288,22 @@ def _dump_blobs(
288288
storage_safe_thread_id = to_storage_safe_id(thread_id)
289289
storage_safe_checkpoint_ns = to_storage_safe_str(checkpoint_ns)
290290

291+
# Ensure all versions are converted to strings to avoid TypeError with Tag filters
292+
str_versions = {k: str(v) for k, v in versions.items()}
293+
291294
return [
292295
(
293296
BaseRedisSaver._make_redis_checkpoint_blob_key(
294297
storage_safe_thread_id,
295298
storage_safe_checkpoint_ns,
296299
k,
297-
cast(str, ver),
300+
str_versions[k], # Use the string version
298301
),
299302
{
300303
"thread_id": storage_safe_thread_id,
301304
"checkpoint_ns": storage_safe_checkpoint_ns,
302305
"channel": k,
303-
"version": cast(str, ver),
306+
"version": str_versions[k], # Use the string version
304307
"type": (
305308
self._get_type_and_blob(values[k])[0]
306309
if k in values
@@ -311,7 +314,7 @@ def _dump_blobs(
311314
),
312315
},
313316
)
314-
for k, ver in versions.items()
317+
for k in str_versions.keys()
315318
]
316319

317320
def _dump_writes(

tests/test_numeric_version_fix.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
Test for the fix to issue #40 - Fixing numeric version handling with Tag type.
3+
"""
4+
5+
from contextlib import contextmanager
6+
7+
import pytest
8+
from langgraph.checkpoint.base import empty_checkpoint
9+
from redis import Redis
10+
11+
from langgraph.checkpoint.redis import RedisSaver
12+
13+
14+
@pytest.fixture(autouse=True)
15+
async def clear_test_redis(redis_url: str) -> None:
16+
"""Clear Redis before each test."""
17+
client = Redis.from_url(redis_url)
18+
try:
19+
client.flushall()
20+
finally:
21+
client.close()
22+
23+
24+
@contextmanager
25+
def patched_redis_saver(redis_url):
26+
"""
27+
Create a RedisSaver with a patched _dump_blobs method to fix the issue.
28+
This demonstrates the fix approach.
29+
"""
30+
original_dump_blobs = RedisSaver._dump_blobs
31+
32+
def patched_dump_blobs(self, thread_id, checkpoint_ns, values, versions):
33+
"""
34+
Patched version of _dump_blobs that ensures version is a string.
35+
"""
36+
# Convert version to string in versions dictionary
37+
string_versions = {k: str(v) for k, v in versions.items()}
38+
39+
# Call the original method with string versions
40+
return original_dump_blobs(
41+
self, thread_id, checkpoint_ns, values, string_versions
42+
)
43+
44+
# Apply the patch
45+
RedisSaver._dump_blobs = patched_dump_blobs
46+
47+
try:
48+
# Create the saver with patched method
49+
saver = RedisSaver(redis_url)
50+
saver.setup()
51+
yield saver
52+
finally:
53+
# Restore the original method
54+
RedisSaver._dump_blobs = original_dump_blobs
55+
# Clean up
56+
if saver._owns_its_client:
57+
saver._redis.close()
58+
59+
60+
def test_numeric_version_fix(redis_url: str) -> None:
61+
"""
62+
Test that demonstrates the fix for issue #40.
63+
64+
This shows how to handle numeric versions correctly by ensuring
65+
they are converted to strings before being used with Tag.
66+
"""
67+
# Use our patched version that converts numeric versions to strings
68+
with patched_redis_saver(redis_url) as saver:
69+
# Set up a basic config
70+
config = {
71+
"configurable": {
72+
"thread_id": "thread-numeric-version-fix",
73+
"checkpoint_ns": "",
74+
}
75+
}
76+
77+
# Create a basic checkpoint
78+
checkpoint = empty_checkpoint()
79+
80+
# Store the checkpoint with our patched method
81+
saved_config = saver.put(
82+
config, checkpoint, {}, {"test_channel": 1}
83+
) # Numeric version
84+
85+
# Get the checkpoint ID from the saved config
86+
thread_id = saved_config["configurable"]["thread_id"]
87+
checkpoint_ns = saved_config["configurable"].get("checkpoint_ns", "")
88+
89+
# Now query the data - this should work with the fix
90+
query = f"@channel:{{test_channel}}"
91+
92+
# This should not raise an error now with our patch
93+
results = saver.checkpoint_blobs_index.search(query)
94+
95+
# Verify we can find the data
96+
assert len(results.docs) > 0
97+
98+
# Load one document and verify the version is a string
99+
doc = results.docs[0]
100+
data = saver._redis.json().get(doc.id)
101+
102+
# The key test: version should be a string even though we passed a numeric value
103+
assert isinstance(data["version"], str)

tests/test_numeric_version_issue.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""
2+
Test for issue #40 - Error when comparing numeric version with Tag type.
3+
"""
4+
5+
import pytest
6+
from langgraph.checkpoint.base import empty_checkpoint
7+
from redis import Redis
8+
from redisvl.query.filter import Tag
9+
10+
from langgraph.checkpoint.redis import RedisSaver
11+
12+
13+
@pytest.fixture(autouse=True)
14+
async def clear_test_redis(redis_url: str) -> None:
15+
"""Clear Redis before each test."""
16+
client = Redis.from_url(redis_url)
17+
try:
18+
client.flushall()
19+
finally:
20+
client.close()
21+
22+
23+
def test_numeric_version_issue(redis_url: str) -> None:
24+
"""
25+
Test reproduction for issue #40.
26+
27+
This test explicitly creates a scenario where a numeric version field
28+
is compared with a Tag type, which should cause a TypeError.
29+
"""
30+
# Create a Redis saver with default configuration
31+
saver = RedisSaver(redis_url)
32+
saver.setup()
33+
34+
try:
35+
# Here we'll directly test the specific problem from issue #40
36+
# In the real app, the version field is stored as a number in Redis
37+
# Then when the code in _dump_blobs tries to pass that numeric version
38+
# to the Tag filter, it causes a TypeError
39+
40+
# First create a fixed test with direct Tag usage to demonstrate the issue
41+
tag_filter = Tag("version")
42+
43+
with pytest.raises(TypeError) as excinfo:
44+
# This will trigger the error because we're comparing Tag with integer
45+
result = tag_filter == 1 # Integer instead of string
46+
47+
# Verify the specific error message related to Tag comparison
48+
assert "Right side argument passed to operator" in str(excinfo.value)
49+
assert "Tag must be of type" in str(excinfo.value)
50+
51+
# Another approach would be a direct test of our _dump_blobs method
52+
# by creating a fake numeric version and then trying to create a Tag query
53+
# based on it
54+
channel_name = "test_channel"
55+
numeric_version = 1 # This is the root issue - numeric version not string
56+
57+
# This mimics the code in _dump_blobs that would fail
58+
versions = {channel_name: numeric_version}
59+
60+
# We can't directly patch the method, but we can verify the same type issue
61+
# Here we simulate what happens when a numeric version is passed to Tag filter
62+
tag_filter = Tag("version")
63+
with pytest.raises(TypeError) as excinfo2:
64+
# This fails because we're comparing a Tag with a numeric value directly
65+
result = tag_filter == versions[channel_name] # Numeric version
66+
67+
# Check the error message
68+
assert "must be of type" in str(excinfo2.value)
69+
70+
finally:
71+
# Clean up
72+
if saver._owns_its_client:
73+
saver._redis.close()

0 commit comments

Comments
 (0)