diff --git a/cmake/Modules/SourceFiles.cmake b/cmake/Modules/SourceFiles.cmake index 866c4f9169f..84cd1c109b7 100644 --- a/cmake/Modules/SourceFiles.cmake +++ b/cmake/Modules/SourceFiles.cmake @@ -34,6 +34,8 @@ set(VALKEY_SERVER_SRCS ${CMAKE_SOURCE_DIR}/src/t_set.c ${CMAKE_SOURCE_DIR}/src/t_zset.c ${CMAKE_SOURCE_DIR}/src/skiplist.c + ${CMAKE_SOURCE_DIR}/src/skiplist_ordered_index.c + ${CMAKE_SOURCE_DIR}/src/ordered_index.c ${CMAKE_SOURCE_DIR}/src/t_hash.c ${CMAKE_SOURCE_DIR}/src/config.c ${CMAKE_SOURCE_DIR}/src/aof.c diff --git a/src/Makefile b/src/Makefile index 82b06d7bdb1..825b2d48c9a 100644 --- a/src/Makefile +++ b/src/Makefile @@ -555,6 +555,8 @@ ENGINE_SERVER_OBJ = \ sha1.o \ sha256.o \ siphash.o \ + skiplist_ordered_index.o \ + ordered_index.o \ socket.o \ sort.o \ sparkline.o \ diff --git a/src/aof.c b/src/aof.c index 395acb9e2c9..de1af6ffa89 100644 --- a/src/aof.c +++ b/src/aof.c @@ -28,7 +28,7 @@ */ #include "server.h" -#include "skiplist.h" +#include "ordered_index.h" #include "bio.h" #include "rio.h" #include "functions.h" @@ -2027,7 +2027,7 @@ int rewriteSortedSetObject(rio *r, robj *key, robj *o) { hashtableInitIterator(&iter, zs->ht, 0); void *next; while (hashtableNext(&iter, &next)) { - zskiplistNode *node = next; + OrderedIndexItem *node = next; if (count == 0) { int cmd_items = (items > AOF_REWRITE_ITEMS_PER_CMD) ? AOF_REWRITE_ITEMS_PER_CMD : items; @@ -2037,8 +2037,10 @@ int rewriteSortedSetObject(rio *r, robj *key, robj *o) { return 0; } } - sds ele = zslGetNodeElement(node); - if (!rioWriteBulkDouble(r, node->score) || !rioWriteBulkString(r, ele, sdslen(ele))) { + const char *ele; + size_t ele_len; + orderedIndexGetElementRaw(node, &ele, &ele_len); + if (!rioWriteBulkDouble(r, orderedIndexGetScore(node)) || !rioWriteBulkString(r, ele, ele_len)) { hashtableCleanupIterator(&iter); return 0; } diff --git a/src/db.c b/src/db.c index dd748e5f03c..c3f3009a0b4 100644 --- a/src/db.c +++ b/src/db.c @@ -28,7 +28,7 @@ */ #include "server.h" -#include "skiplist.h" +#include "ordered_index.h" #include "cluster.h" #include "cluster_migrateslots.h" #include "latency.h" @@ -1052,6 +1052,8 @@ void hashtableScanCallback(void *privdata, void *entry) { scanData *data = (scanData *)privdata; stringRef val = {NULL, 0}; sds key = NULL; + const char *zset_ptr = NULL; + size_t zset_ele_len = 0; robj *o = data->o; data->sampled++; @@ -1064,9 +1066,8 @@ void hashtableScanCallback(void *privdata, void *entry) { if (o->type == OBJ_SET) { key = (sds)entry; } else if (o->type == OBJ_ZSET) { - zskiplistNode *node = (zskiplistNode *)entry; - key = zslGetNodeElement(node); - /* zset data is copied after filtering by key */ + orderedIndexGetElementRaw((const OrderedIndexItem *)entry, &zset_ptr, &zset_ele_len); + /* zset data is copied after filtering */ } else if (o->type == OBJ_HASH) { key = entryGetField(entry); if (!data->only_keys) { @@ -1078,7 +1079,9 @@ void hashtableScanCallback(void *privdata, void *entry) { /* Filter element if it does not match the pattern. */ if (data->pattern) { - if (!stringmatchlen(data->pattern, sdslen(data->pattern), key, sdslen(key), 0)) { + const char *match_ptr = (o->type == OBJ_ZSET) ? zset_ptr : key; + size_t match_len = (o->type == OBJ_ZSET) ? zset_ele_len : sdslen(key); + if (!stringmatchlen(data->pattern, sdslen(data->pattern), match_ptr, match_len, 0)) { return; } } @@ -1087,11 +1090,13 @@ void hashtableScanCallback(void *privdata, void *entry) { * allocations. */ if (o->type == OBJ_ZSET) { /* zset data is copied */ - zskiplistNode *node = (zskiplistNode *)entry; - key = sdsdup(zslGetNodeElement(node)); + const char *ptr; + size_t ele_len; + orderedIndexGetElementRaw((const OrderedIndexItem *)entry, &ptr, &ele_len); + key = sdsnewlen(ptr, ele_len); if (!data->only_keys) { char buf[MAX_LONG_DOUBLE_CHARS]; - int len = ld2string(buf, sizeof(buf), node->score, LD_STR_AUTO); + int len = ld2string(buf, sizeof(buf), orderedIndexGetScore((const OrderedIndexItem *)entry), LD_STR_AUTO); sds tmp = sdsnewlen(buf, len); val.buf = (const char *)tmp; val.len = sdslen(tmp); diff --git a/src/debug.c b/src/debug.c index 3ae119dc60a..00cf6c5dec5 100644 --- a/src/debug.c +++ b/src/debug.c @@ -28,7 +28,7 @@ */ #include "server.h" -#include "skiplist.h" +#include "ordered_index.h" #include "util.h" #include "sha1.h" /* SHA1 is used for DEBUG DIGEST */ #include "crc64.h" @@ -217,12 +217,14 @@ void xorObjectDigest(serverDb *db, robj *keyobj, unsigned char *digest, robj *o) void *next; while (hashtableNext(&iter, &next)) { - zskiplistNode *node = next; - const int len = fpconv_dtoa(node->score, buf); + OrderedIndexItem *node = next; + const char *ele; + size_t ele_len; + orderedIndexGetElementRaw(node, &ele, &ele_len); + const int len = fpconv_dtoa(orderedIndexGetScore(node), buf); buf[len] = '\0'; memset(eledigest, 0, 20); - sds ele = zslGetNodeElement(node); - mixDigest(eledigest, ele, sdslen(ele)); + mixDigest(eledigest, ele, ele_len); mixDigest(eledigest, buf, strlen(buf)); xorDigest(digest, eledigest, 20); } @@ -1184,8 +1186,11 @@ void serverLogObjectDebugInfo(const robj *o) { serverLog(LL_WARNING, "Hash size: %d", (int)hashTypeLength(o)); } else if (o->type == OBJ_ZSET) { serverLog(LL_WARNING, "Sorted set size: %d", (int)zsetLength(o)); - if (o->encoding == OBJ_ENCODING_SKIPLIST) - serverLog(LL_WARNING, "Skiplist level: %d", (int)((const zset *)o->ptr)->zsl->level); + if (o->encoding == OBJ_ENCODING_SKIPLIST) { + /* Not declared in ordered_index.h — debug-only introspection. */ + extern int orderedIndexGetDepth(OrderedIndex * oi); + serverLog(LL_WARNING, "Index depth: %d", orderedIndexGetDepth(((const zset *)o->ptr)->oi)); + } } else if (o->type == OBJ_STREAM) { serverLog(LL_WARNING, "Stream size: %d", (int)streamLength(o)); } diff --git a/src/defrag.c b/src/defrag.c index 367c092cd7a..ce84d257315 100644 --- a/src/defrag.c +++ b/src/defrag.c @@ -39,7 +39,7 @@ */ #include "server.h" -#include "skiplist.h" +#include "ordered_index.h" #include "hashtable.h" #include "eval.h" #include "script.h" @@ -237,61 +237,13 @@ robj *activeDefragStringOb(robj *ob) { return new_robj; } -/* Internal function used by zslDefrag */ -static void zslUpdateNode(zskiplist *zsl, zskiplistNode *oldnode, zskiplistNode *newnode, zskiplistNode **update) { - int i; - for (i = 0; i < zslGetHeight(zsl); i++) { - if (update[i]->level[i].forward == oldnode) update[i]->level[i].forward = newnode; - } - serverAssert(zslGetHeader(zsl) != oldnode); - if (newnode->level[0].forward) { - serverAssert(newnode->level[0].forward->backward == oldnode); - newnode->level[0].forward->backward = newnode; - } else { - serverAssert(zslGetTail(zsl) == oldnode); - zslSetTail(zsl, newnode); - } -} - -/* Hashtable scan callback for sorted set. It defragments a single skiplist - * node, updates skiplist pointers, and updates the hashtable pointer to the - * node. */ -static void activeDefragZsetNode(void *privdata, void *entry_ref) { - zskiplist *zsl = privdata; - zskiplistNode **node_ref = (zskiplistNode **)entry_ref; - zskiplistNode *node = *node_ref; - - size_t allocation_size; - zskiplistNode *newnode = activeDefragAllocWithoutFree(node, &allocation_size); - if (newnode == NULL) return; - - const double score = node->score; - - /* find skiplist pointers that need to be updated if we end up moving the - * skiplist node. */ - sds ele = zslGetNodeElement(node); - zskiplistNode *update[ZSKIPLIST_MAXLEVEL]; - zskiplistNode *x = zslGetHeader(zsl); - for (int i = zslGetHeight(zsl) - 1; i >= 0; i--) { - /* stop when we've reached the end of this level or the next node comes - * after our target in sorted order. Even though defrag replacements does not impact the skip list order, - * when scores are equal, we MUST compare elements lexicographically to maintain correct skip list ordering. - * Otherwise we might miss locating the entry. */ - zskiplistNode *next = x->level[i].forward; - while (next && - (next->score < score || - (next->score == score && sdscmp(zslGetNodeElement(next), ele) < 0))) { - x = next; - next = x->level[i].forward; - } - update[i] = x; - } - /* should have arrived at intended node */ - serverAssert(x->level[0].forward == node); - - zslUpdateNode(zsl, node, newnode, update); - *node_ref = newnode; /* update hashtable pointer */ - allocatorDefragFree(node, allocation_size); +/* Callback for orderedIndexScanDefrag — when a node is reallocated, update + * the hashtable's pointer to it. */ +static void defragZsetNodeCallback(OrderedIndexItem *old_item, OrderedIndexItem *new_item, void *ctx) { + hashtable *ht = ctx; + bool replaced = hashtableReplaceReallocatedEntry(ht, old_item, new_item); + serverAssert(replaced); + server.stat_active_defrag_scanned++; } #define DEFRAG_SDS_DICT_NO_VAL 0 @@ -434,15 +386,10 @@ static long scanLaterList(robj *ob, unsigned long *cursor, monotime endtime) { return 0; } -static void scanLaterZsetCallback(void *privdata, void *element_ref) { - activeDefragZsetNode(privdata, element_ref); - server.stat_active_defrag_scanned++; -} - static void scanLaterZset(robj *ob, unsigned long *cursor) { serverAssert(ob->type == OBJ_ZSET && ob->encoding == OBJ_ENCODING_SKIPLIST); zset *zs = (zset *)objectGetVal(ob); - *cursor = hashtableScanDefrag(zs->ht, *cursor, scanLaterZsetCallback, zs->zsl, activeDefragAlloc, HASHTABLE_SCAN_EMIT_REF); + *cursor = orderedIndexScanDefrag(zs->oi, *cursor, defragZsetNodeCallback, zs->ht, activeDefragAlloc); } /* Used as hashtable scan callback when all we need is to defrag the hashtable @@ -482,12 +429,12 @@ static void defragZsetSkiplist(robj *ob) { zset *zs = (zset *)objectGetVal(ob); zset *newzs; - zskiplist *newzsl; if ((newzs = activeDefragAlloc(zs))) { objectSetVal(ob, newzs); zs = newzs; } - if ((newzsl = activeDefragAlloc(zs->zsl))) zs->zsl = newzsl; + OrderedIndex *newoi = orderedIndexDefragInternals(zs->oi, activeDefragAlloc); + if (newoi) zs->oi = newoi; hashtable *newtable; if ((newtable = hashtableDefragTables(zs->ht, activeDefragAlloc))) zs->ht = newtable; @@ -497,7 +444,7 @@ static void defragZsetSkiplist(robj *ob) { else { unsigned long cursor = 0; do { - cursor = hashtableScanDefrag(zs->ht, cursor, activeDefragZsetNode, zs->zsl, activeDefragAlloc, HASHTABLE_SCAN_EMIT_REF); + cursor = orderedIndexScanDefrag(zs->oi, cursor, defragZsetNodeCallback, zs->ht, activeDefragAlloc); } while (cursor != 0); } } diff --git a/src/geo.c b/src/geo.c index 6723ef1723c..fc26794af22 100644 --- a/src/geo.c +++ b/src/geo.c @@ -29,7 +29,7 @@ */ #include "geo.h" -#include "skiplist.h" +#include "ordered_index.h" #include "geohash_helper.h" #include "debugmacro.h" #include "pqsort.h" @@ -37,7 +37,6 @@ /* Things exported from t_zset.c only for geo.c, since it is the only other * part of the server that requires close zset introspection. */ unsigned char *zzlFirstInRange(unsigned char *zl, zrangespec *range); -int zslValueLteMax(double value, zrangespec *spec); /* ==================================================================== * This file implements the following commands: @@ -295,7 +294,7 @@ int geoGetPointsInRange(robj *zobj, double min, double max, GeoShape *shape, geo score = zzlGetScore(sptr); /* If we fell out of range, break. */ - if (!zslValueLteMax(score, &range)) break; + if (!zsetScoreLteMax(score, &range)) break; vstr = lpGetValue(eptr, &vlen, &vlong); if (geoWithinShape(shape, score, xy, &distance) == C_OK) { @@ -308,26 +307,25 @@ int geoGetPointsInRange(robj *zobj, double min, double max, GeoShape *shape, geo } } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(zobj); - zskiplist *zsl = zs->zsl; - zskiplistNode *ln; + OrderedIndexIterator iter; + orderedIndexInitIterator(&iter, zs->oi); + orderedIndexSeekToScoreRange(&iter, range.min, range.max, range.minex, range.maxex, 0); + OrderedIndexItem *ln; - if ((ln = zslNthInRange(zsl, &range, 0, NULL)) == NULL) { - /* Nothing exists starting at our min. No results. */ - return 0; - } - - while (ln) { + while ((ln = orderedIndexNext(&iter)) != NULL) { double xy[2]; double distance = 0; + double score = orderedIndexGetScore(ln); /* Abort when the node is no longer in range. */ - if (!zslValueLteMax(ln->score, &range)) break; - if (geoWithinShape(shape, ln->score, xy, &distance) == C_OK) { + if (!zsetScoreLteMax(score, &range)) break; + if (geoWithinShape(shape, score, xy, &distance) == C_OK) { /* Append the new element. */ - sds ele = zslGetNodeElement(ln); - geoArrayAppend(ga, xy, distance, ln->score, sdsdup(ele)); + const char *ele; + size_t ele_len; + orderedIndexGetElementRaw(ln, &ele, &ele_len); + geoArrayAppend(ga, xy, distance, score, sdsnewlen(ele, ele_len)); } if (ga->used && limit && ga->used >= limit) break; - ln = ln->level[0].forward; } } return ga->used - origincount; @@ -833,7 +831,7 @@ void georadiusGeneric(client *c, int srcKeyIndex, int flags) { } for (i = 0; i < returned_items; i++) { - zskiplistNode *znode; + OrderedIndexItem *znode; geoPoint *gp = ga.array + i; gp->dist /= shape.conversion; /* Fix according to unit. */ double score = storedist ? gp->dist : gp->score; @@ -841,7 +839,7 @@ void georadiusGeneric(client *c, int srcKeyIndex, int flags) { if (maxelelen < elelen) maxelelen = elelen; totelelen += elelen; - znode = zslInsert(zs->zsl, score, gp->member); + znode = orderedIndexInsert(zs->oi, score, gp->member, elelen); serverAssert(hashtableAdd(zs->ht, znode)); sdsfree(gp->member); gp->member = NULL; diff --git a/src/lazyfree.c b/src/lazyfree.c index 45dd5c49073..8bf9ae68896 100644 --- a/src/lazyfree.c +++ b/src/lazyfree.c @@ -1,5 +1,5 @@ #include "server.h" -#include "skiplist.h" +#include "ordered_index.h" #include "bio.h" #include "functions.h" #include "cluster.h" @@ -144,7 +144,7 @@ size_t lazyfreeGetFreeEffort(robj *key, robj *obj, int dbid) { return hashtableSize(ht); } else if (obj->type == OBJ_ZSET && obj->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(obj); - return zslGetLength(zs->zsl); + return orderedIndexLength(zs->oi); } else if (obj->type == OBJ_HASH && obj->encoding == OBJ_ENCODING_HASHTABLE) { hashtable *ht = objectGetVal(obj); return hashtableSize(ht); diff --git a/src/module.c b/src/module.c index 8a549255599..ece535dce28 100644 --- a/src/module.c +++ b/src/module.c @@ -56,7 +56,7 @@ * function names. For details, see the script src/modules/gendoc.rb. * -------------------------------------------------------------------------- */ #include "server.h" -#include "skiplist.h" +#include "ordered_index.h" #include "cluster.h" #include "commandlog.h" #include "rdb.h" @@ -220,14 +220,15 @@ struct ValkeyModuleKey { } list; struct { /* Zset iterator, use only if value->type == OBJ_ZSET */ - uint32_t type; /* VALKEYMODULE_ZSET_RANGE_* */ - zrangespec rs; /* Score range. */ - zlexrangespec lrs; /* Lex range. */ - uint32_t start; /* Start pos for positional ranges. */ - uint32_t end; /* End pos for positional ranges. */ - void *current; /* Zset iterator current node. */ - int er; /* Zset iterator end reached flag - (true if end was reached). */ + uint32_t type; /* VALKEYMODULE_ZSET_RANGE_* */ + zrangespec rs; /* Score range. */ + zlexrangespec lrs; /* Lex range. */ + uint32_t start; /* Start pos for positional ranges. */ + uint32_t end; /* End pos for positional ranges. */ + void *current; /* Zset iterator current node. */ + OrderedIndexIterator oi; /* OrderedIndex iterator for skiplist encoding. */ + int er; /* Zset iterator end reached flag + (true if end was reached). */ } zset; struct { /* Stream, use only if value->type == OBJ_STREAM */ @@ -5092,6 +5093,11 @@ void VM_ZsetRangeStop(ValkeyModuleKey *key) { if (!key->value || key->value->type != OBJ_ZSET) return; /* Free resources if needed. */ if (key->u.zset.type == VALKEYMODULE_ZSET_RANGE_LEX) zsetFreeLexRange(&key->u.zset.lrs); + /* Reset the ordered index iterator if one was active. */ + if (key->value->encoding == OBJ_ENCODING_SKIPLIST && + key->u.zset.type != VALKEYMODULE_ZSET_RANGE_NONE) { + orderedIndexResetIterator(&key->u.zset.oi); + } /* Setup sensible values so that misused iteration API calls when an * iterator is not active will result into something more sensible * than crashing. */ @@ -5129,8 +5135,9 @@ int zsetInitScoreRange(ValkeyModuleKey *key, double min, double max, int minex, key->u.zset.current = first ? zzlFirstInRange(objectGetVal(key->value), zrs) : zzlLastInRange(objectGetVal(key->value), zrs); } else if (key->value->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(key->value); - zskiplist *zsl = zs->zsl; - key->u.zset.current = first ? zslNthInRange(zsl, zrs, 0, NULL) : zslNthInRange(zsl, zrs, -1, NULL); + orderedIndexInitIterator(&key->u.zset.oi, zs->oi); + orderedIndexSeekToScoreRange(&key->u.zset.oi, zrs->min, zrs->max, zrs->minex, zrs->maxex, first ? 0 : -1); + key->u.zset.current = first ? orderedIndexNext(&key->u.zset.oi) : orderedIndexPrev(&key->u.zset.oi); } else { serverPanic("Unsupported zset encoding"); } @@ -5192,8 +5199,9 @@ int zsetInitLexRange(ValkeyModuleKey *key, ValkeyModuleString *min, ValkeyModule first ? zzlFirstInLexRange(objectGetVal(key->value), zlrs) : zzlLastInLexRange(objectGetVal(key->value), zlrs); } else if (key->value->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(key->value); - zskiplist *zsl = zs->zsl; - key->u.zset.current = first ? zslNthInLexRange(zsl, zlrs, 0) : zslNthInLexRange(zsl, zlrs, -1); + orderedIndexInitIterator(&key->u.zset.oi, zs->oi); + orderedIndexSeekToLexRange(&key->u.zset.oi, zlrs->min, zlrs->max, zlrs->minex, zlrs->maxex, first ? 0 : -1); + key->u.zset.current = first ? orderedIndexNext(&key->u.zset.oi) : orderedIndexPrev(&key->u.zset.oi); } else { serverPanic("Unsupported zset encoding"); } @@ -5242,10 +5250,12 @@ ValkeyModuleString *VM_ZsetRangeCurrentElement(ValkeyModuleKey *key, double *sco } str = createObject(OBJ_STRING, ele); } else if (key->value->encoding == OBJ_ENCODING_SKIPLIST) { - zskiplistNode *ln = key->u.zset.current; - if (score) *score = ln->score; - sds ele = zslGetNodeElement(ln); - str = createStringObject(ele, sdslen(ele)); + OrderedIndexItem *ln = key->u.zset.current; + if (score) *score = orderedIndexGetScore(ln); + const char *ele; + size_t ele_len; + orderedIndexGetElementRaw(ln, &ele, &ele_len); + str = createStringObject(ele, ele_len); } else { serverPanic("Unsupported zset encoding"); } @@ -5277,7 +5287,7 @@ int VM_ZsetRangeNext(ValkeyModuleKey *key) { unsigned char *saved_next = next; next = lpNext(zl, next); /* Skip next element. */ double score = zzlGetScore(next); /* Obtain the next score. */ - if (!zslValueLteMax(score, &key->u.zset.rs)) { + if (!zsetScoreLteMax(score, &key->u.zset.rs)) { key->u.zset.er = 1; return 0; } @@ -5292,17 +5302,20 @@ int VM_ZsetRangeNext(ValkeyModuleKey *key) { return 1; } } else if (key->value->encoding == OBJ_ENCODING_SKIPLIST) { - zskiplistNode *ln = key->u.zset.current, *next = ln->level[0].forward; + OrderedIndexItem *next = orderedIndexNext(&key->u.zset.oi); if (next == NULL) { key->u.zset.er = 1; return 0; } else { /* Are we still within the range? */ - if (key->u.zset.type == VALKEYMODULE_ZSET_RANGE_SCORE && !zslValueLteMax(next->score, &key->u.zset.rs)) { + if (key->u.zset.type == VALKEYMODULE_ZSET_RANGE_SCORE && !zsetScoreLteMax(orderedIndexGetScore(next), &key->u.zset.rs)) { key->u.zset.er = 1; return 0; } else if (key->u.zset.type == VALKEYMODULE_ZSET_RANGE_LEX) { - if (!zslLexValueLteMax(zslGetNodeElement(next), &key->u.zset.lrs)) { + const char *ele; + size_t ele_len; + orderedIndexGetElementRaw(next, &ele, &ele_len); + if (!zsetLexLteMax(ele, ele_len, &key->u.zset.lrs)) { key->u.zset.er = 1; return 0; } @@ -5339,7 +5352,7 @@ int VM_ZsetRangePrev(ValkeyModuleKey *key) { unsigned char *saved_prev = prev; prev = lpNext(zl, prev); /* Skip element to get the score.*/ double score = zzlGetScore(prev); /* Obtain the prev score. */ - if (!zslValueGteMin(score, &key->u.zset.rs)) { + if (!zsetScoreGteMin(score, &key->u.zset.rs)) { key->u.zset.er = 1; return 0; } @@ -5354,17 +5367,20 @@ int VM_ZsetRangePrev(ValkeyModuleKey *key) { return 1; } } else if (key->value->encoding == OBJ_ENCODING_SKIPLIST) { - zskiplistNode *ln = key->u.zset.current, *prev = ln->backward; + OrderedIndexItem *prev = orderedIndexPrev(&key->u.zset.oi); if (prev == NULL) { key->u.zset.er = 1; return 0; } else { /* Are we still within the range? */ - if (key->u.zset.type == VALKEYMODULE_ZSET_RANGE_SCORE && !zslValueGteMin(prev->score, &key->u.zset.rs)) { + if (key->u.zset.type == VALKEYMODULE_ZSET_RANGE_SCORE && !zsetScoreGteMin(orderedIndexGetScore(prev), &key->u.zset.rs)) { key->u.zset.er = 1; return 0; } else if (key->u.zset.type == VALKEYMODULE_ZSET_RANGE_LEX) { - if (!zslLexValueGteMin(zslGetNodeElement(prev), &key->u.zset.lrs)) { + const char *ele; + size_t ele_len; + orderedIndexGetElementRaw(prev, &ele, &ele_len); + if (!zsetLexGteMin(ele, ele_len, &key->u.zset.lrs)) { key->u.zset.er = 1; return 0; } @@ -11999,24 +12015,26 @@ static void moduleScanKeyHashtableCallback(void *privdata, void *entry) { ScanKeyCBData *data = privdata; robj *o = data->key->value; robj *value = NULL; - sds key = NULL; + const char *key_ptr = NULL; + size_t key_len = 0; if (o->type == OBJ_SET) { - key = entry; + key_ptr = entry; + key_len = sdslen(entry); /* no value */ } else if (o->type == OBJ_ZSET) { - zskiplistNode *node = (zskiplistNode *)entry; - key = zslGetNodeElement(node); - value = createStringObjectFromLongDouble(node->score, 0); + orderedIndexGetElementRaw((const OrderedIndexItem *)entry, &key_ptr, &key_len); + value = createStringObjectFromLongDouble(orderedIndexGetScore((const OrderedIndexItem *)entry), 0); } else if (o->type == OBJ_HASH) { - key = entryGetField(entry); + key_ptr = entryGetField(entry); + key_len = sdslen(entryGetField(entry)); size_t val_len; char *val = entryGetValue(entry, &val_len); value = createStringObject(val, val_len); } else { serverPanic("unexpected object type"); } - robj *field = createStringObject(key, sdslen(key)); + robj *field = createStringObject(key_ptr, key_len); data->fn(data->key, field, value, data->user_data); decrRefCount(field); diff --git a/src/object.c b/src/object.c index fa5cd5e1ae1..e37dc2c4660 100644 --- a/src/object.c +++ b/src/object.c @@ -30,7 +30,7 @@ #include "hashtable.h" #include "server.h" -#include "skiplist.h" +#include "ordered_index.h" #include "serverassert.h" #include "functions.h" #include "intset.h" /* Compact integer set structure */ @@ -526,7 +526,7 @@ robj *createZsetObject(void) { robj *o; zs->ht = hashtableCreate(&zsetHashtableType); - zs->zsl = zslCreate(); + zs->oi = orderedIndexCreate(); o = createObject(OBJ_ZSET, zs); o->encoding = OBJ_ENCODING_SKIPLIST; return o; @@ -584,7 +584,7 @@ void freeZsetObject(robj *o) { case OBJ_ENCODING_SKIPLIST: zs = objectGetVal(o); hashtableRelease(zs->ht); - zslFree(zs->zsl); + orderedIndexFree(zs->oi); zfree(zs); break; case OBJ_ENCODING_LISTPACK: zfree(objectGetVal(o)); break; @@ -720,17 +720,12 @@ void dismissSetObject(robj *o, size_t size_hint) { void dismissZsetObject(robj *o, size_t size_hint) { if (o->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(o); - zskiplist *zsl = zs->zsl; - serverAssert(zslGetLength(zsl) != 0); + unsigned long len = orderedIndexLength(zs->oi); + serverAssert(len != 0); /* We iterate all nodes only when average member size is bigger than a * page size, and there's a high chance we'll actually dismiss something. */ - if (size_hint / zslGetLength(zsl) >= server.page_size) { - zskiplistNode *zn = zslGetTail(zsl); - while (zn != NULL) { - zskiplistNode *next = zn->backward; - dismissMemory(zn, 0); - zn = next; - } + if (size_hint / len >= server.page_size) { + orderedIndexDismissMemory(zs->oi); } dismissHashtable(zs->ht); @@ -1244,16 +1239,8 @@ size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) { asize += zmalloc_size(objectGetVal(o)); } else if (o->encoding == OBJ_ENCODING_SKIPLIST) { hashtable *ht = ((zset *)objectGetVal(o))->ht; - zskiplist *zsl = ((zset *)objectGetVal(o))->zsl; - zskiplistNode *zheader = zslGetHeader(zsl); - zskiplistNode *znode = zheader->level[0].forward; - asize += sizeof(zset) + zslGetAllocSize() + hashtableMemUsage(ht); - while (znode != NULL && samples < sample_size) { - elesize += zmalloc_size(znode); - samples++; - znode = znode->level[0].forward; - } - if (samples) asize += (double)elesize / samples * hashtableSize(ht); + OrderedIndex *oi = ((zset *)objectGetVal(o))->oi; + asize += sizeof(zset) + orderedIndexEstimateMemory(oi, sample_size) + hashtableMemUsage(ht); } else { serverPanic("Unknown sorted set encoding"); } diff --git a/src/ordered_index.c b/src/ordered_index.c new file mode 100644 index 00000000000..4ffd634b000 --- /dev/null +++ b/src/ordered_index.c @@ -0,0 +1,163 @@ +/* + * Copyright (c) Valkey Contributors + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +/* OrderedIndex implementation — delegates to the active backend. + * Currently only the skiplist backend exists. When a B+ tree backend is added, + * a compile-time or link-time switch will select the implementation. */ + +#include "ordered_index.h" +#include "skiplist_ordered_index.h" + +/* Lifecycle */ + +OrderedIndex *orderedIndexCreate(void) { + return skiplistCreate(); +} + +void orderedIndexFree(OrderedIndex *oi) { + skiplistFree(oi); +} + +/* Modification */ + +OrderedIndexItem *orderedIndexInsert(OrderedIndex *oi, double score, const char *ele, size_t len) { + return skiplistInsert(oi, score, ele, len); +} + +void orderedIndexDelete(OrderedIndex *oi, OrderedIndexItem *item) { + skiplistDelete(oi, item); +} + +OrderedIndexItem *orderedIndexUpdateScore(OrderedIndex *oi, OrderedIndexItem *item, double newscore) { + return skiplistUpdateScore(oi, item, newscore); +} + +OrderedIndexItem *orderedIndexPopFirst(OrderedIndex *oi) { + return skiplistPopFirst(oi); +} + +OrderedIndexItem *orderedIndexPopLast(OrderedIndex *oi) { + return skiplistPopLast(oi); +} + +void orderedIndexFreeItem(OrderedIndexItem *item) { + skiplistFreeItem(item); +} + +OrderedIndexItem *orderedIndexCreateDetached(double score, const char *ele, size_t len) { + return skiplistCreateDetached(score, ele, len); +} + +void orderedIndexDetachedSetScore(OrderedIndexItem *item, double score) { + skiplistDetachedSetScore(item, score); +} + +OrderedIndexItem *orderedIndexInsertDetached(OrderedIndex *oi, OrderedIndexItem *item) { + return skiplistInsertDetached(oi, item); +} + +unsigned long orderedIndexDeleteRangeByScore(OrderedIndex *oi, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) { + return skiplistDeleteRangeByScore(oi, min, max, min_ex, max_ex, on_delete, ctx); +} + +unsigned long orderedIndexDeleteRangeByIndex(OrderedIndex *oi, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) { + return skiplistDeleteRangeByIndex(oi, start, end, on_delete, ctx); +} + +unsigned long orderedIndexDeleteRangeByLex(OrderedIndex *oi, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) { + return skiplistDeleteRangeByLex(oi, min, max, min_ex, max_ex, on_delete, ctx); +} + +/* Query */ + +unsigned long orderedIndexLength(OrderedIndex *oi) { + return skiplistLength(oi); +} + +OrderedIndexItem *orderedIndexGetByIndex(OrderedIndex *oi, unsigned long rank) { + return skiplistGetByIndex(oi, rank); +} + +OrderedIndexItem *orderedIndexGetFirst(OrderedIndex *oi) { + return skiplistGetFirst(oi); +} + +OrderedIndexItem *orderedIndexGetLast(OrderedIndex *oi) { + return skiplistGetLast(oi); +} + +unsigned long orderedIndexGetIndex(OrderedIndex *oi, const OrderedIndexItem *item) { + return skiplistGetIndex(oi, item); +} + +void orderedIndexGetElementRaw(const OrderedIndexItem *item, const char **ptr, size_t *len) { + skiplistGetElementRaw(item, ptr, len); +} + +double orderedIndexGetScore(const OrderedIndexItem *item) { + return skiplistGetScore(item); +} + +unsigned long orderedIndexCountScoreRange(OrderedIndex *oi, double min, double max, int min_ex, int max_ex) { + return skiplistCountScoreRange(oi, min, max, min_ex, max_ex); +} + +unsigned long orderedIndexCountLexRange(OrderedIndex *oi, const_sds min, const_sds max, int min_ex, int max_ex) { + return skiplistCountLexRange(oi, min, max, min_ex, max_ex); +} + +/* Iterator */ + +void orderedIndexInitIterator(OrderedIndexIterator *iter, OrderedIndex *oi) { + skiplistInitIterator(iter, oi); +} + +void orderedIndexResetIterator(OrderedIndexIterator *iter) { + skiplistResetIterator(iter); +} + +OrderedIndexItem *orderedIndexNext(OrderedIndexIterator *iter) { + return skiplistNext(iter); +} + +OrderedIndexItem *orderedIndexPrev(OrderedIndexIterator *iter) { + return skiplistPrev(iter); +} + +void orderedIndexSeekToIndex(OrderedIndexIterator *iter, unsigned long rank) { + skiplistSeekToIndex(iter, rank); +} + +void orderedIndexSeekToScoreRange(OrderedIndexIterator *iter, double min, double max, int min_ex, int max_ex, long offset) { + skiplistSeekToScoreRange(iter, min, max, min_ex, max_ex, offset); +} + +void orderedIndexSeekToLexRange(OrderedIndexIterator *iter, const_sds min, const_sds max, int min_ex, int max_ex, long offset) { + skiplistSeekToLexRange(iter, min, max, min_ex, max_ex, offset); +} + +/* Memory */ + +void orderedIndexDismissMemory(OrderedIndex *oi) { + skiplistDismissMemory(oi); +} + +size_t orderedIndexEstimateMemory(OrderedIndex *oi, size_t sample_size) { + return skiplistEstimateMemory(oi, sample_size); +} + +OrderedIndex *orderedIndexDefragInternals(OrderedIndex *oi, void *(*defragfn)(void *)) { + return skiplistDefragInternals(oi, defragfn); +} + +unsigned long orderedIndexScanDefrag(OrderedIndex *oi, unsigned long cursor, OrderedIndexDefragCallback callback, void *ctx, void *(*defragfn)(void *)) { + return skiplistScanDefrag(oi, cursor, callback, ctx, defragfn); +} + +/* Not declared in ordered_index.h — debug-only introspection. */ +int orderedIndexGetDepth(OrderedIndex *oi) { + return skiplistGetHeight(oi); +} diff --git a/src/ordered_index.h b/src/ordered_index.h new file mode 100644 index 00000000000..6222837df93 --- /dev/null +++ b/src/ordered_index.h @@ -0,0 +1,186 @@ +/* + * Copyright (c) Valkey Contributors + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef ORDERED_INDEX_H +#define ORDERED_INDEX_H + +/* OrderedIndex — a secondary data structure providing ordered access to + * (score, element) pairs. + * + * An OrderedIndex stores items ordered primarily by a double-precision score, + * with lexicographic ordering of the element string as a tiebreaker. It + * supports O(log N) insertion, deletion, score update, rank lookup, and + * range queries by score, rank, or lexicographic bounds. + * + * IMPORTANT: An OrderedIndex does NOT enforce element uniqueness. It is + * designed to be used alongside a companion hashtable that provides O(1) + * membership testing and prevents duplicate insertions. The caller is + * responsible for checking the hashtable before inserting. + * + * The interface is implementation-agnostic. Currently implemented as a skiplist + * (see skiplist_ordered_index.c). A B+ tree implementation is planned. Implementation + * selection is resolved at link time — all orderedIndex* functions are + * implemented in ordered_index.c which delegates to the active implementation. */ + +#include "sds.h" +#include + +/* Opaque types. The concrete definitions are backend-specific. */ +typedef struct OrderedIndex OrderedIndex; +typedef struct OrderedIndexItem OrderedIndexItem; +typedef uint64_t OrderedIndexIterator[2]; + +/* Callback invoked for each item removed during a range-delete operation. + * The item pointer is valid for the duration of the callback but will be + * freed by the index immediately after the callback returns. Do NOT free + * the item or store the pointer beyond the callback's scope. */ +typedef void (*OrderedIndexOnDelete)(OrderedIndexItem *item, void *ctx); + +/* Callback invoked during defrag when an item is reallocated. Allows the + * caller to update external references (e.g. hashtable pointers). */ +typedef void (*OrderedIndexDefragCallback)(OrderedIndexItem *old_item, OrderedIndexItem *new_item, void *ctx); + +/* ============================================================ + * Lifecycle + * ============================================================ */ + +/* Create a new empty ordered index. */ +OrderedIndex *orderedIndexCreate(void); + +/* Free an ordered index and all items it contains. */ +void orderedIndexFree(OrderedIndex *oi); + +/* ============================================================ + * Modification + * ============================================================ */ + +/* Insert a new item with the given score and element (copied). + * Returns a pointer to the inserted item (for storing in a companion HT). + * The caller must ensure the element is not already present. */ +OrderedIndexItem *orderedIndexInsert(OrderedIndex *oi, double score, const char *ele, size_t len); + +/* Remove an item from the index and free it. */ +void orderedIndexDelete(OrderedIndex *oi, OrderedIndexItem *item); + +/* Update the score of an existing item. May reposition it in the index. + * Returns the (possibly new) item pointer — the old pointer may be invalid + * if the item was repositioned. Always returns a valid pointer; callers can + * compare old vs returned to detect whether the item moved. */ +OrderedIndexItem *orderedIndexUpdateScore(OrderedIndex *oi, OrderedIndexItem *item, double newscore); + +/* Remove and return the first (lowest-score) item without freeing it. */ +OrderedIndexItem *orderedIndexPopFirst(OrderedIndex *oi); + +/* Remove and return the last (highest-score) item without freeing it. */ +OrderedIndexItem *orderedIndexPopLast(OrderedIndex *oi); + +/* Free a detached item (one that is not in any index). */ +void orderedIndexFreeItem(OrderedIndexItem *item); + +/* Create an item that is not inserted into any index. Used for multi-set + * operations (e.g. ZUNIONSTORE) where scores are accumulated in a hashtable + * before final insertion, avoiding repeated delete+reinsert costs. */ +OrderedIndexItem *orderedIndexCreateDetached(double score, const char *ele, size_t len); + +/* Set the score on a detached item. Do NOT use on items that are currently + * in an index — that would corrupt sort order. Use orderedIndexUpdateScore + * for items that are in an index. */ +void orderedIndexDetachedSetScore(OrderedIndexItem *item, double score); + +/* Insert a previously-detached item into the index. The index takes ownership. */ +OrderedIndexItem *orderedIndexInsertDetached(OrderedIndex *oi, OrderedIndexItem *item); + +/* Delete all items with score in [min, max]. If min_ex is set, min is exclusive; if max_ex is set, max is exclusive. + * Calls on_delete for each removed item. Returns count of items removed. */ +unsigned long orderedIndexDeleteRangeByScore(OrderedIndex *oi, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx); + +/* Delete all items with index in [start, end] (0-based, inclusive). + * Calls on_delete for each removed item. Returns count of items removed. */ +unsigned long orderedIndexDeleteRangeByIndex(OrderedIndex *oi, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx); + +/* Delete all items with element in lex range [min, max]. If min_ex is set, min is exclusive; if max_ex is set, max is exclusive. + * Calls on_delete for each removed item. Returns count of items removed. */ +unsigned long orderedIndexDeleteRangeByLex(OrderedIndex *oi, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx); + +/* ============================================================ + * Query + * ============================================================ */ + +/* Return the number of items in the index. */ +unsigned long orderedIndexLength(OrderedIndex *oi); + +/* Return the item at the given 0-based index, or NULL if out of range. */ +OrderedIndexItem *orderedIndexGetByIndex(OrderedIndex *oi, unsigned long index); + +/* Return the first (lowest) item, or NULL if empty. O(1). */ +OrderedIndexItem *orderedIndexGetFirst(OrderedIndex *oi); + +/* Return the last (highest) item, or NULL if empty. O(1). */ +OrderedIndexItem *orderedIndexGetLast(OrderedIndex *oi); + +/* Return the 0-based index of an item. The item must be in the index. */ +unsigned long orderedIndexGetIndex(OrderedIndex *oi, const OrderedIndexItem *item); + +/* Get the element data from an item as a raw pointer + length. */ +void orderedIndexGetElementRaw(const OrderedIndexItem *item, const char **ptr, size_t *len); + +/* Get the score of an item. */ +double orderedIndexGetScore(const OrderedIndexItem *item); + +/* Count items with score in [min, max]. If min_ex is set, min is exclusive; if max_ex is set, max is exclusive. */ +unsigned long orderedIndexCountScoreRange(OrderedIndex *oi, double min, double max, int min_ex, int max_ex); + +/* Count items with element in lex range [min, max]. If min_ex is set, min is exclusive; if max_ex is set, max is exclusive. */ +unsigned long orderedIndexCountLexRange(OrderedIndex *oi, const_sds min, const_sds max, int min_ex, int max_ex); + +/* ============================================================ + * Iterator + * ============================================================ */ + +/* Initialize a stack-allocated iterator. If no seek function is called, + * next() starts from the beginning and prev() starts from the end. + * Use orderedIndexSeekToIndex/ScoreRange/LexRange to start elsewhere. */ +void orderedIndexInitIterator(OrderedIndexIterator *iter, OrderedIndex *oi); + +/* Reset iterator position (keeps the index association). */ +void orderedIndexResetIterator(OrderedIndexIterator *iter); + +/* Advance iterator forward. Returns the next item, or NULL at end. */ +OrderedIndexItem *orderedIndexNext(OrderedIndexIterator *iter); + +/* Advance iterator backward. Returns the previous item, or NULL at start. */ +OrderedIndexItem *orderedIndexPrev(OrderedIndexIterator *iter); + +/* Position iterator at the given 0-based index. next() returns index+1, prev() returns index. */ +void orderedIndexSeekToIndex(OrderedIndexIterator *iter, unsigned long index); + +/* Position iterator within a score range. + * offset >= 0: next() returns the (offset)th element in range. + * offset < 0: prev() returns the (-offset-1)th element from end of range. */ +void orderedIndexSeekToScoreRange(OrderedIndexIterator *iter, double min, double max, int min_ex, int max_ex, long offset); + +/* Position iterator within a lex range. Offset semantics same as score range. */ +void orderedIndexSeekToLexRange(OrderedIndexIterator *iter, const_sds min, const_sds max, int min_ex, int max_ex, long offset); + +/* ============================================================ + * Memory + * ============================================================ */ + +/* Hint to the OS that the index memory can be reclaimed (e.g. via madvise). */ +void orderedIndexDismissMemory(OrderedIndex *oi); + +/* Estimate total memory usage by averaging the specified number of sample elements. */ +size_t orderedIndexEstimateMemory(OrderedIndex *oi, size_t sample_size); + +/* Defrag data structure internals. Returns new pointer if reallocated. */ +OrderedIndex *orderedIndexDefragInternals(OrderedIndex *oi, void *(*defragfn)(void *)); + +/* Incremental defrag scan. Walks items in batches, calling defragfn on each. + * When an item is reallocated, callback is invoked to update external refs. + * Returns next cursor, or 0 when complete. */ +unsigned long orderedIndexScanDefrag(OrderedIndex *oi, unsigned long cursor, OrderedIndexDefragCallback callback, void *ctx, void *(*defragfn)(void *)); + +#endif /* ORDERED_INDEX_H */ diff --git a/src/rdb.c b/src/rdb.c index 94fe6a2cf6e..d11069aab09 100644 --- a/src/rdb.c +++ b/src/rdb.c @@ -34,7 +34,7 @@ #include "hashtable.h" #include "server.h" -#include "skiplist.h" +#include "ordered_index.h" #include "lzf.h" /* LZF compression library */ #include "zipmap.h" #include "endianconv.h" @@ -957,9 +957,8 @@ ssize_t rdbSaveObject(rio *rdb, robj *o, robj *key, int dbid, unsigned char rdbt nwritten += n; } else if (o->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(o); - zskiplist *zsl = zs->zsl; - if ((n = rdbSaveLen(rdb, zslGetLength(zsl))) == -1) return -1; + if ((n = rdbSaveLen(rdb, orderedIndexLength(zs->oi))) == -1) return -1; nwritten += n; /* We save the skiplist elements from the greatest to the smallest @@ -968,16 +967,19 @@ ssize_t rdbSaveObject(rio *rdb, robj *o, robj *key, int dbid, unsigned char rdbt * element will always be the smaller, so adding to the skiplist * will always immediately stop at the head, making the insertion * O(1) instead of O(log(N)). */ - zskiplistNode *zn = zslGetTail(zsl); - while (zn != NULL) { - sds ele = zslGetNodeElement(zn); - if ((n = rdbSaveRawString(rdb, (unsigned char *)ele, sdslen(ele))) == -1) { + OrderedIndexIterator iter; + orderedIndexInitIterator(&iter, zs->oi); + OrderedIndexItem *item; + while ((item = orderedIndexPrev(&iter)) != NULL) { + const char *ele; + size_t ele_len; + orderedIndexGetElementRaw(item, &ele, &ele_len); + if ((n = rdbSaveRawString(rdb, (unsigned char *)ele, ele_len)) == -1) { return -1; } nwritten += n; - if ((n = rdbSaveBinaryDoubleValue(rdb, zn->score)) == -1) return -1; + if ((n = rdbSaveBinaryDoubleValue(rdb, orderedIndexGetScore(item))) == -1) return -1; nwritten += n; - zn = zn->backward; } } else { serverPanic("Unknown sorted set encoding"); @@ -2084,7 +2086,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error, int rd while (zsetlen--) { sds sdsele; double score; - zskiplistNode *znode; + OrderedIndexItem *znode; if ((sdsele = rdbGenericLoadStringObject(rdb, RDB_LOAD_SDS, NULL)) == NULL) { decrRefCount(o); @@ -2116,7 +2118,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error, int rd if (sdslen(sdsele) > maxelelen) maxelelen = sdslen(sdsele); totelelen += sdslen(sdsele); - znode = zslInsert(zs->zsl, score, sdsele); + znode = orderedIndexInsert(zs->oi, score, sdsele, sdslen(sdsele)); sdsfree(sdsele); if (!hashtableAdd(zs->ht, znode)) { rdbReportCorruptRDB("Duplicate zset fields detected"); diff --git a/src/server.c b/src/server.c index 20fb6ede32f..e5c11a08412 100644 --- a/src/server.c +++ b/src/server.c @@ -32,7 +32,7 @@ * SPDX-License-Identifier: BSD-3-Clause */ #include "server.h" -#include "skiplist.h" +#include "ordered_index.h" #include "connection.h" #include "monotonic.h" #include "cluster.h" @@ -626,16 +626,41 @@ hashtableType setHashtableType = { .keyCompare = dictSdsKeyCompare, .entryDestructor = dictSdsDestructor}; +/* Extract the element portion of a zset hashtable key as (ptr, len). + * Handles both stored OrderedIndex items and plain sds lookup keys. + * Currently both are plain sds; the fbtree backend (next PR) extends this + * to skip an 8-byte score prefix on packed items using the sds aux bit. */ +static const char *zsetExtractElement(const void *key, size_t *len) { + *len = sdslen((const_sds)key); + return (const char *)key; +} + const void *zsetHashtableGetKey(const void *element) { - const zskiplistNode *node = element; - return zslGetNodeElement(node); + const char *ptr; + size_t len; + orderedIndexGetElementRaw((const OrderedIndexItem *)element, &ptr, &len); + return ptr; +} + +static uint64_t zsetHashFunction(const void *key) { + size_t len; + const char *ptr = zsetExtractElement(key, &len); + return genHashFunctionConfigurableSeed(ptr, len); } -/* Sorted sets hash (note: a skiplist is used in addition to the hash table) */ +static int zsetKeyCompare(const void *a, const void *b) { + size_t alen, blen; + const char *aptr = zsetExtractElement(a, &alen); + const char *bptr = zsetExtractElement(b, &blen); + if (alen != blen) return 0; + return memcmp(aptr, bptr, alen) == 0; +} + +/* Sorted sets hash (an ordered index is used in addition to the hash table) */ hashtableType zsetHashtableType = { - .hashFunction = sdsHashConfigurableSeed, + .hashFunction = zsetHashFunction, .entryGetKey = zsetHashtableGetKey, - .keyCompare = dictSdsKeyCompare, + .keyCompare = zsetKeyCompare, }; uint64_t hashtableSdsHash(const void *key) { diff --git a/src/server.h b/src/server.h index 94fa9a5647e..6ba135e9b2c 100644 --- a/src/server.h +++ b/src/server.h @@ -1488,12 +1488,12 @@ struct sharedObjectsStruct { sds minstring, maxstring; }; -/* Skiplist types - full definitions in skiplist.h */ -struct zskiplist; +/* OrderedIndex - full definition in ordered_index.h */ +typedef struct OrderedIndex OrderedIndex; typedef struct zset { hashtable *ht; - struct zskiplist *zsl; + OrderedIndex *oi; } zset; typedef struct clientBufferLimitsConfig { @@ -3366,6 +3366,13 @@ typedef struct { int minex, maxex; /* are min or max exclusive? */ } zlexrangespec; +/* Zset range comparison utilities (used by both listpack and ordered index encodings) */ +int zsetScoreGteMin(double value, zrangespec *spec); +int zsetScoreLteMax(double value, zrangespec *spec); +int zsetLexCompare(const char *a, size_t alen, sds b); +int zsetLexGteMin(const char *value, size_t len, zlexrangespec *spec); +int zsetLexLteMax(const char *value, size_t len, zlexrangespec *spec); + /* flags for incrCommandFailedCalls */ #define ERROR_COMMAND_REJECTED (1 << 0) /* Indicate to update the command rejected stats */ #define ERROR_COMMAND_FAILED (1 << 1) /* Indicate to update the command failed stats */ diff --git a/src/skiplist.c b/src/skiplist.c index 84b00437508..c9c2cf28a07 100644 --- a/src/skiplist.c +++ b/src/skiplist.c @@ -52,8 +52,8 @@ static zskiplistNode *zslGetElementByRankFromNode(zskiplistNode *start_node, int * * sds-header-size and element-sds are only valid for non-header nodes. */ -zskiplistNode *zslCreateNode(int height, double score, const_sds ele) { - size_t ele_sds_len = sdslen(ele); +zskiplistNode *zslCreateNode(int height, double score, const char *ele, size_t ele_len) { + size_t ele_sds_len = ele_len; char ele_sds_type = sdsReqType(ele_sds_len); size_t ele_sds_size = sdsReqSize(ele_sds_len, ele_sds_type); /* Allocate enough space for the node, levels, and the element sds. @@ -227,14 +227,14 @@ zskiplistNode *zslInsertNode(zskiplist *zsl, zskiplistNode *node) { * exist (up to the caller to enforce that). The string 'ele' is copied. */ zskiplistNode *zslInsert(zskiplist *zsl, double score, const_sds ele) { const int level = zslRandomLevel(); - zskiplistNode *node = zslCreateNode(level, score, ele); + zskiplistNode *node = zslCreateNode(level, score, ele, sdslen(ele)); zslInsertNode(zsl, node); return node; } /* Internal function used by zslDelete, zslDeleteRangeByScore and * zslDeleteRangeByRank. */ -void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) { +void zslUnlinkNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) { int i; for (i = 0; i < zslGetHeight(zsl); i++) { if (update[i]->level[i].forward == x) { @@ -270,7 +270,7 @@ void zslDelete(zskiplist *zsl, zskiplistNode *node) { /* We should have arrived at the correct node */ serverAssert(x->level[0].forward == node); - zslDeleteNode(zsl, node, update); + zslUnlinkNode(zsl, node, update); zslFreeNode(node); } @@ -304,20 +304,12 @@ zskiplistNode *zslUpdateScore(zskiplist *zsl, zskiplistNode *node, double newsco /* We assume that the node exists in the skiplist */ serverAssert(x->level[0].forward == node); - zslDeleteNode(zsl, node, update); + zslUnlinkNode(zsl, node, update); node->score = newscore; /* reuse existing node to avoid memory allocation */ zslInsertNode(zsl, node); return node; } -int zslValueGteMin(double value, zrangespec *spec) { - return spec->minex ? (value > spec->min) : (value >= spec->min); -} - -int zslValueLteMax(double value, zrangespec *spec) { - return spec->maxex ? (value < spec->max) : (value <= spec->max); -} - /* Returns if there is a part of the zset is in range. */ int zslIsInRange(zskiplist *zsl, zrangespec *range) { zskiplistNode *x; @@ -325,10 +317,10 @@ int zslIsInRange(zskiplist *zsl, zrangespec *range) { /* Test for ranges that will always be empty. */ if (range->min > range->max || (range->min == range->max && (range->minex || range->maxex))) return 0; x = zslGetTail(zsl); - if (x == NULL || !zslValueGteMin(x->score, range)) return 0; + if (x == NULL || !zsetScoreGteMin(x->score, range)) return 0; zskiplistNode *zheader = zslGetHeader(zsl); x = zheader->level[0].forward; - if (x == NULL || !zslValueLteMax(x->score, range)) return 0; + if (x == NULL || !zsetScoreLteMax(x->score, range)) return 0; return 1; } @@ -344,7 +336,7 @@ zskiplistNode *zslNthInRange(zskiplist *zsl, zrangespec *range, long n, long *ra zskiplistNode *x = zslGetHeader(zsl); int i = zslGetHeight(zsl) - 1; long last_highest_level_rank = 0; - while (x->level[i].forward && !zslValueGteMin(x->level[i].forward->score, range)) { + while (x->level[i].forward && !zsetScoreGteMin(x->level[i].forward->score, range)) { last_highest_level_rank += zslGetNodeSpanAtLevel(x, i); x = x->level[i].forward; } @@ -355,7 +347,7 @@ zskiplistNode *zslNthInRange(zskiplist *zsl, zrangespec *range, long n, long *ra long start_rank = last_highest_level_rank; for (i = zslGetHeight(zsl) - 2; i >= 0; i--) { /* Go forward while *OUT* of range. */ - while (x->level[i].forward && !zslValueGteMin(x->level[i].forward->score, range)) { + while (x->level[i].forward && !zsetScoreGteMin(x->level[i].forward->score, range)) { /* Count the rank of the last element smaller than the range. */ start_rank += zslGetNodeSpanAtLevel(x, i); x = x->level[i].forward; @@ -375,13 +367,13 @@ zskiplistNode *zslNthInRange(zskiplist *zsl, zrangespec *range, long n, long *ra x = zslGetElementByRankFromNode(last_highest_level_node, zslGetHeight(zsl) - 1, rank_diff); } /* Check if score <= max. */ - if (x && !zslValueLteMax(x->score, range)) return NULL; + if (x && !zsetScoreLteMax(x->score, range)) return NULL; if (rank) *rank = start_rank + n; } else { long end_rank = last_highest_level_rank; for (i = zslGetHeight(zsl) - 1; i >= 0; i--) { /* Go forward while *IN* range. */ - while (x->level[i].forward && zslValueLteMax(x->level[i].forward->score, range)) { + while (x->level[i].forward && zsetScoreLteMax(x->level[i].forward->score, range)) { /* Count the rank of the last element in range. */ end_rank += zslGetNodeSpanAtLevel(x, i); x = x->level[i].forward; @@ -402,7 +394,7 @@ zskiplistNode *zslNthInRange(zskiplist *zsl, zrangespec *range, long n, long *ra x = zslGetElementByRankFromNode(last_highest_level_node, zslGetHeight(zsl) - 1, rank_diff); } /* Check if score >= min. */ - if (x && !zslValueGteMin(x->score, range)) return NULL; + if (x && !zsetScoreGteMin(x->score, range)) return NULL; if (rank) *rank = end_rank + n; } @@ -421,7 +413,7 @@ unsigned long zslDeleteRangeByScore(zskiplist *zsl, zrangespec *range, hashtable x = zslGetHeader(zsl); for (i = zslGetHeight(zsl) - 1; i >= 0; i--) { - while (x->level[i].forward && !zslValueGteMin(x->level[i].forward->score, range)) x = x->level[i].forward; + while (x->level[i].forward && !zsetScoreGteMin(x->level[i].forward->score, range)) x = x->level[i].forward; update[i] = x; } @@ -429,9 +421,9 @@ unsigned long zslDeleteRangeByScore(zskiplist *zsl, zrangespec *range, hashtable x = x->level[0].forward; /* Delete nodes while in range. */ - while (x && zslValueLteMax(x->score, range)) { + while (x && zsetScoreLteMax(x->score, range)) { zskiplistNode *next = x->level[0].forward; - zslDeleteNode(zsl, x, update); + zslUnlinkNode(zsl, x, update); sds ele = zslGetNodeElement(x); hashtablePop(ht, ele, NULL); zslFreeNode(x); @@ -450,7 +442,7 @@ unsigned long zslDeleteRangeByLex(zskiplist *zsl, zlexrangespec *range, hashtabl x = zslGetHeader(zsl); for (i = zslGetHeight(zsl) - 1; i >= 0; i--) { while (x->level[i].forward && - !zslLexValueGteMin(zslGetNodeElement(x->level[i].forward), range)) { + !zsetLexGteMin(zslGetNodeElement(x->level[i].forward), sdslen(zslGetNodeElement(x->level[i].forward)), range)) { x = x->level[i].forward; } update[i] = x; @@ -460,9 +452,9 @@ unsigned long zslDeleteRangeByLex(zskiplist *zsl, zlexrangespec *range, hashtabl x = x->level[0].forward; /* Delete nodes while in range. */ - while (x && zslLexValueLteMax(zslGetNodeElement(x), range)) { + while (x && zsetLexLteMax(zslGetNodeElement(x), sdslen(zslGetNodeElement(x)), range)) { zskiplistNode *next = x->level[0].forward; - zslDeleteNode(zsl, x, update); + zslUnlinkNode(zsl, x, update); hashtableDelete(ht, zslGetNodeElement(x)); zslFreeNode(x); /* Here is where x->ele is actually released. */ removed++; @@ -491,7 +483,7 @@ unsigned long zslDeleteRangeByRank(zskiplist *zsl, unsigned int start, unsigned x = x->level[0].forward; while (x && traversed <= end) { zskiplistNode *next = x->level[0].forward; - zslDeleteNode(zsl, x, update); + zslUnlinkNode(zsl, x, update); hashtableDelete(ht, zslGetNodeElement(x)); zslFreeNode(x); removed++; @@ -557,38 +549,22 @@ zskiplistNode *zslGetElementByRank(zskiplist *zsl, unsigned long rank) { * If the string is not a valid range C_ERR is returned, and the value * of *dest and *ex is undefined. */ -/* This is just a wrapper to sdscmp() that is able to - * handle shared.minstring and shared.maxstring as the equivalent of - * -inf and +inf for strings */ -int sdscmplex(sds a, sds b) { - if (a == b) return 0; - if (a == shared.minstring || b == shared.maxstring) return -1; - if (a == shared.maxstring || b == shared.minstring) return 1; - return sdscmp(a, b); -} - -int zslLexValueGteMin(sds value, zlexrangespec *spec) { - return spec->minex ? (sdscmplex(value, spec->min) > 0) : (sdscmplex(value, spec->min) >= 0); -} - -int zslLexValueLteMax(sds value, zlexrangespec *spec) { - return spec->maxex ? (sdscmplex(value, spec->max) < 0) : (sdscmplex(value, spec->max) <= 0); -} - /* Returns if there is a part of the zset is in the lex range. */ static int zslIsInLexRange(zskiplist *zsl, zlexrangespec *range) { zskiplistNode *x; /* Test for ranges that will always be empty. */ - int cmp = sdscmplex(range->min, range->max); + int cmp = zsetLexCompare(range->min, sdslen(range->min), range->max); if (cmp > 0 || (cmp == 0 && (range->minex || range->maxex))) return 0; x = zslGetTail(zsl); + if (x == NULL) return 0; sds ele = zslGetNodeElement(x); - if (x == NULL || !zslLexValueGteMin(ele, range)) return 0; + if (!zsetLexGteMin(ele, sdslen(ele), range)) return 0; zskiplistNode *zheader = zslGetHeader(zsl); x = zheader->level[0].forward; + if (x == NULL) return 0; ele = zslGetNodeElement(x); - if (x == NULL || !zslLexValueLteMax(ele, range)) return 0; + if (!zsetLexLteMax(ele, sdslen(ele), range)) return 0; return 1; } @@ -609,7 +585,7 @@ zskiplistNode *zslNthInLexRange(zskiplist *zsl, zlexrangespec *range, long n) { /* Go forward while *OUT* of range at highest level. */ x = zslGetHeader(zsl); i = zslGetHeight(zsl) - 1; - while (x->level[i].forward && !zslLexValueGteMin(zslGetNodeElement(x->level[i].forward), range)) { + while (x->level[i].forward && !zsetLexGteMin(zslGetNodeElement(x->level[i].forward), sdslen(zslGetNodeElement(x->level[i].forward)), range)) { edge_rank += zslGetNodeSpanAtLevel(x, i); x = x->level[i].forward; } @@ -620,7 +596,7 @@ zskiplistNode *zslNthInLexRange(zskiplist *zsl, zlexrangespec *range, long n) { if (n >= 0) { for (i = zslGetHeight(zsl) - 2; i >= 0; i--) { /* Go forward while *OUT* of range. */ - while (x->level[i].forward && !zslLexValueGteMin(zslGetNodeElement(x->level[i].forward), range)) { + while (x->level[i].forward && !zsetLexGteMin(zslGetNodeElement(x->level[i].forward), sdslen(zslGetNodeElement(x->level[i].forward)), range)) { /* Count the rank of the last element smaller than the range. */ edge_rank += zslGetNodeSpanAtLevel(x, i); x = x->level[i].forward; @@ -640,11 +616,11 @@ zskiplistNode *zslNthInLexRange(zskiplist *zsl, zlexrangespec *range, long n) { x = zslGetElementByRankFromNode(last_highest_level_node, zslGetHeight(zsl) - 1, rank_diff); } /* Check if score <= max. */ - if (x && !zslLexValueLteMax(zslGetNodeElement(x), range)) return NULL; + if (x && !zsetLexLteMax(zslGetNodeElement(x), sdslen(zslGetNodeElement(x)), range)) return NULL; } else { for (i = zslGetHeight(zsl) - 1; i >= 0; i--) { /* Go forward while *IN* range. */ - while (x->level[i].forward && zslLexValueLteMax(zslGetNodeElement(x->level[i].forward), range)) { + while (x->level[i].forward && zsetLexLteMax(zslGetNodeElement(x->level[i].forward), sdslen(zslGetNodeElement(x->level[i].forward)), range)) { /* Count the rank of the last element in range. */ edge_rank += zslGetNodeSpanAtLevel(x, i); x = x->level[i].forward; @@ -664,8 +640,121 @@ zskiplistNode *zslNthInLexRange(zskiplist *zsl, zlexrangespec *range, long n) { x = zslGetElementByRankFromNode(last_highest_level_node, zslGetHeight(zsl) - 1, rank_diff); } /* Check if score >= min. */ - if (x && !zslLexValueGteMin(zslGetNodeElement(x), range)) return NULL; + if (x && !zsetLexGteMin(zslGetNodeElement(x), sdslen(zslGetNodeElement(x)), range)) return NULL; } return x; } + + +zskiplistNode *zslGetFirst(const zskiplist *zsl) { + return ((zskiplist *)zsl)->header.level[0].forward; +} + +double zslGetScore(const zskiplistNode *node) { + return node->score; +} + +/* Detach a node from the skiplist without freeing it. */ +zskiplistNode *zslDetachNode(zskiplist *zsl, zskiplistNode *node) { + zskiplistNode *update[ZSKIPLIST_MAXLEVEL]; + zskiplistNode *x = zslGetHeader(zsl); + for (int i = zslGetHeight(zsl) - 1; i >= 0; i--) { + while (zslCompareNodes(x->level[i].forward, node) < 0) { + x = x->level[i].forward; + } + update[i] = x; + } + serverAssert(x->level[0].forward == node); + zslUnlinkNode(zsl, node, update); + return node; +} + +/* --- Iterator implementation --- */ + +void zslInitIterator(zslIter *iter, zskiplist *zsl) { + iter->zsl = zsl; + iter->node = NULL; +} + +void zslResetIterator(zslIter *iter) { + iter->node = NULL; +} + +zslIter *zslCreateIterator(zskiplist *zsl) { + zslIter *iter = zmalloc(sizeof(*iter)); + zslInitIterator(iter, zsl); + return iter; +} + +void zslReleaseIterator(zslIter *iter) { + zfree(iter); +} + +zskiplistNode *zslNext(zslIter *iter) { + if (iter->zsl == NULL) { + return NULL; + } + if (iter->node == NULL) { + iter->node = zslGetHeader(iter->zsl)->level[0].forward; + } else if (iter->node == zslGetTail(iter->zsl)) { + return NULL; + } else { + iter->node = iter->node->level[0].forward; + } + if (iter->node == NULL) { + return NULL; + } + return iter->node; +} + +zskiplistNode *zslPrev(zslIter *iter) { + if (iter->zsl == NULL) { + return NULL; + } + if (iter->node == zslGetHeader(iter->zsl)) { + return NULL; + } + if (iter->node == NULL) { + iter->node = zslGetTail(iter->zsl); + if (iter->node == NULL) { + return NULL; + } + } + zskiplistNode *ret = iter->node; + iter->node = iter->node->backward; + if (iter->node == NULL) iter->node = zslGetHeader(iter->zsl); + return ret; +} + +void zslSeekToRank(zslIter *iter, unsigned long rank) { + if (iter->zsl == NULL) return; + if (rank == 0) + iter->node = zslGetHeader(iter->zsl); + else if (rank >= zslGetLength(iter->zsl)) + iter->node = zslGetTail(iter->zsl); + else + iter->node = zslGetElementByRank(iter->zsl, rank); +} + +void zslSeekToScoreRange(zslIter *iter, double min, double max, int min_ex, int max_ex, long offset) { + if (iter->zsl == NULL) return; + zrangespec range = {.min = min, .max = max, .minex = min_ex, .maxex = max_ex}; + zskiplistNode *node = zslNthInRange(iter->zsl, &range, offset, NULL); + if (node == NULL) { + iter->node = (offset < 0) ? zslGetHeader(iter->zsl) : zslGetTail(iter->zsl); + return; + } + iter->node = (offset < 0) ? node : node->backward; +} + +void zslSeekToLexRange(zslIter *iter, const_sds min, const_sds max, int min_ex, int max_ex, long offset) { + if (iter->zsl == NULL) return; + zlexrangespec range = {.min = (sds)min, .max = (sds)max, .minex = min_ex, .maxex = max_ex}; + zskiplistNode *node = zslNthInLexRange(iter->zsl, &range, offset); + if (node == NULL) { + iter->node = (offset < 0) ? zslGetHeader(iter->zsl) : zslGetTail(iter->zsl); + return; + } + iter->node = (offset < 0) ? node : node->backward; +} diff --git a/src/skiplist.h b/src/skiplist.h index c179d7301c0..a3a77b081fd 100644 --- a/src/skiplist.h +++ b/src/skiplist.h @@ -131,13 +131,13 @@ zskiplistNode *zslGetHeader(zskiplist *zsl); sds zslGetNodeElement(const zskiplistNode *x); /* Insertion */ -zskiplistNode *zslCreateNode(int height, double score, const_sds ele); +zskiplistNode *zslCreateNode(int height, double score, const char *ele, size_t ele_len); int zslRandomLevel(void); zskiplistNode *zslInsertNode(zskiplist *zsl, zskiplistNode *node); zskiplistNode *zslInsert(zskiplist *zsl, double score, const_sds ele); /* Deletion */ -void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update); +void zslUnlinkNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update); void zslDelete(zskiplist *zsl, zskiplistNode *node); void zslFreeNode(zskiplistNode *node); unsigned long zslDeleteRangeByScore(zskiplist *zsl, zrangespec *range, hashtable *ht); @@ -148,17 +148,33 @@ unsigned long zslDeleteRangeByRank(zskiplist *zsl, unsigned int start, unsigned zskiplistNode *zslUpdateScore(zskiplist *zsl, zskiplistNode *node, double newscore); /* Queries */ -int zslValueGteMin(double value, zrangespec *spec); -int zslValueLteMax(double value, zrangespec *spec); int zslIsInRange(zskiplist *zsl, zrangespec *range); zskiplistNode *zslNthInRange(zskiplist *zsl, zrangespec *range, long n, long *rank); unsigned long zslGetRank(zskiplist *zsl, const zskiplistNode *node); zskiplistNode *zslGetElementByRank(zskiplist *zsl, unsigned long rank); /* Lex queries */ -int zslLexValueGteMin(sds value, zlexrangespec *spec); -int zslLexValueLteMax(sds value, zlexrangespec *spec); -int sdscmplex(sds a, sds b); zskiplistNode *zslNthInLexRange(zskiplist *zsl, zlexrangespec *range, long n); +/* Iterator */ +typedef struct { + zskiplist *zsl; /* The skiplist being iterated */ + zskiplistNode *node; /* Current node (NULL before first call) */ +} zslIter; + +void zslInitIterator(zslIter *iter, zskiplist *zsl); +void zslResetIterator(zslIter *iter); +zslIter *zslCreateIterator(zskiplist *zsl); +void zslReleaseIterator(zslIter *iter); +zskiplistNode *zslNext(zslIter *iter); +zskiplistNode *zslPrev(zslIter *iter); +void zslSeekToRank(zslIter *iter, unsigned long rank); +void zslSeekToScoreRange(zslIter *iter, double min, double max, int min_ex, int max_ex, long offset); +void zslSeekToLexRange(zslIter *iter, const_sds min, const_sds max, int min_ex, int max_ex, long offset); + +/* Additional accessors */ +zskiplistNode *zslGetFirst(const zskiplist *zsl); +double zslGetScore(const zskiplistNode *node); +zskiplistNode *zslDetachNode(zskiplist *zsl, zskiplistNode *node); + #endif /* SKIPLIST_H */ diff --git a/src/skiplist_ordered_index.c b/src/skiplist_ordered_index.c new file mode 100644 index 00000000000..8459c2ebb8c --- /dev/null +++ b/src/skiplist_ordered_index.c @@ -0,0 +1,521 @@ +/* + * Copyright (c) Valkey Contributors + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "server.h" +#include "ordered_index.h" +#include "skiplist.h" + +static_assert(sizeof(OrderedIndexIterator) >= sizeof(zslIter), + "OrderedIndexIterator must be large enough to hold zslIter"); + +/* Skiplist implementation of OrderedIndex interface */ + +/*----------------------------------------------------------------------------- + * Internal skiplist helpers + *---------------------------------------------------------------------------*/ + +/* Lifecycle */ + +OrderedIndex *skiplistCreate(void) { + return (OrderedIndex *)zslCreate(); +} + +void skiplistFree(OrderedIndex *oi) { + zslFree((zskiplist *)oi); +} + +/* Modification */ + +OrderedIndexItem *skiplistInsert(OrderedIndex *oi, double score, const char *ele, size_t len) { + zskiplistNode *node = zslCreateNode(zslRandomLevel(), score, ele, len); + zslInsertNode((zskiplist *)oi, node); + return (OrderedIndexItem *)node; +} + +void skiplistDelete(OrderedIndex *oi, OrderedIndexItem *node) { + zslDelete((zskiplist *)oi, (zskiplistNode *)node); +} + +OrderedIndexItem *skiplistUpdateScore(OrderedIndex *oi, OrderedIndexItem *node, double newscore) { + zskiplistNode *result = zslUpdateScore((zskiplist *)oi, (zskiplistNode *)node, newscore); + return result ? (OrderedIndexItem *)result : (OrderedIndexItem *)node; +} + +OrderedIndexItem *skiplistPopFirst(OrderedIndex *oi) { + zskiplist *zsl = (zskiplist *)oi; + zskiplistNode *first = zslGetFirst(zsl); + if (!first) return NULL; + zslDetachNode(zsl, first); + return (OrderedIndexItem *)first; +} + +OrderedIndexItem *skiplistPopLast(OrderedIndex *oi) { + zskiplist *zsl = (zskiplist *)oi; + zskiplistNode *last = zslGetTail(zsl); + if (!last) return NULL; + zslDetachNode(zsl, last); + return (OrderedIndexItem *)last; +} + +void skiplistFreeItem(OrderedIndexItem *item) { + zslFreeNode((zskiplistNode *)item); +} + +OrderedIndexItem *skiplistCreateDetached(double score, const char *ele, size_t len) { + zskiplistNode *node = zslCreateNode(zslRandomLevel(), score, ele, len); + return (OrderedIndexItem *)node; +} + +/* Set the score on a detached item (created by skiplistCreateDetached but not + * yet inserted). Must NOT be used on items that are already in an index — + * doing so would silently corrupt the sort order. Use skiplistUpdateScore + * (orderedIndexUpdateScore) to change the score of an inserted item. */ +void skiplistDetachedSetScore(OrderedIndexItem *item, double score) { + ((zskiplistNode *)item)->score = score; +} + +OrderedIndexItem *skiplistInsertDetached(OrderedIndex *oi, OrderedIndexItem *item) { + zskiplistNode *node = zslInsertNode((zskiplist *)oi, (zskiplistNode *)item); + return (OrderedIndexItem *)node; +} + +unsigned long skiplistDeleteRangeByScore(OrderedIndex *oi, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) { + zskiplist *zsl = (zskiplist *)oi; + zrangespec range = {.min = min, .max = max, .minex = min_ex, .maxex = max_ex}; + zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; + unsigned long removed = 0; + int i; + + x = zslGetHeader(zsl); + for (i = zslGetHeight(zsl) - 1; i >= 0; i--) { + while (x->level[i].forward && !zsetScoreGteMin(x->level[i].forward->score, &range)) + x = x->level[i].forward; + update[i] = x; + } + + /* Current node is the last with score < or <= min. */ + x = x->level[0].forward; + + /* Delete nodes while in range. */ + while (x && zsetScoreLteMax(x->score, &range)) { + zskiplistNode *next = x->level[0].forward; + zslUnlinkNode(zsl, x, update); + if (on_delete) on_delete((OrderedIndexItem *)x, ctx); + zslFreeNode(x); + + removed++; + x = next; + } + return removed; +} + +unsigned long skiplistDeleteRangeByIndex(OrderedIndex *oi, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) { + zskiplist *zsl = (zskiplist *)oi; + zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; + unsigned long traversed = 0, removed = 0; + int i; + + /* Convert 0-based inclusive range to 1-based for internal traversal. */ + start++; + end++; + + x = zslGetHeader(zsl); + for (i = zslGetHeight(zsl) - 1; i >= 0; i--) { + while (x->level[i].forward && (traversed + zslGetNodeSpanAtLevel(x, i)) < start) { + traversed += zslGetNodeSpanAtLevel(x, i); + x = x->level[i].forward; + } + update[i] = x; + } + + traversed++; + x = x->level[0].forward; + while (x && traversed <= end) { + zskiplistNode *next = x->level[0].forward; + zslUnlinkNode(zsl, x, update); + if (on_delete) on_delete((OrderedIndexItem *)x, ctx); + zslFreeNode(x); + + removed++; + traversed++; + x = next; + } + return removed; +} + +unsigned long skiplistDeleteRangeByLex(OrderedIndex *oi, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) { + zskiplist *zsl = (zskiplist *)oi; + zlexrangespec range = {.min = (sds)min, .max = (sds)max, .minex = min_ex, .maxex = max_ex}; + zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; + unsigned long removed = 0; + int i; + + x = zslGetHeader(zsl); + for (i = zslGetHeight(zsl) - 1; i >= 0; i--) { + while (x->level[i].forward) { + sds fwd_ele = zslGetNodeElement(x->level[i].forward); + if (zsetLexGteMin(fwd_ele, sdslen(fwd_ele), &range)) break; + x = x->level[i].forward; + } + update[i] = x; + } + + /* Current node is the last with element < or <= min. */ + x = x->level[0].forward; + + /* Delete nodes while in range. */ + while (x) { + sds ele = zslGetNodeElement(x); + if (!zsetLexLteMax(ele, sdslen(ele), &range)) break; + zskiplistNode *next = x->level[0].forward; + zslUnlinkNode(zsl, x, update); + if (on_delete) on_delete((OrderedIndexItem *)x, ctx); + zslFreeNode(x); + + removed++; + x = next; + } + return removed; +} + +/* Query */ + +unsigned long skiplistLength(OrderedIndex *oi) { + return zslGetLength((zskiplist *)oi); +} + +OrderedIndexItem *skiplistGetByIndex(OrderedIndex *oi, unsigned long index) { + return (OrderedIndexItem *)zslGetElementByRank((zskiplist *)oi, index + 1); +} + +OrderedIndexItem *skiplistGetFirst(OrderedIndex *oi) { + return (OrderedIndexItem *)zslGetFirst((const zskiplist *)oi); +} + +OrderedIndexItem *skiplistGetLast(OrderedIndex *oi) { + return (OrderedIndexItem *)zslGetTail((const zskiplist *)oi); +} + +unsigned long skiplistGetIndex(OrderedIndex *oi, const OrderedIndexItem *node) { + return zslGetRank((zskiplist *)oi, (const zskiplistNode *)node) - 1; +} + +void skiplistGetElementRaw(const OrderedIndexItem *node, const char **ptr, size_t *len) { + const zskiplistNode *znode = (const zskiplistNode *)node; + sds ele = zslGetNodeElement(znode); + *ptr = ele; + *len = sdslen(ele); +} + +double skiplistGetScore(const OrderedIndexItem *node) { + return zslGetScore((const zskiplistNode *)node); +} + +unsigned long skiplistCountScoreRange(OrderedIndex *oi, double min, double max, int min_ex, int max_ex) { + zskiplist *zsl = (zskiplist *)oi; + zrangespec range = {.min = min, .max = max, .minex = min_ex, .maxex = max_ex}; + long first_rank, last_rank; + + /* Find first element in range and its rank. */ + zskiplistNode *first = zslNthInRange(zsl, &range, 0, &first_rank); + if (first == NULL) return 0; + + /* Find last element in range and its rank. */ + zskiplistNode *last_node = zslNthInRange(zsl, &range, -1, &last_rank); + if (last_node == NULL) return 0; + + return (unsigned long)(last_rank - first_rank + 1); +} + +unsigned long skiplistCountLexRange(OrderedIndex *oi, const_sds min, const_sds max, int min_ex, int max_ex) { + zskiplist *zsl = (zskiplist *)oi; + zlexrangespec range = {.min = (sds)min, .max = (sds)max, .minex = min_ex, .maxex = max_ex}; + + /* Find first element in range. */ + zskiplistNode *first = zslNthInLexRange(zsl, &range, 0); + if (first == NULL) return 0; + unsigned long first_rank = zslGetRank(zsl, first); + + /* Find last element in range. */ + zskiplistNode *last_node = zslNthInLexRange(zsl, &range, -1); + if (last_node == NULL) return 0; + unsigned long last_rank = zslGetRank(zsl, last_node); + + return last_rank - first_rank + 1; +} + +/* Iterator */ + +void skiplistInitIterator(OrderedIndexIterator *iter, OrderedIndex *oi) { + zslInitIterator((zslIter *)iter, (zskiplist *)oi); +} + +void skiplistResetIterator(OrderedIndexIterator *iter) { + zslResetIterator((zslIter *)iter); +} + +OrderedIndexItem *skiplistNext(OrderedIndexIterator *iter) { + return (OrderedIndexItem *)zslNext((zslIter *)iter); +} + +OrderedIndexItem *skiplistPrev(OrderedIndexIterator *iter) { + return (OrderedIndexItem *)zslPrev((zslIter *)iter); +} + +void skiplistSeekToIndex(OrderedIndexIterator *iter, unsigned long index) { + zslSeekToRank((zslIter *)iter, index + 1); +} + +void skiplistSeekToScoreRange(OrderedIndexIterator *iter, double min, double max, int min_ex, int max_ex, long offset) { + zslSeekToScoreRange((zslIter *)iter, min, max, min_ex, max_ex, offset); +} + +void skiplistSeekToLexRange(OrderedIndexIterator *iter, const_sds min, const_sds max, int min_ex, int max_ex, long offset) { + zslSeekToLexRange((zslIter *)iter, min, max, min_ex, max_ex, offset); +} + +/* Memory */ + +void skiplistDismissMemory(OrderedIndex *oi) { + zskiplist *zsl = (zskiplist *)oi; + zskiplistNode *zn = zslGetTail(zsl); + while (zn != NULL) { + zskiplistNode *prev = zn->backward; + dismissMemory(zn, 0); + zn = prev; + } +} + +size_t skiplistEstimateMemory(OrderedIndex *oi, size_t sample_size) { + zskiplist *zsl = (zskiplist *)oi; + unsigned long length = zslGetLength(zsl); + size_t asize = zslGetAllocSize(); + + if (length == 0) return asize; + + size_t elesize = 0; + size_t samples = 0; + zskiplistNode *znode = zslGetHeader(zsl)->level[0].forward; + while (znode != NULL && samples < sample_size) { + elesize += zmalloc_size(znode); + samples++; + znode = znode->level[0].forward; + } + if (samples) asize += (double)elesize / samples * length; + return asize; +} + +/* Defrag */ + +OrderedIndex *skiplistDefragInternals(OrderedIndex *oi, void *(*defragfn)(void *)) { + OrderedIndex *new_oi = defragfn(oi); + return new_oi; /* NULL if no move needed */ +} + +/* Patch skiplist pointers after a node has been reallocated to a new address. + * update[] contains the predecessor at each level. */ +static void skiplistPatchNodePointers(zskiplist *zsl, zskiplistNode *oldnode, zskiplistNode *newnode, zskiplistNode **update) { + for (int i = 0; i < zslGetHeight(zsl); i++) { + if (update[i]->level[i].forward == oldnode) + update[i]->level[i].forward = newnode; + } + if (newnode->level[0].forward) { + newnode->level[0].forward->backward = newnode; + } else { + zslSetTail(zsl, newnode); + } +} + +/* Cursor-based incremental defrag of skiplist nodes. + * + * Walks nodes forward from cursor position (a rank), defragging as it + * goes via defragfn. When an item is reallocated there is a callback + * for any other pointer updates needed. + * + * Processes up to 16 nodes per call to bound latency, returning the + * next cursor position (or 0 when complete). */ +unsigned long skiplistScanDefrag(OrderedIndex *oi, unsigned long cursor, OrderedIndexDefragCallback callback, void *ctx, void *(*defragfn)(void *)) { + zskiplist *zsl = (zskiplist *)oi; + zskiplistNode *header = zslGetHeader(zsl); + + /* cursor is the 1-based rank of the next node to process, 0 means start */ + zskiplistNode *prev = header; + if (cursor > 0) { + /* Seek to the node just before cursor position */ + zskiplistNode *target = zslGetElementByRank(zsl, cursor); + if (target == NULL) return 0; /* past end */ + prev = target->backward ? target->backward : header; + } + + zskiplistNode *node = prev->level[0].forward; + unsigned long count = 0; + unsigned long rank = cursor > 0 ? cursor : 1; + + while (node != NULL && count < 16) { + zskiplistNode *next = node->level[0].forward; + zskiplistNode *newnode = defragfn(node); + + if (newnode) { + /* Node was reallocated. Find predecessors and patch pointers. */ + zskiplistNode *update[ZSKIPLIST_MAXLEVEL]; + zskiplistNode *x = header; + double score = newnode->score; + sds ele = zslGetNodeElement(newnode); + for (int i = zslGetHeight(zsl) - 1; i >= 0; i--) { + while (x->level[i].forward && + (x->level[i].forward->score < score || + (x->level[i].forward->score == score && + sdscmp(zslGetNodeElement(x->level[i].forward), ele) < 0))) { + x = x->level[i].forward; + } + update[i] = x; + } + skiplistPatchNodePointers(zsl, node, newnode, update); + callback((OrderedIndexItem *)node, (OrderedIndexItem *)newnode, ctx); + } + + node = next; + rank++; + count++; + } + + return node ? rank : 0; /* 0 = done */ +} + +/* Debug */ + +int skiplistGetHeight(OrderedIndex *oi) { + return zslGetHeight((zskiplist *)oi); +} + +/* Verify the structural integrity of the skiplist. + * Returns 1 if valid, 0 if corrupt (with a description in errmsg). */ +int skiplistVerifyIntegrity(OrderedIndex *oi, char *errmsg, size_t errmsg_len) { + zskiplist *zsl = (zskiplist *)oi; + zskiplistNode *header = zslGetHeader(zsl); + int height = zslGetHeight(zsl); + unsigned long length = zslGetLength(zsl); + +#define FAIL(...) \ + do { \ + snprintf(errmsg, errmsg_len, __VA_ARGS__); \ + return 0; \ + } while (0) + + /* 1. Height must be in [1, ZSKIPLIST_MAXLEVEL]. */ + if (height < 1 || height > ZSKIPLIST_MAXLEVEL) + FAIL("height %d out of range [1, %d]", height, ZSKIPLIST_MAXLEVEL); + + /* 2. All levels above height must have NULL forward from header. */ + for (int i = height; i < ZSKIPLIST_MAXLEVEL; i++) { + if (header->level[i].forward != NULL) + FAIL("header level %d forward is non-NULL above height %d", i, height); + } + + /* 3. Walk level 0 to count nodes, verify ordering, backward pointers, and tail. */ + unsigned long count = 0; + zskiplistNode *prev = NULL; + zskiplistNode *node = header->level[0].forward; + zskiplistNode *last = NULL; + + while (node != NULL) { + count++; + + /* Verify backward pointer. */ + if (node->backward != prev) + FAIL("node at rank %lu: backward pointer mismatch", count); + + /* Verify sort order (score, then element). */ + if (prev != NULL) { + if (node->score < prev->score) + FAIL("node at rank %lu: score %.17g < previous %.17g", count, node->score, prev->score); + if (node->score == prev->score) { + sds prev_ele = zslGetNodeElement(prev); + sds node_ele = zslGetNodeElement(node); + if (sdscmp(node_ele, prev_ele) <= 0) + FAIL("node at rank %lu: element not lexicographically after previous at same score", count); + } + } + + /* Verify node height is in valid range. */ + unsigned long node_height = zslGetNodeHeight(node); + if (node_height < 1 || node_height > (unsigned long)ZSKIPLIST_MAXLEVEL) + FAIL("node at rank %lu: height %lu out of range", count, node_height); + + /* Node height should not exceed skiplist height. */ + if (node_height > (unsigned long)height) + FAIL("node at rank %lu: height %lu exceeds skiplist height %d", count, node_height, height); + + last = node; + prev = node; + node = node->level[0].forward; + } + + /* 4. Verify length. */ + if (count != length) + FAIL("length mismatch: stored %lu, counted %lu", length, count); + + /* 5. Verify tail pointer. */ + zskiplistNode *tail = zslGetTail(zsl); + if (length == 0) { + if (tail != NULL) + FAIL("tail should be NULL for empty skiplist"); + } else { + if (tail != last) + FAIL("tail pointer does not point to last node"); + } + + /* 6. Verify the highest non-empty level matches height. */ + if (length > 0) { + if (header->level[height - 1].forward == NULL) + FAIL("highest level %d has NULL forward but skiplist is non-empty", height - 1); + } + + /* 7. Verify spans at each level. */ + for (int i = 1; i < height; i++) { + unsigned long rank = 0; + zskiplistNode *x = header; + + while (x != NULL) { + zskiplistNode *next_at_level = x->level[i].forward; + unsigned long span = zslGetNodeSpanAtLevel(x, i); + + if (next_at_level == NULL) { + unsigned long remaining = length - rank; + if (span != remaining) + FAIL("level %d: node at rank %lu has span %lu but %lu nodes remain", + i, rank, span, remaining); + break; + } + + if (zslGetNodeHeight(next_at_level) <= (unsigned long)i) + FAIL("level %d: forward node has height %lu, expected > %d", + i, zslGetNodeHeight(next_at_level), i); + + unsigned long actual_span = 0; + zskiplistNode *walk_next = (x == header) ? header->level[0].forward : x->level[0].forward; + while (walk_next != NULL && walk_next != next_at_level) { + actual_span++; + walk_next = walk_next->level[0].forward; + } + actual_span++; + + if (walk_next != next_at_level) + FAIL("level %d: forward pointer from rank %lu does not appear in level-0 chain", i, rank); + + if (span != actual_span) + FAIL("level %d: node at rank %lu has span %lu but actual distance is %lu", + i, rank, span, actual_span); + + rank += span; + x = next_at_level; + } + } + +#undef FAIL + if (errmsg_len > 0) errmsg[0] = '\0'; + return 1; +} diff --git a/src/skiplist_ordered_index.h b/src/skiplist_ordered_index.h new file mode 100644 index 00000000000..e498e2aca28 --- /dev/null +++ b/src/skiplist_ordered_index.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) Valkey Contributors + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef SKIPLIST_ORDERED_INDEX_H +#define SKIPLIST_ORDERED_INDEX_H + +/* Skiplist backend for the OrderedIndex interface. + * + * This file declares the skiplist-specific implementations of all OrderedIndex + * operations. These are called by ordered_index.c (the dispatch layer) and + * should not be called directly by application code. + * + * The skiplist stores (score, element) pairs in a probabilistic balanced + * structure providing O(log N) operations with good cache behavior. */ + +#include "ordered_index.h" + +/* Lifecycle */ +OrderedIndex *skiplistCreate(void); +void skiplistFree(OrderedIndex *oi); + +/* Modification */ +OrderedIndexItem *skiplistInsert(OrderedIndex *oi, double score, const char *ele, size_t len); +void skiplistDelete(OrderedIndex *oi, OrderedIndexItem *item); +OrderedIndexItem *skiplistUpdateScore(OrderedIndex *oi, OrderedIndexItem *item, double newscore); +OrderedIndexItem *skiplistPopFirst(OrderedIndex *oi); +OrderedIndexItem *skiplistPopLast(OrderedIndex *oi); +void skiplistFreeItem(OrderedIndexItem *item); +OrderedIndexItem *skiplistCreateDetached(double score, const char *ele, size_t len); +void skiplistDetachedSetScore(OrderedIndexItem *item, double score); +OrderedIndexItem *skiplistInsertDetached(OrderedIndex *oi, OrderedIndexItem *item); +unsigned long skiplistDeleteRangeByScore(OrderedIndex *oi, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx); +unsigned long skiplistDeleteRangeByIndex(OrderedIndex *oi, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx); +unsigned long skiplistDeleteRangeByLex(OrderedIndex *oi, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx); + +/* Query */ +unsigned long skiplistLength(OrderedIndex *oi); +OrderedIndexItem *skiplistGetByIndex(OrderedIndex *oi, unsigned long rank); +OrderedIndexItem *skiplistGetFirst(OrderedIndex *oi); +OrderedIndexItem *skiplistGetLast(OrderedIndex *oi); +unsigned long skiplistGetIndex(OrderedIndex *oi, const OrderedIndexItem *item); +void skiplistGetElementRaw(const OrderedIndexItem *item, const char **ptr, size_t *len); +double skiplistGetScore(const OrderedIndexItem *item); +unsigned long skiplistCountScoreRange(OrderedIndex *oi, double min, double max, int min_ex, int max_ex); +unsigned long skiplistCountLexRange(OrderedIndex *oi, const_sds min, const_sds max, int min_ex, int max_ex); + +/* Iterator */ +void skiplistInitIterator(OrderedIndexIterator *iter, OrderedIndex *oi); +void skiplistResetIterator(OrderedIndexIterator *iter); +OrderedIndexItem *skiplistNext(OrderedIndexIterator *iter); +OrderedIndexItem *skiplistPrev(OrderedIndexIterator *iter); +void skiplistSeekToIndex(OrderedIndexIterator *iter, unsigned long rank); +void skiplistSeekToScoreRange(OrderedIndexIterator *iter, double min, double max, int min_ex, int max_ex, long offset); +void skiplistSeekToLexRange(OrderedIndexIterator *iter, const_sds min, const_sds max, int min_ex, int max_ex, long offset); + +/* Memory */ +void skiplistDismissMemory(OrderedIndex *oi); +size_t skiplistEstimateMemory(OrderedIndex *oi, size_t sample_size); + +/* Defrag */ +OrderedIndex *skiplistDefragInternals(OrderedIndex *oi, void *(*defragfn)(void *)); +unsigned long skiplistScanDefrag(OrderedIndex *oi, unsigned long cursor, OrderedIndexDefragCallback callback, void *ctx, void *(*defragfn)(void *)); + +/* Debug (used by unit tests and DEBUG command, not part of public API) */ +int skiplistGetHeight(OrderedIndex *oi); +int skiplistVerifyIntegrity(OrderedIndex *oi, char *errmsg, size_t errmsg_len); + +#endif /* SKIPLIST_ORDERED_INDEX_H */ diff --git a/src/sort.c b/src/sort.c index 216b9b89fde..049bd3db41e 100644 --- a/src/sort.c +++ b/src/sort.c @@ -36,8 +36,7 @@ #include "cluster.h" #include "valkey_strtod.h" - -zskiplistNode *zslGetElementByRank(zskiplist *zsl, unsigned long rank); +#include "ordered_index.h" serverSortOperation *createSortOperation(int type, robj *pattern) { serverSortOperation *so = zmalloc(sizeof(*so)); @@ -418,31 +417,33 @@ void sortCommandGeneric(client *c, int readonly) { * way, just getting the required range, as an optimization. */ zset *zs = objectGetVal(sortval); - zskiplist *zsl = zs->zsl; - zskiplistNode *ln; - sds sdsele; + OrderedIndexIterator iter; + orderedIndexInitIterator(&iter, zs->oi); + OrderedIndexItem *ln; int rangelen = vectorlen; /* Check if starting point is trivial, before doing log(N) lookup. */ if (desc) { long zsetlen = hashtableSize(((zset *)objectGetVal(sortval))->ht); - - ln = zslGetTail(zsl); - if (start > 0) ln = zslGetElementByRank(zsl, zsetlen - start); + orderedIndexSeekToIndex(&iter, zsetlen - 1 - start); + ln = orderedIndexPrev(&iter); } else { - zskiplistNode *zheader = zslGetHeader(zsl); - ln = zheader->level[0].forward; - if (start > 0) ln = zslGetElementByRank(zsl, start + 1); + if (start > 0) { + orderedIndexSeekToIndex(&iter, start - 1); + } + ln = orderedIndexNext(&iter); } while (rangelen--) { serverAssertWithInfo(c, sortval, ln != NULL); - sdsele = zslGetNodeElement(ln); - vector[j].obj = createStringObject(sdsele, sdslen(sdsele)); + const char *ele; + size_t ele_len; + orderedIndexGetElementRaw(ln, &ele, &ele_len); + vector[j].obj = createStringObject(ele, ele_len); vector[j].u.score = 0; vector[j].u.cmpobj = NULL; j++; - ln = desc ? ln->backward : ln->level[0].forward; + ln = desc ? orderedIndexPrev(&iter) : orderedIndexNext(&iter); } /* Fix start/end: output code is not aware of this optimization. */ end -= start; @@ -453,9 +454,10 @@ void sortCommandGeneric(client *c, int readonly) { hashtableInitIterator(&iter, ht, 0); void *next; while (hashtableNext(&iter, &next)) { - zskiplistNode *node = next; - sds sdsele = zslGetNodeElement(node); - vector[j].obj = createStringObject(sdsele, sdslen(sdsele)); + const char *ele; + size_t ele_len; + orderedIndexGetElementRaw((const OrderedIndexItem *)next, &ele, &ele_len); + vector[j].obj = createStringObject(ele, ele_len); vector[j].u.score = 0; vector[j].u.cmpobj = NULL; j++; diff --git a/src/t_zset.c b/src/t_zset.c index 00bdf1cd1fc..1c79a171c93 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -40,34 +40,59 @@ * in order to get O(log(N)) INSERT and REMOVE operations into a sorted * data structure. * - * The elements are added to a hash table mapping Objects to scores. - * At the same time the elements are added to a skip list mapping scores - * to Objects (so objects are sorted by scores in this "view"). + * The elements are added to a hash table mapping elements to scores. + * At the same time the elements are added to an ordered index mapping scores + * to elements (so elements are sorted by scores in this "view"). * - * Note that the SDS string representing the element is the same in both - * the hash table and skiplist in order to save memory. What we do in order - * to manage the shared SDS string more easily is to free the SDS string - * only in zslFreeNode(). The dictionary has no value free method set. - * So we should always remove an element from the dictionary, and later from - * the skiplist. - * - * This skiplist implementation is almost a C translation of the original - * algorithm described by William Pugh in "Skip Lists: A Probabilistic - * Alternative to Balanced Trees", modified in three ways: - * a) this implementation allows for repeated scores. - * b) the comparison is not just by key (our 'score') but by satellite data. - * c) there is a back pointer, so it's a doubly linked list with the back - * pointers being only at "level 1". This allows to traverse the list - * from tail to head, useful for ZREVRANGE. */ + * Note that the element string is shared between the hash table and the + * ordered index in order to save memory. The element is freed only in + * orderedIndexFreeItem(). The hash table has no value free method set. + * So we should always remove an element from the hash table, and later from + * the ordered index. */ #include "server.h" -#include "skiplist.h" +#include "ordered_index.h" #include "intset.h" /* Compact integer set structure */ #include "mt19937-64.h" #include #include "valkey_strtod.h" +/*----------------------------------------------------------------------------- + * Zset range comparison utilities + * + * These are generic range-spec helpers used by both listpack and ordered index + * encoded zsets. They have no dependency on any specific data structure. + *----------------------------------------------------------------------------*/ + +int zsetScoreGteMin(double value, zrangespec *spec) { + return spec->minex ? (value > spec->min) : (value >= spec->min); +} + +int zsetScoreLteMax(double value, zrangespec *spec) { + return spec->maxex ? (value < spec->max) : (value <= spec->max); +} + +/* Compare two sds strings handling shared.minstring/maxstring as -inf/+inf. */ +/* Compare a raw element (ptr+len) against an sds range bound. + * Either side may be shared.minstring/maxstring as -inf/+inf sentinels. */ +int zsetLexCompare(const char *a, size_t alen, sds b) { + if ((const char *)a == (const char *)b) return 0; + if (a == shared.minstring || b == shared.maxstring) return -1; + if (a == shared.maxstring || b == shared.minstring) return 1; + int cmp = memcmp(a, b, alen < sdslen(b) ? alen : sdslen(b)); + if (cmp != 0) return cmp; + return alen < sdslen(b) ? -1 : (alen > sdslen(b) ? 1 : 0); +} + +int zsetLexGteMin(const char *value, size_t len, zlexrangespec *spec) { + return spec->minex ? (zsetLexCompare(value, len, spec->min) > 0) : (zsetLexCompare(value, len, spec->min) >= 0); +} + +int zsetLexLteMax(const char *value, size_t len, zlexrangespec *spec) { + return spec->maxex ? (zsetLexCompare(value, len, spec->max) < 0) : (zsetLexCompare(value, len, spec->max) <= 0); +} + void zsetConvertAndExpand(robj *zobj, int encoding, unsigned long cap); static int zslParseRange(robj *min, robj *max, zrangespec *spec) { @@ -278,12 +303,12 @@ int zzlIsInRange(unsigned char *zl, zrangespec *range) { p = lpSeek(zl, -1); /* Last score. */ if (p == NULL) return 0; /* Empty sorted set */ score = zzlGetScore(p); - if (!zslValueGteMin(score, range)) return 0; + if (!zsetScoreGteMin(score, range)) return 0; p = lpSeek(zl, 1); /* First score. */ serverAssert(p != NULL); score = zzlGetScore(p); - if (!zslValueLteMax(score, range)) return 0; + if (!zsetScoreLteMax(score, range)) return 0; return 1; } @@ -302,9 +327,9 @@ unsigned char *zzlFirstInRange(unsigned char *zl, zrangespec *range) { serverAssert(sptr != NULL); score = zzlGetScore(sptr); - if (zslValueGteMin(score, range)) { + if (zsetScoreGteMin(score, range)) { /* Check if score <= max. */ - if (zslValueLteMax(score, range)) return eptr; + if (zsetScoreLteMax(score, range)) return eptr; return NULL; } @@ -329,9 +354,9 @@ unsigned char *zzlLastInRange(unsigned char *zl, zrangespec *range) { serverAssert(sptr != NULL); score = zzlGetScore(sptr); - if (zslValueLteMax(score, range)) { + if (zsetScoreLteMax(score, range)) { /* Check if score >= min. */ - if (zslValueGteMin(score, range)) return eptr; + if (zsetScoreGteMin(score, range)) return eptr; return NULL; } @@ -348,17 +373,29 @@ unsigned char *zzlLastInRange(unsigned char *zl, zrangespec *range) { } int zzlLexValueGteMin(unsigned char *p, zlexrangespec *spec) { - sds value = lpGetObject(p); - int res = zslLexValueGteMin(value, spec); - sdsfree(value); - return res; + unsigned int len; + long long lval; + unsigned char *vstr = lpGetValue(p, &len, &lval); + if (vstr) { + return zsetLexGteMin((char *)vstr, len, spec); + } else { + char buf[LONG_STR_SIZE]; + int blen = ll2string(buf, sizeof(buf), lval); + return zsetLexGteMin(buf, blen, spec); + } } int zzlLexValueLteMax(unsigned char *p, zlexrangespec *spec) { - sds value = lpGetObject(p); - int res = zslLexValueLteMax(value, spec); - sdsfree(value); - return res; + unsigned int len; + long long lval; + unsigned char *vstr = lpGetValue(p, &len, &lval); + if (vstr) { + return zsetLexLteMax((char *)vstr, len, spec); + } else { + char buf[LONG_STR_SIZE]; + int blen = ll2string(buf, sizeof(buf), lval); + return zsetLexLteMax(buf, blen, spec); + } } /* Returns if there is a part of the zset is in range. Should only be used @@ -367,7 +404,7 @@ int zzlIsInLexRange(unsigned char *zl, zlexrangespec *range) { unsigned char *p; /* Test for ranges that will always be empty. */ - int cmp = sdscmplex(range->min, range->max); + int cmp = zsetLexCompare(range->min, sdslen(range->min), range->max); if (cmp > 0 || (cmp == 0 && (range->minex || range->maxex))) return 0; p = lpSeek(zl, -2); /* Last element. */ @@ -455,7 +492,7 @@ static unsigned char *zzlDelete(unsigned char *zl, unsigned char *eptr) { return lpDeleteRangeWithEntry(zl, &eptr, 2); } -static unsigned char *zzlInsertAt(unsigned char *zl, unsigned char *eptr, sds ele, double score) { +static unsigned char *zzlInsertAt(unsigned char *zl, unsigned char *eptr, const char *ele, size_t ele_len, double score) { unsigned char *sptr; char scorebuf[MAX_D2STRING_CHARS]; int scorelen = 0; @@ -463,14 +500,14 @@ static unsigned char *zzlInsertAt(unsigned char *zl, unsigned char *eptr, sds el int score_is_long = double2ll(score, &lscore); if (!score_is_long) scorelen = d2string(scorebuf, sizeof(scorebuf), score); if (eptr == NULL) { - zl = lpAppend(zl, (unsigned char *)ele, sdslen(ele)); + zl = lpAppend(zl, (unsigned char *)ele, ele_len); if (score_is_long) zl = lpAppendInteger(zl, lscore); else zl = lpAppend(zl, (unsigned char *)scorebuf, scorelen); } else { /* Insert member before the element 'eptr'. */ - zl = lpInsertString(zl, (unsigned char *)ele, sdslen(ele), eptr, LP_BEFORE, &sptr); + zl = lpInsertString(zl, (unsigned char *)ele, ele_len, eptr, LP_BEFORE, &sptr); /* Insert score after the member. */ if (score_is_long) @@ -496,12 +533,12 @@ static unsigned char *zzlInsert(unsigned char *zl, sds ele, double score) { /* First element with score larger than score for element to be * inserted. This means we should take its spot in the list to * maintain ordering. */ - zl = zzlInsertAt(zl, eptr, ele, score); + zl = zzlInsertAt(zl, eptr, ele, sdslen(ele), score); break; } else if (s == score) { /* Ensure lexicographical ordering for elements. */ if (zzlCompareElements(eptr, (unsigned char *)ele, sdslen(ele)) > 0) { - zl = zzlInsertAt(zl, eptr, ele, score); + zl = zzlInsertAt(zl, eptr, ele, sdslen(ele), score); break; } } @@ -511,7 +548,7 @@ static unsigned char *zzlInsert(unsigned char *zl, sds ele, double score) { } /* Push on tail of list when it was not yet inserted. */ - if (eptr == NULL) zl = zzlInsertAt(zl, NULL, ele, score); + if (eptr == NULL) zl = zzlInsertAt(zl, NULL, ele, sdslen(ele), score); return zl; } @@ -528,7 +565,7 @@ static unsigned char *zzlDeleteRangeByScore(unsigned char *zl, zrangespec *range /* When the tail of the listpack is deleted, eptr will be NULL. */ while (eptr && (sptr = lpNext(zl, eptr)) != NULL) { score = zzlGetScore(sptr); - if (zslValueLteMax(score, range)) { + if (zsetScoreLteMax(score, range)) { /* Delete both the element and the score. */ zl = lpDeleteRangeWithEntry(zl, &eptr, 2); num++; @@ -567,7 +604,7 @@ static unsigned char *zzlDeleteRangeByLex(unsigned char *zl, zlexrangespec *rang return zl; } -/* Delete all the elements with rank between start and end from the skiplist. +/* Delete all the elements with rank between start and end from the listpack. * Start and end are inclusive. Note that start and end need to be 1-based */ unsigned char *zzlDeleteRangeByRank(unsigned char *zl, unsigned int start, unsigned int end, unsigned long *deleted) { unsigned int num = (end - start) + 1; @@ -585,7 +622,7 @@ unsigned long zsetLength(const robj *zobj) { if (zobj->encoding == OBJ_ENCODING_LISTPACK) { length = zzlLength(objectGetVal(zobj)); } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { - length = zslGetLength(((const zset *)objectGetVal(zobj))->zsl); + length = orderedIndexLength(((const zset *)objectGetVal(zobj))->oi); } else { serverPanic("Unknown sorted set encoding"); } @@ -620,9 +657,9 @@ void zsetTypeMaybeConvert(robj *zobj, size_t size_hint, size_t value_len_hint) { } } -/* Convert the zset to specified encoding. The zset dict (when converting - * to a skiplist) is presized to hold the number of elements in the original - * zset. */ +/* Convert the zset to specified encoding. The hashtable (when converting + * to ordered index encoding) is presized to hold the number of elements in + * the original zset. */ void zsetConvert(robj *zobj, int encoding) { zsetConvertAndExpand(zobj, encoding, zsetLength(zobj)); } @@ -630,8 +667,7 @@ void zsetConvert(robj *zobj, int encoding) { /* Converts a zset to the specified encoding, pre-sizing it for 'cap' elements. */ void zsetConvertAndExpand(robj *zobj, int encoding, unsigned long cap) { zset *zs; - zskiplistNode *node, *next; - sds ele; + OrderedIndexItem *node; double score; if (zobj->encoding == encoding) return; @@ -646,7 +682,7 @@ void zsetConvertAndExpand(robj *zobj, int encoding, unsigned long cap) { zs = zmalloc(sizeof(*zs)); zs->ht = hashtableCreate(&zsetHashtableType); - zs->zsl = zslCreate(); + zs->oi = orderedIndexCreate(); /* Presize the dict to avoid rehashing */ hashtableExpand(zs->ht, cap); @@ -660,13 +696,18 @@ void zsetConvertAndExpand(robj *zobj, int encoding, unsigned long cap) { while (eptr != NULL) { score = zzlGetScore(sptr); vstr = lpGetValue(eptr, &vlen, &vlong); - if (vstr == NULL) - ele = sdsfromlonglong(vlong); - else - ele = sdsnewlen((char *)vstr, vlen); + char buf[LONG_STR_SIZE]; + const char *ele_ptr; + size_t ele_len; + if (vstr == NULL) { + ele_len = ll2string(buf, sizeof(buf), vlong); + ele_ptr = buf; + } else { + ele_ptr = (char *)vstr; + ele_len = vlen; + } - node = zslInsert(zs->zsl, score, ele); - sdsfree(ele); + node = orderedIndexInsert(zs->oi, score, ele_ptr, ele_len); serverAssert(hashtableAdd(zs->ht, node)); zzlNext(zl, &eptr, &sptr); } @@ -679,20 +720,19 @@ void zsetConvertAndExpand(robj *zobj, int encoding, unsigned long cap) { if (encoding != OBJ_ENCODING_LISTPACK) serverPanic("Unknown target encoding"); - /* Approach similar to zslFree(), since we want to free the skiplist at - * the same time as creating the listpack. */ + /* Free the ordered index by popping items one at a time into the listpack. */ zs = objectGetVal(zobj); hashtableRelease(zs->ht); - zskiplistNode *zheader = zslGetHeader(zs->zsl); - node = zheader->level[0].forward; - zfree(zs->zsl); - while (node) { - zl = zzlInsertAt(zl, NULL, zslGetNodeElement(node), node->score); - next = node->level[0].forward; - zslFreeNode(node); - node = next; + OrderedIndexItem *node; + while ((node = orderedIndexPopFirst(zs->oi)) != NULL) { + const char *ele_ptr; + size_t ele_len; + orderedIndexGetElementRaw(node, &ele_ptr, &ele_len); + zl = zzlInsertAt(zl, NULL, ele_ptr, ele_len, orderedIndexGetScore(node)); + orderedIndexFreeItem(node); } + orderedIndexFree(zs->oi); zfree(zs); objectSetVal(zobj, zl); @@ -709,7 +749,7 @@ void zsetConvertToListpackIfNeeded(robj *zobj, size_t maxelelen, size_t totelele if (zobj->encoding == OBJ_ENCODING_LISTPACK) return; zset *zset = objectGetVal(zobj); - if (zslGetLength(zset->zsl) <= server.zset_max_listpack_entries && + if (orderedIndexLength(zset->oi) <= server.zset_max_listpack_entries && maxelelen <= server.zset_max_listpack_value && lpSafeToAdd(NULL, totelelen)) { zsetConvert(zobj, OBJ_ENCODING_LISTPACK); } @@ -728,8 +768,8 @@ int zsetScore(robj *zobj, sds member, double *score) { zset *zs = objectGetVal(zobj); void *entry; if (!hashtableFind(zs->ht, member, &entry)) return C_ERR; - zskiplistNode *setElement = entry; - *score = setElement->score; + OrderedIndexItem *setElement = entry; + *score = orderedIndexGetScore(setElement); } else { serverPanic("Unknown sorted set encoding"); } @@ -775,7 +815,7 @@ int zsetScore(robj *zobj, sds member, double *score) { * start. * * The command as a side effect of adding a new element may convert the sorted - * set internal encoding from listpack to hashtable+skiplist. + * set internal encoding from listpack to hashtable+ordered index. * * Memory management of 'ele': * @@ -851,7 +891,7 @@ int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, dou } /* Note that the above block handling listpack would have either returned or - * converted the key to skiplist. */ + * converted the key to ordered index encoding. */ if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(zobj); @@ -863,8 +903,8 @@ int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, dou return 1; } - zskiplistNode *old_node = *node_ref_in_hashtable; - curscore = old_node->score; + OrderedIndexItem *old_node = *node_ref_in_hashtable; + curscore = orderedIndexGetScore(old_node); /* Prepare the score for the increment if needed. */ if (incr) { @@ -885,15 +925,14 @@ int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, dou /* Remove and re-insert when score changes. */ if (score != curscore) { - zskiplistNode *new_node = zslUpdateScore(zs->zsl, old_node, score); - /* Note that this assignment updates the node pointer stored in - * the hashtable */ - if (new_node) *node_ref_in_hashtable = new_node; + OrderedIndexItem *new_node = orderedIndexUpdateScore(zs->oi, old_node, score); + /* Update the node pointer stored in the hashtable. */ + *node_ref_in_hashtable = new_node; *out_flags |= ZADD_OUT_UPDATED; } return 1; } else if (!xx) { - zskiplistNode *new_node = zslInsert(zs->zsl, score, ele); + OrderedIndexItem *new_node = orderedIndexInsert(zs->oi, score, ele, sdslen(ele)); serverAssert(hashtableAdd(zs->ht, new_node)); *out_flags |= ZADD_OUT_ADDED; if (newscore) *newscore = score; @@ -908,18 +947,18 @@ int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, dou return 0; /* Never reached. */ } -/* Deletes the element 'ele' from the sorted set encoded as a skiplist+hashtable, +/* Deletes the element 'ele' from the sorted set encoded as ordered index+hashtable, * returning 1 if the element existed and was deleted, 0 otherwise (the * element was not there). */ -static int zsetRemoveFromSkiplist(zset *zs, sds ele) { +static int zsetRemoveFromIndex(zset *zs, sds ele) { void *entry; if (!hashtablePop(zs->ht, ele, &entry)) return 0; - zskiplistNode *node = entry; + OrderedIndexItem *node = entry; - /* hashtable only contains pointers to skiplist nodes. Nothing to free. */ + /* hashtable only contains pointers to ordered index items. Nothing to free. */ - /* Delete from skiplist. */ - zslDelete(zs->zsl, node); + /* Delete from ordered index. */ + orderedIndexDelete(zs->oi, node); return 1; } @@ -936,7 +975,7 @@ int zsetDel(robj *zobj, sds ele) { } } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(zobj); - if (zsetRemoveFromSkiplist(zs, ele)) { + if (zsetRemoveFromIndex(zs, ele)) { return 1; } } else { @@ -992,16 +1031,14 @@ static long zsetRank(robj *zobj, sds ele, int reverse, double *output_score) { void *entry; if (!hashtableFind(zs->ht, ele, &entry)) return -1; - zskiplistNode *node = entry; + OrderedIndexItem *node = entry; - rank = zslGetRank(zs->zsl, node); - /* Existing elements always have a rank. */ - serverAssert(rank != 0); - if (output_score) *output_score = node->score; + rank = orderedIndexGetIndex(zs->oi, node); + if (output_score) *output_score = orderedIndexGetScore(node); if (reverse) - return llen - rank; + return llen - rank - 1; else - return rank - 1; + return rank; } else { serverPanic("Unknown sorted set encoding"); } @@ -1032,23 +1069,20 @@ robj *zsetDup(robj *o) { zs = objectGetVal(o); new_zs = objectGetVal(zobj); hashtableExpand(new_zs->ht, hashtableSize(zs->ht)); - zskiplist *zsl = zs->zsl; - zskiplistNode *ln; - sds ele; - long llen = zsetLength(o); - - /* We copy the skiplist elements from the greatest to the - * smallest (that's trivial since the elements are already ordered in - * the skiplist): this improves the load process, since the next loaded - * element will always be the smaller, so adding to the skiplist - * will always immediately stop at the head, making the insertion - * O(1) instead of O(log(N)). */ - ln = zslGetTail(zsl); - while (llen--) { - ele = zslGetNodeElement(ln); - zskiplistNode *znode = zslInsert(new_zs->zsl, ln->score, ele); + OrderedIndex *oi = zs->oi; + OrderedIndexItem *ln; + + /* We copy elements from the greatest to the smallest (that's trivial + * since the elements are already ordered in the index): inserting in + * descending order is optimal for the ordered index backend. */ + OrderedIndexIterator iter; + orderedIndexInitIterator(&iter, oi); + while ((ln = orderedIndexPrev(&iter)) != NULL) { + const char *ele_ptr; + size_t ele_len; + orderedIndexGetElementRaw(ln, &ele_ptr, &ele_len); + OrderedIndexItem *znode = orderedIndexInsert(new_zs->oi, orderedIndexGetScore(ln), ele_ptr, ele_len); hashtableAdd(new_zs->ht, znode); - ln = ln->backward; } } else { serverPanic("Unknown sorted set encoding"); @@ -1079,11 +1113,13 @@ static void zsetTypeRandomElement(robj *zsetobj, unsigned long zsetsize, listpac zset *zs = objectGetVal(zsetobj); void *entry; hashtableFairRandomEntry(zs->ht, &entry); - zskiplistNode *node = entry; - sds ele = zslGetNodeElement(node); - key->sval = (unsigned char *)ele; - key->slen = sdslen(ele); - if (score) *score = node->score; + OrderedIndexItem *node = entry; + const char *ele_ptr_tmp; + size_t ele_len_tmp; + orderedIndexGetElementRaw(node, &ele_ptr_tmp, &ele_len_tmp); + key->sval = (unsigned char *)ele_ptr_tmp; + key->slen = ele_len_tmp; + if (score) *score = orderedIndexGetScore(node); } else if (zsetobj->encoding == OBJ_ENCODING_LISTPACK) { listpackEntry val; lpRandomPair(objectGetVal(zsetobj), zsetsize, key, &val); @@ -1281,6 +1317,16 @@ typedef enum { ZRANGE_LEX, } zrange_type; +/* Callback for orderedIndexDeleteRangeBy* — removes the item from the hashtable + * and frees it. The callback receives ownership per the API contract. */ +static void zsetIndexDeleteCallback(OrderedIndexItem *item, void *ctx) { + hashtable *ht = ctx; + const char *ptr; + size_t len; + orderedIndexGetElementRaw(item, &ptr, &len); + hashtableDelete(ht, (sds)ptr); +} + /* Implements ZREMRANGEBYRANK, ZREMRANGEBYSCORE, ZREMRANGEBYLEX commands. */ void zremrangeGenericCommand(client *c, zrange_type rangetype) { robj *key = c->argv[1]; @@ -1350,9 +1396,9 @@ void zremrangeGenericCommand(client *c, zrange_type rangetype) { hashtablePauseAutoShrink(zs->ht); switch (rangetype) { case ZRANGE_AUTO: - case ZRANGE_RANK: deleted = zslDeleteRangeByRank(zs->zsl, start + 1, end + 1, zs->ht); break; - case ZRANGE_SCORE: deleted = zslDeleteRangeByScore(zs->zsl, &range, zs->ht); break; - case ZRANGE_LEX: deleted = zslDeleteRangeByLex(zs->zsl, &lexrange, zs->ht); break; + case ZRANGE_RANK: deleted = orderedIndexDeleteRangeByIndex(zs->oi, start, end, zsetIndexDeleteCallback, zs->ht); break; + case ZRANGE_SCORE: deleted = orderedIndexDeleteRangeByScore(zs->oi, range.min, range.max, range.minex, range.maxex, zsetIndexDeleteCallback, zs->ht); break; + case ZRANGE_LEX: deleted = orderedIndexDeleteRangeByLex(zs->oi, lexrange.min, lexrange.max, lexrange.minex, lexrange.maxex, zsetIndexDeleteCallback, zs->ht); break; } hashtableResumeAutoShrink(zs->ht); if (hashtableSize(zs->ht) == 0) { @@ -1418,7 +1464,8 @@ typedef struct { } zl; struct { zset *zs; - zskiplistNode *node; + OrderedIndexIterator iter; + OrderedIndexItem *node; } sl; } zset; } iter; @@ -1479,7 +1526,8 @@ static void zuiInitIterator(zsetopsrc *op) { } } else if (op->encoding == OBJ_ENCODING_SKIPLIST) { it->sl.zs = objectGetVal(op->subject); - it->sl.node = zslGetTail(it->sl.zs->zsl); + orderedIndexInitIterator(&it->sl.iter, it->sl.zs->oi); + it->sl.node = NULL; } else { serverPanic("Unknown sorted set encoding"); } @@ -1534,7 +1582,7 @@ static unsigned long zuiLength(zsetopsrc *op) { return zzlLength(objectGetVal(op->subject)); } else if (op->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(op->subject); - return zslGetLength(zs->zsl); + return orderedIndexLength(zs->oi); } else { serverPanic("Unknown sorted set encoding"); } @@ -1590,12 +1638,14 @@ static int zuiNext(zsetopsrc *op, zsetopval *val) { /* Move to next element (going backwards, see zuiInitIterator). */ zzlPrev(it->zl.zl, &it->zl.eptr, &it->zl.sptr); } else if (op->encoding == OBJ_ENCODING_SKIPLIST) { + it->sl.node = orderedIndexPrev(&it->sl.iter); if (it->sl.node == NULL) return 0; - val->ele = zslGetNodeElement(it->sl.node); - val->score = it->sl.node->score; - - /* Move to next element. (going backwards, see zuiInitIterator) */ - it->sl.node = it->sl.node->backward; + const char *val_ele_ptr; + size_t val_ele_len; + orderedIndexGetElementRaw(it->sl.node, &val_ele_ptr, &val_ele_len); + val->estr = (unsigned char *)val_ele_ptr; + val->elen = val_ele_len; + val->score = orderedIndexGetScore(it->sl.node); } else { serverPanic("Unknown sorted set encoding"); } @@ -1663,8 +1713,8 @@ static int zuiFind(zsetopsrc *op, zsetopval *val, double *score) { zset *zs = objectGetVal(op->subject); void *entry; if (hashtableFind(zs->ht, val->ele, &entry)) { - zskiplistNode *node = entry; - *score = node->score; + OrderedIndexItem *node = entry; + *score = orderedIndexGetScore(node); return 1; } else { return 0; @@ -1718,11 +1768,12 @@ static size_t zsetHashtableGetMaxElementLength(hashtable *ht, size_t *totallen) hashtableInitIterator(&iter, ht, 0); void *next; while (hashtableNext(&iter, &next)) { - zskiplistNode *node = next; - sds ele = zslGetNodeElement(node); - size_t elelen = sdslen(ele); - if (elelen > maxelelen) maxelelen = elelen; - if (totallen) (*totallen) += elelen; + OrderedIndexItem *node = next; + const char *ele_ptr_tmp; + size_t ele_len_tmp; + orderedIndexGetElementRaw(node, &ele_ptr_tmp, &ele_len_tmp); + if (ele_len_tmp > maxelelen) maxelelen = ele_len_tmp; + if (totallen) (*totallen) += ele_len_tmp; } hashtableCleanupIterator(&iter); @@ -1745,7 +1796,7 @@ static void zdiffAlgorithm1(zsetopsrc *src, long setnum, zset *dstzset, size_t * * The final complexity of this algorithm is O(N*M + K*log(K)). */ int j; zsetopval zval; - zskiplistNode *znode; + OrderedIndexItem *znode; sds tmp; /* With algorithm 1 it is better to order the sets to subtract @@ -1773,7 +1824,7 @@ static void zdiffAlgorithm1(zsetopsrc *src, long setnum, zset *dstzset, size_t * if (!exists) { tmp = zuiNewSdsFromValue(&zval); - znode = zslInsert(dstzset->zsl, zval.score, tmp); + znode = orderedIndexInsert(dstzset->oi, zval.score, tmp, sdslen(tmp)); hashtableAdd(dstzset->ht, znode); if (sdslen(tmp) > *maxelelen) *maxelelen = sdslen(tmp); (*totelelen) += sdslen(tmp); @@ -1794,7 +1845,7 @@ static void zdiffAlgorithm2(zsetopsrc *src, long setnum, zset *dstzset, size_t * * This is O(L + (N-K)log(N)) where L is the sum of all the elements in every * set, N is the size of the first set, and K is the size of the result set. * - * Note that from the (L-N) dict searches, (N-K) got to the zsetRemoveFromSkiplist + * Note that from the (L-N) dict searches, (N-K) got to the zsetRemoveFromIndex * which costs log(N) * * There is also a O(K) cost at the end for finding the largest element @@ -1803,7 +1854,7 @@ static void zdiffAlgorithm2(zsetopsrc *src, long setnum, zset *dstzset, size_t * int j; int cardinality = 0; zsetopval zval; - zskiplistNode *znode; + OrderedIndexItem *znode; sds tmp; hashtablePauseAutoShrink(dstzset->ht); @@ -1815,13 +1866,13 @@ static void zdiffAlgorithm2(zsetopsrc *src, long setnum, zset *dstzset, size_t * while (zuiNext(&src[j], &zval)) { if (j == 0) { tmp = zuiNewSdsFromValue(&zval); - znode = zslInsert(dstzset->zsl, zval.score, tmp); + znode = orderedIndexInsert(dstzset->oi, zval.score, tmp, sdslen(tmp)); sdsfree(tmp); hashtableAdd(dstzset->ht, znode); cardinality++; } else { tmp = zuiSdsFromValue(&zval); - if (zsetRemoveFromSkiplist(dstzset, tmp)) { + if (zsetRemoveFromIndex(dstzset, tmp)) { cardinality--; } } @@ -2072,7 +2123,7 @@ static void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIn } } else if (j == setnum) { tmp = zuiNewSdsFromValue(&zval); - zskiplistNode *znode = zslInsert(dstzset->zsl, score, tmp); + OrderedIndexItem *znode = orderedIndexInsert(dstzset->oi, score, tmp, sdslen(tmp)); hashtableAdd(dstzset->ht, znode); totelelen += sdslen(tmp); if (sdslen(tmp) > maxelelen) maxelelen = sdslen(tmp); @@ -2083,8 +2134,8 @@ static void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIn } } else if (op == SET_OP_UNION) { /* Step 1: Create the hash table first by iterating one sorted set after - * the other. We wait to create the skiplist until scores/ordering are - * finalized. */ + * the other. We wait to create the ordered index until scores/ordering + * are finalized. */ if (setnum) { /* Our union is at least as large as the largest set. * Resize the dictionary ASAP to avoid useless rehashing. */ @@ -2106,35 +2157,39 @@ static void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIn void *existing; if (hashtableFindPositionForInsert(dstzset->ht, sdsval, &position, &existing)) { sds tmp_ele = zuiNewSdsFromValue(&zval); - zskiplistNode *new_node = zslCreateNode(zslRandomLevel(), score, tmp_ele); + OrderedIndexItem *new_node = orderedIndexCreateDetached(score, tmp_ele, sdslen(tmp_ele)); sdsfree(tmp_ele); hashtableInsertAtPosition(dstzset->ht, new_node, &position); /* Remember the longest single element encountered, * to understand if it's possible to convert to listpack * at the end. */ - sds ele = zslGetNodeElement(new_node); - totelelen += sdslen(ele); - if (sdslen(ele) > maxelelen) { - maxelelen = sdslen(ele); + const char *ele_ptr_tmp; + size_t ele_len_tmp; + orderedIndexGetElementRaw(new_node, &ele_ptr_tmp, &ele_len_tmp); + totelelen += ele_len_tmp; + if (ele_len_tmp > maxelelen) { + maxelelen = ele_len_tmp; } } else { /* Update the score with the score of the new instance * of the element found in the current sorted set. */ - zskiplistNode *node = existing; - zunionInterAggregate(&node->score, score, aggregate); + OrderedIndexItem *node = existing; + double cur = orderedIndexGetScore(node); + zunionInterAggregate(&cur, score, aggregate); + orderedIndexDetachedSetScore(node, cur); } } zuiClearIterator(&src[i]); } - /* Step 2: Create the skiplist using final score ordering */ + /* Step 2: Insert all detached items into the ordered index */ hashtableIterator iter; hashtableInitIterator(&iter, dstzset->ht, 0); void *next; while (hashtableNext(&iter, &next)) { - zskiplistNode *node = next; - zslInsertNode(dstzset->zsl, node); + OrderedIndexItem *node = next; + orderedIndexInsertDetached(dstzset->oi, node); } hashtableCleanupIterator(&iter); } else if (op == SET_OP_DIFF) { @@ -2144,7 +2199,7 @@ static void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIn } if (dstkey) { - if (zslGetLength(dstzset->zsl)) { + if (orderedIndexLength(dstzset->oi)) { zsetConvertToListpackIfNeeded(dstobj, maxelelen, totelelen); setKey(c, c->db, dstkey, &dstobj, 0); notifyKeyspaceEvent(NOTIFY_ZSET, (op == SET_OP_UNION) ? "zunionstore" : (op == SET_OP_INTER ? "zinterstore" : "zdiffstore"), @@ -2163,10 +2218,10 @@ static void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIn } else if (cardinality_only) { addReplyLongLong(c, cardinality); } else { - unsigned long length = zslGetLength(dstzset->zsl); - zskiplist *zsl = dstzset->zsl; - zskiplistNode *zheader = zslGetHeader(zsl); - zskiplistNode *zn = zheader->level[0].forward; + unsigned long length = orderedIndexLength(dstzset->oi); + OrderedIndex *oi = dstzset->oi; + OrderedIndexIterator iter; + orderedIndexInitIterator(&iter, oi); /* In case of WITHSCORES, respond with a single array in RESP2, and * nested arrays in RESP3. We can't use a map response type since the * client library needs to know to respect the order. */ @@ -2175,12 +2230,14 @@ static void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIn else addReplyArrayLen(c, length); - while (zn != NULL) { + OrderedIndexItem *zn; + while ((zn = orderedIndexNext(&iter)) != NULL) { if (withscores && c->resp > 2) addReplyArrayLen(c, 2); - sds ele = zslGetNodeElement(zn); - addReplyBulkCBuffer(c, ele, sdslen(ele)); - if (withscores) addReplyDouble(c, zn->score); - zn = zn->level[0].forward; + const char *ele_ptr; + size_t ele_len; + orderedIndexGetElementRaw(zn, &ele_ptr, &ele_len); + addReplyBulkCBuffer(c, ele_ptr, ele_len); + if (withscores) addReplyDouble(c, orderedIndexGetScore(zn)); } server.lazyfree_lazy_server_del ? freeObjAsync(NULL, dstobj, -1) : decrRefCount(dstobj); } @@ -2465,24 +2522,26 @@ void genericZrangebyrankCommand(zrange_result_handler *handler, } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(zobj); - zskiplist *zsl = zs->zsl; - zskiplistNode *ln; + OrderedIndex *oi = zs->oi; + OrderedIndexItem *ln; + OrderedIndexIterator iter; + orderedIndexInitIterator(&iter, oi); - /* Check if starting point is trivial, before doing log(N) lookup. */ + /* Seek to starting position */ if (reverse) { - ln = zslGetTail(zsl); - if (start > 0) ln = zslGetElementByRank(zsl, llen - start); + unsigned long seek_idx = (start > 0) ? (unsigned long)(llen - start - 1) : orderedIndexLength(oi) - 1; + orderedIndexSeekToIndex(&iter, seek_idx); } else { - zskiplistNode *zheader = zslGetHeader(zsl); - ln = zheader->level[0].forward; - if (start > 0) ln = zslGetElementByRank(zsl, start + 1); + if (start > 0) orderedIndexSeekToIndex(&iter, (unsigned long)(start - 1)); } while (rangelen--) { + ln = reverse ? orderedIndexPrev(&iter) : orderedIndexNext(&iter); serverAssertWithInfo(c, zobj, ln != NULL); - sds ele = zslGetNodeElement(ln); - handler->emitResultFromCBuffer(handler, ele, sdslen(ele), ln->score); - ln = reverse ? ln->backward : ln->level[0].forward; + const char *ele_ptr; + size_t ele_len; + orderedIndexGetElementRaw(ln, &ele_ptr, &ele_len); + handler->emitResultFromCBuffer(handler, ele_ptr, ele_len, orderedIndexGetScore(ln)); } } else { serverPanic("Unknown sorted set encoding"); @@ -2563,9 +2622,9 @@ void genericZrangebyscoreCommand(zrange_result_handler *handler, /* Abort when the node is no longer in range. */ if (reverse) { - if (!zslValueGteMin(score, range)) break; + if (!zsetScoreGteMin(score, range)) break; } else { - if (!zslValueLteMax(score, range)) break; + if (!zsetScoreLteMax(score, range)) break; } vstr = lpGetValue(eptr, &vlen, &vlong); @@ -2585,34 +2644,29 @@ void genericZrangebyscoreCommand(zrange_result_handler *handler, } } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(zobj); - zskiplist *zsl = zs->zsl; - zskiplistNode *ln; + OrderedIndex *oi = zs->oi; + OrderedIndexItem *ln; + OrderedIndexIterator iter; + orderedIndexInitIterator(&iter, oi); - /* If reversed, get the last node in range as starting point. */ - if (reverse) { - ln = zslNthInRange(zsl, range, -offset - 1, NULL); - } else { - ln = zslNthInRange(zsl, range, offset, NULL); - } + /* Seek to position within score range */ + orderedIndexSeekToScoreRange(&iter, range->min, range->max, range->minex, range->maxex, reverse ? -offset - 1 : offset); - while (ln && limit--) { + while (limit--) { + ln = reverse ? orderedIndexPrev(&iter) : orderedIndexNext(&iter); + if (ln == NULL) break; /* Abort when the node is no longer in range. */ if (reverse) { - if (!zslValueGteMin(ln->score, range)) break; + if (!zsetScoreGteMin(orderedIndexGetScore(ln), range)) break; } else { - if (!zslValueLteMax(ln->score, range)) break; + if (!zsetScoreLteMax(orderedIndexGetScore(ln), range)) break; } rangelen++; - sds ele = zslGetNodeElement(ln); - handler->emitResultFromCBuffer(handler, ele, sdslen(ele), ln->score); - - /* Move to next node */ - if (reverse) { - ln = ln->backward; - } else { - ln = ln->level[0].forward; - } + const char *ele_ptr; + size_t ele_len; + orderedIndexGetElementRaw(ln, &ele_ptr, &ele_len); + handler->emitResultFromCBuffer(handler, ele_ptr, ele_len, orderedIndexGetScore(ln)); } } else { serverPanic("Unknown sorted set encoding"); @@ -2667,14 +2721,14 @@ void zcountCommand(client *c) { /* First element is in range */ sptr = lpNext(zl, eptr); score = zzlGetScore(sptr); - serverAssertWithInfo(c, zobj, zslValueLteMax(score, &range)); + serverAssertWithInfo(c, zobj, zsetScoreLteMax(score, &range)); /* Iterate over elements in range */ while (eptr) { score = zzlGetScore(sptr); /* Abort when the node is no longer in range. */ - if (!zslValueLteMax(score, &range)) { + if (!zsetScoreLteMax(score, &range)) { break; } else { count++; @@ -2683,25 +2737,9 @@ void zcountCommand(client *c) { } } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(zobj); - zskiplist *zsl = zs->zsl; - zskiplistNode *zn; - long rank; + OrderedIndex *oi = zs->oi; - /* Find first element in range */ - zn = zslNthInRange(zsl, &range, 0, &rank); - - /* Use rank of first element, if any, to determine preliminary count */ - if (zn != NULL) { - count = (zslGetLength(zsl) - (rank - 1)); - - /* Find last element in range */ - zn = zslNthInRange(zsl, &range, -1, &rank); - - /* Use rank of last element, if any, to determine the actual count */ - if (zn != NULL) { - count -= (zslGetLength(zsl) - rank); - } - } + count = orderedIndexCountScoreRange(oi, range.min, range.max, range.minex, range.maxex); } else { serverPanic("Unknown sorted set encoding"); } @@ -2757,27 +2795,9 @@ void zlexcountCommand(client *c) { } } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(zobj); - zskiplist *zsl = zs->zsl; - zskiplistNode *zn; - unsigned long rank; - - /* Find first element in range */ - zn = zslNthInLexRange(zsl, &range, 0); - - /* Use rank of first element, if any, to determine preliminary count */ - if (zn != NULL) { - rank = zslGetRank(zsl, zn); - count = (zslGetLength(zsl) - (rank - 1)); - - /* Find last element in range */ - zn = zslNthInLexRange(zsl, &range, -1); + OrderedIndex *oi = zs->oi; - /* Use rank of last element, if any, to determine the actual count */ - if (zn != NULL) { - rank = zslGetRank(zsl, zn); - count -= (zslGetLength(zsl) - rank); - } - } + count = orderedIndexCountLexRange(oi, range.min, range.max, range.minex, range.maxex); } else { serverPanic("Unknown sorted set encoding"); } @@ -2854,34 +2874,29 @@ void genericZrangebylexCommand(zrange_result_handler *handler, } } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(zobj); - zskiplist *zsl = zs->zsl; - zskiplistNode *ln; + OrderedIndex *oi = zs->oi; + OrderedIndexItem *ln; + OrderedIndexIterator iter; + orderedIndexInitIterator(&iter, oi); - /* If reversed, get the last node in range as starting point. */ - if (reverse) { - ln = zslNthInLexRange(zsl, range, -offset - 1); - } else { - ln = zslNthInLexRange(zsl, range, offset); - } + /* Seek to position within lex range */ + orderedIndexSeekToLexRange(&iter, range->min, range->max, range->minex, range->maxex, reverse ? -offset - 1 : offset); - while (ln && limit--) { + while (limit--) { + ln = reverse ? orderedIndexPrev(&iter) : orderedIndexNext(&iter); + if (ln == NULL) break; /* Abort when the node is no longer in range. */ - sds ele = zslGetNodeElement(ln); + const char *ele_ptr; + size_t ele_len; + orderedIndexGetElementRaw(ln, &ele_ptr, &ele_len); if (reverse) { - if (!zslLexValueGteMin(ele, range)) break; + if (!zsetLexGteMin(ele_ptr, ele_len, range)) break; } else { - if (!zslLexValueLteMax(ele, range)) break; + if (!zsetLexLteMax(ele_ptr, ele_len, range)) break; } rangelen++; - handler->emitResultFromCBuffer(handler, ele, sdslen(ele), ln->score); - - /* Move to next node */ - if (reverse) { - ln = ln->backward; - } else { - ln = ln->level[0].forward; - } + handler->emitResultFromCBuffer(handler, ele_ptr, ele_len, orderedIndexGetScore(ln)); } } else { serverPanic("Unknown sorted set encoding"); @@ -3268,17 +3283,19 @@ void genericZpopCommand(client *c, score = zzlGetScore(sptr); } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(zobj); - zskiplist *zsl = zs->zsl; - zskiplistNode *zln; + OrderedIndex *oi = zs->oi; + OrderedIndexItem *zln; /* Get the first or last element in the sorted set. */ - zskiplistNode *zheader = zslGetHeader(zsl); - zln = (where == ZSET_MAX ? zslGetTail(zsl) : zheader->level[0].forward); + zln = (where == ZSET_MAX ? orderedIndexGetLast(oi) : orderedIndexGetFirst(oi)); /* There must be an element in the sorted set. */ serverAssertWithInfo(c, zobj, zln != NULL); - ele = sdsdup(zslGetNodeElement(zln)); - score = zln->score; + const char *ele_ptr; + size_t ele_len; + orderedIndexGetElementRaw(zln, &ele_ptr, &ele_len); + ele = sdsnewlen(ele_ptr, ele_len); + score = orderedIndexGetScore(zln); } else { serverPanic("Unknown sorted set encoding"); } @@ -3485,11 +3502,13 @@ void zrandmemberWithCountCommand(client *c, long l, int withscores) { while (count--) { void *entry; serverAssert(hashtableFairRandomEntry(zs->ht, &entry)); - zskiplistNode *node = entry; + OrderedIndexItem *node = entry; if (withscores && c->resp > 2) addReplyArrayLen(c, 2); - sds ele = zslGetNodeElement(node); - addReplyBulkCBuffer(c, ele, sdslen(ele)); - if (withscores) addReplyDouble(c, node->score); + const char *ele_ptr_tmp; + size_t ele_len_tmp; + orderedIndexGetElementRaw(node, &ele_ptr_tmp, &ele_len_tmp); + addReplyBulkCBuffer(c, ele_ptr_tmp, ele_len_tmp); + if (withscores) addReplyDouble(c, orderedIndexGetScore(node)); if (c->flag.close_asap) break; } } else if (zsetobj->encoding == OBJ_ENCODING_LISTPACK) { @@ -3587,7 +3606,10 @@ void zrandmemberWithCountCommand(client *c, long l, int withscores) { while (size > count) { void *element; hashtableFairRandomEntry(ht, &element); - hashtableDelete(ht, zslGetNodeElement((zskiplistNode *)element)); + const char *del_ele_ptr; + size_t del_ele_len; + orderedIndexGetElementRaw((OrderedIndexItem *)element, &del_ele_ptr, &del_ele_len); + hashtableDelete(ht, (sds)del_ele_ptr); size--; } hashtableCleanupIterator(&iter); @@ -3596,11 +3618,13 @@ void zrandmemberWithCountCommand(client *c, long l, int withscores) { hashtableInitIterator(&iter, ht, 0); void *next; while (hashtableNext(&iter, &next)) { - zskiplistNode *node = (zskiplistNode *)next; - sds key = zslGetNodeElement(node); + OrderedIndexItem *node = (OrderedIndexItem *)next; + const char *key_ptr_tmp; + size_t key_len_tmp; + orderedIndexGetElementRaw(node, &key_ptr_tmp, &key_len_tmp); if (withscores && c->resp > 2) addReplyArrayLen(c, 2); - addReplyBulkCBuffer(c, key, sdslen(key)); - if (withscores) addReplyDouble(c, node->score); + addReplyBulkCBuffer(c, key_ptr_tmp, key_len_tmp); + if (withscores) addReplyDouble(c, orderedIndexGetScore(node)); } hashtableCleanupIterator(&iter); @@ -3619,7 +3643,7 @@ void zrandmemberWithCountCommand(client *c, long l, int withscores) { while (added < count) { listpackEntry key; - double score; + double score = 0; zsetTypeRandomElement(zsetobj, size, &key, withscores ? &score : NULL); /* Try to add the object to the hashtable. If it already exists diff --git a/src/unit/ordered_index_test.h b/src/unit/ordered_index_test.h new file mode 100644 index 00000000000..709e7cf8c88 --- /dev/null +++ b/src/unit/ordered_index_test.h @@ -0,0 +1,182 @@ +#ifndef ORDERED_INDEX_TEST_H +#define ORDERED_INDEX_TEST_H + +/* + * Test-only interface for ordered index implementations. + * + * Defines an abstract C++ interface that each implementation subclasses. + * Production code uses the functions declared in ordered_index.h + * (implemented in ordered_index.c) which delegate to the active implementation. + */ + +extern "C" { +#include "ordered_index.h" +#include "skiplist_ordered_index.h" +} + +#include + +/* ---- Abstract interface ---- */ + +class OrderedIndexTestApi { + public: + virtual ~OrderedIndexTestApi() = default; + + /* Lifecycle */ + virtual OrderedIndex *create() = 0; + virtual void free(OrderedIndex *oi) = 0; + + /* Modification */ + virtual OrderedIndexItem *insert(OrderedIndex *oi, double score, const char *ele, size_t len) = 0; + virtual void deleteItem(OrderedIndex *oi, OrderedIndexItem *pos) = 0; + virtual OrderedIndexItem *updateScore(OrderedIndex *oi, OrderedIndexItem *pos, double newscore) = 0; + virtual OrderedIndexItem *popFirst(OrderedIndex *oi) = 0; + virtual OrderedIndexItem *popLast(OrderedIndex *oi) = 0; + virtual void freeItem(OrderedIndexItem *item) = 0; + virtual unsigned long deleteRangeByScore(OrderedIndex *oi, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) = 0; + virtual unsigned long deleteRangeByIndex(OrderedIndex *oi, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) = 0; + virtual unsigned long deleteRangeByLex(OrderedIndex *oi, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) = 0; + + /* Query */ + virtual unsigned long length(OrderedIndex *oi) = 0; + virtual OrderedIndexItem *getByIndex(OrderedIndex *oi, unsigned long rank) = 0; + virtual OrderedIndexItem *getFirst(OrderedIndex *oi) = 0; + virtual OrderedIndexItem *getLast(OrderedIndex *oi) = 0; + virtual unsigned long getIndex(OrderedIndex *oi, const OrderedIndexItem *pos) = 0; + virtual void getElementRaw(const OrderedIndexItem *pos, const char **ptr, size_t *len) = 0; + virtual double getScore(const OrderedIndexItem *pos) = 0; + + /* Memory */ + virtual size_t estimateMemory(OrderedIndex *oi, size_t sample_size) = 0; + + /* Debug / verification */ + virtual int verifyIntegrity(OrderedIndex *oi, char *errmsg, size_t errmsg_len) = 0; + + /* Count */ + virtual unsigned long countScoreRange(OrderedIndex *oi, double min, double max, int min_ex, int max_ex) = 0; + virtual unsigned long countLexRange(OrderedIndex *oi, const_sds min, const_sds max, int min_ex, int max_ex) = 0; + + /* Iterator */ + virtual void initIterator(OrderedIndexIterator *iter, OrderedIndex *oi) = 0; + virtual void resetIterator(OrderedIndexIterator *iter) = 0; + virtual OrderedIndexItem *next(OrderedIndexIterator *iter) = 0; + virtual OrderedIndexItem *prev(OrderedIndexIterator *iter) = 0; + virtual void seekToIndex(OrderedIndexIterator *iter, unsigned long rank) = 0; + virtual void seekToScoreRange(OrderedIndexIterator *iter, double min, double max, int min_ex, int max_ex, long offset) = 0; + virtual void seekToLexRange(OrderedIndexIterator *iter, const_sds min, const_sds max, int min_ex, int max_ex, long offset) = 0; + + /* Convenience (non-virtual) */ + OrderedIndexItem *insertSds(OrderedIndex *oi, double score, const_sds ele) { + return insert(oi, score, ele, sdslen(ele)); + } +}; + +/* ---- Skiplist implementation ---- */ + +class SkiplistOrderedIndex : public OrderedIndexTestApi { + public: + OrderedIndex *create() override { + return skiplistCreate(); + } + void free(OrderedIndex *oi) override { + skiplistFree(oi); + } + + OrderedIndexItem *insert(OrderedIndex *oi, double score, const char *ele, size_t len) override { + return skiplistInsert(oi, score, ele, len); + } + void deleteItem(OrderedIndex *oi, OrderedIndexItem *pos) override { + skiplistDelete(oi, pos); + } + OrderedIndexItem *updateScore(OrderedIndex *oi, OrderedIndexItem *pos, double newscore) override { + return skiplistUpdateScore(oi, pos, newscore); + } + OrderedIndexItem *popFirst(OrderedIndex *oi) override { + return skiplistPopFirst(oi); + } + OrderedIndexItem *popLast(OrderedIndex *oi) override { + return skiplistPopLast(oi); + } + void freeItem(OrderedIndexItem *item) override { + skiplistFreeItem(item); + } + unsigned long deleteRangeByScore(OrderedIndex *oi, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) override { + return skiplistDeleteRangeByScore(oi, min, max, min_ex, max_ex, on_delete, ctx); + } + unsigned long deleteRangeByIndex(OrderedIndex *oi, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) override { + return skiplistDeleteRangeByIndex(oi, start, end, on_delete, ctx); + } + unsigned long deleteRangeByLex(OrderedIndex *oi, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) override { + return skiplistDeleteRangeByLex(oi, min, max, min_ex, max_ex, on_delete, ctx); + } + + unsigned long length(OrderedIndex *oi) override { + return skiplistLength(oi); + } + OrderedIndexItem *getByIndex(OrderedIndex *oi, unsigned long rank) override { + return skiplistGetByIndex(oi, rank); + } + OrderedIndexItem *getFirst(OrderedIndex *oi) override { + return skiplistGetFirst(oi); + } + OrderedIndexItem *getLast(OrderedIndex *oi) override { + return skiplistGetLast(oi); + } + unsigned long getIndex(OrderedIndex *oi, const OrderedIndexItem *pos) override { + return skiplistGetIndex(oi, pos); + } + void getElementRaw(const OrderedIndexItem *pos, const char **ptr, size_t *len) override { + skiplistGetElementRaw(pos, ptr, len); + } + double getScore(const OrderedIndexItem *pos) override { + return skiplistGetScore(pos); + } + + size_t estimateMemory(OrderedIndex *oi, size_t sample_size) override { + return skiplistEstimateMemory(oi, sample_size); + } + + int verifyIntegrity(OrderedIndex *oi, char *errmsg, size_t errmsg_len) override { + return skiplistVerifyIntegrity(oi, errmsg, errmsg_len); + } + + unsigned long countScoreRange(OrderedIndex *oi, double min, double max, int min_ex, int max_ex) override { + return skiplistCountScoreRange(oi, min, max, min_ex, max_ex); + } + unsigned long countLexRange(OrderedIndex *oi, const_sds min, const_sds max, int min_ex, int max_ex) override { + return skiplistCountLexRange(oi, min, max, min_ex, max_ex); + } + + void initIterator(OrderedIndexIterator *iter, OrderedIndex *oi) override { + skiplistInitIterator(iter, oi); + } + void resetIterator(OrderedIndexIterator *iter) override { + skiplistResetIterator(iter); + } + OrderedIndexItem *next(OrderedIndexIterator *iter) override { + return skiplistNext(iter); + } + OrderedIndexItem *prev(OrderedIndexIterator *iter) override { + return skiplistPrev(iter); + } + void seekToIndex(OrderedIndexIterator *iter, unsigned long rank) override { + skiplistSeekToIndex(iter, rank); + } + void seekToScoreRange(OrderedIndexIterator *iter, double min, double max, int min_ex, int max_ex, long offset) override { + skiplistSeekToScoreRange(iter, min, max, min_ex, max_ex, offset); + } + void seekToLexRange(OrderedIndexIterator *iter, const_sds min, const_sds max, int min_ex, int max_ex, long offset) override { + skiplistSeekToLexRange(iter, min, max, min_ex, max_ex, offset); + } +}; + +/* ---- Static instances & test parameterization helpers ---- */ + +static SkiplistOrderedIndex skiplistImpl; + +static std::string orderedIndexTestName(const ::testing::TestParamInfo &info) { + if (info.param == &skiplistImpl) return "Skiplist"; + return "Unknown"; +} + +#endif /* ORDERED_INDEX_TEST_H */ diff --git a/src/unit/test_ordered_index.cpp b/src/unit/test_ordered_index.cpp new file mode 100644 index 00000000000..8cf8a5dfe96 --- /dev/null +++ b/src/unit/test_ordered_index.cpp @@ -0,0 +1,2309 @@ +/* + * Copyright (c) Valkey Contributors + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "generated_wrappers.hpp" + +extern "C" { +#include "server.h" +} + +/* Undefine min/max macros from server.h to avoid conflicts */ +#undef min +#undef max + +#include "ordered_index_test.h" + +#include +#include +#include /* only for GTest name generator */ + +/* Clean up shared lex sentinels allocated by OrderedIndexTest::SetUp(). */ +static void cleanupSharedSentinels(void) __attribute__((destructor)); +static void cleanupSharedSentinels(void) { + if (shared.minstring) { + sdsfree(shared.minstring); + shared.minstring = NULL; + } + if (shared.maxstring) { + sdsfree(shared.maxstring); + shared.maxstring = NULL; + } +} + +/* ---- C-style test helpers ---- */ + +#define TEST_MIN(a, b) ((a) < (b) ? (a) : (b)) +#define TEST_MAX(a, b) ((a) > (b) ? (a) : (b)) + +/* Collect all elements from an ordered index into a pre-allocated sds array. + * Caller must free each sds and the array itself. */ +static sds *collectIndexToSds(OrderedIndexTestApi &api, OrderedIndex *oi, size_t *out_n) { + size_t n = api.length(oi); + *out_n = n; + if (n == 0) return NULL; + sds *arr = (sds *)zmalloc(sizeof(sds) * n); + OrderedIndexIterator iter; + api.initIterator(&iter, oi); + for (size_t i = 0; i < n; i++) { + OrderedIndexItem *pos = api.next(&iter); + const char *ptr; + size_t len; + api.getElementRaw(pos, &ptr, &len); + arr[i] = sdsnewlen(ptr, len); + } + api.resetIterator(&iter); + return arr; +} + +static void freeSdsArray(sds *arr, size_t n) { + for (size_t i = 0; i < n; i++) sdsfree(arr[i]); + zfree(arr); +} + +/* Assert that an sds array matches an expected list of C strings. */ +#define ASSERT_SDS_ARRAY_EQ(arr, n, ...) \ + do { \ + const char *_exp[] = {__VA_ARGS__}; \ + size_t _exp_n = sizeof(_exp) / sizeof(_exp[0]); \ + ASSERT_EQ((size_t)(n), _exp_n); \ + for (size_t _i = 0; _i < _exp_n; _i++) { \ + ASSERT_STREQ(arr[_i], _exp[_i]); \ + } \ + } while (0) + +static int sdsArrayCmp(const void *a, const void *b) { + return sdscmp(*(sds *)a, *(sds *)b); +} + +static void sortSdsArray(sds *arr, size_t n) { + qsort(arr, n, sizeof(sds), sdsArrayCmp); +} + +static void reverseDoubleArray(double *arr, size_t n) { + for (size_t i = 0; i < n / 2; i++) { + double tmp = arr[i]; + arr[i] = arr[n - 1 - i]; + arr[n - 1 - i] = tmp; + } +} + +/* Verify structural integrity of the ordered index after mutations. */ +static ::testing::AssertionResult verifyIntegrity(OrderedIndexTestApi &api, OrderedIndex *oi) { + char errmsg[256]; + if (api.verifyIntegrity(oi, errmsg, sizeof(errmsg))) + return ::testing::AssertionResult(true); + return ::testing::AssertionFailure() << errmsg; +} + +#define VERIFY_INTEGRITY(api_ref, idx_ptr) ASSERT_TRUE(verifyIntegrity(api_ref, idx_ptr)) + +/* Use double infinity to avoid -Wdouble-promotion on macOS where INFINITY is float */ +static const double POS_INF = (double)INFINITY; +static const double NEG_INF = (double)-INFINITY; + +/* ========== Shared test data ========== */ + +static const char *FRUITS[] = {"apple", "banana", "cherry", "date", "elderberry"}; +static const int FRUITS_COUNT = 5; +static const char *NATO[] = {"alpha", "bravo", "charlie", "delta", "echo", "foxtrot"}; +static const int NATO_COUNT = 6; + +/* ========== Parameterized test fixture ========== */ + +class OrderedIndexTest : public ::testing::TestWithParam { + protected: + OrderedIndexTestApi &api = *GetParam(); + OrderedIndex *oi = nullptr; + + void SetUp() override { + /* Ensure shared lex sentinels are initialized (normally done by createSharedObjects) */ + if (!shared.minstring) shared.minstring = sdsnew("minstring"); + if (!shared.maxstring) shared.maxstring = sdsnew("maxstring"); + oi = api.create(); + } + void TearDown() override { + if (oi) api.free(oi); + } + + /* Insert a string literal at given score. */ + OrderedIndexItem *insert(double score, const char *ele) { + sds s = sdsnew(ele); + OrderedIndexItem *node = api.insertSds(oi, score, s); + sdsfree(s); + return node; + } + + /* Insert N sequential elements ("key0"..."keyN-1") at scores 0..N-1. */ + void populateSequential(int n) { + for (int i = 0; i < n; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + insert((double)i, buf); + } + } + + /* Assert next iterator element has expected score. */ + OrderedIndexItem *assertNextScore(OrderedIndexIterator *iter, double expected) { + OrderedIndexItem *pos = api.next(iter); + EXPECT_NE(pos, nullptr); + if (pos) { + EXPECT_DOUBLE_EQ(api.getScore(pos), expected); + } + return pos; + } + + /* Assert prev iterator element has expected score. */ + OrderedIndexItem *assertPrevScore(OrderedIndexIterator *iter, double expected) { + OrderedIndexItem *pos = api.prev(iter); + EXPECT_NE(pos, nullptr); + if (pos) { + EXPECT_DOUBLE_EQ(api.getScore(pos), expected); + } + return pos; + } + + /* Assert element content matches expected string. */ + void assertElement(OrderedIndexItem *node, const char *expected) { + const char *ptr; + size_t len; + api.getElementRaw(node, &ptr, &len); + ASSERT_EQ(len, strlen(expected)); + ASSERT_EQ(memcmp(ptr, expected, len), 0); + } + + /* Assert node has expected score. */ + void assertScore(OrderedIndexItem *node, double expected) { + ASSERT_DOUBLE_EQ(api.getScore(node), expected); + } + + /* Delete lex range using const char* (handles sds lifecycle). */ + unsigned long deleteLexRange(const char *min_str, const char *max_str, int min_ex, int max_ex, OrderedIndexOnDelete cb, void *ctx) { + sds min = sdsnew(min_str); + sds max = sdsnew(max_str); + unsigned long deleted = api.deleteRangeByLex(oi, min, max, min_ex, max_ex, cb, ctx); + sdsfree(min); + sdsfree(max); + return deleted; + } + + /* Count lex range using const char*. */ + unsigned long countLexRange(const char *min_str, const char *max_str, int min_ex, int max_ex) { + sds min = sdsnew(min_str); + sds max = sdsnew(max_str); + unsigned long count = api.countLexRange(oi, min, max, min_ex, max_ex); + sdsfree(min); + sdsfree(max); + return count; + } + + /* Seek to lex range using const char*. */ + void seekToLexRange(OrderedIndexIterator *it, const char *min_str, const char *max_str, int min_ex, int max_ex, long offset) { + sds min = sdsnew(min_str); + sds max = sdsnew(max_str); + api.seekToLexRange(it, min, max, min_ex, max_ex, offset); + sdsfree(min); + sdsfree(max); + } + + /* Assert full forward traversal matches expected element names. */ + void assertAllElements(const char *expected[], size_t count) { + OrderedIndexIterator it; + api.initIterator(&it, oi); + OrderedIndexItem *pos; + size_t i = 0; + while ((pos = api.next(&it)) != NULL) { + ASSERT_LT(i, count) << "More elements than expected"; + assertElement(pos, expected[i]); + i++; + } + api.resetIterator(&it); + ASSERT_EQ(i, count) << "Fewer elements than expected"; + } + + /* Verify structural integrity. */ + void verifyOI() { + ASSERT_TRUE(verifyIntegrity(api, oi)); + } +}; + +/* Variadic macro for clean assertAllElements call sites. */ +#define ASSERT_ALL_ELEMENTS(...) \ + do { \ + const char *_elems[] = {__VA_ARGS__}; \ + assertAllElements(_elems, sizeof(_elems) / sizeof(*_elems)); \ + } while (0) + +/* ========== Basic tests ========== */ + +TEST_P(OrderedIndexTest, CreateFree) { + ASSERT_NE(oi, nullptr); + ASSERT_EQ(api.length(oi), 0UL); + verifyOI(); + + OrderedIndexIterator iter; + api.initIterator(&iter, oi); + ASSERT_EQ(api.next(&iter), nullptr); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, InsertSingle) { + OrderedIndexItem *node = insert(1.0, "test"); + verifyOI(); + + ASSERT_NE(node, nullptr); + ASSERT_EQ(api.length(oi), 1UL); + assertScore(node, 1.0); + assertElement(node, "test"); + + OrderedIndexIterator iter; + api.initIterator(&iter, oi); + ASSERT_EQ(api.next(&iter), node); + ASSERT_EQ(api.next(&iter), nullptr); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, InsertMultipleOrdered) { + populateSequential(10); + + ASSERT_EQ(api.length(oi), 10UL); + verifyOI(); + + /* Verify forward traversal */ + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, oi); + for (int i = 0; i < 10; i++) { + ASSERT_NE((pos = api.next(&iter)), nullptr); + ASSERT_DOUBLE_EQ(api.getScore(pos), (double)i); + } + ASSERT_EQ(api.next(&iter), nullptr); + api.resetIterator(&iter); + + /* Verify backward traversal */ + api.initIterator(&iter, oi); + for (int i = 9; i >= 0; i--) { + ASSERT_NE((pos = api.prev(&iter)), nullptr); + ASSERT_DOUBLE_EQ(api.getScore(pos), (double)i); + } + ASSERT_EQ(api.prev(&iter), nullptr); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, DuplicateScores) { + for (int i = 0; i < 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + insert(1.0, buf); + } + + ASSERT_EQ(api.length(oi), 5UL); + verifyOI(); + + /* Verify lexicographic ordering for same scores */ + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, oi); + for (int i = 0; i < 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + pos = assertNextScore(&iter, 1.0); + assertElement(pos, buf); + } + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, IndexOperations) { + OrderedIndexItem *nodes[10]; + + for (int i = 0; i < 10; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + nodes[i] = insert((double)i, buf); + } + verifyOI(); + + for (int i = 0; i < 10; i++) { + unsigned long idx = api.getIndex(oi, nodes[i]); + ASSERT_EQ(idx, (unsigned long)i); + } + + for (int i = 0; i < 10; i++) { + OrderedIndexItem *node = api.getByIndex(oi, i); + ASSERT_EQ(node, nodes[i]); + } +} + +TEST_P(OrderedIndexTest, Delete) { + OrderedIndexItem *nodes[5]; + for (int i = 0; i < 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + nodes[i] = insert((double)i, buf); + } + ASSERT_EQ(api.length(oi), 5UL); + + api.deleteItem(oi, nodes[2]); + ASSERT_EQ(api.length(oi), 4UL); + verifyOI(); + + OrderedIndexIterator iter; + api.initIterator(&iter, oi); + assertNextScore(&iter, 0.0); + assertNextScore(&iter, 1.0); + assertNextScore(&iter, 3.0); /* Skipped 2.0 */ + assertNextScore(&iter, 4.0); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, PopFirst) { + ASSERT_EQ(api.popFirst(oi), nullptr); + + populateSequential(5); + ASSERT_EQ(api.length(oi), 5UL); + + OrderedIndexItem *item = api.popFirst(oi); + ASSERT_NE(item, nullptr); + assertScore(item, 0.0); + assertElement(item, "key0"); + api.freeItem(item); + ASSERT_EQ(api.length(oi), 4UL); + verifyOI(); + + item = api.popFirst(oi); + assertScore(item, 1.0); + api.freeItem(item); + ASSERT_EQ(api.length(oi), 3UL); + verifyOI(); +} + +TEST_P(OrderedIndexTest, PopLast) { + ASSERT_EQ(api.popLast(oi), nullptr); + + populateSequential(5); + ASSERT_EQ(api.length(oi), 5UL); + + OrderedIndexItem *item = api.popLast(oi); + ASSERT_NE(item, nullptr); + assertScore(item, 4.0); + assertElement(item, "key4"); + api.freeItem(item); + ASSERT_EQ(api.length(oi), 4UL); + verifyOI(); + + item = api.popLast(oi); + assertScore(item, 3.0); + api.freeItem(item); + ASSERT_EQ(api.length(oi), 3UL); + verifyOI(); +} + +TEST_P(OrderedIndexTest, GetFirstAndLast) { + /* Empty index returns NULL */ + ASSERT_EQ(api.getFirst(oi), nullptr); + ASSERT_EQ(api.getLast(oi), nullptr); + + insert(2.0, "bravo"); + insert(1.0, "alpha"); + insert(3.0, "charlie"); + + assertElement(api.getFirst(oi), "alpha"); + assertScore(api.getFirst(oi), 1.0); + assertElement(api.getLast(oi), "charlie"); + assertScore(api.getLast(oi), 3.0); + + /* Single element: first == last */ + api.deleteItem(oi, api.getFirst(oi)); + api.deleteItem(oi, api.getLast(oi)); + ASSERT_EQ(api.length(oi), 1UL); + assertElement(api.getFirst(oi), "bravo"); + assertElement(api.getLast(oi), "bravo"); +} + +TEST_P(OrderedIndexTest, UpdateScore) { + OrderedIndexItem *node1 = insert(1.0, "key1"); + OrderedIndexItem *node2 = insert(2.0, "key2"); + insert(3.0, "key3"); + + OrderedIndexItem *updated = api.updateScore(oi, node2, 4.0); + ASSERT_NE(updated, nullptr); + assertScore(updated, 4.0); + verifyOI(); + assertElement(updated, "key2"); + + /* Verify order: key1(1.0), key3(3.0), key2(4.0) */ + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, oi); + assertNextScore(&iter, 1.0); + assertNextScore(&iter, 3.0); + pos = assertNextScore(&iter, 4.0); + assertElement(pos, "key2"); + api.resetIterator(&iter); + + /* Update to same score (no-op) */ + updated = api.updateScore(oi, node1, 1.0); + assertScore(updated, 1.0); + verifyOI(); +} + +TEST_P(OrderedIndexTest, DeleteRangeByScore) { + populateSequential(10); + + /* Delete range [3, 6] inclusive */ + unsigned long deleted = api.deleteRangeByScore(oi, 3.0, 6.0, 0, 0, NULL, NULL); + ASSERT_EQ(deleted, 4UL); + ASSERT_EQ(api.length(oi), 6UL); + verifyOI(); + + OrderedIndexIterator iter; + api.initIterator(&iter, oi); + for (int i = 0; i < 3; i++) { + assertNextScore(&iter, (double)i); + } + for (int i = 7; i < 10; i++) { + assertNextScore(&iter, (double)i); + } + api.resetIterator(&iter); + + /* Delete with exclusive bounds (2, 8) - should delete 7 */ + deleted = api.deleteRangeByScore(oi, 2.0, 8.0, 1, 1, NULL, NULL); + ASSERT_EQ(deleted, 1UL); + ASSERT_EQ(api.length(oi), 5UL); + verifyOI(); +} + +TEST_P(OrderedIndexTest, DeleteRangeByIndex) { + populateSequential(10); + + /* Delete indices 2-4 (elements at scores 2,3,4) */ + unsigned long deleted = api.deleteRangeByIndex(oi, 2, 4, NULL, NULL); + ASSERT_EQ(deleted, 3UL); + ASSERT_EQ(api.length(oi), 7UL); + verifyOI(); + + OrderedIndexIterator iter; + api.initIterator(&iter, oi); + assertNextScore(&iter, 0.0); + api.resetIterator(&iter); + + /* Verify index 2 is now score 5 (was index 5) */ + OrderedIndexItem *node = api.getByIndex(oi, 2); + assertScore(node, 5.0); +} + +TEST_P(OrderedIndexTest, MixedOperationsIndexIntegrity) { + OrderedIndexItem *nodes[100]; + + for (int i = 0; i < 100; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + nodes[i] = insert((double)i, buf); + } + + for (int i = 2; i < 100; i += 3) { + api.deleteItem(oi, nodes[i]); + nodes[i] = NULL; + } + verifyOI(); + + /* Update scores of surviving nodes (indices not ≡ 2 mod 3) */ + nodes[10] = api.updateScore(oi, nodes[10], 150.0); + nodes[19] = api.updateScore(oi, nodes[19], 160.0); + verifyOI(); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, oi); + unsigned long expectedIdx = 0; + while (((pos = api.next(&iter)) != NULL)) { + unsigned long actualIdx = api.getIndex(oi, pos); + ASSERT_EQ(actualIdx, expectedIdx); + expectedIdx++; + } + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, BackwardTraversalAfterDeletions) { + OrderedIndexItem *nodes[20]; + + for (int i = 0; i < 20; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + nodes[i] = insert((double)i, buf); + } + + api.deleteItem(oi, nodes[5]); + api.deleteItem(oi, nodes[10]); + api.deleteItem(oi, nodes[15]); + verifyOI(); + + OrderedIndexIterator iter; + api.initIterator(&iter, oi); + int expected_scores[] = {19, 18, 17, 16, 14, 13, 12, 11, 9, 8, 7, 6, 4, 3, 2, 1, 0}; + + for (int i = 0; i < 17; i++) { + assertPrevScore(&iter, (double)expected_scores[i]); + } + ASSERT_EQ(api.prev(&iter), nullptr); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, LexicographicEdgeCases) { + insert(1.0, "z"); + insert(1.0, ""); + insert(1.0, "a"); + + /* Verify lexicographic order: "", "a", "z" */ + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, oi); + pos = assertNextScore(&iter, 1.0); + assertElement(pos, ""); + pos = assertNextScore(&iter, 1.0); + assertElement(pos, "a"); + pos = assertNextScore(&iter, 1.0); + assertElement(pos, "z"); + api.resetIterator(&iter); + api.free(oi); + + /* Test very long string (1KB) */ + oi = api.create(); + char long_buf[1024]; + memset(long_buf, 'x', 1023); + long_buf[1023] = '\0'; + insert(1.0, long_buf); + insert(1.0, "short"); + + api.initIterator(&iter, oi); + pos = assertNextScore(&iter, 1.0); + assertElement(pos, "short"); + pos = assertNextScore(&iter, 1.0); + assertElement(pos, long_buf); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, RangeBoundaryPrecision) { + double base = 1.0; + double epsilon = 1e-10; + + insert(base, "at_base"); + insert(base + epsilon, "at_base_plus_epsilon"); + insert(base + 2 * epsilon, "at_base_plus_2epsilon"); + + unsigned long deleted = api.deleteRangeByScore(oi, base, base + 2 * epsilon, 1, 1, NULL, NULL); + ASSERT_EQ(deleted, 1UL); + ASSERT_EQ(api.length(oi), 2UL); + + OrderedIndexIterator iter; + api.initIterator(&iter, oi); + assertNextScore(&iter, base); + assertNextScore(&iter, base + 2 * epsilon); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, SpecialDoubleValues) { + insert(NEG_INF, "neg_inf"); + insert(POS_INF, "pos_inf"); + insert(0.0, "zero"); + insert(1.0, "one"); + + /* Verify ordering: -inf, 0, 1, +inf */ + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, oi); + assertNextScore(&iter, NEG_INF); + assertNextScore(&iter, 0.0); + assertNextScore(&iter, 1.0); + assertNextScore(&iter, POS_INF); + api.resetIterator(&iter); + api.free(oi); + + /* Test +0.0 vs -0.0 */ + oi = api.create(); + insert(0.0, "pos_zero"); + insert(-0.0, "neg_zero"); + + /* Both should be in the list, ordered lexicographically since scores are equal */ + ASSERT_EQ(api.length(oi), 2UL); + api.initIterator(&iter, oi); + pos = assertNextScore(&iter, 0.0); + assertElement(pos, "neg_zero"); + pos = assertNextScore(&iter, 0.0); + assertElement(pos, "pos_zero"); + api.resetIterator(&iter); + api.free(oi); + + /* Test denormalized double */ + oi = api.create(); + double denorm = 1e-320; /* Denormalized double */ + insert(denorm, "denorm"); + insert(1.0, "normal"); + + ASSERT_EQ(api.length(oi), 2UL); + api.initIterator(&iter, oi); + pos = assertNextScore(&iter, denorm); + ASSERT_TRUE(api.getScore(pos) < 1.0); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, EmptyIndexOperations) { + ASSERT_EQ(api.length(oi), 0UL); + OrderedIndexIterator iter; + api.initIterator(&iter, oi); + ASSERT_EQ(api.next(&iter), nullptr); + ASSERT_EQ(api.prev(&iter), nullptr); + api.resetIterator(&iter); + ASSERT_EQ(api.getByIndex(oi, 0), nullptr); +} + +TEST_P(OrderedIndexTest, DeleteEdgeCases) { + /* Delete only element */ + OrderedIndexItem *node = insert(1.0, "only"); + api.deleteItem(oi, node); + ASSERT_EQ(api.length(oi), 0UL); + verifyOI(); + OrderedIndexIterator iter; + api.initIterator(&iter, oi); + ASSERT_EQ(api.next(&iter), nullptr); + api.resetIterator(&iter); + + /* Delete first element */ + OrderedIndexItem *nodes[3]; + for (int i = 0; i < 3; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + nodes[i] = insert((double)i, buf); + } + api.deleteItem(oi, nodes[0]); + ASSERT_EQ(api.length(oi), 2UL); + verifyOI(); + api.initIterator(&iter, oi); + assertNextScore(&iter, 1.0); + api.resetIterator(&iter); + + /* Delete last element */ + api.deleteItem(oi, nodes[2]); + ASSERT_EQ(api.length(oi), 1UL); + verifyOI(); + api.initIterator(&iter, oi); + assertPrevScore(&iter, 1.0); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, IndexEdgeCases) { + populateSequential(5); + + ASSERT_EQ(api.getByIndex(oi, 5), nullptr); + ASSERT_EQ(api.getByIndex(oi, 99), nullptr); + ASSERT_NE(api.getByIndex(oi, 0), nullptr); + ASSERT_NE(api.getByIndex(oi, 4), nullptr); +} + +TEST_P(OrderedIndexTest, DuplicateInsert) { + OrderedIndexItem *node1 = insert(1.0, "duplicate"); + OrderedIndexItem *node2 = insert(1.0, "duplicate"); + + /* Should have 2 nodes (duplicates allowed) */ + ASSERT_EQ(api.length(oi), 2UL); + ASSERT_NE(node1, node2); +} + +TEST_P(OrderedIndexTest, UpdateScoreEdgeCases) { + populateSequential(5); /* scores: 0, 1, 2, 3, 4 */ + + /* Move first element to last position */ + OrderedIndexItem *first = api.getByIndex(oi, 0); + OrderedIndexItem *updated = api.updateScore(oi, first, 10.0); + assertScore(updated, 10.0); + verifyOI(); + OrderedIndexIterator iter; + api.initIterator(&iter, oi); + ASSERT_EQ(api.prev(&iter), updated); /* now last */ + api.resetIterator(&iter); + + /* Move last element to first position */ + OrderedIndexItem *last = api.getByIndex(oi, api.length(oi) - 1); + updated = api.updateScore(oi, last, -1.0); + assertScore(updated, -1.0); + verifyOI(); + api.initIterator(&iter, oi); + ASSERT_EQ(api.next(&iter), updated); /* now first */ + api.resetIterator(&iter); + + /* Move middle element backward past multiple */ + OrderedIndexItem *middle = api.getByIndex(oi, 2); + updated = api.updateScore(oi, middle, 0.5); + assertScore(updated, 0.5); + verifyOI(); + ASSERT_EQ(api.getIndex(oi, updated), 1UL); /* moved from index 2 to 1 */ + + /* Update score without changing position (stays between neighbors) */ + OrderedIndexItem *node = api.getByIndex(oi, 3); + unsigned long idx_before = api.getIndex(oi, node); + updated = api.updateScore(oi, node, api.getScore(node) + 0.1); + ASSERT_EQ(api.getIndex(oi, updated), idx_before); + verifyOI(); +} + +TEST_P(OrderedIndexTest, RangeDeleteEdgeCases) { + populateSequential(10); + + /* Delete empty range (min > max) */ + unsigned long deleted = api.deleteRangeByScore(oi, 5.0, 4.0, 0, 0, NULL, NULL); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(api.length(oi), 10UL); + + /* Delete range with no matches */ + deleted = api.deleteRangeByScore(oi, 10.5, 11.5, 0, 0, NULL, NULL); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(api.length(oi), 10UL); + + /* Delete first elements by index */ + deleted = api.deleteRangeByIndex(oi, 0, 1, NULL, NULL); + ASSERT_EQ(deleted, 2UL); + verifyOI(); + OrderedIndexIterator iter; + api.initIterator(&iter, oi); + assertNextScore(&iter, 2.0); + api.resetIterator(&iter); + + /* Delete last elements by index */ + unsigned long len = api.length(oi); + deleted = api.deleteRangeByIndex(oi, len - 2, len - 1, NULL, NULL); + ASSERT_EQ(deleted, 2UL); + verifyOI(); + api.initIterator(&iter, oi); + assertPrevScore(&iter, 7.0); + api.resetIterator(&iter); + + /* Delete entire remaining index by score */ + deleted = api.deleteRangeByScore(oi, -100.0, 100.0, 0, 0, NULL, NULL); + ASSERT_EQ(deleted, 6UL); + ASSERT_EQ(api.length(oi), 0UL); + verifyOI(); +} + +TEST_P(OrderedIndexTest, TraversalEdgeCases) { + insert(1.0, "single"); + + OrderedIndexIterator iter; + api.initIterator(&iter, oi); + assertNextScore(&iter, 1.0); + ASSERT_EQ(api.next(&iter), nullptr); + api.resetIterator(&iter); + + api.initIterator(&iter, oi); + assertPrevScore(&iter, 1.0); + ASSERT_EQ(api.prev(&iter), nullptr); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, SeekToIndex) { + for (int i = 1; i <= 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + insert((double)i, buf); + } + + OrderedIndexIterator iter; + + /* Seek to index 0 (first element) */ + api.initIterator(&iter, oi); + api.seekToIndex(&iter, 0); + assertNextScore(&iter, 2.0); /* next after first = second */ + api.resetIterator(&iter); + + api.initIterator(&iter, oi); + api.seekToIndex(&iter, 0); + assertPrevScore(&iter, 1.0); /* prev at first = first itself */ + api.resetIterator(&iter); + + /* Seek to index 1 (second element) */ + api.initIterator(&iter, oi); + api.seekToIndex(&iter, 1); + assertNextScore(&iter, 3.0); + api.resetIterator(&iter); + + api.initIterator(&iter, oi); + api.seekToIndex(&iter, 1); + assertPrevScore(&iter, 2.0); + api.resetIterator(&iter); + + /* Seek to index 2 (middle) */ + api.initIterator(&iter, oi); + api.seekToIndex(&iter, 2); + assertNextScore(&iter, 4.0); + api.resetIterator(&iter); + + api.initIterator(&iter, oi); + api.seekToIndex(&iter, 2); + assertPrevScore(&iter, 3.0); + api.resetIterator(&iter); + + /* Seek to index 4 (last) */ + api.initIterator(&iter, oi); + api.seekToIndex(&iter, 4); + ASSERT_EQ(api.next(&iter), nullptr); + api.resetIterator(&iter); + + api.initIterator(&iter, oi); + api.seekToIndex(&iter, 4); + assertPrevScore(&iter, 5.0); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, ReverseIteration) { + for (int i = 1; i <= 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + insert((double)i, buf); + } + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + + /* Full reverse traversal */ + api.initIterator(&iter, oi); + int count = 0; + double expected = 5.0; + while (((pos = api.prev(&iter)) != NULL)) { + assertScore(pos, expected); + expected -= 1.0; + count++; + } + ASSERT_EQ(count, 5); + api.resetIterator(&iter); + + /* Reverse then forward */ + api.initIterator(&iter, oi); + assertPrevScore(&iter, 5.0); + assertNextScore(&iter, 5.0); + api.resetIterator(&iter); + + /* Forward then reverse */ + api.initIterator(&iter, oi); + assertNextScore(&iter, 1.0); + assertPrevScore(&iter, 1.0); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, SeekToScoreRange) { + /* Insert elements with scores 0,2,4,6,8 */ + for (int i = 0; i < 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + insert((double)(i * 2), buf); + } + + OrderedIndexIterator iter; + + /* Seek to first in range [2, 6] with offset 0 */ + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, 0); + assertNextScore(&iter, 2.0); + api.resetIterator(&iter); + + /* Seek to second in range [2, 6] with offset 1 */ + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, 1); + assertNextScore(&iter, 4.0); + api.resetIterator(&iter); + + /* Seek to last in range [2, 6] with offset -1, positioned for prev() */ + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, -1); + assertPrevScore(&iter, 6.0); + api.resetIterator(&iter); + + /* Seek with exclusive bounds (2, 6) - should start at 4 */ + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, 2.0, 6.0, 1, 1, 0); + assertNextScore(&iter, 4.0); + api.resetIterator(&iter); + + /* Seek to empty range above all elements */ + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, 10.0, 20.0, 0, 0, 0); + ASSERT_EQ(api.next(&iter), nullptr); + api.resetIterator(&iter); + + /* Seek to empty range below all elements */ + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, -20.0, -10.0, 0, 0, 0); + ASSERT_EQ(api.next(&iter), nullptr); + api.resetIterator(&iter); + + /* Out of range positive offset */ + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, 10); + ASSERT_EQ(api.next(&iter), nullptr); + api.resetIterator(&iter); + + /* Negative offset beyond range */ + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, -10); + ASSERT_EQ(api.prev(&iter), nullptr); + api.resetIterator(&iter); + + /* Second from last with offset -2, positioned for prev() */ + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, -2); + assertPrevScore(&iter, 4.0); + api.resetIterator(&iter); + + /* Empty range where min > max */ + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, 6.0, 2.0, 0, 0, 0); + ASSERT_EQ(api.next(&iter), nullptr); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, SeekToScoreRangeIteration) { + populateSequential(10); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + + /* Seek to range [3, 7] and iterate forward */ + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, 3.0, 7.0, 0, 0, 0); + int count = 0; + double expected = 3.0; + while (((pos = api.next(&iter)) != NULL) && api.getScore(pos) <= 7.0) { + assertScore(pos, expected); + expected += 1.0; + count++; + } + ASSERT_EQ(count, 5); + api.resetIterator(&iter); + + /* Seek to last in range and iterate backward */ + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, 3.0, 7.0, 0, 0, -1); + count = 0; + expected = 7.0; + while (((pos = api.prev(&iter)) != NULL) && api.getScore(pos) >= 3.0) { + assertScore(pos, expected); + expected -= 1.0; + count++; + } + ASSERT_EQ(count, 5); + api.resetIterator(&iter); + + /* Seek with offset and continue iteration */ + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, 2.0, 8.0, 0, 0, 2); + assertNextScore(&iter, 4.0); + assertNextScore(&iter, 5.0); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, SeekInfReverseIteration) { + for (int i = 1; i <= 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + insert((double)i, buf); + } + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, NEG_INF, POS_INF, 0, 0, -1); + int count = 0; + double expected = 5.0; + while (((pos = api.prev(&iter)) != NULL)) { + assertScore(pos, expected); + expected -= 1.0; + count++; + } + ASSERT_EQ(count, 5); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, SeekInfForwardIteration) { + for (int i = 1; i <= 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + insert((double)i, buf); + } + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + + api.initIterator(&iter, oi); + api.seekToScoreRange(&iter, NEG_INF, POS_INF, 0, 0, 0); + int count = 0; + double expected = 1.0; + while (((pos = api.next(&iter)) != NULL)) { + assertScore(pos, expected); + expected += 1.0; + count++; + } + ASSERT_EQ(count, 5); + api.resetIterator(&iter); +} + +TEST_P(OrderedIndexTest, SeekToLexRange) { + for (int i = 0; i < FRUITS_COUNT; i++) insert(1.0, FRUITS[i]); + + OrderedIndexIterator it; + OrderedIndexItem *pos; + + /* Seek to first in lex range [banana, date] with offset 0 */ + api.initIterator(&it, oi); + seekToLexRange(&it, "banana", "date", 0, 0, 0); + ASSERT_NE((pos = api.next(&it)), nullptr); + assertElement(pos, "banana"); + api.resetIterator(&it); + + /* Seek to second in lex range with offset 1 */ + api.initIterator(&it, oi); + seekToLexRange(&it, "banana", "date", 0, 0, 1); + ASSERT_NE((pos = api.next(&it)), nullptr); + assertElement(pos, "cherry"); + api.resetIterator(&it); + + /* Seek to last in lex range with offset -1, positioned for prev() */ + api.initIterator(&it, oi); + seekToLexRange(&it, "banana", "date", 0, 0, -1); + ASSERT_NE((pos = api.prev(&it)), nullptr); + assertElement(pos, "date"); + api.resetIterator(&it); + + /* Seek with exclusive bounds (banana, date) - should start at cherry */ + api.initIterator(&it, oi); + seekToLexRange(&it, "banana", "date", 1, 1, 0); + ASSERT_NE((pos = api.next(&it)), nullptr); + assertElement(pos, "cherry"); + api.resetIterator(&it); + + /* Seek to empty lex range */ + api.initIterator(&it, oi); + seekToLexRange(&it, "zzz", "zzzz", 0, 0, 0); + ASSERT_EQ(api.next(&it), nullptr); + api.resetIterator(&it); + + /* Out of range positive offset */ + api.initIterator(&it, oi); + seekToLexRange(&it, "banana", "date", 0, 0, 10); + ASSERT_EQ(api.next(&it), nullptr); + api.resetIterator(&it); +} + +TEST_P(OrderedIndexTest, DeleteRangeByLexInclusive) { + for (int i = 0; i < FRUITS_COUNT; i++) insert(1.0, FRUITS[i]); + + ASSERT_EQ(deleteLexRange("banana", "date", 0, 0, NULL, NULL), 3UL); + ASSERT_EQ(api.length(oi), 2UL); + verifyOI(); + ASSERT_ALL_ELEMENTS("apple", "elderberry"); +} + +TEST_P(OrderedIndexTest, DeleteRangeByLexExclusive) { + for (int i = 0; i < FRUITS_COUNT; i++) insert(1.0, FRUITS[i]); + + ASSERT_EQ(deleteLexRange("banana", "date", 1, 1, NULL, NULL), 1UL); + ASSERT_EQ(api.length(oi), 4UL); + ASSERT_ALL_ELEMENTS("apple", "banana", "date", "elderberry"); +} + +TEST_P(OrderedIndexTest, DeleteRangeByLex_EmptyRange) { + for (int i = 0; i < 3; i++) insert(1.0, FRUITS[i]); + + ASSERT_EQ(deleteLexRange("zzz", "aaa", 0, 0, NULL, NULL), 0UL); + ASSERT_EQ(api.length(oi), 3UL); +} + +TEST_P(OrderedIndexTest, DeleteRangeByLex_All) { + for (int i = 0; i < 3; i++) insert(1.0, FRUITS[i]); + + ASSERT_EQ(deleteLexRange("a", "z", 0, 0, NULL, NULL), 3UL); + ASSERT_EQ(api.length(oi), 0UL); +} + +TEST_P(OrderedIndexTest, DeleteRangeByLex_SingleElement) { + for (int i = 0; i < 3; i++) insert(1.0, FRUITS[i]); + + ASSERT_EQ(deleteLexRange("banana", "banana", 0, 0, NULL, NULL), 1UL); + ASSERT_EQ(api.length(oi), 2UL); + ASSERT_ALL_ELEMENTS("apple", "cherry"); +} + +TEST_P(OrderedIndexTest, DeleteRangeByLexPreservesOutside) { + for (int i = 0; i < NATO_COUNT; i++) insert(1.0, NATO[i]); + + ASSERT_EQ(deleteLexRange("charlie", "delta", 0, 0, NULL, NULL), 2UL); + ASSERT_EQ(api.length(oi), 4UL); + ASSERT_ALL_ELEMENTS("alpha", "bravo", "echo", "foxtrot"); + + /* Verify scores are preserved */ + OrderedIndexIterator it; + api.initIterator(&it, oi); + OrderedIndexItem *pos; + while ((pos = api.next(&it)) != NULL) { + assertScore(pos, 1.0); + } + api.resetIterator(&it); + + /* Verify indices are correct after deletion */ + for (unsigned long r = 0; r < 4; r++) { + OrderedIndexItem *node = api.getByIndex(oi, r); + ASSERT_NE(node, nullptr); + ASSERT_EQ(api.getIndex(oi, node), r); + } +} + +TEST_P(OrderedIndexTest, LexRangeSentinels) { + /* Insert 5 elements at the same score (lex ordering) */ + insert(0.0, "alpha"); + insert(0.0, "bravo"); + insert(0.0, "charlie"); + insert(0.0, "delta"); + insert(0.0, "echo"); + + sds charlie = sdsnew("charlie"); + + /* Count with sentinels */ + ASSERT_EQ(countLexRange("minstring", "maxstring", 0, 0), 0UL); /* literal strings, not sentinels */ + ASSERT_EQ(api.countLexRange(oi, shared.minstring, shared.maxstring, 0, 0), 5UL); + ASSERT_EQ(api.countLexRange(oi, shared.minstring, charlie, 0, 0), 3UL); + ASSERT_EQ(api.countLexRange(oi, charlie, shared.maxstring, 0, 0), 3UL); + + /* Inverted range (max < min sentinel) should return 0 */ + ASSERT_EQ(api.countLexRange(oi, shared.maxstring, shared.minstring, 0, 0), 0UL); + ASSERT_EQ(api.countLexRange(oi, charlie, shared.minstring, 0, 0), 0UL); + + /* Seek with sentinels - iterate all */ + OrderedIndexIterator it; + api.initIterator(&it, oi); + api.seekToLexRange(&it, shared.minstring, shared.maxstring, 0, 0, 0); + assertNextScore(&it, 0.0); /* alpha */ + assertNextScore(&it, 0.0); /* bravo */ + assertNextScore(&it, 0.0); /* charlie */ + assertNextScore(&it, 0.0); /* delta */ + assertNextScore(&it, 0.0); /* echo */ + ASSERT_EQ(api.next(&it), nullptr); + api.resetIterator(&it); + + /* Delete with sentinels - delete all */ + ASSERT_EQ(api.deleteRangeByLex(oi, shared.minstring, shared.maxstring, 0, 0, NULL, NULL), 5UL); + ASSERT_EQ(api.length(oi), 0UL); + + sdsfree(charlie); +} + +/* ========== Randomized property tests ========== */ + +/* Default fuzz seed — overridden by --seed flag if provided. */ +extern char *seed; +static uint32_t test_fuzz_seed(void) { + uint32_t s = seed ? (uint32_t)atoi(seed) : 42; + printf(" [fuzz seed: %u]\n", s); + return s; +} + +/* Simple xorshift32 PRNG — deterministic and seedable. */ +static uint32_t test_rand_next(uint32_t *state) { + *state ^= *state << 13; + *state ^= *state >> 17; + *state ^= *state << 5; + return *state; +} + +static int test_rand_range(uint32_t *state, int min, int max) { + return min + (int)(test_rand_next(state) % (uint32_t)(max - min + 1)); +} + +static double test_rand_double(uint32_t *state, double lo, double hi) { + return lo + (hi - lo) * ((double)test_rand_next(state) / (double)UINT32_MAX); +} + +struct RandomIndexEntry { + OrderedIndexItem *node; + double score; + sds element; +}; + +static sds test_random_element(uint32_t *state, int maxLen) { + int len = test_rand_range(state, 1, maxLen); + sds s = sdsnewlen(NULL, len); + for (int i = 0; i < len; i++) s[i] = (char)test_rand_range(state, 'a', 'z'); + return s; +} + +static double test_random_score(uint32_t *state) { + return test_rand_double(state, -1e6, 1e6); +} + +static RandomIndexEntry *test_build_random_index(OrderedIndexTestApi &api, OrderedIndex *oi, uint32_t *state, int count) { + RandomIndexEntry *entries = (RandomIndexEntry *)zmalloc(sizeof(RandomIndexEntry) * count); + for (int i = 0; i < count; i++) { + double score = test_random_score(state); + sds elem = test_random_element(state, 16); + /* Append index to ensure uniqueness */ + elem = sdscatfmt(elem, "%i", i); + OrderedIndexItem *node = api.insert(oi, score, elem, sdslen(elem)); + entries[i] = {node, score, elem}; + } + return entries; +} + +static void freeRandomEntries(RandomIndexEntry *entries, int count) { + for (int i = 0; i < count; i++) sdsfree(entries[i].element); + zfree(entries); +} + +TEST_P(OrderedIndexTest, RandomizedInsertAndTraversal) { + uint32_t rng = test_fuzz_seed(); + for (int trial = 0; trial < 20; trial++) { + int n = test_rand_range(&rng, 1, 50); + + { + RandomIndexEntry *_e = test_build_random_index(api, oi, &rng, n); + freeRandomEntries(_e, n); + } + + ASSERT_EQ(api.length(oi), (unsigned long)n); + verifyOI(); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, oi); + int count = 0; + double prevScore = NEG_INF; + while (((pos = api.next(&iter)) != NULL)) { + double s = api.getScore(pos); + ASSERT_GE(s, prevScore); + prevScore = s; + count++; + } + ASSERT_EQ(count, n); + api.resetIterator(&iter); + api.free(oi); + oi = api.create(); + } +} + +TEST_P(OrderedIndexTest, RandomizedBackwardTraversal) { + uint32_t rng = test_fuzz_seed(); + for (int trial = 0; trial < 20; trial++) { + int n = test_rand_range(&rng, 1, 50); + + { + RandomIndexEntry *_e = test_build_random_index(api, oi, &rng, n); + freeRandomEntries(_e, n); + } + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, oi); + int count = 0; + double prevScore = POS_INF; + while (((pos = api.prev(&iter)) != NULL)) { + double s = api.getScore(pos); + ASSERT_LE(s, prevScore); + prevScore = s; + count++; + } + ASSERT_EQ(count, n); + api.resetIterator(&iter); + api.free(oi); + oi = api.create(); + } +} + +TEST_P(OrderedIndexTest, RandomizedScoreRetrieval) { + uint32_t rng = test_fuzz_seed(); + for (int trial = 0; trial < 20; trial++) { + int n = test_rand_range(&rng, 1, 50); + + RandomIndexEntry *entries = test_build_random_index(api, oi, &rng, n); + + for (int i = 0; i < n; i++) { + assertScore(entries[i].node, entries[i].score); + } + freeRandomEntries(entries, n); + api.free(oi); + oi = api.create(); + } +} + +TEST_P(OrderedIndexTest, RandomizedIndexConsistency) { + uint32_t rng = test_fuzz_seed(); + for (int trial = 0; trial < 20; trial++) { + int n = test_rand_range(&rng, 1, 50); + + { + RandomIndexEntry *_e = test_build_random_index(api, oi, &rng, n); + freeRandomEntries(_e, n); + } + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, oi); + unsigned long expectedIdx = 0; + while (((pos = api.next(&iter)) != NULL)) { + unsigned long idx = api.getIndex(oi, pos); + ASSERT_EQ(idx, expectedIdx); + OrderedIndexItem *byIdx = api.getByIndex(oi, expectedIdx); + ASSERT_EQ(byIdx, pos); + expectedIdx++; + } + ASSERT_EQ(expectedIdx, (unsigned long)n); + api.resetIterator(&iter); + api.free(oi); + oi = api.create(); + } +} + +TEST_P(OrderedIndexTest, RandomizedDelete) { + uint32_t rng = test_fuzz_seed(); + for (int trial = 0; trial < 20; trial++) { + int n = test_rand_range(&rng, 2, 30); + + RandomIndexEntry *entries = test_build_random_index(api, oi, &rng, n); + + int delIdx = test_rand_range(&rng, 0, n - 1); + api.deleteItem(oi, entries[delIdx].node); + + ASSERT_EQ(api.length(oi), (unsigned long)(n - 1)); + verifyOI(); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, oi); + int count = 0; + double prevScore = NEG_INF; + while (((pos = api.next(&iter)) != NULL)) { + ASSERT_GE(api.getScore(pos), prevScore); + prevScore = api.getScore(pos); + count++; + } + ASSERT_EQ(count, n - 1); + api.resetIterator(&iter); + freeRandomEntries(entries, n); + api.free(oi); + oi = api.create(); + } +} + +TEST_P(OrderedIndexTest, RandomizedUpdateScore) { + uint32_t rng = test_fuzz_seed(); + for (int trial = 0; trial < 20; trial++) { + int n = test_rand_range(&rng, 2, 30); + + RandomIndexEntry *entries = test_build_random_index(api, oi, &rng, n); + + int updIdx = test_rand_range(&rng, 0, n - 1); + double newScore = test_random_score(&rng); + + OrderedIndexItem *updated = api.updateScore(oi, entries[updIdx].node, newScore); + ASSERT_NE(updated, nullptr); + assertScore(updated, newScore); + ASSERT_EQ(api.length(oi), (unsigned long)n); + verifyOI(); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, oi); + double prevScore = NEG_INF; + while (((pos = api.next(&iter)) != NULL)) { + ASSERT_GE(api.getScore(pos), prevScore); + prevScore = api.getScore(pos); + } + api.resetIterator(&iter); + api.free(oi); + freeRandomEntries(entries, n); + oi = api.create(); + } +} + +TEST_P(OrderedIndexTest, RandomizedPop) { + uint32_t rng = test_fuzz_seed(); + for (int trial = 0; trial < 10; trial++) { + int n = test_rand_range(&rng, 3, 30); + + { + RandomIndexEntry *_e = test_build_random_index(api, oi, &rng, n); + freeRandomEntries(_e, n); + } + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, oi); + ASSERT_NE((pos = api.next(&iter)), nullptr); + double minScore = api.getScore(pos); + api.resetIterator(&iter); + + api.initIterator(&iter, oi); + ASSERT_NE((pos = api.prev(&iter)), nullptr); + double maxScore = api.getScore(pos); + api.resetIterator(&iter); + + OrderedIndexItem *first = api.popFirst(oi); + ASSERT_NE(first, nullptr); + assertScore(first, minScore); + ASSERT_EQ(api.length(oi), (unsigned long)(n - 1)); + api.freeItem(first); + verifyOI(); + + OrderedIndexItem *last = api.popLast(oi); + ASSERT_NE(last, nullptr); + assertScore(last, maxScore); + ASSERT_EQ(api.length(oi), (unsigned long)(n - 2)); + api.freeItem(last); + verifyOI(); + + api.initIterator(&iter, oi); + double prevScore = NEG_INF; + while (((pos = api.next(&iter)) != NULL)) { + ASSERT_GE(api.getScore(pos), prevScore); + prevScore = api.getScore(pos); + } + api.resetIterator(&iter); + api.free(oi); + oi = api.create(); + } +} + +TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { + uint32_t rng = test_fuzz_seed(); + for (int trial = 0; trial < 20; trial++) { + int n = test_rand_range(&rng, 5, 40); + + RandomIndexEntry *entries = test_build_random_index(api, oi, &rng, n); + + double s1 = test_random_score(&rng), s2 = test_random_score(&rng); + double lo = TEST_MIN(s1, s2), hi = TEST_MAX(s1, s2); + + int expectedDeleted = 0; + for (int i = 0; i < n; i++) { + if (entries[i].score >= lo && entries[i].score <= hi) expectedDeleted++; + } + + unsigned long deleted = api.deleteRangeByScore(oi, lo, hi, 0, 0, NULL, NULL); + ASSERT_EQ(deleted, (unsigned long)expectedDeleted); + ASSERT_EQ(api.length(oi), (unsigned long)(n - expectedDeleted)); + verifyOI(); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, oi); + double prevScore = NEG_INF; + while (((pos = api.next(&iter)) != NULL)) { + double s = api.getScore(pos); + ASSERT_TRUE(s < lo || s > hi); + ASSERT_GE(s, prevScore); + prevScore = s; + } + api.resetIterator(&iter); + api.free(oi); + freeRandomEntries(entries, n); + oi = api.create(); + } +} + +TEST_P(OrderedIndexTest, RandomizedDeleteRangeByIndex) { + uint32_t rng = test_fuzz_seed(); + for (int trial = 0; trial < 20; trial++) { + int n = test_rand_range(&rng, 5, 40); + + { + RandomIndexEntry *_e = test_build_random_index(api, oi, &rng, n); + freeRandomEntries(_e, n); + } + + int r1 = test_rand_range(&rng, 0, n - 1), r2 = test_rand_range(&rng, 0, n - 1); + unsigned long start = (unsigned long)TEST_MIN(r1, r2); + unsigned long end = (unsigned long)TEST_MAX(r1, r2); + unsigned long expectedDeleted = end - start + 1; + + unsigned long deleted = api.deleteRangeByIndex(oi, start, end, NULL, NULL); + ASSERT_EQ(deleted, expectedDeleted); + ASSERT_EQ(api.length(oi), (unsigned long)(n)-expectedDeleted); + verifyOI(); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, oi); + int remaining = 0; + double prevScore = NEG_INF; + while (((pos = api.next(&iter)) != NULL)) { + ASSERT_GE(api.getScore(pos), prevScore); + prevScore = api.getScore(pos); + remaining++; + } + ASSERT_EQ(remaining, n - (int)expectedDeleted); + api.resetIterator(&iter); + api.free(oi); + oi = api.create(); + } +} + +TEST_P(OrderedIndexTest, RandomizedForwardBackwardMirror) { + uint32_t rng = test_fuzz_seed(); + for (int trial = 0; trial < 20; trial++) { + int n = test_rand_range(&rng, 1, 50); + + { + RandomIndexEntry *_e = test_build_random_index(api, oi, &rng, n); + freeRandomEntries(_e, n); + } + + double *forwardScores = (double *)zmalloc(sizeof(double) * n); + OrderedIndexIterator iter; + OrderedIndexItem *pos; + int fi = 0; + api.initIterator(&iter, oi); + while (((pos = api.next(&iter)) != NULL)) { + forwardScores[fi++] = api.getScore(pos); + } + api.resetIterator(&iter); + + double *backwardScores = (double *)zmalloc(sizeof(double) * n); + int bi = 0; + api.initIterator(&iter, oi); + while (((pos = api.prev(&iter)) != NULL)) { + backwardScores[bi++] = api.getScore(pos); + } + api.resetIterator(&iter); + + ASSERT_EQ(fi, bi); + reverseDoubleArray(backwardScores, bi); + for (int i = 0; i < fi; i++) { + ASSERT_DOUBLE_EQ(forwardScores[i], backwardScores[i]); + } + zfree(forwardScores); + zfree(backwardScores); + api.free(oi); + oi = api.create(); + } +} + +/* ========== Count range tests ========== */ + +TEST_P(OrderedIndexTest, CountScoreRange) { + populateSequential(10); + + /* Full range */ + ASSERT_EQ(api.countScoreRange(oi, NEG_INF, POS_INF, 0, 0), 10UL); + + /* Inclusive [3, 6] */ + ASSERT_EQ(api.countScoreRange(oi, 3.0, 6.0, 0, 0), 4UL); + + /* Exclusive (3, 6) */ + ASSERT_EQ(api.countScoreRange(oi, 3.0, 6.0, 1, 1), 2UL); + + /* Single element [5, 5] */ + ASSERT_EQ(api.countScoreRange(oi, 5.0, 5.0, 0, 0), 1UL); + + /* Empty exclusive (5, 5) */ + ASSERT_EQ(api.countScoreRange(oi, 5.0, 5.0, 1, 0), 0UL); + + /* No match above */ + ASSERT_EQ(api.countScoreRange(oi, 10.0, 20.0, 0, 0), 0UL); + + /* No match below */ + ASSERT_EQ(api.countScoreRange(oi, -20.0, -10.0, 0, 0), 0UL); + + /* Min > max */ + ASSERT_EQ(api.countScoreRange(oi, 6.0, 3.0, 0, 0), 0UL); + + /* First element only [0, 0] */ + ASSERT_EQ(api.countScoreRange(oi, 0.0, 0.0, 0, 0), 1UL); + + /* Last element only [9, 9] */ + ASSERT_EQ(api.countScoreRange(oi, 9.0, 9.0, 0, 0), 1UL); +} + +TEST_P(OrderedIndexTest, CountScoreRangeEmpty) { + ASSERT_EQ(api.countScoreRange(oi, NEG_INF, POS_INF, 0, 0), 0UL); +} + +TEST_P(OrderedIndexTest, CountLexRange) { + for (int i = 0; i < FRUITS_COUNT; i++) insert(1.0, FRUITS[i]); + + ASSERT_EQ(countLexRange("banana", "date", 0, 0), 3UL); /* Inclusive [banana, date] */ + ASSERT_EQ(countLexRange("banana", "date", 1, 1), 1UL); /* Exclusive (banana, date) */ + ASSERT_EQ(countLexRange("cherry", "cherry", 0, 0), 1UL); /* Single element */ + ASSERT_EQ(countLexRange("fig", "grape", 0, 0), 0UL); /* No match */ + ASSERT_EQ(countLexRange("a", "z", 0, 0), 5UL); /* All elements */ +} + +TEST_P(OrderedIndexTest, CountLexRangeEmpty) { + ASSERT_EQ(countLexRange("a", "z", 0, 0), 0UL); +} + +/* ========== Instantiate parameterized tests for all implementations ========== */ + +INSTANTIATE_TEST_SUITE_P(AllImplementations, + OrderedIndexTest, + ::testing::Values(&skiplistImpl), + orderedIndexTestName); + +/* ========== On-Delete Callback Tests ========== */ + +struct OnDeleteRecord { + OrderedIndexTestApi *api; + int count; + int capacity; + sds *elements; /* Fixed-size array allocated at init */ +}; + +static void initOnDeleteRecord(OnDeleteRecord *rec, OrderedIndexTestApi *api, int capacity) { + rec->api = api; + rec->count = 0; + rec->capacity = capacity; + rec->elements = (sds *)zmalloc(sizeof(sds) * capacity); +} + +static void freeOnDeleteRecord(OnDeleteRecord *rec) { + for (int i = 0; i < rec->count; i++) sdsfree(rec->elements[i]); + zfree(rec->elements); +} + +static void testOnDeleteCallback(OrderedIndexItem *item, void *ctx) { + OnDeleteRecord *rec = (OnDeleteRecord *)ctx; + const char *ptr; + size_t len; + rec->api->getElementRaw(item, &ptr, &len); + rec->elements[rec->count] = sdsnewlen(ptr, len); + rec->count++; + /* Item is freed by the index after this callback returns. */ +} + +class OnDeleteCallbackTest : public ::testing::TestWithParam { + protected: + OrderedIndexTestApi *api = GetParam(); + OrderedIndex *oi = nullptr; + + void SetUp() override { + oi = api->create(); + } + void TearDown() override { + if (oi) api->free(oi); + } + + void verifyOI() { + char errmsg[256]; + ASSERT_TRUE(api->verifyIntegrity(oi, errmsg, sizeof(errmsg))) << errmsg; + } + + void insert(double score, const char *ele) { + api->insert(oi, score, ele, strlen(ele)); + } + + void insertN(int n) { + for (int i = 0; i < n; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + insert((double)i, buf); + } + } + + void insertLex(const char *elems[], int count, double score = 1.0) { + for (int i = 0; i < count; i++) { + insert(score, elems[i]); + } + } + + /* Collect elements into caller-owned sds array. Caller must freeSdsArray(). */ + sds *collectElements(OrderedIndex *idx, size_t *out_n) { + return collectIndexToSds(*api, idx, out_n); + } +}; + +/* DeleteRangeByScore */ + +TEST_P(OnDeleteCallbackTest, DeleteRangeByScore_EmptyAndNoMatch) { + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + + unsigned long deleted = api->deleteRangeByScore(oi, 0.0, 10.0, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(rec.count, 0); + api->free(oi); + + oi = api->create(); + insertN(5); + rec.count = 0; + deleted = api->deleteRangeByScore(oi, 10.0, 20.0, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(rec.count, 0); + ASSERT_EQ(api->length(oi), 5UL); + freeOnDeleteRecord(&rec); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByScore_Subset) { + insertN(10); + + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + unsigned long deleted = api->deleteRangeByScore(oi, 3.0, 6.0, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 4UL); + ASSERT_EQ(rec.count, 4); + ASSERT_EQ(api->length(oi), 6UL); + verifyOI(); + + sortSdsArray(rec.elements, rec.count); + ASSERT_SDS_ARRAY_EQ(rec.elements, rec.count, "key3", "key4", "key5", "key6"); + + { + size_t _rn; + sds *_r = collectElements(oi, &_rn); + ASSERT_SDS_ARRAY_EQ(_r, _rn, "key0", "key1", "key2", "key7", "key8", "key9"); + freeSdsArray(_r, _rn); + } + freeOnDeleteRecord(&rec); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByScore_All) { + insertN(5); + + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + unsigned long deleted = api->deleteRangeByScore(oi, NEG_INF, POS_INF, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 5UL); + ASSERT_EQ(rec.count, 5); + ASSERT_EQ(api->length(oi), 0UL); + verifyOI(); + freeOnDeleteRecord(&rec); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByScore_NullCallback) { + insertN(5); + + unsigned long deleted = api->deleteRangeByScore(oi, 1.0, 3.0, 0, 0, NULL, NULL); + ASSERT_EQ(deleted, 3UL); + ASSERT_EQ(api->length(oi), 2UL); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByScore_ExclusiveBounds) { + insertN(10); + + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + unsigned long deleted = api->deleteRangeByScore(oi, 3.0, 7.0, 1, 1, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 3UL); + ASSERT_EQ(rec.count, 3); + sortSdsArray(rec.elements, rec.count); + ASSERT_SDS_ARRAY_EQ(rec.elements, rec.count, "key4", "key5", "key6"); + ASSERT_EQ(api->length(oi), 7UL); + freeOnDeleteRecord(&rec); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByScore_SingleElement) { + insertN(5); + + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + unsigned long deleted = api->deleteRangeByScore(oi, 2.0, 2.0, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 1UL); + ASSERT_EQ(rec.count, 1); + ASSERT_STREQ(rec.elements[0], "key2"); + ASSERT_EQ(api->length(oi), 4UL); + freeOnDeleteRecord(&rec); +} + +/* DeleteRangeByIndex */ + +TEST_P(OnDeleteCallbackTest, DeleteRangeByIndex_EmptyAndNoMatch) { + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + + unsigned long deleted = api->deleteRangeByIndex(oi, 0, 4, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(rec.count, 0); + api->free(oi); + + oi = api->create(); + insertN(3); + rec.count = 0; + deleted = api->deleteRangeByIndex(oi, 10, 20, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(rec.count, 0); + ASSERT_EQ(api->length(oi), 3UL); + freeOnDeleteRecord(&rec); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByIndex_Subset) { + insertN(10); + + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + unsigned long deleted = api->deleteRangeByIndex(oi, 2, 4, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 3UL); + ASSERT_EQ(rec.count, 3); + ASSERT_EQ(api->length(oi), 7UL); + + sortSdsArray(rec.elements, rec.count); + ASSERT_SDS_ARRAY_EQ(rec.elements, rec.count, "key2", "key3", "key4"); + + { + size_t _rn; + sds *_r = collectElements(oi, &_rn); + ASSERT_SDS_ARRAY_EQ(_r, _rn, "key0", "key1", "key5", "key6", "key7", "key8", "key9"); + freeSdsArray(_r, _rn); + } + freeOnDeleteRecord(&rec); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByIndex_All) { + insertN(5); + + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + unsigned long deleted = api->deleteRangeByIndex(oi, 0, 4, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 5UL); + ASSERT_EQ(rec.count, 5); + ASSERT_EQ(api->length(oi), 0UL); + freeOnDeleteRecord(&rec); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByIndex_NullCallback) { + insertN(5); + + unsigned long deleted = api->deleteRangeByIndex(oi, 2, 4, NULL, NULL); + ASSERT_EQ(deleted, 3UL); + ASSERT_EQ(api->length(oi), 2UL); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByIndex_ExclusiveBounds) { + insertN(5); + + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + unsigned long deleted = api->deleteRangeByIndex(oi, 2, 2, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 1UL); + ASSERT_EQ(rec.count, 1); + ASSERT_STREQ(rec.elements[0], "key2"); + + { + size_t _rn; + sds *_r = collectElements(oi, &_rn); + ASSERT_SDS_ARRAY_EQ(_r, _rn, "key0", "key1", "key3", "key4"); + freeSdsArray(_r, _rn); + } + freeOnDeleteRecord(&rec); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByIndex_SingleElement) { + insertN(5); + + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + unsigned long deleted = api->deleteRangeByIndex(oi, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 1UL); + ASSERT_EQ(rec.count, 1); + ASSERT_STREQ(rec.elements[0], "key0"); + ASSERT_EQ(api->length(oi), 4UL); + freeOnDeleteRecord(&rec); +} + +/* DeleteRangeByLex */ + +TEST_P(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + + sds min = sdsnew("a"); + sds max = sdsnew("z"); + unsigned long deleted = api->deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(rec.count, 0); + sdsfree(min); + sdsfree(max); + api->free(oi); + + oi = api->create(); + { + const char *_l[] = {"apple", "banana", "cherry"}; + insertLex(_l, 3); + } + rec.count = 0; + min = sdsnew("x"); + max = sdsnew("z"); + deleted = api->deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(rec.count, 0); + ASSERT_EQ(api->length(oi), 3UL); + sdsfree(min); + sdsfree(max); + freeOnDeleteRecord(&rec); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByLex_Subset) { + { + const char *_l[] = {"apple", "banana", "cherry", "date", "elderberry"}; + insertLex(_l, 5); + } + + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + sds min = sdsnew("banana"); + sds max = sdsnew("date"); + unsigned long deleted = api->deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 3UL); + ASSERT_EQ(rec.count, 3); + ASSERT_EQ(api->length(oi), 2UL); + + sortSdsArray(rec.elements, rec.count); + ASSERT_SDS_ARRAY_EQ(rec.elements, rec.count, "banana", "cherry", "date"); + + { + size_t _rn; + sds *_r = collectElements(oi, &_rn); + ASSERT_SDS_ARRAY_EQ(_r, _rn, "apple", "elderberry"); + freeSdsArray(_r, _rn); + } + + sdsfree(min); + sdsfree(max); + freeOnDeleteRecord(&rec); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByLex_All) { + { + const char *_l[] = {"apple", "banana", "cherry"}; + insertLex(_l, 3); + } + + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + sds min = sdsnew("a"); + sds max = sdsnew("z"); + unsigned long deleted = api->deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 3UL); + ASSERT_EQ(rec.count, 3); + ASSERT_EQ(api->length(oi), 0UL); + + sdsfree(min); + sdsfree(max); + freeOnDeleteRecord(&rec); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByLex_NullCallback) { + { + const char *_l[] = {"apple", "banana", "cherry", "date"}; + insertLex(_l, 4); + } + + sds min = sdsnew("banana"); + sds max = sdsnew("cherry"); + unsigned long deleted = api->deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); + ASSERT_EQ(deleted, 2UL); + ASSERT_EQ(api->length(oi), 2UL); + + { + size_t _rn; + sds *_r = collectElements(oi, &_rn); + ASSERT_SDS_ARRAY_EQ(_r, _rn, "apple", "date"); + freeSdsArray(_r, _rn); + } + + sdsfree(min); + sdsfree(max); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByLex_ExclusiveBounds) { + { + const char *_l[] = {"apple", "banana", "cherry", "date", "elderberry"}; + insertLex(_l, 5); + } + + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + sds min = sdsnew("banana"); + sds max = sdsnew("date"); + unsigned long deleted = api->deleteRangeByLex(oi, min, max, 1, 1, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 1UL); + ASSERT_EQ(rec.count, 1); + ASSERT_STREQ(rec.elements[0], "cherry"); + ASSERT_EQ(api->length(oi), 4UL); + + { + size_t _rn; + sds *_r = collectElements(oi, &_rn); + ASSERT_SDS_ARRAY_EQ(_r, _rn, "apple", "banana", "date", "elderberry"); + freeSdsArray(_r, _rn); + } + + sdsfree(min); + sdsfree(max); + freeOnDeleteRecord(&rec); +} + +TEST_P(OnDeleteCallbackTest, DeleteRangeByLex_SingleElement) { + { + const char *_l[] = {"apple", "banana", "cherry"}; + insertLex(_l, 3); + } + + OnDeleteRecord rec; + initOnDeleteRecord(&rec, api, 10); + sds min = sdsnew("banana"); + sds max = sdsnew("banana"); + unsigned long deleted = api->deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 1UL); + ASSERT_EQ(rec.count, 1); + ASSERT_STREQ(rec.elements[0], "banana"); + ASSERT_EQ(api->length(oi), 2UL); + + { + size_t _rn; + sds *_r = collectElements(oi, &_rn); + ASSERT_SDS_ARRAY_EQ(_r, _rn, "apple", "cherry"); + freeSdsArray(_r, _rn); + } + + sdsfree(min); + sdsfree(max); + freeOnDeleteRecord(&rec); +} + +/* ========== Range-Delete Hashtable Consistency Tests ========== */ + +/* Simulated hashtable: sorted sds array (allows set-equality comparison). */ +struct SimHt { + sds *elems; + int count; + int capacity; +}; + +static void simHtInit(SimHt *ht, int cap) { + ht->elems = (sds *)zmalloc(sizeof(sds) * cap); + ht->count = 0; + ht->capacity = cap; +} + +static void simHtFree(SimHt *ht) { + for (int i = 0; i < ht->count; i++) sdsfree(ht->elems[i]); + zfree(ht->elems); +} + +static void simHtAdd(SimHt *ht, const char *s, size_t len) { + ht->elems[ht->count++] = sdsnewlen(s, len); +} + +static void simHtRemove(SimHt *ht, const char *s, size_t len) { + for (int i = 0; i < ht->count; i++) { + if (sdslen(ht->elems[i]) == len && memcmp(ht->elems[i], s, len) == 0) { + sdsfree(ht->elems[i]); + ht->elems[i] = ht->elems[--ht->count]; + return; + } + } +} + +static void simHtSort(SimHt *ht) { + sortSdsArray(ht->elems, ht->count); +} + +static void hashtableConsistencyOnDelete(OrderedIndexItem *item, void *ctx) { + /* ctx is a two-element array: [0]=SimHt*, [1]=OrderedIndexTestApi* */ + void **args = (void **)ctx; + SimHt *ht = (SimHt *)args[0]; + OrderedIndexTestApi *api = (OrderedIndexTestApi *)args[1]; + const char *ptr; + size_t len; + api->getElementRaw(item, &ptr, &len); + simHtRemove(ht, ptr, len); + /* Item is freed by the index after this callback returns. */ +} + +class RangeDeleteHashtableConsistencyTest : public ::testing::TestWithParam { + protected: + OrderedIndexTestApi *api = GetParam(); + OrderedIndex *oi = nullptr; + + void SetUp() override { + oi = api->create(); + } + void TearDown() override { + if (oi) api->free(oi); + } + + void insert(double score, const char *ele) { + api->insert(oi, score, ele, strlen(ele)); + } + + void insertN(SimHt &ht, int n) { + for (int i = 0; i < n; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + insert((double)i, buf); + simHtAdd(&ht, buf, strlen(buf)); + } + } + + void insertLex(SimHt &ht, const char *elems[], int count, double score = 1.0) { + for (int i = 0; i < count; i++) { + insert(score, elems[i]); + simHtAdd(&ht, elems[i], strlen(elems[i])); + } + } + + void assertHtMatchesIndex(SimHt &ht) { + size_t idx_n; + sds *idx_elems = collectIndexToSds(*api, oi, &idx_n); + sortSdsArray(idx_elems, idx_n); + simHtSort(&ht); + ASSERT_EQ(idx_n, (size_t)ht.count); + for (size_t i = 0; i < idx_n; i++) { + ASSERT_STREQ(idx_elems[i], ht.elems[i]); + } + freeSdsArray(idx_elems, idx_n); + } +}; + +/* ByScore */ + +TEST_P(RangeDeleteHashtableConsistencyTest, ByScore_PartialDelete) { + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); + void *cbCtx[] = {&simulatedHt, api}; + insertN(simulatedHt, 10); + + api->deleteRangeByScore(oi, 3.0, 6.0, 0, 0, hashtableConsistencyOnDelete, cbCtx); + + assertHtMatchesIndex(simulatedHt); + simHtFree(&simulatedHt); +} + +TEST_P(RangeDeleteHashtableConsistencyTest, ByScore_FullDelete) { + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); + void *cbCtx[] = {&simulatedHt, api}; + insertN(simulatedHt, 10); + + api->deleteRangeByScore(oi, NEG_INF, POS_INF, 0, 0, hashtableConsistencyOnDelete, cbCtx); + + assertHtMatchesIndex(simulatedHt); + simHtFree(&simulatedHt); +} + +TEST_P(RangeDeleteHashtableConsistencyTest, ByScore_EmptyRange) { + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); + void *cbCtx[] = {&simulatedHt, api}; + insertN(simulatedHt, 10); + + api->deleteRangeByScore(oi, 20.0, 30.0, 0, 0, hashtableConsistencyOnDelete, cbCtx); + + assertHtMatchesIndex(simulatedHt); + simHtFree(&simulatedHt); +} + +/* ByIndex */ + +TEST_P(RangeDeleteHashtableConsistencyTest, ByIndex_PartialDelete) { + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); + void *cbCtx[] = {&simulatedHt, api}; + insertN(simulatedHt, 10); + + api->deleteRangeByIndex(oi, 2, 4, hashtableConsistencyOnDelete, cbCtx); + + assertHtMatchesIndex(simulatedHt); + simHtFree(&simulatedHt); +} + +TEST_P(RangeDeleteHashtableConsistencyTest, ByIndex_FullDelete) { + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); + void *cbCtx[] = {&simulatedHt, api}; + insertN(simulatedHt, 10); + + api->deleteRangeByIndex(oi, 0, 9, hashtableConsistencyOnDelete, cbCtx); + + assertHtMatchesIndex(simulatedHt); + simHtFree(&simulatedHt); +} + +TEST_P(RangeDeleteHashtableConsistencyTest, ByIndex_EmptyRange) { + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); + void *cbCtx[] = {&simulatedHt, api}; + insertN(simulatedHt, 10); + + api->deleteRangeByIndex(oi, 20, 30, hashtableConsistencyOnDelete, cbCtx); + + assertHtMatchesIndex(simulatedHt); + simHtFree(&simulatedHt); +} + +/* ByLex */ + +TEST_P(RangeDeleteHashtableConsistencyTest, ByLex_PartialDelete) { + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); + void *cbCtx[] = {&simulatedHt, api}; + { + const char *_l[] = {"apple", "banana", "cherry", "date", "elderberry"}; + insertLex(simulatedHt, _l, 5); + } + + sds min = sdsnew("banana"); + sds max = sdsnew("date"); + api->deleteRangeByLex(oi, min, max, 0, 0, hashtableConsistencyOnDelete, cbCtx); + + assertHtMatchesIndex(simulatedHt); + + sdsfree(min); + sdsfree(max); + simHtFree(&simulatedHt); +} + +TEST_P(RangeDeleteHashtableConsistencyTest, ByLex_FullDelete) { + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); + void *cbCtx[] = {&simulatedHt, api}; + { + const char *_l[] = {"apple", "banana", "cherry", "date", "elderberry"}; + insertLex(simulatedHt, _l, 5); + } + + sds min = sdsnew("a"); + sds max = sdsnew("z"); + api->deleteRangeByLex(oi, min, max, 0, 0, hashtableConsistencyOnDelete, cbCtx); + + assertHtMatchesIndex(simulatedHt); + + sdsfree(min); + sdsfree(max); + simHtFree(&simulatedHt); +} + +TEST_P(RangeDeleteHashtableConsistencyTest, ByLex_EmptyRange) { + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); + void *cbCtx[] = {&simulatedHt, api}; + { + const char *_l[] = {"apple", "banana", "cherry", "date", "elderberry"}; + insertLex(simulatedHt, _l, 5); + } + + sds min = sdsnew("zzz"); + sds max = sdsnew("zzzz"); + api->deleteRangeByLex(oi, min, max, 0, 0, hashtableConsistencyOnDelete, cbCtx); + + assertHtMatchesIndex(simulatedHt); + + sdsfree(min); + sdsfree(max); + simHtFree(&simulatedHt); +} + +INSTANTIATE_TEST_SUITE_P(AllImplementations, + OnDeleteCallbackTest, + ::testing::Values(&skiplistImpl), + orderedIndexTestName); + +INSTANTIATE_TEST_SUITE_P(AllImplementations, + RangeDeleteHashtableConsistencyTest, + ::testing::Values(&skiplistImpl), + orderedIndexTestName); diff --git a/src/valkey-check-rdb.c b/src/valkey-check-rdb.c index 7f82e98a50d..fe27f00957d 100644 --- a/src/valkey-check-rdb.c +++ b/src/valkey-check-rdb.c @@ -29,7 +29,7 @@ #include "mt19937-64.h" #include "server.h" -#include "skiplist.h" +#include "ordered_index.h" #include "rdb.h" #include "module.h" #include "hdr_histogram.h" @@ -324,13 +324,15 @@ void computeDatasetProfile(int dbid, robj *keyobj, robj *o, long long expiretime void *next; while (hashtableNext(&iter, &next)) { - zskiplistNode *node = next; + OrderedIndexItem *node = next; size_t eleLen = 0; + const char *ele; + size_t ele_len; + orderedIndexGetElementRaw(node, &ele, &ele_len); - const int len = fpconv_dtoa(node->score, buf); + const int len = fpconv_dtoa(orderedIndexGetScore(node), buf); buf[len] = '\0'; - sds ele = zslGetNodeElement(node); - eleLen += sdslen(ele) + strlen(buf); + eleLen += ele_len + strlen(buf); statsRecordElementSize(eleLen, 1, stats); } hashtableCleanupIterator(&iter); diff --git a/tests/support/util.tcl b/tests/support/util.tcl index 7a3674f2a22..f5abfd61bef 100644 --- a/tests/support/util.tcl +++ b/tests/support/util.tcl @@ -1314,3 +1314,14 @@ proc memcmp {string1 string2} { } return [expr {$len1 - $len2}] } + +# Execute body with a temporary config override, restoring the original +# value even if the body fails. +proc with_config {config value body} { + set old [lindex [r config get $config] 1] + r config set $config $value + catch {uplevel 1 $body} result opts + r config set $config $old + dict incr opts -level + return -options $opts $result +} diff --git a/tests/unit/sort.tcl b/tests/unit/sort.tcl index 3838a8e05a5..fb6bd377c64 100644 --- a/tests/unit/sort.tcl +++ b/tests/unit/sort.tcl @@ -171,6 +171,51 @@ foreach command {SORT SORT_RO} { assert_equal [r sort zset by nosort limit -10 100] {a c e b d} } + test "SORT sorted set skiplist BY nosort should retain ordering" { + with_config zset-max-ziplist-entries 0 { + r del zset + r zadd zset 1 a + r zadd zset 5 b + r zadd zset 2 c + r zadd zset 10 d + r zadd zset 3 e + assert_encoding skiplist zset + assert_equal [r sort zset by nosort asc] {a c e b d} + assert_equal [r sort zset by nosort desc] {d b e c a} + } + } + + test "SORT sorted set skiplist BY nosort + LIMIT" { + with_config zset-max-ziplist-entries 0 { + r del zset + r zadd zset 1 a + r zadd zset 5 b + r zadd zset 2 c + r zadd zset 10 d + r zadd zset 3 e + assert_encoding skiplist zset + assert_equal [r sort zset by nosort asc limit 0 1] {a} + assert_equal [r sort zset by nosort desc limit 0 1] {d} + assert_equal [r sort zset by nosort asc limit 0 2] {a c} + assert_equal [r sort zset by nosort desc limit 0 2] {d b} + assert_equal [r sort zset by nosort limit 5 10] {} + assert_equal [r sort zset by nosort limit -10 100] {a c e b d} + } + } + + test "SORT sorted set skiplist with BY pattern" { + with_config zset-max-ziplist-entries 0 { + r del zset + r zadd zset 1 a + r zadd zset 5 b + r zadd zset 2 c + r zadd zset 10 d + r zadd zset 3 e + assert_encoding skiplist zset + assert_equal [r sort zset alpha desc] {e d c b a} + } + } + test "SORT sorted set BY nosort works as expected from scripts" { r del zset r zadd zset 1 a diff --git a/tests/unit/type/zset.tcl b/tests/unit/type/zset.tcl index bbe80b9995b..06fccf35939 100644 --- a/tests/unit/type/zset.tcl +++ b/tests/unit/type/zset.tcl @@ -2249,29 +2249,29 @@ start_server {tags {"zset"}} { } {0} {cluster:skip} test {ZSET skiplist order consistency when elements are moved} { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set zset-max-ziplist-entries 0 - for {set times 0} {$times < 10} {incr times} { - r del zset - for {set j 0} {$j < 1000} {incr j} { - r zadd zset [randomInt 50] ele-[randomInt 10] - } + with_config zset-max-ziplist-entries 0 { + for {set times 0} {$times < 10} {incr times} { + r del zset + for {set j 0} {$j < 1000} {incr j} { + r zadd zset [randomInt 50] ele-[randomInt 10] + } - # Make sure that element ordering is correct - set prev_element {} - set prev_score -1 - foreach {element score} [r zrange zset 0 -1 WITHSCORES] { - # Assert that elements are in increasing ordering - assert { - $prev_score < $score || - ($prev_score == $score && - [string compare $prev_element $element] == -1) + # Make sure that element ordering is correct + set prev_element {} + set prev_score -1 + foreach {element score} [r zrange zset 0 -1 WITHSCORES] { + # Assert that elements are in increasing ordering + assert { + $prev_score < $score || + ($prev_score == $score && + [string compare $prev_element $element] == -1) + } + set prev_element $element + set prev_score $score } - set prev_element $element - set prev_score $score } + } - r config set zset-max-ziplist-entries $original_max } test {ZRANGESTORE basic} { @@ -2949,3 +2949,176 @@ start_server [list overrides [list save ""] tags {"zset needs:debug external:ski assert_equal 1 $can_break } } + +start_server {tags {"zset" "cluster:skip"}} { + test {ZUNIONSTORE with skiplist-encoded inputs} { + with_config zset-max-ziplist-entries 0 { + + r del src1 src2 dst + r zadd src1 1 a 2 b 3 c + r zadd src2 2 b 4 d 5 e + assert_encoding skiplist src1 + assert_encoding skiplist src2 + + assert_equal 5 [r zunionstore dst 2 src1 src2] + assert_encoding skiplist dst + assert_equal {a 1 c 3 b 4 d 4 e 5} [r zrange dst 0 -1 WITHSCORES] + } + } + + test {ZUNIONSTORE with skiplist-encoded inputs and WEIGHTS} { + with_config zset-max-ziplist-entries 0 { + + r del src1 src2 dst + r zadd src1 1 a 2 b + r zadd src2 3 b 4 c + assert_encoding skiplist src1 + assert_encoding skiplist src2 + + assert_equal 3 [r zunionstore dst 2 src1 src2 WEIGHTS 2 1] + assert_equal {a 2 c 4 b 7} [r zrange dst 0 -1 WITHSCORES] + } + } + + test {ZINTERSTORE with skiplist-encoded inputs} { + with_config zset-max-ziplist-entries 0 { + + r del src1 src2 dst + r zadd src1 1 a 2 b 3 c + r zadd src2 10 b 20 c 30 d + assert_encoding skiplist src1 + assert_encoding skiplist src2 + + assert_equal 2 [r zinterstore dst 2 src1 src2] + assert_equal {b 12 c 23} [r zrange dst 0 -1 WITHSCORES] + } + } + + test {ZINTERSTORE with skiplist-encoded inputs and AGGREGATE MIN} { + with_config zset-max-ziplist-entries 0 { + + r del src1 src2 dst + r zadd src1 5 a 10 b + r zadd src2 1 a 20 b + assert_encoding skiplist src1 + assert_encoding skiplist src2 + + assert_equal 2 [r zinterstore dst 2 src1 src2 AGGREGATE MIN] + assert_equal {a 1 b 10} [r zrange dst 0 -1 WITHSCORES] + } + } + + test {ZRANGEBYSCORE with LIMIT on skiplist-encoded set} { + with_config zset-max-ziplist-entries 0 { + + r del zset + for {set i 0} {$i < 20} {incr i} { + r zadd zset $i "key:$i" + } + assert_encoding skiplist zset + + # Forward with offset and count + assert_equal {key:5 key:6 key:7} [r zrangebyscore zset 0 19 LIMIT 5 3] + # Reverse with offset and count + assert_equal {key:14 key:13 key:12} [r zrevrangebyscore zset 19 0 LIMIT 5 3] + # Offset past end + assert_equal {} [r zrangebyscore zset 0 19 LIMIT 25 5] + # Count of 0 + assert_equal {} [r zrangebyscore zset 0 19 LIMIT 0 0] + } + } + + test {ZRANGEBYLEX with LIMIT on skiplist-encoded set} { + with_config zset-max-ziplist-entries 0 { + + r del zset + foreach elem {a b c d e f g h i j} { + r zadd zset 0 $elem + } + assert_encoding skiplist zset + + # Forward with offset and count + assert_equal {d e f} [r zrangebylex zset "\[a" "\[j" LIMIT 3 3] + # Reverse with offset and count + assert_equal {g f e} [r zrevrangebylex zset "\[j" "\[a" LIMIT 3 3] + # Offset past end + assert_equal {} [r zrangebylex zset "\[a" "\[j" LIMIT 15 5] + } + } + + test {ZPOPMIN on skiplist-encoded set} { + with_config zset-max-ziplist-entries 0 { + + r del zset + r zadd zset 3 c 1 a 2 b 5 e 4 d + assert_encoding skiplist zset + + assert_equal {a 1} [r zpopmin zset] + assert_equal {b 2} [r zpopmin zset] + assert_equal 3 [r zcard zset] + + # Pop multiple + assert_equal {c 3 d 4} [r zpopmin zset 2] + assert_equal 1 [r zcard zset] + } + } + + test {ZPOPMAX on skiplist-encoded set} { + with_config zset-max-ziplist-entries 0 { + + r del zset + r zadd zset 3 c 1 a 2 b 5 e 4 d + assert_encoding skiplist zset + + assert_equal {e 5} [r zpopmax zset] + assert_equal {d 4} [r zpopmax zset] + assert_equal 3 [r zcard zset] + + # Pop multiple + assert_equal {c 3 b 2} [r zpopmax zset 2] + assert_equal 1 [r zcard zset] + } + } + + test {ZPOPMIN/ZPOPMAX empty skiplist-encoded set} { + with_config zset-max-ziplist-entries 0 { + + r del zset + r zadd zset 1 a + r zpopmin zset + assert_equal 0 [r exists zset] + } + } + + test {ZCOUNT on skiplist-encoded set} { + with_config zset-max-ziplist-entries 0 { + + r del zset + for {set i 0} {$i < 10} {incr i} { + r zadd zset $i "key:$i" + } + assert_encoding skiplist zset + + assert_equal 10 [r zcount zset -inf +inf] + assert_equal 4 [r zcount zset 3 6] + assert_equal 2 [r zcount zset (3 (6] + assert_equal 0 [r zcount zset 20 30] + } + } + + test {ZLEXCOUNT on skiplist-encoded set} { + with_config zset-max-ziplist-entries 0 { + + r del zset + foreach elem {a b c d e f} { + r zadd zset 0 $elem + } + assert_encoding skiplist zset + + assert_equal 6 [r zlexcount zset - +] + assert_equal 3 [r zlexcount zset "\[b" "\[d"] + assert_equal 1 [r zlexcount zset "(b" "(d"] + assert_equal 0 [r zlexcount zset "\[x" "\[z"] + } + } +}