Skip to content

Commit cd92428

Browse files
authored
Support NOVALUES parameter for HSCAN (#3157)
* 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>
1 parent a2bcea2 commit cd92428

File tree

4 files changed

+84
-7
lines changed

4 files changed

+84
-7
lines changed

redis/_parsers/helpers.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,12 @@ def parse_scan(response, **options):
354354

355355
def parse_hscan(response, **options):
356356
cursor, r = response
357-
return int(cursor), r and pairs_to_dict(r) or {}
357+
no_values = options.get("no_values", False)
358+
if no_values:
359+
payload = r or []
360+
else:
361+
payload = r and pairs_to_dict(r) or {}
362+
return int(cursor), payload
358363

359364

360365
def parse_zscan(response, **options):

redis/commands/core.py

+26-6
Original file line numberDiff line numberDiff line change
@@ -3106,6 +3106,7 @@ def hscan(
31063106
cursor: int = 0,
31073107
match: Union[PatternT, None] = None,
31083108
count: Union[int, None] = None,
3109+
no_values: Union[bool, None] = None,
31093110
) -> ResponseT:
31103111
"""
31113112
Incrementally return key/value slices in a hash. Also return a cursor
@@ -3115,20 +3116,25 @@ def hscan(
31153116
31163117
``count`` allows for hint the minimum number of returns
31173118
3119+
``no_values`` indicates to return only the keys, without values.
3120+
31183121
For more information see https://redis.io/commands/hscan
31193122
"""
31203123
pieces: list[EncodableT] = [name, cursor]
31213124
if match is not None:
31223125
pieces.extend([b"MATCH", match])
31233126
if count is not None:
31243127
pieces.extend([b"COUNT", count])
3125-
return self.execute_command("HSCAN", *pieces)
3128+
if no_values is not None:
3129+
pieces.extend([b"NOVALUES"])
3130+
return self.execute_command("HSCAN", *pieces, no_values=no_values)
31263131

31273132
def hscan_iter(
31283133
self,
31293134
name: str,
31303135
match: Union[PatternT, None] = None,
31313136
count: Union[int, None] = None,
3137+
no_values: Union[bool, None] = None,
31323138
) -> Iterator:
31333139
"""
31343140
Make an iterator using the HSCAN command so that the client doesn't
@@ -3137,11 +3143,18 @@ def hscan_iter(
31373143
``match`` allows for filtering the keys by pattern
31383144
31393145
``count`` allows for hint the minimum number of returns
3146+
3147+
``no_values`` indicates to return only the keys, without values
31403148
"""
31413149
cursor = "0"
31423150
while cursor != 0:
3143-
cursor, data = self.hscan(name, cursor=cursor, match=match, count=count)
3144-
yield from data.items()
3151+
cursor, data = self.hscan(
3152+
name, cursor=cursor, match=match, count=count, no_values=no_values
3153+
)
3154+
if no_values:
3155+
yield from data
3156+
else:
3157+
yield from data.items()
31453158

31463159
def zscan(
31473160
self,
@@ -3257,6 +3270,7 @@ async def hscan_iter(
32573270
name: str,
32583271
match: Union[PatternT, None] = None,
32593272
count: Union[int, None] = None,
3273+
no_values: Union[bool, None] = None,
32603274
) -> AsyncIterator:
32613275
"""
32623276
Make an iterator using the HSCAN command so that the client doesn't
@@ -3265,14 +3279,20 @@ async def hscan_iter(
32653279
``match`` allows for filtering the keys by pattern
32663280
32673281
``count`` allows for hint the minimum number of returns
3282+
3283+
``no_values`` indicates to return only the keys, without values
32683284
"""
32693285
cursor = "0"
32703286
while cursor != 0:
32713287
cursor, data = await self.hscan(
3272-
name, cursor=cursor, match=match, count=count
3288+
name, cursor=cursor, match=match, count=count, no_values=no_values
32733289
)
3274-
for it in data.items():
3275-
yield it
3290+
if no_values:
3291+
for it in data:
3292+
yield it
3293+
else:
3294+
for it in data.items():
3295+
yield it
32763296

32773297
async def zscan_iter(
32783298
self,

tests/test_asyncio/test_commands.py

+27
Original file line numberDiff line numberDiff line change
@@ -1349,6 +1349,19 @@ async def test_hscan(self, r: redis.Redis):
13491349
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
13501350
_, dic = await r.hscan("a", match="a")
13511351
assert dic == {b"a": b"1"}
1352+
_, dic = await r.hscan("a_notset", match="a")
1353+
assert dic == {}
1354+
1355+
@skip_if_server_version_lt("7.4.0")
1356+
async def test_hscan_novalues(self, r: redis.Redis):
1357+
await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
1358+
cursor, keys = await r.hscan("a", no_values=True)
1359+
assert cursor == 0
1360+
assert sorted(keys) == [b"a", b"b", b"c"]
1361+
_, keys = await r.hscan("a", match="a", no_values=True)
1362+
assert keys == [b"a"]
1363+
_, keys = await r.hscan("a_notset", match="a", no_values=True)
1364+
assert keys == []
13521365

13531366
@skip_if_server_version_lt("2.8.0")
13541367
async def test_hscan_iter(self, r: redis.Redis):
@@ -1357,6 +1370,20 @@ async def test_hscan_iter(self, r: redis.Redis):
13571370
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
13581371
dic = {k: v async for k, v in r.hscan_iter("a", match="a")}
13591372
assert dic == {b"a": b"1"}
1373+
dic = {k: v async for k, v in r.hscan_iter("a_notset", match="a")}
1374+
assert dic == {}
1375+
1376+
@skip_if_server_version_lt("7.4.0")
1377+
async def test_hscan_iter_novalues(self, r: redis.Redis):
1378+
await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
1379+
keys = list([k async for k in r.hscan_iter("a", no_values=True)])
1380+
assert sorted(keys) == [b"a", b"b", b"c"]
1381+
keys = list([k async for k in r.hscan_iter("a", match="a", no_values=True)])
1382+
assert keys == [b"a"]
1383+
keys = list(
1384+
[k async for k in r.hscan_iter("a", match="a_notset", no_values=True)]
1385+
)
1386+
assert keys == []
13601387

13611388
@skip_if_server_version_lt("2.8.0")
13621389
async def test_zscan(self, r: redis.Redis):

tests/test_commands.py

+25
Original file line numberDiff line numberDiff line change
@@ -2171,6 +2171,19 @@ def test_hscan(self, r):
21712171
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
21722172
_, dic = r.hscan("a", match="a")
21732173
assert dic == {b"a": b"1"}
2174+
_, dic = r.hscan("a_notset")
2175+
assert dic == {}
2176+
2177+
@skip_if_server_version_lt("7.4.0")
2178+
def test_hscan_novalues(self, r):
2179+
r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
2180+
cursor, keys = r.hscan("a", no_values=True)
2181+
assert cursor == 0
2182+
assert sorted(keys) == [b"a", b"b", b"c"]
2183+
_, keys = r.hscan("a", match="a", no_values=True)
2184+
assert keys == [b"a"]
2185+
_, keys = r.hscan("a_notset", no_values=True)
2186+
assert keys == []
21742187

21752188
@skip_if_server_version_lt("2.8.0")
21762189
def test_hscan_iter(self, r):
@@ -2179,6 +2192,18 @@ def test_hscan_iter(self, r):
21792192
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
21802193
dic = dict(r.hscan_iter("a", match="a"))
21812194
assert dic == {b"a": b"1"}
2195+
dic = dict(r.hscan_iter("a_notset"))
2196+
assert dic == {}
2197+
2198+
@skip_if_server_version_lt("7.4.0")
2199+
def test_hscan_iter_novalues(self, r):
2200+
r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
2201+
keys = list(r.hscan_iter("a", no_values=True))
2202+
assert keys == [b"a", b"b", b"c"]
2203+
keys = list(r.hscan_iter("a", match="a", no_values=True))
2204+
assert keys == [b"a"]
2205+
keys = list(r.hscan_iter("a_notset", no_values=True))
2206+
assert keys == []
21822207

21832208
@skip_if_server_version_lt("2.8.0")
21842209
def test_zscan(self, r):

0 commit comments

Comments
 (0)