Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/812.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix hash method parameters: rename 'name' → 'key', 'key' → 'field' to align with Redis/Valkey terminology. Add version parameter to hlen() and hkeys(). Fix make_key() to only apply to hash key, not fields.
34 changes: 19 additions & 15 deletions django_redis/client/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -1131,76 +1131,80 @@ def touch(

def hset(
self,
name: str,
key: KeyT,
field: KeyT,
value: EncodableT,
version: Optional[int] = None,
client: Optional[Redis] = None,
) -> int:
"""
Set the value of hash name at key to value.
Set the value of hash key at field to value.
Returns the number of fields added to the hash.
"""
if client is None:
client = self.get_client(write=True)
nkey = self.make_key(key, version=version)
nvalue = self.encode(value)
return int(client.hset(name, nkey, nvalue))
return int(client.hset(nkey, field, nvalue))

def hdel(
self,
name: str,
key: KeyT,
field: KeyT,
version: Optional[int] = None,
client: Optional[Redis] = None,
) -> int:
"""
Remove keys from hash name.
Remove fields from hash key.
Returns the number of fields deleted from the hash.
"""
if client is None:
client = self.get_client(write=True)
nkey = self.make_key(key, version=version)
return int(client.hdel(name, nkey))
return int(client.hdel(nkey, field))

def hlen(
self,
name: str,
key: KeyT,
version: Optional[int] = None,
client: Optional[Redis] = None,
) -> int:
"""
Return the number of items in hash name.
Return the number of items in hash key.
"""
if client is None:
client = self.get_client(write=False)
return int(client.hlen(name))
nkey = self.make_key(key, version=version)
return int(client.hlen(nkey))

def hkeys(
self,
name: str,
key: KeyT,
version: Optional[int] = None,
client: Optional[Redis] = None,
) -> list[Any]:
"""
Return a list of keys in hash name.
Return a list of fields in hash key.
"""
if client is None:
client = self.get_client(write=False)
nkey = self.make_key(key, version=version)
try:
return [self.reverse_key(k.decode()) for k in client.hkeys(name)]
return [k.decode() for k in client.hkeys(nkey)]
except _main_exceptions as e:
raise ConnectionInterrupted(connection=client) from e

def hexists(
self,
name: str,
key: KeyT,
field: KeyT,
version: Optional[int] = None,
client: Optional[Redis] = None,
) -> bool:
"""
Return True if key exists in hash name, else False.
Return True if field exists in hash key, else False.
"""
if client is None:
client = self.get_client(write=False)
nkey = self.make_key(key, version=version)
return bool(client.hexists(name, nkey))
return bool(client.hexists(nkey, field))
48 changes: 0 additions & 48 deletions tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,54 +823,6 @@ def test_clear(self, cache: RedisCache):
value_from_cache_after_clear = cache.get("foo")
assert value_from_cache_after_clear is None

def test_hset(self, cache: RedisCache):
if isinstance(cache.client, ShardClient):
pytest.skip("ShardClient doesn't support get_client")
cache.hset("foo_hash1", "foo1", "bar1")
cache.hset("foo_hash1", "foo2", "bar2")
assert cache.hlen("foo_hash1") == 2
assert cache.hexists("foo_hash1", "foo1")
assert cache.hexists("foo_hash1", "foo2")

def test_hdel(self, cache: RedisCache):
if isinstance(cache.client, ShardClient):
pytest.skip("ShardClient doesn't support get_client")
cache.hset("foo_hash2", "foo1", "bar1")
cache.hset("foo_hash2", "foo2", "bar2")
assert cache.hlen("foo_hash2") == 2
deleted_count = cache.hdel("foo_hash2", "foo1")
assert deleted_count == 1
assert cache.hlen("foo_hash2") == 1
assert not cache.hexists("foo_hash2", "foo1")
assert cache.hexists("foo_hash2", "foo2")

def test_hlen(self, cache: RedisCache):
if isinstance(cache.client, ShardClient):
pytest.skip("ShardClient doesn't support get_client")
assert cache.hlen("foo_hash3") == 0
cache.hset("foo_hash3", "foo1", "bar1")
assert cache.hlen("foo_hash3") == 1
cache.hset("foo_hash3", "foo2", "bar2")
assert cache.hlen("foo_hash3") == 2

def test_hkeys(self, cache: RedisCache):
if isinstance(cache.client, ShardClient):
pytest.skip("ShardClient doesn't support get_client")
cache.hset("foo_hash4", "foo1", "bar1")
cache.hset("foo_hash4", "foo2", "bar2")
cache.hset("foo_hash4", "foo3", "bar3")
keys = cache.hkeys("foo_hash4")
assert len(keys) == 3
for i in range(len(keys)):
assert keys[i] == f"foo{i + 1}"

def test_hexists(self, cache: RedisCache):
if isinstance(cache.client, ShardClient):
pytest.skip("ShardClient doesn't support get_client")
cache.hset("foo_hash5", "foo1", "bar1")
assert cache.hexists("foo_hash5", "foo1")
assert not cache.hexists("foo_hash5", "foo")

def test_sadd(self, cache: RedisCache):
assert cache.sadd("foo", "bar") == 1
assert cache.smembers("foo") == {"bar"}
Expand Down
120 changes: 120 additions & 0 deletions tests/test_backend_hash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import pytest

from django_redis.cache import RedisCache
from django_redis.client import ShardClient


class TestHashOperations:
"""Tests for Redis hash operations."""

def test_hset(self, cache: RedisCache):
if isinstance(cache.client, ShardClient):
pytest.skip("ShardClient doesn't support get_client")
cache.hset("foo_hash1", "foo1", "bar1")
cache.hset("foo_hash1", "foo2", "bar2")
assert cache.hlen("foo_hash1") == 2
assert cache.hexists("foo_hash1", "foo1")
assert cache.hexists("foo_hash1", "foo2")

def test_hdel(self, cache: RedisCache):
if isinstance(cache.client, ShardClient):
pytest.skip("ShardClient doesn't support get_client")
cache.hset("foo_hash2", "foo1", "bar1")
cache.hset("foo_hash2", "foo2", "bar2")
assert cache.hlen("foo_hash2") == 2
deleted_count = cache.hdel("foo_hash2", "foo1")
assert deleted_count == 1
assert cache.hlen("foo_hash2") == 1
assert not cache.hexists("foo_hash2", "foo1")
assert cache.hexists("foo_hash2", "foo2")

def test_hlen(self, cache: RedisCache):
if isinstance(cache.client, ShardClient):
pytest.skip("ShardClient doesn't support get_client")
assert cache.hlen("foo_hash3") == 0
cache.hset("foo_hash3", "foo1", "bar1")
assert cache.hlen("foo_hash3") == 1
cache.hset("foo_hash3", "foo2", "bar2")
assert cache.hlen("foo_hash3") == 2

def test_hkeys(self, cache: RedisCache):
if isinstance(cache.client, ShardClient):
pytest.skip("ShardClient doesn't support get_client")
cache.hset("foo_hash4", "foo1", "bar1")
cache.hset("foo_hash4", "foo2", "bar2")
cache.hset("foo_hash4", "foo3", "bar3")
keys = cache.hkeys("foo_hash4")
assert len(keys) == 3
for i in range(len(keys)):
assert keys[i] == f"foo{i + 1}"

def test_hexists(self, cache: RedisCache):
if isinstance(cache.client, ShardClient):
pytest.skip("ShardClient doesn't support get_client")
cache.hset("foo_hash5", "foo1", "bar1")
assert cache.hexists("foo_hash5", "foo1")
assert not cache.hexists("foo_hash5", "foo")

def test_hash_version_support(self, cache: RedisCache):
"""Test that version parameter works correctly for hash methods."""
if isinstance(cache.client, ShardClient):
pytest.skip("ShardClient doesn't support get_client")

# Set values with different versions
cache.hset("my_hash", "field1", "value1", version=1)
cache.hset("my_hash", "field2", "value2", version=1)
cache.hset("my_hash", "field1", "different_value", version=2)

# Verify both versions exist independently
assert cache.hexists("my_hash", "field1", version=1)
assert cache.hexists("my_hash", "field2", version=1)
assert cache.hexists("my_hash", "field1", version=2)
assert not cache.hexists("my_hash", "field2", version=2)

# Verify hlen works with versions
assert cache.hlen("my_hash", version=1) == 2
assert cache.hlen("my_hash", version=2) == 1

# Verify hkeys works with versions
keys_v1 = cache.hkeys("my_hash", version=1)
assert len(keys_v1) == 2
assert "field1" in keys_v1
assert "field2" in keys_v1

keys_v2 = cache.hkeys("my_hash", version=2)
assert len(keys_v2) == 1
assert "field1" in keys_v2

# Verify hdel works with versions
cache.hdel("my_hash", "field1", version=1)
assert not cache.hexists("my_hash", "field1", version=1)
assert cache.hexists("my_hash", "field1", version=2) # v2 should still exist

def test_hash_key_structure_in_redis(self, cache: RedisCache):
"""Test that hash keys are prefixed but fields are not."""
if isinstance(cache.client, ShardClient):
pytest.skip("ShardClient doesn't support get_client")

# Get raw Redis client
client = cache.client.get_client(write=False)

# Set some hash data
cache.hset("user:1000", "email", "alice@example.com", version=2)
cache.hset("user:1000", "name", "Alice", version=2)

# Get the actual Redis key that was created
expected_key = cache.client.make_key("user:1000", version=2)

# Verify the hash exists in Redis with the prefixed key
assert client.exists(expected_key)
assert client.type(expected_key) == b"hash"

# Verify fields are stored WITHOUT prefix
actual_fields = client.hkeys(expected_key)
# Fields should be plain "email" and "name", not prefixed
assert b"email" in actual_fields
assert b"name" in actual_fields

# Verify field values are correct
assert client.hget(expected_key, b"email") is not None
assert client.hget(expected_key, b"name") is not None
Loading