Skip to content

Commit

Permalink
Support NOVALUES parameter for HSCAN (#3157)
Browse files Browse the repository at this point in the history
* Support NOVALUES parameter for HSCAN

Issue #3153

The NOVALUES parameter instructs HSCAN to only return the hash keys,
without values.

Co-authored-by: Gabriel Erzse <gabriel.erzse@redis.com>
  • Loading branch information
gerzse and gerzse committed Jul 11, 2024
1 parent 2820e87 commit 6b7c292
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 7 deletions.
7 changes: 6 additions & 1 deletion redis/_parsers/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,12 @@ def parse_scan(response, **options):

def parse_hscan(response, **options):
cursor, r = response
return int(cursor), r and pairs_to_dict(r) or {}
no_values = options.get("no_values", False)
if no_values:
payload = r or []
else:
payload = r and pairs_to_dict(r) or {}
return int(cursor), payload


def parse_zscan(response, **options):
Expand Down
32 changes: 26 additions & 6 deletions redis/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3106,6 +3106,7 @@ def hscan(
cursor: int = 0,
match: Union[PatternT, None] = None,
count: Union[int, None] = None,
no_values: Union[bool, None] = None,
) -> ResponseT:
"""
Incrementally return key/value slices in a hash. Also return a cursor
Expand All @@ -3115,20 +3116,25 @@ def hscan(
``count`` allows for hint the minimum number of returns
``no_values`` indicates to return only the keys, without values.
For more information see https://redis.io/commands/hscan
"""
pieces: list[EncodableT] = [name, cursor]
if match is not None:
pieces.extend([b"MATCH", match])
if count is not None:
pieces.extend([b"COUNT", count])
return self.execute_command("HSCAN", *pieces)
if no_values is not None:
pieces.extend([b"NOVALUES"])
return self.execute_command("HSCAN", *pieces, no_values=no_values)

def hscan_iter(
self,
name: str,
match: Union[PatternT, None] = None,
count: Union[int, None] = None,
no_values: Union[bool, None] = None,
) -> Iterator:
"""
Make an iterator using the HSCAN command so that the client doesn't
Expand All @@ -3137,11 +3143,18 @@ def hscan_iter(
``match`` allows for filtering the keys by pattern
``count`` allows for hint the minimum number of returns
``no_values`` indicates to return only the keys, without values
"""
cursor = "0"
while cursor != 0:
cursor, data = self.hscan(name, cursor=cursor, match=match, count=count)
yield from data.items()
cursor, data = self.hscan(
name, cursor=cursor, match=match, count=count, no_values=no_values
)
if no_values:
yield from data
else:
yield from data.items()

def zscan(
self,
Expand Down Expand Up @@ -3257,6 +3270,7 @@ async def hscan_iter(
name: str,
match: Union[PatternT, None] = None,
count: Union[int, None] = None,
no_values: Union[bool, None] = None,
) -> AsyncIterator:
"""
Make an iterator using the HSCAN command so that the client doesn't
Expand All @@ -3265,14 +3279,20 @@ async def hscan_iter(
``match`` allows for filtering the keys by pattern
``count`` allows for hint the minimum number of returns
``no_values`` indicates to return only the keys, without values
"""
cursor = "0"
while cursor != 0:
cursor, data = await self.hscan(
name, cursor=cursor, match=match, count=count
name, cursor=cursor, match=match, count=count, no_values=no_values
)
for it in data.items():
yield it
if no_values:
for it in data:
yield it
else:
for it in data.items():
yield it

async def zscan_iter(
self,
Expand Down
27 changes: 27 additions & 0 deletions tests/test_asyncio/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,19 @@ async def test_hscan(self, r: redis.Redis):
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
_, dic = await r.hscan("a", match="a")
assert dic == {b"a": b"1"}
_, dic = await r.hscan("a_notset", match="a")
assert dic == {}

@skip_if_server_version_lt("7.4.0")
async def test_hscan_novalues(self, r: redis.Redis):
await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
cursor, keys = await r.hscan("a", no_values=True)
assert cursor == 0
assert sorted(keys) == [b"a", b"b", b"c"]
_, keys = await r.hscan("a", match="a", no_values=True)
assert keys == [b"a"]
_, keys = await r.hscan("a_notset", match="a", no_values=True)
assert keys == []

@skip_if_server_version_lt("2.8.0")
async def test_hscan_iter(self, r: redis.Redis):
Expand All @@ -1356,6 +1369,20 @@ async def test_hscan_iter(self, r: redis.Redis):
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
dic = {k: v async for k, v in r.hscan_iter("a", match="a")}
assert dic == {b"a": b"1"}
dic = {k: v async for k, v in r.hscan_iter("a_notset", match="a")}
assert dic == {}

@skip_if_server_version_lt("7.4.0")
async def test_hscan_iter_novalues(self, r: redis.Redis):
await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
keys = list([k async for k in r.hscan_iter("a", no_values=True)])
assert sorted(keys) == [b"a", b"b", b"c"]
keys = list([k async for k in r.hscan_iter("a", match="a", no_values=True)])
assert keys == [b"a"]
keys = list(
[k async for k in r.hscan_iter("a", match="a_notset", no_values=True)]
)
assert keys == []

@skip_if_server_version_lt("2.8.0")
async def test_zscan(self, r: redis.Redis):
Expand Down
25 changes: 25 additions & 0 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2171,6 +2171,19 @@ def test_hscan(self, r):
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
_, dic = r.hscan("a", match="a")
assert dic == {b"a": b"1"}
_, dic = r.hscan("a_notset")
assert dic == {}

@skip_if_server_version_lt("7.4.0")
def test_hscan_novalues(self, r):
r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
cursor, keys = r.hscan("a", no_values=True)
assert cursor == 0
assert sorted(keys) == [b"a", b"b", b"c"]
_, keys = r.hscan("a", match="a", no_values=True)
assert keys == [b"a"]
_, keys = r.hscan("a_notset", no_values=True)
assert keys == []

@skip_if_server_version_lt("2.8.0")
def test_hscan_iter(self, r):
Expand All @@ -2179,6 +2192,18 @@ def test_hscan_iter(self, r):
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
dic = dict(r.hscan_iter("a", match="a"))
assert dic == {b"a": b"1"}
dic = dict(r.hscan_iter("a_notset"))
assert dic == {}

@skip_if_server_version_lt("7.4.0")
def test_hscan_iter_novalues(self, r):
r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
keys = list(r.hscan_iter("a", no_values=True))
assert keys == [b"a", b"b", b"c"]
keys = list(r.hscan_iter("a", match="a", no_values=True))
assert keys == [b"a"]
keys = list(r.hscan_iter("a_notset", no_values=True))
assert keys == []

@skip_if_server_version_lt("2.8.0")
def test_zscan(self, r):
Expand Down

0 comments on commit 6b7c292

Please sign in to comment.