Add novalues option to command HSCAN. (#12765)

Add a way to HSCAN a hash key, and get only the filed names.
Command syntax is now:
```
HSCAN key cursor [MATCH pattern] [COUNT count] [NOVALUES]
```
when `NOVALUES` is on, the command will only return keys in the hash.

---------

Co-authored-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
This commit is contained in:
Chen Tianjie 2024-01-31 02:32:58 +08:00 committed by GitHub
parent 24f6d08b3f
commit f469dd8ca6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 39 additions and 8 deletions

View File

@ -3567,6 +3567,7 @@ struct COMMAND_ARG HSCAN_Args[] = {
{MAKE_ARG("cursor",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("pattern",ARG_TYPE_PATTERN,-1,"MATCH",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
{MAKE_ARG("count",ARG_TYPE_INTEGER,-1,"COUNT",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
{MAKE_ARG("novalues",ARG_TYPE_PURE_TOKEN,-1,"NOVALUES",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
};
/********** HSET ********************/
@ -10713,7 +10714,7 @@ struct COMMAND_STRUCT redisCommandTable[] = {
{MAKE_CMD("hmget","Returns the values of all fields in a hash.","O(N) where N is the number of fields being requested.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HMGET_History,0,HMGET_Tips,0,hmgetCommand,-3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HMGET_Keyspecs,1,NULL,2),.args=HMGET_Args},
{MAKE_CMD("hmset","Sets the values of multiple fields.","O(N) where N is the number of fields being set.","2.0.0",CMD_DOC_DEPRECATED,"`HSET` with multiple field-value pairs","4.0.0","hash",COMMAND_GROUP_HASH,HMSET_History,0,HMSET_Tips,0,hsetCommand,-4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HMSET_Keyspecs,1,NULL,2),.args=HMSET_Args},
{MAKE_CMD("hrandfield","Returns one or more random fields from a hash.","O(N) where N is the number of fields returned","6.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HRANDFIELD_History,0,HRANDFIELD_Tips,1,hrandfieldCommand,-2,CMD_READONLY,ACL_CATEGORY_HASH,HRANDFIELD_Keyspecs,1,NULL,2),.args=HRANDFIELD_Args},
{MAKE_CMD("hscan","Iterates over fields and values of a hash.","O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.","2.8.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSCAN_History,0,HSCAN_Tips,1,hscanCommand,-3,CMD_READONLY,ACL_CATEGORY_HASH,HSCAN_Keyspecs,1,NULL,4),.args=HSCAN_Args},
{MAKE_CMD("hscan","Iterates over fields and values of a hash.","O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.","2.8.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSCAN_History,0,HSCAN_Tips,1,hscanCommand,-3,CMD_READONLY,ACL_CATEGORY_HASH,HSCAN_Keyspecs,1,NULL,5),.args=HSCAN_Args},
{MAKE_CMD("hset","Creates or modifies the value of a field in a hash.","O(1) for each field/value pair added, so O(N) to add N field/value pairs when the command is called with multiple field/value pairs.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSET_History,1,HSET_Tips,0,hsetCommand,-4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSET_Keyspecs,1,NULL,2),.args=HSET_Args},
{MAKE_CMD("hsetnx","Sets the value of a field in a hash only when the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSETNX_History,0,HSETNX_Tips,0,hsetnxCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSETNX_Keyspecs,1,NULL,3),.args=HSETNX_Args},
{MAKE_CMD("hstrlen","Returns the length of the value of a field.","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSTRLEN_History,0,HSTRLEN_Tips,0,hstrlenCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HSTRLEN_Keyspecs,1,NULL,2),.args=HSTRLEN_Args},

View File

@ -56,6 +56,12 @@
"name": "count",
"type": "integer",
"optional": true
},
{
"token": "NOVALUES",
"name": "novalues",
"type": "pure-token",
"optional": true
}
],
"reply_schema": {
@ -69,7 +75,7 @@
"type": "string"
},
{
"description": "list of key/value pairs from the hash where each even element is the key, and each odd element is the value",
"description": "list of key/value pairs from the hash where each even element is the key, and each odd element is the value, or when novalues option is on, a list of keys from the hash",
"type": "array",
"items": {
"type": "string"

View File

@ -1052,6 +1052,7 @@ typedef struct {
long long type; /* the particular type when scan the db */
sds pattern; /* pattern string, NULL means no pattern */
long sampled; /* cumulative number of keys sampled */
int no_values; /* set to 1 means to return keys only */
} scanData;
/* Helper function to compare key type in scan commands */
@ -1114,7 +1115,7 @@ void scanCallback(void *privdata, const dictEntry *de) {
}
listAddNodeTail(keys, key);
if (val) listAddNodeTail(keys, val);
if (val && !data->no_values) listAddNodeTail(keys, val);
}
/* Try to parse a SCAN cursor stored at object 'o':
@ -1187,7 +1188,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
sds pat = NULL;
sds typename = NULL;
long long type = LLONG_MAX;
int patlen = 0, use_pattern = 0;
int patlen = 0, use_pattern = 0, no_values = 0;
dict *ht;
/* Object must be NULL (to iterate keys names), or the type of the object
@ -1233,6 +1234,13 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
return; */
}
i+= 2;
} else if (!strcasecmp(c->argv[i]->ptr, "novalues")) {
if (!o || o->type != OBJ_HASH) {
addReplyError(c, "NOVALUES option can only be used in HSCAN");
return;
}
no_values = 1;
i++;
} else {
addReplyErrorObject(c,shared.syntaxerr);
return;
@ -1287,17 +1295,20 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
* it is possible to fetch more data in a type-dependent way;
* 3. data.type: the specified type scan in the db, LLONG_MAX means
* type matching is no needed;
* 4. data.pattern: the pattern string
* 4. data.pattern: the pattern string;
* 5. data.sampled: the maxiteration limit is there in case we're
* working on an empty dict, one with a lot of empty buckets, and
* for the buckets are not empty, we need to limit the spampled number
* to prevent a long hang time caused by filtering too many keys*/
* to prevent a long hang time caused by filtering too many keys;
* 6. data.no_values: to control whether values will be returned or
* only keys are returned. */
scanData data = {
.keys = keys,
.o = o,
.type = type,
.pattern = use_pattern ? pat : NULL,
.sampled = 0,
.no_values = no_values,
};
/* A pattern may restrict all matching keys to one cluster slot. */
@ -1352,8 +1363,10 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
/* add key object */
listAddNodeTail(keys, sdsnewlen(str, len));
/* add value object */
str = lpGet(p, &len, intbuf);
listAddNodeTail(keys, sdsnewlen(str, len));
if (!no_values) {
str = lpGet(p, &len, intbuf);
listAddNodeTail(keys, sdsnewlen(str, len));
}
p = lpNext(o->ptr, p);
}
cursor = 0;

View File

@ -272,6 +272,10 @@ proc test_scan {type} {
set keys2 [lsort -unique $keys2]
assert_equal $count [llength $keys2]
# Test NOVALUES
set res [r hscan hash 0 count 1000 novalues]
assert_equal [lsort $keys2] [lsort [lindex $res 1]]
}
}
@ -368,6 +372,13 @@ proc test_scan {type} {
lsort -unique [lindex $res 1]
} {1 10 foo foobar}
test "{$type} HSCAN with NOVALUES" {
r del mykey
r hmset mykey foo 1 fab 2 fiz 3 foobar 10 1 a 2 b 3 c 4 d
set res [r hscan mykey 0 NOVALUES]
lsort -unique [lindex $res 1]
} {1 2 3 4 fab fiz foo foobar}
test "{$type} ZSCAN with PATTERN" {
r del mykey
r zadd mykey 1 foo 2 fab 3 fiz 10 foobar