From 5d4c7b7c0ceee5a7fc402d7df2f5eac6f4f64237 Mon Sep 17 00:00:00 2001 From: Chen Tianjie Date: Tue, 7 May 2024 14:39:28 +0800 Subject: [PATCH] Add noscores option to command ZSCAN. (#324) 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 --- src/commands.def | 7 +++++-- src/commands/zscan.json | 14 +++++++++++++- src/db.c | 33 ++++++++++++++++++++++----------- tests/unit/scan.tcl | 11 +++++++++++ 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/commands.def b/src/commands.def index 6afdf34b19..bc5a1261f2 100644 --- a/src/commands.def +++ b/src/commands.def @@ -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 @@ -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 ********************/ @@ -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}, diff --git a/src/commands/zscan.json b/src/commands/zscan.json index edadb8561b..7948e393a5 100644 --- a/src/commands/zscan.json +++ b/src/commands/zscan.json @@ -6,6 +6,12 @@ "since": "2.8.0", "arity": -3, "function": "zscanCommand", + "history": [ + [ + "8.0.0", + "Added noscores option." + ] + ], "command_flags": [ "READONLY" ], @@ -56,6 +62,12 @@ "name": "count", "type": "integer", "optional": true + }, + { + "token": "NOSCORES", + "name": "noscores", + "type": "pure-token", + "optional": true } ], "reply_schema": { @@ -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" diff --git a/src/db.c b/src/db.c index 17e74f5881..d49d09db22 100644 --- a/src/db.c +++ b/src/db.c @@ -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 */ @@ -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': @@ -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 @@ -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); @@ -1101,7 +1112,7 @@ 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, @@ -1109,7 +1120,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) { .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. */ @@ -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)); } diff --git a/tests/unit/scan.tcl b/tests/unit/scan.tcl index 49939288fb..2317a03e6a 100644 --- a/tests/unit/scan.tcl +++ b/tests/unit/scan.tcl @@ -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]] } } @@ -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} {