Skip to content

Commit

Permalink
Add noscores option to command ZSCAN. (valkey-io#324)
Browse files Browse the repository at this point in the history
Command syntax is now:
```
ZSCAN key cursor [MATCH pattern] [COUNT count] [NOSCORES]
```
Return format:
```
127.0.0.1:6379> zadd z 1 a 2 b 3 c
(integer) 3
127.0.0.1:6379> zscan z 0
1) "0"
2) 1) "a"
   2) "1"
   3) "b"
   4) "2"
   5) "c"
   6) "3"
127.0.0.1:6379> zscan z 0 noscores
1) "0"
2) 1) "a"
   2) "b"
   3) "c"
```
when NOSCORES is on, the command will only return members in the zset,
without scores.

For client side parsing the command return, I believe it is fine as long
as the command is backwards compatible. The return structure are still
lists, what has changed is the content. And clients can tell the
difference by the subcommand they use.

Since `novalues` option of `HSCAN` is already accepted
(redis/redis#12765), I think similar thing can be done to `ZSCAN`.

---------

Signed-off-by: Chen Tianjie <TJ_Chen@outlook.com>
  • Loading branch information
CharlesChen888 authored and liziang.arthur committed May 7, 2024
1 parent 6e7af94 commit 5d4c7b7
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 14 deletions.
7 changes: 5 additions & 2 deletions src/commands.def
Expand Up @@ -9060,7 +9060,9 @@ struct COMMAND_ARG ZREVRANK_Args[] = {

#ifndef SKIP_CMD_HISTORY_TABLE
/* ZSCAN history */
#define ZSCAN_History NULL
commandHistory ZSCAN_History[] = {
{"8.0.0","Added noscores option."},
};
#endif

#ifndef SKIP_CMD_TIPS_TABLE
Expand All @@ -9083,6 +9085,7 @@ struct COMMAND_ARG ZSCAN_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("noscores",ARG_TYPE_PURE_TOKEN,-1,"NOSCORES",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
};

/********** ZSCORE ********************/
Expand Down Expand Up @@ -10867,7 +10870,7 @@ struct COMMAND_STRUCT serverCommandTable[] = {
{MAKE_CMD("zrevrangebylex","Returns members in a sorted set within a lexicographical range in reverse order.","O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).","2.8.9",CMD_DOC_DEPRECATED,"`ZRANGE` with the `REV` and `BYLEX` arguments","6.2.0","sorted_set",COMMAND_GROUP_SORTED_SET,ZREVRANGEBYLEX_History,0,ZREVRANGEBYLEX_Tips,0,zrevrangebylexCommand,-4,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZREVRANGEBYLEX_Keyspecs,1,NULL,4),.args=ZREVRANGEBYLEX_Args},
{MAKE_CMD("zrevrangebyscore","Returns members in a sorted set within a range of scores in reverse order.","O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).","2.2.0",CMD_DOC_DEPRECATED,"`ZRANGE` with the `REV` and `BYSCORE` arguments","6.2.0","sorted_set",COMMAND_GROUP_SORTED_SET,ZREVRANGEBYSCORE_History,1,ZREVRANGEBYSCORE_Tips,0,zrevrangebyscoreCommand,-4,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZREVRANGEBYSCORE_Keyspecs,1,NULL,5),.args=ZREVRANGEBYSCORE_Args},
{MAKE_CMD("zrevrank","Returns the index of a member in a sorted set ordered by descending scores.","O(log(N))","2.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZREVRANK_History,1,ZREVRANK_Tips,0,zrevrankCommand,-3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,ZREVRANK_Keyspecs,1,NULL,3),.args=ZREVRANK_Args},
{MAKE_CMD("zscan","Iterates over members and scores of a sorted set.","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,"sorted_set",COMMAND_GROUP_SORTED_SET,ZSCAN_History,0,ZSCAN_Tips,1,zscanCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZSCAN_Keyspecs,1,NULL,4),.args=ZSCAN_Args},
{MAKE_CMD("zscan","Iterates over members and scores of a sorted set.","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,"sorted_set",COMMAND_GROUP_SORTED_SET,ZSCAN_History,1,ZSCAN_Tips,1,zscanCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZSCAN_Keyspecs,1,NULL,5),.args=ZSCAN_Args},
{MAKE_CMD("zscore","Returns the score of a member in a sorted set.","O(1)","1.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZSCORE_History,0,ZSCORE_Tips,0,zscoreCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,ZSCORE_Keyspecs,1,NULL,2),.args=ZSCORE_Args},
{MAKE_CMD("zunion","Returns the union of multiple sorted sets.","O(N)+O(M*log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZUNION_History,0,ZUNION_Tips,0,zunionCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZUNION_Keyspecs,1,zunionInterDiffGetKeys,5),.args=ZUNION_Args},
{MAKE_CMD("zunionstore","Stores the union of multiple sorted sets in a key.","O(N)+O(M log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set.","2.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZUNIONSTORE_History,0,ZUNIONSTORE_Tips,0,zunionstoreCommand,-4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SORTEDSET,ZUNIONSTORE_Keyspecs,2,zunionInterDiffStoreGetKeys,5),.args=ZUNIONSTORE_Args},
Expand Down
14 changes: 13 additions & 1 deletion src/commands/zscan.json
Expand Up @@ -6,6 +6,12 @@
"since": "2.8.0",
"arity": -3,
"function": "zscanCommand",
"history": [
[
"8.0.0",
"Added noscores option."
]
],
"command_flags": [
"READONLY"
],
Expand Down Expand Up @@ -56,6 +62,12 @@
"name": "count",
"type": "integer",
"optional": true
},
{
"token": "NOSCORES",
"name": "noscores",
"type": "pure-token",
"optional": true
}
],
"reply_schema": {
Expand All @@ -69,7 +81,7 @@
"type": "string"
},
{
"description": "List of elements of the sorted set, where each even element is the member, and each odd value is its associated score.",
"description": "List of elements of the sorted set, where each even element is the member, and each odd value is its associated score, or when noscores option is on, a list of members from the sorted set.",
"type": "array",
"items": {
"type": "string"
Expand Down
33 changes: 22 additions & 11 deletions src/db.c
Expand Up @@ -853,7 +853,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 */
int only_keys; /* set to 1 means to return keys only */
} scanData;

/* Helper function to compare key type in scan commands */
Expand Down Expand Up @@ -905,18 +905,22 @@ void scanCallback(void *privdata, const dictEntry *de) {
key = keysds;
} else if (o->type == OBJ_HASH) {
key = keysds;
val = dictGetVal(de);
if (!data->only_keys) {
val = dictGetVal(de);
}
} else if (o->type == OBJ_ZSET) {
char buf[MAX_LONG_DOUBLE_CHARS];
int len = ld2string(buf, sizeof(buf), *(double *)dictGetVal(de), LD_STR_AUTO);
key = sdsdup(keysds);
val = sdsnewlen(buf, len);
if (!data->only_keys) {
char buf[MAX_LONG_DOUBLE_CHARS];
int len = ld2string(buf, sizeof(buf), *(double *)dictGetVal(de), LD_STR_AUTO);
val = sdsnewlen(buf, len);
}
} else {
serverPanic("Type not handled in SCAN callback.");
}

listAddNodeTail(keys, key);
if (val && !data->no_values) listAddNodeTail(keys, val);
if (val) listAddNodeTail(keys, val);
}

/* Try to parse a SCAN cursor stored at object 'o':
Expand Down Expand Up @@ -989,7 +993,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, no_values = 0;
int patlen = 0, use_pattern = 0, only_keys = 0;
dict *ht;

/* Object must be NULL (to iterate keys names), or the type of the object
Expand Down Expand Up @@ -1040,7 +1044,14 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
addReplyError(c, "NOVALUES option can only be used in HSCAN");
return;
}
no_values = 1;
only_keys = 1;
i++;
} else if (!strcasecmp(c->argv[i]->ptr, "noscores")) {
if (!o || o->type != OBJ_ZSET) {
addReplyError(c, "NOSCORES option can only be used in ZSCAN");
return;
}
only_keys = 1;
i++;
} else {
addReplyErrorObject(c,shared.syntaxerr);
Expand Down Expand Up @@ -1101,15 +1112,15 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
* 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;
* 6. data.no_values: to control whether values will be returned or
* 6. data.only_keys: 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,
.only_keys = only_keys,
};

/* A pattern may restrict all matching keys to one cluster slot. */
Expand Down Expand Up @@ -1164,7 +1175,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
/* add key object */
listAddNodeTail(keys, sdsnewlen(str, len));
/* add value object */
if (!no_values) {
if (!only_keys) {
str = lpGet(p, &len, intbuf);
listAddNodeTail(keys, sdsnewlen(str, len));
}
Expand Down
11 changes: 11 additions & 0 deletions tests/unit/scan.tcl
Expand Up @@ -316,6 +316,10 @@ proc test_scan {type} {

set keys2 [lsort -unique $keys2]
assert_equal $count [llength $keys2]

# Test NOSCORES
set res [r zscan zset 0 count 1000 noscores]
assert_equal [lsort $keys2] [lsort [lindex $res 1]]
}
}

Expand Down Expand Up @@ -386,6 +390,13 @@ proc test_scan {type} {
lsort -unique [lindex $res 1]
}

test "{$type} ZSCAN with NOSCORES" {
r del mykey
r zadd mykey 1 foo 2 fab 3 fiz 10 foobar
set res [r zscan mykey 0 NOSCORES]
lsort -unique [lindex $res 1]
} {fab fiz foo foobar}

test "{$type} ZSCAN scores: regression test for issue #2175" {
r del mykey
for {set j 0} {$j < 500} {incr j} {
Expand Down

0 comments on commit 5d4c7b7

Please sign in to comment.