diff --git a/src/t_zset.c b/src/t_zset.c index ff61afdd3..3b3857743 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -389,17 +389,31 @@ static void zslDelete(zskiplist *zsl, zskiplistNode *node) { zslFreeNode(zsl, node); } +/* Returns true if node would still be strictly between its level-0 neighbors + * after changing its score. The sorted-set order is (score, element), so equal + * scores still need the lexicographic tie-breaker. */ +static int zslNodeCanKeepPosition(zskiplistNode *node, double newscore) { + sds ele = zslGetNodeElement(node); + zskiplistNode *prev = node->backward; + zskiplistNode *next = node->level[0].forward; + + if (prev != NULL && zslCompareWithNode(newscore, ele, prev) <= 0) + return 0; + if (next != NULL && zslCompareWithNode(newscore, ele, next) >= 0) + return 0; + return 1; +} + /* Update the score of an element inside the sorted set skiplist. - * If the new score would keep the node in its current position, updates in-place and returns NULL. - * Otherwise, unlinks the node, updates score, reinserts at correct position, and returns node. - * Anyway, the node pointer stays the same (no dict update needed). */ + * If the new score would keep the node in its current position, update it + * in-place. Otherwise, unlink the node, update the score, and reinsert it at + * the correct position. The node pointer stays the same (no dict update + * needed). */ static void zslUpdateScore(zskiplist *zsl, zskiplistNode *node, double newscore) { /* Fast path: if the node, after the score update, would be still exactly * at the same position, we can just update the score without * actually removing and re-inserting the element in the skiplist. */ - if ((node->backward == NULL || node->backward->score < newscore) && - (node->level[0].forward == NULL || node->level[0].forward->score > newscore)) - { + if (zslNodeCanKeepPosition(node, newscore)) { node->score = newscore; return; } diff --git a/tests/unit/type/zset.tcl b/tests/unit/type/zset.tcl index e840b2a16..1ed965ebc 100644 --- a/tests/unit/type/zset.tcl +++ b/tests/unit/type/zset.tcl @@ -127,6 +127,30 @@ start_server {tags {"zset"}} { assert_equal {y x z} [r zrange ztmp 0 -1] } + test "ZSET score update with equal-score neighbor - $encoding" { + r del ztmp + r zadd ztmp 1 a 2 b 3 c + r zadd ztmp 3 b + assert_equal {a b c} [r zrange ztmp 0 -1] + assert_equal {a 1 b 3 c 3} [r zrange ztmp 0 -1 withscores] + + r zadd ztmp 1 b + assert_equal {a b c} [r zrange ztmp 0 -1] + assert_equal {a 1 b 1 c 3} [r zrange ztmp 0 -1 withscores] + + r del ztmp + r zadd ztmp 1 a 2 c 3 b + r zadd ztmp 3 c + assert_equal {a b c} [r zrange ztmp 0 -1] + assert_equal {a 1 b 3 c 3} [r zrange ztmp 0 -1 withscores] + + r del ztmp + r zadd ztmp 1 b 2 a 3 c + r zadd ztmp 1 a + assert_equal {a b c} [r zrange ztmp 0 -1] + assert_equal {a 1 b 1 c 3} [r zrange ztmp 0 -1 withscores] + } + test "ZSET element can't be set to NaN with ZADD - $encoding" { assert_error "*not*float*" {r zadd myzset nan abc} }