From 0e3493e148d5c28d3fef51ae3c978b2e5117abf2 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Sun, 26 Apr 2026 00:23:12 +0000 Subject: [PATCH 01/45] zset: fix NULL deref in zslIsInLexRange on empty skiplist zslIsInLexRange called zslGetNodeElement on the tail node before checking if it was NULL, crashing on an empty skiplist. Move the NULL checks before the element access. Same issue existed for the head node check. Signed-off-by: Rain Valentine --- src/skiplist.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/skiplist.c b/src/skiplist.c index 84b00437508..5c3300c99c5 100644 --- a/src/skiplist.c +++ b/src/skiplist.c @@ -583,12 +583,14 @@ static int zslIsInLexRange(zskiplist *zsl, zlexrangespec *range) { int cmp = sdscmplex(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 (!zslLexValueGteMin(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 (!zslLexValueLteMax(ele, range)) return 0; return 1; } From 3d2a789e787fddb37a89511cf08a7c687e77e141 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Sun, 26 Apr 2026 00:24:53 +0000 Subject: [PATCH 02/45] ordered-index: add interface, skiplist backend, and unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the OrderedIndex abstraction layer for sorted set data structures: - ordered_index.h: compile-time dispatched interface with inline wrappers for lifecycle, modification, query, iteration, memory, defrag, and debug - skiplist_ordered_index.h/c: skiplist backend implementation - Thin wrappers casting between opaque types and zskiplist/zskiplistNode - Range deletion by score, rank, and lex with on_delete callback - Count range by score and lex - Cursor-based batched defrag scan - Structural integrity verification - ordered_index_test.h: parameterized C++ test fixture for future backends - test_ordered_index.cpp: comprehensive unit tests - Deterministic tests for all interface operations - Randomized property tests (insert, traverse, delete, update, pop) - On-delete callback tests for range deletion - Hashtable consistency tests for range deletion No existing call sites are converted — the interface is only exercised by the new unit tests. Call-site conversion follows in subsequent PRs. Signed-off-by: Rain Valentine --- cmake/Modules/SourceFiles.cmake | 1 + src/Makefile | 1 + src/ordered_index.h | 187 +++ src/skiplist.c | 114 +- src/skiplist.h | 24 +- src/skiplist_ordered_index.c | 506 +++++++ src/skiplist_ordered_index.h | 54 + src/t_zset.c | 2 +- src/unit/ordered_index_test.h | 191 +++ src/unit/test_ordered_index.cpp | 2456 +++++++++++++++++++++++++++++++ 10 files changed, 3531 insertions(+), 5 deletions(-) create mode 100644 src/ordered_index.h create mode 100644 src/skiplist_ordered_index.c create mode 100644 src/skiplist_ordered_index.h create mode 100644 src/unit/ordered_index_test.h create mode 100644 src/unit/test_ordered_index.cpp diff --git a/cmake/Modules/SourceFiles.cmake b/cmake/Modules/SourceFiles.cmake index 866c4f9169f..fabb5ba041a 100644 --- a/cmake/Modules/SourceFiles.cmake +++ b/cmake/Modules/SourceFiles.cmake @@ -34,6 +34,7 @@ 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/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..10546a8482f 100644 --- a/src/Makefile +++ b/src/Makefile @@ -555,6 +555,7 @@ ENGINE_SERVER_OBJ = \ sha1.o \ sha256.o \ siphash.o \ + skiplist_ordered_index.o \ socket.o \ sort.o \ sparkline.o \ diff --git a/src/ordered_index.h b/src/ordered_index.h new file mode 100644 index 00000000000..9db801cf84a --- /dev/null +++ b/src/ordered_index.h @@ -0,0 +1,187 @@ +#ifndef ORDERED_INDEX_H +#define ORDERED_INDEX_H + +#include "sds.h" + +/* Opaque types for ordered index, positions, and iterators */ +typedef struct OrderedIndex OrderedIndex; +typedef struct OrderedIndexItem OrderedIndexItem; +typedef uint64_t OrderedIndexIterator[2]; + +/* Callback invoked for each item removed during a range-delete operation. */ +typedef void (*OrderedIndexOnDelete)(OrderedIndexItem *item, void *ctx); + +/* ---- Production inline wrappers ---- + * + * Currently hardcoded to the skiplist implementation. When additional ordered + * index backends are added (e.g. B-tree), a compile-time switch can select + * the implementation here without changing any call sites. */ +#include "skiplist_ordered_index.h" + +/* Lifecycle */ +static inline OrderedIndex *orderedIndexCreate(void) { + return skiplistCreate(); +} + +static inline void orderedIndexFree(OrderedIndex *idx) { + skiplistFree(idx); +} + +/* Modification */ +static inline OrderedIndexItem *orderedIndexInsertRaw(OrderedIndex *idx, double score, const char *ele, size_t len) { + return skiplistInsert(idx, score, ele, len); +} + +static inline OrderedIndexItem *orderedIndexInsert(OrderedIndex *idx, double score, const_sds ele) { + return skiplistInsert(idx, score, ele, sdslen(ele)); +} + +static inline void orderedIndexDelete(OrderedIndex *idx, OrderedIndexItem *pos) { + skiplistDelete(idx, pos); +} + +static inline OrderedIndexItem *orderedIndexUpdateScore(OrderedIndex *idx, OrderedIndexItem *pos, double newscore) { + return skiplistUpdateScore(idx, pos, newscore); +} + +static inline OrderedIndexItem *orderedIndexPopFirst(OrderedIndex *idx) { + return skiplistPopFirst(idx); +} + +static inline OrderedIndexItem *orderedIndexPopLast(OrderedIndex *idx) { + return skiplistPopLast(idx); +} + +static inline void orderedIndexFreeItem(OrderedIndexItem *item) { + skiplistFreeItem(item); +} + +static inline OrderedIndexItem *orderedIndexCreateDetached(double score, const char *ele, size_t len) { + return skiplistCreateDetached(score, ele, len); +} + +/* Set the score on a detached item — one created via orderedIndexCreateDetached + * but not yet inserted into an ordered index. Items that live only in a + * hashtable (e.g. during ZUNIONSTORE score accumulation) are still considered + * "detached" for this purpose because they are not part of any ordered index. + * + * Do NOT use on items that have been inserted into an ordered index — doing so + * would silently corrupt the sort order. Use orderedIndexUpdateScore instead. */ +static inline void orderedIndexDetachedSetScore(OrderedIndexItem *item, double score) { + skiplistDetachedSetScore(item, score); +} + +static inline OrderedIndexItem *orderedIndexInsertDetached(OrderedIndex *idx, OrderedIndexItem *item) { + return skiplistInsertDetached(idx, item); +} + +static inline unsigned long orderedIndexDeleteRangeByScore(OrderedIndex *idx, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) { + return skiplistDeleteRangeByScore(idx, min, max, min_ex, max_ex, on_delete, ctx); +} + +static inline unsigned long orderedIndexDeleteRangeByRank(OrderedIndex *idx, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) { + return skiplistDeleteRangeByRank(idx, start, end, on_delete, ctx); +} + +static inline unsigned long orderedIndexDeleteRangeByLex(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) { + return skiplistDeleteRangeByLex(idx, min, max, min_ex, max_ex, on_delete, ctx); +} + +/* Query */ +static inline unsigned long orderedIndexLength(OrderedIndex *idx) { + return skiplistLength(idx); +} + +static inline OrderedIndexItem *orderedIndexGetByRank(OrderedIndex *idx, unsigned long rank) { + return skiplistGetByRank(idx, rank); +} + +static inline unsigned long orderedIndexGetRank(OrderedIndex *idx, const OrderedIndexItem *pos) { + return skiplistGetRank(idx, pos); +} + +static inline void orderedIndexGetElementRaw(const OrderedIndexItem *pos, const char **ptr, size_t *len) { + skiplistGetElementRaw(pos, ptr, len); +} + +static inline double orderedIndexGetScore(const OrderedIndexItem *pos) { + return skiplistGetScore(pos); +} + +static inline unsigned long orderedIndexCountScoreRange(OrderedIndex *idx, double min, double max, int min_ex, int max_ex) { + return skiplistCountScoreRange(idx, min, max, min_ex, max_ex); +} + +static inline unsigned long orderedIndexCountLexRange(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex) { + return skiplistCountLexRange(idx, min, max, min_ex, max_ex); +} + +/* Iterator */ +static inline void orderedIndexInitIterator(OrderedIndexIterator *iter, OrderedIndex *idx) { + skiplistInitIterator(iter, idx); +} + +static inline void orderedIndexResetIterator(OrderedIndexIterator *iter) { + skiplistResetIterator(iter); +} + +static inline bool orderedIndexNext(OrderedIndexIterator *iter, OrderedIndexItem **pos) { + return skiplistNext(iter, pos); +} + +static inline bool orderedIndexPrev(OrderedIndexIterator *iter, OrderedIndexItem **pos) { + return skiplistPrev(iter, pos); +} + +static inline void orderedIndexSeekToRank(OrderedIndexIterator *iter, unsigned long rank) { + skiplistSeekToRank(iter, rank); +} + +/* Seek to a position within a score/lex range. + * + * offset >= 0: positions for forward iteration (next() returns the element). + * offset 0 = first element in range, 1 = second, etc. + * offset < 0: positions for reverse iteration (prev() returns the element). + * offset -1 = last element in range, -2 = second-to-last, etc. */ +static inline 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); +} + +static inline 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 */ +static inline void orderedIndexDismissMemory(OrderedIndex *idx) { + skiplistDismissMemory(idx); +} + +static inline size_t orderedIndexEstimateMemory(OrderedIndex *idx, size_t sample_size) { + return skiplistEstimateMemory(idx, sample_size); +} + +/* Defrag */ +typedef void (*OrderedIndexDefragCallback)(OrderedIndexItem *old_item, OrderedIndexItem *new_item, void *ctx); + +static inline OrderedIndex *orderedIndexDefragInternals(OrderedIndex *idx, void *(*defragfn)(void *)) { + return skiplistDefragInternals(idx, defragfn); +} + +/* Cursor-based incremental defrag. Walks the ordered index in batches, + * calling defragfn on each item. When an item is reallocated, the callback + * is invoked so the caller can update external references (e.g. hashtable). + * Returns the next cursor, or 0 when the scan is complete. */ +static inline unsigned long orderedIndexScanDefrag(OrderedIndex *idx, unsigned long cursor, OrderedIndexDefragCallback callback, void *ctx, void *(*defragfn)(void *)) { + return skiplistScanDefrag(idx, cursor, callback, ctx, defragfn); +} + +/* Debug */ +static inline int orderedIndexGetHeight(OrderedIndex *idx) { + return skiplistGetHeight(idx); +} + +static inline int orderedIndexVerifyIntegrity(OrderedIndex *idx, char *errmsg, size_t errmsg_len) { + return skiplistVerifyIntegrity(idx, errmsg, errmsg_len); +} + +#endif /* ORDERED_INDEX_H */ diff --git a/src/skiplist.c b/src/skiplist.c index 5c3300c99c5..599ad3eb32c 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,7 +227,7 @@ 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; } @@ -671,3 +671,111 @@ zskiplistNode *zslNthInLexRange(zskiplist *zsl, zlexrangespec *range, long n) { return x; } + +/* --- Accessors added for OrderedIndex --- */ + +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 (x->level[i].forward && x->level[i].forward != node) { + x = x->level[i].forward; + } + update[i] = x; + } + serverAssert(x->level[0].forward == node); + zslDeleteNode(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); +} + +bool zslNext(zslIter *iter, zskiplistNode **nodeptr) { + if (iter->zsl == NULL) { *nodeptr = NULL; return false; } + if (iter->node == NULL) { + iter->node = zslGetHeader(iter->zsl)->level[0].forward; + } else if (iter->node == zslGetTail(iter->zsl)) { + *nodeptr = NULL; return false; + } else { + iter->node = iter->node->level[0].forward; + } + if (iter->node == NULL) { *nodeptr = NULL; return false; } + *nodeptr = iter->node; + return true; +} + +bool zslPrev(zslIter *iter, zskiplistNode **nodeptr) { + if (iter->zsl == NULL) { *nodeptr = NULL; return false; } + if (iter->node == zslGetHeader(iter->zsl)) { + *nodeptr = NULL; return false; + } + if (iter->node == NULL) { + iter->node = zslGetTail(iter->zsl); + if (iter->node == NULL) { *nodeptr = NULL; return false; } + } + zskiplistNode *ret = iter->node; + iter->node = iter->node->backward; + if (iter->node == NULL) iter->node = zslGetHeader(iter->zsl); + *nodeptr = ret; + return true; +} + +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..6e9fefbf9ef 100644 --- a/src/skiplist.h +++ b/src/skiplist.h @@ -37,6 +37,7 @@ #define SKIPLIST_H #include "server.h" +#include /* * This skiplist implementation is almost a C translation of the original @@ -131,7 +132,7 @@ 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); @@ -161,4 +162,25 @@ 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); +bool zslNext(zslIter *iter, zskiplistNode **nodeptr); +bool zslPrev(zslIter *iter, zskiplistNode **nodeptr); +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..44b2e8cce77 --- /dev/null +++ b/src/skiplist_ordered_index.c @@ -0,0 +1,506 @@ +#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 *idx) { + zslFree((zskiplist *)idx); +} + +/* Modification */ + +OrderedIndexItem *skiplistInsert(OrderedIndex *idx, double score, const char *ele, size_t len) { + zskiplistNode *node = zslCreateNode(zslRandomLevel(), score, ele, len); + zslInsertNode((zskiplist *)idx, node); + return (OrderedIndexItem *)node; +} + +void skiplistDelete(OrderedIndex *idx, OrderedIndexItem *node) { + zslDelete((zskiplist *)idx, (zskiplistNode *)node); +} + +OrderedIndexItem *skiplistUpdateScore(OrderedIndex *idx, OrderedIndexItem *node, double newscore) { + zskiplistNode *result = zslUpdateScore((zskiplist *)idx, (zskiplistNode *)node, newscore); + return result ? (OrderedIndexItem *)result : (OrderedIndexItem *)node; +} + +OrderedIndexItem *skiplistPopFirst(OrderedIndex *idx) { + zskiplist *zsl = (zskiplist *)idx; + zskiplistNode *first = zslGetFirst(zsl); + if (!first) return NULL; + zslDetachNode(zsl, first); + return (OrderedIndexItem *)first; +} + +OrderedIndexItem *skiplistPopLast(OrderedIndex *idx) { + zskiplist *zsl = (zskiplist *)idx; + 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 *idx, OrderedIndexItem *item) { + zskiplistNode *node = zslInsertNode((zskiplist *)idx, (zskiplistNode *)item); + return (OrderedIndexItem *)node; +} + +unsigned long skiplistDeleteRangeByScore(OrderedIndex *idx, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) { + zskiplist *zsl = (zskiplist *)idx; + 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 && !zslValueGteMin(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 && zslValueLteMax(x->score, &range)) { + zskiplistNode *next = x->level[0].forward; + zslDeleteNode(zsl, x, update); + if (on_delete) { + on_delete((OrderedIndexItem *)x, ctx); + } + zslFreeNode(x); + removed++; + x = next; + } + return removed; +} + +unsigned long skiplistDeleteRangeByRank(OrderedIndex *idx, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) { + zskiplist *zsl = (zskiplist *)idx; + zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; + unsigned long traversed = 0, removed = 0; + int i; + + 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; + zslDeleteNode(zsl, x, update); + if (on_delete) { + on_delete((OrderedIndexItem *)x, ctx); + } + zslFreeNode(x); + removed++; + traversed++; + x = next; + } + return removed; +} + +unsigned long skiplistDeleteRangeByLex(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) { + zskiplist *zsl = (zskiplist *)idx; + 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 (zslLexValueGteMin(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 (!zslLexValueLteMax(ele, &range)) break; + zskiplistNode *next = x->level[0].forward; + zslDeleteNode(zsl, x, update); + if (on_delete) { + on_delete((OrderedIndexItem *)x, ctx); + } + zslFreeNode(x); + removed++; + x = next; + } + return removed; +} + +/* Query */ + +unsigned long skiplistLength(OrderedIndex *idx) { + return zslGetLength((zskiplist *)idx); +} + +OrderedIndexItem *skiplistGetByRank(OrderedIndex *idx, unsigned long rank) { + return (OrderedIndexItem *)zslGetElementByRank((zskiplist *)idx, rank); +} + +unsigned long skiplistGetRank(OrderedIndex *idx, const OrderedIndexItem *node) { + return zslGetRank((zskiplist *)idx, (const zskiplistNode *)node); +} + +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 *idx, double min, double max, int min_ex, int max_ex) { + zskiplist *zsl = (zskiplist *)idx; + 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 *idx, const_sds min, const_sds max, int min_ex, int max_ex) { + zskiplist *zsl = (zskiplist *)idx; + 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 *idx) { + zslInitIterator((zslIter *)iter, (zskiplist *)idx); +} + +void skiplistResetIterator(OrderedIndexIterator *iter) { + zslResetIterator((zslIter *)iter); +} + +bool skiplistNext(OrderedIndexIterator *iter, OrderedIndexItem **pos) { + return zslNext((zslIter *)iter, (zskiplistNode **)pos); +} + +bool skiplistPrev(OrderedIndexIterator *iter, OrderedIndexItem **pos) { + return zslPrev((zslIter *)iter, (zskiplistNode **)pos); +} + +void skiplistSeekToRank(OrderedIndexIterator *iter, unsigned long rank) { + zslSeekToRank((zslIter *)iter, rank); +} + +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 *idx) { + zskiplist *zsl = (zskiplist *)idx; + zskiplistNode *zn = zslGetTail(zsl); + while (zn != NULL) { + zskiplistNode *prev = zn->backward; + dismissMemory(zn, 0); + zn = prev; + } +} + +size_t skiplistEstimateMemory(OrderedIndex *idx, size_t sample_size) { + zskiplist *zsl = (zskiplist *)idx; + 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 *idx, void *(*defragfn)(void *)) { + OrderedIndex *newidx = defragfn(idx); + return newidx; /* 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 *idx, unsigned long cursor, void (*callback)(OrderedIndexItem *old_item, OrderedIndexItem *new_item, void *ctx), void *ctx, void *(*defragfn)(void *)) { + zskiplist *zsl = (zskiplist *)idx; + 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 *idx) { + return zslGetHeight((zskiplist *)idx); +} + +/* Verify the structural integrity of the skiplist. + * Returns 1 if valid, 0 if corrupt (with a description in errmsg). */ +int skiplistVerifyIntegrity(OrderedIndex *idx, char *errmsg, size_t errmsg_len) { + zskiplist *zsl = (zskiplist *)idx; + 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 + 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..985856d2c4c --- /dev/null +++ b/src/skiplist_ordered_index.h @@ -0,0 +1,54 @@ +#ifndef SKIPLIST_ORDERED_INDEX_H +#define SKIPLIST_ORDERED_INDEX_H + +#include "sds.h" + +/* Lifecycle */ +OrderedIndex *skiplistCreate(void); +void skiplistFree(OrderedIndex *idx); + +/* Modification */ +OrderedIndexItem *skiplistInsert(OrderedIndex *idx, double score, const char *ele, size_t len); +void skiplistDelete(OrderedIndex *idx, OrderedIndexItem *node); +OrderedIndexItem *skiplistUpdateScore(OrderedIndex *idx, OrderedIndexItem *node, double newscore); +OrderedIndexItem *skiplistPopFirst(OrderedIndex *idx); +OrderedIndexItem *skiplistPopLast(OrderedIndex *idx); +void skiplistFreeItem(OrderedIndexItem *item); +OrderedIndexItem *skiplistCreateDetached(double score, const char *ele, size_t len); +void skiplistDetachedSetScore(OrderedIndexItem *item, double score); +OrderedIndexItem *skiplistInsertDetached(OrderedIndex *idx, OrderedIndexItem *item); +unsigned long skiplistDeleteRangeByScore(OrderedIndex *idx, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx); +unsigned long skiplistDeleteRangeByRank(OrderedIndex *idx, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx); +unsigned long skiplistDeleteRangeByLex(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx); + +/* Query */ +unsigned long skiplistLength(OrderedIndex *idx); +OrderedIndexItem *skiplistGetByRank(OrderedIndex *idx, unsigned long rank); +unsigned long skiplistGetRank(OrderedIndex *idx, const OrderedIndexItem *node); +void skiplistGetElementRaw(const OrderedIndexItem *node, const char **ptr, size_t *len); +double skiplistGetScore(const OrderedIndexItem *node); +unsigned long skiplistCountScoreRange(OrderedIndex *idx, double min, double max, int min_ex, int max_ex); +unsigned long skiplistCountLexRange(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex); + +/* Iterator */ +void skiplistInitIterator(OrderedIndexIterator *iter, OrderedIndex *idx); +void skiplistResetIterator(OrderedIndexIterator *iter); +bool skiplistNext(OrderedIndexIterator *iter, OrderedIndexItem **pos); +bool skiplistPrev(OrderedIndexIterator *iter, OrderedIndexItem **pos); +void skiplistSeekToRank(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 *idx); +size_t skiplistEstimateMemory(OrderedIndex *idx, size_t sample_size); + +/* Defrag */ +OrderedIndex *skiplistDefragInternals(OrderedIndex *idx, void *(*defragfn)(void *)); +unsigned long skiplistScanDefrag(OrderedIndex *idx, unsigned long cursor, void (*callback)(OrderedIndexItem *old_item, OrderedIndexItem *new_item, void *ctx), void *ctx, void *(*defragfn)(void *)); + +/* Debug */ +int skiplistGetHeight(OrderedIndex *idx); +int skiplistVerifyIntegrity(OrderedIndex *idx, char *errmsg, size_t errmsg_len); + +#endif /* SKIPLIST_ORDERED_INDEX_H */ diff --git a/src/t_zset.c b/src/t_zset.c index 00bdf1cd1fc..ca74bec28c6 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -2106,7 +2106,7 @@ 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); + zskiplistNode *new_node = zslCreateNode(zslRandomLevel(), score, tmp_ele, sdslen(tmp_ele)); sdsfree(tmp_ele); hashtableInsertAtPosition(dstzset->ht, new_node, &position); /* Remember the longest single element encountered, diff --git a/src/unit/ordered_index_test.h b/src/unit/ordered_index_test.h new file mode 100644 index 00000000000..c7aa22d372b --- /dev/null +++ b/src/unit/ordered_index_test.h @@ -0,0 +1,191 @@ +#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 compile-time dispatch via the inline wrappers in + * ordered_index.h instead. + */ + +extern "C" { +#include "ordered_index.h" +#include "skiplist_ordered_index.h" +} + +#include +#include +#include + +/* ---- Abstract interface ---- */ + +class OrderedIndexTestApi { + public: + virtual ~OrderedIndexTestApi() = default; + + /* Lifecycle */ + virtual OrderedIndex *create() = 0; + virtual void free(OrderedIndex *idx) = 0; + + /* Modification */ + virtual OrderedIndexItem *insert(OrderedIndex *idx, double score, const char *ele, size_t len) = 0; + virtual void deleteItem(OrderedIndex *idx, OrderedIndexItem *pos) = 0; + virtual OrderedIndexItem *updateScore(OrderedIndex *idx, OrderedIndexItem *pos, double newscore) = 0; + virtual OrderedIndexItem *popFirst(OrderedIndex *idx) = 0; + virtual OrderedIndexItem *popLast(OrderedIndex *idx) = 0; + virtual void freeItem(OrderedIndexItem *item) = 0; + virtual unsigned long deleteRangeByScore(OrderedIndex *idx, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) = 0; + virtual unsigned long deleteRangeByRank(OrderedIndex *idx, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) = 0; + virtual unsigned long deleteRangeByLex(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) = 0; + + /* Query */ + virtual unsigned long length(OrderedIndex *idx) = 0; + virtual OrderedIndexItem *getByRank(OrderedIndex *idx, unsigned long rank) = 0; + virtual unsigned long getRank(OrderedIndex *idx, 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 *idx, size_t sample_size) = 0; + + /* Debug / verification */ + virtual int verifyIntegrity(OrderedIndex *idx, char *errmsg, size_t errmsg_len) = 0; + + /* Count */ + virtual unsigned long countScoreRange(OrderedIndex *idx, double min, double max, int min_ex, int max_ex) = 0; + virtual unsigned long countLexRange(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex) = 0; + + /* Iterator */ + virtual void initIterator(OrderedIndexIterator *iter, OrderedIndex *idx) = 0; + virtual void resetIterator(OrderedIndexIterator *iter) = 0; + virtual bool next(OrderedIndexIterator *iter, OrderedIndexItem **pos) = 0; + virtual bool prev(OrderedIndexIterator *iter, OrderedIndexItem **pos) = 0; + virtual void seekToRank(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 *idx, double score, const_sds ele) { + return insert(idx, score, ele, sdslen(ele)); + } + + std::vector> collectAll(OrderedIndex *idx) { + std::vector> result; + OrderedIndexIterator iter; + OrderedIndexItem *pos; + initIterator(&iter, idx); + while (next(&iter, &pos)) { + const char *ptr; + size_t len; + getElementRaw(pos, &ptr, &len); + result.emplace_back(getScore(pos), std::string(ptr, len)); + } + resetIterator(&iter); + return result; + } +}; + +/* ---- Skiplist implementation ---- */ + +class SkiplistOrderedIndex : public OrderedIndexTestApi { + public: + OrderedIndex *create() override { + return skiplistCreate(); + } + void free(OrderedIndex *idx) override { + skiplistFree(idx); + } + + OrderedIndexItem *insert(OrderedIndex *idx, double score, const char *ele, size_t len) override { + return skiplistInsert(idx, score, ele, len); + } + void deleteItem(OrderedIndex *idx, OrderedIndexItem *pos) override { + skiplistDelete(idx, pos); + } + OrderedIndexItem *updateScore(OrderedIndex *idx, OrderedIndexItem *pos, double newscore) override { + return skiplistUpdateScore(idx, pos, newscore); + } + OrderedIndexItem *popFirst(OrderedIndex *idx) override { + return skiplistPopFirst(idx); + } + OrderedIndexItem *popLast(OrderedIndex *idx) override { + return skiplistPopLast(idx); + } + void freeItem(OrderedIndexItem *item) override { + skiplistFreeItem(item); + } + unsigned long deleteRangeByScore(OrderedIndex *idx, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) override { + return skiplistDeleteRangeByScore(idx, min, max, min_ex, max_ex, on_delete, ctx); + } + unsigned long deleteRangeByRank(OrderedIndex *idx, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) override { + return skiplistDeleteRangeByRank(idx, start, end, on_delete, ctx); + } + unsigned long deleteRangeByLex(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) override { + return skiplistDeleteRangeByLex(idx, min, max, min_ex, max_ex, on_delete, ctx); + } + + unsigned long length(OrderedIndex *idx) override { + return skiplistLength(idx); + } + OrderedIndexItem *getByRank(OrderedIndex *idx, unsigned long rank) override { + return skiplistGetByRank(idx, rank); + } + unsigned long getRank(OrderedIndex *idx, const OrderedIndexItem *pos) override { + return skiplistGetRank(idx, 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 *idx, size_t sample_size) override { + return skiplistEstimateMemory(idx, sample_size); + } + + int verifyIntegrity(OrderedIndex *idx, char *errmsg, size_t errmsg_len) override { + return skiplistVerifyIntegrity(idx, errmsg, errmsg_len); + } + + unsigned long countScoreRange(OrderedIndex *idx, double min, double max, int min_ex, int max_ex) override { + return skiplistCountScoreRange(idx, min, max, min_ex, max_ex); + } + unsigned long countLexRange(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex) override { + return skiplistCountLexRange(idx, min, max, min_ex, max_ex); + } + + void initIterator(OrderedIndexIterator *iter, OrderedIndex *idx) override { + skiplistInitIterator(iter, idx); + } + void resetIterator(OrderedIndexIterator *iter) override { + skiplistResetIterator(iter); + } + bool next(OrderedIndexIterator *iter, OrderedIndexItem **pos) override { + return skiplistNext(iter, pos); + } + bool prev(OrderedIndexIterator *iter, OrderedIndexItem **pos) override { + return skiplistPrev(iter, pos); + } + void seekToRank(OrderedIndexIterator *iter, unsigned long rank) override { + skiplistSeekToRank(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..372006d39c4 --- /dev/null +++ b/src/unit/test_ordered_index.cpp @@ -0,0 +1,2456 @@ +/* + * 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 with C++ standard library */ +#undef min +#undef max + +#include "ordered_index_test.h" + +#include +#include +#include +#include +#include +#include +#include + +#define TEST_ASSERT(x) ASSERT_TRUE(x) +#define TEST_ASSERT_SCORE_EQ(a, b) ASSERT_DOUBLE_EQ(a, b) + +/* Verify structural integrity of the ordered index after mutations. */ +static ::testing::AssertionResult verifyIntegrity(OrderedIndexTestApi &api, OrderedIndex *idx) { + char errmsg[256]; + if (api.verifyIntegrity(idx, 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; + +/* ========== Parameterized test fixture ========== */ + +class OrderedIndexTest : public ::testing::TestWithParam { + protected: + OrderedIndexTestApi &api = *GetParam(); +}; + +/* ========== Basic tests ========== */ + +TEST_P(OrderedIndexTest, CreateFree) { + OrderedIndex *idx = api.create(); + TEST_ASSERT(idx != NULL); + TEST_ASSERT(api.length(idx) == 0); + VERIFY_INTEGRITY(api, idx); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, InsertSingle) { + OrderedIndex *idx = api.create(); + sds ele = sdsnew("test"); + OrderedIndexItem *node = api.insertSds(idx, 1.0, ele); + VERIFY_INTEGRITY(api, idx); + + TEST_ASSERT(node != NULL); + TEST_ASSERT(api.length(idx) == 1); + TEST_ASSERT_SCORE_EQ(api.getScore(node), 1.0); + + const char *ptr; + size_t len; + api.getElementRaw(node, &ptr, &len); + TEST_ASSERT(len == 4 && memcmp(ptr, "test", 4) == 0); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT(pos == node); + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + + sdsfree(ele); + api.free(idx); +} + +TEST_P(OrderedIndexTest, InsertMultipleOrdered) { + OrderedIndex *idx = api.create(); + + for (int i = 0; i < 10; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + TEST_ASSERT(api.length(idx) == 10); + VERIFY_INTEGRITY(api, idx); + + /* Verify forward traversal */ + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + for (int i = 0; i < 10; i++) { + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)i); + } + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + + /* Verify backward traversal */ + api.initIterator(&iter, idx); + for (int i = 9; i >= 0; i--) { + TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)i); + } + TEST_ASSERT(!api.prev(&iter, &pos)); + api.resetIterator(&iter); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, DuplicateScores) { + OrderedIndex *idx = api.create(); + + for (int i = 0; i < 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, 1.0, ele); + sdsfree(ele); + } + + TEST_ASSERT(api.length(idx) == 5); + VERIFY_INTEGRITY(api, idx); + + /* Verify lexicographic ordering for same scores */ + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + for (int i = 0; i < 5; i++) { + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + const char *ptr; + size_t len; + api.getElementRaw(pos, &ptr, &len); + char expected[32]; + snprintf(expected, sizeof(expected), "key%d", i); + TEST_ASSERT(len == strlen(expected) && memcmp(ptr, expected, len) == 0); + } + api.resetIterator(&iter); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, RankOperations) { + OrderedIndex *idx = api.create(); + OrderedIndexItem *nodes[10]; + + for (int i = 0; i < 10; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + nodes[i] = api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + VERIFY_INTEGRITY(api, idx); + + for (int i = 0; i < 10; i++) { + unsigned long rank = api.getRank(idx, nodes[i]); + TEST_ASSERT(rank == (unsigned long)(i + 1)); /* 1-based */ + } + + for (int i = 0; i < 10; i++) { + OrderedIndexItem *node = api.getByRank(idx, i + 1); + TEST_ASSERT(node == nodes[i]); + } + + api.free(idx); +} + +TEST_P(OrderedIndexTest, Delete) { + OrderedIndex *idx = api.create(); + OrderedIndexItem *nodes[5]; + + for (int i = 0; i < 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + nodes[i] = api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + TEST_ASSERT(api.length(idx) == 5); + + api.deleteItem(idx, nodes[2]); + TEST_ASSERT(api.length(idx) == 4); + VERIFY_INTEGRITY(api, idx); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 0.0); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 3.0); /* Skipped 2.0 */ + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); + api.resetIterator(&iter); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, PopFirst) { + OrderedIndex *idx = api.create(); + + TEST_ASSERT(api.popFirst(idx) == NULL); + + for (int i = 0; i < 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + TEST_ASSERT(api.length(idx) == 5); + + OrderedIndexItem *item = api.popFirst(idx); + TEST_ASSERT(item != NULL); + TEST_ASSERT_SCORE_EQ(api.getScore(item), 0.0); + const char *ptr; + size_t len; + api.getElementRaw(item, &ptr, &len); + TEST_ASSERT(len == 4 && memcmp(ptr, "key0", 4) == 0); + api.freeItem(item); + TEST_ASSERT(api.length(idx) == 4); + VERIFY_INTEGRITY(api, idx); + + item = api.popFirst(idx); + TEST_ASSERT_SCORE_EQ(api.getScore(item), 1.0); + api.freeItem(item); + TEST_ASSERT(api.length(idx) == 3); + VERIFY_INTEGRITY(api, idx); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, PopLast) { + OrderedIndex *idx = api.create(); + + TEST_ASSERT(api.popLast(idx) == NULL); + + for (int i = 0; i < 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + TEST_ASSERT(api.length(idx) == 5); + + OrderedIndexItem *item = api.popLast(idx); + TEST_ASSERT(item != NULL); + TEST_ASSERT_SCORE_EQ(api.getScore(item), 4.0); + const char *ptr; + size_t len; + api.getElementRaw(item, &ptr, &len); + TEST_ASSERT(len == 4 && memcmp(ptr, "key4", 4) == 0); + api.freeItem(item); + TEST_ASSERT(api.length(idx) == 4); + VERIFY_INTEGRITY(api, idx); + + item = api.popLast(idx); + TEST_ASSERT_SCORE_EQ(api.getScore(item), 3.0); + api.freeItem(item); + TEST_ASSERT(api.length(idx) == 3); + VERIFY_INTEGRITY(api, idx); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, UpdateScore) { + OrderedIndex *idx = api.create(); + + sds ele1 = sdsnew("key1"); + sds ele2 = sdsnew("key2"); + sds ele3 = sdsnew("key3"); + OrderedIndexItem *node1 = api.insertSds(idx, 1.0, ele1); + OrderedIndexItem *node2 = api.insertSds(idx, 2.0, ele2); + api.insertSds(idx, 3.0, ele3); + sdsfree(ele1); + sdsfree(ele2); + sdsfree(ele3); + + OrderedIndexItem *updated = api.updateScore(idx, node2, 4.0); + TEST_ASSERT(updated != NULL); + TEST_ASSERT_SCORE_EQ(api.getScore(updated), 4.0); + VERIFY_INTEGRITY(api, idx); + const char *ptr; + size_t len; + api.getElementRaw(updated, &ptr, &len); + TEST_ASSERT(len == 4 && memcmp(ptr, "key2", 4) == 0); + + /* Verify order: key1(1.0), key3(3.0), key2(4.0) */ + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 3.0); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 4 && memcmp(ptr, "key2", 4) == 0); + api.resetIterator(&iter); + + /* Update to same score (no-op) */ + updated = api.updateScore(idx, node1, 1.0); + TEST_ASSERT_SCORE_EQ(api.getScore(updated), 1.0); + VERIFY_INTEGRITY(api, idx); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, DeleteRangeByScore) { + OrderedIndex *idx = api.create(); + + for (int i = 0; i < 10; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + /* Delete range [3, 6] inclusive */ + unsigned long deleted = api.deleteRangeByScore(idx, 3.0, 6.0, 0, 0, NULL, NULL); + TEST_ASSERT(deleted == 4); /* 3, 4, 5, 6 */ + TEST_ASSERT(api.length(idx) == 6); + VERIFY_INTEGRITY(api, idx); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + for (int i = 0; i < 3; i++) { + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)i); + } + for (int i = 7; i < 10; i++) { + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)i); + } + api.resetIterator(&iter); + + /* Delete with exclusive bounds (2, 8) - should delete 7 */ + deleted = api.deleteRangeByScore(idx, 2.0, 8.0, 1, 1, NULL, NULL); + TEST_ASSERT(deleted == 1); + TEST_ASSERT(api.length(idx) == 5); + VERIFY_INTEGRITY(api, idx); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, DeleteRangeByRank) { + OrderedIndex *idx = api.create(); + + for (int i = 0; i < 10; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + /* Delete ranks 3-5 (1-based, so elements at scores 2,3,4) */ + unsigned long deleted = api.deleteRangeByRank(idx, 3, 5, NULL, NULL); + TEST_ASSERT(deleted == 3); + TEST_ASSERT(api.length(idx) == 7); + VERIFY_INTEGRITY(api, idx); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 0.0); + api.resetIterator(&iter); + + /* Verify rank 3 is now score 5 (was rank 6) */ + OrderedIndexItem *node = api.getByRank(idx, 3); + TEST_ASSERT_SCORE_EQ(api.getScore(node), 5.0); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, MixedOperationsRankIntegrity) { + OrderedIndex *idx = api.create(); + OrderedIndexItem *nodes[100]; + + for (int i = 0; i < 100; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + nodes[i] = api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + for (int i = 2; i < 100; i += 3) { + api.deleteItem(idx, nodes[i]); + nodes[i] = NULL; + } + VERIFY_INTEGRITY(api, idx); + + if (nodes[10]) nodes[10] = api.updateScore(idx, nodes[10], 150.0); + if (nodes[20]) nodes[20] = api.updateScore(idx, nodes[20], 160.0); + VERIFY_INTEGRITY(api, idx); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + unsigned long expected_rank = 1; + while (api.next(&iter, &pos)) { + unsigned long actual_rank = api.getRank(idx, pos); + TEST_ASSERT(actual_rank == expected_rank); + expected_rank++; + } + api.resetIterator(&iter); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, BackwardTraversalAfterDeletions) { + OrderedIndex *idx = api.create(); + OrderedIndexItem *nodes[20]; + + for (int i = 0; i < 20; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + nodes[i] = api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + api.deleteItem(idx, nodes[5]); + api.deleteItem(idx, nodes[10]); + api.deleteItem(idx, nodes[15]); + VERIFY_INTEGRITY(api, idx); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + int expected_scores[] = {19, 18, 17, 16, 14, 13, 12, 11, 9, 8, 7, 6, 4, 3, 2, 1, 0}; + int idx_score = 0; + + while (api.prev(&iter, &pos)) { + TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)expected_scores[idx_score]); + idx_score++; + } + TEST_ASSERT(idx_score == 17); /* Should have traversed all 17 remaining elements */ + api.resetIterator(&iter); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, LexicographicEdgeCases) { + OrderedIndex *idx = api.create(); + + sds empty = sdsnew(""); + sds a = sdsnew("a"); + sds z = sdsnew("z"); + + api.insertSds(idx, 1.0, z); + api.insertSds(idx, 1.0, empty); + api.insertSds(idx, 1.0, a); + + /* Verify lexicographic order: "", "a", "z" */ + OrderedIndexIterator iter; + OrderedIndexItem *pos; + const char *ptr; + size_t len; + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 0); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 1 && memcmp(ptr, "a", 1) == 0); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 1 && memcmp(ptr, "z", 1) == 0); + api.resetIterator(&iter); + + sdsfree(empty); + sdsfree(a); + sdsfree(z); + api.free(idx); + + /* Test very long string (1KB) */ + idx = api.create(); + char long_buf[1024]; + memset(long_buf, 'x', 1023); + long_buf[1023] = '\0'; + sds long_str = sdsnew(long_buf); + sds short_str = sdsnew("short"); + + api.insertSds(idx, 1.0, long_str); + api.insertSds(idx, 1.0, short_str); + + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 5 && memcmp(ptr, "short", 5) == 0); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 1023 && memcmp(ptr, long_buf, 1023) == 0); + api.resetIterator(&iter); + + sdsfree(long_str); + sdsfree(short_str); + api.free(idx); +} + +TEST_P(OrderedIndexTest, RangeBoundaryPrecision) { + OrderedIndex *idx = api.create(); + + double base = 1.0; + double epsilon = 1e-10; + + sds ele1 = sdsnew("at_base"); + sds ele2 = sdsnew("at_base_plus_epsilon"); + sds ele3 = sdsnew("at_base_plus_2epsilon"); + + api.insertSds(idx, base, ele1); + api.insertSds(idx, base + epsilon, ele2); + api.insertSds(idx, base + 2 * epsilon, ele3); + + unsigned long deleted = api.deleteRangeByScore(idx, base, base + 2 * epsilon, 1, 1, NULL, NULL); + TEST_ASSERT(deleted == 1); + TEST_ASSERT(api.length(idx) == 2); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), base); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), base + 2 * epsilon); + api.resetIterator(&iter); + + sdsfree(ele1); + sdsfree(ele2); + sdsfree(ele3); + api.free(idx); +} + +TEST_P(OrderedIndexTest, SpecialDoubleValues) { + OrderedIndex *idx = api.create(); + const char *ptr; + size_t len; + + sds neg_inf = sdsnew("neg_inf"); + sds pos_inf = sdsnew("pos_inf"); + sds zero = sdsnew("zero"); + sds one = sdsnew("one"); + + api.insertSds(idx, NEG_INF, neg_inf); + api.insertSds(idx, POS_INF, pos_inf); + api.insertSds(idx, 0.0, zero); + api.insertSds(idx, 1.0, one); + + /* Verify ordering: -inf, 0, 1, +inf */ + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), NEG_INF); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 0.0); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), POS_INF); + api.resetIterator(&iter); + + sdsfree(neg_inf); + sdsfree(pos_inf); + sdsfree(zero); + sdsfree(one); + api.free(idx); + + /* Test +0.0 vs -0.0 */ + idx = api.create(); + sds pos_zero = sdsnew("pos_zero"); + sds neg_zero = sdsnew("neg_zero"); + + api.insertSds(idx, 0.0, pos_zero); + api.insertSds(idx, -0.0, neg_zero); + + /* Both should be in the list, ordered lexicographically since scores are equal */ + TEST_ASSERT(api.length(idx) == 2); + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 8 && memcmp(ptr, "neg_zero", 8) == 0); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 8 && memcmp(ptr, "pos_zero", 8) == 0); + api.resetIterator(&iter); + + sdsfree(pos_zero); + sdsfree(neg_zero); + api.free(idx); + + /* Test denormalized double */ + idx = api.create(); + double denorm = 1e-320; /* Denormalized double */ + sds denorm_ele = sdsnew("denorm"); + sds normal_ele = sdsnew("normal"); + + api.insertSds(idx, denorm, denorm_ele); + api.insertSds(idx, 1.0, normal_ele); + + TEST_ASSERT(api.length(idx) == 2); + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), denorm); + TEST_ASSERT(api.getScore(pos) < 1.0); + api.resetIterator(&iter); + + sdsfree(denorm_ele); + sdsfree(normal_ele); + api.free(idx); +} + +TEST_P(OrderedIndexTest, EdgeCases) { + OrderedIndex *idx = api.create(); + + TEST_ASSERT(api.length(idx) == 0); + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT(!api.prev(&iter, &pos)); + api.resetIterator(&iter); + TEST_ASSERT(api.getByRank(idx, 1) == NULL); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, DeleteEdgeCases) { + OrderedIndex *idx = api.create(); + + /* Delete only element */ + sds ele = sdsnew("only"); + OrderedIndexItem *node = api.insertSds(idx, 1.0, ele); + api.deleteItem(idx, node); + TEST_ASSERT(api.length(idx) == 0); + VERIFY_INTEGRITY(api, idx); + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + sdsfree(ele); + + /* Delete first element */ + OrderedIndexItem *nodes[3]; + for (int i = 0; i < 3; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds e = sdsnew(buf); + nodes[i] = api.insertSds(idx, (double)i, e); + sdsfree(e); + } + api.deleteItem(idx, nodes[0]); + TEST_ASSERT(api.length(idx) == 2); + VERIFY_INTEGRITY(api, idx); + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + api.resetIterator(&iter); + + /* Delete last element */ + api.deleteItem(idx, nodes[2]); + TEST_ASSERT(api.length(idx) == 1); + VERIFY_INTEGRITY(api, idx); + api.initIterator(&iter, idx); + TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + api.resetIterator(&iter); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, RankEdgeCases) { + OrderedIndex *idx = api.create(); + + for (int i = 0; i < 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + TEST_ASSERT(api.getByRank(idx, 6) == NULL); + TEST_ASSERT(api.getByRank(idx, 100) == NULL); + TEST_ASSERT(api.getByRank(idx, 1) != NULL); + TEST_ASSERT(api.getByRank(idx, 5) != NULL); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, DuplicateInsert) { + OrderedIndex *idx = api.create(); + + sds ele1 = sdsnew("duplicate"); + sds ele2 = sdsnew("duplicate"); + OrderedIndexItem *node1 = api.insertSds(idx, 1.0, ele1); + OrderedIndexItem *node2 = api.insertSds(idx, 1.0, ele2); + + /* Should have 2 nodes (duplicates allowed) */ + TEST_ASSERT(api.length(idx) == 2); + TEST_ASSERT(node1 != node2); + + sdsfree(ele1); + sdsfree(ele2); + api.free(idx); +} + +TEST_P(OrderedIndexTest, UpdateScoreEdgeCases) { + OrderedIndex *idx = api.create(); + + for (int i = 0; i < 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + /* Update first element to move backward */ + OrderedIndexItem *first = api.getByRank(idx, 1); + OrderedIndexItem *updated = api.updateScore(idx, first, -1.0); + TEST_ASSERT_SCORE_EQ(api.getScore(updated), -1.0); + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT(pos == updated); + api.resetIterator(&iter); + + /* Update last element to move forward */ + unsigned long len = api.length(idx); + OrderedIndexItem *last = api.getByRank(idx, len); + updated = api.updateScore(idx, last, 10.0); + TEST_ASSERT_SCORE_EQ(api.getScore(updated), 10.0); + api.initIterator(&iter, idx); + TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT(pos == updated); + api.resetIterator(&iter); + + /* Update middle element to move backward */ + OrderedIndexItem *middle = api.getByRank(idx, 3); + double old_score = api.getScore(middle); + updated = api.updateScore(idx, middle, 0.5); + TEST_ASSERT_SCORE_EQ(api.getScore(updated), 0.5); + TEST_ASSERT(api.getScore(updated) < old_score); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, RangeDeleteEdgeCases) { + OrderedIndex *idx = api.create(); + + for (int i = 0; i < 10; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + /* Delete empty range (min > max) */ + unsigned long deleted = api.deleteRangeByScore(idx, 5.0, 4.0, 0, 0, NULL, NULL); + TEST_ASSERT(deleted == 0); + TEST_ASSERT(api.length(idx) == 10); + + /* Delete range with no matches */ + deleted = api.deleteRangeByScore(idx, 10.5, 11.5, 0, 0, NULL, NULL); + TEST_ASSERT(deleted == 0); + TEST_ASSERT(api.length(idx) == 10); + + /* Delete first elements by rank */ + deleted = api.deleteRangeByRank(idx, 1, 2, NULL, NULL); + TEST_ASSERT(deleted == 2); + VERIFY_INTEGRITY(api, idx); + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 2.0); + api.resetIterator(&iter); + + /* Delete last elements by rank */ + unsigned long len = api.length(idx); + deleted = api.deleteRangeByRank(idx, len - 1, len, NULL, NULL); + TEST_ASSERT(deleted == 2); + VERIFY_INTEGRITY(api, idx); + api.initIterator(&iter, idx); + TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 7.0); + api.resetIterator(&iter); + + /* Delete entire remaining index by score */ + deleted = api.deleteRangeByScore(idx, -100.0, 100.0, 0, 0, NULL, NULL); + TEST_ASSERT(deleted == 6); + TEST_ASSERT(api.length(idx) == 0); + VERIFY_INTEGRITY(api, idx); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, TraversalEdgeCases) { + OrderedIndex *idx = api.create(); + + sds ele = sdsnew("single"); + api.insertSds(idx, 1.0, ele); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + + api.initIterator(&iter, idx); + TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + TEST_ASSERT(!api.prev(&iter, &pos)); + api.resetIterator(&iter); + + sdsfree(ele); + api.free(idx); +} + +TEST_P(OrderedIndexTest, SeekToRank) { + OrderedIndex *idx = api.create(); + + for (int i = 1; i <= 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + + /* Seek to rank 0 (before first) */ + api.initIterator(&iter, idx); + api.seekToRank(&iter, 0); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + api.resetIterator(&iter); + + api.initIterator(&iter, idx); + api.seekToRank(&iter, 0); + TEST_ASSERT(!api.prev(&iter, &pos)); + api.resetIterator(&iter); + + /* Seek to rank 1 */ + api.initIterator(&iter, idx); + api.seekToRank(&iter, 1); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 2.0); + api.resetIterator(&iter); + + api.initIterator(&iter, idx); + api.seekToRank(&iter, 1); + TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + api.resetIterator(&iter); + + /* Seek to rank 3 (middle) */ + api.initIterator(&iter, idx); + api.seekToRank(&iter, 3); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); + api.resetIterator(&iter); + + api.initIterator(&iter, idx); + api.seekToRank(&iter, 3); + TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 3.0); + api.resetIterator(&iter); + + /* Seek to rank 5 (last) */ + api.initIterator(&iter, idx); + api.seekToRank(&iter, 5); + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + + api.initIterator(&iter, idx); + api.seekToRank(&iter, 5); + TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 5.0); + api.resetIterator(&iter); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, ReverseIteration) { + OrderedIndex *idx = api.create(); + + for (int i = 1; i <= 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + + /* Full reverse traversal */ + api.initIterator(&iter, idx); + int count = 0; + double expected = 5.0; + while (api.prev(&iter, &pos)) { + TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); + expected -= 1.0; + count++; + } + TEST_ASSERT(count == 5); + api.resetIterator(&iter); + + /* Reverse then forward */ + api.initIterator(&iter, idx); + TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 5.0); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 5.0); + api.resetIterator(&iter); + + /* Forward then reverse */ + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + api.resetIterator(&iter); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, SeekToScoreRange) { + OrderedIndex *idx = api.create(); + + /* 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); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)(i * 2), ele); + sdsfree(ele); + } + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + + /* Seek to first in range [2, 6] with offset 0 */ + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, 0); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 2.0); + api.resetIterator(&iter); + + /* Seek to second in range [2, 6] with offset 1 */ + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, 1); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); + api.resetIterator(&iter); + + /* Seek to last in range [2, 6] with offset -1, positioned for prev() */ + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, -1); + TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 6.0); + api.resetIterator(&iter); + + /* Seek with exclusive bounds (2, 6) - should start at 4 */ + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, 2.0, 6.0, 1, 1, 0); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); + api.resetIterator(&iter); + + /* Seek to empty range above all elements */ + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, 10.0, 20.0, 0, 0, 0); + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + + /* Seek to empty range below all elements */ + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, -20.0, -10.0, 0, 0, 0); + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + + /* Out of range positive offset */ + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, 10); + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + + /* Negative offset beyond range */ + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, -10); + TEST_ASSERT(!api.prev(&iter, &pos)); + api.resetIterator(&iter); + + /* Second from last with offset -2, positioned for prev() */ + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, -2); + TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); + api.resetIterator(&iter); + + /* Empty range where min > max */ + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, 6.0, 2.0, 0, 0, 0); + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, SeekToScoreRangeIteration) { + OrderedIndex *idx = api.create(); + + for (int i = 0; i < 10; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + + /* Seek to range [3, 7] and iterate forward */ + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, 3.0, 7.0, 0, 0, 0); + int count = 0; + double expected = 3.0; + while (api.next(&iter, &pos) && api.getScore(pos) <= 7.0) { + TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); + expected += 1.0; + count++; + } + TEST_ASSERT(count == 5); + api.resetIterator(&iter); + + /* Seek to last in range and iterate backward */ + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, 3.0, 7.0, 0, 0, -1); + count = 0; + expected = 7.0; + while (api.prev(&iter, &pos) && api.getScore(pos) >= 3.0) { + TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); + expected -= 1.0; + count++; + } + TEST_ASSERT(count == 5); + api.resetIterator(&iter); + + /* Seek with offset and continue iteration */ + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, 2.0, 8.0, 0, 0, 2); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); + TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 5.0); + api.resetIterator(&iter); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, SeekInfReverseIteration) { + OrderedIndex *idx = api.create(); + + for (int i = 1; i <= 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, NEG_INF, POS_INF, 0, 0, -1); + int count = 0; + double expected = 5.0; + while (api.prev(&iter, &pos)) { + TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); + expected -= 1.0; + count++; + } + TEST_ASSERT(count == 5); + api.resetIterator(&iter); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, SeekInfForwardIteration) { + OrderedIndex *idx = api.create(); + + for (int i = 1; i <= 5; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + + api.initIterator(&iter, idx); + api.seekToScoreRange(&iter, NEG_INF, POS_INF, 0, 0, 0); + int count = 0; + double expected = 1.0; + while (api.next(&iter, &pos)) { + TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); + expected += 1.0; + count++; + } + TEST_ASSERT(count == 5); + api.resetIterator(&iter); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, SeekToLexRange) { + OrderedIndex *idx = api.create(); + + const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; + for (int i = 0; i < 5; i++) { + sds ele = sdsnew(elements[i]); + api.insertSds(idx, 1.0, ele); + sdsfree(ele); + } + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + const char *ptr; + size_t len; + + sds minLex = sdsnew("banana"); + sds maxLex = sdsnew("date"); + + /* Seek to first in lex range [banana, date] with offset 0 */ + api.initIterator(&iter, idx); + api.seekToLexRange(&iter, minLex, maxLex, 0, 0, 0); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 6 && memcmp(ptr, "banana", 6) == 0); + api.resetIterator(&iter); + + /* Seek to second in lex range with offset 1 */ + api.initIterator(&iter, idx); + api.seekToLexRange(&iter, minLex, maxLex, 0, 0, 1); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 6 && memcmp(ptr, "cherry", 6) == 0); + api.resetIterator(&iter); + + /* Seek to last in lex range with offset -1 */ + /* Seek to last in lex range with offset -1, positioned for prev() */ + api.initIterator(&iter, idx); + api.seekToLexRange(&iter, minLex, maxLex, 0, 0, -1); + TEST_ASSERT(api.prev(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 4 && memcmp(ptr, "date", 4) == 0); + api.resetIterator(&iter); + + /* Seek with exclusive bounds (banana, date) - should start at cherry */ + api.initIterator(&iter, idx); + api.seekToLexRange(&iter, minLex, maxLex, 1, 1, 0); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 6 && memcmp(ptr, "cherry", 6) == 0); + api.resetIterator(&iter); + + sdsfree(minLex); + sdsfree(maxLex); + + /* Seek to empty lex range */ + sds minEmpty = sdsnew("zzz"); + sds maxEmpty = sdsnew("zzzz"); + api.initIterator(&iter, idx); + api.seekToLexRange(&iter, minEmpty, maxEmpty, 0, 0, 0); + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + sdsfree(minEmpty); + sdsfree(maxEmpty); + + /* Out of range positive offset */ + minLex = sdsnew("banana"); + maxLex = sdsnew("date"); + api.initIterator(&iter, idx); + api.seekToLexRange(&iter, minLex, maxLex, 0, 0, 10); + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + sdsfree(minLex); + sdsfree(maxLex); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, DeleteRangeByLexInclusive) { + OrderedIndex *idx = api.create(); + + const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; + for (int i = 0; i < 5; i++) { + sds ele = sdsnew(elements[i]); + api.insertSds(idx, 1.0, ele); + sdsfree(ele); + } + + sds min = sdsnew("banana"); + sds max = sdsnew("date"); + unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, NULL, NULL); + TEST_ASSERT(deleted == 3); + TEST_ASSERT(api.length(idx) == 2); + VERIFY_INTEGRITY(api, idx); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + const char *ptr; + size_t len; + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 5 && memcmp(ptr, "apple", 5) == 0); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 10 && memcmp(ptr, "elderberry", 10) == 0); + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + + sdsfree(min); + sdsfree(max); + api.free(idx); +} + +TEST_P(OrderedIndexTest, DeleteRangeByLexExclusive) { + OrderedIndex *idx = api.create(); + + const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; + for (int i = 0; i < 5; i++) { + sds ele = sdsnew(elements[i]); + api.insertSds(idx, 1.0, ele); + sdsfree(ele); + } + + sds min = sdsnew("banana"); + sds max = sdsnew("date"); + unsigned long deleted = api.deleteRangeByLex(idx, min, max, 1, 1, NULL, NULL); + TEST_ASSERT(deleted == 1); + TEST_ASSERT(api.length(idx) == 4); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + const char *ptr; + size_t len; + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 5 && memcmp(ptr, "apple", 5) == 0); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 6 && memcmp(ptr, "banana", 6) == 0); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 4 && memcmp(ptr, "date", 4) == 0); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 10 && memcmp(ptr, "elderberry", 10) == 0); + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + + sdsfree(min); + sdsfree(max); + api.free(idx); +} + +TEST_P(OrderedIndexTest, DeleteRangeByLexBoundaryCases) { + /* Empty range: min > max lexicographically */ + OrderedIndex *idx = api.create(); + const char *elements[] = {"apple", "banana", "cherry"}; + for (int i = 0; i < 3; i++) { + sds ele = sdsnew(elements[i]); + api.insertSds(idx, 1.0, ele); + sdsfree(ele); + } + + sds min = sdsnew("zzz"); + sds max = sdsnew("aaa"); + unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, NULL, NULL); + TEST_ASSERT(deleted == 0); + TEST_ASSERT(api.length(idx) == 3); + sdsfree(min); + sdsfree(max); + api.free(idx); + + /* Delete all elements */ + idx = api.create(); + for (int i = 0; i < 3; i++) { + sds ele = sdsnew(elements[i]); + api.insertSds(idx, 1.0, ele); + sdsfree(ele); + } + + min = sdsnew("a"); + max = sdsnew("z"); + deleted = api.deleteRangeByLex(idx, min, max, 0, 0, NULL, NULL); + TEST_ASSERT(deleted == 3); + TEST_ASSERT(api.length(idx) == 0); + sdsfree(min); + sdsfree(max); + api.free(idx); + + /* Delete single element */ + idx = api.create(); + for (int i = 0; i < 3; i++) { + sds ele = sdsnew(elements[i]); + api.insertSds(idx, 1.0, ele); + sdsfree(ele); + } + + min = sdsnew("banana"); + max = sdsnew("banana"); + deleted = api.deleteRangeByLex(idx, min, max, 0, 0, NULL, NULL); + TEST_ASSERT(deleted == 1); + TEST_ASSERT(api.length(idx) == 2); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + const char *ptr; + size_t len; + api.initIterator(&iter, idx); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 5 && memcmp(ptr, "apple", 5) == 0); + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == 6 && memcmp(ptr, "cherry", 6) == 0); + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + + sdsfree(min); + sdsfree(max); + api.free(idx); +} + +TEST_P(OrderedIndexTest, DeleteRangeByLexPreservesOutside) { + OrderedIndex *idx = api.create(); + + const char *elements[] = {"alpha", "bravo", "charlie", "delta", "echo", "foxtrot"}; + for (int i = 0; i < 6; i++) { + sds ele = sdsnew(elements[i]); + api.insertSds(idx, 1.0, ele); + sdsfree(ele); + } + + sds min = sdsnew("charlie"); + sds max = sdsnew("delta"); + unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, NULL, NULL); + TEST_ASSERT(deleted == 2); + TEST_ASSERT(api.length(idx) == 4); + + const char *expected[] = {"alpha", "bravo", "echo", "foxtrot"}; + size_t expected_lens[] = {5, 5, 4, 7}; + OrderedIndexIterator iter; + OrderedIndexItem *pos; + const char *ptr; + size_t len; + api.initIterator(&iter, idx); + for (int i = 0; i < 4; i++) { + TEST_ASSERT(api.next(&iter, &pos)); + api.getElementRaw(pos, &ptr, &len); + TEST_ASSERT(len == expected_lens[i] && memcmp(ptr, expected[i], len) == 0); + } + TEST_ASSERT(!api.next(&iter, &pos)); + api.resetIterator(&iter); + + /* Verify scores are preserved */ + api.initIterator(&iter, idx); + while (api.next(&iter, &pos)) { + TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + } + api.resetIterator(&iter); + + /* Verify ranks are correct after deletion */ + for (unsigned long r = 1; r <= 4; r++) { + OrderedIndexItem *node = api.getByRank(idx, r); + TEST_ASSERT(node != NULL); + unsigned long rank = api.getRank(idx, node); + TEST_ASSERT(rank == r); + } + + sdsfree(min); + sdsfree(max); + api.free(idx); +} + +/* ========== Randomized property tests ========== */ + +struct RandomIndexEntry { + OrderedIndexItem *node; + double score; + std::string element; +}; + +static std::string test_random_element(std::mt19937 &rng, int maxLen = 16) { + std::uniform_int_distribution lenDist(1, maxLen); + std::uniform_int_distribution charDist('a', 'z'); + int len = lenDist(rng); + std::string s(len, ' '); + for (int i = 0; i < len; i++) s[i] = (char)charDist(rng); + return s; +} + +static double test_random_score(std::mt19937 &rng) { + std::uniform_real_distribution dist(-1e6, 1e6); + return dist(rng); +} + +static std::vector test_build_random_index(OrderedIndexTestApi &api, OrderedIndex *idx, std::mt19937 &rng, int count) { + std::vector entries; + for (int i = 0; i < count; i++) { + double score = test_random_score(rng); + std::string elem = test_random_element(rng) + std::to_string(i); + sds ele = sdsnew(elem.c_str()); + OrderedIndexItem *node = api.insertSds(idx, score, ele); + entries.push_back({node, score, elem}); + sdsfree(ele); + } + return entries; +} + +TEST_P(OrderedIndexTest, RandomizedInsertAndTraversal) { + std::mt19937 rng(42); + for (int trial = 0; trial < 20; trial++) { + std::uniform_int_distribution sizeDist(1, 50); + int n = sizeDist(rng); + + OrderedIndex *idx = api.create(); + test_build_random_index(api, idx, rng, n); + + ASSERT_EQ(api.length(idx), (unsigned long)n); + VERIFY_INTEGRITY(api, idx); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + int count = 0; + double prevScore = -INFINITY; + while (api.next(&iter, &pos)) { + double s = api.getScore(pos); + ASSERT_GE(s, prevScore); + prevScore = s; + count++; + } + ASSERT_EQ(count, n); + api.resetIterator(&iter); + api.free(idx); + } +} + +TEST_P(OrderedIndexTest, RandomizedBackwardTraversal) { + std::mt19937 rng(42); + for (int trial = 0; trial < 20; trial++) { + std::uniform_int_distribution sizeDist(1, 50); + int n = sizeDist(rng); + + OrderedIndex *idx = api.create(); + test_build_random_index(api, idx, rng, n); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + int count = 0; + double prevScore = INFINITY; + while (api.prev(&iter, &pos)) { + double s = api.getScore(pos); + ASSERT_LE(s, prevScore); + prevScore = s; + count++; + } + ASSERT_EQ(count, n); + api.resetIterator(&iter); + api.free(idx); + } +} + +TEST_P(OrderedIndexTest, RandomizedScoreRetrieval) { + std::mt19937 rng(42); + for (int trial = 0; trial < 20; trial++) { + std::uniform_int_distribution sizeDist(1, 50); + int n = sizeDist(rng); + + OrderedIndex *idx = api.create(); + auto entries = test_build_random_index(api, idx, rng, n); + + for (auto &e : entries) { + ASSERT_EQ(api.getScore(e.node), e.score); + } + api.free(idx); + } +} + +TEST_P(OrderedIndexTest, RandomizedRankConsistency) { + std::mt19937 rng(42); + for (int trial = 0; trial < 20; trial++) { + std::uniform_int_distribution sizeDist(1, 50); + int n = sizeDist(rng); + + OrderedIndex *idx = api.create(); + test_build_random_index(api, idx, rng, n); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + unsigned long expectedRank = 1; + while (api.next(&iter, &pos)) { + unsigned long rank = api.getRank(idx, pos); + ASSERT_EQ(rank, expectedRank); + OrderedIndexItem *byRank = api.getByRank(idx, expectedRank); + ASSERT_EQ(byRank, pos); + expectedRank++; + } + ASSERT_EQ(expectedRank - 1, (unsigned long)n); + api.resetIterator(&iter); + api.free(idx); + } +} + +TEST_P(OrderedIndexTest, RandomizedDelete) { + std::mt19937 rng(42); + for (int trial = 0; trial < 20; trial++) { + std::uniform_int_distribution sizeDist(2, 30); + int n = sizeDist(rng); + + OrderedIndex *idx = api.create(); + auto entries = test_build_random_index(api, idx, rng, n); + + std::uniform_int_distribution pickDist(0, n - 1); + int delIdx = pickDist(rng); + api.deleteItem(idx, entries[delIdx].node); + + ASSERT_EQ(api.length(idx), (unsigned long)(n - 1)); + VERIFY_INTEGRITY(api, idx); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + int count = 0; + double prevScore = -INFINITY; + while (api.next(&iter, &pos)) { + ASSERT_GE(api.getScore(pos), prevScore); + prevScore = api.getScore(pos); + count++; + } + ASSERT_EQ(count, n - 1); + api.resetIterator(&iter); + api.free(idx); + } +} + +TEST_P(OrderedIndexTest, RandomizedUpdateScore) { + std::mt19937 rng(42); + for (int trial = 0; trial < 20; trial++) { + std::uniform_int_distribution sizeDist(2, 30); + int n = sizeDist(rng); + + OrderedIndex *idx = api.create(); + auto entries = test_build_random_index(api, idx, rng, n); + + std::uniform_int_distribution pickDist(0, n - 1); + int updIdx = pickDist(rng); + double newScore = test_random_score(rng); + + OrderedIndexItem *updated = api.updateScore(idx, entries[updIdx].node, newScore); + ASSERT_NE(updated, nullptr); + ASSERT_EQ(api.getScore(updated), newScore); + ASSERT_EQ(api.length(idx), (unsigned long)n); + VERIFY_INTEGRITY(api, idx); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + double prevScore = -INFINITY; + while (api.next(&iter, &pos)) { + ASSERT_GE(api.getScore(pos), prevScore); + prevScore = api.getScore(pos); + } + api.resetIterator(&iter); + api.free(idx); + } +} + +TEST_P(OrderedIndexTest, RandomizedPop) { + std::mt19937 rng(42); + for (int trial = 0; trial < 10; trial++) { + std::uniform_int_distribution sizeDist(3, 30); + int n = sizeDist(rng); + + OrderedIndex *idx = api.create(); + test_build_random_index(api, idx, rng, n); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + ASSERT_TRUE(api.next(&iter, &pos)); + double minScore = api.getScore(pos); + api.resetIterator(&iter); + + api.initIterator(&iter, idx); + ASSERT_TRUE(api.prev(&iter, &pos)); + double maxScore = api.getScore(pos); + api.resetIterator(&iter); + + OrderedIndexItem *first = api.popFirst(idx); + ASSERT_NE(first, nullptr); + ASSERT_EQ(api.getScore(first), minScore); + ASSERT_EQ(api.length(idx), (unsigned long)(n - 1)); + api.freeItem(first); + VERIFY_INTEGRITY(api, idx); + + OrderedIndexItem *last = api.popLast(idx); + ASSERT_NE(last, nullptr); + ASSERT_EQ(api.getScore(last), maxScore); + ASSERT_EQ(api.length(idx), (unsigned long)(n - 2)); + api.freeItem(last); + VERIFY_INTEGRITY(api, idx); + + api.initIterator(&iter, idx); + double prevScore = -INFINITY; + while (api.next(&iter, &pos)) { + ASSERT_GE(api.getScore(pos), prevScore); + prevScore = api.getScore(pos); + } + api.resetIterator(&iter); + api.free(idx); + } +} + +TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { + std::mt19937 rng(42); + for (int trial = 0; trial < 20; trial++) { + std::uniform_int_distribution sizeDist(5, 40); + int n = sizeDist(rng); + + OrderedIndex *idx = api.create(); + auto entries = test_build_random_index(api, idx, rng, n); + + double s1 = test_random_score(rng), s2 = test_random_score(rng); + double lo = (std::min)(s1, s2), hi = (std::max)(s1, s2); + + int expectedDeleted = 0; + for (auto &e : entries) { + if (e.score >= lo && e.score <= hi) expectedDeleted++; + } + + unsigned long deleted = api.deleteRangeByScore(idx, lo, hi, 0, 0, NULL, NULL); + ASSERT_EQ(deleted, (unsigned long)expectedDeleted); + ASSERT_EQ(api.length(idx), (unsigned long)(n - expectedDeleted)); + VERIFY_INTEGRITY(api, idx); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + double prevScore = -INFINITY; + while (api.next(&iter, &pos)) { + double s = api.getScore(pos); + ASSERT_TRUE(s < lo || s > hi); + ASSERT_GE(s, prevScore); + prevScore = s; + } + api.resetIterator(&iter); + api.free(idx); + } +} + +TEST_P(OrderedIndexTest, RandomizedDeleteRangeByRank) { + std::mt19937 rng(42); + for (int trial = 0; trial < 20; trial++) { + std::uniform_int_distribution sizeDist(5, 40); + int n = sizeDist(rng); + + OrderedIndex *idx = api.create(); + test_build_random_index(api, idx, rng, n); + + std::uniform_int_distribution rankDist(1, n); + int r1 = rankDist(rng), r2 = rankDist(rng); + unsigned long start = (unsigned long)(std::min)(r1, r2); + unsigned long end = (unsigned long)(std::max)(r1, r2); + unsigned long expectedDeleted = end - start + 1; + + unsigned long deleted = api.deleteRangeByRank(idx, start, end, NULL, NULL); + ASSERT_EQ(deleted, expectedDeleted); + ASSERT_EQ(api.length(idx), (unsigned long)(n)-expectedDeleted); + VERIFY_INTEGRITY(api, idx); + + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + int remaining = 0; + double prevScore = -INFINITY; + while (api.next(&iter, &pos)) { + ASSERT_GE(api.getScore(pos), prevScore); + prevScore = api.getScore(pos); + remaining++; + } + ASSERT_EQ(remaining, n - (int)expectedDeleted); + api.resetIterator(&iter); + api.free(idx); + } +} + +TEST_P(OrderedIndexTest, RandomizedForwardBackwardMirror) { + std::mt19937 rng(42); + for (int trial = 0; trial < 20; trial++) { + std::uniform_int_distribution sizeDist(1, 50); + int n = sizeDist(rng); + + OrderedIndex *idx = api.create(); + test_build_random_index(api, idx, rng, n); + + std::vector forwardScores; + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + while (api.next(&iter, &pos)) { + forwardScores.push_back(api.getScore(pos)); + } + api.resetIterator(&iter); + + std::vector backwardScores; + api.initIterator(&iter, idx); + while (api.prev(&iter, &pos)) { + backwardScores.push_back(api.getScore(pos)); + } + api.resetIterator(&iter); + + ASSERT_EQ(forwardScores.size(), backwardScores.size()); + std::reverse(backwardScores.begin(), backwardScores.end()); + for (size_t i = 0; i < forwardScores.size(); i++) { + ASSERT_EQ(forwardScores[i], backwardScores[i]); + } + api.free(idx); + } +} + +/* ========== Count range tests ========== */ + +TEST_P(OrderedIndexTest, CountScoreRange) { + OrderedIndex *idx = api.create(); + + for (int i = 0; i < 10; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + sds ele = sdsnew(buf); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + + /* Full range */ + ASSERT_EQ(api.countScoreRange(idx, NEG_INF, POS_INF, 0, 0), 10UL); + + /* Inclusive [3, 6] */ + ASSERT_EQ(api.countScoreRange(idx, 3.0, 6.0, 0, 0), 4UL); + + /* Exclusive (3, 6) */ + ASSERT_EQ(api.countScoreRange(idx, 3.0, 6.0, 1, 1), 2UL); + + /* Single element [5, 5] */ + ASSERT_EQ(api.countScoreRange(idx, 5.0, 5.0, 0, 0), 1UL); + + /* Empty exclusive (5, 5) */ + ASSERT_EQ(api.countScoreRange(idx, 5.0, 5.0, 1, 0), 0UL); + + /* No match above */ + ASSERT_EQ(api.countScoreRange(idx, 10.0, 20.0, 0, 0), 0UL); + + /* No match below */ + ASSERT_EQ(api.countScoreRange(idx, -20.0, -10.0, 0, 0), 0UL); + + /* Min > max */ + ASSERT_EQ(api.countScoreRange(idx, 6.0, 3.0, 0, 0), 0UL); + + /* First element only [0, 0] */ + ASSERT_EQ(api.countScoreRange(idx, 0.0, 0.0, 0, 0), 1UL); + + /* Last element only [9, 9] */ + ASSERT_EQ(api.countScoreRange(idx, 9.0, 9.0, 0, 0), 1UL); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, CountScoreRangeEmpty) { + OrderedIndex *idx = api.create(); + ASSERT_EQ(api.countScoreRange(idx, NEG_INF, POS_INF, 0, 0), 0UL); + api.free(idx); +} + +TEST_P(OrderedIndexTest, CountLexRange) { + OrderedIndex *idx = api.create(); + + const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; + for (int i = 0; i < 5; i++) { + sds ele = sdsnew(elements[i]); + api.insertSds(idx, 1.0, ele); + sdsfree(ele); + } + + /* Inclusive [banana, date] */ + sds min = sdsnew("banana"); + sds max = sdsnew("date"); + ASSERT_EQ(api.countLexRange(idx, min, max, 0, 0), 3UL); + sdsfree(min); + sdsfree(max); + + /* Exclusive (banana, date) */ + min = sdsnew("banana"); + max = sdsnew("date"); + ASSERT_EQ(api.countLexRange(idx, min, max, 1, 1), 1UL); + sdsfree(min); + sdsfree(max); + + /* Single element [cherry, cherry] */ + min = sdsnew("cherry"); + max = sdsnew("cherry"); + ASSERT_EQ(api.countLexRange(idx, min, max, 0, 0), 1UL); + sdsfree(min); + sdsfree(max); + + /* No match */ + min = sdsnew("fig"); + max = sdsnew("grape"); + ASSERT_EQ(api.countLexRange(idx, min, max, 0, 0), 0UL); + sdsfree(min); + sdsfree(max); + + /* All elements */ + min = sdsnew("a"); + max = sdsnew("z"); + ASSERT_EQ(api.countLexRange(idx, min, max, 0, 0), 5UL); + sdsfree(min); + sdsfree(max); + + api.free(idx); +} + +TEST_P(OrderedIndexTest, CountLexRangeEmpty) { + OrderedIndex *idx = api.create(); + sds min = sdsnew("a"); + sds max = sdsnew("z"); + ASSERT_EQ(api.countLexRange(idx, min, max, 0, 0), 0UL); + sdsfree(min); + sdsfree(max); + api.free(idx); +} + +/* ========== Instantiate parameterized tests for all implementations ========== */ + +INSTANTIATE_TEST_SUITE_P(AllImplementations, + OrderedIndexTest, + ::testing::Values(&skiplistImpl), + orderedIndexTestName); + +/* ========== On-Delete Callback Tests ========== */ + +struct OnDeleteRecord { + int count; + std::vector elements; +}; + +static void testOnDeleteCallback(OrderedIndexItem *item, void *ctx) { + OnDeleteRecord *rec = (OnDeleteRecord *)ctx; + rec->count++; + const char *ptr; + size_t len; + skiplistGetElementRaw(item, &ptr, &len); + rec->elements.emplace_back(ptr, len); +} + +class OnDeleteCallbackTest : public ::testing::Test { + protected: + SkiplistOrderedIndex api; + + void insertN(OrderedIndex *idx, int n) { + for (int i = 0; i < n; i++) { + std::string name = "key" + std::to_string(i); + sds ele = sdsnew(name.c_str()); + api.insertSds(idx, (double)i, ele); + sdsfree(ele); + } + } + + void insertLex(OrderedIndex *idx, const std::vector &elems, double score = 1.0) { + for (auto &e : elems) { + sds ele = sdsnew(e.c_str()); + api.insertSds(idx, score, ele); + sdsfree(ele); + } + } + + std::vector collectElements(OrderedIndex *idx) { + std::vector result; + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + while (api.next(&iter, &pos)) { + const char *ptr; + size_t len; + api.getElementRaw(pos, &ptr, &len); + result.emplace_back(ptr, len); + } + api.resetIterator(&iter); + return result; + } +}; + +/* DeleteRangeByScore */ + +TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_EmptyAndNoMatch) { + OnDeleteRecord rec = {0, {}}; + + OrderedIndex *idx = api.create(); + unsigned long deleted = api.deleteRangeByScore(idx, 0.0, 10.0, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(rec.count, 0); + api.free(idx); + + idx = api.create(); + insertN(idx, 5); + rec = {0, {}}; + deleted = api.deleteRangeByScore(idx, 10.0, 20.0, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(rec.count, 0); + ASSERT_EQ(api.length(idx), 5UL); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_Subset) { + OrderedIndex *idx = api.create(); + insertN(idx, 10); + + OnDeleteRecord rec = {0, {}}; + unsigned long deleted = api.deleteRangeByScore(idx, 3.0, 6.0, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 4UL); + ASSERT_EQ(rec.count, 4); + ASSERT_EQ(api.length(idx), 6UL); + VERIFY_INTEGRITY(api, idx); + + std::sort(rec.elements.begin(), rec.elements.end()); + ASSERT_EQ(rec.elements, (std::vector{"key3", "key4", "key5", "key6"})); + + auto remaining = collectElements(idx); + ASSERT_EQ(remaining, (std::vector{"key0", "key1", "key2", "key7", "key8", "key9"})); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_All) { + OrderedIndex *idx = api.create(); + insertN(idx, 5); + + OnDeleteRecord rec = {0, {}}; + unsigned long deleted = api.deleteRangeByScore(idx, NEG_INF, POS_INF, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 5UL); + ASSERT_EQ(rec.count, 5); + ASSERT_EQ(api.length(idx), 0UL); + VERIFY_INTEGRITY(api, idx); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_NullCallback) { + OrderedIndex *idx = api.create(); + insertN(idx, 5); + + unsigned long deleted = api.deleteRangeByScore(idx, 1.0, 3.0, 0, 0, NULL, NULL); + ASSERT_EQ(deleted, 3UL); + ASSERT_EQ(api.length(idx), 2UL); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_ExclusiveBounds) { + OrderedIndex *idx = api.create(); + insertN(idx, 10); + + OnDeleteRecord rec = {0, {}}; + unsigned long deleted = api.deleteRangeByScore(idx, 3.0, 7.0, 1, 1, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 3UL); + ASSERT_EQ(rec.count, 3); + std::sort(rec.elements.begin(), rec.elements.end()); + ASSERT_EQ(rec.elements, (std::vector{"key4", "key5", "key6"})); + ASSERT_EQ(api.length(idx), 7UL); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_SingleElement) { + OrderedIndex *idx = api.create(); + insertN(idx, 5); + + OnDeleteRecord rec = {0, {}}; + unsigned long deleted = api.deleteRangeByScore(idx, 2.0, 2.0, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 1UL); + ASSERT_EQ(rec.count, 1); + ASSERT_EQ(rec.elements[0], "key2"); + ASSERT_EQ(api.length(idx), 4UL); + api.free(idx); +} + +/* DeleteRangeByRank */ + +TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_EmptyAndNoMatch) { + OnDeleteRecord rec = {0, {}}; + + OrderedIndex *idx = api.create(); + unsigned long deleted = api.deleteRangeByRank(idx, 1, 5, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(rec.count, 0); + api.free(idx); + + idx = api.create(); + insertN(idx, 3); + rec = {0, {}}; + deleted = api.deleteRangeByRank(idx, 10, 20, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(rec.count, 0); + ASSERT_EQ(api.length(idx), 3UL); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_Subset) { + OrderedIndex *idx = api.create(); + insertN(idx, 10); + + OnDeleteRecord rec = {0, {}}; + unsigned long deleted = api.deleteRangeByRank(idx, 3, 5, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 3UL); + ASSERT_EQ(rec.count, 3); + ASSERT_EQ(api.length(idx), 7UL); + + std::sort(rec.elements.begin(), rec.elements.end()); + ASSERT_EQ(rec.elements, (std::vector{"key2", "key3", "key4"})); + + auto remaining = collectElements(idx); + ASSERT_EQ(remaining, (std::vector{"key0", "key1", "key5", "key6", "key7", "key8", "key9"})); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_All) { + OrderedIndex *idx = api.create(); + insertN(idx, 5); + + OnDeleteRecord rec = {0, {}}; + unsigned long deleted = api.deleteRangeByRank(idx, 1, 5, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 5UL); + ASSERT_EQ(rec.count, 5); + ASSERT_EQ(api.length(idx), 0UL); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_NullCallback) { + OrderedIndex *idx = api.create(); + insertN(idx, 5); + + unsigned long deleted = api.deleteRangeByRank(idx, 2, 4, NULL, NULL); + ASSERT_EQ(deleted, 3UL); + ASSERT_EQ(api.length(idx), 2UL); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_ExclusiveBounds) { + OrderedIndex *idx = api.create(); + insertN(idx, 5); + + OnDeleteRecord rec = {0, {}}; + unsigned long deleted = api.deleteRangeByRank(idx, 3, 3, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 1UL); + ASSERT_EQ(rec.count, 1); + ASSERT_EQ(rec.elements[0], "key2"); + + auto remaining = collectElements(idx); + ASSERT_EQ(remaining, (std::vector{"key0", "key1", "key3", "key4"})); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_SingleElement) { + OrderedIndex *idx = api.create(); + insertN(idx, 5); + + OnDeleteRecord rec = {0, {}}; + unsigned long deleted = api.deleteRangeByRank(idx, 1, 1, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 1UL); + ASSERT_EQ(rec.count, 1); + ASSERT_EQ(rec.elements[0], "key0"); + ASSERT_EQ(api.length(idx), 4UL); + api.free(idx); +} + +/* DeleteRangeByLex */ + +TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { + OnDeleteRecord rec = {0, {}}; + + OrderedIndex *idx = api.create(); + sds min = sdsnew("a"); + sds max = sdsnew("z"); + unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(rec.count, 0); + sdsfree(min); + sdsfree(max); + api.free(idx); + + idx = api.create(); + insertLex(idx, {"apple", "banana", "cherry"}); + rec = {0, {}}; + min = sdsnew("x"); + max = sdsnew("z"); + deleted = api.deleteRangeByLex(idx, min, max, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(rec.count, 0); + ASSERT_EQ(api.length(idx), 3UL); + sdsfree(min); + sdsfree(max); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_Subset) { + OrderedIndex *idx = api.create(); + insertLex(idx, {"apple", "banana", "cherry", "date", "elderberry"}); + + OnDeleteRecord rec = {0, {}}; + sds min = sdsnew("banana"); + sds max = sdsnew("date"); + unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 3UL); + ASSERT_EQ(rec.count, 3); + ASSERT_EQ(api.length(idx), 2UL); + + std::sort(rec.elements.begin(), rec.elements.end()); + ASSERT_EQ(rec.elements, (std::vector{"banana", "cherry", "date"})); + + auto remaining = collectElements(idx); + ASSERT_EQ(remaining, (std::vector{"apple", "elderberry"})); + + sdsfree(min); + sdsfree(max); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_All) { + OrderedIndex *idx = api.create(); + insertLex(idx, {"apple", "banana", "cherry"}); + + OnDeleteRecord rec = {0, {}}; + sds min = sdsnew("a"); + sds max = sdsnew("z"); + unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 3UL); + ASSERT_EQ(rec.count, 3); + ASSERT_EQ(api.length(idx), 0UL); + + sdsfree(min); + sdsfree(max); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_NullCallback) { + OrderedIndex *idx = api.create(); + insertLex(idx, {"apple", "banana", "cherry", "date"}); + + sds min = sdsnew("banana"); + sds max = sdsnew("cherry"); + unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, NULL, NULL); + ASSERT_EQ(deleted, 2UL); + ASSERT_EQ(api.length(idx), 2UL); + + auto remaining = collectElements(idx); + ASSERT_EQ(remaining, (std::vector{"apple", "date"})); + + sdsfree(min); + sdsfree(max); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_ExclusiveBounds) { + OrderedIndex *idx = api.create(); + insertLex(idx, {"apple", "banana", "cherry", "date", "elderberry"}); + + OnDeleteRecord rec = {0, {}}; + sds min = sdsnew("banana"); + sds max = sdsnew("date"); + unsigned long deleted = api.deleteRangeByLex(idx, min, max, 1, 1, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 1UL); + ASSERT_EQ(rec.count, 1); + ASSERT_EQ(rec.elements[0], "cherry"); + ASSERT_EQ(api.length(idx), 4UL); + + auto remaining = collectElements(idx); + ASSERT_EQ(remaining, (std::vector{"apple", "banana", "date", "elderberry"})); + + sdsfree(min); + sdsfree(max); + api.free(idx); +} + +TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_SingleElement) { + OrderedIndex *idx = api.create(); + insertLex(idx, {"apple", "banana", "cherry"}); + + OnDeleteRecord rec = {0, {}}; + sds min = sdsnew("banana"); + sds max = sdsnew("banana"); + unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, testOnDeleteCallback, &rec); + ASSERT_EQ(deleted, 1UL); + ASSERT_EQ(rec.count, 1); + ASSERT_EQ(rec.elements[0], "banana"); + ASSERT_EQ(api.length(idx), 2UL); + + auto remaining = collectElements(idx); + ASSERT_EQ(remaining, (std::vector{"apple", "cherry"})); + + sdsfree(min); + sdsfree(max); + api.free(idx); +} + +/* ========== Range-Delete Hashtable Consistency Tests ========== */ + +static void hashtableConsistencyOnDelete(OrderedIndexItem *item, void *ctx) { + std::set *ht = (std::set *)ctx; + const char *ptr; + size_t len; + skiplistGetElementRaw(item, &ptr, &len); + ht->erase(std::string(ptr, len)); +} + +class RangeDeleteHashtableConsistencyTest : public ::testing::Test { + protected: + SkiplistOrderedIndex api; + + void insertN(OrderedIndex *idx, std::set &ht, int n) { + for (int i = 0; i < n; i++) { + std::string name = "key" + std::to_string(i); + sds ele = sdsnew(name.c_str()); + api.insertSds(idx, (double)i, ele); + ht.insert(name); + sdsfree(ele); + } + } + + void insertLex(OrderedIndex *idx, std::set &ht, const std::vector &elems, double score = 1.0) { + for (auto &e : elems) { + sds ele = sdsnew(e.c_str()); + api.insertSds(idx, score, ele); + ht.insert(e); + sdsfree(ele); + } + } + + std::set collectIndexElements(OrderedIndex *idx) { + std::set result; + OrderedIndexIterator iter; + OrderedIndexItem *pos; + api.initIterator(&iter, idx); + while (api.next(&iter, &pos)) { + const char *ptr; + size_t len; + api.getElementRaw(pos, &ptr, &len); + result.insert(std::string(ptr, len)); + } + api.resetIterator(&iter); + return result; + } +}; + +/* ByScore */ + +TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_PartialDelete) { + OrderedIndex *idx = api.create(); + std::set simulatedHt; + insertN(idx, simulatedHt, 10); + + api.deleteRangeByScore(idx, 3.0, 6.0, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + + std::set indexElements = collectIndexElements(idx); + ASSERT_EQ(indexElements, simulatedHt); + ASSERT_EQ(indexElements.size(), 6UL); + + api.free(idx); +} + +TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_FullDelete) { + OrderedIndex *idx = api.create(); + std::set simulatedHt; + insertN(idx, simulatedHt, 10); + + api.deleteRangeByScore(idx, NEG_INF, POS_INF, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + + std::set indexElements = collectIndexElements(idx); + ASSERT_EQ(indexElements, simulatedHt); + ASSERT_TRUE(indexElements.empty()); + + api.free(idx); +} + +TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_EmptyRange) { + OrderedIndex *idx = api.create(); + std::set simulatedHt; + insertN(idx, simulatedHt, 10); + + api.deleteRangeByScore(idx, 20.0, 30.0, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + + std::set indexElements = collectIndexElements(idx); + ASSERT_EQ(indexElements, simulatedHt); + ASSERT_EQ(indexElements.size(), 10UL); + + api.free(idx); +} + +/* ByRank */ + +TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_PartialDelete) { + OrderedIndex *idx = api.create(); + std::set simulatedHt; + insertN(idx, simulatedHt, 10); + + api.deleteRangeByRank(idx, 3, 5, hashtableConsistencyOnDelete, &simulatedHt); + + std::set indexElements = collectIndexElements(idx); + ASSERT_EQ(indexElements, simulatedHt); + ASSERT_EQ(indexElements.size(), 7UL); + + api.free(idx); +} + +TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_FullDelete) { + OrderedIndex *idx = api.create(); + std::set simulatedHt; + insertN(idx, simulatedHt, 10); + + api.deleteRangeByRank(idx, 1, 10, hashtableConsistencyOnDelete, &simulatedHt); + + std::set indexElements = collectIndexElements(idx); + ASSERT_EQ(indexElements, simulatedHt); + ASSERT_TRUE(indexElements.empty()); + + api.free(idx); +} + +TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_EmptyRange) { + OrderedIndex *idx = api.create(); + std::set simulatedHt; + insertN(idx, simulatedHt, 10); + + api.deleteRangeByRank(idx, 20, 30, hashtableConsistencyOnDelete, &simulatedHt); + + std::set indexElements = collectIndexElements(idx); + ASSERT_EQ(indexElements, simulatedHt); + ASSERT_EQ(indexElements.size(), 10UL); + + api.free(idx); +} + +/* ByLex */ + +TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_PartialDelete) { + OrderedIndex *idx = api.create(); + std::set simulatedHt; + insertLex(idx, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); + + sds min = sdsnew("banana"); + sds max = sdsnew("date"); + api.deleteRangeByLex(idx, min, max, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + + std::set indexElements = collectIndexElements(idx); + ASSERT_EQ(indexElements, simulatedHt); + ASSERT_EQ(indexElements.size(), 2UL); + + sdsfree(min); + sdsfree(max); + api.free(idx); +} + +TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_FullDelete) { + OrderedIndex *idx = api.create(); + std::set simulatedHt; + insertLex(idx, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); + + sds min = sdsnew("a"); + sds max = sdsnew("z"); + api.deleteRangeByLex(idx, min, max, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + + std::set indexElements = collectIndexElements(idx); + ASSERT_EQ(indexElements, simulatedHt); + ASSERT_TRUE(indexElements.empty()); + + sdsfree(min); + sdsfree(max); + api.free(idx); +} + +TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_EmptyRange) { + OrderedIndex *idx = api.create(); + std::set simulatedHt; + insertLex(idx, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); + + sds min = sdsnew("zzz"); + sds max = sdsnew("zzzz"); + api.deleteRangeByLex(idx, min, max, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + + std::set indexElements = collectIndexElements(idx); + ASSERT_EQ(indexElements, simulatedHt); + ASSERT_EQ(indexElements.size(), 5UL); + + sdsfree(min); + sdsfree(max); + api.free(idx); +} From bd0c609c32c7b239f32159ac2f1bc18845a3445d Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Wed, 13 May 2026 05:38:27 +0000 Subject: [PATCH 03/45] ordered-index: address interface review feedback - Remove static inline wrappers; add ordered_index.c with regular functions that delegate to the backend (LTO handles inlining) - Add comprehensive header comment to ordered_index.h explaining what an OrderedIndex is, its relationship to the companion hashtable, and why it does not enforce uniqueness - Add per-function doc comments for all API functions - Add header comment to skiplist_ordered_index.h - Rename 'idx' parameter to 'oi' throughout to avoid confusion with numeric array indices - Drop orderedIndexInsert(sds) convenience wrapper; rename orderedIndexInsertRaw to orderedIndexInsert (callers pass ptr+len) - Remove debug functions (skiplistGetHeight, skiplistVerifyIntegrity) from ordered_index.h public API; they remain in skiplist_ordered_index.h for unit tests and DEBUG command - Fix include ordering: skiplist_ordered_index.h now includes ordered_index.h for type visibility Feedback from JimB123 on PR #3552. Signed-off-by: Rain Valentine --- cmake/Modules/SourceFiles.cmake | 1 + src/Makefile | 1 + src/ordered_index.c | 150 ++++ src/ordered_index.h | 333 ++++---- src/skiplist_ordered_index.c | 100 +-- src/skiplist_ordered_index.h | 71 +- src/unit/ordered_index_test.h | 126 +-- src/unit/test_ordered_index.cpp | 1344 +++++++++++++++---------------- 8 files changed, 1142 insertions(+), 984 deletions(-) create mode 100644 src/ordered_index.c diff --git a/cmake/Modules/SourceFiles.cmake b/cmake/Modules/SourceFiles.cmake index fabb5ba041a..84cd1c109b7 100644 --- a/cmake/Modules/SourceFiles.cmake +++ b/cmake/Modules/SourceFiles.cmake @@ -35,6 +35,7 @@ set(VALKEY_SERVER_SRCS ${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 10546a8482f..825b2d48c9a 100644 --- a/src/Makefile +++ b/src/Makefile @@ -556,6 +556,7 @@ ENGINE_SERVER_OBJ = \ sha256.o \ siphash.o \ skiplist_ordered_index.o \ + ordered_index.o \ socket.o \ sort.o \ sparkline.o \ diff --git a/src/ordered_index.c b/src/ordered_index.c new file mode 100644 index 00000000000..5996461067c --- /dev/null +++ b/src/ordered_index.c @@ -0,0 +1,150 @@ +/* + * 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 orderedIndexDeleteRangeByRank(OrderedIndex *oi, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) { + return skiplistDeleteRangeByRank(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 *orderedIndexGetByRank(OrderedIndex *oi, unsigned long rank) { + return skiplistGetByRank(oi, rank); +} + +unsigned long orderedIndexGetRank(OrderedIndex *oi, const OrderedIndexItem *item) { + return skiplistGetRank(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 orderedIndexSeekToRank(OrderedIndexIterator *iter, unsigned long rank) { + skiplistSeekToRank(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); +} diff --git a/src/ordered_index.h b/src/ordered_index.h index 9db801cf84a..a9f6449c460 100644 --- a/src/ordered_index.h +++ b/src/ordered_index.h @@ -1,187 +1,176 @@ +/* + * 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 backend-agnostic. Currently the only backend is a skiplist + * (see skiplist_ordered_index.c). A B+ tree backend is planned. Backend + * selection is resolved at link time — all orderedIndex* functions are + * implemented in ordered_index.c which delegates to the active backend. */ + #include "sds.h" +#include -/* Opaque types for ordered index, positions, and iterators */ +/* 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. */ +/* Callback invoked for each item removed during a range-delete operation. + * The callback receives ownership of the item — it must free it or store it. */ typedef void (*OrderedIndexOnDelete)(OrderedIndexItem *item, void *ctx); -/* ---- Production inline wrappers ---- - * - * Currently hardcoded to the skiplist implementation. When additional ordered - * index backends are added (e.g. B-tree), a compile-time switch can select - * the implementation here without changing any call sites. */ -#include "skiplist_ordered_index.h" - -/* Lifecycle */ -static inline OrderedIndex *orderedIndexCreate(void) { - return skiplistCreate(); -} - -static inline void orderedIndexFree(OrderedIndex *idx) { - skiplistFree(idx); -} - -/* Modification */ -static inline OrderedIndexItem *orderedIndexInsertRaw(OrderedIndex *idx, double score, const char *ele, size_t len) { - return skiplistInsert(idx, score, ele, len); -} - -static inline OrderedIndexItem *orderedIndexInsert(OrderedIndex *idx, double score, const_sds ele) { - return skiplistInsert(idx, score, ele, sdslen(ele)); -} - -static inline void orderedIndexDelete(OrderedIndex *idx, OrderedIndexItem *pos) { - skiplistDelete(idx, pos); -} - -static inline OrderedIndexItem *orderedIndexUpdateScore(OrderedIndex *idx, OrderedIndexItem *pos, double newscore) { - return skiplistUpdateScore(idx, pos, newscore); -} - -static inline OrderedIndexItem *orderedIndexPopFirst(OrderedIndex *idx) { - return skiplistPopFirst(idx); -} - -static inline OrderedIndexItem *orderedIndexPopLast(OrderedIndex *idx) { - return skiplistPopLast(idx); -} - -static inline void orderedIndexFreeItem(OrderedIndexItem *item) { - skiplistFreeItem(item); -} - -static inline OrderedIndexItem *orderedIndexCreateDetached(double score, const char *ele, size_t len) { - return skiplistCreateDetached(score, ele, len); -} - -/* Set the score on a detached item — one created via orderedIndexCreateDetached - * but not yet inserted into an ordered index. Items that live only in a - * hashtable (e.g. during ZUNIONSTORE score accumulation) are still considered - * "detached" for this purpose because they are not part of any ordered index. - * - * Do NOT use on items that have been inserted into an ordered index — doing so - * would silently corrupt the sort order. Use orderedIndexUpdateScore instead. */ -static inline void orderedIndexDetachedSetScore(OrderedIndexItem *item, double score) { - skiplistDetachedSetScore(item, score); -} - -static inline OrderedIndexItem *orderedIndexInsertDetached(OrderedIndex *idx, OrderedIndexItem *item) { - return skiplistInsertDetached(idx, item); -} - -static inline unsigned long orderedIndexDeleteRangeByScore(OrderedIndex *idx, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) { - return skiplistDeleteRangeByScore(idx, min, max, min_ex, max_ex, on_delete, ctx); -} - -static inline unsigned long orderedIndexDeleteRangeByRank(OrderedIndex *idx, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) { - return skiplistDeleteRangeByRank(idx, start, end, on_delete, ctx); -} - -static inline unsigned long orderedIndexDeleteRangeByLex(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) { - return skiplistDeleteRangeByLex(idx, min, max, min_ex, max_ex, on_delete, ctx); -} - -/* Query */ -static inline unsigned long orderedIndexLength(OrderedIndex *idx) { - return skiplistLength(idx); -} - -static inline OrderedIndexItem *orderedIndexGetByRank(OrderedIndex *idx, unsigned long rank) { - return skiplistGetByRank(idx, rank); -} - -static inline unsigned long orderedIndexGetRank(OrderedIndex *idx, const OrderedIndexItem *pos) { - return skiplistGetRank(idx, pos); -} - -static inline void orderedIndexGetElementRaw(const OrderedIndexItem *pos, const char **ptr, size_t *len) { - skiplistGetElementRaw(pos, ptr, len); -} - -static inline double orderedIndexGetScore(const OrderedIndexItem *pos) { - return skiplistGetScore(pos); -} - -static inline unsigned long orderedIndexCountScoreRange(OrderedIndex *idx, double min, double max, int min_ex, int max_ex) { - return skiplistCountScoreRange(idx, min, max, min_ex, max_ex); -} - -static inline unsigned long orderedIndexCountLexRange(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex) { - return skiplistCountLexRange(idx, min, max, min_ex, max_ex); -} - -/* Iterator */ -static inline void orderedIndexInitIterator(OrderedIndexIterator *iter, OrderedIndex *idx) { - skiplistInitIterator(iter, idx); -} - -static inline void orderedIndexResetIterator(OrderedIndexIterator *iter) { - skiplistResetIterator(iter); -} - -static inline bool orderedIndexNext(OrderedIndexIterator *iter, OrderedIndexItem **pos) { - return skiplistNext(iter, pos); -} - -static inline bool orderedIndexPrev(OrderedIndexIterator *iter, OrderedIndexItem **pos) { - return skiplistPrev(iter, pos); -} - -static inline void orderedIndexSeekToRank(OrderedIndexIterator *iter, unsigned long rank) { - skiplistSeekToRank(iter, rank); -} - -/* Seek to a position within a score/lex range. - * - * offset >= 0: positions for forward iteration (next() returns the element). - * offset 0 = first element in range, 1 = second, etc. - * offset < 0: positions for reverse iteration (prev() returns the element). - * offset -1 = last element in range, -2 = second-to-last, etc. */ -static inline 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); -} - -static inline 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 */ -static inline void orderedIndexDismissMemory(OrderedIndex *idx) { - skiplistDismissMemory(idx); -} - -static inline size_t orderedIndexEstimateMemory(OrderedIndex *idx, size_t sample_size) { - return skiplistEstimateMemory(idx, sample_size); -} - -/* Defrag */ +/* 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); -static inline OrderedIndex *orderedIndexDefragInternals(OrderedIndex *idx, void *(*defragfn)(void *)) { - return skiplistDefragInternals(idx, defragfn); -} - -/* Cursor-based incremental defrag. Walks the ordered index in batches, - * calling defragfn on each item. When an item is reallocated, the callback - * is invoked so the caller can update external references (e.g. hashtable). - * Returns the next cursor, or 0 when the scan is complete. */ -static inline unsigned long orderedIndexScanDefrag(OrderedIndex *idx, unsigned long cursor, OrderedIndexDefragCallback callback, void *ctx, void *(*defragfn)(void *)) { - return skiplistScanDefrag(idx, cursor, callback, ctx, defragfn); -} - -/* Debug */ -static inline int orderedIndexGetHeight(OrderedIndex *idx) { - return skiplistGetHeight(idx); -} - -static inline int orderedIndexVerifyIntegrity(OrderedIndex *idx, char *errmsg, size_t errmsg_len) { - return skiplistVerifyIntegrity(idx, errmsg, errmsg_len); -} +/* ============================================================ + * 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. + * Returns NULL if the item stayed in place (score updated in-place). */ +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] (exclusive if min_ex/max_ex set). + * 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 rank in [start, end] (1-based, inclusive). + * Calls on_delete for each removed item. Returns count of items removed. */ +unsigned long orderedIndexDeleteRangeByRank(OrderedIndex *oi, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx); + +/* Delete all items with element in lex range [min, max]. + * 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 1-based rank, or NULL if out of range. */ +OrderedIndexItem *orderedIndexGetByRank(OrderedIndex *oi, unsigned long rank); + +/* Return the 1-based rank of an item. The item must be in the index. */ +unsigned long orderedIndexGetRank(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] (exclusive if min_ex/max_ex set). */ +unsigned long orderedIndexCountScoreRange(OrderedIndex *oi, double min, double max, int min_ex, int max_ex); + +/* Count items with element in lex range [min, max]. */ +unsigned long orderedIndexCountLexRange(OrderedIndex *oi, const_sds min, const_sds max, int min_ex, int max_ex); + +/* ============================================================ + * Iterator + * ============================================================ */ + +/* Initialize a stack-allocated iterator. Must call a seek function before + * iterating, or next()/prev() will start from the beginning/end. */ +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 rank. next() returns rank+1, prev() returns rank. */ +void orderedIndexSeekToRank(OrderedIndexIterator *iter, unsigned long rank); + +/* 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 sampling. */ +size_t orderedIndexEstimateMemory(OrderedIndex *oi, size_t sample_size); + +/* Defrag the index header/metadata. 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/skiplist_ordered_index.c b/src/skiplist_ordered_index.c index 44b2e8cce77..087c942dfb6 100644 --- a/src/skiplist_ordered_index.c +++ b/src/skiplist_ordered_index.c @@ -17,37 +17,37 @@ OrderedIndex *skiplistCreate(void) { return (OrderedIndex *)zslCreate(); } -void skiplistFree(OrderedIndex *idx) { - zslFree((zskiplist *)idx); +void skiplistFree(OrderedIndex *oi) { + zslFree((zskiplist *)oi); } /* Modification */ -OrderedIndexItem *skiplistInsert(OrderedIndex *idx, double score, const char *ele, size_t len) { +OrderedIndexItem *skiplistInsert(OrderedIndex *oi, double score, const char *ele, size_t len) { zskiplistNode *node = zslCreateNode(zslRandomLevel(), score, ele, len); - zslInsertNode((zskiplist *)idx, node); + zslInsertNode((zskiplist *)oi, node); return (OrderedIndexItem *)node; } -void skiplistDelete(OrderedIndex *idx, OrderedIndexItem *node) { - zslDelete((zskiplist *)idx, (zskiplistNode *)node); +void skiplistDelete(OrderedIndex *oi, OrderedIndexItem *node) { + zslDelete((zskiplist *)oi, (zskiplistNode *)node); } -OrderedIndexItem *skiplistUpdateScore(OrderedIndex *idx, OrderedIndexItem *node, double newscore) { - zskiplistNode *result = zslUpdateScore((zskiplist *)idx, (zskiplistNode *)node, newscore); +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 *idx) { - zskiplist *zsl = (zskiplist *)idx; +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 *idx) { - zskiplist *zsl = (zskiplist *)idx; +OrderedIndexItem *skiplistPopLast(OrderedIndex *oi) { + zskiplist *zsl = (zskiplist *)oi; zskiplistNode *last = zslGetTail(zsl); if (!last) return NULL; zslDetachNode(zsl, last); @@ -71,13 +71,13 @@ void skiplistDetachedSetScore(OrderedIndexItem *item, double score) { ((zskiplistNode *)item)->score = score; } -OrderedIndexItem *skiplistInsertDetached(OrderedIndex *idx, OrderedIndexItem *item) { - zskiplistNode *node = zslInsertNode((zskiplist *)idx, (zskiplistNode *)item); +OrderedIndexItem *skiplistInsertDetached(OrderedIndex *oi, OrderedIndexItem *item) { + zskiplistNode *node = zslInsertNode((zskiplist *)oi, (zskiplistNode *)item); return (OrderedIndexItem *)node; } -unsigned long skiplistDeleteRangeByScore(OrderedIndex *idx, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) { - zskiplist *zsl = (zskiplist *)idx; +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; @@ -107,8 +107,8 @@ unsigned long skiplistDeleteRangeByScore(OrderedIndex *idx, double min, double m return removed; } -unsigned long skiplistDeleteRangeByRank(OrderedIndex *idx, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) { - zskiplist *zsl = (zskiplist *)idx; +unsigned long skiplistDeleteRangeByRank(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; @@ -138,8 +138,8 @@ unsigned long skiplistDeleteRangeByRank(OrderedIndex *idx, unsigned long start, return removed; } -unsigned long skiplistDeleteRangeByLex(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) { - zskiplist *zsl = (zskiplist *)idx; +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; @@ -176,16 +176,16 @@ unsigned long skiplistDeleteRangeByLex(OrderedIndex *idx, const_sds min, const_s /* Query */ -unsigned long skiplistLength(OrderedIndex *idx) { - return zslGetLength((zskiplist *)idx); +unsigned long skiplistLength(OrderedIndex *oi) { + return zslGetLength((zskiplist *)oi); } -OrderedIndexItem *skiplistGetByRank(OrderedIndex *idx, unsigned long rank) { - return (OrderedIndexItem *)zslGetElementByRank((zskiplist *)idx, rank); +OrderedIndexItem *skiplistGetByRank(OrderedIndex *oi, unsigned long rank) { + return (OrderedIndexItem *)zslGetElementByRank((zskiplist *)oi, rank); } -unsigned long skiplistGetRank(OrderedIndex *idx, const OrderedIndexItem *node) { - return zslGetRank((zskiplist *)idx, (const zskiplistNode *)node); +unsigned long skiplistGetRank(OrderedIndex *oi, const OrderedIndexItem *node) { + return zslGetRank((zskiplist *)oi, (const zskiplistNode *)node); } void skiplistGetElementRaw(const OrderedIndexItem *node, const char **ptr, size_t *len) { @@ -199,8 +199,8 @@ double skiplistGetScore(const OrderedIndexItem *node) { return zslGetScore((const zskiplistNode *)node); } -unsigned long skiplistCountScoreRange(OrderedIndex *idx, double min, double max, int min_ex, int max_ex) { - zskiplist *zsl = (zskiplist *)idx; +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; @@ -215,8 +215,8 @@ unsigned long skiplistCountScoreRange(OrderedIndex *idx, double min, double max, return (unsigned long)(last_rank - first_rank + 1); } -unsigned long skiplistCountLexRange(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex) { - zskiplist *zsl = (zskiplist *)idx; +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. */ @@ -234,20 +234,22 @@ unsigned long skiplistCountLexRange(OrderedIndex *idx, const_sds min, const_sds /* Iterator */ -void skiplistInitIterator(OrderedIndexIterator *iter, OrderedIndex *idx) { - zslInitIterator((zslIter *)iter, (zskiplist *)idx); +void skiplistInitIterator(OrderedIndexIterator *iter, OrderedIndex *oi) { + zslInitIterator((zslIter *)iter, (zskiplist *)oi); } void skiplistResetIterator(OrderedIndexIterator *iter) { zslResetIterator((zslIter *)iter); } -bool skiplistNext(OrderedIndexIterator *iter, OrderedIndexItem **pos) { - return zslNext((zslIter *)iter, (zskiplistNode **)pos); +OrderedIndexItem *skiplistNext(OrderedIndexIterator *iter) { + zskiplistNode *node; + return zslNext((zslIter *)iter, &node) ? (OrderedIndexItem *)node : NULL; } -bool skiplistPrev(OrderedIndexIterator *iter, OrderedIndexItem **pos) { - return zslPrev((zslIter *)iter, (zskiplistNode **)pos); +OrderedIndexItem *skiplistPrev(OrderedIndexIterator *iter) { + zskiplistNode *node; + return zslPrev((zslIter *)iter, &node) ? (OrderedIndexItem *)node : NULL; } void skiplistSeekToRank(OrderedIndexIterator *iter, unsigned long rank) { @@ -264,8 +266,8 @@ void skiplistSeekToLexRange(OrderedIndexIterator *iter, const_sds min, const_sds /* Memory */ -void skiplistDismissMemory(OrderedIndex *idx) { - zskiplist *zsl = (zskiplist *)idx; +void skiplistDismissMemory(OrderedIndex *oi) { + zskiplist *zsl = (zskiplist *)oi; zskiplistNode *zn = zslGetTail(zsl); while (zn != NULL) { zskiplistNode *prev = zn->backward; @@ -274,8 +276,8 @@ void skiplistDismissMemory(OrderedIndex *idx) { } } -size_t skiplistEstimateMemory(OrderedIndex *idx, size_t sample_size) { - zskiplist *zsl = (zskiplist *)idx; +size_t skiplistEstimateMemory(OrderedIndex *oi, size_t sample_size) { + zskiplist *zsl = (zskiplist *)oi; unsigned long length = zslGetLength(zsl); size_t asize = zslGetAllocSize(); @@ -295,9 +297,9 @@ size_t skiplistEstimateMemory(OrderedIndex *idx, size_t sample_size) { /* Defrag */ -OrderedIndex *skiplistDefragInternals(OrderedIndex *idx, void *(*defragfn)(void *)) { - OrderedIndex *newidx = defragfn(idx); - return newidx; /* NULL if no move needed */ +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. @@ -322,8 +324,8 @@ static void skiplistPatchNodePointers(zskiplist *zsl, zskiplistNode *oldnode, zs * * Processes up to 16 nodes per call to bound latency, returning the * next cursor position (or 0 when complete). */ -unsigned long skiplistScanDefrag(OrderedIndex *idx, unsigned long cursor, void (*callback)(OrderedIndexItem *old_item, OrderedIndexItem *new_item, void *ctx), void *ctx, void *(*defragfn)(void *)) { - zskiplist *zsl = (zskiplist *)idx; +unsigned long skiplistScanDefrag(OrderedIndex *oi, unsigned long cursor, void (*callback)(OrderedIndexItem *old_item, OrderedIndexItem *new_item, void *ctx), 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 */ @@ -372,14 +374,14 @@ unsigned long skiplistScanDefrag(OrderedIndex *idx, unsigned long cursor, void ( /* Debug */ -int skiplistGetHeight(OrderedIndex *idx) { - return zslGetHeight((zskiplist *)idx); +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 *idx, char *errmsg, size_t errmsg_len) { - zskiplist *zsl = (zskiplist *)idx; +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); diff --git a/src/skiplist_ordered_index.h b/src/skiplist_ordered_index.h index 985856d2c4c..f9831301e48 100644 --- a/src/skiplist_ordered_index.h +++ b/src/skiplist_ordered_index.h @@ -1,54 +1,69 @@ +/* + * Copyright (c) Valkey Contributors + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + #ifndef SKIPLIST_ORDERED_INDEX_H #define SKIPLIST_ORDERED_INDEX_H -#include "sds.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 *idx); +void skiplistFree(OrderedIndex *oi); /* Modification */ -OrderedIndexItem *skiplistInsert(OrderedIndex *idx, double score, const char *ele, size_t len); -void skiplistDelete(OrderedIndex *idx, OrderedIndexItem *node); -OrderedIndexItem *skiplistUpdateScore(OrderedIndex *idx, OrderedIndexItem *node, double newscore); -OrderedIndexItem *skiplistPopFirst(OrderedIndex *idx); -OrderedIndexItem *skiplistPopLast(OrderedIndex *idx); +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 *idx, OrderedIndexItem *item); -unsigned long skiplistDeleteRangeByScore(OrderedIndex *idx, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx); -unsigned long skiplistDeleteRangeByRank(OrderedIndex *idx, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx); -unsigned long skiplistDeleteRangeByLex(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx); +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 skiplistDeleteRangeByRank(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 *idx); -OrderedIndexItem *skiplistGetByRank(OrderedIndex *idx, unsigned long rank); -unsigned long skiplistGetRank(OrderedIndex *idx, const OrderedIndexItem *node); -void skiplistGetElementRaw(const OrderedIndexItem *node, const char **ptr, size_t *len); -double skiplistGetScore(const OrderedIndexItem *node); -unsigned long skiplistCountScoreRange(OrderedIndex *idx, double min, double max, int min_ex, int max_ex); -unsigned long skiplistCountLexRange(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex); +unsigned long skiplistLength(OrderedIndex *oi); +OrderedIndexItem *skiplistGetByRank(OrderedIndex *oi, unsigned long rank); +unsigned long skiplistGetRank(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 *idx); +void skiplistInitIterator(OrderedIndexIterator *iter, OrderedIndex *oi); void skiplistResetIterator(OrderedIndexIterator *iter); -bool skiplistNext(OrderedIndexIterator *iter, OrderedIndexItem **pos); -bool skiplistPrev(OrderedIndexIterator *iter, OrderedIndexItem **pos); +OrderedIndexItem *skiplistNext(OrderedIndexIterator *iter); +OrderedIndexItem *skiplistPrev(OrderedIndexIterator *iter); void skiplistSeekToRank(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 *idx); -size_t skiplistEstimateMemory(OrderedIndex *idx, size_t sample_size); +void skiplistDismissMemory(OrderedIndex *oi); +size_t skiplistEstimateMemory(OrderedIndex *oi, size_t sample_size); /* Defrag */ -OrderedIndex *skiplistDefragInternals(OrderedIndex *idx, void *(*defragfn)(void *)); -unsigned long skiplistScanDefrag(OrderedIndex *idx, unsigned long cursor, void (*callback)(OrderedIndexItem *old_item, OrderedIndexItem *new_item, void *ctx), void *ctx, void *(*defragfn)(void *)); +OrderedIndex *skiplistDefragInternals(OrderedIndex *oi, void *(*defragfn)(void *)); +unsigned long skiplistScanDefrag(OrderedIndex *oi, unsigned long cursor, OrderedIndexDefragCallback callback, void *ctx, void *(*defragfn)(void *)); -/* Debug */ -int skiplistGetHeight(OrderedIndex *idx); -int skiplistVerifyIntegrity(OrderedIndex *idx, char *errmsg, size_t errmsg_len); +/* 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/unit/ordered_index_test.h b/src/unit/ordered_index_test.h index c7aa22d372b..07d98073b22 100644 --- a/src/unit/ordered_index_test.h +++ b/src/unit/ordered_index_test.h @@ -5,8 +5,8 @@ * Test-only interface for ordered index implementations. * * Defines an abstract C++ interface that each implementation subclasses. - * Production code uses compile-time dispatch via the inline wrappers in - * ordered_index.h instead. + * Production code uses the functions declared in ordered_index.h + * (implemented in ordered_index.c) which delegate to the active backend. */ extern "C" { @@ -26,55 +26,55 @@ class OrderedIndexTestApi { /* Lifecycle */ virtual OrderedIndex *create() = 0; - virtual void free(OrderedIndex *idx) = 0; + virtual void free(OrderedIndex *oi) = 0; /* Modification */ - virtual OrderedIndexItem *insert(OrderedIndex *idx, double score, const char *ele, size_t len) = 0; - virtual void deleteItem(OrderedIndex *idx, OrderedIndexItem *pos) = 0; - virtual OrderedIndexItem *updateScore(OrderedIndex *idx, OrderedIndexItem *pos, double newscore) = 0; - virtual OrderedIndexItem *popFirst(OrderedIndex *idx) = 0; - virtual OrderedIndexItem *popLast(OrderedIndex *idx) = 0; + 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 *idx, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) = 0; - virtual unsigned long deleteRangeByRank(OrderedIndex *idx, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) = 0; - virtual unsigned long deleteRangeByLex(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) = 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 deleteRangeByRank(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 *idx) = 0; - virtual OrderedIndexItem *getByRank(OrderedIndex *idx, unsigned long rank) = 0; - virtual unsigned long getRank(OrderedIndex *idx, const OrderedIndexItem *pos) = 0; + virtual unsigned long length(OrderedIndex *oi) = 0; + virtual OrderedIndexItem *getByRank(OrderedIndex *oi, unsigned long rank) = 0; + virtual unsigned long getRank(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 *idx, size_t sample_size) = 0; + virtual size_t estimateMemory(OrderedIndex *oi, size_t sample_size) = 0; /* Debug / verification */ - virtual int verifyIntegrity(OrderedIndex *idx, char *errmsg, size_t errmsg_len) = 0; + virtual int verifyIntegrity(OrderedIndex *oi, char *errmsg, size_t errmsg_len) = 0; /* Count */ - virtual unsigned long countScoreRange(OrderedIndex *idx, double min, double max, int min_ex, int max_ex) = 0; - virtual unsigned long countLexRange(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex) = 0; + 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 *idx) = 0; + virtual void initIterator(OrderedIndexIterator *iter, OrderedIndex *oi) = 0; virtual void resetIterator(OrderedIndexIterator *iter) = 0; - virtual bool next(OrderedIndexIterator *iter, OrderedIndexItem **pos) = 0; - virtual bool prev(OrderedIndexIterator *iter, OrderedIndexItem **pos) = 0; + virtual OrderedIndexItem *next(OrderedIndexIterator *iter) = 0; + virtual OrderedIndexItem *prev(OrderedIndexIterator *iter) = 0; virtual void seekToRank(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 *idx, double score, const_sds ele) { - return insert(idx, score, ele, sdslen(ele)); + OrderedIndexItem *insertSds(OrderedIndex *oi, double score, const_sds ele) { + return insert(oi, score, ele, sdslen(ele)); } - std::vector> collectAll(OrderedIndex *idx) { + std::vector> collectAll(OrderedIndex *oi) { std::vector> result; OrderedIndexIterator iter; OrderedIndexItem *pos; - initIterator(&iter, idx); + initIterator(&iter, oi); while (next(&iter, &pos)) { const char *ptr; size_t len; @@ -93,46 +93,46 @@ class SkiplistOrderedIndex : public OrderedIndexTestApi { OrderedIndex *create() override { return skiplistCreate(); } - void free(OrderedIndex *idx) override { - skiplistFree(idx); + void free(OrderedIndex *oi) override { + skiplistFree(oi); } - OrderedIndexItem *insert(OrderedIndex *idx, double score, const char *ele, size_t len) override { - return skiplistInsert(idx, score, ele, len); + OrderedIndexItem *insert(OrderedIndex *oi, double score, const char *ele, size_t len) override { + return skiplistInsert(oi, score, ele, len); } - void deleteItem(OrderedIndex *idx, OrderedIndexItem *pos) override { - skiplistDelete(idx, pos); + void deleteItem(OrderedIndex *oi, OrderedIndexItem *pos) override { + skiplistDelete(oi, pos); } - OrderedIndexItem *updateScore(OrderedIndex *idx, OrderedIndexItem *pos, double newscore) override { - return skiplistUpdateScore(idx, pos, newscore); + OrderedIndexItem *updateScore(OrderedIndex *oi, OrderedIndexItem *pos, double newscore) override { + return skiplistUpdateScore(oi, pos, newscore); } - OrderedIndexItem *popFirst(OrderedIndex *idx) override { - return skiplistPopFirst(idx); + OrderedIndexItem *popFirst(OrderedIndex *oi) override { + return skiplistPopFirst(oi); } - OrderedIndexItem *popLast(OrderedIndex *idx) override { - return skiplistPopLast(idx); + OrderedIndexItem *popLast(OrderedIndex *oi) override { + return skiplistPopLast(oi); } void freeItem(OrderedIndexItem *item) override { skiplistFreeItem(item); } - unsigned long deleteRangeByScore(OrderedIndex *idx, double min, double max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) override { - return skiplistDeleteRangeByScore(idx, min, max, min_ex, max_ex, on_delete, ctx); + 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 deleteRangeByRank(OrderedIndex *idx, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) override { - return skiplistDeleteRangeByRank(idx, start, end, on_delete, ctx); + unsigned long deleteRangeByRank(OrderedIndex *oi, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) override { + return skiplistDeleteRangeByRank(oi, start, end, on_delete, ctx); } - unsigned long deleteRangeByLex(OrderedIndex *idx, const_sds min, const_sds max, int min_ex, int max_ex, OrderedIndexOnDelete on_delete, void *ctx) override { - return skiplistDeleteRangeByLex(idx, min, max, min_ex, max_ex, 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 *idx) override { - return skiplistLength(idx); + unsigned long length(OrderedIndex *oi) override { + return skiplistLength(oi); } - OrderedIndexItem *getByRank(OrderedIndex *idx, unsigned long rank) override { - return skiplistGetByRank(idx, rank); + OrderedIndexItem *getByRank(OrderedIndex *oi, unsigned long rank) override { + return skiplistGetByRank(oi, rank); } - unsigned long getRank(OrderedIndex *idx, const OrderedIndexItem *pos) override { - return skiplistGetRank(idx, pos); + unsigned long getRank(OrderedIndex *oi, const OrderedIndexItem *pos) override { + return skiplistGetRank(oi, pos); } void getElementRaw(const OrderedIndexItem *pos, const char **ptr, size_t *len) override { skiplistGetElementRaw(pos, ptr, len); @@ -141,32 +141,32 @@ class SkiplistOrderedIndex : public OrderedIndexTestApi { return skiplistGetScore(pos); } - size_t estimateMemory(OrderedIndex *idx, size_t sample_size) override { - return skiplistEstimateMemory(idx, sample_size); + size_t estimateMemory(OrderedIndex *oi, size_t sample_size) override { + return skiplistEstimateMemory(oi, sample_size); } - int verifyIntegrity(OrderedIndex *idx, char *errmsg, size_t errmsg_len) override { - return skiplistVerifyIntegrity(idx, errmsg, errmsg_len); + int verifyIntegrity(OrderedIndex *oi, char *errmsg, size_t errmsg_len) override { + return skiplistVerifyIntegrity(oi, errmsg, errmsg_len); } - unsigned long countScoreRange(OrderedIndex *idx, double min, double max, int min_ex, int max_ex) override { - return skiplistCountScoreRange(idx, min, max, min_ex, max_ex); + 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 *idx, const_sds min, const_sds max, int min_ex, int max_ex) override { - return skiplistCountLexRange(idx, 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 *idx) override { - skiplistInitIterator(iter, idx); + void initIterator(OrderedIndexIterator *iter, OrderedIndex *oi) override { + skiplistInitIterator(iter, oi); } void resetIterator(OrderedIndexIterator *iter) override { skiplistResetIterator(iter); } - bool next(OrderedIndexIterator *iter, OrderedIndexItem **pos) override { - return skiplistNext(iter, pos); + OrderedIndexItem *next(OrderedIndexIterator *iter) override { + return skiplistNext(iter); } - bool prev(OrderedIndexIterator *iter, OrderedIndexItem **pos) override { - return skiplistPrev(iter, pos); + OrderedIndexItem *prev(OrderedIndexIterator *iter) override { + return skiplistPrev(iter); } void seekToRank(OrderedIndexIterator *iter, unsigned long rank) override { skiplistSeekToRank(iter, rank); diff --git a/src/unit/test_ordered_index.cpp b/src/unit/test_ordered_index.cpp index 372006d39c4..b2ca714f088 100644 --- a/src/unit/test_ordered_index.cpp +++ b/src/unit/test_ordered_index.cpp @@ -28,9 +28,9 @@ extern "C" { #define TEST_ASSERT_SCORE_EQ(a, b) ASSERT_DOUBLE_EQ(a, b) /* Verify structural integrity of the ordered index after mutations. */ -static ::testing::AssertionResult verifyIntegrity(OrderedIndexTestApi &api, OrderedIndex *idx) { +static ::testing::AssertionResult verifyIntegrity(OrderedIndexTestApi &api, OrderedIndex *oi) { char errmsg[256]; - if (api.verifyIntegrity(idx, errmsg, sizeof(errmsg))) + if (api.verifyIntegrity(oi, errmsg, sizeof(errmsg))) return ::testing::AssertionResult(true); return ::testing::AssertionFailure() << errmsg; } @@ -51,28 +51,28 @@ class OrderedIndexTest : public ::testing::TestWithParam /* ========== Basic tests ========== */ TEST_P(OrderedIndexTest, CreateFree) { - OrderedIndex *idx = api.create(); - TEST_ASSERT(idx != NULL); - TEST_ASSERT(api.length(idx) == 0); - VERIFY_INTEGRITY(api, idx); + OrderedIndex *oi = api.create(); + TEST_ASSERT(oi != NULL); + TEST_ASSERT(api.length(oi) == 0); + VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - TEST_ASSERT(!api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, InsertSingle) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); sds ele = sdsnew("test"); - OrderedIndexItem *node = api.insertSds(idx, 1.0, ele); - VERIFY_INTEGRITY(api, idx); + OrderedIndexItem *node = api.insertSds(oi, 1.0, ele); + VERIFY_INTEGRITY(api, oi); TEST_ASSERT(node != NULL); - TEST_ASSERT(api.length(idx) == 1); + TEST_ASSERT(api.length(oi) == 1); TEST_ASSERT_SCORE_EQ(api.getScore(node), 1.0); const char *ptr; @@ -82,73 +82,73 @@ TEST_P(OrderedIndexTest, InsertSingle) { OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT(pos == node); - TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); sdsfree(ele); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, InsertMultipleOrdered) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); for (int i = 0; i < 10; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } - TEST_ASSERT(api.length(idx) == 10); - VERIFY_INTEGRITY(api, idx); + TEST_ASSERT(api.length(oi) == 10); + VERIFY_INTEGRITY(api, oi); /* Verify forward traversal */ OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); for (int i = 0; i < 10; i++) { - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)i); } - TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); /* Verify backward traversal */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); for (int i = 9; i >= 0; i--) { - TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT((pos = api.prev(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)i); } - TEST_ASSERT(!api.prev(&iter, &pos)); + TEST_ASSERT((pos = api.prev(&iter)) == NULL); api.resetIterator(&iter); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, DuplicateScores) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); for (int i = 0; i < 5; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, 1.0, ele); + api.insertSds(oi, 1.0, ele); sdsfree(ele); } - TEST_ASSERT(api.length(idx) == 5); - VERIFY_INTEGRITY(api, idx); + TEST_ASSERT(api.length(oi) == 5); + VERIFY_INTEGRITY(api, oi); /* Verify lexicographic ordering for same scores */ OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); for (int i = 0; i < 5; i++) { - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); const char *ptr; size_t len; @@ -159,84 +159,84 @@ TEST_P(OrderedIndexTest, DuplicateScores) { } api.resetIterator(&iter); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, RankOperations) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); OrderedIndexItem *nodes[10]; for (int i = 0; i < 10; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - nodes[i] = api.insertSds(idx, (double)i, ele); + nodes[i] = api.insertSds(oi, (double)i, ele); sdsfree(ele); } - VERIFY_INTEGRITY(api, idx); + VERIFY_INTEGRITY(api, oi); for (int i = 0; i < 10; i++) { - unsigned long rank = api.getRank(idx, nodes[i]); + unsigned long rank = api.getRank(oi, nodes[i]); TEST_ASSERT(rank == (unsigned long)(i + 1)); /* 1-based */ } for (int i = 0; i < 10; i++) { - OrderedIndexItem *node = api.getByRank(idx, i + 1); + OrderedIndexItem *node = api.getByRank(oi, i + 1); TEST_ASSERT(node == nodes[i]); } - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, Delete) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); OrderedIndexItem *nodes[5]; for (int i = 0; i < 5; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - nodes[i] = api.insertSds(idx, (double)i, ele); + nodes[i] = api.insertSds(oi, (double)i, ele); sdsfree(ele); } - TEST_ASSERT(api.length(idx) == 5); + TEST_ASSERT(api.length(oi) == 5); - api.deleteItem(idx, nodes[2]); - TEST_ASSERT(api.length(idx) == 4); - VERIFY_INTEGRITY(api, idx); + api.deleteItem(oi, nodes[2]); + TEST_ASSERT(api.length(oi) == 4); + VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 0.0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 3.0); /* Skipped 2.0 */ - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); api.resetIterator(&iter); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, PopFirst) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); - TEST_ASSERT(api.popFirst(idx) == NULL); + TEST_ASSERT(api.popFirst(oi) == NULL); for (int i = 0; i < 5; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } - TEST_ASSERT(api.length(idx) == 5); + TEST_ASSERT(api.length(oi) == 5); - OrderedIndexItem *item = api.popFirst(idx); + OrderedIndexItem *item = api.popFirst(oi); TEST_ASSERT(item != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(item), 0.0); const char *ptr; @@ -244,33 +244,33 @@ TEST_P(OrderedIndexTest, PopFirst) { api.getElementRaw(item, &ptr, &len); TEST_ASSERT(len == 4 && memcmp(ptr, "key0", 4) == 0); api.freeItem(item); - TEST_ASSERT(api.length(idx) == 4); - VERIFY_INTEGRITY(api, idx); + TEST_ASSERT(api.length(oi) == 4); + VERIFY_INTEGRITY(api, oi); - item = api.popFirst(idx); + item = api.popFirst(oi); TEST_ASSERT_SCORE_EQ(api.getScore(item), 1.0); api.freeItem(item); - TEST_ASSERT(api.length(idx) == 3); - VERIFY_INTEGRITY(api, idx); + TEST_ASSERT(api.length(oi) == 3); + VERIFY_INTEGRITY(api, oi); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, PopLast) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); - TEST_ASSERT(api.popLast(idx) == NULL); + TEST_ASSERT(api.popLast(oi) == NULL); for (int i = 0; i < 5; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } - TEST_ASSERT(api.length(idx) == 5); + TEST_ASSERT(api.length(oi) == 5); - OrderedIndexItem *item = api.popLast(idx); + OrderedIndexItem *item = api.popLast(oi); TEST_ASSERT(item != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(item), 4.0); const char *ptr; @@ -278,35 +278,35 @@ TEST_P(OrderedIndexTest, PopLast) { api.getElementRaw(item, &ptr, &len); TEST_ASSERT(len == 4 && memcmp(ptr, "key4", 4) == 0); api.freeItem(item); - TEST_ASSERT(api.length(idx) == 4); - VERIFY_INTEGRITY(api, idx); + TEST_ASSERT(api.length(oi) == 4); + VERIFY_INTEGRITY(api, oi); - item = api.popLast(idx); + item = api.popLast(oi); TEST_ASSERT_SCORE_EQ(api.getScore(item), 3.0); api.freeItem(item); - TEST_ASSERT(api.length(idx) == 3); - VERIFY_INTEGRITY(api, idx); + TEST_ASSERT(api.length(oi) == 3); + VERIFY_INTEGRITY(api, oi); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, UpdateScore) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); sds ele1 = sdsnew("key1"); sds ele2 = sdsnew("key2"); sds ele3 = sdsnew("key3"); - OrderedIndexItem *node1 = api.insertSds(idx, 1.0, ele1); - OrderedIndexItem *node2 = api.insertSds(idx, 2.0, ele2); - api.insertSds(idx, 3.0, ele3); + OrderedIndexItem *node1 = api.insertSds(oi, 1.0, ele1); + OrderedIndexItem *node2 = api.insertSds(oi, 2.0, ele2); + api.insertSds(oi, 3.0, ele3); sdsfree(ele1); sdsfree(ele2); sdsfree(ele3); - OrderedIndexItem *updated = api.updateScore(idx, node2, 4.0); + OrderedIndexItem *updated = api.updateScore(oi, node2, 4.0); TEST_ASSERT(updated != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(updated), 4.0); - VERIFY_INTEGRITY(api, idx); + VERIFY_INTEGRITY(api, oi); const char *ptr; size_t len; api.getElementRaw(updated, &ptr, &len); @@ -315,188 +315,188 @@ TEST_P(OrderedIndexTest, UpdateScore) { /* Verify order: key1(1.0), key3(3.0), key2(4.0) */ OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 3.0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 4 && memcmp(ptr, "key2", 4) == 0); api.resetIterator(&iter); /* Update to same score (no-op) */ - updated = api.updateScore(idx, node1, 1.0); + updated = api.updateScore(oi, node1, 1.0); TEST_ASSERT_SCORE_EQ(api.getScore(updated), 1.0); - VERIFY_INTEGRITY(api, idx); + VERIFY_INTEGRITY(api, oi); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, DeleteRangeByScore) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); for (int i = 0; i < 10; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } /* Delete range [3, 6] inclusive */ - unsigned long deleted = api.deleteRangeByScore(idx, 3.0, 6.0, 0, 0, NULL, NULL); + unsigned long deleted = api.deleteRangeByScore(oi, 3.0, 6.0, 0, 0, NULL, NULL); TEST_ASSERT(deleted == 4); /* 3, 4, 5, 6 */ - TEST_ASSERT(api.length(idx) == 6); - VERIFY_INTEGRITY(api, idx); + TEST_ASSERT(api.length(oi) == 6); + VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); for (int i = 0; i < 3; i++) { - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)i); } for (int i = 7; i < 10; i++) { - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)i); } api.resetIterator(&iter); /* Delete with exclusive bounds (2, 8) - should delete 7 */ - deleted = api.deleteRangeByScore(idx, 2.0, 8.0, 1, 1, NULL, NULL); + deleted = api.deleteRangeByScore(oi, 2.0, 8.0, 1, 1, NULL, NULL); TEST_ASSERT(deleted == 1); - TEST_ASSERT(api.length(idx) == 5); - VERIFY_INTEGRITY(api, idx); + TEST_ASSERT(api.length(oi) == 5); + VERIFY_INTEGRITY(api, oi); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, DeleteRangeByRank) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); for (int i = 0; i < 10; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } /* Delete ranks 3-5 (1-based, so elements at scores 2,3,4) */ - unsigned long deleted = api.deleteRangeByRank(idx, 3, 5, NULL, NULL); + unsigned long deleted = api.deleteRangeByRank(oi, 3, 5, NULL, NULL); TEST_ASSERT(deleted == 3); - TEST_ASSERT(api.length(idx) == 7); - VERIFY_INTEGRITY(api, idx); + TEST_ASSERT(api.length(oi) == 7); + VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 0.0); api.resetIterator(&iter); /* Verify rank 3 is now score 5 (was rank 6) */ - OrderedIndexItem *node = api.getByRank(idx, 3); + OrderedIndexItem *node = api.getByRank(oi, 3); TEST_ASSERT_SCORE_EQ(api.getScore(node), 5.0); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, MixedOperationsRankIntegrity) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); OrderedIndexItem *nodes[100]; for (int i = 0; i < 100; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - nodes[i] = api.insertSds(idx, (double)i, ele); + nodes[i] = api.insertSds(oi, (double)i, ele); sdsfree(ele); } for (int i = 2; i < 100; i += 3) { - api.deleteItem(idx, nodes[i]); + api.deleteItem(oi, nodes[i]); nodes[i] = NULL; } - VERIFY_INTEGRITY(api, idx); + VERIFY_INTEGRITY(api, oi); - if (nodes[10]) nodes[10] = api.updateScore(idx, nodes[10], 150.0); - if (nodes[20]) nodes[20] = api.updateScore(idx, nodes[20], 160.0); - VERIFY_INTEGRITY(api, idx); + if (nodes[10]) nodes[10] = api.updateScore(oi, nodes[10], 150.0); + if (nodes[20]) nodes[20] = api.updateScore(oi, nodes[20], 160.0); + VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); unsigned long expected_rank = 1; - while (api.next(&iter, &pos)) { - unsigned long actual_rank = api.getRank(idx, pos); + while (((pos = api.next(&iter)) != NULL)) { + unsigned long actual_rank = api.getRank(oi, pos); TEST_ASSERT(actual_rank == expected_rank); expected_rank++; } api.resetIterator(&iter); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, BackwardTraversalAfterDeletions) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); OrderedIndexItem *nodes[20]; for (int i = 0; i < 20; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - nodes[i] = api.insertSds(idx, (double)i, ele); + nodes[i] = api.insertSds(oi, (double)i, ele); sdsfree(ele); } - api.deleteItem(idx, nodes[5]); - api.deleteItem(idx, nodes[10]); - api.deleteItem(idx, nodes[15]); - VERIFY_INTEGRITY(api, idx); + api.deleteItem(oi, nodes[5]); + api.deleteItem(oi, nodes[10]); + api.deleteItem(oi, nodes[15]); + VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); int expected_scores[] = {19, 18, 17, 16, 14, 13, 12, 11, 9, 8, 7, 6, 4, 3, 2, 1, 0}; int idx_score = 0; - while (api.prev(&iter, &pos)) { + while (((pos = api.prev(&iter)) != NULL)) { TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)expected_scores[idx_score]); idx_score++; } TEST_ASSERT(idx_score == 17); /* Should have traversed all 17 remaining elements */ api.resetIterator(&iter); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, LexicographicEdgeCases) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); sds empty = sdsnew(""); sds a = sdsnew("a"); sds z = sdsnew("z"); - api.insertSds(idx, 1.0, z); - api.insertSds(idx, 1.0, empty); - api.insertSds(idx, 1.0, a); + api.insertSds(oi, 1.0, z); + api.insertSds(oi, 1.0, empty); + api.insertSds(oi, 1.0, a); /* Verify lexicographic order: "", "a", "z" */ OrderedIndexIterator iter; OrderedIndexItem *pos; const char *ptr; size_t len; - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 1 && memcmp(ptr, "a", 1) == 0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 1 && memcmp(ptr, "z", 1) == 0); api.resetIterator(&iter); @@ -504,35 +504,35 @@ TEST_P(OrderedIndexTest, LexicographicEdgeCases) { sdsfree(empty); sdsfree(a); sdsfree(z); - api.free(idx); + api.free(oi); /* Test very long string (1KB) */ - idx = api.create(); + oi = api.create(); char long_buf[1024]; memset(long_buf, 'x', 1023); long_buf[1023] = '\0'; sds long_str = sdsnew(long_buf); sds short_str = sdsnew("short"); - api.insertSds(idx, 1.0, long_str); - api.insertSds(idx, 1.0, short_str); + api.insertSds(oi, 1.0, long_str); + api.insertSds(oi, 1.0, short_str); - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 5 && memcmp(ptr, "short", 5) == 0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 1023 && memcmp(ptr, long_buf, 1023) == 0); api.resetIterator(&iter); sdsfree(long_str); sdsfree(short_str); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, RangeBoundaryPrecision) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); double base = 1.0; double epsilon = 1e-10; @@ -541,31 +541,31 @@ TEST_P(OrderedIndexTest, RangeBoundaryPrecision) { sds ele2 = sdsnew("at_base_plus_epsilon"); sds ele3 = sdsnew("at_base_plus_2epsilon"); - api.insertSds(idx, base, ele1); - api.insertSds(idx, base + epsilon, ele2); - api.insertSds(idx, base + 2 * epsilon, ele3); + api.insertSds(oi, base, ele1); + api.insertSds(oi, base + epsilon, ele2); + api.insertSds(oi, base + 2 * epsilon, ele3); - unsigned long deleted = api.deleteRangeByScore(idx, base, base + 2 * epsilon, 1, 1, NULL, NULL); + unsigned long deleted = api.deleteRangeByScore(oi, base, base + 2 * epsilon, 1, 1, NULL, NULL); TEST_ASSERT(deleted == 1); - TEST_ASSERT(api.length(idx) == 2); + TEST_ASSERT(api.length(oi) == 2); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), base); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), base + 2 * epsilon); api.resetIterator(&iter); sdsfree(ele1); sdsfree(ele2); sdsfree(ele3); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, SpecialDoubleValues) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); const char *ptr; size_t len; @@ -574,22 +574,22 @@ TEST_P(OrderedIndexTest, SpecialDoubleValues) { sds zero = sdsnew("zero"); sds one = sdsnew("one"); - api.insertSds(idx, NEG_INF, neg_inf); - api.insertSds(idx, POS_INF, pos_inf); - api.insertSds(idx, 0.0, zero); - api.insertSds(idx, 1.0, one); + api.insertSds(oi, NEG_INF, neg_inf); + api.insertSds(oi, POS_INF, pos_inf); + api.insertSds(oi, 0.0, zero); + api.insertSds(oi, 1.0, one); /* Verify ordering: -inf, 0, 1, +inf */ OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), NEG_INF); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 0.0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), POS_INF); api.resetIterator(&iter); @@ -597,80 +597,80 @@ TEST_P(OrderedIndexTest, SpecialDoubleValues) { sdsfree(pos_inf); sdsfree(zero); sdsfree(one); - api.free(idx); + api.free(oi); /* Test +0.0 vs -0.0 */ - idx = api.create(); + oi = api.create(); sds pos_zero = sdsnew("pos_zero"); sds neg_zero = sdsnew("neg_zero"); - api.insertSds(idx, 0.0, pos_zero); - api.insertSds(idx, -0.0, neg_zero); + api.insertSds(oi, 0.0, pos_zero); + api.insertSds(oi, -0.0, neg_zero); /* Both should be in the list, ordered lexicographically since scores are equal */ - TEST_ASSERT(api.length(idx) == 2); - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT(api.length(oi) == 2); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 8 && memcmp(ptr, "neg_zero", 8) == 0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 8 && memcmp(ptr, "pos_zero", 8) == 0); api.resetIterator(&iter); sdsfree(pos_zero); sdsfree(neg_zero); - api.free(idx); + api.free(oi); /* Test denormalized double */ - idx = api.create(); + oi = api.create(); double denorm = 1e-320; /* Denormalized double */ sds denorm_ele = sdsnew("denorm"); sds normal_ele = sdsnew("normal"); - api.insertSds(idx, denorm, denorm_ele); - api.insertSds(idx, 1.0, normal_ele); + api.insertSds(oi, denorm, denorm_ele); + api.insertSds(oi, 1.0, normal_ele); - TEST_ASSERT(api.length(idx) == 2); - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT(api.length(oi) == 2); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), denorm); TEST_ASSERT(api.getScore(pos) < 1.0); api.resetIterator(&iter); sdsfree(denorm_ele); sdsfree(normal_ele); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, EdgeCases) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); - TEST_ASSERT(api.length(idx) == 0); + TEST_ASSERT(api.length(oi) == 0); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - TEST_ASSERT(!api.next(&iter, &pos)); - TEST_ASSERT(!api.prev(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) == NULL); + TEST_ASSERT((pos = api.prev(&iter)) == NULL); api.resetIterator(&iter); - TEST_ASSERT(api.getByRank(idx, 1) == NULL); + TEST_ASSERT(api.getByRank(oi, 1) == NULL); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, DeleteEdgeCases) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); /* Delete only element */ sds ele = sdsnew("only"); - OrderedIndexItem *node = api.insertSds(idx, 1.0, ele); - api.deleteItem(idx, node); - TEST_ASSERT(api.length(idx) == 0); - VERIFY_INTEGRITY(api, idx); + OrderedIndexItem *node = api.insertSds(oi, 1.0, ele); + api.deleteItem(oi, node); + TEST_ASSERT(api.length(oi) == 0); + VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - TEST_ASSERT(!api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); sdsfree(ele); @@ -680,190 +680,190 @@ TEST_P(OrderedIndexTest, DeleteEdgeCases) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds e = sdsnew(buf); - nodes[i] = api.insertSds(idx, (double)i, e); + nodes[i] = api.insertSds(oi, (double)i, e); sdsfree(e); } - api.deleteItem(idx, nodes[0]); - TEST_ASSERT(api.length(idx) == 2); - VERIFY_INTEGRITY(api, idx); - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.deleteItem(oi, nodes[0]); + TEST_ASSERT(api.length(oi) == 2); + VERIFY_INTEGRITY(api, oi); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); api.resetIterator(&iter); /* Delete last element */ - api.deleteItem(idx, nodes[2]); - TEST_ASSERT(api.length(idx) == 1); - VERIFY_INTEGRITY(api, idx); - api.initIterator(&iter, idx); - TEST_ASSERT(api.prev(&iter, &pos)); + api.deleteItem(oi, nodes[2]); + TEST_ASSERT(api.length(oi) == 1); + VERIFY_INTEGRITY(api, oi); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.prev(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); api.resetIterator(&iter); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, RankEdgeCases) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); for (int i = 0; i < 5; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } - TEST_ASSERT(api.getByRank(idx, 6) == NULL); - TEST_ASSERT(api.getByRank(idx, 100) == NULL); - TEST_ASSERT(api.getByRank(idx, 1) != NULL); - TEST_ASSERT(api.getByRank(idx, 5) != NULL); + TEST_ASSERT(api.getByRank(oi, 6) == NULL); + TEST_ASSERT(api.getByRank(oi, 100) == NULL); + TEST_ASSERT(api.getByRank(oi, 1) != NULL); + TEST_ASSERT(api.getByRank(oi, 5) != NULL); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, DuplicateInsert) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); sds ele1 = sdsnew("duplicate"); sds ele2 = sdsnew("duplicate"); - OrderedIndexItem *node1 = api.insertSds(idx, 1.0, ele1); - OrderedIndexItem *node2 = api.insertSds(idx, 1.0, ele2); + OrderedIndexItem *node1 = api.insertSds(oi, 1.0, ele1); + OrderedIndexItem *node2 = api.insertSds(oi, 1.0, ele2); /* Should have 2 nodes (duplicates allowed) */ - TEST_ASSERT(api.length(idx) == 2); + TEST_ASSERT(api.length(oi) == 2); TEST_ASSERT(node1 != node2); sdsfree(ele1); sdsfree(ele2); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, UpdateScoreEdgeCases) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); for (int i = 0; i < 5; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } /* Update first element to move backward */ - OrderedIndexItem *first = api.getByRank(idx, 1); - OrderedIndexItem *updated = api.updateScore(idx, first, -1.0); + OrderedIndexItem *first = api.getByRank(oi, 1); + OrderedIndexItem *updated = api.updateScore(oi, first, -1.0); TEST_ASSERT_SCORE_EQ(api.getScore(updated), -1.0); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT(pos == updated); api.resetIterator(&iter); /* Update last element to move forward */ - unsigned long len = api.length(idx); - OrderedIndexItem *last = api.getByRank(idx, len); - updated = api.updateScore(idx, last, 10.0); + unsigned long len = api.length(oi); + OrderedIndexItem *last = api.getByRank(oi, len); + updated = api.updateScore(oi, last, 10.0); TEST_ASSERT_SCORE_EQ(api.getScore(updated), 10.0); - api.initIterator(&iter, idx); - TEST_ASSERT(api.prev(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.prev(&iter)) != NULL); TEST_ASSERT(pos == updated); api.resetIterator(&iter); /* Update middle element to move backward */ - OrderedIndexItem *middle = api.getByRank(idx, 3); + OrderedIndexItem *middle = api.getByRank(oi, 3); double old_score = api.getScore(middle); - updated = api.updateScore(idx, middle, 0.5); + updated = api.updateScore(oi, middle, 0.5); TEST_ASSERT_SCORE_EQ(api.getScore(updated), 0.5); TEST_ASSERT(api.getScore(updated) < old_score); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, RangeDeleteEdgeCases) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); for (int i = 0; i < 10; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } /* Delete empty range (min > max) */ - unsigned long deleted = api.deleteRangeByScore(idx, 5.0, 4.0, 0, 0, NULL, NULL); + unsigned long deleted = api.deleteRangeByScore(oi, 5.0, 4.0, 0, 0, NULL, NULL); TEST_ASSERT(deleted == 0); - TEST_ASSERT(api.length(idx) == 10); + TEST_ASSERT(api.length(oi) == 10); /* Delete range with no matches */ - deleted = api.deleteRangeByScore(idx, 10.5, 11.5, 0, 0, NULL, NULL); + deleted = api.deleteRangeByScore(oi, 10.5, 11.5, 0, 0, NULL, NULL); TEST_ASSERT(deleted == 0); - TEST_ASSERT(api.length(idx) == 10); + TEST_ASSERT(api.length(oi) == 10); /* Delete first elements by rank */ - deleted = api.deleteRangeByRank(idx, 1, 2, NULL, NULL); + deleted = api.deleteRangeByRank(oi, 1, 2, NULL, NULL); TEST_ASSERT(deleted == 2); - VERIFY_INTEGRITY(api, idx); + VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 2.0); api.resetIterator(&iter); /* Delete last elements by rank */ - unsigned long len = api.length(idx); - deleted = api.deleteRangeByRank(idx, len - 1, len, NULL, NULL); + unsigned long len = api.length(oi); + deleted = api.deleteRangeByRank(oi, len - 1, len, NULL, NULL); TEST_ASSERT(deleted == 2); - VERIFY_INTEGRITY(api, idx); - api.initIterator(&iter, idx); - TEST_ASSERT(api.prev(&iter, &pos)); + VERIFY_INTEGRITY(api, oi); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.prev(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 7.0); api.resetIterator(&iter); /* Delete entire remaining index by score */ - deleted = api.deleteRangeByScore(idx, -100.0, 100.0, 0, 0, NULL, NULL); + deleted = api.deleteRangeByScore(oi, -100.0, 100.0, 0, 0, NULL, NULL); TEST_ASSERT(deleted == 6); - TEST_ASSERT(api.length(idx) == 0); - VERIFY_INTEGRITY(api, idx); + TEST_ASSERT(api.length(oi) == 0); + VERIFY_INTEGRITY(api, oi); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, TraversalEdgeCases) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); sds ele = sdsnew("single"); - api.insertSds(idx, 1.0, ele); + api.insertSds(oi, 1.0, ele); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); - TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); - api.initIterator(&iter, idx); - TEST_ASSERT(api.prev(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.prev(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); - TEST_ASSERT(!api.prev(&iter, &pos)); + TEST_ASSERT((pos = api.prev(&iter)) == NULL); api.resetIterator(&iter); sdsfree(ele); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, SeekToRank) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); for (int i = 1; i <= 5; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } @@ -871,66 +871,66 @@ TEST_P(OrderedIndexTest, SeekToRank) { OrderedIndexItem *pos; /* Seek to rank 0 (before first) */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToRank(&iter, 0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); api.resetIterator(&iter); - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToRank(&iter, 0); - TEST_ASSERT(!api.prev(&iter, &pos)); + TEST_ASSERT((pos = api.prev(&iter)) == NULL); api.resetIterator(&iter); /* Seek to rank 1 */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToRank(&iter, 1); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 2.0); api.resetIterator(&iter); - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToRank(&iter, 1); - TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT((pos = api.prev(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); api.resetIterator(&iter); /* Seek to rank 3 (middle) */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToRank(&iter, 3); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); api.resetIterator(&iter); - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToRank(&iter, 3); - TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT((pos = api.prev(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 3.0); api.resetIterator(&iter); /* Seek to rank 5 (last) */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToRank(&iter, 5); - TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToRank(&iter, 5); - TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT((pos = api.prev(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 5.0); api.resetIterator(&iter); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, ReverseIteration) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); for (int i = 1; i <= 5; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } @@ -938,10 +938,10 @@ TEST_P(OrderedIndexTest, ReverseIteration) { OrderedIndexItem *pos; /* Full reverse traversal */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); int count = 0; double expected = 5.0; - while (api.prev(&iter, &pos)) { + while (((pos = api.prev(&iter)) != NULL)) { TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); expected -= 1.0; count++; @@ -950,33 +950,33 @@ TEST_P(OrderedIndexTest, ReverseIteration) { api.resetIterator(&iter); /* Reverse then forward */ - api.initIterator(&iter, idx); - TEST_ASSERT(api.prev(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.prev(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 5.0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 5.0); api.resetIterator(&iter); /* Forward then reverse */ - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); - TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT((pos = api.prev(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); api.resetIterator(&iter); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, SeekToScoreRange) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); /* 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); sds ele = sdsnew(buf); - api.insertSds(idx, (double)(i * 2), ele); + api.insertSds(oi, (double)(i * 2), ele); sdsfree(ele); } @@ -984,81 +984,81 @@ TEST_P(OrderedIndexTest, SeekToScoreRange) { OrderedIndexItem *pos; /* Seek to first in range [2, 6] with offset 0 */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, 0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 2.0); api.resetIterator(&iter); /* Seek to second in range [2, 6] with offset 1 */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, 1); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); api.resetIterator(&iter); /* Seek to last in range [2, 6] with offset -1, positioned for prev() */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, -1); - TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT((pos = api.prev(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 6.0); api.resetIterator(&iter); /* Seek with exclusive bounds (2, 6) - should start at 4 */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 2.0, 6.0, 1, 1, 0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); api.resetIterator(&iter); /* Seek to empty range above all elements */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 10.0, 20.0, 0, 0, 0); - TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); /* Seek to empty range below all elements */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, -20.0, -10.0, 0, 0, 0); - TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); /* Out of range positive offset */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, 10); - TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); /* Negative offset beyond range */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, -10); - TEST_ASSERT(!api.prev(&iter, &pos)); + TEST_ASSERT((pos = api.prev(&iter)) == NULL); api.resetIterator(&iter); /* Second from last with offset -2, positioned for prev() */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, -2); - TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT((pos = api.prev(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); api.resetIterator(&iter); /* Empty range where min > max */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 6.0, 2.0, 0, 0, 0); - TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, SeekToScoreRangeIteration) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); for (int i = 0; i < 10; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } @@ -1066,11 +1066,11 @@ TEST_P(OrderedIndexTest, SeekToScoreRangeIteration) { OrderedIndexItem *pos; /* Seek to range [3, 7] and iterate forward */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 3.0, 7.0, 0, 0, 0); int count = 0; double expected = 3.0; - while (api.next(&iter, &pos) && api.getScore(pos) <= 7.0) { + while (((pos = api.next(&iter)) != NULL) && api.getScore(pos) <= 7.0) { TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); expected += 1.0; count++; @@ -1079,11 +1079,11 @@ TEST_P(OrderedIndexTest, SeekToScoreRangeIteration) { api.resetIterator(&iter); /* Seek to last in range and iterate backward */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 3.0, 7.0, 0, 0, -1); count = 0; expected = 7.0; - while (api.prev(&iter, &pos) && api.getScore(pos) >= 3.0) { + while (((pos = api.prev(&iter)) != NULL) && api.getScore(pos) >= 3.0) { TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); expected -= 1.0; count++; @@ -1092,36 +1092,36 @@ TEST_P(OrderedIndexTest, SeekToScoreRangeIteration) { api.resetIterator(&iter); /* Seek with offset and continue iteration */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 2.0, 8.0, 0, 0, 2); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 5.0); api.resetIterator(&iter); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, SeekInfReverseIteration) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); for (int i = 1; i <= 5; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, NEG_INF, POS_INF, 0, 0, -1); int count = 0; double expected = 5.0; - while (api.prev(&iter, &pos)) { + while (((pos = api.prev(&iter)) != NULL)) { TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); expected -= 1.0; count++; @@ -1129,28 +1129,28 @@ TEST_P(OrderedIndexTest, SeekInfReverseIteration) { TEST_ASSERT(count == 5); api.resetIterator(&iter); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, SeekInfForwardIteration) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); for (int i = 1; i <= 5; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToScoreRange(&iter, NEG_INF, POS_INF, 0, 0, 0); int count = 0; double expected = 1.0; - while (api.next(&iter, &pos)) { + while (((pos = api.next(&iter)) != NULL)) { TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); expected += 1.0; count++; @@ -1158,16 +1158,16 @@ TEST_P(OrderedIndexTest, SeekInfForwardIteration) { TEST_ASSERT(count == 5); api.resetIterator(&iter); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, SeekToLexRange) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; for (int i = 0; i < 5; i++) { sds ele = sdsnew(elements[i]); - api.insertSds(idx, 1.0, ele); + api.insertSds(oi, 1.0, ele); sdsfree(ele); } @@ -1180,34 +1180,34 @@ TEST_P(OrderedIndexTest, SeekToLexRange) { sds maxLex = sdsnew("date"); /* Seek to first in lex range [banana, date] with offset 0 */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToLexRange(&iter, minLex, maxLex, 0, 0, 0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 6 && memcmp(ptr, "banana", 6) == 0); api.resetIterator(&iter); /* Seek to second in lex range with offset 1 */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToLexRange(&iter, minLex, maxLex, 0, 0, 1); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 6 && memcmp(ptr, "cherry", 6) == 0); api.resetIterator(&iter); /* Seek to last in lex range with offset -1 */ /* Seek to last in lex range with offset -1, positioned for prev() */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToLexRange(&iter, minLex, maxLex, 0, 0, -1); - TEST_ASSERT(api.prev(&iter, &pos)); + TEST_ASSERT((pos = api.prev(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 4 && memcmp(ptr, "date", 4) == 0); api.resetIterator(&iter); /* Seek with exclusive bounds (banana, date) - should start at cherry */ - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToLexRange(&iter, minLex, maxLex, 1, 1, 0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 6 && memcmp(ptr, "cherry", 6) == 0); api.resetIterator(&iter); @@ -1218,9 +1218,9 @@ TEST_P(OrderedIndexTest, SeekToLexRange) { /* Seek to empty lex range */ sds minEmpty = sdsnew("zzz"); sds maxEmpty = sdsnew("zzzz"); - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToLexRange(&iter, minEmpty, maxEmpty, 0, 0, 0); - TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); sdsfree(minEmpty); sdsfree(maxEmpty); @@ -1228,177 +1228,177 @@ TEST_P(OrderedIndexTest, SeekToLexRange) { /* Out of range positive offset */ minLex = sdsnew("banana"); maxLex = sdsnew("date"); - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); api.seekToLexRange(&iter, minLex, maxLex, 0, 0, 10); - TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); sdsfree(minLex); sdsfree(maxLex); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, DeleteRangeByLexInclusive) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; for (int i = 0; i < 5; i++) { sds ele = sdsnew(elements[i]); - api.insertSds(idx, 1.0, ele); + api.insertSds(oi, 1.0, ele); sdsfree(ele); } sds min = sdsnew("banana"); sds max = sdsnew("date"); - unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, NULL, NULL); + unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); TEST_ASSERT(deleted == 3); - TEST_ASSERT(api.length(idx) == 2); - VERIFY_INTEGRITY(api, idx); + TEST_ASSERT(api.length(oi) == 2); + VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; const char *ptr; size_t len; - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 5 && memcmp(ptr, "apple", 5) == 0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 10 && memcmp(ptr, "elderberry", 10) == 0); - TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, DeleteRangeByLexExclusive) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; for (int i = 0; i < 5; i++) { sds ele = sdsnew(elements[i]); - api.insertSds(idx, 1.0, ele); + api.insertSds(oi, 1.0, ele); sdsfree(ele); } sds min = sdsnew("banana"); sds max = sdsnew("date"); - unsigned long deleted = api.deleteRangeByLex(idx, min, max, 1, 1, NULL, NULL); + unsigned long deleted = api.deleteRangeByLex(oi, min, max, 1, 1, NULL, NULL); TEST_ASSERT(deleted == 1); - TEST_ASSERT(api.length(idx) == 4); + TEST_ASSERT(api.length(oi) == 4); OrderedIndexIterator iter; OrderedIndexItem *pos; const char *ptr; size_t len; - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 5 && memcmp(ptr, "apple", 5) == 0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 6 && memcmp(ptr, "banana", 6) == 0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 4 && memcmp(ptr, "date", 4) == 0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 10 && memcmp(ptr, "elderberry", 10) == 0); - TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, DeleteRangeByLexBoundaryCases) { /* Empty range: min > max lexicographically */ - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); const char *elements[] = {"apple", "banana", "cherry"}; for (int i = 0; i < 3; i++) { sds ele = sdsnew(elements[i]); - api.insertSds(idx, 1.0, ele); + api.insertSds(oi, 1.0, ele); sdsfree(ele); } sds min = sdsnew("zzz"); sds max = sdsnew("aaa"); - unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, NULL, NULL); + unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); TEST_ASSERT(deleted == 0); - TEST_ASSERT(api.length(idx) == 3); + TEST_ASSERT(api.length(oi) == 3); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); /* Delete all elements */ - idx = api.create(); + oi = api.create(); for (int i = 0; i < 3; i++) { sds ele = sdsnew(elements[i]); - api.insertSds(idx, 1.0, ele); + api.insertSds(oi, 1.0, ele); sdsfree(ele); } min = sdsnew("a"); max = sdsnew("z"); - deleted = api.deleteRangeByLex(idx, min, max, 0, 0, NULL, NULL); + deleted = api.deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); TEST_ASSERT(deleted == 3); - TEST_ASSERT(api.length(idx) == 0); + TEST_ASSERT(api.length(oi) == 0); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); /* Delete single element */ - idx = api.create(); + oi = api.create(); for (int i = 0; i < 3; i++) { sds ele = sdsnew(elements[i]); - api.insertSds(idx, 1.0, ele); + api.insertSds(oi, 1.0, ele); sdsfree(ele); } min = sdsnew("banana"); max = sdsnew("banana"); - deleted = api.deleteRangeByLex(idx, min, max, 0, 0, NULL, NULL); + deleted = api.deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); TEST_ASSERT(deleted == 1); - TEST_ASSERT(api.length(idx) == 2); + TEST_ASSERT(api.length(oi) == 2); OrderedIndexIterator iter; OrderedIndexItem *pos; const char *ptr; size_t len; - api.initIterator(&iter, idx); - TEST_ASSERT(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 5 && memcmp(ptr, "apple", 5) == 0); - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == 6 && memcmp(ptr, "cherry", 6) == 0); - TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, DeleteRangeByLexPreservesOutside) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); const char *elements[] = {"alpha", "bravo", "charlie", "delta", "echo", "foxtrot"}; for (int i = 0; i < 6; i++) { sds ele = sdsnew(elements[i]); - api.insertSds(idx, 1.0, ele); + api.insertSds(oi, 1.0, ele); sdsfree(ele); } sds min = sdsnew("charlie"); sds max = sdsnew("delta"); - unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, NULL, NULL); + unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); TEST_ASSERT(deleted == 2); - TEST_ASSERT(api.length(idx) == 4); + TEST_ASSERT(api.length(oi) == 4); const char *expected[] = {"alpha", "bravo", "echo", "foxtrot"}; size_t expected_lens[] = {5, 5, 4, 7}; @@ -1406,33 +1406,33 @@ TEST_P(OrderedIndexTest, DeleteRangeByLexPreservesOutside) { OrderedIndexItem *pos; const char *ptr; size_t len; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); for (int i = 0; i < 4; i++) { - TEST_ASSERT(api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) != NULL); api.getElementRaw(pos, &ptr, &len); TEST_ASSERT(len == expected_lens[i] && memcmp(ptr, expected[i], len) == 0); } - TEST_ASSERT(!api.next(&iter, &pos)); + TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); /* Verify scores are preserved */ - api.initIterator(&iter, idx); - while (api.next(&iter, &pos)) { + api.initIterator(&iter, oi); + while (((pos = api.next(&iter)) != NULL)) { TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); } api.resetIterator(&iter); /* Verify ranks are correct after deletion */ for (unsigned long r = 1; r <= 4; r++) { - OrderedIndexItem *node = api.getByRank(idx, r); + OrderedIndexItem *node = api.getByRank(oi, r); TEST_ASSERT(node != NULL); - unsigned long rank = api.getRank(idx, node); + unsigned long rank = api.getRank(oi, node); TEST_ASSERT(rank == r); } sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } /* ========== Randomized property tests ========== */ @@ -1457,13 +1457,13 @@ static double test_random_score(std::mt19937 &rng) { return dist(rng); } -static std::vector test_build_random_index(OrderedIndexTestApi &api, OrderedIndex *idx, std::mt19937 &rng, int count) { +static std::vector test_build_random_index(OrderedIndexTestApi &api, OrderedIndex *oi, std::mt19937 &rng, int count) { std::vector entries; for (int i = 0; i < count; i++) { double score = test_random_score(rng); std::string elem = test_random_element(rng) + std::to_string(i); sds ele = sdsnew(elem.c_str()); - OrderedIndexItem *node = api.insertSds(idx, score, ele); + OrderedIndexItem *node = api.insertSds(oi, score, ele); entries.push_back({node, score, elem}); sdsfree(ele); } @@ -1476,18 +1476,18 @@ TEST_P(OrderedIndexTest, RandomizedInsertAndTraversal) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - OrderedIndex *idx = api.create(); - test_build_random_index(api, idx, rng, n); + OrderedIndex *oi = api.create(); + test_build_random_index(api, oi, rng, n); - ASSERT_EQ(api.length(idx), (unsigned long)n); - VERIFY_INTEGRITY(api, idx); + ASSERT_EQ(api.length(oi), (unsigned long)n); + VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); int count = 0; double prevScore = -INFINITY; - while (api.next(&iter, &pos)) { + while (((pos = api.next(&iter)) != NULL)) { double s = api.getScore(pos); ASSERT_GE(s, prevScore); prevScore = s; @@ -1495,7 +1495,7 @@ TEST_P(OrderedIndexTest, RandomizedInsertAndTraversal) { } ASSERT_EQ(count, n); api.resetIterator(&iter); - api.free(idx); + api.free(oi); } } @@ -1505,15 +1505,15 @@ TEST_P(OrderedIndexTest, RandomizedBackwardTraversal) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - OrderedIndex *idx = api.create(); - test_build_random_index(api, idx, rng, n); + OrderedIndex *oi = api.create(); + test_build_random_index(api, oi, rng, n); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); int count = 0; double prevScore = INFINITY; - while (api.prev(&iter, &pos)) { + while (((pos = api.prev(&iter)) != NULL)) { double s = api.getScore(pos); ASSERT_LE(s, prevScore); prevScore = s; @@ -1521,7 +1521,7 @@ TEST_P(OrderedIndexTest, RandomizedBackwardTraversal) { } ASSERT_EQ(count, n); api.resetIterator(&iter); - api.free(idx); + api.free(oi); } } @@ -1531,13 +1531,13 @@ TEST_P(OrderedIndexTest, RandomizedScoreRetrieval) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - OrderedIndex *idx = api.create(); - auto entries = test_build_random_index(api, idx, rng, n); + OrderedIndex *oi = api.create(); + auto entries = test_build_random_index(api, oi, rng, n); for (auto &e : entries) { ASSERT_EQ(api.getScore(e.node), e.score); } - api.free(idx); + api.free(oi); } } @@ -1547,23 +1547,23 @@ TEST_P(OrderedIndexTest, RandomizedRankConsistency) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - OrderedIndex *idx = api.create(); - test_build_random_index(api, idx, rng, n); + OrderedIndex *oi = api.create(); + test_build_random_index(api, oi, rng, n); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); unsigned long expectedRank = 1; - while (api.next(&iter, &pos)) { - unsigned long rank = api.getRank(idx, pos); + while (((pos = api.next(&iter)) != NULL)) { + unsigned long rank = api.getRank(oi, pos); ASSERT_EQ(rank, expectedRank); - OrderedIndexItem *byRank = api.getByRank(idx, expectedRank); + OrderedIndexItem *byRank = api.getByRank(oi, expectedRank); ASSERT_EQ(byRank, pos); expectedRank++; } ASSERT_EQ(expectedRank - 1, (unsigned long)n); api.resetIterator(&iter); - api.free(idx); + api.free(oi); } } @@ -1573,29 +1573,29 @@ TEST_P(OrderedIndexTest, RandomizedDelete) { std::uniform_int_distribution sizeDist(2, 30); int n = sizeDist(rng); - OrderedIndex *idx = api.create(); - auto entries = test_build_random_index(api, idx, rng, n); + OrderedIndex *oi = api.create(); + auto entries = test_build_random_index(api, oi, rng, n); std::uniform_int_distribution pickDist(0, n - 1); int delIdx = pickDist(rng); - api.deleteItem(idx, entries[delIdx].node); + api.deleteItem(oi, entries[delIdx].node); - ASSERT_EQ(api.length(idx), (unsigned long)(n - 1)); - VERIFY_INTEGRITY(api, idx); + ASSERT_EQ(api.length(oi), (unsigned long)(n - 1)); + VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); int count = 0; double prevScore = -INFINITY; - while (api.next(&iter, &pos)) { + 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); - api.free(idx); + api.free(oi); } } @@ -1605,29 +1605,29 @@ TEST_P(OrderedIndexTest, RandomizedUpdateScore) { std::uniform_int_distribution sizeDist(2, 30); int n = sizeDist(rng); - OrderedIndex *idx = api.create(); - auto entries = test_build_random_index(api, idx, rng, n); + OrderedIndex *oi = api.create(); + auto entries = test_build_random_index(api, oi, rng, n); std::uniform_int_distribution pickDist(0, n - 1); int updIdx = pickDist(rng); double newScore = test_random_score(rng); - OrderedIndexItem *updated = api.updateScore(idx, entries[updIdx].node, newScore); + OrderedIndexItem *updated = api.updateScore(oi, entries[updIdx].node, newScore); ASSERT_NE(updated, nullptr); ASSERT_EQ(api.getScore(updated), newScore); - ASSERT_EQ(api.length(idx), (unsigned long)n); - VERIFY_INTEGRITY(api, idx); + ASSERT_EQ(api.length(oi), (unsigned long)n); + VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); double prevScore = -INFINITY; - while (api.next(&iter, &pos)) { + while (((pos = api.next(&iter)) != NULL)) { ASSERT_GE(api.getScore(pos), prevScore); prevScore = api.getScore(pos); } api.resetIterator(&iter); - api.free(idx); + api.free(oi); } } @@ -1637,43 +1637,43 @@ TEST_P(OrderedIndexTest, RandomizedPop) { std::uniform_int_distribution sizeDist(3, 30); int n = sizeDist(rng); - OrderedIndex *idx = api.create(); - test_build_random_index(api, idx, rng, n); + OrderedIndex *oi = api.create(); + test_build_random_index(api, oi, rng, n); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - ASSERT_TRUE(api.next(&iter, &pos)); + api.initIterator(&iter, oi); + ASSERT_TRUE(((pos = api.next(&iter)) != NULL)); double minScore = api.getScore(pos); api.resetIterator(&iter); - api.initIterator(&iter, idx); - ASSERT_TRUE(api.prev(&iter, &pos)); + api.initIterator(&iter, oi); + ASSERT_TRUE(((pos = api.prev(&iter)) != NULL)); double maxScore = api.getScore(pos); api.resetIterator(&iter); - OrderedIndexItem *first = api.popFirst(idx); + OrderedIndexItem *first = api.popFirst(oi); ASSERT_NE(first, nullptr); ASSERT_EQ(api.getScore(first), minScore); - ASSERT_EQ(api.length(idx), (unsigned long)(n - 1)); + ASSERT_EQ(api.length(oi), (unsigned long)(n - 1)); api.freeItem(first); - VERIFY_INTEGRITY(api, idx); + VERIFY_INTEGRITY(api, oi); - OrderedIndexItem *last = api.popLast(idx); + OrderedIndexItem *last = api.popLast(oi); ASSERT_NE(last, nullptr); ASSERT_EQ(api.getScore(last), maxScore); - ASSERT_EQ(api.length(idx), (unsigned long)(n - 2)); + ASSERT_EQ(api.length(oi), (unsigned long)(n - 2)); api.freeItem(last); - VERIFY_INTEGRITY(api, idx); + VERIFY_INTEGRITY(api, oi); - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); double prevScore = -INFINITY; - while (api.next(&iter, &pos)) { + while (((pos = api.next(&iter)) != NULL)) { ASSERT_GE(api.getScore(pos), prevScore); prevScore = api.getScore(pos); } api.resetIterator(&iter); - api.free(idx); + api.free(oi); } } @@ -1683,8 +1683,8 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { std::uniform_int_distribution sizeDist(5, 40); int n = sizeDist(rng); - OrderedIndex *idx = api.create(); - auto entries = test_build_random_index(api, idx, rng, n); + OrderedIndex *oi = api.create(); + auto entries = test_build_random_index(api, oi, rng, n); double s1 = test_random_score(rng), s2 = test_random_score(rng); double lo = (std::min)(s1, s2), hi = (std::max)(s1, s2); @@ -1694,23 +1694,23 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { if (e.score >= lo && e.score <= hi) expectedDeleted++; } - unsigned long deleted = api.deleteRangeByScore(idx, lo, hi, 0, 0, NULL, NULL); + unsigned long deleted = api.deleteRangeByScore(oi, lo, hi, 0, 0, NULL, NULL); ASSERT_EQ(deleted, (unsigned long)expectedDeleted); - ASSERT_EQ(api.length(idx), (unsigned long)(n - expectedDeleted)); - VERIFY_INTEGRITY(api, idx); + ASSERT_EQ(api.length(oi), (unsigned long)(n - expectedDeleted)); + VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); double prevScore = -INFINITY; - while (api.next(&iter, &pos)) { + 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(idx); + api.free(oi); } } @@ -1720,8 +1720,8 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByRank) { std::uniform_int_distribution sizeDist(5, 40); int n = sizeDist(rng); - OrderedIndex *idx = api.create(); - test_build_random_index(api, idx, rng, n); + OrderedIndex *oi = api.create(); + test_build_random_index(api, oi, rng, n); std::uniform_int_distribution rankDist(1, n); int r1 = rankDist(rng), r2 = rankDist(rng); @@ -1729,24 +1729,24 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByRank) { unsigned long end = (unsigned long)(std::max)(r1, r2); unsigned long expectedDeleted = end - start + 1; - unsigned long deleted = api.deleteRangeByRank(idx, start, end, NULL, NULL); + unsigned long deleted = api.deleteRangeByRank(oi, start, end, NULL, NULL); ASSERT_EQ(deleted, expectedDeleted); - ASSERT_EQ(api.length(idx), (unsigned long)(n)-expectedDeleted); - VERIFY_INTEGRITY(api, idx); + ASSERT_EQ(api.length(oi), (unsigned long)(n)-expectedDeleted); + VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); + api.initIterator(&iter, oi); int remaining = 0; double prevScore = -INFINITY; - while (api.next(&iter, &pos)) { + 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(idx); + api.free(oi); } } @@ -1756,21 +1756,21 @@ TEST_P(OrderedIndexTest, RandomizedForwardBackwardMirror) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - OrderedIndex *idx = api.create(); - test_build_random_index(api, idx, rng, n); + OrderedIndex *oi = api.create(); + test_build_random_index(api, oi, rng, n); std::vector forwardScores; OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - while (api.next(&iter, &pos)) { + api.initIterator(&iter, oi); + while (((pos = api.next(&iter)) != NULL)) { forwardScores.push_back(api.getScore(pos)); } api.resetIterator(&iter); std::vector backwardScores; - api.initIterator(&iter, idx); - while (api.prev(&iter, &pos)) { + api.initIterator(&iter, oi); + while (((pos = api.prev(&iter)) != NULL)) { backwardScores.push_back(api.getScore(pos)); } api.resetIterator(&iter); @@ -1780,118 +1780,118 @@ TEST_P(OrderedIndexTest, RandomizedForwardBackwardMirror) { for (size_t i = 0; i < forwardScores.size(); i++) { ASSERT_EQ(forwardScores[i], backwardScores[i]); } - api.free(idx); + api.free(oi); } } /* ========== Count range tests ========== */ TEST_P(OrderedIndexTest, CountScoreRange) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); for (int i = 0; i < 10; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); sds ele = sdsnew(buf); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } /* Full range */ - ASSERT_EQ(api.countScoreRange(idx, NEG_INF, POS_INF, 0, 0), 10UL); + ASSERT_EQ(api.countScoreRange(oi, NEG_INF, POS_INF, 0, 0), 10UL); /* Inclusive [3, 6] */ - ASSERT_EQ(api.countScoreRange(idx, 3.0, 6.0, 0, 0), 4UL); + ASSERT_EQ(api.countScoreRange(oi, 3.0, 6.0, 0, 0), 4UL); /* Exclusive (3, 6) */ - ASSERT_EQ(api.countScoreRange(idx, 3.0, 6.0, 1, 1), 2UL); + ASSERT_EQ(api.countScoreRange(oi, 3.0, 6.0, 1, 1), 2UL); /* Single element [5, 5] */ - ASSERT_EQ(api.countScoreRange(idx, 5.0, 5.0, 0, 0), 1UL); + ASSERT_EQ(api.countScoreRange(oi, 5.0, 5.0, 0, 0), 1UL); /* Empty exclusive (5, 5) */ - ASSERT_EQ(api.countScoreRange(idx, 5.0, 5.0, 1, 0), 0UL); + ASSERT_EQ(api.countScoreRange(oi, 5.0, 5.0, 1, 0), 0UL); /* No match above */ - ASSERT_EQ(api.countScoreRange(idx, 10.0, 20.0, 0, 0), 0UL); + ASSERT_EQ(api.countScoreRange(oi, 10.0, 20.0, 0, 0), 0UL); /* No match below */ - ASSERT_EQ(api.countScoreRange(idx, -20.0, -10.0, 0, 0), 0UL); + ASSERT_EQ(api.countScoreRange(oi, -20.0, -10.0, 0, 0), 0UL); /* Min > max */ - ASSERT_EQ(api.countScoreRange(idx, 6.0, 3.0, 0, 0), 0UL); + ASSERT_EQ(api.countScoreRange(oi, 6.0, 3.0, 0, 0), 0UL); /* First element only [0, 0] */ - ASSERT_EQ(api.countScoreRange(idx, 0.0, 0.0, 0, 0), 1UL); + ASSERT_EQ(api.countScoreRange(oi, 0.0, 0.0, 0, 0), 1UL); /* Last element only [9, 9] */ - ASSERT_EQ(api.countScoreRange(idx, 9.0, 9.0, 0, 0), 1UL); + ASSERT_EQ(api.countScoreRange(oi, 9.0, 9.0, 0, 0), 1UL); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, CountScoreRangeEmpty) { - OrderedIndex *idx = api.create(); - ASSERT_EQ(api.countScoreRange(idx, NEG_INF, POS_INF, 0, 0), 0UL); - api.free(idx); + OrderedIndex *oi = api.create(); + ASSERT_EQ(api.countScoreRange(oi, NEG_INF, POS_INF, 0, 0), 0UL); + api.free(oi); } TEST_P(OrderedIndexTest, CountLexRange) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; for (int i = 0; i < 5; i++) { sds ele = sdsnew(elements[i]); - api.insertSds(idx, 1.0, ele); + api.insertSds(oi, 1.0, ele); sdsfree(ele); } /* Inclusive [banana, date] */ sds min = sdsnew("banana"); sds max = sdsnew("date"); - ASSERT_EQ(api.countLexRange(idx, min, max, 0, 0), 3UL); + ASSERT_EQ(api.countLexRange(oi, min, max, 0, 0), 3UL); sdsfree(min); sdsfree(max); /* Exclusive (banana, date) */ min = sdsnew("banana"); max = sdsnew("date"); - ASSERT_EQ(api.countLexRange(idx, min, max, 1, 1), 1UL); + ASSERT_EQ(api.countLexRange(oi, min, max, 1, 1), 1UL); sdsfree(min); sdsfree(max); /* Single element [cherry, cherry] */ min = sdsnew("cherry"); max = sdsnew("cherry"); - ASSERT_EQ(api.countLexRange(idx, min, max, 0, 0), 1UL); + ASSERT_EQ(api.countLexRange(oi, min, max, 0, 0), 1UL); sdsfree(min); sdsfree(max); /* No match */ min = sdsnew("fig"); max = sdsnew("grape"); - ASSERT_EQ(api.countLexRange(idx, min, max, 0, 0), 0UL); + ASSERT_EQ(api.countLexRange(oi, min, max, 0, 0), 0UL); sdsfree(min); sdsfree(max); /* All elements */ min = sdsnew("a"); max = sdsnew("z"); - ASSERT_EQ(api.countLexRange(idx, min, max, 0, 0), 5UL); + ASSERT_EQ(api.countLexRange(oi, min, max, 0, 0), 5UL); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } TEST_P(OrderedIndexTest, CountLexRangeEmpty) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); sds min = sdsnew("a"); sds max = sdsnew("z"); - ASSERT_EQ(api.countLexRange(idx, min, max, 0, 0), 0UL); + ASSERT_EQ(api.countLexRange(oi, min, max, 0, 0), 0UL); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } /* ========== Instantiate parameterized tests for all implementations ========== */ @@ -1921,29 +1921,29 @@ class OnDeleteCallbackTest : public ::testing::Test { protected: SkiplistOrderedIndex api; - void insertN(OrderedIndex *idx, int n) { + void insertN(OrderedIndex *oi, int n) { for (int i = 0; i < n; i++) { std::string name = "key" + std::to_string(i); sds ele = sdsnew(name.c_str()); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); sdsfree(ele); } } - void insertLex(OrderedIndex *idx, const std::vector &elems, double score = 1.0) { + void insertLex(OrderedIndex *oi, const std::vector &elems, double score = 1.0) { for (auto &e : elems) { sds ele = sdsnew(e.c_str()); - api.insertSds(idx, score, ele); + api.insertSds(oi, score, ele); sdsfree(ele); } } - std::vector collectElements(OrderedIndex *idx) { + std::vector collectElements(OrderedIndex *oi) { std::vector result; OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - while (api.next(&iter, &pos)) { + api.initIterator(&iter, oi); + while (((pos = api.next(&iter)) != NULL)) { const char *ptr; size_t len; api.getElementRaw(pos, &ptr, &len); @@ -1959,89 +1959,89 @@ class OnDeleteCallbackTest : public ::testing::Test { TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_EmptyAndNoMatch) { OnDeleteRecord rec = {0, {}}; - OrderedIndex *idx = api.create(); - unsigned long deleted = api.deleteRangeByScore(idx, 0.0, 10.0, 0, 0, testOnDeleteCallback, &rec); + OrderedIndex *oi = api.create(); + 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(idx); + api.free(oi); - idx = api.create(); - insertN(idx, 5); + oi = api.create(); + insertN(oi, 5); rec = {0, {}}; - deleted = api.deleteRangeByScore(idx, 10.0, 20.0, 0, 0, testOnDeleteCallback, &rec); + 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(idx), 5UL); - api.free(idx); + ASSERT_EQ(api.length(oi), 5UL); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_Subset) { - OrderedIndex *idx = api.create(); - insertN(idx, 10); + OrderedIndex *oi = api.create(); + insertN(oi, 10); OnDeleteRecord rec = {0, {}}; - unsigned long deleted = api.deleteRangeByScore(idx, 3.0, 6.0, 0, 0, testOnDeleteCallback, &rec); + 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(idx), 6UL); - VERIFY_INTEGRITY(api, idx); + ASSERT_EQ(api.length(oi), 6UL); + VERIFY_INTEGRITY(api, oi); std::sort(rec.elements.begin(), rec.elements.end()); ASSERT_EQ(rec.elements, (std::vector{"key3", "key4", "key5", "key6"})); - auto remaining = collectElements(idx); + auto remaining = collectElements(oi); ASSERT_EQ(remaining, (std::vector{"key0", "key1", "key2", "key7", "key8", "key9"})); - api.free(idx); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_All) { - OrderedIndex *idx = api.create(); - insertN(idx, 5); + OrderedIndex *oi = api.create(); + insertN(oi, 5); OnDeleteRecord rec = {0, {}}; - unsigned long deleted = api.deleteRangeByScore(idx, NEG_INF, POS_INF, 0, 0, testOnDeleteCallback, &rec); + 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(idx), 0UL); - VERIFY_INTEGRITY(api, idx); - api.free(idx); + ASSERT_EQ(api.length(oi), 0UL); + VERIFY_INTEGRITY(api, oi); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_NullCallback) { - OrderedIndex *idx = api.create(); - insertN(idx, 5); + OrderedIndex *oi = api.create(); + insertN(oi, 5); - unsigned long deleted = api.deleteRangeByScore(idx, 1.0, 3.0, 0, 0, NULL, NULL); + unsigned long deleted = api.deleteRangeByScore(oi, 1.0, 3.0, 0, 0, NULL, NULL); ASSERT_EQ(deleted, 3UL); - ASSERT_EQ(api.length(idx), 2UL); - api.free(idx); + ASSERT_EQ(api.length(oi), 2UL); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_ExclusiveBounds) { - OrderedIndex *idx = api.create(); - insertN(idx, 10); + OrderedIndex *oi = api.create(); + insertN(oi, 10); OnDeleteRecord rec = {0, {}}; - unsigned long deleted = api.deleteRangeByScore(idx, 3.0, 7.0, 1, 1, testOnDeleteCallback, &rec); + unsigned long deleted = api.deleteRangeByScore(oi, 3.0, 7.0, 1, 1, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 3UL); ASSERT_EQ(rec.count, 3); std::sort(rec.elements.begin(), rec.elements.end()); ASSERT_EQ(rec.elements, (std::vector{"key4", "key5", "key6"})); - ASSERT_EQ(api.length(idx), 7UL); - api.free(idx); + ASSERT_EQ(api.length(oi), 7UL); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_SingleElement) { - OrderedIndex *idx = api.create(); - insertN(idx, 5); + OrderedIndex *oi = api.create(); + insertN(oi, 5); OnDeleteRecord rec = {0, {}}; - unsigned long deleted = api.deleteRangeByScore(idx, 2.0, 2.0, 0, 0, testOnDeleteCallback, &rec); + unsigned long deleted = api.deleteRangeByScore(oi, 2.0, 2.0, 0, 0, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 1UL); ASSERT_EQ(rec.count, 1); ASSERT_EQ(rec.elements[0], "key2"); - ASSERT_EQ(api.length(idx), 4UL); - api.free(idx); + ASSERT_EQ(api.length(oi), 4UL); + api.free(oi); } /* DeleteRangeByRank */ @@ -2049,88 +2049,88 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_SingleElement) { TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_EmptyAndNoMatch) { OnDeleteRecord rec = {0, {}}; - OrderedIndex *idx = api.create(); - unsigned long deleted = api.deleteRangeByRank(idx, 1, 5, testOnDeleteCallback, &rec); + OrderedIndex *oi = api.create(); + unsigned long deleted = api.deleteRangeByRank(oi, 1, 5, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 0UL); ASSERT_EQ(rec.count, 0); - api.free(idx); + api.free(oi); - idx = api.create(); - insertN(idx, 3); + oi = api.create(); + insertN(oi, 3); rec = {0, {}}; - deleted = api.deleteRangeByRank(idx, 10, 20, testOnDeleteCallback, &rec); + deleted = api.deleteRangeByRank(oi, 10, 20, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 0UL); ASSERT_EQ(rec.count, 0); - ASSERT_EQ(api.length(idx), 3UL); - api.free(idx); + ASSERT_EQ(api.length(oi), 3UL); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_Subset) { - OrderedIndex *idx = api.create(); - insertN(idx, 10); + OrderedIndex *oi = api.create(); + insertN(oi, 10); OnDeleteRecord rec = {0, {}}; - unsigned long deleted = api.deleteRangeByRank(idx, 3, 5, testOnDeleteCallback, &rec); + unsigned long deleted = api.deleteRangeByRank(oi, 3, 5, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 3UL); ASSERT_EQ(rec.count, 3); - ASSERT_EQ(api.length(idx), 7UL); + ASSERT_EQ(api.length(oi), 7UL); std::sort(rec.elements.begin(), rec.elements.end()); ASSERT_EQ(rec.elements, (std::vector{"key2", "key3", "key4"})); - auto remaining = collectElements(idx); + auto remaining = collectElements(oi); ASSERT_EQ(remaining, (std::vector{"key0", "key1", "key5", "key6", "key7", "key8", "key9"})); - api.free(idx); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_All) { - OrderedIndex *idx = api.create(); - insertN(idx, 5); + OrderedIndex *oi = api.create(); + insertN(oi, 5); OnDeleteRecord rec = {0, {}}; - unsigned long deleted = api.deleteRangeByRank(idx, 1, 5, testOnDeleteCallback, &rec); + unsigned long deleted = api.deleteRangeByRank(oi, 1, 5, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 5UL); ASSERT_EQ(rec.count, 5); - ASSERT_EQ(api.length(idx), 0UL); - api.free(idx); + ASSERT_EQ(api.length(oi), 0UL); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_NullCallback) { - OrderedIndex *idx = api.create(); - insertN(idx, 5); + OrderedIndex *oi = api.create(); + insertN(oi, 5); - unsigned long deleted = api.deleteRangeByRank(idx, 2, 4, NULL, NULL); + unsigned long deleted = api.deleteRangeByRank(oi, 2, 4, NULL, NULL); ASSERT_EQ(deleted, 3UL); - ASSERT_EQ(api.length(idx), 2UL); - api.free(idx); + ASSERT_EQ(api.length(oi), 2UL); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_ExclusiveBounds) { - OrderedIndex *idx = api.create(); - insertN(idx, 5); + OrderedIndex *oi = api.create(); + insertN(oi, 5); OnDeleteRecord rec = {0, {}}; - unsigned long deleted = api.deleteRangeByRank(idx, 3, 3, testOnDeleteCallback, &rec); + unsigned long deleted = api.deleteRangeByRank(oi, 3, 3, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 1UL); ASSERT_EQ(rec.count, 1); ASSERT_EQ(rec.elements[0], "key2"); - auto remaining = collectElements(idx); + auto remaining = collectElements(oi); ASSERT_EQ(remaining, (std::vector{"key0", "key1", "key3", "key4"})); - api.free(idx); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_SingleElement) { - OrderedIndex *idx = api.create(); - insertN(idx, 5); + OrderedIndex *oi = api.create(); + insertN(oi, 5); OnDeleteRecord rec = {0, {}}; - unsigned long deleted = api.deleteRangeByRank(idx, 1, 1, testOnDeleteCallback, &rec); + unsigned long deleted = api.deleteRangeByRank(oi, 1, 1, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 1UL); ASSERT_EQ(rec.count, 1); ASSERT_EQ(rec.elements[0], "key0"); - ASSERT_EQ(api.length(idx), 4UL); - api.free(idx); + ASSERT_EQ(api.length(oi), 4UL); + api.free(oi); } /* DeleteRangeByLex */ @@ -2138,128 +2138,128 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_SingleElement) { TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { OnDeleteRecord rec = {0, {}}; - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); sds min = sdsnew("a"); sds max = sdsnew("z"); - unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, testOnDeleteCallback, &rec); + 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(idx); + api.free(oi); - idx = api.create(); - insertLex(idx, {"apple", "banana", "cherry"}); + oi = api.create(); + insertLex(oi, {"apple", "banana", "cherry"}); rec = {0, {}}; min = sdsnew("x"); max = sdsnew("z"); - deleted = api.deleteRangeByLex(idx, min, max, 0, 0, testOnDeleteCallback, &rec); + deleted = api.deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 0UL); ASSERT_EQ(rec.count, 0); - ASSERT_EQ(api.length(idx), 3UL); + ASSERT_EQ(api.length(oi), 3UL); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_Subset) { - OrderedIndex *idx = api.create(); - insertLex(idx, {"apple", "banana", "cherry", "date", "elderberry"}); + OrderedIndex *oi = api.create(); + insertLex(oi, {"apple", "banana", "cherry", "date", "elderberry"}); OnDeleteRecord rec = {0, {}}; sds min = sdsnew("banana"); sds max = sdsnew("date"); - unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, testOnDeleteCallback, &rec); + 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(idx), 2UL); + ASSERT_EQ(api.length(oi), 2UL); std::sort(rec.elements.begin(), rec.elements.end()); ASSERT_EQ(rec.elements, (std::vector{"banana", "cherry", "date"})); - auto remaining = collectElements(idx); + auto remaining = collectElements(oi); ASSERT_EQ(remaining, (std::vector{"apple", "elderberry"})); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_All) { - OrderedIndex *idx = api.create(); - insertLex(idx, {"apple", "banana", "cherry"}); + OrderedIndex *oi = api.create(); + insertLex(oi, {"apple", "banana", "cherry"}); OnDeleteRecord rec = {0, {}}; sds min = sdsnew("a"); sds max = sdsnew("z"); - unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, testOnDeleteCallback, &rec); + 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(idx), 0UL); + ASSERT_EQ(api.length(oi), 0UL); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_NullCallback) { - OrderedIndex *idx = api.create(); - insertLex(idx, {"apple", "banana", "cherry", "date"}); + OrderedIndex *oi = api.create(); + insertLex(oi, {"apple", "banana", "cherry", "date"}); sds min = sdsnew("banana"); sds max = sdsnew("cherry"); - unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, NULL, NULL); + unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); ASSERT_EQ(deleted, 2UL); - ASSERT_EQ(api.length(idx), 2UL); + ASSERT_EQ(api.length(oi), 2UL); - auto remaining = collectElements(idx); + auto remaining = collectElements(oi); ASSERT_EQ(remaining, (std::vector{"apple", "date"})); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_ExclusiveBounds) { - OrderedIndex *idx = api.create(); - insertLex(idx, {"apple", "banana", "cherry", "date", "elderberry"}); + OrderedIndex *oi = api.create(); + insertLex(oi, {"apple", "banana", "cherry", "date", "elderberry"}); OnDeleteRecord rec = {0, {}}; sds min = sdsnew("banana"); sds max = sdsnew("date"); - unsigned long deleted = api.deleteRangeByLex(idx, min, max, 1, 1, testOnDeleteCallback, &rec); + unsigned long deleted = api.deleteRangeByLex(oi, min, max, 1, 1, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 1UL); ASSERT_EQ(rec.count, 1); ASSERT_EQ(rec.elements[0], "cherry"); - ASSERT_EQ(api.length(idx), 4UL); + ASSERT_EQ(api.length(oi), 4UL); - auto remaining = collectElements(idx); + auto remaining = collectElements(oi); ASSERT_EQ(remaining, (std::vector{"apple", "banana", "date", "elderberry"})); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_SingleElement) { - OrderedIndex *idx = api.create(); - insertLex(idx, {"apple", "banana", "cherry"}); + OrderedIndex *oi = api.create(); + insertLex(oi, {"apple", "banana", "cherry"}); OnDeleteRecord rec = {0, {}}; sds min = sdsnew("banana"); sds max = sdsnew("banana"); - unsigned long deleted = api.deleteRangeByLex(idx, min, max, 0, 0, testOnDeleteCallback, &rec); + unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 1UL); ASSERT_EQ(rec.count, 1); ASSERT_EQ(rec.elements[0], "banana"); - ASSERT_EQ(api.length(idx), 2UL); + ASSERT_EQ(api.length(oi), 2UL); - auto remaining = collectElements(idx); + auto remaining = collectElements(oi); ASSERT_EQ(remaining, (std::vector{"apple", "cherry"})); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } /* ========== Range-Delete Hashtable Consistency Tests ========== */ @@ -2276,31 +2276,31 @@ class RangeDeleteHashtableConsistencyTest : public ::testing::Test { protected: SkiplistOrderedIndex api; - void insertN(OrderedIndex *idx, std::set &ht, int n) { + void insertN(OrderedIndex *oi, std::set &ht, int n) { for (int i = 0; i < n; i++) { std::string name = "key" + std::to_string(i); sds ele = sdsnew(name.c_str()); - api.insertSds(idx, (double)i, ele); + api.insertSds(oi, (double)i, ele); ht.insert(name); sdsfree(ele); } } - void insertLex(OrderedIndex *idx, std::set &ht, const std::vector &elems, double score = 1.0) { + void insertLex(OrderedIndex *oi, std::set &ht, const std::vector &elems, double score = 1.0) { for (auto &e : elems) { sds ele = sdsnew(e.c_str()); - api.insertSds(idx, score, ele); + api.insertSds(oi, score, ele); ht.insert(e); sdsfree(ele); } } - std::set collectIndexElements(OrderedIndex *idx) { + std::set collectIndexElements(OrderedIndex *oi) { std::set result; OrderedIndexIterator iter; OrderedIndexItem *pos; - api.initIterator(&iter, idx); - while (api.next(&iter, &pos)) { + api.initIterator(&iter, oi); + while (((pos = api.next(&iter)) != NULL)) { const char *ptr; size_t len; api.getElementRaw(pos, &ptr, &len); @@ -2314,143 +2314,143 @@ class RangeDeleteHashtableConsistencyTest : public ::testing::Test { /* ByScore */ TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_PartialDelete) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); std::set simulatedHt; - insertN(idx, simulatedHt, 10); + insertN(oi, simulatedHt, 10); - api.deleteRangeByScore(idx, 3.0, 6.0, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + api.deleteRangeByScore(oi, 3.0, 6.0, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(idx); + std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_EQ(indexElements.size(), 6UL); - api.free(idx); + api.free(oi); } TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_FullDelete) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); std::set simulatedHt; - insertN(idx, simulatedHt, 10); + insertN(oi, simulatedHt, 10); - api.deleteRangeByScore(idx, NEG_INF, POS_INF, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + api.deleteRangeByScore(oi, NEG_INF, POS_INF, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(idx); + std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_TRUE(indexElements.empty()); - api.free(idx); + api.free(oi); } TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_EmptyRange) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); std::set simulatedHt; - insertN(idx, simulatedHt, 10); + insertN(oi, simulatedHt, 10); - api.deleteRangeByScore(idx, 20.0, 30.0, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + api.deleteRangeByScore(oi, 20.0, 30.0, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(idx); + std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_EQ(indexElements.size(), 10UL); - api.free(idx); + api.free(oi); } /* ByRank */ TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_PartialDelete) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); std::set simulatedHt; - insertN(idx, simulatedHt, 10); + insertN(oi, simulatedHt, 10); - api.deleteRangeByRank(idx, 3, 5, hashtableConsistencyOnDelete, &simulatedHt); + api.deleteRangeByRank(oi, 3, 5, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(idx); + std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_EQ(indexElements.size(), 7UL); - api.free(idx); + api.free(oi); } TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_FullDelete) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); std::set simulatedHt; - insertN(idx, simulatedHt, 10); + insertN(oi, simulatedHt, 10); - api.deleteRangeByRank(idx, 1, 10, hashtableConsistencyOnDelete, &simulatedHt); + api.deleteRangeByRank(oi, 1, 10, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(idx); + std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_TRUE(indexElements.empty()); - api.free(idx); + api.free(oi); } TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_EmptyRange) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); std::set simulatedHt; - insertN(idx, simulatedHt, 10); + insertN(oi, simulatedHt, 10); - api.deleteRangeByRank(idx, 20, 30, hashtableConsistencyOnDelete, &simulatedHt); + api.deleteRangeByRank(oi, 20, 30, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(idx); + std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_EQ(indexElements.size(), 10UL); - api.free(idx); + api.free(oi); } /* ByLex */ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_PartialDelete) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); std::set simulatedHt; - insertLex(idx, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); + insertLex(oi, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); sds min = sdsnew("banana"); sds max = sdsnew("date"); - api.deleteRangeByLex(idx, min, max, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + api.deleteRangeByLex(oi, min, max, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(idx); + std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_EQ(indexElements.size(), 2UL); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_FullDelete) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); std::set simulatedHt; - insertLex(idx, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); + insertLex(oi, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); sds min = sdsnew("a"); sds max = sdsnew("z"); - api.deleteRangeByLex(idx, min, max, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + api.deleteRangeByLex(oi, min, max, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(idx); + std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_TRUE(indexElements.empty()); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_EmptyRange) { - OrderedIndex *idx = api.create(); + OrderedIndex *oi = api.create(); std::set simulatedHt; - insertLex(idx, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); + insertLex(oi, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); sds min = sdsnew("zzz"); sds max = sdsnew("zzzz"); - api.deleteRangeByLex(idx, min, max, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + api.deleteRangeByLex(oi, min, max, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(idx); + std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_EQ(indexElements.size(), 5UL); sdsfree(min); sdsfree(max); - api.free(idx); + api.free(oi); } From 62db3457b4440e96434fa2562566a062fda9623d Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Sat, 16 May 2026 02:05:45 +0000 Subject: [PATCH 04/45] skiplist: fix zslDetachNode search and deleteRange callback ownership MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix two bugs in the skiplist OrderedIndex backend: 1. zslDetachNode: use zslCompareNodes (score-based search) instead of pointer equality (x->level[i].forward != node). Pointer equality overshoots at higher skiplist levels because higher-level forward pointers skip over intermediate nodes. The score-based search correctly stops at the predecessor at each level. 2. skiplistDeleteRangeByScore/Lex/Rank: when an on_delete callback is provided, the callback receives ownership of the item (per the API contract in ordered_index.h). The backend must not call zslFreeNode after invoking the callback — that would double-free. Also fix a stale 2-arg next() call in ordered_index_test.h that didn't get updated when the iterator API changed to pointer-return style. Signed-off-by: Rain Valentine --- src/skiplist.c | 2 +- src/skiplist_ordered_index.c | 27 +++++++++++++++------------ src/unit/ordered_index_test.h | 2 +- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/skiplist.c b/src/skiplist.c index 599ad3eb32c..f0776a6346b 100644 --- a/src/skiplist.c +++ b/src/skiplist.c @@ -687,7 +687,7 @@ zskiplistNode *zslDetachNode(zskiplist *zsl, zskiplistNode *node) { zskiplistNode *update[ZSKIPLIST_MAXLEVEL]; zskiplistNode *x = zslGetHeader(zsl); for (int i = zslGetHeight(zsl) - 1; i >= 0; i--) { - while (x->level[i].forward && x->level[i].forward != node) { + while (zslCompareNodes(x->level[i].forward, node) < 0) { x = x->level[i].forward; } update[i] = x; diff --git a/src/skiplist_ordered_index.c b/src/skiplist_ordered_index.c index 087c942dfb6..c1426892028 100644 --- a/src/skiplist_ordered_index.c +++ b/src/skiplist_ordered_index.c @@ -97,10 +97,11 @@ unsigned long skiplistDeleteRangeByScore(OrderedIndex *oi, double min, double ma while (x && zslValueLteMax(x->score, &range)) { zskiplistNode *next = x->level[0].forward; zslDeleteNode(zsl, x, update); - if (on_delete) { - on_delete((OrderedIndexItem *)x, ctx); - } - zslFreeNode(x); + if (on_delete) { on_delete((OrderedIndexItem *)x, ctx); } else { zslFreeNode(x); } + + + + removed++; x = next; } @@ -127,10 +128,11 @@ unsigned long skiplistDeleteRangeByRank(OrderedIndex *oi, unsigned long start, u while (x && traversed <= end) { zskiplistNode *next = x->level[0].forward; zslDeleteNode(zsl, x, update); - if (on_delete) { - on_delete((OrderedIndexItem *)x, ctx); - } - zslFreeNode(x); + if (on_delete) { on_delete((OrderedIndexItem *)x, ctx); } else { zslFreeNode(x); } + + + + removed++; traversed++; x = next; @@ -164,10 +166,11 @@ unsigned long skiplistDeleteRangeByLex(OrderedIndex *oi, const_sds min, const_sd if (!zslLexValueLteMax(ele, &range)) break; zskiplistNode *next = x->level[0].forward; zslDeleteNode(zsl, x, update); - if (on_delete) { - on_delete((OrderedIndexItem *)x, ctx); - } - zslFreeNode(x); + if (on_delete) { on_delete((OrderedIndexItem *)x, ctx); } else { zslFreeNode(x); } + + + + removed++; x = next; } diff --git a/src/unit/ordered_index_test.h b/src/unit/ordered_index_test.h index 07d98073b22..4bdf08dd18a 100644 --- a/src/unit/ordered_index_test.h +++ b/src/unit/ordered_index_test.h @@ -75,7 +75,7 @@ class OrderedIndexTestApi { OrderedIndexIterator iter; OrderedIndexItem *pos; initIterator(&iter, oi); - while (next(&iter, &pos)) { + while ((pos = next(&iter)) != NULL) { const char *ptr; size_t len; getElementRaw(pos, &ptr, &len); From 268687c6e04155dccdf74e6e03c77b3597317dd0 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Mon, 18 May 2026 21:51:04 +0000 Subject: [PATCH 05/45] ordered-index: address PR 2 self-review feedback - Replace 'backend' with 'implementation' throughout - Fix header comment claiming only skiplist exists - Add consistent min_ex/max_ex documentation on all range functions - Soften iterator init comment to describe behavior not mandate - Improve memory estimation and defrag descriptions - Rename zslDeleteNode -> zslUnlinkNode (detach semantics) - Remove unnecessary section header in skiplist.c - Add SPDX copyright header to skiplist_ordered_index.c - Remove spurious empty lines in skiplist_ordered_index.c - Fix test header to say 'active implementation' not 'active backend' Signed-off-by: Rain Valentine --- src/ordered_index.h | 23 ++++++++++++----------- src/skiplist.c | 15 +++++++-------- src/skiplist.h | 2 +- src/skiplist_ordered_index.c | 21 +++++++++------------ src/unit/ordered_index_test.h | 2 +- 5 files changed, 30 insertions(+), 33 deletions(-) diff --git a/src/ordered_index.h b/src/ordered_index.h index a9f6449c460..52787187c83 100644 --- a/src/ordered_index.h +++ b/src/ordered_index.h @@ -20,10 +20,10 @@ * membership testing and prevents duplicate insertions. The caller is * responsible for checking the hashtable before inserting. * - * The interface is backend-agnostic. Currently the only backend is a skiplist - * (see skiplist_ordered_index.c). A B+ tree backend is planned. Backend + * 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 backend. */ + * implemented in ordered_index.c which delegates to the active implementation. */ #include "sds.h" #include @@ -90,7 +90,7 @@ 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] (exclusive if min_ex/max_ex set). +/* 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); @@ -98,7 +98,7 @@ unsigned long orderedIndexDeleteRangeByScore(OrderedIndex *oi, double min, doubl * Calls on_delete for each removed item. Returns count of items removed. */ unsigned long orderedIndexDeleteRangeByRank(OrderedIndex *oi, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx); -/* Delete all items with element in lex range [min, max]. +/* 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); @@ -121,18 +121,19 @@ void orderedIndexGetElementRaw(const OrderedIndexItem *item, const char **ptr, s /* Get the score of an item. */ double orderedIndexGetScore(const OrderedIndexItem *item); -/* Count items with score in [min, max] (exclusive if min_ex/max_ex set). */ +/* 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]. */ +/* 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. Must call a seek function before - * iterating, or next()/prev() will start from the beginning/end. */ +/* Initialize a stack-allocated iterator. If no seek function is called, + * next() starts from the beginning and prev() starts from the end. + * Use orderedIndexSeekToRank/ScoreRange/LexRange to start elsewhere. */ void orderedIndexInitIterator(OrderedIndexIterator *iter, OrderedIndex *oi); /* Reset iterator position (keeps the index association). */ @@ -162,10 +163,10 @@ void orderedIndexSeekToLexRange(OrderedIndexIterator *iter, const_sds min, const /* Hint to the OS that the index memory can be reclaimed (e.g. via madvise). */ void orderedIndexDismissMemory(OrderedIndex *oi); -/* Estimate total memory usage by sampling. */ +/* Estimate total memory usage by averaging the specified number of sample elements. */ size_t orderedIndexEstimateMemory(OrderedIndex *oi, size_t sample_size); -/* Defrag the index header/metadata. Returns new pointer if reallocated. */ +/* 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. diff --git a/src/skiplist.c b/src/skiplist.c index f0776a6346b..0e679dfef03 100644 --- a/src/skiplist.c +++ b/src/skiplist.c @@ -234,7 +234,7 @@ zskiplistNode *zslInsert(zskiplist *zsl, double score, const_sds ele) { /* 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,7 +304,7 @@ 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; @@ -431,7 +431,7 @@ unsigned long zslDeleteRangeByScore(zskiplist *zsl, zrangespec *range, hashtable /* Delete nodes while in range. */ while (x && zslValueLteMax(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); @@ -462,7 +462,7 @@ unsigned long zslDeleteRangeByLex(zskiplist *zsl, zlexrangespec *range, hashtabl /* Delete nodes while in range. */ while (x && zslLexValueLteMax(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 +491,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++; @@ -672,7 +672,6 @@ zskiplistNode *zslNthInLexRange(zskiplist *zsl, zlexrangespec *range, long n) { return x; } -/* --- Accessors added for OrderedIndex --- */ zskiplistNode *zslGetFirst(const zskiplist *zsl) { return ((zskiplist *)zsl)->header.level[0].forward; @@ -693,7 +692,7 @@ zskiplistNode *zslDetachNode(zskiplist *zsl, zskiplistNode *node) { update[i] = x; } serverAssert(x->level[0].forward == node); - zslDeleteNode(zsl, node, update); + zslUnlinkNode(zsl, node, update); return node; } diff --git a/src/skiplist.h b/src/skiplist.h index 6e9fefbf9ef..f3673e13855 100644 --- a/src/skiplist.h +++ b/src/skiplist.h @@ -138,7 +138,7 @@ 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); diff --git a/src/skiplist_ordered_index.c b/src/skiplist_ordered_index.c index c1426892028..b0386c017fc 100644 --- a/src/skiplist_ordered_index.c +++ b/src/skiplist_ordered_index.c @@ -1,3 +1,9 @@ +/* + * Copyright (c) Valkey Contributors + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + #include "server.h" #include "ordered_index.h" #include "skiplist.h" @@ -96,12 +102,9 @@ unsigned long skiplistDeleteRangeByScore(OrderedIndex *oi, double min, double ma /* Delete nodes while in range. */ while (x && zslValueLteMax(x->score, &range)) { zskiplistNode *next = x->level[0].forward; - zslDeleteNode(zsl, x, update); + zslUnlinkNode(zsl, x, update); if (on_delete) { on_delete((OrderedIndexItem *)x, ctx); } else { zslFreeNode(x); } - - - removed++; x = next; } @@ -127,12 +130,9 @@ unsigned long skiplistDeleteRangeByRank(OrderedIndex *oi, unsigned long start, u x = x->level[0].forward; while (x && traversed <= end) { zskiplistNode *next = x->level[0].forward; - zslDeleteNode(zsl, x, update); + zslUnlinkNode(zsl, x, update); if (on_delete) { on_delete((OrderedIndexItem *)x, ctx); } else { zslFreeNode(x); } - - - removed++; traversed++; x = next; @@ -165,12 +165,9 @@ unsigned long skiplistDeleteRangeByLex(OrderedIndex *oi, const_sds min, const_sd sds ele = zslGetNodeElement(x); if (!zslLexValueLteMax(ele, &range)) break; zskiplistNode *next = x->level[0].forward; - zslDeleteNode(zsl, x, update); + zslUnlinkNode(zsl, x, update); if (on_delete) { on_delete((OrderedIndexItem *)x, ctx); } else { zslFreeNode(x); } - - - removed++; x = next; } diff --git a/src/unit/ordered_index_test.h b/src/unit/ordered_index_test.h index 4bdf08dd18a..fe1e29898ff 100644 --- a/src/unit/ordered_index_test.h +++ b/src/unit/ordered_index_test.h @@ -6,7 +6,7 @@ * * 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 backend. + * (implemented in ordered_index.c) which delegate to the active implementation. */ extern "C" { From 02b9f6cf414d51bc5957265cdcfe62e404f94b46 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Tue, 19 May 2026 08:37:53 +0000 Subject: [PATCH 06/45] tests: fix CI failures (clang-format, macOS warnings, ASAN leaks) - skiplist.c: reformat one-liner if statements (clang-format-check) - test_ordered_index.cpp: use NEG_INF/POS_INF instead of bare INFINITY to avoid -Wdouble-promotion on macOS where INFINITY is float - test_ordered_index.cpp: use TEST_ASSERT_SCORE_EQ for score comparisons to avoid -Wfloat-equal from gtest ASSERT_EQ on doubles - test_ordered_index.cpp: free items in on_delete callbacks to fix LeakSanitizer failures (callback takes ownership per API contract) Signed-off-by: Rain Valentine Signed-off-by: Rain Valentine --- src/skiplist.c | 26 ++++++++++++++++++++------ src/unit/test_ordered_index.cpp | 24 +++++++++++++----------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/skiplist.c b/src/skiplist.c index 0e679dfef03..5a4a66577a1 100644 --- a/src/skiplist.c +++ b/src/skiplist.c @@ -718,27 +718,41 @@ void zslReleaseIterator(zslIter *iter) { } bool zslNext(zslIter *iter, zskiplistNode **nodeptr) { - if (iter->zsl == NULL) { *nodeptr = NULL; return false; } + if (iter->zsl == NULL) { + *nodeptr = NULL; + return false; + } if (iter->node == NULL) { iter->node = zslGetHeader(iter->zsl)->level[0].forward; } else if (iter->node == zslGetTail(iter->zsl)) { - *nodeptr = NULL; return false; + *nodeptr = NULL; + return false; } else { iter->node = iter->node->level[0].forward; } - if (iter->node == NULL) { *nodeptr = NULL; return false; } + if (iter->node == NULL) { + *nodeptr = NULL; + return false; + } *nodeptr = iter->node; return true; } bool zslPrev(zslIter *iter, zskiplistNode **nodeptr) { - if (iter->zsl == NULL) { *nodeptr = NULL; return false; } + if (iter->zsl == NULL) { + *nodeptr = NULL; + return false; + } if (iter->node == zslGetHeader(iter->zsl)) { - *nodeptr = NULL; return false; + *nodeptr = NULL; + return false; } if (iter->node == NULL) { iter->node = zslGetTail(iter->zsl); - if (iter->node == NULL) { *nodeptr = NULL; return false; } + if (iter->node == NULL) { + *nodeptr = NULL; + return false; + } } zskiplistNode *ret = iter->node; iter->node = iter->node->backward; diff --git a/src/unit/test_ordered_index.cpp b/src/unit/test_ordered_index.cpp index b2ca714f088..993cdbc1615 100644 --- a/src/unit/test_ordered_index.cpp +++ b/src/unit/test_ordered_index.cpp @@ -1486,7 +1486,7 @@ TEST_P(OrderedIndexTest, RandomizedInsertAndTraversal) { OrderedIndexItem *pos; api.initIterator(&iter, oi); int count = 0; - double prevScore = -INFINITY; + double prevScore = NEG_INF; while (((pos = api.next(&iter)) != NULL)) { double s = api.getScore(pos); ASSERT_GE(s, prevScore); @@ -1512,7 +1512,7 @@ TEST_P(OrderedIndexTest, RandomizedBackwardTraversal) { OrderedIndexItem *pos; api.initIterator(&iter, oi); int count = 0; - double prevScore = INFINITY; + double prevScore = POS_INF; while (((pos = api.prev(&iter)) != NULL)) { double s = api.getScore(pos); ASSERT_LE(s, prevScore); @@ -1535,7 +1535,7 @@ TEST_P(OrderedIndexTest, RandomizedScoreRetrieval) { auto entries = test_build_random_index(api, oi, rng, n); for (auto &e : entries) { - ASSERT_EQ(api.getScore(e.node), e.score); + TEST_ASSERT_SCORE_EQ(api.getScore(e.node), e.score); } api.free(oi); } @@ -1587,7 +1587,7 @@ TEST_P(OrderedIndexTest, RandomizedDelete) { OrderedIndexItem *pos; api.initIterator(&iter, oi); int count = 0; - double prevScore = -INFINITY; + double prevScore = NEG_INF; while (((pos = api.next(&iter)) != NULL)) { ASSERT_GE(api.getScore(pos), prevScore); prevScore = api.getScore(pos); @@ -1614,14 +1614,14 @@ TEST_P(OrderedIndexTest, RandomizedUpdateScore) { OrderedIndexItem *updated = api.updateScore(oi, entries[updIdx].node, newScore); ASSERT_NE(updated, nullptr); - ASSERT_EQ(api.getScore(updated), newScore); + TEST_ASSERT_SCORE_EQ(api.getScore(updated), newScore); ASSERT_EQ(api.length(oi), (unsigned long)n); VERIFY_INTEGRITY(api, oi); OrderedIndexIterator iter; OrderedIndexItem *pos; api.initIterator(&iter, oi); - double prevScore = -INFINITY; + double prevScore = NEG_INF; while (((pos = api.next(&iter)) != NULL)) { ASSERT_GE(api.getScore(pos), prevScore); prevScore = api.getScore(pos); @@ -1654,20 +1654,20 @@ TEST_P(OrderedIndexTest, RandomizedPop) { OrderedIndexItem *first = api.popFirst(oi); ASSERT_NE(first, nullptr); - ASSERT_EQ(api.getScore(first), minScore); + TEST_ASSERT_SCORE_EQ(api.getScore(first), minScore); ASSERT_EQ(api.length(oi), (unsigned long)(n - 1)); api.freeItem(first); VERIFY_INTEGRITY(api, oi); OrderedIndexItem *last = api.popLast(oi); ASSERT_NE(last, nullptr); - ASSERT_EQ(api.getScore(last), maxScore); + TEST_ASSERT_SCORE_EQ(api.getScore(last), maxScore); ASSERT_EQ(api.length(oi), (unsigned long)(n - 2)); api.freeItem(last); VERIFY_INTEGRITY(api, oi); api.initIterator(&iter, oi); - double prevScore = -INFINITY; + double prevScore = NEG_INF; while (((pos = api.next(&iter)) != NULL)) { ASSERT_GE(api.getScore(pos), prevScore); prevScore = api.getScore(pos); @@ -1702,7 +1702,7 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { OrderedIndexIterator iter; OrderedIndexItem *pos; api.initIterator(&iter, oi); - double prevScore = -INFINITY; + double prevScore = NEG_INF; while (((pos = api.next(&iter)) != NULL)) { double s = api.getScore(pos); ASSERT_TRUE(s < lo || s > hi); @@ -1738,7 +1738,7 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByRank) { OrderedIndexItem *pos; api.initIterator(&iter, oi); int remaining = 0; - double prevScore = -INFINITY; + double prevScore = NEG_INF; while (((pos = api.next(&iter)) != NULL)) { ASSERT_GE(api.getScore(pos), prevScore); prevScore = api.getScore(pos); @@ -1915,6 +1915,7 @@ static void testOnDeleteCallback(OrderedIndexItem *item, void *ctx) { size_t len; skiplistGetElementRaw(item, &ptr, &len); rec->elements.emplace_back(ptr, len); + orderedIndexFreeItem(item); } class OnDeleteCallbackTest : public ::testing::Test { @@ -2270,6 +2271,7 @@ static void hashtableConsistencyOnDelete(OrderedIndexItem *item, void *ctx) { size_t len; skiplistGetElementRaw(item, &ptr, &len); ht->erase(std::string(ptr, len)); + orderedIndexFreeItem(item); } class RangeDeleteHashtableConsistencyTest : public ::testing::Test { From 517895b46bfe5d8daf9d6a7fb14c179f2ab55687 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Tue, 19 May 2026 22:25:58 +0000 Subject: [PATCH 07/45] ci: fix clang-format and macOS -Wfloat-equal warnings - Expand 3 one-liner if/else blocks in skiplist_ordered_index.c to satisfy clang-format-18 - Use TEST_ASSERT_SCORE_EQ instead of ASSERT_EQ for double comparison in RandomizedForwardBackwardMirror test (gtest 1.17 on macOS triggers -Werror,-Wfloat-equal) Signed-off-by: Rain Valentine --- src/skiplist_ordered_index.c | 18 +++++++++++++++--- src/unit/test_ordered_index.cpp | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/skiplist_ordered_index.c b/src/skiplist_ordered_index.c index b0386c017fc..8a8f6f30fc2 100644 --- a/src/skiplist_ordered_index.c +++ b/src/skiplist_ordered_index.c @@ -103,7 +103,11 @@ unsigned long skiplistDeleteRangeByScore(OrderedIndex *oi, double min, double ma while (x && zslValueLteMax(x->score, &range)) { zskiplistNode *next = x->level[0].forward; zslUnlinkNode(zsl, x, update); - if (on_delete) { on_delete((OrderedIndexItem *)x, ctx); } else { zslFreeNode(x); } + if (on_delete) { + on_delete((OrderedIndexItem *)x, ctx); + } else { + zslFreeNode(x); + } removed++; x = next; @@ -131,7 +135,11 @@ unsigned long skiplistDeleteRangeByRank(OrderedIndex *oi, unsigned long start, u while (x && traversed <= end) { zskiplistNode *next = x->level[0].forward; zslUnlinkNode(zsl, x, update); - if (on_delete) { on_delete((OrderedIndexItem *)x, ctx); } else { zslFreeNode(x); } + if (on_delete) { + on_delete((OrderedIndexItem *)x, ctx); + } else { + zslFreeNode(x); + } removed++; traversed++; @@ -166,7 +174,11 @@ unsigned long skiplistDeleteRangeByLex(OrderedIndex *oi, const_sds min, const_sd if (!zslLexValueLteMax(ele, &range)) break; zskiplistNode *next = x->level[0].forward; zslUnlinkNode(zsl, x, update); - if (on_delete) { on_delete((OrderedIndexItem *)x, ctx); } else { zslFreeNode(x); } + if (on_delete) { + on_delete((OrderedIndexItem *)x, ctx); + } else { + zslFreeNode(x); + } removed++; x = next; diff --git a/src/unit/test_ordered_index.cpp b/src/unit/test_ordered_index.cpp index 993cdbc1615..ea4e8f302f1 100644 --- a/src/unit/test_ordered_index.cpp +++ b/src/unit/test_ordered_index.cpp @@ -1778,7 +1778,7 @@ TEST_P(OrderedIndexTest, RandomizedForwardBackwardMirror) { ASSERT_EQ(forwardScores.size(), backwardScores.size()); std::reverse(backwardScores.begin(), backwardScores.end()); for (size_t i = 0; i < forwardScores.size(); i++) { - ASSERT_EQ(forwardScores[i], backwardScores[i]); + TEST_ASSERT_SCORE_EQ(forwardScores[i], backwardScores[i]); } api.free(oi); } From 4016e0091116575205031fa1ad2006a726b32c12 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Fri, 15 May 2026 23:04:08 +0000 Subject: [PATCH 08/45] zset: convert t_zset.c to OrderedIndex interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert all sorted set command implementations in t_zset.c to use the OrderedIndex abstraction instead of direct skiplist calls. Changes: - zset struct: zskiplist *zsl -> OrderedIndex *oi (in server.h) - All zsl* calls in t_zset.c replaced with orderedIndex* equivalents - Range iteration converted to OrderedIndex iterator API - Range deletion uses on_delete callback (zsetIndexDeleteCallback) - orderedIndexCountScoreRange/LexRange replaces rank-difference counting - orderedIndexGetElementRaw used for element access (cast to sds where needed for hashtable compatibility) - Other files (geo.c, defrag.c, module.c, etc.) use (zskiplist *) cast on zs->oi until converted in subsequent PRs Bug fixes: - zslDetachNode: fixed search to use zslCompareNodes (score-based) instead of pointer equality which overshoots at higher skiplist levels - skiplistDeleteRangeBy*: fixed double-free when on_delete callback is provided (backend no longer frees after invoking callback) ZUNION/ZINTER/ZDIFF detached-item path still uses raw skiplist calls with casts — will be converted in a follow-up commit. All 326 zset integration tests pass. Signed-off-by: Rain Valentine --- src/debug.c | 2 +- src/defrag.c | 6 +- src/geo.c | 4 +- src/lazyfree.c | 2 +- src/module.c | 4 +- src/object.c | 8 +- src/rdb.c | 4 +- src/server.h | 5 +- src/sort.c | 2 +- src/t_zset.c | 353 +++++++++++++++++++++++++------------------------ 10 files changed, 200 insertions(+), 190 deletions(-) diff --git a/src/debug.c b/src/debug.c index 3ae119dc60a..a7489976b63 100644 --- a/src/debug.c +++ b/src/debug.c @@ -1185,7 +1185,7 @@ void serverLogObjectDebugInfo(const robj *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); + serverLog(LL_WARNING, "Skiplist level: %d", (int)((const zset *)o->ptr)->oi->level); } 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..de0226ec3d2 100644 --- a/src/defrag.c +++ b/src/defrag.c @@ -442,7 +442,7 @@ static void scanLaterZsetCallback(void *privdata, void *element_ref) { 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 = hashtableScanDefrag(zs->ht, *cursor, scanLaterZsetCallback, (zskiplist *)zs->oi, activeDefragAlloc, HASHTABLE_SCAN_EMIT_REF); } /* Used as hashtable scan callback when all we need is to defrag the hashtable @@ -487,7 +487,7 @@ static void defragZsetSkiplist(robj *ob) { objectSetVal(ob, newzs); zs = newzs; } - if ((newzsl = activeDefragAlloc(zs->zsl))) zs->zsl = newzsl; + if ((newzsl = activeDefragAlloc((zskiplist *)zs->oi))) zs->oi = (OrderedIndex *)newzsl; hashtable *newtable; if ((newtable = hashtableDefragTables(zs->ht, activeDefragAlloc))) zs->ht = newtable; @@ -497,7 +497,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 = hashtableScanDefrag(zs->ht, cursor, activeDefragZsetNode, (zskiplist *)zs->oi, activeDefragAlloc, HASHTABLE_SCAN_EMIT_REF); } while (cursor != 0); } } diff --git a/src/geo.c b/src/geo.c index 6723ef1723c..4222433f24d 100644 --- a/src/geo.c +++ b/src/geo.c @@ -308,7 +308,7 @@ 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; + zskiplist *zsl = (zskiplist *)zs->oi; zskiplistNode *ln; if ((ln = zslNthInRange(zsl, &range, 0, NULL)) == NULL) { @@ -841,7 +841,7 @@ void georadiusGeneric(client *c, int srcKeyIndex, int flags) { if (maxelelen < elelen) maxelelen = elelen; totelelen += elelen; - znode = zslInsert(zs->zsl, score, gp->member); + znode = zslInsert((zskiplist *)zs->oi, score, gp->member); serverAssert(hashtableAdd(zs->ht, znode)); sdsfree(gp->member); gp->member = NULL; diff --git a/src/lazyfree.c b/src/lazyfree.c index 45dd5c49073..eae9216862d 100644 --- a/src/lazyfree.c +++ b/src/lazyfree.c @@ -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 zslGetLength((zskiplist *)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..09214f689e5 100644 --- a/src/module.c +++ b/src/module.c @@ -5129,7 +5129,7 @@ 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; + zskiplist *zsl = (zskiplist *)zs->oi; key->u.zset.current = first ? zslNthInRange(zsl, zrs, 0, NULL) : zslNthInRange(zsl, zrs, -1, NULL); } else { serverPanic("Unsupported zset encoding"); @@ -5192,7 +5192,7 @@ 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; + zskiplist *zsl = (zskiplist *)zs->oi; key->u.zset.current = first ? zslNthInLexRange(zsl, zlrs, 0) : zslNthInLexRange(zsl, zlrs, -1); } else { serverPanic("Unsupported zset encoding"); diff --git a/src/object.c b/src/object.c index fa5cd5e1ae1..4703a8f3405 100644 --- a/src/object.c +++ b/src/object.c @@ -526,7 +526,7 @@ robj *createZsetObject(void) { robj *o; zs->ht = hashtableCreate(&zsetHashtableType); - zs->zsl = zslCreate(); + zs->oi = (OrderedIndex *)zslCreate(); 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); + zslFree((zskiplist *)zs->oi); zfree(zs); break; case OBJ_ENCODING_LISTPACK: zfree(objectGetVal(o)); break; @@ -720,7 +720,7 @@ 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; + zskiplist *zsl = (zskiplist *)zs->oi; serverAssert(zslGetLength(zsl) != 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. */ @@ -1244,7 +1244,7 @@ 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; + zskiplist *zsl = ((zset *)objectGetVal(o))->oi; zskiplistNode *zheader = zslGetHeader(zsl); zskiplistNode *znode = zheader->level[0].forward; asize += sizeof(zset) + zslGetAllocSize() + hashtableMemUsage(ht); diff --git a/src/rdb.c b/src/rdb.c index 94fe6a2cf6e..af0f68e1098 100644 --- a/src/rdb.c +++ b/src/rdb.c @@ -957,7 +957,7 @@ 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; + zskiplist *zsl = (zskiplist *)zs->oi; if ((n = rdbSaveLen(rdb, zslGetLength(zsl))) == -1) return -1; nwritten += n; @@ -2116,7 +2116,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 = zslInsert((zskiplist *)zs->oi, score, sdsele); sdsfree(sdsele); if (!hashtableAdd(zs->ht, znode)) { rdbReportCorruptRDB("Duplicate zset fields detected"); diff --git a/src/server.h b/src/server.h index 94fa9a5647e..e4b4a01f880 100644 --- a/src/server.h +++ b/src/server.h @@ -1491,9 +1491,12 @@ struct sharedObjectsStruct { /* 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 { diff --git a/src/sort.c b/src/sort.c index 216b9b89fde..35a2b5bd3cb 100644 --- a/src/sort.c +++ b/src/sort.c @@ -418,7 +418,7 @@ void sortCommandGeneric(client *c, int readonly) { * way, just getting the required range, as an optimization. */ zset *zs = objectGetVal(sortval); - zskiplist *zsl = zs->zsl; + zskiplist *zsl = (zskiplist *)zs->oi; zskiplistNode *ln; sds sdsele; int rangelen = vectorlen; diff --git a/src/t_zset.c b/src/t_zset.c index ca74bec28c6..53bd7215381 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -47,7 +47,7 @@ * 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. + * only in orderedIndexFreeItem(). The dictionary has no value free method set. * So we should always remove an element from the dictionary, and later from * the skiplist. * @@ -62,6 +62,7 @@ #include "server.h" #include "skiplist.h" +#include "ordered_index.h" #include "intset.h" /* Compact integer set structure */ #include "mt19937-64.h" #include @@ -585,7 +586,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"); } @@ -630,7 +631,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; + OrderedIndexItem *node; sds ele; double score; @@ -646,7 +647,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); @@ -665,7 +666,7 @@ void zsetConvertAndExpand(robj *zobj, int encoding, unsigned long cap) { else ele = sdsnewlen((char *)vstr, vlen); - node = zslInsert(zs->zsl, score, ele); + node = orderedIndexInsert(zs->oi, score, ele, sdslen(ele)); sdsfree(ele); serverAssert(hashtableAdd(zs->ht, node)); zzlNext(zl, &eptr, &sptr); @@ -679,20 +680,21 @@ 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 skiplist 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); + sds ele = sdsnewlen(ele_ptr, ele_len); + zl = zzlInsertAt(zl, NULL, ele, orderedIndexGetScore(node)); + sdsfree(ele); + orderedIndexFreeItem(node); } + orderedIndexFree(zs->oi); zfree(zs); objectSetVal(zobj, zl); @@ -709,7 +711,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 +730,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"); } @@ -863,8 +865,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,7 +887,7 @@ 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); + OrderedIndexItem *new_node = orderedIndexUpdateScore(zs->oi, old_node, score); /* Note that this assignment updates the node pointer stored in * the hashtable */ if (new_node) *node_ref_in_hashtable = new_node; @@ -893,7 +895,7 @@ int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, dou } 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; @@ -914,12 +916,12 @@ int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, dou static int zsetRemoveFromSkiplist(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. */ /* Delete from skiplist. */ - zslDelete(zs->zsl, node); + orderedIndexDelete(zs->oi, node); return 1; } @@ -992,12 +994,12 @@ 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); + rank = orderedIndexGetRank(zs->oi, node); /* Existing elements always have a rank. */ serverAssert(rank != 0); - if (output_score) *output_score = node->score; + if (output_score) *output_score = orderedIndexGetScore(node); if (reverse) return llen - rank; else @@ -1032,9 +1034,8 @@ 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; + OrderedIndex *oi = zs->oi; + OrderedIndexItem *ln; long llen = zsetLength(o); /* We copy the skiplist elements from the greatest to the @@ -1043,12 +1044,16 @@ robj *zsetDup(robj *o) { * 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); + OrderedIndexIterator iter; + orderedIndexInitIterator(&iter, oi); + orderedIndexSeekToRank(&iter, orderedIndexLength(oi)); while (llen--) { - ele = zslGetNodeElement(ln); - zskiplistNode *znode = zslInsert(new_zs->zsl, ln->score, ele); + ln = orderedIndexPrev(&iter); + 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 +1084,14 @@ 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); + OrderedIndexItem *node = entry; + const char *ele_ptr_tmp; + size_t ele_len_tmp; + orderedIndexGetElementRaw(node, &ele_ptr_tmp, &ele_len_tmp); + sds ele = (sds)ele_ptr_tmp; key->sval = (unsigned char *)ele; key->slen = sdslen(ele); - if (score) *score = node->score; + if (score) *score = orderedIndexGetScore(node); } else if (zsetobj->encoding == OBJ_ENCODING_LISTPACK) { listpackEntry val; lpRandomPair(objectGetVal(zsetobj), zsetsize, key, &val); @@ -1282,6 +1290,17 @@ typedef enum { } zrange_type; /* Implements ZREMRANGEBYRANK, ZREMRANGEBYSCORE, ZREMRANGEBYLEX commands. */ +/* 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); + orderedIndexFreeItem(item); +} + void zremrangeGenericCommand(client *c, zrange_type rangetype) { robj *key = c->argv[1]; robj *zobj; @@ -1350,9 +1369,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 = orderedIndexDeleteRangeByRank(zs->oi, start + 1, end + 1, 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 +1437,7 @@ typedef struct { } zl; struct { zset *zs; - zskiplistNode *node; + OrderedIndexItem *node; } sl; } zset; } iter; @@ -1479,7 +1498,7 @@ 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); + it->sl.node = orderedIndexGetByRank(it->sl.zs->oi, orderedIndexLength(it->sl.zs->oi)); } else { serverPanic("Unknown sorted set encoding"); } @@ -1534,7 +1553,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"); } @@ -1591,11 +1610,14 @@ static int zuiNext(zsetopsrc *op, zsetopval *val) { zzlPrev(it->zl.zl, &it->zl.eptr, &it->zl.sptr); } else if (op->encoding == OBJ_ENCODING_SKIPLIST) { if (it->sl.node == NULL) return 0; - val->ele = zslGetNodeElement(it->sl.node); - val->score = it->sl.node->score; + const char *val_ele_ptr; + size_t val_ele_len; + orderedIndexGetElementRaw(it->sl.node, &val_ele_ptr, &val_ele_len); + val->ele = (sds)val_ele_ptr; + val->score = orderedIndexGetScore(it->sl.node); /* Move to next element. (going backwards, see zuiInitIterator) */ - it->sl.node = it->sl.node->backward; + it->sl.node = (OrderedIndexItem *)((zskiplistNode *)it->sl.node)->backward; } else { serverPanic("Unknown sorted set encoding"); } @@ -1663,8 +1685,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,8 +1740,11 @@ 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); + OrderedIndexItem *node = next; + const char *ele_ptr_tmp; + size_t ele_len_tmp; + orderedIndexGetElementRaw(node, &ele_ptr_tmp, &ele_len_tmp); + sds ele = (sds)ele_ptr_tmp; size_t elelen = sdslen(ele); if (elelen > maxelelen) maxelelen = elelen; if (totallen) (*totallen) += elelen; @@ -1745,7 +1770,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 +1798,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); @@ -1803,7 +1828,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,7 +1840,7 @@ 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++; @@ -2072,7 +2097,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); @@ -2106,13 +2131,16 @@ 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, sdslen(tmp_ele)); + OrderedIndexItem *new_node = zslCreateNode(zslRandomLevel(), 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); + const char *ele_ptr_tmp; + size_t ele_len_tmp; + orderedIndexGetElementRaw(new_node, &ele_ptr_tmp, &ele_len_tmp); + sds ele = (sds)ele_ptr_tmp; totelelen += sdslen(ele); if (sdslen(ele) > maxelelen) { maxelelen = sdslen(ele); @@ -2120,8 +2148,8 @@ static void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIn } 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; + zunionInterAggregate(&((zskiplistNode *)node)->score, score, aggregate); } } zuiClearIterator(&src[i]); @@ -2133,8 +2161,8 @@ static void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIn void *next; while (hashtableNext(&iter, &next)) { - zskiplistNode *node = next; - zslInsertNode(dstzset->zsl, node); + OrderedIndexItem *node = next; + zslInsertNode((zskiplist *)dstzset->oi, (zskiplistNode *)node); } hashtableCleanupIterator(&iter); } else if (op == SET_OP_DIFF) { @@ -2144,7 +2172,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 +2191,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 +2203,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 +2495,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_rank = (start > 0) ? (unsigned long)(llen - start) : orderedIndexLength(oi); + orderedIndexSeekToRank(&iter, seek_rank); } else { - zskiplistNode *zheader = zslGetHeader(zsl); - ln = zheader->level[0].forward; - if (start > 0) ln = zslGetElementByRank(zsl, start + 1); + orderedIndexSeekToRank(&iter, (unsigned long)start); } 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"); @@ -2585,34 +2617,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 (!zslValueGteMin(orderedIndexGetScore(ln), range)) break; } else { - if (!zslValueLteMax(ln->score, range)) break; + if (!zslValueLteMax(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"); @@ -2683,25 +2710,9 @@ void zcountCommand(client *c) { } } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(zobj); - zskiplist *zsl = zs->zsl; - zskiplistNode *zn; - long rank; - - /* Find first element in range */ - zn = zslNthInRange(zsl, &range, 0, &rank); + OrderedIndex *oi = zs->oi; - /* 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 +2768,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; + OrderedIndex *oi = zs->oi; - /* 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); - - /* 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 +2847,37 @@ 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); + sds ele = sdsnewlen(ele_ptr, ele_len); if (reverse) { - if (!zslLexValueGteMin(ele, range)) break; + if (!zslLexValueGteMin(ele, range)) { + sdsfree(ele); + break; + } } else { - if (!zslLexValueLteMax(ele, range)) break; + if (!zslLexValueLteMax(ele, range)) { + sdsfree(ele); + 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)); + sdsfree(ele); } } else { serverPanic("Unknown sorted set encoding"); @@ -3268,17 +3264,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 ? orderedIndexGetByRank(oi, orderedIndexLength(oi)) : orderedIndexGetByRank(oi, 1)); /* 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 +3483,14 @@ 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); + const char *ele_ptr_tmp; + size_t ele_len_tmp; + orderedIndexGetElementRaw(node, &ele_ptr_tmp, &ele_len_tmp); + sds ele = (sds)ele_ptr_tmp; addReplyBulkCBuffer(c, ele, sdslen(ele)); - if (withscores) addReplyDouble(c, node->score); + if (withscores) addReplyDouble(c, orderedIndexGetScore(node)); if (c->flag.close_asap) break; } } else if (zsetobj->encoding == OBJ_ENCODING_LISTPACK) { @@ -3587,7 +3588,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 +3600,14 @@ 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); + sds key = (sds)key_ptr_tmp; if (withscores && c->resp > 2) addReplyArrayLen(c, 2); addReplyBulkCBuffer(c, key, sdslen(key)); - if (withscores) addReplyDouble(c, node->score); + if (withscores) addReplyDouble(c, orderedIndexGetScore(node)); } hashtableCleanupIterator(&iter); From d6c17461b61a18cbc2d017fef44e28aab5529fc7 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Sat, 16 May 2026 02:59:57 +0000 Subject: [PATCH 09/45] zset: convert ZUNION/ZINTER/ZDIFF to OrderedIndex detached-item API Convert the ZUNION SET_OP_UNION path from raw skiplist calls to the OrderedIndex detached-item API: - zslCreateNode + zslRandomLevel -> orderedIndexCreateDetached - Direct node->score mutation -> orderedIndexDetachedSetScore - zslInsertNode bulk insert -> orderedIndexInsertDetached Convert the zuiNext/zuiInitIterator skiplist path from raw backward pointer traversal to OrderedIndex iterator (orderedIndexPrev). After this commit, t_zset.c has zero references to zskiplistNode, zskiplist, or any zsl* function (except range-parsing helpers which operate on zrangespec/zlexrangespec, not the data structure). All 326 integration tests and all unit tests pass. Signed-off-by: Rain Valentine --- src/t_zset.c | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/t_zset.c b/src/t_zset.c index 53bd7215381..e5acc2049d5 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -1437,6 +1437,7 @@ typedef struct { } zl; struct { zset *zs; + OrderedIndexIterator iter; OrderedIndexItem *node; } sl; } zset; @@ -1498,7 +1499,9 @@ static void zuiInitIterator(zsetopsrc *op) { } } else if (op->encoding == OBJ_ENCODING_SKIPLIST) { it->sl.zs = objectGetVal(op->subject); - it->sl.node = orderedIndexGetByRank(it->sl.zs->oi, orderedIndexLength(it->sl.zs->oi)); + orderedIndexInitIterator(&it->sl.iter, it->sl.zs->oi); + orderedIndexSeekToRank(&it->sl.iter, orderedIndexLength(it->sl.zs->oi)); + it->sl.node = NULL; } else { serverPanic("Unknown sorted set encoding"); } @@ -1609,15 +1612,13 @@ 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; const char *val_ele_ptr; size_t val_ele_len; orderedIndexGetElementRaw(it->sl.node, &val_ele_ptr, &val_ele_len); val->ele = (sds)val_ele_ptr; val->score = orderedIndexGetScore(it->sl.node); - - /* Move to next element. (going backwards, see zuiInitIterator) */ - it->sl.node = (OrderedIndexItem *)((zskiplistNode *)it->sl.node)->backward; } else { serverPanic("Unknown sorted set encoding"); } @@ -2131,7 +2132,7 @@ static void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIn void *existing; if (hashtableFindPositionForInsert(dstzset->ht, sdsval, &position, &existing)) { sds tmp_ele = zuiNewSdsFromValue(&zval); - OrderedIndexItem *new_node = zslCreateNode(zslRandomLevel(), score, tmp_ele, sdslen(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, @@ -2149,20 +2150,22 @@ static void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIn /* Update the score with the score of the new instance * of the element found in the current sorted set. */ OrderedIndexItem *node = existing; - zunionInterAggregate(&((zskiplistNode *)node)->score, score, aggregate); + 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)) { OrderedIndexItem *node = next; - zslInsertNode((zskiplist *)dstzset->oi, (zskiplistNode *)node); + orderedIndexInsertDetached(dstzset->oi, node); } hashtableCleanupIterator(&iter); } else if (op == SET_OP_DIFF) { From 578c6a1ca20f4fdf8479d21422e2de32098028e0 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Mon, 18 May 2026 22:25:15 +0000 Subject: [PATCH 10/45] zset: convert all remaining files to OrderedIndex interface Convert module.c, object.c, rdb.c, aof.c, geo.c, sort.c, debug.c, db.c, server.c, lazyfree.c, defrag.c, and valkey-check-rdb.c to use the OrderedIndex abstraction instead of direct skiplist access. Add orderedIndexItemNext/Prev to the interface for item-level navigation (needed by module API which stores current position across calls). After this commit, zero raw skiplist references remain outside of: - skiplist.c/skiplist.h (the implementation) - skiplist_ordered_index.c (the backend adapter) - defrag.c (skiplist-internal node relocation, inherently impl-specific) - t_zset.c range utilities (zslParseRange, zslValueGteMin, etc. operate on zrangespec structs, not the skiplist data structure) Signed-off-by: Rain Valentine Signed-off-by: Rain Valentine --- src/aof.c | 10 +++--- src/db.c | 16 ++++++---- src/debug.c | 12 +++++--- src/defrag.c | 11 ++++--- src/geo.c | 29 ++++++++++-------- src/lazyfree.c | 4 +-- src/module.c | 69 +++++++++++++++++++++++++++--------------- src/object.c | 31 ++++++------------- src/rdb.c | 24 ++++++++------- src/server.c | 8 +++-- src/sort.c | 34 ++++++++++----------- src/valkey-check-rdb.c | 12 +++++--- 12 files changed, 143 insertions(+), 117 deletions(-) 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..dc5695015e0 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" @@ -1064,8 +1064,10 @@ 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); + const char *ptr; + size_t ele_len; + orderedIndexGetElementRaw((const OrderedIndexItem *)entry, &ptr, &ele_len); + key = (sds)ptr; /* zset data is copied after filtering by key */ } else if (o->type == OBJ_HASH) { key = entryGetField(entry); @@ -1087,11 +1089,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 = sdsdup((sds)ptr); 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 a7489976b63..64794b91c8b 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); } diff --git a/src/defrag.c b/src/defrag.c index de0226ec3d2..3098b4377bf 100644 --- a/src/defrag.c +++ b/src/defrag.c @@ -40,6 +40,7 @@ #include "server.h" #include "skiplist.h" +#include "ordered_index.h" #include "hashtable.h" #include "eval.h" #include "script.h" @@ -257,7 +258,7 @@ static void zslUpdateNode(zskiplist *zsl, zskiplistNode *oldnode, zskiplistNode * node, updates skiplist pointers, and updates the hashtable pointer to the * node. */ static void activeDefragZsetNode(void *privdata, void *entry_ref) { - zskiplist *zsl = privdata; + zskiplist *zsl = (zskiplist *)privdata; zskiplistNode **node_ref = (zskiplistNode **)entry_ref; zskiplistNode *node = *node_ref; @@ -442,7 +443,7 @@ static void scanLaterZsetCallback(void *privdata, void *element_ref) { 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, (zskiplist *)zs->oi, activeDefragAlloc, HASHTABLE_SCAN_EMIT_REF); + *cursor = hashtableScanDefrag(zs->ht, *cursor, scanLaterZsetCallback, zs->oi, activeDefragAlloc, HASHTABLE_SCAN_EMIT_REF); } /* Used as hashtable scan callback when all we need is to defrag the hashtable @@ -482,12 +483,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((zskiplist *)zs->oi))) zs->oi = (OrderedIndex *)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 +498,7 @@ static void defragZsetSkiplist(robj *ob) { else { unsigned long cursor = 0; do { - cursor = hashtableScanDefrag(zs->ht, cursor, activeDefragZsetNode, (zskiplist *)zs->oi, activeDefragAlloc, HASHTABLE_SCAN_EMIT_REF); + cursor = hashtableScanDefrag(zs->ht, cursor, activeDefragZsetNode, zs->oi, activeDefragAlloc, HASHTABLE_SCAN_EMIT_REF); } while (cursor != 0); } } diff --git a/src/geo.c b/src/geo.c index 4222433f24d..8f52254e91a 100644 --- a/src/geo.c +++ b/src/geo.c @@ -30,6 +30,7 @@ #include "geo.h" #include "skiplist.h" +#include "ordered_index.h" #include "geohash_helper.h" #include "debugmacro.h" #include "pqsort.h" @@ -308,27 +309,31 @@ int geoGetPointsInRange(robj *zobj, double min, double max, GeoShape *shape, geo } } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = objectGetVal(zobj); - zskiplist *zsl = (zskiplist *)zs->oi; - 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) { + if ((ln = orderedIndexNext(&iter)) == NULL) { /* Nothing exists starting at our min. No results. */ return 0; } - while (ln) { + do { 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 (!zslValueLteMax(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; - } + } while ((ln = orderedIndexNext(&iter)) != NULL); } return ga->used - origincount; } @@ -833,7 +838,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 +846,7 @@ void georadiusGeneric(client *c, int srcKeyIndex, int flags) { if (maxelelen < elelen) maxelelen = elelen; totelelen += elelen; - znode = zslInsert((zskiplist *)zs->oi, 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 eae9216862d..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((zskiplist *)zs->oi); + 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 09214f689e5..ef871a51957 100644 --- a/src/module.c +++ b/src/module.c @@ -57,6 +57,7 @@ * -------------------------------------------------------------------------- */ #include "server.h" #include "skiplist.h" +#include "ordered_index.h" #include "cluster.h" #include "commandlog.h" #include "rdb.h" @@ -220,14 +221,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 +5094,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 +5136,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 = (zskiplist *)zs->oi; - 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 +5200,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 = (zskiplist *)zs->oi; - 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 +5251,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"); } @@ -5292,17 +5303,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 && !zslValueLteMax(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 (!zslLexValueLteMax((sds)ele, &key->u.zset.lrs)) { key->u.zset.er = 1; return 0; } @@ -5354,17 +5368,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 && !zslValueGteMin(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 (!zslLexValueGteMin((sds)ele, &key->u.zset.lrs)) { key->u.zset.er = 1; return 0; } @@ -12005,9 +12022,11 @@ static void moduleScanKeyHashtableCallback(void *privdata, void *entry) { key = entry; /* no value */ } else if (o->type == OBJ_ZSET) { - zskiplistNode *node = (zskiplistNode *)entry; - key = zslGetNodeElement(node); - value = createStringObjectFromLongDouble(node->score, 0); + const char *ele; + size_t ele_len; + orderedIndexGetElementRaw((const OrderedIndexItem *)entry, &ele, &ele_len); + key = (sds)ele; + value = createStringObjectFromLongDouble(orderedIndexGetScore((const OrderedIndexItem *)entry), 0); } else if (o->type == OBJ_HASH) { key = entryGetField(entry); size_t val_len; diff --git a/src/object.c b/src/object.c index 4703a8f3405..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->oi = (OrderedIndex *)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((zskiplist *)zs->oi); + 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 = (zskiplist *)zs->oi; - 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))->oi; - 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/rdb.c b/src/rdb.c index af0f68e1098..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 = (zskiplist *)zs->oi; - 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((zskiplist *)zs->oi, 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..f859e0b348c 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" @@ -627,8 +627,10 @@ hashtableType setHashtableType = { .entryDestructor = dictSdsDestructor}; 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; } /* Sorted sets hash (note: a skiplist is used in addition to the hash table) */ diff --git a/src/sort.c b/src/sort.c index 35a2b5bd3cb..0816fbd478a 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,31 @@ void sortCommandGeneric(client *c, int readonly) { * way, just getting the required range, as an optimization. */ zset *zs = objectGetVal(sortval); - zskiplist *zsl = (zskiplist *)zs->oi; - 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); + orderedIndexSeekToRank(&iter, zsetlen - start); + ln = orderedIndexPrev(&iter); } else { - zskiplistNode *zheader = zslGetHeader(zsl); - ln = zheader->level[0].forward; - if (start > 0) ln = zslGetElementByRank(zsl, start + 1); + orderedIndexSeekToRank(&iter, start); + 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 +452,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/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); From 7b4edd0f8ab4b45010eb04bba9fc0ad7ade6bf79 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Mon, 27 Apr 2026 21:41:34 +0000 Subject: [PATCH 11/45] tests: add integration tests for skiplist-encoded zset paths Add integration tests exercising code paths that changed during the OrderedIndex abstraction, specifically for skiplist-encoded sorted sets. tests/unit/sort.tcl: - SORT BY nosort with skiplist encoding (asc, desc) - SORT BY nosort + LIMIT with skiplist encoding - SORT with BY pattern on skiplist encoding tests/unit/type/zset.tcl: - ZUNIONSTORE with skiplist inputs (basic + WEIGHTS) - ZINTERSTORE with skiplist inputs (basic + AGGREGATE MIN) - ZRANGEBYSCORE with LIMIT on skiplist sets - ZRANGEBYLEX with LIMIT on skiplist sets - ZPOPMIN/ZPOPMAX on skiplist sets (single + multiple) - ZPOPMIN/ZPOPMAX empty skiplist set - ZCOUNT on skiplist sets - ZLEXCOUNT on skiplist sets Signed-off-by: Rain Valentine Signed-off-by: Rain Valentine --- tests/unit/sort.tcl | 48 ++++++++++ tests/unit/type/zset.tcl | 195 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+) diff --git a/tests/unit/sort.tcl b/tests/unit/sort.tcl index 3838a8e05a5..22e9df4ef3b 100644 --- a/tests/unit/sort.tcl +++ b/tests/unit/sort.tcl @@ -171,6 +171,54 @@ 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" { + set original_max [lindex [r config get zset-max-ziplist-entries] 1] + r config set 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} + r config set zset-max-ziplist-entries $original_max + } + + test "SORT sorted set skiplist BY nosort + LIMIT" { + set original_max [lindex [r config get zset-max-ziplist-entries] 1] + r config set 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} + r config set zset-max-ziplist-entries $original_max + } + + test "SORT sorted set skiplist with BY pattern" { + set original_max [lindex [r config get zset-max-ziplist-entries] 1] + r config set 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} + r config set zset-max-ziplist-entries $original_max + } + 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..74aadf55dbd 100644 --- a/tests/unit/type/zset.tcl +++ b/tests/unit/type/zset.tcl @@ -2949,3 +2949,198 @@ start_server [list overrides [list save ""] tags {"zset needs:debug external:ski assert_equal 1 $can_break } } + +start_server {tags {"zset"}} { + test {ZUNIONSTORE with skiplist-encoded inputs} { + set original_max [lindex [r config get zset-max-ziplist-entries] 1] + r config set 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] + + r config set zset-max-ziplist-entries $original_max + } + + test {ZUNIONSTORE with skiplist-encoded inputs and WEIGHTS} { + set original_max [lindex [r config get zset-max-ziplist-entries] 1] + r config set 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] + + r config set zset-max-ziplist-entries $original_max + } + + test {ZINTERSTORE with skiplist-encoded inputs} { + set original_max [lindex [r config get zset-max-ziplist-entries] 1] + r config set 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] + + r config set zset-max-ziplist-entries $original_max + } + + test {ZINTERSTORE with skiplist-encoded inputs and AGGREGATE MIN} { + set original_max [lindex [r config get zset-max-ziplist-entries] 1] + r config set 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] + + r config set zset-max-ziplist-entries $original_max + } + + test {ZRANGEBYSCORE with LIMIT on skiplist-encoded set} { + set original_max [lindex [r config get zset-max-ziplist-entries] 1] + r config set 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] + + r config set zset-max-ziplist-entries $original_max + } + + test {ZRANGEBYLEX with LIMIT on skiplist-encoded set} { + set original_max [lindex [r config get zset-max-ziplist-entries] 1] + r config set 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] + + r config set zset-max-ziplist-entries $original_max + } + + test {ZPOPMIN on skiplist-encoded set} { + set original_max [lindex [r config get zset-max-ziplist-entries] 1] + r config set 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] + + r config set zset-max-ziplist-entries $original_max + } + + test {ZPOPMAX on skiplist-encoded set} { + set original_max [lindex [r config get zset-max-ziplist-entries] 1] + r config set 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] + + r config set zset-max-ziplist-entries $original_max + } + + test {ZPOPMIN/ZPOPMAX empty skiplist-encoded set} { + set original_max [lindex [r config get zset-max-ziplist-entries] 1] + r config set zset-max-ziplist-entries 0 + + r del zset + r zadd zset 1 a + r zpopmin zset + assert_equal 0 [r exists zset] + + r config set zset-max-ziplist-entries $original_max + } + + test {ZCOUNT on skiplist-encoded set} { + set original_max [lindex [r config get zset-max-ziplist-entries] 1] + r config set 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] + + r config set zset-max-ziplist-entries $original_max + } + + test {ZLEXCOUNT on skiplist-encoded set} { + set original_max [lindex [r config get zset-max-ziplist-entries] 1] + r config set 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"] + + r config set zset-max-ziplist-entries $original_max + } +} From bb7d41daa74e921c279127a31c04da5b9d93b5a1 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Tue, 19 May 2026 23:20:50 +0000 Subject: [PATCH 12/45] tests: add cluster:skip tag to skiplist integration tests The ZUNIONSTORE/ZINTERSTORE tests use multi-key operations (DEL src1 src2 dst, ZUNIONSTORE dst 2 src1 src2) that hit different hash slots in cluster mode, causing CROSSSLOT errors. Skip these tests in external-cluster CI. Signed-off-by: Rain Valentine --- tests/unit/type/zset.tcl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/type/zset.tcl b/tests/unit/type/zset.tcl index 74aadf55dbd..6a0fd88e754 100644 --- a/tests/unit/type/zset.tcl +++ b/tests/unit/type/zset.tcl @@ -2950,7 +2950,7 @@ start_server [list overrides [list save ""] tags {"zset needs:debug external:ski } } -start_server {tags {"zset"}} { +start_server {tags {"zset" "cluster:skip"}} { test {ZUNIONSTORE with skiplist-encoded inputs} { set original_max [lindex [r config get zset-max-ziplist-entries] 1] r config set zset-max-ziplist-entries 0 From 8b9797c4997e16ddf673183f3a30bb5d56c3e559 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Tue, 26 May 2026 22:02:39 +0000 Subject: [PATCH 13/45] zset: fix sds assumptions on opaque OrderedIndex elements orderedIndexGetElementRaw returns an opaque pointer+length pair. Several call sites incorrectly cast the result to sds and called sdslen() on it, violating the interface abstraction. Fix all category 1 violations (pure abstraction correctness): t_zset.c: - zsetTypeRandomElement: use ele_len_tmp directly - zsetHashtableGetMaxElementLength: use ele_len_tmp directly - ZUNION element tracking: use ele_len_tmp directly - ZRANDMEMBER reply (2 sites): use addReplyBulkCBuffer(c, ptr, len) db.c: - hashtableScanCallback: track zset_ele_len for pattern matching, use sdsnewlen(ptr, len) instead of sdsdup((sds)ptr) for the copy Signed-off-by: Rain Valentine --- src/db.c | 15 ++++++++------- src/t_zset.c | 24 +++++++++--------------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/db.c b/src/db.c index dc5695015e0..c3f3009a0b4 100644 --- a/src/db.c +++ b/src/db.c @@ -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,11 +1066,8 @@ void hashtableScanCallback(void *privdata, void *entry) { if (o->type == OBJ_SET) { key = (sds)entry; } else if (o->type == OBJ_ZSET) { - const char *ptr; - size_t ele_len; - orderedIndexGetElementRaw((const OrderedIndexItem *)entry, &ptr, &ele_len); - key = (sds)ptr; - /* 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) { @@ -1080,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; } } @@ -1092,7 +1093,7 @@ void hashtableScanCallback(void *privdata, void *entry) { const char *ptr; size_t ele_len; orderedIndexGetElementRaw((const OrderedIndexItem *)entry, &ptr, &ele_len); - key = sdsdup((sds)ptr); + key = sdsnewlen(ptr, ele_len); if (!data->only_keys) { char buf[MAX_LONG_DOUBLE_CHARS]; int len = ld2string(buf, sizeof(buf), orderedIndexGetScore((const OrderedIndexItem *)entry), LD_STR_AUTO); diff --git a/src/t_zset.c b/src/t_zset.c index e5acc2049d5..95b15bd8e88 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -1088,9 +1088,8 @@ static void zsetTypeRandomElement(robj *zsetobj, unsigned long zsetsize, listpac const char *ele_ptr_tmp; size_t ele_len_tmp; orderedIndexGetElementRaw(node, &ele_ptr_tmp, &ele_len_tmp); - sds ele = (sds)ele_ptr_tmp; - key->sval = (unsigned char *)ele; - key->slen = sdslen(ele); + 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; @@ -1745,10 +1744,8 @@ static size_t zsetHashtableGetMaxElementLength(hashtable *ht, size_t *totallen) const char *ele_ptr_tmp; size_t ele_len_tmp; orderedIndexGetElementRaw(node, &ele_ptr_tmp, &ele_len_tmp); - sds ele = (sds)ele_ptr_tmp; - size_t elelen = sdslen(ele); - if (elelen > maxelelen) maxelelen = elelen; - if (totallen) (*totallen) += elelen; + if (ele_len_tmp > maxelelen) maxelelen = ele_len_tmp; + if (totallen) (*totallen) += ele_len_tmp; } hashtableCleanupIterator(&iter); @@ -2141,10 +2138,9 @@ static void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIn const char *ele_ptr_tmp; size_t ele_len_tmp; orderedIndexGetElementRaw(new_node, &ele_ptr_tmp, &ele_len_tmp); - sds ele = (sds)ele_ptr_tmp; - totelelen += sdslen(ele); - if (sdslen(ele) > maxelelen) { - maxelelen = sdslen(ele); + totelelen += ele_len_tmp; + if (ele_len_tmp > maxelelen) { + maxelelen = ele_len_tmp; } } else { /* Update the score with the score of the new instance @@ -3491,8 +3487,7 @@ void zrandmemberWithCountCommand(client *c, long l, int withscores) { const char *ele_ptr_tmp; size_t ele_len_tmp; orderedIndexGetElementRaw(node, &ele_ptr_tmp, &ele_len_tmp); - sds ele = (sds)ele_ptr_tmp; - addReplyBulkCBuffer(c, ele, sdslen(ele)); + addReplyBulkCBuffer(c, ele_ptr_tmp, ele_len_tmp); if (withscores) addReplyDouble(c, orderedIndexGetScore(node)); if (c->flag.close_asap) break; } @@ -3607,9 +3602,8 @@ void zrandmemberWithCountCommand(client *c, long l, int withscores) { const char *key_ptr_tmp; size_t key_len_tmp; orderedIndexGetElementRaw(node, &key_ptr_tmp, &key_len_tmp); - sds key = (sds)key_ptr_tmp; if (withscores && c->resp > 2) addReplyArrayLen(c, 2); - addReplyBulkCBuffer(c, key, sdslen(key)); + addReplyBulkCBuffer(c, key_ptr_tmp, key_len_tmp); if (withscores) addReplyDouble(c, orderedIndexGetScore(node)); } From 7494ab01547bc7d7d3a6b814f73e55dc5b68ff21 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Tue, 26 May 2026 22:46:18 +0000 Subject: [PATCH 14/45] defrag: use orderedIndexScanDefrag instead of raw skiplist access Replace the skiplist-internal defrag code (activeDefragZsetNode, which manually walked skiplist levels to relink pointers) with the interface method orderedIndexScanDefrag. The backend handles node relocation internally; the callback just updates the hashtable pointer via hashtableReplaceReallocatedEntry. This removes the last skiplist dependency from defrag.c: - Remove #include "skiplist.h" - Remove zslUpdateNode helper - Remove activeDefragZsetNode (40 lines of skiplist internals) - Replace with 5-line defragZsetNodeCallback using the interface Signed-off-by: Rain Valentine --- src/defrag.c | 71 ++++++---------------------------------------------- 1 file changed, 8 insertions(+), 63 deletions(-) diff --git a/src/defrag.c b/src/defrag.c index 3098b4377bf..d301ecc57a2 100644 --- a/src/defrag.c +++ b/src/defrag.c @@ -39,7 +39,6 @@ */ #include "server.h" -#include "skiplist.h" #include "ordered_index.h" #include "hashtable.h" #include "eval.h" @@ -238,61 +237,12 @@ 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 = (zskiplist *)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; + hashtableReplaceReallocatedEntry(ht, old_item, new_item); + server.stat_active_defrag_scanned++; } #define DEFRAG_SDS_DICT_NO_VAL 0 @@ -435,15 +385,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->oi, 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 @@ -498,7 +443,7 @@ static void defragZsetSkiplist(robj *ob) { else { unsigned long cursor = 0; do { - cursor = hashtableScanDefrag(zs->ht, cursor, activeDefragZsetNode, zs->oi, activeDefragAlloc, HASHTABLE_SCAN_EMIT_REF); + cursor = orderedIndexScanDefrag(zs->oi, cursor, defragZsetNodeCallback, zs->ht, activeDefragAlloc); } while (cursor != 0); } } From add9eeac03b8cc1061ef409b93107d0ce8ce9e35 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Tue, 26 May 2026 23:00:08 +0000 Subject: [PATCH 15/45] server.h: remove dead zskiplist forward declaration No code outside skiplist.c/skiplist_ordered_index.c references zskiplist directly anymore. The forward declaration in server.h is dead code. Signed-off-by: Rain Valentine --- src/server.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server.h b/src/server.h index e4b4a01f880..97723bc6915 100644 --- a/src/server.h +++ b/src/server.h @@ -1488,8 +1488,7 @@ struct sharedObjectsStruct { sds minstring, maxstring; }; -/* Skiplist types - full definitions in skiplist.h */ -struct zskiplist; +/* OrderedIndex types - full definitions in ordered_index.h */ /* OrderedIndex - full definition in ordered_index.h */ typedef struct OrderedIndex OrderedIndex; From f14e3db4b5e17a4bda0cada3fe0145a0620691d5 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Tue, 26 May 2026 23:04:57 +0000 Subject: [PATCH 16/45] skiplist: change zslNext/zslPrev to return pointer directly Match the orderedIndexNext/Prev interface convention: return the node pointer (NULL = end) instead of bool + out-param. This simplifies the adapter to a direct cast. Signed-off-by: Rain Valentine --- src/skiplist.c | 28 ++++++++++------------------ src/skiplist.h | 4 ++-- src/skiplist_ordered_index.c | 6 ++---- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/skiplist.c b/src/skiplist.c index 5a4a66577a1..f78c13c327c 100644 --- a/src/skiplist.c +++ b/src/skiplist.c @@ -717,48 +717,40 @@ void zslReleaseIterator(zslIter *iter) { zfree(iter); } -bool zslNext(zslIter *iter, zskiplistNode **nodeptr) { +zskiplistNode *zslNext(zslIter *iter) { if (iter->zsl == NULL) { - *nodeptr = NULL; - return false; + return NULL; } if (iter->node == NULL) { iter->node = zslGetHeader(iter->zsl)->level[0].forward; } else if (iter->node == zslGetTail(iter->zsl)) { - *nodeptr = NULL; - return false; + return NULL; } else { iter->node = iter->node->level[0].forward; } if (iter->node == NULL) { - *nodeptr = NULL; - return false; + return NULL; } - *nodeptr = iter->node; - return true; + return iter->node; } -bool zslPrev(zslIter *iter, zskiplistNode **nodeptr) { +zskiplistNode *zslPrev(zslIter *iter) { if (iter->zsl == NULL) { - *nodeptr = NULL; - return false; + return NULL; } if (iter->node == zslGetHeader(iter->zsl)) { - *nodeptr = NULL; - return false; + return NULL; } if (iter->node == NULL) { iter->node = zslGetTail(iter->zsl); if (iter->node == NULL) { - *nodeptr = NULL; - return false; + return NULL; } } zskiplistNode *ret = iter->node; iter->node = iter->node->backward; if (iter->node == NULL) iter->node = zslGetHeader(iter->zsl); - *nodeptr = ret; - return true; + return ret; } void zslSeekToRank(zslIter *iter, unsigned long rank) { diff --git a/src/skiplist.h b/src/skiplist.h index f3673e13855..8f35dd9c5f9 100644 --- a/src/skiplist.h +++ b/src/skiplist.h @@ -172,8 +172,8 @@ void zslInitIterator(zslIter *iter, zskiplist *zsl); void zslResetIterator(zslIter *iter); zslIter *zslCreateIterator(zskiplist *zsl); void zslReleaseIterator(zslIter *iter); -bool zslNext(zslIter *iter, zskiplistNode **nodeptr); -bool zslPrev(zslIter *iter, zskiplistNode **nodeptr); +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); diff --git a/src/skiplist_ordered_index.c b/src/skiplist_ordered_index.c index 8a8f6f30fc2..55528eecda9 100644 --- a/src/skiplist_ordered_index.c +++ b/src/skiplist_ordered_index.c @@ -255,13 +255,11 @@ void skiplistResetIterator(OrderedIndexIterator *iter) { } OrderedIndexItem *skiplistNext(OrderedIndexIterator *iter) { - zskiplistNode *node; - return zslNext((zslIter *)iter, &node) ? (OrderedIndexItem *)node : NULL; + return (OrderedIndexItem *)zslNext((zslIter *)iter); } OrderedIndexItem *skiplistPrev(OrderedIndexIterator *iter) { - zskiplistNode *node; - return zslPrev((zslIter *)iter, &node) ? (OrderedIndexItem *)node : NULL; + return (OrderedIndexItem *)zslPrev((zslIter *)iter); } void skiplistSeekToRank(OrderedIndexIterator *iter, unsigned long rank) { From a8fd1c53c1ae5ffc0fb3c686bb8c6cf7c952d102 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Tue, 26 May 2026 23:11:10 +0000 Subject: [PATCH 17/45] debug: use skiplistGetHeight instead of reaching into opaque struct Replace the raw oi->level access (which dereferences an opaque type) with a call to skiplistGetHeight via a local extern declaration. This makes the dependency explicit and compiles correctly regardless of UNSAFE_CRASH_REPORT. Signed-off-by: Rain Valentine --- src/debug.c | 7 +++++-- src/ordered_index.c | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/debug.c b/src/debug.c index 64794b91c8b..fe1d1c893c8 100644 --- a/src/debug.c +++ b/src/debug.c @@ -1186,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)->oi->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/ordered_index.c b/src/ordered_index.c index 5996461067c..fa50cbe8af9 100644 --- a/src/ordered_index.c +++ b/src/ordered_index.c @@ -148,3 +148,8 @@ OrderedIndex *orderedIndexDefragInternals(OrderedIndex *oi, void *(*defragfn)(vo 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); +} From 2802d206dd7dca6d7046493ac1939ef69684d658 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Tue, 26 May 2026 23:16:37 +0000 Subject: [PATCH 18/45] skiplist.h: remove unused stdbool.h include No bool types remain in the header after zslNext/zslPrev signature change. Signed-off-by: Rain Valentine --- src/skiplist.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/skiplist.h b/src/skiplist.h index 8f35dd9c5f9..f4f176e57bb 100644 --- a/src/skiplist.h +++ b/src/skiplist.h @@ -37,7 +37,6 @@ #define SKIPLIST_H #include "server.h" -#include /* * This skiplist implementation is almost a C translation of the original From 5a4fbbd74a8dcb66b900f44e168af615fcdd4902 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Tue, 26 May 2026 23:24:05 +0000 Subject: [PATCH 19/45] geo: simplify score range iteration to plain while loop Remove the early NULL check and do-while pattern. A plain while loop naturally short-circuits when orderedIndexNext returns NULL, producing the same return value (0) with less code. Signed-off-by: Rain Valentine --- src/geo.c | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/geo.c b/src/geo.c index 8f52254e91a..8d9f0eef237 100644 --- a/src/geo.c +++ b/src/geo.c @@ -314,12 +314,7 @@ int geoGetPointsInRange(robj *zobj, double min, double max, GeoShape *shape, geo orderedIndexSeekToScoreRange(&iter, range.min, range.max, range.minex, range.maxex, 0); OrderedIndexItem *ln; - if ((ln = orderedIndexNext(&iter)) == NULL) { - /* Nothing exists starting at our min. No results. */ - return 0; - } - - do { + while ((ln = orderedIndexNext(&iter)) != NULL) { double xy[2]; double distance = 0; double score = orderedIndexGetScore(ln); @@ -333,7 +328,7 @@ int geoGetPointsInRange(robj *zobj, double min, double max, GeoShape *shape, geo geoArrayAppend(ga, xy, distance, score, sdsnewlen(ele, ele_len)); } if (ga->used && limit && ga->used >= limit) break; - } while ((ln = orderedIndexNext(&iter)) != NULL); + } } return ga->used - origincount; } From e4b55229f4fc6460987580e60bcd2e3ee37d3b67 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Tue, 26 May 2026 23:35:30 +0000 Subject: [PATCH 20/45] server.c: fix stale skiplist comment in zsetHashtableType Signed-off-by: Rain Valentine --- src/server.c | 2 +- src/server.h | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/server.c b/src/server.c index f859e0b348c..f8e97d164c9 100644 --- a/src/server.c +++ b/src/server.c @@ -633,7 +633,7 @@ const void *zsetHashtableGetKey(const void *element) { return ptr; } -/* Sorted sets hash (note: a skiplist is used in addition to the hash table) */ +/* Sorted sets hash (an ordered index is used in addition to the hash table) */ hashtableType zsetHashtableType = { .hashFunction = sdsHashConfigurableSeed, .entryGetKey = zsetHashtableGetKey, diff --git a/src/server.h b/src/server.h index 97723bc6915..5559aea46ae 100644 --- a/src/server.h +++ b/src/server.h @@ -1488,8 +1488,6 @@ struct sharedObjectsStruct { sds minstring, maxstring; }; -/* OrderedIndex types - full definitions in ordered_index.h */ - /* OrderedIndex - full definition in ordered_index.h */ typedef struct OrderedIndex OrderedIndex; From 9da40894fbd34e395452632ce8ea5adab4774d8a Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Tue, 26 May 2026 23:47:29 +0000 Subject: [PATCH 21/45] t_zset.c: update stale skiplist references in comments Replace all comment references to 'skiplist' with 'ordered index' to reflect the current abstraction. The skiplist.h include remains because t_zset.c uses range utility functions (zslParseRange, zslValueGteMin, etc.) that operate on zrangespec structs. Signed-off-by: Rain Valentine --- src/t_zset.c | 61 +++++++++++++++++++++------------------------------- 1 file changed, 25 insertions(+), 36 deletions(-) diff --git a/src/t_zset.c b/src/t_zset.c index 95b15bd8e88..603ab68e393 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -40,25 +40,15 @@ * 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 orderedIndexFreeItem(). 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" @@ -568,7 +558,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; @@ -621,9 +611,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)); } @@ -680,7 +670,7 @@ void zsetConvertAndExpand(robj *zobj, int encoding, unsigned long cap) { if (encoding != OBJ_ENCODING_LISTPACK) serverPanic("Unknown target encoding"); - /* Free the skiplist by popping items one at a time into the listpack. */ + /* Free the ordered index by popping items one at a time into the listpack. */ zs = objectGetVal(zobj); hashtableRelease(zs->ht); @@ -777,7 +767,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': * @@ -853,7 +843,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); @@ -910,7 +900,7 @@ 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) { @@ -918,9 +908,9 @@ static int zsetRemoveFromSkiplist(zset *zs, sds ele) { if (!hashtablePop(zs->ht, ele, &entry)) return 0; 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. */ + /* Delete from ordered index. */ orderedIndexDelete(zs->oi, node); return 1; @@ -1038,12 +1028,11 @@ robj *zsetDup(robj *o) { OrderedIndexItem *ln; 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)). */ + /* We copy elements from the greatest to the smallest (that's trivial + * since the elements are already ordered in the index): this improves + * the load process, since the next loaded element will always be the + * smallest, so adding to the ordered index will always immediately + * stop at the head, making the insertion O(1) instead of O(log(N)). */ OrderedIndexIterator iter; orderedIndexInitIterator(&iter, oi); orderedIndexSeekToRank(&iter, orderedIndexLength(oi)); @@ -2106,8 +2095,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. */ From f8aeac501224e8acd8cd58f8049ebf6b59eca86c Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Wed, 27 May 2026 00:04:04 +0000 Subject: [PATCH 22/45] zset: relocate range comparison utils from skiplist to t_zset Move zset range comparison functions out of skiplist.c into t_zset.c and rename with zset prefix since they are generic utilities used by both listpack and ordered index encodings: zslValueGteMin -> zsetScoreGteMin zslValueLteMax -> zsetScoreLteMax sdscmplex -> zsetLexCompare zslLexValueGteMin -> zsetLexGteMin zslLexValueLteMax -> zsetLexLteMax Declarations move from skiplist.h to server.h. This removes the need for t_zset.c, geo.c, and module.c to include skiplist.h. Signed-off-by: Rain Valentine --- src/geo.c | 6 ++-- src/module.c | 13 ++++--- src/server.h | 7 ++++ src/skiplist.c | 64 ++++++++++----------------------- src/skiplist.h | 5 --- src/skiplist_ordered_index.c | 8 ++--- src/t_zset.c | 68 ++++++++++++++++++++++++++---------- 7 files changed, 87 insertions(+), 84 deletions(-) diff --git a/src/geo.c b/src/geo.c index 8d9f0eef237..fc26794af22 100644 --- a/src/geo.c +++ b/src/geo.c @@ -29,7 +29,6 @@ */ #include "geo.h" -#include "skiplist.h" #include "ordered_index.h" #include "geohash_helper.h" #include "debugmacro.h" @@ -38,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: @@ -296,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) { @@ -319,7 +317,7 @@ int geoGetPointsInRange(robj *zobj, double min, double max, GeoShape *shape, geo double distance = 0; double score = orderedIndexGetScore(ln); /* Abort when the node is no longer in range. */ - if (!zslValueLteMax(score, &range)) break; + if (!zsetScoreLteMax(score, &range)) break; if (geoWithinShape(shape, score, xy, &distance) == C_OK) { /* Append the new element. */ const char *ele; diff --git a/src/module.c b/src/module.c index ef871a51957..a487e2c3f37 100644 --- a/src/module.c +++ b/src/module.c @@ -56,7 +56,6 @@ * 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" @@ -5288,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; } @@ -5309,14 +5308,14 @@ int VM_ZsetRangeNext(ValkeyModuleKey *key) { return 0; } else { /* Are we still within the range? */ - if (key->u.zset.type == VALKEYMODULE_ZSET_RANGE_SCORE && !zslValueLteMax(orderedIndexGetScore(next), &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) { const char *ele; size_t ele_len; orderedIndexGetElementRaw(next, &ele, &ele_len); - if (!zslLexValueLteMax((sds)ele, &key->u.zset.lrs)) { + if (!zsetLexLteMax((sds)ele, &key->u.zset.lrs)) { key->u.zset.er = 1; return 0; } @@ -5353,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; } @@ -5374,14 +5373,14 @@ int VM_ZsetRangePrev(ValkeyModuleKey *key) { return 0; } else { /* Are we still within the range? */ - if (key->u.zset.type == VALKEYMODULE_ZSET_RANGE_SCORE && !zslValueGteMin(orderedIndexGetScore(prev), &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) { const char *ele; size_t ele_len; orderedIndexGetElementRaw(prev, &ele, &ele_len); - if (!zslLexValueGteMin((sds)ele, &key->u.zset.lrs)) { + if (!zsetLexGteMin((sds)ele, &key->u.zset.lrs)) { key->u.zset.er = 1; return 0; } diff --git a/src/server.h b/src/server.h index 5559aea46ae..e46a69f39ed 100644 --- a/src/server.h +++ b/src/server.h @@ -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(sds a, sds b); +int zsetLexGteMin(sds value, zlexrangespec *spec); +int zsetLexLteMax(sds value, 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 f78c13c327c..39cbd7f2b10 100644 --- a/src/skiplist.c +++ b/src/skiplist.c @@ -310,14 +310,6 @@ zskiplistNode *zslUpdateScore(zskiplist *zsl, zskiplistNode *node, double newsco 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,7 +421,7 @@ 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; zslUnlinkNode(zsl, x, update); sds ele = zslGetNodeElement(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), range)) { x = x->level[i].forward; } update[i] = x; @@ -460,7 +452,7 @@ 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), range)) { zskiplistNode *next = x->level[0].forward; zslUnlinkNode(zsl, x, update); hashtableDelete(ht, zslGetNodeElement(x)); @@ -557,40 +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, 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 (!zslLexValueGteMin(ele, range)) return 0; + if (!zsetLexGteMin(ele, range)) return 0; zskiplistNode *zheader = zslGetHeader(zsl); x = zheader->level[0].forward; if (x == NULL) return 0; ele = zslGetNodeElement(x); - if (!zslLexValueLteMax(ele, range)) return 0; + if (!zsetLexLteMax(ele, range)) return 0; return 1; } @@ -611,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), range)) { edge_rank += zslGetNodeSpanAtLevel(x, i); x = x->level[i].forward; } @@ -622,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), range)) { /* Count the rank of the last element smaller than the range. */ edge_rank += zslGetNodeSpanAtLevel(x, i); x = x->level[i].forward; @@ -642,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), 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), range)) { /* Count the rank of the last element in range. */ edge_rank += zslGetNodeSpanAtLevel(x, i); x = x->level[i].forward; @@ -666,7 +640,7 @@ 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), range)) return NULL; } return x; diff --git a/src/skiplist.h b/src/skiplist.h index f4f176e57bb..a3a77b081fd 100644 --- a/src/skiplist.h +++ b/src/skiplist.h @@ -148,17 +148,12 @@ 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 */ diff --git a/src/skiplist_ordered_index.c b/src/skiplist_ordered_index.c index 55528eecda9..bc16f6e7197 100644 --- a/src/skiplist_ordered_index.c +++ b/src/skiplist_ordered_index.c @@ -91,7 +91,7 @@ unsigned long skiplistDeleteRangeByScore(OrderedIndex *oi, double min, double ma x = zslGetHeader(zsl); for (i = zslGetHeight(zsl) - 1; i >= 0; i--) { - while (x->level[i].forward && !zslValueGteMin(x->level[i].forward->score, &range)) + while (x->level[i].forward && !zsetScoreGteMin(x->level[i].forward->score, &range)) x = x->level[i].forward; update[i] = x; } @@ -100,7 +100,7 @@ unsigned long skiplistDeleteRangeByScore(OrderedIndex *oi, double min, double ma 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; zslUnlinkNode(zsl, x, update); if (on_delete) { @@ -159,7 +159,7 @@ unsigned long skiplistDeleteRangeByLex(OrderedIndex *oi, const_sds min, const_sd for (i = zslGetHeight(zsl) - 1; i >= 0; i--) { while (x->level[i].forward) { sds fwd_ele = zslGetNodeElement(x->level[i].forward); - if (zslLexValueGteMin(fwd_ele, &range)) break; + if (zsetLexGteMin(fwd_ele, &range)) break; x = x->level[i].forward; } update[i] = x; @@ -171,7 +171,7 @@ unsigned long skiplistDeleteRangeByLex(OrderedIndex *oi, const_sds min, const_sd /* Delete nodes while in range. */ while (x) { sds ele = zslGetNodeElement(x); - if (!zslLexValueLteMax(ele, &range)) break; + if (!zsetLexLteMax(ele, &range)) break; zskiplistNode *next = x->level[0].forward; zslUnlinkNode(zsl, x, update); if (on_delete) { diff --git a/src/t_zset.c b/src/t_zset.c index 603ab68e393..37138e6b0e0 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -51,7 +51,6 @@ * the ordered index. */ #include "server.h" -#include "skiplist.h" #include "ordered_index.h" #include "intset.h" /* Compact integer set structure */ #include "mt19937-64.h" @@ -59,6 +58,37 @@ #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. */ +int zsetLexCompare(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 zsetLexGteMin(sds value, zlexrangespec *spec) { + return spec->minex ? (zsetLexCompare(value, spec->min) > 0) : (zsetLexCompare(value, spec->min) >= 0); +} + +int zsetLexLteMax(sds value, zlexrangespec *spec) { + return spec->maxex ? (zsetLexCompare(value, spec->max) < 0) : (zsetLexCompare(value, spec->max) <= 0); +} + void zsetConvertAndExpand(robj *zobj, int encoding, unsigned long cap); static int zslParseRange(robj *min, robj *max, zrangespec *spec) { @@ -269,12 +299,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; } @@ -293,9 +323,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; } @@ -320,9 +350,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; } @@ -340,14 +370,14 @@ unsigned char *zzlLastInRange(unsigned char *zl, zrangespec *range) { int zzlLexValueGteMin(unsigned char *p, zlexrangespec *spec) { sds value = lpGetObject(p); - int res = zslLexValueGteMin(value, spec); + int res = zsetLexGteMin(value, spec); sdsfree(value); return res; } int zzlLexValueLteMax(unsigned char *p, zlexrangespec *spec) { sds value = lpGetObject(p); - int res = zslLexValueLteMax(value, spec); + int res = zsetLexLteMax(value, spec); sdsfree(value); return res; } @@ -358,7 +388,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, range->max); if (cmp > 0 || (cmp == 0 && (range->minex || range->maxex))) return 0; p = lpSeek(zl, -2); /* Last element. */ @@ -519,7 +549,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++; @@ -2583,9 +2613,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); @@ -2618,9 +2648,9 @@ void genericZrangebyscoreCommand(zrange_result_handler *handler, if (ln == NULL) break; /* Abort when the node is no longer in range. */ if (reverse) { - if (!zslValueGteMin(orderedIndexGetScore(ln), range)) break; + if (!zsetScoreGteMin(orderedIndexGetScore(ln), range)) break; } else { - if (!zslValueLteMax(orderedIndexGetScore(ln), range)) break; + if (!zsetScoreLteMax(orderedIndexGetScore(ln), range)) break; } rangelen++; @@ -2682,14 +2712,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++; @@ -2852,12 +2882,12 @@ void genericZrangebylexCommand(zrange_result_handler *handler, orderedIndexGetElementRaw(ln, &ele_ptr, &ele_len); sds ele = sdsnewlen(ele_ptr, ele_len); if (reverse) { - if (!zslLexValueGteMin(ele, range)) { + if (!zsetLexGteMin(ele, range)) { sdsfree(ele); break; } } else { - if (!zslLexValueLteMax(ele, range)) { + if (!zsetLexLteMax(ele, range)) { sdsfree(ele); break; } From 8ac098e3404ca5cc915a30665d195d5ce5f4e26b Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Wed, 27 May 2026 00:30:08 +0000 Subject: [PATCH 23/45] skiplist_ordered_index: use OrderedIndexDefragCallback typedef Match the declaration in the header file. Signed-off-by: Rain Valentine --- src/skiplist_ordered_index.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/skiplist_ordered_index.c b/src/skiplist_ordered_index.c index bc16f6e7197..117c4247a05 100644 --- a/src/skiplist_ordered_index.c +++ b/src/skiplist_ordered_index.c @@ -334,7 +334,7 @@ static void skiplistPatchNodePointers(zskiplist *zsl, zskiplistNode *oldnode, zs * * 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, void (*callback)(OrderedIndexItem *old_item, OrderedIndexItem *new_item, void *ctx), void *ctx, void *(*defragfn)(void *)) { +unsigned long skiplistScanDefrag(OrderedIndex *oi, unsigned long cursor, OrderedIndexDefragCallback callback, void *ctx, void *(*defragfn)(void *)) { zskiplist *zsl = (zskiplist *)oi; zskiplistNode *header = zslGetHeader(zsl); From 15a2bfeca9f24c1793ae3e12fb21303ee95261bc Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Wed, 27 May 2026 00:35:42 +0000 Subject: [PATCH 24/45] t_zset: change zzlInsertAt to accept ptr+len instead of sds Eliminates a temporary sds allocation+free in the ordered index to listpack conversion path. The function only needs a pointer and length for lpAppend/lpInsertString. Signed-off-by: Rain Valentine --- src/t_zset.c | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/t_zset.c b/src/t_zset.c index 37138e6b0e0..7f8c0b63017 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -476,7 +476,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; @@ -484,14 +484,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) @@ -517,12 +517,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; } } @@ -532,7 +532,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; } @@ -709,9 +709,7 @@ void zsetConvertAndExpand(robj *zobj, int encoding, unsigned long cap) { const char *ele_ptr; size_t ele_len; orderedIndexGetElementRaw(node, &ele_ptr, &ele_len); - sds ele = sdsnewlen(ele_ptr, ele_len); - zl = zzlInsertAt(zl, NULL, ele, orderedIndexGetScore(node)); - sdsfree(ele); + zl = zzlInsertAt(zl, NULL, ele_ptr, ele_len, orderedIndexGetScore(node)); orderedIndexFreeItem(node); } orderedIndexFree(zs->oi); @@ -1056,18 +1054,13 @@ robj *zsetDup(robj *o) { hashtableExpand(new_zs->ht, hashtableSize(zs->ht)); OrderedIndex *oi = zs->oi; OrderedIndexItem *ln; - long llen = zsetLength(o); /* We copy elements from the greatest to the smallest (that's trivial - * since the elements are already ordered in the index): this improves - * the load process, since the next loaded element will always be the - * smallest, so adding to the ordered index will always immediately - * stop at the head, making the insertion O(1) instead of O(log(N)). */ + * 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); - orderedIndexSeekToRank(&iter, orderedIndexLength(oi)); - while (llen--) { - ln = orderedIndexPrev(&iter); + while ((ln = orderedIndexPrev(&iter)) != NULL) { const char *ele_ptr; size_t ele_len; orderedIndexGetElementRaw(ln, &ele_ptr, &ele_len); From 81e69f78fcb890333fec7a8647ed54be97f5283c Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Wed, 27 May 2026 00:57:40 +0000 Subject: [PATCH 25/45] t_zset: eliminate temp sds in listpack to ordered index conversion Use a stack buffer with ll2string for integers and pass vstr/vlen directly for strings, avoiding an sds allocation+free per element during encoding conversion. Signed-off-by: Rain Valentine --- src/t_zset.c | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/t_zset.c b/src/t_zset.c index 7f8c0b63017..4863faedd7d 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -681,13 +681,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 = orderedIndexInsert(zs->oi, score, ele, sdslen(ele)); - sdsfree(ele); + node = orderedIndexInsert(zs->oi, score, ele_ptr, ele_len); serverAssert(hashtableAdd(zs->ht, node)); zzlNext(zl, &eptr, &sptr); } From 083712c9682186401cb3830480a53e1b9c00140d Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Wed, 27 May 2026 01:01:01 +0000 Subject: [PATCH 26/45] t_zset: move misplaced ZREMRANGE comment to correct function Signed-off-by: Rain Valentine --- src/t_zset.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/t_zset.c b/src/t_zset.c index 4863faedd7d..6c1f12264e0 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -1305,7 +1305,6 @@ typedef enum { ZRANGE_LEX, } zrange_type; -/* Implements ZREMRANGEBYRANK, ZREMRANGEBYSCORE, ZREMRANGEBYLEX commands. */ /* 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) { @@ -1317,6 +1316,7 @@ static void zsetIndexDeleteCallback(OrderedIndexItem *item, void *ctx) { orderedIndexFreeItem(item); } +/* Implements ZREMRANGEBYRANK, ZREMRANGEBYSCORE, ZREMRANGEBYLEX commands. */ void zremrangeGenericCommand(client *c, zrange_type rangetype) { robj *key = c->argv[1]; robj *zobj; From 77a02e4896b9f33d8654ef73b179ea6387995bce Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Wed, 27 May 2026 01:54:14 +0000 Subject: [PATCH 27/45] zset: refactor lex comparison to accept ptr+len Change zsetLexCompare, zsetLexGteMin, and zsetLexLteMax to accept (const char *value, size_t len) instead of sds. This eliminates temporary sds allocations in: - genericZrangebylexCommand (ordered index path) - zzlLexValueGteMin/LteMax (listpack path, now uses lpGetValue directly) - module.c ZsetRangeNext/Prev lex bounds (removes (sds) cast on opaque ptr) The shared.minstring/maxstring sentinels are handled via pointer comparison on both sides. Signed-off-by: Rain Valentine --- src/module.c | 4 +-- src/server.h | 6 ++-- src/skiplist.c | 20 ++++++------ src/skiplist_ordered_index.c | 4 +-- src/t_zset.c | 60 ++++++++++++++++++++---------------- 5 files changed, 51 insertions(+), 43 deletions(-) diff --git a/src/module.c b/src/module.c index a487e2c3f37..cb8281dc6da 100644 --- a/src/module.c +++ b/src/module.c @@ -5315,7 +5315,7 @@ int VM_ZsetRangeNext(ValkeyModuleKey *key) { const char *ele; size_t ele_len; orderedIndexGetElementRaw(next, &ele, &ele_len); - if (!zsetLexLteMax((sds)ele, &key->u.zset.lrs)) { + if (!zsetLexLteMax(ele, ele_len, &key->u.zset.lrs)) { key->u.zset.er = 1; return 0; } @@ -5380,7 +5380,7 @@ int VM_ZsetRangePrev(ValkeyModuleKey *key) { const char *ele; size_t ele_len; orderedIndexGetElementRaw(prev, &ele, &ele_len); - if (!zsetLexGteMin((sds)ele, &key->u.zset.lrs)) { + if (!zsetLexGteMin(ele, ele_len, &key->u.zset.lrs)) { key->u.zset.er = 1; return 0; } diff --git a/src/server.h b/src/server.h index e46a69f39ed..6ba135e9b2c 100644 --- a/src/server.h +++ b/src/server.h @@ -3369,9 +3369,9 @@ typedef struct { /* 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(sds a, sds b); -int zsetLexGteMin(sds value, zlexrangespec *spec); -int zsetLexLteMax(sds value, zlexrangespec *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 */ diff --git a/src/skiplist.c b/src/skiplist.c index 39cbd7f2b10..c9c2cf28a07 100644 --- a/src/skiplist.c +++ b/src/skiplist.c @@ -442,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 && - !zsetLexGteMin(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; @@ -452,7 +452,7 @@ unsigned long zslDeleteRangeByLex(zskiplist *zsl, zlexrangespec *range, hashtabl x = x->level[0].forward; /* Delete nodes while in range. */ - while (x && zsetLexLteMax(zslGetNodeElement(x), range)) { + while (x && zsetLexLteMax(zslGetNodeElement(x), sdslen(zslGetNodeElement(x)), range)) { zskiplistNode *next = x->level[0].forward; zslUnlinkNode(zsl, x, update); hashtableDelete(ht, zslGetNodeElement(x)); @@ -554,17 +554,17 @@ static int zslIsInLexRange(zskiplist *zsl, zlexrangespec *range) { zskiplistNode *x; /* Test for ranges that will always be empty. */ - int cmp = zsetLexCompare(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 (!zsetLexGteMin(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 (!zsetLexLteMax(ele, range)) return 0; + if (!zsetLexLteMax(ele, sdslen(ele), range)) return 0; return 1; } @@ -585,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 && !zsetLexGteMin(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; } @@ -596,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 && !zsetLexGteMin(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; @@ -616,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 && !zsetLexLteMax(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 && zsetLexLteMax(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; @@ -640,7 +640,7 @@ 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 && !zsetLexGteMin(zslGetNodeElement(x), range)) return NULL; + if (x && !zsetLexGteMin(zslGetNodeElement(x), sdslen(zslGetNodeElement(x)), range)) return NULL; } return x; diff --git a/src/skiplist_ordered_index.c b/src/skiplist_ordered_index.c index 117c4247a05..14d43a3cb7b 100644 --- a/src/skiplist_ordered_index.c +++ b/src/skiplist_ordered_index.c @@ -159,7 +159,7 @@ unsigned long skiplistDeleteRangeByLex(OrderedIndex *oi, const_sds min, const_sd 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, &range)) break; + if (zsetLexGteMin(fwd_ele, sdslen(fwd_ele), &range)) break; x = x->level[i].forward; } update[i] = x; @@ -171,7 +171,7 @@ unsigned long skiplistDeleteRangeByLex(OrderedIndex *oi, const_sds min, const_sd /* Delete nodes while in range. */ while (x) { sds ele = zslGetNodeElement(x); - if (!zsetLexLteMax(ele, &range)) break; + if (!zsetLexLteMax(ele, sdslen(ele), &range)) break; zskiplistNode *next = x->level[0].forward; zslUnlinkNode(zsl, x, update); if (on_delete) { diff --git a/src/t_zset.c b/src/t_zset.c index 6c1f12264e0..25ece6d6c04 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -74,19 +74,23 @@ int zsetScoreLteMax(double value, zrangespec *spec) { } /* Compare two sds strings handling shared.minstring/maxstring as -inf/+inf. */ -int zsetLexCompare(sds a, sds b) { - if (a == b) return 0; +/* 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; - return sdscmp(a, b); + 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(sds value, zlexrangespec *spec) { - return spec->minex ? (zsetLexCompare(value, spec->min) > 0) : (zsetLexCompare(value, spec->min) >= 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(sds value, zlexrangespec *spec) { - return spec->maxex ? (zsetLexCompare(value, spec->max) < 0) : (zsetLexCompare(value, spec->max) <= 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); @@ -369,17 +373,29 @@ unsigned char *zzlLastInRange(unsigned char *zl, zrangespec *range) { } int zzlLexValueGteMin(unsigned char *p, zlexrangespec *spec) { - sds value = lpGetObject(p); - int res = zsetLexGteMin(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 = zsetLexLteMax(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 @@ -388,7 +404,7 @@ int zzlIsInLexRange(unsigned char *zl, zlexrangespec *range) { unsigned char *p; /* Test for ranges that will always be empty. */ - int cmp = zsetLexCompare(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. */ @@ -2878,22 +2894,14 @@ void genericZrangebylexCommand(zrange_result_handler *handler, const char *ele_ptr; size_t ele_len; orderedIndexGetElementRaw(ln, &ele_ptr, &ele_len); - sds ele = sdsnewlen(ele_ptr, ele_len); if (reverse) { - if (!zsetLexGteMin(ele, range)) { - sdsfree(ele); - break; - } + if (!zsetLexGteMin(ele_ptr, ele_len, range)) break; } else { - if (!zsetLexLteMax(ele, range)) { - sdsfree(ele); - break; - } + if (!zsetLexLteMax(ele_ptr, ele_len, range)) break; } rangelen++; handler->emitResultFromCBuffer(handler, ele_ptr, ele_len, orderedIndexGetScore(ln)); - sdsfree(ele); } } else { serverPanic("Unknown sorted set encoding"); From 9ef73da2abe6c24aa89c1c56b278ba81331f3e7e Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Wed, 27 May 2026 02:22:41 +0000 Subject: [PATCH 28/45] ci: fix unused variable, uninitialized warning, and clang-format - Remove unused 'sds ele' in zsetConvertAndExpand (leftover from refactoring to ptr+len) - Initialize score=0 to silence -Wmaybe-uninitialized on older GCC - Fix clang-format spacing in debug.c extern declaration Signed-off-by: Rain Valentine --- src/debug.c | 2 +- src/t_zset.c | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/debug.c b/src/debug.c index fe1d1c893c8..00cf6c5dec5 100644 --- a/src/debug.c +++ b/src/debug.c @@ -1188,7 +1188,7 @@ void serverLogObjectDebugInfo(const robj *o) { serverLog(LL_WARNING, "Sorted set size: %d", (int)zsetLength(o)); if (o->encoding == OBJ_ENCODING_SKIPLIST) { /* Not declared in ordered_index.h — debug-only introspection. */ - extern int orderedIndexGetDepth(OrderedIndex *oi); + extern int orderedIndexGetDepth(OrderedIndex * oi); serverLog(LL_WARNING, "Index depth: %d", orderedIndexGetDepth(((const zset *)o->ptr)->oi)); } } else if (o->type == OBJ_STREAM) { diff --git a/src/t_zset.c b/src/t_zset.c index 25ece6d6c04..183d5b3d33c 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -668,7 +668,6 @@ void zsetConvert(robj *zobj, int encoding) { void zsetConvertAndExpand(robj *zobj, int encoding, unsigned long cap) { zset *zs; OrderedIndexItem *node; - sds ele; double score; if (zobj->encoding == encoding) return; @@ -3648,7 +3647,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 From d80a7f3ad26c8c58a866e3f2798dd594c8e8d5f1 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Wed, 27 May 2026 02:31:58 +0000 Subject: [PATCH 29/45] tests: add fixture helpers and reduce boilerplate in ordered index tests Add SetUp/TearDown to all test fixtures (OrderedIndexTest, OnDeleteCallbackTest, RangeDeleteHashtableConsistencyTest) to manage OrderedIndex lifecycle automatically. Add helper methods to OrderedIndexTest: - insert(score, ele): insert string literal without manual sds - populateSequential(n): insert key0..keyN-1 at scores 0..N-1 - assertNextScore/assertPrevScore: one-line iterator assertions - assertElement: one-line element content check - verifyOI(): shorthand for integrity verification Net reduction: -62 lines (184 deleted, 122 added). Signed-off-by: Rain Valentine --- src/unit/test_ordered_index.cpp | 523 +++++++++++--------------------- 1 file changed, 174 insertions(+), 349 deletions(-) diff --git a/src/unit/test_ordered_index.cpp b/src/unit/test_ordered_index.cpp index ea4e8f302f1..b26b88e5924 100644 --- a/src/unit/test_ordered_index.cpp +++ b/src/unit/test_ordered_index.cpp @@ -46,15 +46,65 @@ static const double NEG_INF = (double)-INFINITY; class OrderedIndexTest : public ::testing::TestWithParam { protected: OrderedIndexTestApi &api = *GetParam(); + OrderedIndex *oi = nullptr; + + void SetUp() override { 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); + } + + /* Verify structural integrity. */ + void verifyOI() { ASSERT_TRUE(verifyIntegrity(api, oi)); } }; /* ========== Basic tests ========== */ TEST_P(OrderedIndexTest, CreateFree) { - OrderedIndex *oi = api.create(); TEST_ASSERT(oi != NULL); TEST_ASSERT(api.length(oi) == 0); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -62,23 +112,18 @@ TEST_P(OrderedIndexTest, CreateFree) { TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); - api.free(oi); } TEST_P(OrderedIndexTest, InsertSingle) { - OrderedIndex *oi = api.create(); sds ele = sdsnew("test"); OrderedIndexItem *node = api.insertSds(oi, 1.0, ele); - VERIFY_INTEGRITY(api, oi); + verifyOI(); TEST_ASSERT(node != NULL); TEST_ASSERT(api.length(oi) == 1); TEST_ASSERT_SCORE_EQ(api.getScore(node), 1.0); - const char *ptr; - size_t len; - api.getElementRaw(node, &ptr, &len); - TEST_ASSERT(len == 4 && memcmp(ptr, "test", 4) == 0); + assertElement(node, "test"); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -89,22 +134,14 @@ TEST_P(OrderedIndexTest, InsertSingle) { api.resetIterator(&iter); sdsfree(ele); - api.free(oi); } TEST_P(OrderedIndexTest, InsertMultipleOrdered) { - OrderedIndex *oi = api.create(); - for (int i = 0; i < 10; i++) { - char buf[32]; - snprintf(buf, sizeof(buf), "key%d", i); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); - } + populateSequential(10); TEST_ASSERT(api.length(oi) == 10); - VERIFY_INTEGRITY(api, oi); + verifyOI(); /* Verify forward traversal */ OrderedIndexIterator iter; @@ -126,11 +163,9 @@ TEST_P(OrderedIndexTest, InsertMultipleOrdered) { TEST_ASSERT((pos = api.prev(&iter)) == NULL); api.resetIterator(&iter); - api.free(oi); } TEST_P(OrderedIndexTest, DuplicateScores) { - OrderedIndex *oi = api.create(); for (int i = 0; i < 5; i++) { char buf[32]; @@ -141,7 +176,7 @@ TEST_P(OrderedIndexTest, DuplicateScores) { } TEST_ASSERT(api.length(oi) == 5); - VERIFY_INTEGRITY(api, oi); + verifyOI(); /* Verify lexicographic ordering for same scores */ OrderedIndexIterator iter; @@ -159,11 +194,9 @@ TEST_P(OrderedIndexTest, DuplicateScores) { } api.resetIterator(&iter); - api.free(oi); } TEST_P(OrderedIndexTest, RankOperations) { - OrderedIndex *oi = api.create(); OrderedIndexItem *nodes[10]; for (int i = 0; i < 10; i++) { @@ -173,7 +206,7 @@ TEST_P(OrderedIndexTest, RankOperations) { nodes[i] = api.insertSds(oi, (double)i, ele); sdsfree(ele); } - VERIFY_INTEGRITY(api, oi); + verifyOI(); for (int i = 0; i < 10; i++) { unsigned long rank = api.getRank(oi, nodes[i]); @@ -185,11 +218,9 @@ TEST_P(OrderedIndexTest, RankOperations) { TEST_ASSERT(node == nodes[i]); } - api.free(oi); } TEST_P(OrderedIndexTest, Delete) { - OrderedIndex *oi = api.create(); OrderedIndexItem *nodes[5]; for (int i = 0; i < 5; i++) { @@ -204,94 +235,67 @@ TEST_P(OrderedIndexTest, Delete) { api.deleteItem(oi, nodes[2]); TEST_ASSERT(api.length(oi) == 4); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexIterator iter; OrderedIndexItem *pos; api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 0.0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + assertNextScore(&iter, 0.0); + pos = assertNextScore(&iter, 1.0); TEST_ASSERT((pos = api.next(&iter)) != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(pos), 3.0); /* Skipped 2.0 */ - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); + assertNextScore(&iter, 4.0); api.resetIterator(&iter); - api.free(oi); } TEST_P(OrderedIndexTest, PopFirst) { - OrderedIndex *oi = api.create(); TEST_ASSERT(api.popFirst(oi) == NULL); - for (int i = 0; i < 5; i++) { - char buf[32]; - snprintf(buf, sizeof(buf), "key%d", i); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); - } + populateSequential(5); TEST_ASSERT(api.length(oi) == 5); OrderedIndexItem *item = api.popFirst(oi); TEST_ASSERT(item != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(item), 0.0); - const char *ptr; - size_t len; - api.getElementRaw(item, &ptr, &len); - TEST_ASSERT(len == 4 && memcmp(ptr, "key0", 4) == 0); + assertElement(item, "key0"); api.freeItem(item); TEST_ASSERT(api.length(oi) == 4); - VERIFY_INTEGRITY(api, oi); + verifyOI(); item = api.popFirst(oi); TEST_ASSERT_SCORE_EQ(api.getScore(item), 1.0); api.freeItem(item); TEST_ASSERT(api.length(oi) == 3); - VERIFY_INTEGRITY(api, oi); + verifyOI(); - api.free(oi); } TEST_P(OrderedIndexTest, PopLast) { - OrderedIndex *oi = api.create(); TEST_ASSERT(api.popLast(oi) == NULL); - for (int i = 0; i < 5; i++) { - char buf[32]; - snprintf(buf, sizeof(buf), "key%d", i); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); - } + populateSequential(5); TEST_ASSERT(api.length(oi) == 5); OrderedIndexItem *item = api.popLast(oi); TEST_ASSERT(item != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(item), 4.0); - const char *ptr; - size_t len; - api.getElementRaw(item, &ptr, &len); - TEST_ASSERT(len == 4 && memcmp(ptr, "key4", 4) == 0); + assertElement(item, "key4"); api.freeItem(item); TEST_ASSERT(api.length(oi) == 4); - VERIFY_INTEGRITY(api, oi); + verifyOI(); item = api.popLast(oi); TEST_ASSERT_SCORE_EQ(api.getScore(item), 3.0); api.freeItem(item); TEST_ASSERT(api.length(oi) == 3); - VERIFY_INTEGRITY(api, oi); + verifyOI(); - api.free(oi); } TEST_P(OrderedIndexTest, UpdateScore) { - OrderedIndex *oi = api.create(); sds ele1 = sdsnew("key1"); sds ele2 = sdsnew("key2"); @@ -306,50 +310,35 @@ TEST_P(OrderedIndexTest, UpdateScore) { OrderedIndexItem *updated = api.updateScore(oi, node2, 4.0); TEST_ASSERT(updated != NULL); TEST_ASSERT_SCORE_EQ(api.getScore(updated), 4.0); - VERIFY_INTEGRITY(api, oi); - const char *ptr; - size_t len; - api.getElementRaw(updated, &ptr, &len); - TEST_ASSERT(len == 4 && memcmp(ptr, "key2", 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); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 3.0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 4 && memcmp(ptr, "key2", 4) == 0); + 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); TEST_ASSERT_SCORE_EQ(api.getScore(updated), 1.0); - VERIFY_INTEGRITY(api, oi); + verifyOI(); - api.free(oi); } TEST_P(OrderedIndexTest, DeleteRangeByScore) { - OrderedIndex *oi = api.create(); - for (int i = 0; i < 10; i++) { - char buf[32]; - snprintf(buf, sizeof(buf), "key%d", i); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); - } + populateSequential(10); /* Delete range [3, 6] inclusive */ unsigned long deleted = api.deleteRangeByScore(oi, 3.0, 6.0, 0, 0, NULL, NULL); TEST_ASSERT(deleted == 4); /* 3, 4, 5, 6 */ TEST_ASSERT(api.length(oi) == 6); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -368,44 +357,32 @@ TEST_P(OrderedIndexTest, DeleteRangeByScore) { deleted = api.deleteRangeByScore(oi, 2.0, 8.0, 1, 1, NULL, NULL); TEST_ASSERT(deleted == 1); TEST_ASSERT(api.length(oi) == 5); - VERIFY_INTEGRITY(api, oi); + verifyOI(); - api.free(oi); } TEST_P(OrderedIndexTest, DeleteRangeByRank) { - OrderedIndex *oi = api.create(); - for (int i = 0; i < 10; i++) { - char buf[32]; - snprintf(buf, sizeof(buf), "key%d", i); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); - } + populateSequential(10); /* Delete ranks 3-5 (1-based, so elements at scores 2,3,4) */ unsigned long deleted = api.deleteRangeByRank(oi, 3, 5, NULL, NULL); TEST_ASSERT(deleted == 3); TEST_ASSERT(api.length(oi) == 7); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexIterator iter; - OrderedIndexItem *pos; api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 0.0); + assertNextScore(&iter, 0.0); api.resetIterator(&iter); /* Verify rank 3 is now score 5 (was rank 6) */ OrderedIndexItem *node = api.getByRank(oi, 3); TEST_ASSERT_SCORE_EQ(api.getScore(node), 5.0); - api.free(oi); } TEST_P(OrderedIndexTest, MixedOperationsRankIntegrity) { - OrderedIndex *oi = api.create(); OrderedIndexItem *nodes[100]; for (int i = 0; i < 100; i++) { @@ -420,11 +397,11 @@ TEST_P(OrderedIndexTest, MixedOperationsRankIntegrity) { api.deleteItem(oi, nodes[i]); nodes[i] = NULL; } - VERIFY_INTEGRITY(api, oi); + verifyOI(); if (nodes[10]) nodes[10] = api.updateScore(oi, nodes[10], 150.0); if (nodes[20]) nodes[20] = api.updateScore(oi, nodes[20], 160.0); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -437,11 +414,9 @@ TEST_P(OrderedIndexTest, MixedOperationsRankIntegrity) { } api.resetIterator(&iter); - api.free(oi); } TEST_P(OrderedIndexTest, BackwardTraversalAfterDeletions) { - OrderedIndex *oi = api.create(); OrderedIndexItem *nodes[20]; for (int i = 0; i < 20; i++) { @@ -455,7 +430,7 @@ TEST_P(OrderedIndexTest, BackwardTraversalAfterDeletions) { api.deleteItem(oi, nodes[5]); api.deleteItem(oi, nodes[10]); api.deleteItem(oi, nodes[15]); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -470,11 +445,9 @@ TEST_P(OrderedIndexTest, BackwardTraversalAfterDeletions) { TEST_ASSERT(idx_score == 17); /* Should have traversed all 17 remaining elements */ api.resetIterator(&iter); - api.free(oi); } TEST_P(OrderedIndexTest, LexicographicEdgeCases) { - OrderedIndex *oi = api.create(); sds empty = sdsnew(""); sds a = sdsnew("a"); @@ -528,11 +501,9 @@ TEST_P(OrderedIndexTest, LexicographicEdgeCases) { sdsfree(long_str); sdsfree(short_str); - api.free(oi); } TEST_P(OrderedIndexTest, RangeBoundaryPrecision) { - OrderedIndex *oi = api.create(); double base = 1.0; double epsilon = 1e-10; @@ -550,22 +521,17 @@ TEST_P(OrderedIndexTest, RangeBoundaryPrecision) { TEST_ASSERT(api.length(oi) == 2); OrderedIndexIterator iter; - OrderedIndexItem *pos; api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), base); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), base + 2 * epsilon); + assertNextScore(&iter, base); + assertNextScore(&iter, base + 2 * epsilon); api.resetIterator(&iter); sdsfree(ele1); sdsfree(ele2); sdsfree(ele3); - api.free(oi); } TEST_P(OrderedIndexTest, SpecialDoubleValues) { - OrderedIndex *oi = api.create(); const char *ptr; size_t len; @@ -583,14 +549,10 @@ TEST_P(OrderedIndexTest, SpecialDoubleValues) { OrderedIndexIterator iter; OrderedIndexItem *pos; api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), NEG_INF); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 0.0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), POS_INF); + assertNextScore(&iter, NEG_INF); + assertNextScore(&iter, 0.0); + assertNextScore(&iter, 1.0); + assertNextScore(&iter, POS_INF); api.resetIterator(&iter); sdsfree(neg_inf); @@ -633,18 +595,15 @@ TEST_P(OrderedIndexTest, SpecialDoubleValues) { TEST_ASSERT(api.length(oi) == 2); api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), denorm); + pos = assertNextScore(&iter, denorm); TEST_ASSERT(api.getScore(pos) < 1.0); api.resetIterator(&iter); sdsfree(denorm_ele); sdsfree(normal_ele); - api.free(oi); } TEST_P(OrderedIndexTest, EdgeCases) { - OrderedIndex *oi = api.create(); TEST_ASSERT(api.length(oi) == 0); OrderedIndexIterator iter; @@ -655,18 +614,16 @@ TEST_P(OrderedIndexTest, EdgeCases) { api.resetIterator(&iter); TEST_ASSERT(api.getByRank(oi, 1) == NULL); - api.free(oi); } TEST_P(OrderedIndexTest, DeleteEdgeCases) { - OrderedIndex *oi = api.create(); /* Delete only element */ sds ele = sdsnew("only"); OrderedIndexItem *node = api.insertSds(oi, 1.0, ele); api.deleteItem(oi, node); TEST_ASSERT(api.length(oi) == 0); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexIterator iter; OrderedIndexItem *pos; api.initIterator(&iter, oi); @@ -685,45 +642,33 @@ TEST_P(OrderedIndexTest, DeleteEdgeCases) { } api.deleteItem(oi, nodes[0]); TEST_ASSERT(api.length(oi) == 2); - VERIFY_INTEGRITY(api, oi); + verifyOI(); api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + assertNextScore(&iter, 1.0); api.resetIterator(&iter); /* Delete last element */ api.deleteItem(oi, nodes[2]); TEST_ASSERT(api.length(oi) == 1); - VERIFY_INTEGRITY(api, oi); + verifyOI(); api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.prev(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + assertPrevScore(&iter, 1.0); api.resetIterator(&iter); - api.free(oi); } TEST_P(OrderedIndexTest, RankEdgeCases) { - OrderedIndex *oi = api.create(); - for (int i = 0; i < 5; i++) { - char buf[32]; - snprintf(buf, sizeof(buf), "key%d", i); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); - } + populateSequential(5); TEST_ASSERT(api.getByRank(oi, 6) == NULL); TEST_ASSERT(api.getByRank(oi, 100) == NULL); TEST_ASSERT(api.getByRank(oi, 1) != NULL); TEST_ASSERT(api.getByRank(oi, 5) != NULL); - api.free(oi); } TEST_P(OrderedIndexTest, DuplicateInsert) { - OrderedIndex *oi = api.create(); sds ele1 = sdsnew("duplicate"); sds ele2 = sdsnew("duplicate"); @@ -736,19 +681,11 @@ TEST_P(OrderedIndexTest, DuplicateInsert) { sdsfree(ele1); sdsfree(ele2); - api.free(oi); } TEST_P(OrderedIndexTest, UpdateScoreEdgeCases) { - OrderedIndex *oi = api.create(); - for (int i = 0; i < 5; i++) { - char buf[32]; - snprintf(buf, sizeof(buf), "key%d", i); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); - } + populateSequential(5); /* Update first element to move backward */ OrderedIndexItem *first = api.getByRank(oi, 1); @@ -778,19 +715,11 @@ TEST_P(OrderedIndexTest, UpdateScoreEdgeCases) { TEST_ASSERT_SCORE_EQ(api.getScore(updated), 0.5); TEST_ASSERT(api.getScore(updated) < old_score); - api.free(oi); } TEST_P(OrderedIndexTest, RangeDeleteEdgeCases) { - OrderedIndex *oi = api.create(); - for (int i = 0; i < 10; i++) { - char buf[32]; - snprintf(buf, sizeof(buf), "key%d", i); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); - } + populateSequential(10); /* Delete empty range (min > max) */ unsigned long deleted = api.deleteRangeByScore(oi, 5.0, 4.0, 0, 0, NULL, NULL); @@ -805,35 +734,30 @@ TEST_P(OrderedIndexTest, RangeDeleteEdgeCases) { /* Delete first elements by rank */ deleted = api.deleteRangeByRank(oi, 1, 2, NULL, NULL); TEST_ASSERT(deleted == 2); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexIterator iter; - OrderedIndexItem *pos; api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 2.0); + assertNextScore(&iter, 2.0); api.resetIterator(&iter); /* Delete last elements by rank */ unsigned long len = api.length(oi); deleted = api.deleteRangeByRank(oi, len - 1, len, NULL, NULL); TEST_ASSERT(deleted == 2); - VERIFY_INTEGRITY(api, oi); + verifyOI(); api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.prev(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 7.0); + 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); TEST_ASSERT(deleted == 6); TEST_ASSERT(api.length(oi) == 0); - VERIFY_INTEGRITY(api, oi); + verifyOI(); - api.free(oi); } TEST_P(OrderedIndexTest, TraversalEdgeCases) { - OrderedIndex *oi = api.create(); sds ele = sdsnew("single"); api.insertSds(oi, 1.0, ele); @@ -841,23 +765,19 @@ TEST_P(OrderedIndexTest, TraversalEdgeCases) { OrderedIndexIterator iter; OrderedIndexItem *pos; api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + pos = assertNextScore(&iter, 1.0); TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.prev(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + pos = assertPrevScore(&iter, 1.0); TEST_ASSERT((pos = api.prev(&iter)) == NULL); api.resetIterator(&iter); sdsfree(ele); - api.free(oi); } TEST_P(OrderedIndexTest, SeekToRank) { - OrderedIndex *oi = api.create(); for (int i = 1; i <= 5; i++) { char buf[32]; @@ -873,8 +793,7 @@ TEST_P(OrderedIndexTest, SeekToRank) { /* Seek to rank 0 (before first) */ api.initIterator(&iter, oi); api.seekToRank(&iter, 0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + assertNextScore(&iter, 1.0); api.resetIterator(&iter); api.initIterator(&iter, oi); @@ -885,27 +804,23 @@ TEST_P(OrderedIndexTest, SeekToRank) { /* Seek to rank 1 */ api.initIterator(&iter, oi); api.seekToRank(&iter, 1); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 2.0); + assertNextScore(&iter, 2.0); api.resetIterator(&iter); api.initIterator(&iter, oi); api.seekToRank(&iter, 1); - TEST_ASSERT((pos = api.prev(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + assertPrevScore(&iter, 1.0); api.resetIterator(&iter); /* Seek to rank 3 (middle) */ api.initIterator(&iter, oi); api.seekToRank(&iter, 3); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); + assertNextScore(&iter, 4.0); api.resetIterator(&iter); api.initIterator(&iter, oi); api.seekToRank(&iter, 3); - TEST_ASSERT((pos = api.prev(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 3.0); + assertPrevScore(&iter, 3.0); api.resetIterator(&iter); /* Seek to rank 5 (last) */ @@ -916,15 +831,12 @@ TEST_P(OrderedIndexTest, SeekToRank) { api.initIterator(&iter, oi); api.seekToRank(&iter, 5); - TEST_ASSERT((pos = api.prev(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 5.0); + assertPrevScore(&iter, 5.0); api.resetIterator(&iter); - api.free(oi); } TEST_P(OrderedIndexTest, ReverseIteration) { - OrderedIndex *oi = api.create(); for (int i = 1; i <= 5; i++) { char buf[32]; @@ -951,25 +863,19 @@ TEST_P(OrderedIndexTest, ReverseIteration) { /* Reverse then forward */ api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.prev(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 5.0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 5.0); + assertPrevScore(&iter, 5.0); + assertNextScore(&iter, 5.0); api.resetIterator(&iter); /* Forward then reverse */ api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); - TEST_ASSERT((pos = api.prev(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + assertNextScore(&iter, 1.0); + assertPrevScore(&iter, 1.0); api.resetIterator(&iter); - api.free(oi); } TEST_P(OrderedIndexTest, SeekToScoreRange) { - OrderedIndex *oi = api.create(); /* Insert elements with scores 0,2,4,6,8 */ for (int i = 0; i < 5; i++) { @@ -986,29 +892,25 @@ TEST_P(OrderedIndexTest, SeekToScoreRange) { /* Seek to first in range [2, 6] with offset 0 */ api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, 0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 2.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); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); + 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); - TEST_ASSERT((pos = api.prev(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 6.0); + 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); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); + assertNextScore(&iter, 4.0); api.resetIterator(&iter); /* Seek to empty range above all elements */ @@ -1038,8 +940,7 @@ TEST_P(OrderedIndexTest, SeekToScoreRange) { /* Second from last with offset -2, positioned for prev() */ api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 2.0, 6.0, 0, 0, -2); - TEST_ASSERT((pos = api.prev(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); + assertPrevScore(&iter, 4.0); api.resetIterator(&iter); /* Empty range where min > max */ @@ -1048,19 +949,11 @@ TEST_P(OrderedIndexTest, SeekToScoreRange) { TEST_ASSERT((pos = api.next(&iter)) == NULL); api.resetIterator(&iter); - api.free(oi); } TEST_P(OrderedIndexTest, SeekToScoreRangeIteration) { - OrderedIndex *oi = api.create(); - for (int i = 0; i < 10; i++) { - char buf[32]; - snprintf(buf, sizeof(buf), "key%d", i); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); - } + populateSequential(10); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1094,17 +987,13 @@ TEST_P(OrderedIndexTest, SeekToScoreRangeIteration) { /* Seek with offset and continue iteration */ api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 2.0, 8.0, 0, 0, 2); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 4.0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 5.0); + assertNextScore(&iter, 4.0); + assertNextScore(&iter, 5.0); api.resetIterator(&iter); - api.free(oi); } TEST_P(OrderedIndexTest, SeekInfReverseIteration) { - OrderedIndex *oi = api.create(); for (int i = 1; i <= 5; i++) { char buf[32]; @@ -1129,11 +1018,9 @@ TEST_P(OrderedIndexTest, SeekInfReverseIteration) { TEST_ASSERT(count == 5); api.resetIterator(&iter); - api.free(oi); } TEST_P(OrderedIndexTest, SeekInfForwardIteration) { - OrderedIndex *oi = api.create(); for (int i = 1; i <= 5; i++) { char buf[32]; @@ -1158,11 +1045,9 @@ TEST_P(OrderedIndexTest, SeekInfForwardIteration) { TEST_ASSERT(count == 5); api.resetIterator(&iter); - api.free(oi); } TEST_P(OrderedIndexTest, SeekToLexRange) { - OrderedIndex *oi = api.create(); const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; for (int i = 0; i < 5; i++) { @@ -1235,11 +1120,9 @@ TEST_P(OrderedIndexTest, SeekToLexRange) { sdsfree(minLex); sdsfree(maxLex); - api.free(oi); } TEST_P(OrderedIndexTest, DeleteRangeByLexInclusive) { - OrderedIndex *oi = api.create(); const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; for (int i = 0; i < 5; i++) { @@ -1253,7 +1136,7 @@ TEST_P(OrderedIndexTest, DeleteRangeByLexInclusive) { unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); TEST_ASSERT(deleted == 3); TEST_ASSERT(api.length(oi) == 2); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1271,11 +1154,9 @@ TEST_P(OrderedIndexTest, DeleteRangeByLexInclusive) { sdsfree(min); sdsfree(max); - api.free(oi); } TEST_P(OrderedIndexTest, DeleteRangeByLexExclusive) { - OrderedIndex *oi = api.create(); const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; for (int i = 0; i < 5; i++) { @@ -1312,12 +1193,10 @@ TEST_P(OrderedIndexTest, DeleteRangeByLexExclusive) { sdsfree(min); sdsfree(max); - api.free(oi); } TEST_P(OrderedIndexTest, DeleteRangeByLexBoundaryCases) { /* Empty range: min > max lexicographically */ - OrderedIndex *oi = api.create(); const char *elements[] = {"apple", "banana", "cherry"}; for (int i = 0; i < 3; i++) { sds ele = sdsnew(elements[i]); @@ -1381,11 +1260,9 @@ TEST_P(OrderedIndexTest, DeleteRangeByLexBoundaryCases) { sdsfree(min); sdsfree(max); - api.free(oi); } TEST_P(OrderedIndexTest, DeleteRangeByLexPreservesOutside) { - OrderedIndex *oi = api.create(); const char *elements[] = {"alpha", "bravo", "charlie", "delta", "echo", "foxtrot"}; for (int i = 0; i < 6; i++) { @@ -1432,7 +1309,6 @@ TEST_P(OrderedIndexTest, DeleteRangeByLexPreservesOutside) { sdsfree(min); sdsfree(max); - api.free(oi); } /* ========== Randomized property tests ========== */ @@ -1476,11 +1352,10 @@ TEST_P(OrderedIndexTest, RandomizedInsertAndTraversal) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - OrderedIndex *oi = api.create(); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, rng, n); ASSERT_EQ(api.length(oi), (unsigned long)n); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1496,6 +1371,7 @@ TEST_P(OrderedIndexTest, RandomizedInsertAndTraversal) { ASSERT_EQ(count, n); api.resetIterator(&iter); api.free(oi); + oi = api.create(); } } @@ -1505,8 +1381,7 @@ TEST_P(OrderedIndexTest, RandomizedBackwardTraversal) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - OrderedIndex *oi = api.create(); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, rng, n); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1522,6 +1397,7 @@ TEST_P(OrderedIndexTest, RandomizedBackwardTraversal) { ASSERT_EQ(count, n); api.resetIterator(&iter); api.free(oi); + oi = api.create(); } } @@ -1531,13 +1407,13 @@ TEST_P(OrderedIndexTest, RandomizedScoreRetrieval) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - OrderedIndex *oi = api.create(); - auto entries = test_build_random_index(api, oi, rng, n); + auto entries = test_build_random_index(api, oi, rng, n); for (auto &e : entries) { TEST_ASSERT_SCORE_EQ(api.getScore(e.node), e.score); } api.free(oi); + oi = api.create(); } } @@ -1547,8 +1423,7 @@ TEST_P(OrderedIndexTest, RandomizedRankConsistency) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - OrderedIndex *oi = api.create(); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, rng, n); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1564,6 +1439,7 @@ TEST_P(OrderedIndexTest, RandomizedRankConsistency) { ASSERT_EQ(expectedRank - 1, (unsigned long)n); api.resetIterator(&iter); api.free(oi); + oi = api.create(); } } @@ -1573,15 +1449,14 @@ TEST_P(OrderedIndexTest, RandomizedDelete) { std::uniform_int_distribution sizeDist(2, 30); int n = sizeDist(rng); - OrderedIndex *oi = api.create(); - auto entries = test_build_random_index(api, oi, rng, n); + auto entries = test_build_random_index(api, oi, rng, n); std::uniform_int_distribution pickDist(0, n - 1); int delIdx = pickDist(rng); api.deleteItem(oi, entries[delIdx].node); ASSERT_EQ(api.length(oi), (unsigned long)(n - 1)); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1596,6 +1471,7 @@ TEST_P(OrderedIndexTest, RandomizedDelete) { ASSERT_EQ(count, n - 1); api.resetIterator(&iter); api.free(oi); + oi = api.create(); } } @@ -1605,8 +1481,7 @@ TEST_P(OrderedIndexTest, RandomizedUpdateScore) { std::uniform_int_distribution sizeDist(2, 30); int n = sizeDist(rng); - OrderedIndex *oi = api.create(); - auto entries = test_build_random_index(api, oi, rng, n); + auto entries = test_build_random_index(api, oi, rng, n); std::uniform_int_distribution pickDist(0, n - 1); int updIdx = pickDist(rng); @@ -1616,7 +1491,7 @@ TEST_P(OrderedIndexTest, RandomizedUpdateScore) { ASSERT_NE(updated, nullptr); TEST_ASSERT_SCORE_EQ(api.getScore(updated), newScore); ASSERT_EQ(api.length(oi), (unsigned long)n); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1628,6 +1503,7 @@ TEST_P(OrderedIndexTest, RandomizedUpdateScore) { } api.resetIterator(&iter); api.free(oi); + oi = api.create(); } } @@ -1637,8 +1513,7 @@ TEST_P(OrderedIndexTest, RandomizedPop) { std::uniform_int_distribution sizeDist(3, 30); int n = sizeDist(rng); - OrderedIndex *oi = api.create(); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, rng, n); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1657,14 +1532,14 @@ TEST_P(OrderedIndexTest, RandomizedPop) { TEST_ASSERT_SCORE_EQ(api.getScore(first), minScore); ASSERT_EQ(api.length(oi), (unsigned long)(n - 1)); api.freeItem(first); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexItem *last = api.popLast(oi); ASSERT_NE(last, nullptr); TEST_ASSERT_SCORE_EQ(api.getScore(last), maxScore); ASSERT_EQ(api.length(oi), (unsigned long)(n - 2)); api.freeItem(last); - VERIFY_INTEGRITY(api, oi); + verifyOI(); api.initIterator(&iter, oi); double prevScore = NEG_INF; @@ -1674,6 +1549,7 @@ TEST_P(OrderedIndexTest, RandomizedPop) { } api.resetIterator(&iter); api.free(oi); + oi = api.create(); } } @@ -1683,8 +1559,7 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { std::uniform_int_distribution sizeDist(5, 40); int n = sizeDist(rng); - OrderedIndex *oi = api.create(); - auto entries = test_build_random_index(api, oi, rng, n); + auto entries = test_build_random_index(api, oi, rng, n); double s1 = test_random_score(rng), s2 = test_random_score(rng); double lo = (std::min)(s1, s2), hi = (std::max)(s1, s2); @@ -1697,7 +1572,7 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { 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)); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1711,6 +1586,7 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { } api.resetIterator(&iter); api.free(oi); + oi = api.create(); } } @@ -1720,8 +1596,7 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByRank) { std::uniform_int_distribution sizeDist(5, 40); int n = sizeDist(rng); - OrderedIndex *oi = api.create(); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, rng, n); std::uniform_int_distribution rankDist(1, n); int r1 = rankDist(rng), r2 = rankDist(rng); @@ -1732,7 +1607,7 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByRank) { unsigned long deleted = api.deleteRangeByRank(oi, start, end, NULL, NULL); ASSERT_EQ(deleted, expectedDeleted); ASSERT_EQ(api.length(oi), (unsigned long)(n)-expectedDeleted); - VERIFY_INTEGRITY(api, oi); + verifyOI(); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1747,6 +1622,7 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByRank) { ASSERT_EQ(remaining, n - (int)expectedDeleted); api.resetIterator(&iter); api.free(oi); + oi = api.create(); } } @@ -1756,8 +1632,7 @@ TEST_P(OrderedIndexTest, RandomizedForwardBackwardMirror) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - OrderedIndex *oi = api.create(); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, rng, n); std::vector forwardScores; OrderedIndexIterator iter; @@ -1781,21 +1656,15 @@ TEST_P(OrderedIndexTest, RandomizedForwardBackwardMirror) { TEST_ASSERT_SCORE_EQ(forwardScores[i], backwardScores[i]); } api.free(oi); + oi = api.create(); } } /* ========== Count range tests ========== */ TEST_P(OrderedIndexTest, CountScoreRange) { - OrderedIndex *oi = api.create(); - for (int i = 0; i < 10; i++) { - char buf[32]; - snprintf(buf, sizeof(buf), "key%d", i); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); - } + populateSequential(10); /* Full range */ ASSERT_EQ(api.countScoreRange(oi, NEG_INF, POS_INF, 0, 0), 10UL); @@ -1827,17 +1696,13 @@ TEST_P(OrderedIndexTest, CountScoreRange) { /* Last element only [9, 9] */ ASSERT_EQ(api.countScoreRange(oi, 9.0, 9.0, 0, 0), 1UL); - api.free(oi); } TEST_P(OrderedIndexTest, CountScoreRangeEmpty) { - OrderedIndex *oi = api.create(); ASSERT_EQ(api.countScoreRange(oi, NEG_INF, POS_INF, 0, 0), 0UL); - api.free(oi); } TEST_P(OrderedIndexTest, CountLexRange) { - OrderedIndex *oi = api.create(); const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; for (int i = 0; i < 5; i++) { @@ -1881,17 +1746,14 @@ TEST_P(OrderedIndexTest, CountLexRange) { sdsfree(min); sdsfree(max); - api.free(oi); } TEST_P(OrderedIndexTest, CountLexRangeEmpty) { - OrderedIndex *oi = api.create(); sds min = sdsnew("a"); sds max = sdsnew("z"); ASSERT_EQ(api.countLexRange(oi, min, max, 0, 0), 0UL); sdsfree(min); sdsfree(max); - api.free(oi); } /* ========== Instantiate parameterized tests for all implementations ========== */ @@ -1921,6 +1783,17 @@ static void testOnDeleteCallback(OrderedIndexItem *item, void *ctx) { class OnDeleteCallbackTest : public ::testing::Test { protected: SkiplistOrderedIndex api; + 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 insertN(OrderedIndex *oi, int n) { for (int i = 0; i < n; i++) { @@ -1960,7 +1833,6 @@ class OnDeleteCallbackTest : public ::testing::Test { TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_EmptyAndNoMatch) { OnDeleteRecord rec = {0, {}}; - OrderedIndex *oi = api.create(); unsigned long deleted = api.deleteRangeByScore(oi, 0.0, 10.0, 0, 0, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 0UL); ASSERT_EQ(rec.count, 0); @@ -1973,11 +1845,9 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_EmptyAndNoMatch) { ASSERT_EQ(deleted, 0UL); ASSERT_EQ(rec.count, 0); ASSERT_EQ(api.length(oi), 5UL); - api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_Subset) { - OrderedIndex *oi = api.create(); insertN(oi, 10); OnDeleteRecord rec = {0, {}}; @@ -1985,18 +1855,16 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_Subset) { ASSERT_EQ(deleted, 4UL); ASSERT_EQ(rec.count, 4); ASSERT_EQ(api.length(oi), 6UL); - VERIFY_INTEGRITY(api, oi); + verifyOI(); std::sort(rec.elements.begin(), rec.elements.end()); ASSERT_EQ(rec.elements, (std::vector{"key3", "key4", "key5", "key6"})); auto remaining = collectElements(oi); ASSERT_EQ(remaining, (std::vector{"key0", "key1", "key2", "key7", "key8", "key9"})); - api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_All) { - OrderedIndex *oi = api.create(); insertN(oi, 5); OnDeleteRecord rec = {0, {}}; @@ -2004,22 +1872,18 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_All) { ASSERT_EQ(deleted, 5UL); ASSERT_EQ(rec.count, 5); ASSERT_EQ(api.length(oi), 0UL); - VERIFY_INTEGRITY(api, oi); - api.free(oi); + verifyOI(); } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_NullCallback) { - OrderedIndex *oi = api.create(); insertN(oi, 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); - api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_ExclusiveBounds) { - OrderedIndex *oi = api.create(); insertN(oi, 10); OnDeleteRecord rec = {0, {}}; @@ -2029,11 +1893,9 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_ExclusiveBounds) { std::sort(rec.elements.begin(), rec.elements.end()); ASSERT_EQ(rec.elements, (std::vector{"key4", "key5", "key6"})); ASSERT_EQ(api.length(oi), 7UL); - api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_SingleElement) { - OrderedIndex *oi = api.create(); insertN(oi, 5); OnDeleteRecord rec = {0, {}}; @@ -2042,7 +1904,6 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_SingleElement) { ASSERT_EQ(rec.count, 1); ASSERT_EQ(rec.elements[0], "key2"); ASSERT_EQ(api.length(oi), 4UL); - api.free(oi); } /* DeleteRangeByRank */ @@ -2050,7 +1911,6 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_SingleElement) { TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_EmptyAndNoMatch) { OnDeleteRecord rec = {0, {}}; - OrderedIndex *oi = api.create(); unsigned long deleted = api.deleteRangeByRank(oi, 1, 5, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 0UL); ASSERT_EQ(rec.count, 0); @@ -2063,11 +1923,9 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_EmptyAndNoMatch) { ASSERT_EQ(deleted, 0UL); ASSERT_EQ(rec.count, 0); ASSERT_EQ(api.length(oi), 3UL); - api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_Subset) { - OrderedIndex *oi = api.create(); insertN(oi, 10); OnDeleteRecord rec = {0, {}}; @@ -2081,11 +1939,9 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_Subset) { auto remaining = collectElements(oi); ASSERT_EQ(remaining, (std::vector{"key0", "key1", "key5", "key6", "key7", "key8", "key9"})); - api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_All) { - OrderedIndex *oi = api.create(); insertN(oi, 5); OnDeleteRecord rec = {0, {}}; @@ -2093,21 +1949,17 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_All) { ASSERT_EQ(deleted, 5UL); ASSERT_EQ(rec.count, 5); ASSERT_EQ(api.length(oi), 0UL); - api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_NullCallback) { - OrderedIndex *oi = api.create(); insertN(oi, 5); unsigned long deleted = api.deleteRangeByRank(oi, 2, 4, NULL, NULL); ASSERT_EQ(deleted, 3UL); ASSERT_EQ(api.length(oi), 2UL); - api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_ExclusiveBounds) { - OrderedIndex *oi = api.create(); insertN(oi, 5); OnDeleteRecord rec = {0, {}}; @@ -2118,11 +1970,9 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_ExclusiveBounds) { auto remaining = collectElements(oi); ASSERT_EQ(remaining, (std::vector{"key0", "key1", "key3", "key4"})); - api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_SingleElement) { - OrderedIndex *oi = api.create(); insertN(oi, 5); OnDeleteRecord rec = {0, {}}; @@ -2131,7 +1981,6 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_SingleElement) { ASSERT_EQ(rec.count, 1); ASSERT_EQ(rec.elements[0], "key0"); ASSERT_EQ(api.length(oi), 4UL); - api.free(oi); } /* DeleteRangeByLex */ @@ -2139,7 +1988,6 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_SingleElement) { TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { OnDeleteRecord rec = {0, {}}; - OrderedIndex *oi = api.create(); sds min = sdsnew("a"); sds max = sdsnew("z"); unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); @@ -2160,11 +2008,9 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { ASSERT_EQ(api.length(oi), 3UL); sdsfree(min); sdsfree(max); - api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_Subset) { - OrderedIndex *oi = api.create(); insertLex(oi, {"apple", "banana", "cherry", "date", "elderberry"}); OnDeleteRecord rec = {0, {}}; @@ -2183,11 +2029,9 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_Subset) { sdsfree(min); sdsfree(max); - api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_All) { - OrderedIndex *oi = api.create(); insertLex(oi, {"apple", "banana", "cherry"}); OnDeleteRecord rec = {0, {}}; @@ -2200,11 +2044,9 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_All) { sdsfree(min); sdsfree(max); - api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_NullCallback) { - OrderedIndex *oi = api.create(); insertLex(oi, {"apple", "banana", "cherry", "date"}); sds min = sdsnew("banana"); @@ -2218,11 +2060,9 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_NullCallback) { sdsfree(min); sdsfree(max); - api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_ExclusiveBounds) { - OrderedIndex *oi = api.create(); insertLex(oi, {"apple", "banana", "cherry", "date", "elderberry"}); OnDeleteRecord rec = {0, {}}; @@ -2239,11 +2079,9 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_ExclusiveBounds) { sdsfree(min); sdsfree(max); - api.free(oi); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_SingleElement) { - OrderedIndex *oi = api.create(); insertLex(oi, {"apple", "banana", "cherry"}); OnDeleteRecord rec = {0, {}}; @@ -2260,7 +2098,6 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_SingleElement) { sdsfree(min); sdsfree(max); - api.free(oi); } /* ========== Range-Delete Hashtable Consistency Tests ========== */ @@ -2277,6 +2114,12 @@ static void hashtableConsistencyOnDelete(OrderedIndexItem *item, void *ctx) { class RangeDeleteHashtableConsistencyTest : public ::testing::Test { protected: SkiplistOrderedIndex api; + OrderedIndex *oi = nullptr; + + void SetUp() override { oi = api.create(); } + void TearDown() override { + if (oi) api.free(oi); + } void insertN(OrderedIndex *oi, std::set &ht, int n) { for (int i = 0; i < n; i++) { @@ -2316,7 +2159,6 @@ class RangeDeleteHashtableConsistencyTest : public ::testing::Test { /* ByScore */ TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_PartialDelete) { - OrderedIndex *oi = api.create(); std::set simulatedHt; insertN(oi, simulatedHt, 10); @@ -2326,11 +2168,9 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_PartialDelete) { ASSERT_EQ(indexElements, simulatedHt); ASSERT_EQ(indexElements.size(), 6UL); - api.free(oi); } TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_FullDelete) { - OrderedIndex *oi = api.create(); std::set simulatedHt; insertN(oi, simulatedHt, 10); @@ -2340,11 +2180,9 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_FullDelete) { ASSERT_EQ(indexElements, simulatedHt); ASSERT_TRUE(indexElements.empty()); - api.free(oi); } TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_EmptyRange) { - OrderedIndex *oi = api.create(); std::set simulatedHt; insertN(oi, simulatedHt, 10); @@ -2354,13 +2192,11 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_EmptyRange) { ASSERT_EQ(indexElements, simulatedHt); ASSERT_EQ(indexElements.size(), 10UL); - api.free(oi); } /* ByRank */ TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_PartialDelete) { - OrderedIndex *oi = api.create(); std::set simulatedHt; insertN(oi, simulatedHt, 10); @@ -2370,11 +2206,9 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_PartialDelete) { ASSERT_EQ(indexElements, simulatedHt); ASSERT_EQ(indexElements.size(), 7UL); - api.free(oi); } TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_FullDelete) { - OrderedIndex *oi = api.create(); std::set simulatedHt; insertN(oi, simulatedHt, 10); @@ -2384,11 +2218,9 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_FullDelete) { ASSERT_EQ(indexElements, simulatedHt); ASSERT_TRUE(indexElements.empty()); - api.free(oi); } TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_EmptyRange) { - OrderedIndex *oi = api.create(); std::set simulatedHt; insertN(oi, simulatedHt, 10); @@ -2398,13 +2230,11 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_EmptyRange) { ASSERT_EQ(indexElements, simulatedHt); ASSERT_EQ(indexElements.size(), 10UL); - api.free(oi); } /* ByLex */ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_PartialDelete) { - OrderedIndex *oi = api.create(); std::set simulatedHt; insertLex(oi, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); @@ -2418,11 +2248,9 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_PartialDelete) { sdsfree(min); sdsfree(max); - api.free(oi); } TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_FullDelete) { - OrderedIndex *oi = api.create(); std::set simulatedHt; insertLex(oi, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); @@ -2436,11 +2264,9 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_FullDelete) { sdsfree(min); sdsfree(max); - api.free(oi); } TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_EmptyRange) { - OrderedIndex *oi = api.create(); std::set simulatedHt; insertLex(oi, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); @@ -2454,5 +2280,4 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_EmptyRange) { sdsfree(min); sdsfree(max); - api.free(oi); } From 1e28aa13c15ed1ebcfbef2583a70e8cba6a71700 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Wed, 27 May 2026 09:38:49 +0000 Subject: [PATCH 30/45] tests: DRY refactoring and clean coding improvements for ordered index unit tests - Add fixture helpers: insert(), populateSequential(), assertElement, assertNextScore/assertPrevScore, assertAllElements, ScopedIter RAII wrapper - Add lex range helpers (deleteLexRange, countLexRange, seekToLexRange) that accept const char* and handle sds lifecycle internally - Replace all insertSds boilerplate with insert() helper - Replace ASSERT_TRUE(x == y) with ASSERT_EQ for better failure diagnostics - Replace raw getElementRaw+memcmp with assertElement helper - Replace inline element arrays with shared FRUITS[]/NATO[] constants - Split multi-scenario DeleteRangeByLexBoundaryCases into 3 focused tests - Run clang-format for consistent style 77 test cases (48 parameterized + 29 fixture) introduced in this PR. Signed-off-by: Rain Valentine --- src/ordered_index.c | 16 +- src/ordered_index.h | 18 +- src/skiplist_ordered_index.c | 18 +- src/skiplist_ordered_index.h | 8 +- src/sort.c | 6 +- src/t_zset.c | 28 +- src/unit/ordered_index_test.h | 24 +- src/unit/test_ordered_index.cpp | 1236 ++++++++++++------------------- 8 files changed, 542 insertions(+), 812 deletions(-) diff --git a/src/ordered_index.c b/src/ordered_index.c index fa50cbe8af9..83e6eed9951 100644 --- a/src/ordered_index.c +++ b/src/ordered_index.c @@ -63,8 +63,8 @@ unsigned long orderedIndexDeleteRangeByScore(OrderedIndex *oi, double min, doubl return skiplistDeleteRangeByScore(oi, min, max, min_ex, max_ex, on_delete, ctx); } -unsigned long orderedIndexDeleteRangeByRank(OrderedIndex *oi, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) { - return skiplistDeleteRangeByRank(oi, start, end, 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) { @@ -77,12 +77,12 @@ unsigned long orderedIndexLength(OrderedIndex *oi) { return skiplistLength(oi); } -OrderedIndexItem *orderedIndexGetByRank(OrderedIndex *oi, unsigned long rank) { - return skiplistGetByRank(oi, rank); +OrderedIndexItem *orderedIndexGetByIndex(OrderedIndex *oi, unsigned long rank) { + return skiplistGetByIndex(oi, rank); } -unsigned long orderedIndexGetRank(OrderedIndex *oi, const OrderedIndexItem *item) { - return skiplistGetRank(oi, item); +unsigned long orderedIndexGetIndex(OrderedIndex *oi, const OrderedIndexItem *item) { + return skiplistGetIndex(oi, item); } void orderedIndexGetElementRaw(const OrderedIndexItem *item, const char **ptr, size_t *len) { @@ -119,8 +119,8 @@ OrderedIndexItem *orderedIndexPrev(OrderedIndexIterator *iter) { return skiplistPrev(iter); } -void orderedIndexSeekToRank(OrderedIndexIterator *iter, unsigned long rank) { - skiplistSeekToRank(iter, rank); +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) { diff --git a/src/ordered_index.h b/src/ordered_index.h index 52787187c83..133d164bee7 100644 --- a/src/ordered_index.h +++ b/src/ordered_index.h @@ -94,9 +94,9 @@ OrderedIndexItem *orderedIndexInsertDetached(OrderedIndex *oi, OrderedIndexItem * 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 rank in [start, end] (1-based, inclusive). +/* 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 orderedIndexDeleteRangeByRank(OrderedIndex *oi, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx); +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. */ @@ -109,11 +109,11 @@ unsigned long orderedIndexDeleteRangeByLex(OrderedIndex *oi, const_sds min, cons /* Return the number of items in the index. */ unsigned long orderedIndexLength(OrderedIndex *oi); -/* Return the item at the given 1-based rank, or NULL if out of range. */ -OrderedIndexItem *orderedIndexGetByRank(OrderedIndex *oi, unsigned long rank); +/* Return the item at the given 0-based index, or NULL if out of range. */ +OrderedIndexItem *orderedIndexGetByIndex(OrderedIndex *oi, unsigned long index); -/* Return the 1-based rank of an item. The item must be in the index. */ -unsigned long orderedIndexGetRank(OrderedIndex *oi, const OrderedIndexItem *item); +/* 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); @@ -133,7 +133,7 @@ unsigned long orderedIndexCountLexRange(OrderedIndex *oi, const_sds min, const_s /* Initialize a stack-allocated iterator. If no seek function is called, * next() starts from the beginning and prev() starts from the end. - * Use orderedIndexSeekToRank/ScoreRange/LexRange to start elsewhere. */ + * Use orderedIndexSeekToIndex/ScoreRange/LexRange to start elsewhere. */ void orderedIndexInitIterator(OrderedIndexIterator *iter, OrderedIndex *oi); /* Reset iterator position (keeps the index association). */ @@ -145,8 +145,8 @@ OrderedIndexItem *orderedIndexNext(OrderedIndexIterator *iter); /* Advance iterator backward. Returns the previous item, or NULL at start. */ OrderedIndexItem *orderedIndexPrev(OrderedIndexIterator *iter); -/* Position iterator at the given rank. next() returns rank+1, prev() returns rank. */ -void orderedIndexSeekToRank(OrderedIndexIterator *iter, unsigned long rank); +/* 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. diff --git a/src/skiplist_ordered_index.c b/src/skiplist_ordered_index.c index 14d43a3cb7b..e356ec9eba3 100644 --- a/src/skiplist_ordered_index.c +++ b/src/skiplist_ordered_index.c @@ -115,12 +115,16 @@ unsigned long skiplistDeleteRangeByScore(OrderedIndex *oi, double min, double ma return removed; } -unsigned long skiplistDeleteRangeByRank(OrderedIndex *oi, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) { +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) { @@ -192,12 +196,12 @@ unsigned long skiplistLength(OrderedIndex *oi) { return zslGetLength((zskiplist *)oi); } -OrderedIndexItem *skiplistGetByRank(OrderedIndex *oi, unsigned long rank) { - return (OrderedIndexItem *)zslGetElementByRank((zskiplist *)oi, rank); +OrderedIndexItem *skiplistGetByIndex(OrderedIndex *oi, unsigned long index) { + return (OrderedIndexItem *)zslGetElementByRank((zskiplist *)oi, index + 1); } -unsigned long skiplistGetRank(OrderedIndex *oi, const OrderedIndexItem *node) { - return zslGetRank((zskiplist *)oi, (const zskiplistNode *)node); +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) { @@ -262,8 +266,8 @@ OrderedIndexItem *skiplistPrev(OrderedIndexIterator *iter) { return (OrderedIndexItem *)zslPrev((zslIter *)iter); } -void skiplistSeekToRank(OrderedIndexIterator *iter, unsigned long rank) { - zslSeekToRank((zslIter *)iter, rank); +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) { diff --git a/src/skiplist_ordered_index.h b/src/skiplist_ordered_index.h index f9831301e48..0d6727b3aae 100644 --- a/src/skiplist_ordered_index.h +++ b/src/skiplist_ordered_index.h @@ -33,13 +33,13 @@ OrderedIndexItem *skiplistCreateDetached(double score, const char *ele, size_t l 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 skiplistDeleteRangeByRank(OrderedIndex *oi, unsigned long start, unsigned long end, 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 *skiplistGetByRank(OrderedIndex *oi, unsigned long rank); -unsigned long skiplistGetRank(OrderedIndex *oi, const OrderedIndexItem *item); +OrderedIndexItem *skiplistGetByIndex(OrderedIndex *oi, unsigned long rank); +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); @@ -50,7 +50,7 @@ void skiplistInitIterator(OrderedIndexIterator *iter, OrderedIndex *oi); void skiplistResetIterator(OrderedIndexIterator *iter); OrderedIndexItem *skiplistNext(OrderedIndexIterator *iter); OrderedIndexItem *skiplistPrev(OrderedIndexIterator *iter); -void skiplistSeekToRank(OrderedIndexIterator *iter, unsigned long rank); +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); diff --git a/src/sort.c b/src/sort.c index 0816fbd478a..049bd3db41e 100644 --- a/src/sort.c +++ b/src/sort.c @@ -425,10 +425,12 @@ void sortCommandGeneric(client *c, int readonly) { /* Check if starting point is trivial, before doing log(N) lookup. */ if (desc) { long zsetlen = hashtableSize(((zset *)objectGetVal(sortval))->ht); - orderedIndexSeekToRank(&iter, zsetlen - start); + orderedIndexSeekToIndex(&iter, zsetlen - 1 - start); ln = orderedIndexPrev(&iter); } else { - orderedIndexSeekToRank(&iter, start); + if (start > 0) { + orderedIndexSeekToIndex(&iter, start - 1); + } ln = orderedIndexNext(&iter); } diff --git a/src/t_zset.c b/src/t_zset.c index 183d5b3d33c..4c52dccd2ab 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -951,7 +951,7 @@ int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, dou /* 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; OrderedIndexItem *node = entry; @@ -976,7 +976,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 { @@ -1034,14 +1034,12 @@ static long zsetRank(robj *zobj, sds ele, int reverse, double *output_score) { if (!hashtableFind(zs->ht, ele, &entry)) return -1; OrderedIndexItem *node = entry; - rank = orderedIndexGetRank(zs->oi, node); - /* Existing elements always have a rank. */ - serverAssert(rank != 0); + 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"); } @@ -1400,7 +1398,7 @@ void zremrangeGenericCommand(client *c, zrange_type rangetype) { hashtablePauseAutoShrink(zs->ht); switch (rangetype) { case ZRANGE_AUTO: - case ZRANGE_RANK: deleted = orderedIndexDeleteRangeByRank(zs->oi, start + 1, end + 1, zsetIndexDeleteCallback, 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; } @@ -1531,7 +1529,7 @@ static void zuiInitIterator(zsetopsrc *op) { } else if (op->encoding == OBJ_ENCODING_SKIPLIST) { it->sl.zs = objectGetVal(op->subject); orderedIndexInitIterator(&it->sl.iter, it->sl.zs->oi); - orderedIndexSeekToRank(&it->sl.iter, orderedIndexLength(it->sl.zs->oi)); + orderedIndexSeekToIndex(&it->sl.iter, orderedIndexLength(it->sl.zs->oi) - 1); it->sl.node = NULL; } else { serverPanic("Unknown sorted set encoding"); @@ -1849,7 +1847,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 @@ -1876,7 +1874,7 @@ static void zdiffAlgorithm2(zsetopsrc *src, long setnum, zset *dstzset, size_t * cardinality++; } else { tmp = zuiSdsFromValue(&zval); - if (zsetRemoveFromSkiplist(dstzset, tmp)) { + if (zsetRemoveFromIndex(dstzset, tmp)) { cardinality--; } } @@ -2533,10 +2531,10 @@ void genericZrangebyrankCommand(zrange_result_handler *handler, /* Seek to starting position */ if (reverse) { - unsigned long seek_rank = (start > 0) ? (unsigned long)(llen - start) : orderedIndexLength(oi); - orderedIndexSeekToRank(&iter, seek_rank); + unsigned long seek_idx = (start > 0) ? (unsigned long)(llen - start - 1) : orderedIndexLength(oi) - 1; + orderedIndexSeekToIndex(&iter, seek_idx); } else { - orderedIndexSeekToRank(&iter, (unsigned long)start); + if (start > 0) orderedIndexSeekToIndex(&iter, (unsigned long)(start - 1)); } while (rangelen--) { @@ -3291,7 +3289,7 @@ void genericZpopCommand(client *c, OrderedIndexItem *zln; /* Get the first or last element in the sorted set. */ - zln = (where == ZSET_MAX ? orderedIndexGetByRank(oi, orderedIndexLength(oi)) : orderedIndexGetByRank(oi, 1)); + zln = (where == ZSET_MAX ? orderedIndexGetByIndex(oi, orderedIndexLength(oi) - 1) : orderedIndexGetByIndex(oi, 0)); /* There must be an element in the sorted set. */ serverAssertWithInfo(c, zobj, zln != NULL); diff --git a/src/unit/ordered_index_test.h b/src/unit/ordered_index_test.h index fe1e29898ff..868c342d50c 100644 --- a/src/unit/ordered_index_test.h +++ b/src/unit/ordered_index_test.h @@ -36,13 +36,13 @@ class OrderedIndexTestApi { 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 deleteRangeByRank(OrderedIndex *oi, unsigned long start, unsigned long end, 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 *getByRank(OrderedIndex *oi, unsigned long rank) = 0; - virtual unsigned long getRank(OrderedIndex *oi, const OrderedIndexItem *pos) = 0; + virtual OrderedIndexItem *getByIndex(OrderedIndex *oi, unsigned long rank) = 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; @@ -61,7 +61,7 @@ class OrderedIndexTestApi { virtual void resetIterator(OrderedIndexIterator *iter) = 0; virtual OrderedIndexItem *next(OrderedIndexIterator *iter) = 0; virtual OrderedIndexItem *prev(OrderedIndexIterator *iter) = 0; - virtual void seekToRank(OrderedIndexIterator *iter, unsigned long rank) = 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; @@ -118,8 +118,8 @@ class SkiplistOrderedIndex : public OrderedIndexTestApi { 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 deleteRangeByRank(OrderedIndex *oi, unsigned long start, unsigned long end, OrderedIndexOnDelete on_delete, void *ctx) override { - return skiplistDeleteRangeByRank(oi, start, end, 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); @@ -128,11 +128,11 @@ class SkiplistOrderedIndex : public OrderedIndexTestApi { unsigned long length(OrderedIndex *oi) override { return skiplistLength(oi); } - OrderedIndexItem *getByRank(OrderedIndex *oi, unsigned long rank) override { - return skiplistGetByRank(oi, rank); + OrderedIndexItem *getByIndex(OrderedIndex *oi, unsigned long rank) override { + return skiplistGetByIndex(oi, rank); } - unsigned long getRank(OrderedIndex *oi, const OrderedIndexItem *pos) override { - return skiplistGetRank(oi, pos); + 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); @@ -168,8 +168,8 @@ class SkiplistOrderedIndex : public OrderedIndexTestApi { OrderedIndexItem *prev(OrderedIndexIterator *iter) override { return skiplistPrev(iter); } - void seekToRank(OrderedIndexIterator *iter, unsigned long rank) override { - skiplistSeekToRank(iter, rank); + 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); diff --git a/src/unit/test_ordered_index.cpp b/src/unit/test_ordered_index.cpp index b26b88e5924..758ab48d6be 100644 --- a/src/unit/test_ordered_index.cpp +++ b/src/unit/test_ordered_index.cpp @@ -24,9 +24,6 @@ extern "C" { #include #include -#define TEST_ASSERT(x) ASSERT_TRUE(x) -#define TEST_ASSERT_SCORE_EQ(a, b) ASSERT_DOUBLE_EQ(a, b) - /* Verify structural integrity of the ordered index after mutations. */ static ::testing::AssertionResult verifyIntegrity(OrderedIndexTestApi &api, OrderedIndex *oi) { char errmsg[256]; @@ -41,6 +38,13 @@ static ::testing::AssertionResult verifyIntegrity(OrderedIndexTestApi &api, Orde 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 { @@ -48,7 +52,9 @@ class OrderedIndexTest : public ::testing::TestWithParam OrderedIndexTestApi &api = *GetParam(); OrderedIndex *oi = nullptr; - void SetUp() override { oi = api.create(); } + void SetUp() override { + oi = api.create(); + } void TearDown() override { if (oi) api.free(oi); } @@ -74,7 +80,9 @@ class OrderedIndexTest : public ::testing::TestWithParam OrderedIndexItem *assertNextScore(OrderedIndexIterator *iter, double expected) { OrderedIndexItem *pos = api.next(iter); EXPECT_NE(pos, nullptr); - if (pos) { EXPECT_DOUBLE_EQ(api.getScore(pos), expected); } + if (pos) { + EXPECT_DOUBLE_EQ(api.getScore(pos), expected); + } return pos; } @@ -82,7 +90,9 @@ class OrderedIndexTest : public ::testing::TestWithParam OrderedIndexItem *assertPrevScore(OrderedIndexIterator *iter, double expected) { OrderedIndexItem *pos = api.prev(iter); EXPECT_NE(pos, nullptr); - if (pos) { EXPECT_DOUBLE_EQ(api.getScore(pos), expected); } + if (pos) { + EXPECT_DOUBLE_EQ(api.getScore(pos), expected); + } return pos; } @@ -95,52 +105,122 @@ class OrderedIndexTest : public ::testing::TestWithParam 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); + } + + /* RAII scoped iterator — auto-inits on construction, resets on destruction. */ + struct ScopedIter { + OrderedIndexTestApi &api; + OrderedIndexIterator iter; + ScopedIter(OrderedIndexTestApi &a, OrderedIndex *oi) : + api(a) { + api.initIterator(&iter, oi); + } + ~ScopedIter() { + api.resetIterator(&iter); + } + OrderedIndexItem *next() { + return api.next(&iter); + } + OrderedIndexItem *prev() { + return api.prev(&iter); + } + OrderedIndexIterator *operator->() { + return &iter; + } + OrderedIndexIterator *get() { + return &iter; + } + }; + + /* Create a scoped iterator for this fixture's oi. */ + ScopedIter iter() { + return ScopedIter(api, oi); + } + + /* Delete lex range using const char* (handles sds lifecycle). */ + unsigned long deleteLexRange(const char *min_str, const char *max_str, int min_ex = 0, int max_ex = 0, OrderedIndexOnDelete cb = NULL, void *ctx = NULL) { + 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 = 0, int max_ex = 0) { + 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(std::initializer_list expected) { + auto it = iter(); + OrderedIndexItem *pos; + auto exp = expected.begin(); + while ((pos = it.next()) != NULL) { + ASSERT_NE(exp, expected.end()) << "More elements than expected"; + assertElement(pos, *exp); + ++exp; + } + ASSERT_EQ(exp, expected.end()) << "Fewer elements than expected"; + } + /* Verify structural integrity. */ - void verifyOI() { ASSERT_TRUE(verifyIntegrity(api, oi)); } + void verifyOI() { + ASSERT_TRUE(verifyIntegrity(api, oi)); + } }; /* ========== Basic tests ========== */ TEST_P(OrderedIndexTest, CreateFree) { - TEST_ASSERT(oi != NULL); - TEST_ASSERT(api.length(oi) == 0); + ASSERT_NE(oi, nullptr); + ASSERT_EQ(api.length(oi), 0UL); verifyOI(); OrderedIndexIterator iter; - OrderedIndexItem *pos; api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) == NULL); + ASSERT_EQ(api.next(&iter), nullptr); api.resetIterator(&iter); - } TEST_P(OrderedIndexTest, InsertSingle) { - sds ele = sdsnew("test"); - OrderedIndexItem *node = api.insertSds(oi, 1.0, ele); + OrderedIndexItem *node = insert(1.0, "test"); verifyOI(); - TEST_ASSERT(node != NULL); - TEST_ASSERT(api.length(oi) == 1); - TEST_ASSERT_SCORE_EQ(api.getScore(node), 1.0); - + ASSERT_NE(node, nullptr); + ASSERT_EQ(api.length(oi), 1UL); + assertScore(node, 1.0); assertElement(node, "test"); OrderedIndexIterator iter; - OrderedIndexItem *pos; api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT(pos == node); - TEST_ASSERT((pos = api.next(&iter)) == NULL); + ASSERT_EQ(api.next(&iter), node); + ASSERT_EQ(api.next(&iter), nullptr); api.resetIterator(&iter); - - sdsfree(ele); } TEST_P(OrderedIndexTest, InsertMultipleOrdered) { - populateSequential(10); - TEST_ASSERT(api.length(oi) == 10); + ASSERT_EQ(api.length(oi), 10UL); verifyOI(); /* Verify forward traversal */ @@ -148,34 +228,30 @@ TEST_P(OrderedIndexTest, InsertMultipleOrdered) { OrderedIndexItem *pos; api.initIterator(&iter, oi); for (int i = 0; i < 10; i++) { - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)i); + ASSERT_NE((pos = api.next(&iter)), nullptr); + ASSERT_DOUBLE_EQ(api.getScore(pos), (double)i); } - TEST_ASSERT((pos = api.next(&iter)) == NULL); + ASSERT_EQ(api.next(&iter), nullptr); api.resetIterator(&iter); /* Verify backward traversal */ api.initIterator(&iter, oi); for (int i = 9; i >= 0; i--) { - TEST_ASSERT((pos = api.prev(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)i); + ASSERT_NE((pos = api.prev(&iter)), nullptr); + ASSERT_DOUBLE_EQ(api.getScore(pos), (double)i); } - TEST_ASSERT((pos = api.prev(&iter)) == NULL); + 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); - sds ele = sdsnew(buf); - api.insertSds(oi, 1.0, ele); - sdsfree(ele); + insert(1.0, buf); } - TEST_ASSERT(api.length(oi) == 5); + ASSERT_EQ(api.length(oi), 5UL); verifyOI(); /* Verify lexicographic ordering for same scores */ @@ -183,133 +259,105 @@ TEST_P(OrderedIndexTest, DuplicateScores) { OrderedIndexItem *pos; api.initIterator(&iter, oi); for (int i = 0; i < 5; i++) { - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); - const char *ptr; - size_t len; - api.getElementRaw(pos, &ptr, &len); - char expected[32]; - snprintf(expected, sizeof(expected), "key%d", i); - TEST_ASSERT(len == strlen(expected) && memcmp(ptr, expected, len) == 0); + pos = assertNextScore(&iter, 1.0); + assertElement(pos, (std::string("key") + std::to_string(i)).c_str()); } api.resetIterator(&iter); - } -TEST_P(OrderedIndexTest, RankOperations) { +TEST_P(OrderedIndexTest, IndexOperations) { OrderedIndexItem *nodes[10]; for (int i = 0; i < 10; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); - sds ele = sdsnew(buf); - nodes[i] = api.insertSds(oi, (double)i, ele); - sdsfree(ele); + nodes[i] = insert((double)i, buf); } verifyOI(); for (int i = 0; i < 10; i++) { - unsigned long rank = api.getRank(oi, nodes[i]); - TEST_ASSERT(rank == (unsigned long)(i + 1)); /* 1-based */ + unsigned long idx = api.getIndex(oi, nodes[i]); + ASSERT_EQ(idx, (unsigned long)i); } for (int i = 0; i < 10; i++) { - OrderedIndexItem *node = api.getByRank(oi, i + 1); - TEST_ASSERT(node == nodes[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); - sds ele = sdsnew(buf); - nodes[i] = api.insertSds(oi, (double)i, ele); - sdsfree(ele); + nodes[i] = insert((double)i, buf); } - - TEST_ASSERT(api.length(oi) == 5); + ASSERT_EQ(api.length(oi), 5UL); api.deleteItem(oi, nodes[2]); - TEST_ASSERT(api.length(oi) == 4); + ASSERT_EQ(api.length(oi), 4UL); verifyOI(); OrderedIndexIterator iter; - OrderedIndexItem *pos; api.initIterator(&iter, oi); assertNextScore(&iter, 0.0); - pos = assertNextScore(&iter, 1.0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 3.0); /* Skipped 2.0 */ + assertNextScore(&iter, 1.0); + assertNextScore(&iter, 3.0); /* Skipped 2.0 */ assertNextScore(&iter, 4.0); api.resetIterator(&iter); - } TEST_P(OrderedIndexTest, PopFirst) { - - TEST_ASSERT(api.popFirst(oi) == NULL); + ASSERT_EQ(api.popFirst(oi), nullptr); populateSequential(5); - TEST_ASSERT(api.length(oi) == 5); + ASSERT_EQ(api.length(oi), 5UL); OrderedIndexItem *item = api.popFirst(oi); - TEST_ASSERT(item != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(item), 0.0); + ASSERT_NE(item, nullptr); + assertScore(item, 0.0); assertElement(item, "key0"); api.freeItem(item); - TEST_ASSERT(api.length(oi) == 4); + ASSERT_EQ(api.length(oi), 4UL); verifyOI(); item = api.popFirst(oi); - TEST_ASSERT_SCORE_EQ(api.getScore(item), 1.0); + assertScore(item, 1.0); api.freeItem(item); - TEST_ASSERT(api.length(oi) == 3); + ASSERT_EQ(api.length(oi), 3UL); verifyOI(); - } TEST_P(OrderedIndexTest, PopLast) { - - TEST_ASSERT(api.popLast(oi) == NULL); + ASSERT_EQ(api.popLast(oi), nullptr); populateSequential(5); - TEST_ASSERT(api.length(oi) == 5); + ASSERT_EQ(api.length(oi), 5UL); OrderedIndexItem *item = api.popLast(oi); - TEST_ASSERT(item != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(item), 4.0); + ASSERT_NE(item, nullptr); + assertScore(item, 4.0); assertElement(item, "key4"); api.freeItem(item); - TEST_ASSERT(api.length(oi) == 4); + ASSERT_EQ(api.length(oi), 4UL); verifyOI(); item = api.popLast(oi); - TEST_ASSERT_SCORE_EQ(api.getScore(item), 3.0); + assertScore(item, 3.0); api.freeItem(item); - TEST_ASSERT(api.length(oi) == 3); + ASSERT_EQ(api.length(oi), 3UL); verifyOI(); - } TEST_P(OrderedIndexTest, UpdateScore) { - - sds ele1 = sdsnew("key1"); - sds ele2 = sdsnew("key2"); - sds ele3 = sdsnew("key3"); - OrderedIndexItem *node1 = api.insertSds(oi, 1.0, ele1); - OrderedIndexItem *node2 = api.insertSds(oi, 2.0, ele2); - api.insertSds(oi, 3.0, ele3); - sdsfree(ele1); - sdsfree(ele2); - sdsfree(ele3); + OrderedIndexItem *node1 = insert(1.0, "key1"); + OrderedIndexItem *node2 = insert(2.0, "key2"); + insert(3.0, "key3"); OrderedIndexItem *updated = api.updateScore(oi, node2, 4.0); - TEST_ASSERT(updated != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(updated), 4.0); + ASSERT_NE(updated, nullptr); + assertScore(updated, 4.0); verifyOI(); assertElement(updated, "key2"); @@ -325,50 +373,43 @@ TEST_P(OrderedIndexTest, UpdateScore) { /* Update to same score (no-op) */ updated = api.updateScore(oi, node1, 1.0); - TEST_ASSERT_SCORE_EQ(api.getScore(updated), 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); - TEST_ASSERT(deleted == 4); /* 3, 4, 5, 6 */ - TEST_ASSERT(api.length(oi) == 6); + ASSERT_EQ(deleted, 4UL); + ASSERT_EQ(api.length(oi), 6UL); verifyOI(); OrderedIndexIterator iter; - OrderedIndexItem *pos; api.initIterator(&iter, oi); for (int i = 0; i < 3; i++) { - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)i); + assertNextScore(&iter, (double)i); } for (int i = 7; i < 10; i++) { - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)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); - TEST_ASSERT(deleted == 1); - TEST_ASSERT(api.length(oi) == 5); + ASSERT_EQ(deleted, 1UL); + ASSERT_EQ(api.length(oi), 5UL); verifyOI(); - } -TEST_P(OrderedIndexTest, DeleteRangeByRank) { - +TEST_P(OrderedIndexTest, DeleteRangeByIndex) { populateSequential(10); - /* Delete ranks 3-5 (1-based, so elements at scores 2,3,4) */ - unsigned long deleted = api.deleteRangeByRank(oi, 3, 5, NULL, NULL); - TEST_ASSERT(deleted == 3); - TEST_ASSERT(api.length(oi) == 7); + /* 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; @@ -376,21 +417,18 @@ TEST_P(OrderedIndexTest, DeleteRangeByRank) { assertNextScore(&iter, 0.0); api.resetIterator(&iter); - /* Verify rank 3 is now score 5 (was rank 6) */ - OrderedIndexItem *node = api.getByRank(oi, 3); - TEST_ASSERT_SCORE_EQ(api.getScore(node), 5.0); - + /* Verify index 2 is now score 5 (was index 5) */ + OrderedIndexItem *node = api.getByIndex(oi, 2); + assertScore(node, 5.0); } -TEST_P(OrderedIndexTest, MixedOperationsRankIntegrity) { +TEST_P(OrderedIndexTest, MixedOperationsIndexIntegrity) { OrderedIndexItem *nodes[100]; for (int i = 0; i < 100; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); - sds ele = sdsnew(buf); - nodes[i] = api.insertSds(oi, (double)i, ele); - sdsfree(ele); + nodes[i] = insert((double)i, buf); } for (int i = 2; i < 100; i += 3) { @@ -399,21 +437,21 @@ TEST_P(OrderedIndexTest, MixedOperationsRankIntegrity) { } verifyOI(); - if (nodes[10]) nodes[10] = api.updateScore(oi, nodes[10], 150.0); - if (nodes[20]) nodes[20] = api.updateScore(oi, nodes[20], 160.0); + /* 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 expected_rank = 1; + unsigned long expectedIdx = 0; while (((pos = api.next(&iter)) != NULL)) { - unsigned long actual_rank = api.getRank(oi, pos); - TEST_ASSERT(actual_rank == expected_rank); - expected_rank++; + unsigned long actualIdx = api.getIndex(oi, pos); + ASSERT_EQ(actualIdx, expectedIdx); + expectedIdx++; } api.resetIterator(&iter); - } TEST_P(OrderedIndexTest, BackwardTraversalAfterDeletions) { @@ -422,9 +460,7 @@ TEST_P(OrderedIndexTest, BackwardTraversalAfterDeletions) { for (int i = 0; i < 20; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); - sds ele = sdsnew(buf); - nodes[i] = api.insertSds(oi, (double)i, ele); - sdsfree(ele); + nodes[i] = insert((double)i, buf); } api.deleteItem(oi, nodes[5]); @@ -433,50 +469,32 @@ TEST_P(OrderedIndexTest, BackwardTraversalAfterDeletions) { verifyOI(); OrderedIndexIterator iter; - OrderedIndexItem *pos; api.initIterator(&iter, oi); int expected_scores[] = {19, 18, 17, 16, 14, 13, 12, 11, 9, 8, 7, 6, 4, 3, 2, 1, 0}; - int idx_score = 0; - while (((pos = api.prev(&iter)) != NULL)) { - TEST_ASSERT_SCORE_EQ(api.getScore(pos), (double)expected_scores[idx_score]); - idx_score++; + for (int i = 0; i < 17; i++) { + assertPrevScore(&iter, (double)expected_scores[i]); } - TEST_ASSERT(idx_score == 17); /* Should have traversed all 17 remaining elements */ + ASSERT_EQ(api.prev(&iter), nullptr); api.resetIterator(&iter); - } TEST_P(OrderedIndexTest, LexicographicEdgeCases) { - - sds empty = sdsnew(""); - sds a = sdsnew("a"); - sds z = sdsnew("z"); - - api.insertSds(oi, 1.0, z); - api.insertSds(oi, 1.0, empty); - api.insertSds(oi, 1.0, a); + insert(1.0, "z"); + insert(1.0, ""); + insert(1.0, "a"); /* Verify lexicographic order: "", "a", "z" */ OrderedIndexIterator iter; OrderedIndexItem *pos; - const char *ptr; - size_t len; api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 1 && memcmp(ptr, "a", 1) == 0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 1 && memcmp(ptr, "z", 1) == 0); + 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); - - sdsfree(empty); - sdsfree(a); - sdsfree(z); api.free(oi); /* Test very long string (1KB) */ @@ -484,66 +502,41 @@ TEST_P(OrderedIndexTest, LexicographicEdgeCases) { char long_buf[1024]; memset(long_buf, 'x', 1023); long_buf[1023] = '\0'; - sds long_str = sdsnew(long_buf); - sds short_str = sdsnew("short"); - - api.insertSds(oi, 1.0, long_str); - api.insertSds(oi, 1.0, short_str); + insert(1.0, long_buf); + insert(1.0, "short"); api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 5 && memcmp(ptr, "short", 5) == 0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 1023 && memcmp(ptr, long_buf, 1023) == 0); + pos = assertNextScore(&iter, 1.0); + assertElement(pos, "short"); + pos = assertNextScore(&iter, 1.0); + assertElement(pos, long_buf); api.resetIterator(&iter); - - sdsfree(long_str); - sdsfree(short_str); } TEST_P(OrderedIndexTest, RangeBoundaryPrecision) { - double base = 1.0; double epsilon = 1e-10; - sds ele1 = sdsnew("at_base"); - sds ele2 = sdsnew("at_base_plus_epsilon"); - sds ele3 = sdsnew("at_base_plus_2epsilon"); - - api.insertSds(oi, base, ele1); - api.insertSds(oi, base + epsilon, ele2); - api.insertSds(oi, base + 2 * epsilon, ele3); + 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); - TEST_ASSERT(deleted == 1); - TEST_ASSERT(api.length(oi) == 2); + 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); - - sdsfree(ele1); - sdsfree(ele2); - sdsfree(ele3); } TEST_P(OrderedIndexTest, SpecialDoubleValues) { - const char *ptr; - size_t len; - - sds neg_inf = sdsnew("neg_inf"); - sds pos_inf = sdsnew("pos_inf"); - sds zero = sdsnew("zero"); - sds one = sdsnew("one"); - - api.insertSds(oi, NEG_INF, neg_inf); - api.insertSds(oi, POS_INF, pos_inf); - api.insertSds(oi, 0.0, zero); - api.insertSds(oi, 1.0, one); + 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; @@ -554,94 +547,66 @@ TEST_P(OrderedIndexTest, SpecialDoubleValues) { assertNextScore(&iter, 1.0); assertNextScore(&iter, POS_INF); api.resetIterator(&iter); - - sdsfree(neg_inf); - sdsfree(pos_inf); - sdsfree(zero); - sdsfree(one); api.free(oi); /* Test +0.0 vs -0.0 */ oi = api.create(); - sds pos_zero = sdsnew("pos_zero"); - sds neg_zero = sdsnew("neg_zero"); - - api.insertSds(oi, 0.0, pos_zero); - api.insertSds(oi, -0.0, neg_zero); + insert(0.0, "pos_zero"); + insert(-0.0, "neg_zero"); /* Both should be in the list, ordered lexicographically since scores are equal */ - TEST_ASSERT(api.length(oi) == 2); + ASSERT_EQ(api.length(oi), 2UL); api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 8 && memcmp(ptr, "neg_zero", 8) == 0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 8 && memcmp(ptr, "pos_zero", 8) == 0); + pos = assertNextScore(&iter, 0.0); + assertElement(pos, "neg_zero"); + pos = assertNextScore(&iter, 0.0); + assertElement(pos, "pos_zero"); api.resetIterator(&iter); - - sdsfree(pos_zero); - sdsfree(neg_zero); api.free(oi); /* Test denormalized double */ oi = api.create(); double denorm = 1e-320; /* Denormalized double */ - sds denorm_ele = sdsnew("denorm"); - sds normal_ele = sdsnew("normal"); + insert(denorm, "denorm"); + insert(1.0, "normal"); - api.insertSds(oi, denorm, denorm_ele); - api.insertSds(oi, 1.0, normal_ele); - - TEST_ASSERT(api.length(oi) == 2); + ASSERT_EQ(api.length(oi), 2UL); api.initIterator(&iter, oi); pos = assertNextScore(&iter, denorm); - TEST_ASSERT(api.getScore(pos) < 1.0); + ASSERT_TRUE(api.getScore(pos) < 1.0); api.resetIterator(&iter); - - sdsfree(denorm_ele); - sdsfree(normal_ele); } -TEST_P(OrderedIndexTest, EdgeCases) { - - TEST_ASSERT(api.length(oi) == 0); +TEST_P(OrderedIndexTest, EmptyIndexOperations) { + ASSERT_EQ(api.length(oi), 0UL); OrderedIndexIterator iter; - OrderedIndexItem *pos; api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) == NULL); - TEST_ASSERT((pos = api.prev(&iter)) == NULL); + ASSERT_EQ(api.next(&iter), nullptr); + ASSERT_EQ(api.prev(&iter), nullptr); api.resetIterator(&iter); - TEST_ASSERT(api.getByRank(oi, 1) == NULL); - + ASSERT_EQ(api.getByIndex(oi, 0), nullptr); } TEST_P(OrderedIndexTest, DeleteEdgeCases) { - /* Delete only element */ - sds ele = sdsnew("only"); - OrderedIndexItem *node = api.insertSds(oi, 1.0, ele); + OrderedIndexItem *node = insert(1.0, "only"); api.deleteItem(oi, node); - TEST_ASSERT(api.length(oi) == 0); + ASSERT_EQ(api.length(oi), 0UL); verifyOI(); OrderedIndexIterator iter; - OrderedIndexItem *pos; api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) == NULL); + ASSERT_EQ(api.next(&iter), nullptr); api.resetIterator(&iter); - sdsfree(ele); /* Delete first element */ OrderedIndexItem *nodes[3]; for (int i = 0; i < 3; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); - sds e = sdsnew(buf); - nodes[i] = api.insertSds(oi, (double)i, e); - sdsfree(e); + nodes[i] = insert((double)i, buf); } api.deleteItem(oi, nodes[0]); - TEST_ASSERT(api.length(oi) == 2); + ASSERT_EQ(api.length(oi), 2UL); verifyOI(); api.initIterator(&iter, oi); assertNextScore(&iter, 1.0); @@ -649,101 +614,94 @@ TEST_P(OrderedIndexTest, DeleteEdgeCases) { /* Delete last element */ api.deleteItem(oi, nodes[2]); - TEST_ASSERT(api.length(oi) == 1); + ASSERT_EQ(api.length(oi), 1UL); verifyOI(); api.initIterator(&iter, oi); assertPrevScore(&iter, 1.0); api.resetIterator(&iter); - } -TEST_P(OrderedIndexTest, RankEdgeCases) { - +TEST_P(OrderedIndexTest, IndexEdgeCases) { populateSequential(5); - TEST_ASSERT(api.getByRank(oi, 6) == NULL); - TEST_ASSERT(api.getByRank(oi, 100) == NULL); - TEST_ASSERT(api.getByRank(oi, 1) != NULL); - TEST_ASSERT(api.getByRank(oi, 5) != NULL); - + 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) { - - sds ele1 = sdsnew("duplicate"); - sds ele2 = sdsnew("duplicate"); - OrderedIndexItem *node1 = api.insertSds(oi, 1.0, ele1); - OrderedIndexItem *node2 = api.insertSds(oi, 1.0, ele2); + OrderedIndexItem *node1 = insert(1.0, "duplicate"); + OrderedIndexItem *node2 = insert(1.0, "duplicate"); /* Should have 2 nodes (duplicates allowed) */ - TEST_ASSERT(api.length(oi) == 2); - TEST_ASSERT(node1 != node2); - - sdsfree(ele1); - sdsfree(ele2); + ASSERT_EQ(api.length(oi), 2UL); + ASSERT_NE(node1, node2); } TEST_P(OrderedIndexTest, UpdateScoreEdgeCases) { + populateSequential(5); /* scores: 0, 1, 2, 3, 4 */ - populateSequential(5); - - /* Update first element to move backward */ - OrderedIndexItem *first = api.getByRank(oi, 1); - OrderedIndexItem *updated = api.updateScore(oi, first, -1.0); - TEST_ASSERT_SCORE_EQ(api.getScore(updated), -1.0); + /* 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; - OrderedIndexItem *pos; api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - TEST_ASSERT(pos == updated); + ASSERT_EQ(api.prev(&iter), updated); /* now last */ api.resetIterator(&iter); - /* Update last element to move forward */ - unsigned long len = api.length(oi); - OrderedIndexItem *last = api.getByRank(oi, len); - updated = api.updateScore(oi, last, 10.0); - TEST_ASSERT_SCORE_EQ(api.getScore(updated), 10.0); + /* 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); - TEST_ASSERT((pos = api.prev(&iter)) != NULL); - TEST_ASSERT(pos == updated); + ASSERT_EQ(api.next(&iter), updated); /* now first */ api.resetIterator(&iter); - /* Update middle element to move backward */ - OrderedIndexItem *middle = api.getByRank(oi, 3); - double old_score = api.getScore(middle); + /* Move middle element backward past multiple */ + OrderedIndexItem *middle = api.getByIndex(oi, 2); updated = api.updateScore(oi, middle, 0.5); - TEST_ASSERT_SCORE_EQ(api.getScore(updated), 0.5); - TEST_ASSERT(api.getScore(updated) < old_score); + 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); - TEST_ASSERT(deleted == 0); - TEST_ASSERT(api.length(oi) == 10); + 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); - TEST_ASSERT(deleted == 0); - TEST_ASSERT(api.length(oi) == 10); + ASSERT_EQ(deleted, 0UL); + ASSERT_EQ(api.length(oi), 10UL); - /* Delete first elements by rank */ - deleted = api.deleteRangeByRank(oi, 1, 2, NULL, NULL); - TEST_ASSERT(deleted == 2); + /* 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 rank */ + /* Delete last elements by index */ unsigned long len = api.length(oi); - deleted = api.deleteRangeByRank(oi, len - 1, len, NULL, NULL); - TEST_ASSERT(deleted == 2); + deleted = api.deleteRangeByIndex(oi, len - 2, len - 1, NULL, NULL); + ASSERT_EQ(deleted, 2UL); verifyOI(); api.initIterator(&iter, oi); assertPrevScore(&iter, 7.0); @@ -751,99 +709,85 @@ TEST_P(OrderedIndexTest, RangeDeleteEdgeCases) { /* Delete entire remaining index by score */ deleted = api.deleteRangeByScore(oi, -100.0, 100.0, 0, 0, NULL, NULL); - TEST_ASSERT(deleted == 6); - TEST_ASSERT(api.length(oi) == 0); + ASSERT_EQ(deleted, 6UL); + ASSERT_EQ(api.length(oi), 0UL); verifyOI(); - } TEST_P(OrderedIndexTest, TraversalEdgeCases) { - - sds ele = sdsnew("single"); - api.insertSds(oi, 1.0, ele); + insert(1.0, "single"); OrderedIndexIterator iter; - OrderedIndexItem *pos; api.initIterator(&iter, oi); - pos = assertNextScore(&iter, 1.0); - TEST_ASSERT((pos = api.next(&iter)) == NULL); + assertNextScore(&iter, 1.0); + ASSERT_EQ(api.next(&iter), nullptr); api.resetIterator(&iter); api.initIterator(&iter, oi); - pos = assertPrevScore(&iter, 1.0); - TEST_ASSERT((pos = api.prev(&iter)) == NULL); + assertPrevScore(&iter, 1.0); + ASSERT_EQ(api.prev(&iter), nullptr); api.resetIterator(&iter); - - sdsfree(ele); } -TEST_P(OrderedIndexTest, SeekToRank) { - +TEST_P(OrderedIndexTest, SeekToIndex) { for (int i = 1; i <= 5; i++) { char buf[32]; snprintf(buf, sizeof(buf), "key%d", i); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); + insert((double)i, buf); } OrderedIndexIterator iter; - OrderedIndexItem *pos; - /* Seek to rank 0 (before first) */ + /* Seek to index 0 (first element) */ api.initIterator(&iter, oi); - api.seekToRank(&iter, 0); - assertNextScore(&iter, 1.0); + api.seekToIndex(&iter, 0); + assertNextScore(&iter, 2.0); /* next after first = second */ api.resetIterator(&iter); api.initIterator(&iter, oi); - api.seekToRank(&iter, 0); - TEST_ASSERT((pos = api.prev(&iter)) == NULL); + api.seekToIndex(&iter, 0); + assertPrevScore(&iter, 1.0); /* prev at first = first itself */ api.resetIterator(&iter); - /* Seek to rank 1 */ + /* Seek to index 1 (second element) */ api.initIterator(&iter, oi); - api.seekToRank(&iter, 1); - assertNextScore(&iter, 2.0); + api.seekToIndex(&iter, 1); + assertNextScore(&iter, 3.0); api.resetIterator(&iter); api.initIterator(&iter, oi); - api.seekToRank(&iter, 1); - assertPrevScore(&iter, 1.0); + api.seekToIndex(&iter, 1); + assertPrevScore(&iter, 2.0); api.resetIterator(&iter); - /* Seek to rank 3 (middle) */ + /* Seek to index 2 (middle) */ api.initIterator(&iter, oi); - api.seekToRank(&iter, 3); + api.seekToIndex(&iter, 2); assertNextScore(&iter, 4.0); api.resetIterator(&iter); api.initIterator(&iter, oi); - api.seekToRank(&iter, 3); + api.seekToIndex(&iter, 2); assertPrevScore(&iter, 3.0); api.resetIterator(&iter); - /* Seek to rank 5 (last) */ + /* Seek to index 4 (last) */ api.initIterator(&iter, oi); - api.seekToRank(&iter, 5); - TEST_ASSERT((pos = api.next(&iter)) == NULL); + api.seekToIndex(&iter, 4); + ASSERT_EQ(api.next(&iter), nullptr); api.resetIterator(&iter); api.initIterator(&iter, oi); - api.seekToRank(&iter, 5); + 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); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); + insert((double)i, buf); } OrderedIndexIterator iter; @@ -854,11 +798,11 @@ TEST_P(OrderedIndexTest, ReverseIteration) { int count = 0; double expected = 5.0; while (((pos = api.prev(&iter)) != NULL)) { - TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); + assertScore(pos, expected); expected -= 1.0; count++; } - TEST_ASSERT(count == 5); + ASSERT_EQ(count, 5); api.resetIterator(&iter); /* Reverse then forward */ @@ -872,22 +816,17 @@ TEST_P(OrderedIndexTest, ReverseIteration) { 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); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)(i * 2), ele); - sdsfree(ele); + insert((double)(i * 2), buf); } OrderedIndexIterator iter; - OrderedIndexItem *pos; /* Seek to first in range [2, 6] with offset 0 */ api.initIterator(&iter, oi); @@ -916,25 +855,25 @@ TEST_P(OrderedIndexTest, SeekToScoreRange) { /* Seek to empty range above all elements */ api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 10.0, 20.0, 0, 0, 0); - TEST_ASSERT((pos = api.next(&iter)) == NULL); + 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); - TEST_ASSERT((pos = api.next(&iter)) == NULL); + 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); - TEST_ASSERT((pos = api.next(&iter)) == NULL); + 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); - TEST_ASSERT((pos = api.prev(&iter)) == NULL); + ASSERT_EQ(api.prev(&iter), nullptr); api.resetIterator(&iter); /* Second from last with offset -2, positioned for prev() */ @@ -946,13 +885,11 @@ TEST_P(OrderedIndexTest, SeekToScoreRange) { /* Empty range where min > max */ api.initIterator(&iter, oi); api.seekToScoreRange(&iter, 6.0, 2.0, 0, 0, 0); - TEST_ASSERT((pos = api.next(&iter)) == NULL); + ASSERT_EQ(api.next(&iter), nullptr); api.resetIterator(&iter); - } TEST_P(OrderedIndexTest, SeekToScoreRangeIteration) { - populateSequential(10); OrderedIndexIterator iter; @@ -964,11 +901,11 @@ TEST_P(OrderedIndexTest, SeekToScoreRangeIteration) { int count = 0; double expected = 3.0; while (((pos = api.next(&iter)) != NULL) && api.getScore(pos) <= 7.0) { - TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); + assertScore(pos, expected); expected += 1.0; count++; } - TEST_ASSERT(count == 5); + ASSERT_EQ(count, 5); api.resetIterator(&iter); /* Seek to last in range and iterate backward */ @@ -977,11 +914,11 @@ TEST_P(OrderedIndexTest, SeekToScoreRangeIteration) { count = 0; expected = 7.0; while (((pos = api.prev(&iter)) != NULL) && api.getScore(pos) >= 3.0) { - TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); + assertScore(pos, expected); expected -= 1.0; count++; } - TEST_ASSERT(count == 5); + ASSERT_EQ(count, 5); api.resetIterator(&iter); /* Seek with offset and continue iteration */ @@ -990,17 +927,13 @@ TEST_P(OrderedIndexTest, SeekToScoreRangeIteration) { 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); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); + insert((double)i, buf); } OrderedIndexIterator iter; @@ -1011,23 +944,19 @@ TEST_P(OrderedIndexTest, SeekInfReverseIteration) { int count = 0; double expected = 5.0; while (((pos = api.prev(&iter)) != NULL)) { - TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); + assertScore(pos, expected); expected -= 1.0; count++; } - TEST_ASSERT(count == 5); + 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); - sds ele = sdsnew(buf); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); + insert((double)i, buf); } OrderedIndexIterator iter; @@ -1038,277 +967,120 @@ TEST_P(OrderedIndexTest, SeekInfForwardIteration) { int count = 0; double expected = 1.0; while (((pos = api.next(&iter)) != NULL)) { - TEST_ASSERT_SCORE_EQ(api.getScore(pos), expected); + assertScore(pos, expected); expected += 1.0; count++; } - TEST_ASSERT(count == 5); + ASSERT_EQ(count, 5); api.resetIterator(&iter); - } TEST_P(OrderedIndexTest, SeekToLexRange) { + for (int i = 0; i < FRUITS_COUNT; i++) insert(1.0, FRUITS[i]); - const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; - for (int i = 0; i < 5; i++) { - sds ele = sdsnew(elements[i]); - api.insertSds(oi, 1.0, ele); - sdsfree(ele); - } - - OrderedIndexIterator iter; + OrderedIndexIterator it; OrderedIndexItem *pos; - const char *ptr; - size_t len; - - sds minLex = sdsnew("banana"); - sds maxLex = sdsnew("date"); /* Seek to first in lex range [banana, date] with offset 0 */ - api.initIterator(&iter, oi); - api.seekToLexRange(&iter, minLex, maxLex, 0, 0, 0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 6 && memcmp(ptr, "banana", 6) == 0); - api.resetIterator(&iter); + 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(&iter, oi); - api.seekToLexRange(&iter, minLex, maxLex, 0, 0, 1); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 6 && memcmp(ptr, "cherry", 6) == 0); - api.resetIterator(&iter); + 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 */ /* Seek to last in lex range with offset -1, positioned for prev() */ - api.initIterator(&iter, oi); - api.seekToLexRange(&iter, minLex, maxLex, 0, 0, -1); - TEST_ASSERT((pos = api.prev(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 4 && memcmp(ptr, "date", 4) == 0); - api.resetIterator(&iter); + 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(&iter, oi); - api.seekToLexRange(&iter, minLex, maxLex, 1, 1, 0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 6 && memcmp(ptr, "cherry", 6) == 0); - api.resetIterator(&iter); - - sdsfree(minLex); - sdsfree(maxLex); + 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 */ - sds minEmpty = sdsnew("zzz"); - sds maxEmpty = sdsnew("zzzz"); - api.initIterator(&iter, oi); - api.seekToLexRange(&iter, minEmpty, maxEmpty, 0, 0, 0); - TEST_ASSERT((pos = api.next(&iter)) == NULL); - api.resetIterator(&iter); - sdsfree(minEmpty); - sdsfree(maxEmpty); + 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 */ - minLex = sdsnew("banana"); - maxLex = sdsnew("date"); - api.initIterator(&iter, oi); - api.seekToLexRange(&iter, minLex, maxLex, 0, 0, 10); - TEST_ASSERT((pos = api.next(&iter)) == NULL); - api.resetIterator(&iter); - sdsfree(minLex); - sdsfree(maxLex); - + 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]); - const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; - for (int i = 0; i < 5; i++) { - sds ele = sdsnew(elements[i]); - api.insertSds(oi, 1.0, ele); - sdsfree(ele); - } - - sds min = sdsnew("banana"); - sds max = sdsnew("date"); - unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); - TEST_ASSERT(deleted == 3); - TEST_ASSERT(api.length(oi) == 2); + ASSERT_EQ(deleteLexRange("banana", "date"), 3UL); + ASSERT_EQ(api.length(oi), 2UL); verifyOI(); - - OrderedIndexIterator iter; - OrderedIndexItem *pos; - const char *ptr; - size_t len; - api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 5 && memcmp(ptr, "apple", 5) == 0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 10 && memcmp(ptr, "elderberry", 10) == 0); - TEST_ASSERT((pos = api.next(&iter)) == NULL); - api.resetIterator(&iter); - - sdsfree(min); - sdsfree(max); + assertAllElements({"apple", "elderberry"}); } TEST_P(OrderedIndexTest, DeleteRangeByLexExclusive) { + for (int i = 0; i < FRUITS_COUNT; i++) insert(1.0, FRUITS[i]); - const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; - for (int i = 0; i < 5; i++) { - sds ele = sdsnew(elements[i]); - api.insertSds(oi, 1.0, ele); - sdsfree(ele); - } - - sds min = sdsnew("banana"); - sds max = sdsnew("date"); - unsigned long deleted = api.deleteRangeByLex(oi, min, max, 1, 1, NULL, NULL); - TEST_ASSERT(deleted == 1); - TEST_ASSERT(api.length(oi) == 4); - - OrderedIndexIterator iter; - OrderedIndexItem *pos; - const char *ptr; - size_t len; - api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 5 && memcmp(ptr, "apple", 5) == 0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 6 && memcmp(ptr, "banana", 6) == 0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 4 && memcmp(ptr, "date", 4) == 0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 10 && memcmp(ptr, "elderberry", 10) == 0); - TEST_ASSERT((pos = api.next(&iter)) == NULL); - api.resetIterator(&iter); - - sdsfree(min); - sdsfree(max); + ASSERT_EQ(deleteLexRange("banana", "date", 1, 1), 1UL); + ASSERT_EQ(api.length(oi), 4UL); + assertAllElements({"apple", "banana", "date", "elderberry"}); } -TEST_P(OrderedIndexTest, DeleteRangeByLexBoundaryCases) { - /* Empty range: min > max lexicographically */ - const char *elements[] = {"apple", "banana", "cherry"}; - for (int i = 0; i < 3; i++) { - sds ele = sdsnew(elements[i]); - api.insertSds(oi, 1.0, ele); - sdsfree(ele); - } +TEST_P(OrderedIndexTest, DeleteRangeByLex_EmptyRange) { + for (int i = 0; i < 3; i++) insert(1.0, FRUITS[i]); - sds min = sdsnew("zzz"); - sds max = sdsnew("aaa"); - unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); - TEST_ASSERT(deleted == 0); - TEST_ASSERT(api.length(oi) == 3); - sdsfree(min); - sdsfree(max); - api.free(oi); - - /* Delete all elements */ - oi = api.create(); - for (int i = 0; i < 3; i++) { - sds ele = sdsnew(elements[i]); - api.insertSds(oi, 1.0, ele); - sdsfree(ele); - } - - min = sdsnew("a"); - max = sdsnew("z"); - deleted = api.deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); - TEST_ASSERT(deleted == 3); - TEST_ASSERT(api.length(oi) == 0); - sdsfree(min); - sdsfree(max); - api.free(oi); + ASSERT_EQ(deleteLexRange("zzz", "aaa"), 0UL); + ASSERT_EQ(api.length(oi), 3UL); +} - /* Delete single element */ - oi = api.create(); - for (int i = 0; i < 3; i++) { - sds ele = sdsnew(elements[i]); - api.insertSds(oi, 1.0, ele); - sdsfree(ele); - } +TEST_P(OrderedIndexTest, DeleteRangeByLex_All) { + for (int i = 0; i < 3; i++) insert(1.0, FRUITS[i]); - min = sdsnew("banana"); - max = sdsnew("banana"); - deleted = api.deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); - TEST_ASSERT(deleted == 1); - TEST_ASSERT(api.length(oi) == 2); + ASSERT_EQ(deleteLexRange("a", "z"), 3UL); + ASSERT_EQ(api.length(oi), 0UL); +} - OrderedIndexIterator iter; - OrderedIndexItem *pos; - const char *ptr; - size_t len; - api.initIterator(&iter, oi); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 5 && memcmp(ptr, "apple", 5) == 0); - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == 6 && memcmp(ptr, "cherry", 6) == 0); - TEST_ASSERT((pos = api.next(&iter)) == NULL); - api.resetIterator(&iter); +TEST_P(OrderedIndexTest, DeleteRangeByLex_SingleElement) { + for (int i = 0; i < 3; i++) insert(1.0, FRUITS[i]); - sdsfree(min); - sdsfree(max); + ASSERT_EQ(deleteLexRange("banana", "banana"), 1UL); + ASSERT_EQ(api.length(oi), 2UL); + assertAllElements({"apple", "cherry"}); } TEST_P(OrderedIndexTest, DeleteRangeByLexPreservesOutside) { + for (int i = 0; i < NATO_COUNT; i++) insert(1.0, NATO[i]); - const char *elements[] = {"alpha", "bravo", "charlie", "delta", "echo", "foxtrot"}; - for (int i = 0; i < 6; i++) { - sds ele = sdsnew(elements[i]); - api.insertSds(oi, 1.0, ele); - sdsfree(ele); - } - - sds min = sdsnew("charlie"); - sds max = sdsnew("delta"); - unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); - TEST_ASSERT(deleted == 2); - TEST_ASSERT(api.length(oi) == 4); - - const char *expected[] = {"alpha", "bravo", "echo", "foxtrot"}; - size_t expected_lens[] = {5, 5, 4, 7}; - OrderedIndexIterator iter; - OrderedIndexItem *pos; - const char *ptr; - size_t len; - api.initIterator(&iter, oi); - for (int i = 0; i < 4; i++) { - TEST_ASSERT((pos = api.next(&iter)) != NULL); - api.getElementRaw(pos, &ptr, &len); - TEST_ASSERT(len == expected_lens[i] && memcmp(ptr, expected[i], len) == 0); - } - TEST_ASSERT((pos = api.next(&iter)) == NULL); - api.resetIterator(&iter); + ASSERT_EQ(deleteLexRange("charlie", "delta"), 2UL); + ASSERT_EQ(api.length(oi), 4UL); + assertAllElements({"alpha", "bravo", "echo", "foxtrot"}); /* Verify scores are preserved */ - api.initIterator(&iter, oi); - while (((pos = api.next(&iter)) != NULL)) { - TEST_ASSERT_SCORE_EQ(api.getScore(pos), 1.0); + auto it = iter(); + OrderedIndexItem *pos; + while ((pos = it.next()) != NULL) { + assertScore(pos, 1.0); } - api.resetIterator(&iter); - /* Verify ranks are correct after deletion */ - for (unsigned long r = 1; r <= 4; r++) { - OrderedIndexItem *node = api.getByRank(oi, r); - TEST_ASSERT(node != NULL); - unsigned long rank = api.getRank(oi, node); - TEST_ASSERT(rank == r); + /* 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); } - - sdsfree(min); - sdsfree(max); } /* ========== Randomized property tests ========== */ @@ -1338,10 +1110,8 @@ static std::vector test_build_random_index(OrderedIndexTestApi for (int i = 0; i < count; i++) { double score = test_random_score(rng); std::string elem = test_random_element(rng) + std::to_string(i); - sds ele = sdsnew(elem.c_str()); - OrderedIndexItem *node = api.insertSds(oi, score, ele); + OrderedIndexItem *node = api.insert(oi, score, elem.c_str(), elem.size()); entries.push_back({node, score, elem}); - sdsfree(ele); } return entries; } @@ -1352,7 +1122,7 @@ TEST_P(OrderedIndexTest, RandomizedInsertAndTraversal) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, rng, n); ASSERT_EQ(api.length(oi), (unsigned long)n); verifyOI(); @@ -1381,7 +1151,7 @@ TEST_P(OrderedIndexTest, RandomizedBackwardTraversal) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, rng, n); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1407,36 +1177,36 @@ TEST_P(OrderedIndexTest, RandomizedScoreRetrieval) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - auto entries = test_build_random_index(api, oi, rng, n); + auto entries = test_build_random_index(api, oi, rng, n); for (auto &e : entries) { - TEST_ASSERT_SCORE_EQ(api.getScore(e.node), e.score); + assertScore(e.node, e.score); } api.free(oi); oi = api.create(); } } -TEST_P(OrderedIndexTest, RandomizedRankConsistency) { +TEST_P(OrderedIndexTest, RandomizedIndexConsistency) { std::mt19937 rng(42); for (int trial = 0; trial < 20; trial++) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, rng, n); OrderedIndexIterator iter; OrderedIndexItem *pos; api.initIterator(&iter, oi); - unsigned long expectedRank = 1; + unsigned long expectedIdx = 0; while (((pos = api.next(&iter)) != NULL)) { - unsigned long rank = api.getRank(oi, pos); - ASSERT_EQ(rank, expectedRank); - OrderedIndexItem *byRank = api.getByRank(oi, expectedRank); - ASSERT_EQ(byRank, pos); - expectedRank++; + unsigned long idx = api.getIndex(oi, pos); + ASSERT_EQ(idx, expectedIdx); + OrderedIndexItem *byIdx = api.getByIndex(oi, expectedIdx); + ASSERT_EQ(byIdx, pos); + expectedIdx++; } - ASSERT_EQ(expectedRank - 1, (unsigned long)n); + ASSERT_EQ(expectedIdx, (unsigned long)n); api.resetIterator(&iter); api.free(oi); oi = api.create(); @@ -1449,7 +1219,7 @@ TEST_P(OrderedIndexTest, RandomizedDelete) { std::uniform_int_distribution sizeDist(2, 30); int n = sizeDist(rng); - auto entries = test_build_random_index(api, oi, rng, n); + auto entries = test_build_random_index(api, oi, rng, n); std::uniform_int_distribution pickDist(0, n - 1); int delIdx = pickDist(rng); @@ -1481,7 +1251,7 @@ TEST_P(OrderedIndexTest, RandomizedUpdateScore) { std::uniform_int_distribution sizeDist(2, 30); int n = sizeDist(rng); - auto entries = test_build_random_index(api, oi, rng, n); + auto entries = test_build_random_index(api, oi, rng, n); std::uniform_int_distribution pickDist(0, n - 1); int updIdx = pickDist(rng); @@ -1489,7 +1259,7 @@ TEST_P(OrderedIndexTest, RandomizedUpdateScore) { OrderedIndexItem *updated = api.updateScore(oi, entries[updIdx].node, newScore); ASSERT_NE(updated, nullptr); - TEST_ASSERT_SCORE_EQ(api.getScore(updated), newScore); + assertScore(updated, newScore); ASSERT_EQ(api.length(oi), (unsigned long)n); verifyOI(); @@ -1513,30 +1283,30 @@ TEST_P(OrderedIndexTest, RandomizedPop) { std::uniform_int_distribution sizeDist(3, 30); int n = sizeDist(rng); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, rng, n); OrderedIndexIterator iter; OrderedIndexItem *pos; api.initIterator(&iter, oi); - ASSERT_TRUE(((pos = api.next(&iter)) != NULL)); + ASSERT_NE((pos = api.next(&iter)), nullptr); double minScore = api.getScore(pos); api.resetIterator(&iter); api.initIterator(&iter, oi); - ASSERT_TRUE(((pos = api.prev(&iter)) != NULL)); + ASSERT_NE((pos = api.prev(&iter)), nullptr); double maxScore = api.getScore(pos); api.resetIterator(&iter); OrderedIndexItem *first = api.popFirst(oi); ASSERT_NE(first, nullptr); - TEST_ASSERT_SCORE_EQ(api.getScore(first), minScore); + 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); - TEST_ASSERT_SCORE_EQ(api.getScore(last), maxScore); + assertScore(last, maxScore); ASSERT_EQ(api.length(oi), (unsigned long)(n - 2)); api.freeItem(last); verifyOI(); @@ -1559,7 +1329,7 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { std::uniform_int_distribution sizeDist(5, 40); int n = sizeDist(rng); - auto entries = test_build_random_index(api, oi, rng, n); + auto entries = test_build_random_index(api, oi, rng, n); double s1 = test_random_score(rng), s2 = test_random_score(rng); double lo = (std::min)(s1, s2), hi = (std::max)(s1, s2); @@ -1590,21 +1360,21 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { } } -TEST_P(OrderedIndexTest, RandomizedDeleteRangeByRank) { +TEST_P(OrderedIndexTest, RandomizedDeleteRangeByIndex) { std::mt19937 rng(42); for (int trial = 0; trial < 20; trial++) { std::uniform_int_distribution sizeDist(5, 40); int n = sizeDist(rng); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, rng, n); - std::uniform_int_distribution rankDist(1, n); - int r1 = rankDist(rng), r2 = rankDist(rng); + std::uniform_int_distribution idxDist(0, n - 1); + int r1 = idxDist(rng), r2 = idxDist(rng); unsigned long start = (unsigned long)(std::min)(r1, r2); unsigned long end = (unsigned long)(std::max)(r1, r2); unsigned long expectedDeleted = end - start + 1; - unsigned long deleted = api.deleteRangeByRank(oi, start, end, NULL, NULL); + unsigned long deleted = api.deleteRangeByIndex(oi, start, end, NULL, NULL); ASSERT_EQ(deleted, expectedDeleted); ASSERT_EQ(api.length(oi), (unsigned long)(n)-expectedDeleted); verifyOI(); @@ -1632,7 +1402,7 @@ TEST_P(OrderedIndexTest, RandomizedForwardBackwardMirror) { std::uniform_int_distribution sizeDist(1, 50); int n = sizeDist(rng); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, rng, n); std::vector forwardScores; OrderedIndexIterator iter; @@ -1653,7 +1423,7 @@ TEST_P(OrderedIndexTest, RandomizedForwardBackwardMirror) { ASSERT_EQ(forwardScores.size(), backwardScores.size()); std::reverse(backwardScores.begin(), backwardScores.end()); for (size_t i = 0; i < forwardScores.size(); i++) { - TEST_ASSERT_SCORE_EQ(forwardScores[i], backwardScores[i]); + ASSERT_DOUBLE_EQ(forwardScores[i], backwardScores[i]); } api.free(oi); oi = api.create(); @@ -1663,7 +1433,6 @@ TEST_P(OrderedIndexTest, RandomizedForwardBackwardMirror) { /* ========== Count range tests ========== */ TEST_P(OrderedIndexTest, CountScoreRange) { - populateSequential(10); /* Full range */ @@ -1695,7 +1464,6 @@ TEST_P(OrderedIndexTest, CountScoreRange) { /* Last element only [9, 9] */ ASSERT_EQ(api.countScoreRange(oi, 9.0, 9.0, 0, 0), 1UL); - } TEST_P(OrderedIndexTest, CountScoreRangeEmpty) { @@ -1703,57 +1471,17 @@ TEST_P(OrderedIndexTest, CountScoreRangeEmpty) { } TEST_P(OrderedIndexTest, CountLexRange) { + for (int i = 0; i < FRUITS_COUNT; i++) insert(1.0, FRUITS[i]); - const char *elements[] = {"apple", "banana", "cherry", "date", "elderberry"}; - for (int i = 0; i < 5; i++) { - sds ele = sdsnew(elements[i]); - api.insertSds(oi, 1.0, ele); - sdsfree(ele); - } - - /* Inclusive [banana, date] */ - sds min = sdsnew("banana"); - sds max = sdsnew("date"); - ASSERT_EQ(api.countLexRange(oi, min, max, 0, 0), 3UL); - sdsfree(min); - sdsfree(max); - - /* Exclusive (banana, date) */ - min = sdsnew("banana"); - max = sdsnew("date"); - ASSERT_EQ(api.countLexRange(oi, min, max, 1, 1), 1UL); - sdsfree(min); - sdsfree(max); - - /* Single element [cherry, cherry] */ - min = sdsnew("cherry"); - max = sdsnew("cherry"); - ASSERT_EQ(api.countLexRange(oi, min, max, 0, 0), 1UL); - sdsfree(min); - sdsfree(max); - - /* No match */ - min = sdsnew("fig"); - max = sdsnew("grape"); - ASSERT_EQ(api.countLexRange(oi, min, max, 0, 0), 0UL); - sdsfree(min); - sdsfree(max); - - /* All elements */ - min = sdsnew("a"); - max = sdsnew("z"); - ASSERT_EQ(api.countLexRange(oi, min, max, 0, 0), 5UL); - sdsfree(min); - sdsfree(max); - + ASSERT_EQ(countLexRange("banana", "date"), 3UL); /* Inclusive [banana, date] */ + ASSERT_EQ(countLexRange("banana", "date", 1, 1), 1UL); /* Exclusive (banana, date) */ + ASSERT_EQ(countLexRange("cherry", "cherry"), 1UL); /* Single element */ + ASSERT_EQ(countLexRange("fig", "grape"), 0UL); /* No match */ + ASSERT_EQ(countLexRange("a", "z"), 5UL); /* All elements */ } TEST_P(OrderedIndexTest, CountLexRangeEmpty) { - sds min = sdsnew("a"); - sds max = sdsnew("z"); - ASSERT_EQ(api.countLexRange(oi, min, max, 0, 0), 0UL); - sdsfree(min); - sdsfree(max); + ASSERT_EQ(countLexRange("a", "z"), 0UL); } /* ========== Instantiate parameterized tests for all implementations ========== */ @@ -1785,7 +1513,9 @@ class OnDeleteCallbackTest : public ::testing::Test { SkiplistOrderedIndex api; OrderedIndex *oi = nullptr; - void SetUp() override { oi = api.create(); } + void SetUp() override { + oi = api.create(); + } void TearDown() override { if (oi) api.free(oi); } @@ -1795,20 +1525,20 @@ class OnDeleteCallbackTest : public ::testing::Test { ASSERT_TRUE(api.verifyIntegrity(oi, errmsg, sizeof(errmsg))) << errmsg; } - void insertN(OrderedIndex *oi, int n) { + 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++) { std::string name = "key" + std::to_string(i); - sds ele = sdsnew(name.c_str()); - api.insertSds(oi, (double)i, ele); - sdsfree(ele); + insert((double)i, name.c_str()); } } - void insertLex(OrderedIndex *oi, const std::vector &elems, double score = 1.0) { + void insertLex(const std::vector &elems, double score = 1.0) { for (auto &e : elems) { - sds ele = sdsnew(e.c_str()); - api.insertSds(oi, score, ele); - sdsfree(ele); + insert(score, e.c_str()); } } @@ -1839,7 +1569,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_EmptyAndNoMatch) { api.free(oi); oi = api.create(); - insertN(oi, 5); + insertN(5); rec = {0, {}}; deleted = api.deleteRangeByScore(oi, 10.0, 20.0, 0, 0, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 0UL); @@ -1848,7 +1578,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_EmptyAndNoMatch) { } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_Subset) { - insertN(oi, 10); + insertN(10); OnDeleteRecord rec = {0, {}}; unsigned long deleted = api.deleteRangeByScore(oi, 3.0, 6.0, 0, 0, testOnDeleteCallback, &rec); @@ -1865,7 +1595,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_Subset) { } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_All) { - insertN(oi, 5); + insertN(5); OnDeleteRecord rec = {0, {}}; unsigned long deleted = api.deleteRangeByScore(oi, NEG_INF, POS_INF, 0, 0, testOnDeleteCallback, &rec); @@ -1876,7 +1606,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_All) { } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_NullCallback) { - insertN(oi, 5); + insertN(5); unsigned long deleted = api.deleteRangeByScore(oi, 1.0, 3.0, 0, 0, NULL, NULL); ASSERT_EQ(deleted, 3UL); @@ -1884,7 +1614,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_NullCallback) { } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_ExclusiveBounds) { - insertN(oi, 10); + insertN(10); OnDeleteRecord rec = {0, {}}; unsigned long deleted = api.deleteRangeByScore(oi, 3.0, 7.0, 1, 1, testOnDeleteCallback, &rec); @@ -1896,7 +1626,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_ExclusiveBounds) { } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_SingleElement) { - insertN(oi, 5); + insertN(5); OnDeleteRecord rec = {0, {}}; unsigned long deleted = api.deleteRangeByScore(oi, 2.0, 2.0, 0, 0, testOnDeleteCallback, &rec); @@ -1906,30 +1636,30 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_SingleElement) { ASSERT_EQ(api.length(oi), 4UL); } -/* DeleteRangeByRank */ +/* DeleteRangeByIndex */ -TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_EmptyAndNoMatch) { +TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_EmptyAndNoMatch) { OnDeleteRecord rec = {0, {}}; - unsigned long deleted = api.deleteRangeByRank(oi, 1, 5, testOnDeleteCallback, &rec); + 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(oi, 3); + insertN(3); rec = {0, {}}; - deleted = api.deleteRangeByRank(oi, 10, 20, testOnDeleteCallback, &rec); + deleted = api.deleteRangeByIndex(oi, 10, 20, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 0UL); ASSERT_EQ(rec.count, 0); ASSERT_EQ(api.length(oi), 3UL); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_Subset) { - insertN(oi, 10); +TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_Subset) { + insertN(10); OnDeleteRecord rec = {0, {}}; - unsigned long deleted = api.deleteRangeByRank(oi, 3, 5, testOnDeleteCallback, &rec); + 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); @@ -1941,29 +1671,29 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_Subset) { ASSERT_EQ(remaining, (std::vector{"key0", "key1", "key5", "key6", "key7", "key8", "key9"})); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_All) { - insertN(oi, 5); +TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_All) { + insertN(5); OnDeleteRecord rec = {0, {}}; - unsigned long deleted = api.deleteRangeByRank(oi, 1, 5, testOnDeleteCallback, &rec); + 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); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_NullCallback) { - insertN(oi, 5); +TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_NullCallback) { + insertN(5); - unsigned long deleted = api.deleteRangeByRank(oi, 2, 4, NULL, NULL); + unsigned long deleted = api.deleteRangeByIndex(oi, 2, 4, NULL, NULL); ASSERT_EQ(deleted, 3UL); ASSERT_EQ(api.length(oi), 2UL); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_ExclusiveBounds) { - insertN(oi, 5); +TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_ExclusiveBounds) { + insertN(5); OnDeleteRecord rec = {0, {}}; - unsigned long deleted = api.deleteRangeByRank(oi, 3, 3, testOnDeleteCallback, &rec); + unsigned long deleted = api.deleteRangeByIndex(oi, 2, 2, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 1UL); ASSERT_EQ(rec.count, 1); ASSERT_EQ(rec.elements[0], "key2"); @@ -1972,11 +1702,11 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_ExclusiveBounds) { ASSERT_EQ(remaining, (std::vector{"key0", "key1", "key3", "key4"})); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByRank_SingleElement) { - insertN(oi, 5); +TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_SingleElement) { + insertN(5); OnDeleteRecord rec = {0, {}}; - unsigned long deleted = api.deleteRangeByRank(oi, 1, 1, testOnDeleteCallback, &rec); + unsigned long deleted = api.deleteRangeByIndex(oi, 0, 0, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 1UL); ASSERT_EQ(rec.count, 1); ASSERT_EQ(rec.elements[0], "key0"); @@ -1998,7 +1728,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { api.free(oi); oi = api.create(); - insertLex(oi, {"apple", "banana", "cherry"}); + insertLex({"apple", "banana", "cherry"}); rec = {0, {}}; min = sdsnew("x"); max = sdsnew("z"); @@ -2011,7 +1741,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_Subset) { - insertLex(oi, {"apple", "banana", "cherry", "date", "elderberry"}); + insertLex({"apple", "banana", "cherry", "date", "elderberry"}); OnDeleteRecord rec = {0, {}}; sds min = sdsnew("banana"); @@ -2032,7 +1762,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_Subset) { } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_All) { - insertLex(oi, {"apple", "banana", "cherry"}); + insertLex({"apple", "banana", "cherry"}); OnDeleteRecord rec = {0, {}}; sds min = sdsnew("a"); @@ -2047,7 +1777,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_All) { } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_NullCallback) { - insertLex(oi, {"apple", "banana", "cherry", "date"}); + insertLex({"apple", "banana", "cherry", "date"}); sds min = sdsnew("banana"); sds max = sdsnew("cherry"); @@ -2063,7 +1793,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_NullCallback) { } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_ExclusiveBounds) { - insertLex(oi, {"apple", "banana", "cherry", "date", "elderberry"}); + insertLex({"apple", "banana", "cherry", "date", "elderberry"}); OnDeleteRecord rec = {0, {}}; sds min = sdsnew("banana"); @@ -2082,7 +1812,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_ExclusiveBounds) { } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_SingleElement) { - insertLex(oi, {"apple", "banana", "cherry"}); + insertLex({"apple", "banana", "cherry"}); OnDeleteRecord rec = {0, {}}; sds min = sdsnew("banana"); @@ -2116,27 +1846,29 @@ class RangeDeleteHashtableConsistencyTest : public ::testing::Test { SkiplistOrderedIndex api; OrderedIndex *oi = nullptr; - void SetUp() override { oi = api.create(); } + void SetUp() override { + oi = api.create(); + } void TearDown() override { if (oi) api.free(oi); } - void insertN(OrderedIndex *oi, std::set &ht, int n) { + void insert(double score, const char *ele) { + api.insert(oi, score, ele, strlen(ele)); + } + + void insertN(std::set &ht, int n) { for (int i = 0; i < n; i++) { std::string name = "key" + std::to_string(i); - sds ele = sdsnew(name.c_str()); - api.insertSds(oi, (double)i, ele); + insert((double)i, name.c_str()); ht.insert(name); - sdsfree(ele); } } - void insertLex(OrderedIndex *oi, std::set &ht, const std::vector &elems, double score = 1.0) { + void insertLex(std::set &ht, const std::vector &elems, double score = 1.0) { for (auto &e : elems) { - sds ele = sdsnew(e.c_str()); - api.insertSds(oi, score, ele); + insert(score, e.c_str()); ht.insert(e); - sdsfree(ele); } } @@ -2160,83 +1892,77 @@ class RangeDeleteHashtableConsistencyTest : public ::testing::Test { TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_PartialDelete) { std::set simulatedHt; - insertN(oi, simulatedHt, 10); + insertN(simulatedHt, 10); api.deleteRangeByScore(oi, 3.0, 6.0, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_EQ(indexElements.size(), 6UL); - } TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_FullDelete) { std::set simulatedHt; - insertN(oi, simulatedHt, 10); + insertN(simulatedHt, 10); api.deleteRangeByScore(oi, NEG_INF, POS_INF, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_TRUE(indexElements.empty()); - } TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_EmptyRange) { std::set simulatedHt; - insertN(oi, simulatedHt, 10); + insertN(simulatedHt, 10); api.deleteRangeByScore(oi, 20.0, 30.0, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_EQ(indexElements.size(), 10UL); - } -/* ByRank */ +/* ByIndex */ -TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_PartialDelete) { +TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_PartialDelete) { std::set simulatedHt; - insertN(oi, simulatedHt, 10); + insertN(simulatedHt, 10); - api.deleteRangeByRank(oi, 3, 5, hashtableConsistencyOnDelete, &simulatedHt); + api.deleteRangeByIndex(oi, 2, 4, hashtableConsistencyOnDelete, &simulatedHt); std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_EQ(indexElements.size(), 7UL); - } -TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_FullDelete) { +TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_FullDelete) { std::set simulatedHt; - insertN(oi, simulatedHt, 10); + insertN(simulatedHt, 10); - api.deleteRangeByRank(oi, 1, 10, hashtableConsistencyOnDelete, &simulatedHt); + api.deleteRangeByIndex(oi, 0, 9, hashtableConsistencyOnDelete, &simulatedHt); std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_TRUE(indexElements.empty()); - } -TEST_F(RangeDeleteHashtableConsistencyTest, ByRank_EmptyRange) { +TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_EmptyRange) { std::set simulatedHt; - insertN(oi, simulatedHt, 10); + insertN(simulatedHt, 10); - api.deleteRangeByRank(oi, 20, 30, hashtableConsistencyOnDelete, &simulatedHt); + api.deleteRangeByIndex(oi, 20, 30, hashtableConsistencyOnDelete, &simulatedHt); std::set indexElements = collectIndexElements(oi); ASSERT_EQ(indexElements, simulatedHt); ASSERT_EQ(indexElements.size(), 10UL); - } /* ByLex */ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_PartialDelete) { std::set simulatedHt; - insertLex(oi, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); + insertLex(simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); sds min = sdsnew("banana"); sds max = sdsnew("date"); @@ -2252,7 +1978,7 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_PartialDelete) { TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_FullDelete) { std::set simulatedHt; - insertLex(oi, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); + insertLex(simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); sds min = sdsnew("a"); sds max = sdsnew("z"); @@ -2268,7 +1994,7 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_FullDelete) { TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_EmptyRange) { std::set simulatedHt; - insertLex(oi, simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); + insertLex(simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); sds min = sdsnew("zzz"); sds max = sdsnew("zzzz"); From ae4d0ab2226a07c6f6e070d5b11be7d06d80ac5f Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Thu, 28 May 2026 01:13:05 +0000 Subject: [PATCH 31/45] unit tests: reduce C++ idioms in ordered index tests Replace C++ patterns with C-style equivalents in deterministic tests: - Remove ScopedIter RAII wrapper; use explicit initIterator/resetIterator - Replace std::initializer_list with C array + variadic macro - Remove default parameter values from lex helper functions - Replace std::mt19937 and std::uniform_*_distribution with xorshift32 PRNG Fuzz/randomized tests retain std::vector/string/set for bookkeeping as rewriting those provides no practical benefit. Signed-off-by: Rain Valentine --- src/unit/test_ordered_index.cpp | 216 +++++++++++++++----------------- 1 file changed, 98 insertions(+), 118 deletions(-) diff --git a/src/unit/test_ordered_index.cpp b/src/unit/test_ordered_index.cpp index 758ab48d6be..a716e5333f0 100644 --- a/src/unit/test_ordered_index.cpp +++ b/src/unit/test_ordered_index.cpp @@ -19,7 +19,6 @@ extern "C" { #include #include #include -#include #include #include #include @@ -110,38 +109,8 @@ class OrderedIndexTest : public ::testing::TestWithParam ASSERT_DOUBLE_EQ(api.getScore(node), expected); } - /* RAII scoped iterator — auto-inits on construction, resets on destruction. */ - struct ScopedIter { - OrderedIndexTestApi &api; - OrderedIndexIterator iter; - ScopedIter(OrderedIndexTestApi &a, OrderedIndex *oi) : - api(a) { - api.initIterator(&iter, oi); - } - ~ScopedIter() { - api.resetIterator(&iter); - } - OrderedIndexItem *next() { - return api.next(&iter); - } - OrderedIndexItem *prev() { - return api.prev(&iter); - } - OrderedIndexIterator *operator->() { - return &iter; - } - OrderedIndexIterator *get() { - return &iter; - } - }; - - /* Create a scoped iterator for this fixture's oi. */ - ScopedIter iter() { - return ScopedIter(api, oi); - } - /* Delete lex range using const char* (handles sds lifecycle). */ - unsigned long deleteLexRange(const char *min_str, const char *max_str, int min_ex = 0, int max_ex = 0, OrderedIndexOnDelete cb = NULL, void *ctx = NULL) { + 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); @@ -151,7 +120,7 @@ class OrderedIndexTest : public ::testing::TestWithParam } /* Count lex range using const char*. */ - unsigned long countLexRange(const char *min_str, const char *max_str, int min_ex = 0, int max_ex = 0) { + 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); @@ -170,16 +139,18 @@ class OrderedIndexTest : public ::testing::TestWithParam } /* Assert full forward traversal matches expected element names. */ - void assertAllElements(std::initializer_list expected) { - auto it = iter(); + void assertAllElements(const char *expected[], size_t count) { + OrderedIndexIterator it; + api.initIterator(&it, oi); OrderedIndexItem *pos; - auto exp = expected.begin(); - while ((pos = it.next()) != NULL) { - ASSERT_NE(exp, expected.end()) << "More elements than expected"; - assertElement(pos, *exp); - ++exp; + size_t i = 0; + while ((pos = api.next(&it)) != NULL) { + ASSERT_LT(i, count) << "More elements than expected"; + assertElement(pos, expected[i]); + i++; } - ASSERT_EQ(exp, expected.end()) << "Fewer elements than expected"; + api.resetIterator(&it); + ASSERT_EQ(i, count) << "Fewer elements than expected"; } /* Verify structural integrity. */ @@ -188,6 +159,13 @@ class OrderedIndexTest : public ::testing::TestWithParam } }; +/* 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) { @@ -1025,55 +1003,57 @@ TEST_P(OrderedIndexTest, SeekToLexRange) { TEST_P(OrderedIndexTest, DeleteRangeByLexInclusive) { for (int i = 0; i < FRUITS_COUNT; i++) insert(1.0, FRUITS[i]); - ASSERT_EQ(deleteLexRange("banana", "date"), 3UL); + ASSERT_EQ(deleteLexRange("banana", "date", 0, 0, NULL, NULL), 3UL); ASSERT_EQ(api.length(oi), 2UL); verifyOI(); - assertAllElements({"apple", "elderberry"}); + 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), 1UL); + ASSERT_EQ(deleteLexRange("banana", "date", 1, 1, NULL, NULL), 1UL); ASSERT_EQ(api.length(oi), 4UL); - assertAllElements({"apple", "banana", "date", "elderberry"}); + 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"), 0UL); + 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"), 3UL); + 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"), 1UL); + ASSERT_EQ(deleteLexRange("banana", "banana", 0, 0, NULL, NULL), 1UL); ASSERT_EQ(api.length(oi), 2UL); - assertAllElements({"apple", "cherry"}); + 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"), 2UL); + ASSERT_EQ(deleteLexRange("charlie", "delta", 0, 0, NULL, NULL), 2UL); ASSERT_EQ(api.length(oi), 4UL); - assertAllElements({"alpha", "bravo", "echo", "foxtrot"}); + ASSERT_ALL_ELEMENTS("alpha", "bravo", "echo", "foxtrot"); /* Verify scores are preserved */ - auto it = iter(); + OrderedIndexIterator it; + api.initIterator(&it, oi); OrderedIndexItem *pos; - while ((pos = it.next()) != NULL) { + 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++) { @@ -1085,31 +1065,44 @@ TEST_P(OrderedIndexTest, DeleteRangeByLexPreservesOutside) { /* ========== Randomized property tests ========== */ +/* Simple xorshift32 PRNG — deterministic, seedable, no STL. */ +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; std::string element; }; -static std::string test_random_element(std::mt19937 &rng, int maxLen = 16) { - std::uniform_int_distribution lenDist(1, maxLen); - std::uniform_int_distribution charDist('a', 'z'); - int len = lenDist(rng); +static std::string test_random_element(uint32_t *state, int maxLen) { + int len = test_rand_range(state, 1, maxLen); std::string s(len, ' '); - for (int i = 0; i < len; i++) s[i] = (char)charDist(rng); + for (int i = 0; i < len; i++) s[i] = (char)test_rand_range(state, 'a', 'z'); return s; } -static double test_random_score(std::mt19937 &rng) { - std::uniform_real_distribution dist(-1e6, 1e6); - return dist(rng); +static double test_random_score(uint32_t *state) { + return test_rand_double(state, -1e6, 1e6); } -static std::vector test_build_random_index(OrderedIndexTestApi &api, OrderedIndex *oi, std::mt19937 &rng, int count) { +static std::vector test_build_random_index(OrderedIndexTestApi &api, OrderedIndex *oi, uint32_t *state, int count) { std::vector entries; for (int i = 0; i < count; i++) { - double score = test_random_score(rng); - std::string elem = test_random_element(rng) + std::to_string(i); + double score = test_random_score(state); + std::string elem = test_random_element(state, 16) + std::to_string(i); OrderedIndexItem *node = api.insert(oi, score, elem.c_str(), elem.size()); entries.push_back({node, score, elem}); } @@ -1117,12 +1110,11 @@ static std::vector test_build_random_index(OrderedIndexTestApi } TEST_P(OrderedIndexTest, RandomizedInsertAndTraversal) { - std::mt19937 rng(42); + uint32_t rng = 42; for (int trial = 0; trial < 20; trial++) { - std::uniform_int_distribution sizeDist(1, 50); - int n = sizeDist(rng); + int n = test_rand_range(&rng, 1, 50); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, &rng, n); ASSERT_EQ(api.length(oi), (unsigned long)n); verifyOI(); @@ -1146,12 +1138,11 @@ TEST_P(OrderedIndexTest, RandomizedInsertAndTraversal) { } TEST_P(OrderedIndexTest, RandomizedBackwardTraversal) { - std::mt19937 rng(42); + uint32_t rng = 42; for (int trial = 0; trial < 20; trial++) { - std::uniform_int_distribution sizeDist(1, 50); - int n = sizeDist(rng); + int n = test_rand_range(&rng, 1, 50); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, &rng, n); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1172,12 +1163,11 @@ TEST_P(OrderedIndexTest, RandomizedBackwardTraversal) { } TEST_P(OrderedIndexTest, RandomizedScoreRetrieval) { - std::mt19937 rng(42); + uint32_t rng = 42; for (int trial = 0; trial < 20; trial++) { - std::uniform_int_distribution sizeDist(1, 50); - int n = sizeDist(rng); + int n = test_rand_range(&rng, 1, 50); - auto entries = test_build_random_index(api, oi, rng, n); + auto entries = test_build_random_index(api, oi, &rng, n); for (auto &e : entries) { assertScore(e.node, e.score); @@ -1188,12 +1178,11 @@ TEST_P(OrderedIndexTest, RandomizedScoreRetrieval) { } TEST_P(OrderedIndexTest, RandomizedIndexConsistency) { - std::mt19937 rng(42); + uint32_t rng = 42; for (int trial = 0; trial < 20; trial++) { - std::uniform_int_distribution sizeDist(1, 50); - int n = sizeDist(rng); + int n = test_rand_range(&rng, 1, 50); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, &rng, n); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1214,15 +1203,13 @@ TEST_P(OrderedIndexTest, RandomizedIndexConsistency) { } TEST_P(OrderedIndexTest, RandomizedDelete) { - std::mt19937 rng(42); + uint32_t rng = 42; for (int trial = 0; trial < 20; trial++) { - std::uniform_int_distribution sizeDist(2, 30); - int n = sizeDist(rng); + int n = test_rand_range(&rng, 2, 30); - auto entries = test_build_random_index(api, oi, rng, n); + auto entries = test_build_random_index(api, oi, &rng, n); - std::uniform_int_distribution pickDist(0, n - 1); - int delIdx = pickDist(rng); + int delIdx = test_rand_range(&rng, 0, n - 1); api.deleteItem(oi, entries[delIdx].node); ASSERT_EQ(api.length(oi), (unsigned long)(n - 1)); @@ -1246,16 +1233,14 @@ TEST_P(OrderedIndexTest, RandomizedDelete) { } TEST_P(OrderedIndexTest, RandomizedUpdateScore) { - std::mt19937 rng(42); + uint32_t rng = 42; for (int trial = 0; trial < 20; trial++) { - std::uniform_int_distribution sizeDist(2, 30); - int n = sizeDist(rng); + int n = test_rand_range(&rng, 2, 30); - auto entries = test_build_random_index(api, oi, rng, n); + auto entries = test_build_random_index(api, oi, &rng, n); - std::uniform_int_distribution pickDist(0, n - 1); - int updIdx = pickDist(rng); - double newScore = test_random_score(rng); + 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); @@ -1278,12 +1263,11 @@ TEST_P(OrderedIndexTest, RandomizedUpdateScore) { } TEST_P(OrderedIndexTest, RandomizedPop) { - std::mt19937 rng(42); + uint32_t rng = 42; for (int trial = 0; trial < 10; trial++) { - std::uniform_int_distribution sizeDist(3, 30); - int n = sizeDist(rng); + int n = test_rand_range(&rng, 3, 30); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, &rng, n); OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1324,14 +1308,13 @@ TEST_P(OrderedIndexTest, RandomizedPop) { } TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { - std::mt19937 rng(42); + uint32_t rng = 42; for (int trial = 0; trial < 20; trial++) { - std::uniform_int_distribution sizeDist(5, 40); - int n = sizeDist(rng); + int n = test_rand_range(&rng, 5, 40); - auto entries = test_build_random_index(api, oi, rng, n); + auto entries = test_build_random_index(api, oi, &rng, n); - double s1 = test_random_score(rng), s2 = test_random_score(rng); + double s1 = test_random_score(&rng), s2 = test_random_score(&rng); double lo = (std::min)(s1, s2), hi = (std::max)(s1, s2); int expectedDeleted = 0; @@ -1361,15 +1344,13 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { } TEST_P(OrderedIndexTest, RandomizedDeleteRangeByIndex) { - std::mt19937 rng(42); + uint32_t rng = 42; for (int trial = 0; trial < 20; trial++) { - std::uniform_int_distribution sizeDist(5, 40); - int n = sizeDist(rng); + int n = test_rand_range(&rng, 5, 40); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, &rng, n); - std::uniform_int_distribution idxDist(0, n - 1); - int r1 = idxDist(rng), r2 = idxDist(rng); + int r1 = test_rand_range(&rng, 0, n - 1), r2 = test_rand_range(&rng, 0, n - 1); unsigned long start = (unsigned long)(std::min)(r1, r2); unsigned long end = (unsigned long)(std::max)(r1, r2); unsigned long expectedDeleted = end - start + 1; @@ -1397,12 +1378,11 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByIndex) { } TEST_P(OrderedIndexTest, RandomizedForwardBackwardMirror) { - std::mt19937 rng(42); + uint32_t rng = 42; for (int trial = 0; trial < 20; trial++) { - std::uniform_int_distribution sizeDist(1, 50); - int n = sizeDist(rng); + int n = test_rand_range(&rng, 1, 50); - test_build_random_index(api, oi, rng, n); + test_build_random_index(api, oi, &rng, n); std::vector forwardScores; OrderedIndexIterator iter; @@ -1473,15 +1453,15 @@ TEST_P(OrderedIndexTest, CountScoreRangeEmpty) { TEST_P(OrderedIndexTest, CountLexRange) { for (int i = 0; i < FRUITS_COUNT; i++) insert(1.0, FRUITS[i]); - ASSERT_EQ(countLexRange("banana", "date"), 3UL); /* Inclusive [banana, date] */ - ASSERT_EQ(countLexRange("banana", "date", 1, 1), 1UL); /* Exclusive (banana, date) */ - ASSERT_EQ(countLexRange("cherry", "cherry"), 1UL); /* Single element */ - ASSERT_EQ(countLexRange("fig", "grape"), 0UL); /* No match */ - ASSERT_EQ(countLexRange("a", "z"), 5UL); /* All elements */ + 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"), 0UL); + ASSERT_EQ(countLexRange("a", "z", 0, 0), 0UL); } /* ========== Instantiate parameterized tests for all implementations ========== */ From 140665a956e36520071224004c9b3bc4cbd78e11 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Thu, 28 May 2026 01:22:04 +0000 Subject: [PATCH 32/45] fix: assert defrag hashtable remap + guard errmsg buffer - Assert hashtableReplaceReallocatedEntry succeeds in defragZsetNodeCallback (matches pattern used by other defrag sites in the same file) - Guard errmsg[0] write with length check in skiplistVerifyIntegrity (prevents out-of-bounds write when errmsg_len == 0) Signed-off-by: Rain Valentine --- src/defrag.c | 3 ++- src/skiplist_ordered_index.c | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/defrag.c b/src/defrag.c index d301ecc57a2..ce84d257315 100644 --- a/src/defrag.c +++ b/src/defrag.c @@ -241,7 +241,8 @@ robj *activeDefragStringOb(robj *ob) { * the hashtable's pointer to it. */ static void defragZsetNodeCallback(OrderedIndexItem *old_item, OrderedIndexItem *new_item, void *ctx) { hashtable *ht = ctx; - hashtableReplaceReallocatedEntry(ht, old_item, new_item); + bool replaced = hashtableReplaceReallocatedEntry(ht, old_item, new_item); + serverAssert(replaced); server.stat_active_defrag_scanned++; } diff --git a/src/skiplist_ordered_index.c b/src/skiplist_ordered_index.c index e356ec9eba3..6dbc44bfa06 100644 --- a/src/skiplist_ordered_index.c +++ b/src/skiplist_ordered_index.c @@ -517,6 +517,6 @@ int skiplistVerifyIntegrity(OrderedIndex *oi, char *errmsg, size_t errmsg_len) { } #undef FAIL - errmsg[0] = '\0'; + if (errmsg_len > 0) errmsg[0] = '\0'; return 1; } From 67384948f01d8d8b1f8fb0e0aeb4d7753f8754bd Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Thu, 28 May 2026 01:36:14 +0000 Subject: [PATCH 33/45] tests: add with_config helper for failure-safe config restoration Add a with_config proc to tests/support/util.tcl that wraps a test body with automatic config save/restore, ensuring the original value is restored even if the body raises an error. Convert all skiplist-encoding tests in sort.tcl and zset.tcl to use it. Signed-off-by: Rain Valentine --- tests/support/util.tcl | 11 ++ tests/unit/sort.tcl | 75 +++++----- tests/unit/type/zset.tcl | 290 ++++++++++++++++++--------------------- 3 files changed, 181 insertions(+), 195 deletions(-) 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 22e9df4ef3b..fb6bd377c64 100644 --- a/tests/unit/sort.tcl +++ b/tests/unit/sort.tcl @@ -172,51 +172,48 @@ foreach command {SORT SORT_RO} { } test "SORT sorted set skiplist BY nosort should retain ordering" { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set 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} - r config set zset-max-ziplist-entries $original_max + 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" { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set 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} - r config set zset-max-ziplist-entries $original_max + 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" { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set 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} - r config set zset-max-ziplist-entries $original_max + 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" { diff --git a/tests/unit/type/zset.tcl b/tests/unit/type/zset.tcl index 6a0fd88e754..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} { @@ -2952,195 +2952,173 @@ start_server [list overrides [list save ""] tags {"zset needs:debug external:ski start_server {tags {"zset" "cluster:skip"}} { test {ZUNIONSTORE with skiplist-encoded inputs} { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set zset-max-ziplist-entries 0 + 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 + 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] - - r config set zset-max-ziplist-entries $original_max + 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} { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set 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 + with_config zset-max-ziplist-entries 0 { - 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] + 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 - r config set zset-max-ziplist-entries $original_max + 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} { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set 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 + with_config zset-max-ziplist-entries 0 { - assert_equal 2 [r zinterstore dst 2 src1 src2] - assert_equal {b 12 c 23} [r zrange dst 0 -1 WITHSCORES] + 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 - r config set zset-max-ziplist-entries $original_max + 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} { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set 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 + with_config zset-max-ziplist-entries 0 { - assert_equal 2 [r zinterstore dst 2 src1 src2 AGGREGATE MIN] - assert_equal {a 1 b 10} [r zrange dst 0 -1 WITHSCORES] + 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 - r config set zset-max-ziplist-entries $original_max + 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} { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set zset-max-ziplist-entries 0 + 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] + r del zset + for {set i 0} {$i < 20} {incr i} { + r zadd zset $i "key:$i" + } + assert_encoding skiplist zset - r config set zset-max-ziplist-entries $original_max + # 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} { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set zset-max-ziplist-entries 0 + 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] + r del zset + foreach elem {a b c d e f g h i j} { + r zadd zset 0 $elem + } + assert_encoding skiplist zset - r config set zset-max-ziplist-entries $original_max + # 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} { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set 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 + with_config zset-max-ziplist-entries 0 { - assert_equal {a 1} [r zpopmin zset] - assert_equal {b 2} [r zpopmin zset] - assert_equal 3 [r zcard zset] + r del zset + r zadd zset 3 c 1 a 2 b 5 e 4 d + assert_encoding skiplist zset - # Pop multiple - assert_equal {c 3 d 4} [r zpopmin zset 2] - assert_equal 1 [r zcard zset] + assert_equal {a 1} [r zpopmin zset] + assert_equal {b 2} [r zpopmin zset] + assert_equal 3 [r zcard zset] - r config set zset-max-ziplist-entries $original_max + # Pop multiple + assert_equal {c 3 d 4} [r zpopmin zset 2] + assert_equal 1 [r zcard zset] + } } test {ZPOPMAX on skiplist-encoded set} { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set 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 + with_config zset-max-ziplist-entries 0 { - assert_equal {e 5} [r zpopmax zset] - assert_equal {d 4} [r zpopmax zset] - assert_equal 3 [r zcard zset] + r del zset + r zadd zset 3 c 1 a 2 b 5 e 4 d + assert_encoding skiplist zset - # Pop multiple - assert_equal {c 3 b 2} [r zpopmax zset 2] - assert_equal 1 [r zcard zset] + assert_equal {e 5} [r zpopmax zset] + assert_equal {d 4} [r zpopmax zset] + assert_equal 3 [r zcard zset] - r config set zset-max-ziplist-entries $original_max + # 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} { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set zset-max-ziplist-entries 0 - - r del zset - r zadd zset 1 a - r zpopmin zset - assert_equal 0 [r exists zset] + with_config zset-max-ziplist-entries 0 { - r config set zset-max-ziplist-entries $original_max + r del zset + r zadd zset 1 a + r zpopmin zset + assert_equal 0 [r exists zset] + } } test {ZCOUNT on skiplist-encoded set} { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set 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 + with_config zset-max-ziplist-entries 0 { - 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] + r del zset + for {set i 0} {$i < 10} {incr i} { + r zadd zset $i "key:$i" + } + assert_encoding skiplist zset - r config set zset-max-ziplist-entries $original_max + 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} { - set original_max [lindex [r config get zset-max-ziplist-entries] 1] - r config set zset-max-ziplist-entries 0 + 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"] + r del zset + foreach elem {a b c d e f} { + r zadd zset 0 $elem + } + assert_encoding skiplist zset - r config set zset-max-ziplist-entries $original_max + 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"] + } } } From 97fa9fcf1b93008961b443477518a2881c4675db Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Thu, 28 May 2026 01:46:20 +0000 Subject: [PATCH 34/45] fix: use borrowed bytes path in zuiNext for ordered index elements Store ordered index element as estr/elen (borrowed pointer + length) instead of casting to sds. This matches the listpack path and avoids leaking the skiplist's internal sds storage into the generic ordered index interface. Callers that need an sds (zuiSdsFromValue, zuiNewSdsFromValue) will create one from the borrowed bytes, same as they do for listpack. Signed-off-by: Rain Valentine --- src/t_zset.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/t_zset.c b/src/t_zset.c index 4c52dccd2ab..479fc568ce7 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -1646,7 +1646,8 @@ static int zuiNext(zsetopsrc *op, zsetopval *val) { const char *val_ele_ptr; size_t val_ele_len; orderedIndexGetElementRaw(it->sl.node, &val_ele_ptr, &val_ele_len); - val->ele = (sds)val_ele_ptr; + val->estr = (unsigned char *)val_ele_ptr; + val->elen = val_ele_len; val->score = orderedIndexGetScore(it->sl.node); } else { serverPanic("Unknown sorted set encoding"); From f43f63f60df75b0111fb7707f598a96b8d3f921d Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Thu, 28 May 2026 01:58:32 +0000 Subject: [PATCH 35/45] fix: clarify orderedIndexUpdateScore return contract The function always returns a valid pointer (never NULL). Callers can compare old vs returned to detect whether the item was repositioned. Simplify the ZADD caller to unconditionally update the hashtable ref. Signed-off-by: Rain Valentine --- src/ordered_index.h | 5 +++-- src/t_zset.c | 5 ++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ordered_index.h b/src/ordered_index.h index 133d164bee7..a3ca5c23dd2 100644 --- a/src/ordered_index.h +++ b/src/ordered_index.h @@ -64,8 +64,9 @@ OrderedIndexItem *orderedIndexInsert(OrderedIndex *oi, double score, const char 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. - * Returns NULL if the item stayed in place (score updated in-place). */ + * 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. */ diff --git a/src/t_zset.c b/src/t_zset.c index 479fc568ce7..cdb5596a9bc 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -926,9 +926,8 @@ 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) { OrderedIndexItem *new_node = orderedIndexUpdateScore(zs->oi, old_node, score); - /* Note that this assignment updates the node pointer stored in - * the hashtable */ - if (new_node) *node_ref_in_hashtable = new_node; + /* Update the node pointer stored in the hashtable. */ + *node_ref_in_hashtable = new_node; *out_flags |= ZADD_OUT_UPDATED; } return 1; From 0f6c18a4945e37109fbcce06fc42f8e52d71f7e3 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Thu, 28 May 2026 02:24:13 +0000 Subject: [PATCH 36/45] unit tests: use --seed flag for fuzz test reproducibility Fuzz tests now use the framework's --seed flag if provided, defaulting to 42 for deterministic CI. The seed is printed on each fuzz test run so failures can be reproduced with: valkey-unit-gtests --seed Signed-off-by: Rain Valentine --- src/unit/test_ordered_index.cpp | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/unit/test_ordered_index.cpp b/src/unit/test_ordered_index.cpp index a716e5333f0..c7a33a03e07 100644 --- a/src/unit/test_ordered_index.cpp +++ b/src/unit/test_ordered_index.cpp @@ -1065,6 +1065,14 @@ TEST_P(OrderedIndexTest, DeleteRangeByLexPreservesOutside) { /* ========== 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, seedable, no STL. */ static uint32_t test_rand_next(uint32_t *state) { *state ^= *state << 13; @@ -1110,7 +1118,7 @@ static std::vector test_build_random_index(OrderedIndexTestApi } TEST_P(OrderedIndexTest, RandomizedInsertAndTraversal) { - uint32_t rng = 42; + uint32_t rng = test_fuzz_seed(); for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 1, 50); @@ -1138,7 +1146,7 @@ TEST_P(OrderedIndexTest, RandomizedInsertAndTraversal) { } TEST_P(OrderedIndexTest, RandomizedBackwardTraversal) { - uint32_t rng = 42; + uint32_t rng = test_fuzz_seed(); for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 1, 50); @@ -1163,7 +1171,7 @@ TEST_P(OrderedIndexTest, RandomizedBackwardTraversal) { } TEST_P(OrderedIndexTest, RandomizedScoreRetrieval) { - uint32_t rng = 42; + uint32_t rng = test_fuzz_seed(); for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 1, 50); @@ -1178,7 +1186,7 @@ TEST_P(OrderedIndexTest, RandomizedScoreRetrieval) { } TEST_P(OrderedIndexTest, RandomizedIndexConsistency) { - uint32_t rng = 42; + uint32_t rng = test_fuzz_seed(); for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 1, 50); @@ -1203,7 +1211,7 @@ TEST_P(OrderedIndexTest, RandomizedIndexConsistency) { } TEST_P(OrderedIndexTest, RandomizedDelete) { - uint32_t rng = 42; + uint32_t rng = test_fuzz_seed(); for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 2, 30); @@ -1233,7 +1241,7 @@ TEST_P(OrderedIndexTest, RandomizedDelete) { } TEST_P(OrderedIndexTest, RandomizedUpdateScore) { - uint32_t rng = 42; + uint32_t rng = test_fuzz_seed(); for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 2, 30); @@ -1263,7 +1271,7 @@ TEST_P(OrderedIndexTest, RandomizedUpdateScore) { } TEST_P(OrderedIndexTest, RandomizedPop) { - uint32_t rng = 42; + uint32_t rng = test_fuzz_seed(); for (int trial = 0; trial < 10; trial++) { int n = test_rand_range(&rng, 3, 30); @@ -1308,7 +1316,7 @@ TEST_P(OrderedIndexTest, RandomizedPop) { } TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { - uint32_t rng = 42; + uint32_t rng = test_fuzz_seed(); for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 5, 40); @@ -1344,7 +1352,7 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { } TEST_P(OrderedIndexTest, RandomizedDeleteRangeByIndex) { - uint32_t rng = 42; + uint32_t rng = test_fuzz_seed(); for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 5, 40); @@ -1378,7 +1386,7 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByIndex) { } TEST_P(OrderedIndexTest, RandomizedForwardBackwardMirror) { - uint32_t rng = 42; + uint32_t rng = test_fuzz_seed(); for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 1, 50); From 11370f7a57721cce5e7a365a3adc8f4192d40d87 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Wed, 10 Jun 2026 22:03:04 +0000 Subject: [PATCH 37/45] ordered-index: fix OnDelete callback contract to notification-before-free Change OrderedIndexOnDelete semantics from 'callback takes ownership' to 'notification before free'. The index always frees items after the callback returns. This is simpler and avoids ownership confusion when multiple backends exist. Update skiplist backend to always free the node after calling the callback (previously it skipped the free when callback was provided). Remove orderedIndexFreeItem from zsetIndexDeleteCallback since the index handles freeing. Signed-off-by: Rain Valentine Signed-off-by: Rain Valentine --- src/ordered_index.h | 4 +++- src/skiplist_ordered_index.c | 21 ++++++--------------- src/t_zset.c | 1 - 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/ordered_index.h b/src/ordered_index.h index a3ca5c23dd2..d4b06628069 100644 --- a/src/ordered_index.h +++ b/src/ordered_index.h @@ -34,7 +34,9 @@ typedef struct OrderedIndexItem OrderedIndexItem; typedef uint64_t OrderedIndexIterator[2]; /* Callback invoked for each item removed during a range-delete operation. - * The callback receives ownership of the item — it must free it or store it. */ + * 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 diff --git a/src/skiplist_ordered_index.c b/src/skiplist_ordered_index.c index 6dbc44bfa06..d6f4fad8f1d 100644 --- a/src/skiplist_ordered_index.c +++ b/src/skiplist_ordered_index.c @@ -103,11 +103,8 @@ unsigned long skiplistDeleteRangeByScore(OrderedIndex *oi, double min, double ma while (x && zsetScoreLteMax(x->score, &range)) { zskiplistNode *next = x->level[0].forward; zslUnlinkNode(zsl, x, update); - if (on_delete) { - on_delete((OrderedIndexItem *)x, ctx); - } else { - zslFreeNode(x); - } + if (on_delete) on_delete((OrderedIndexItem *)x, ctx); + zslFreeNode(x); removed++; x = next; @@ -139,11 +136,8 @@ unsigned long skiplistDeleteRangeByIndex(OrderedIndex *oi, unsigned long start, while (x && traversed <= end) { zskiplistNode *next = x->level[0].forward; zslUnlinkNode(zsl, x, update); - if (on_delete) { - on_delete((OrderedIndexItem *)x, ctx); - } else { - zslFreeNode(x); - } + if (on_delete) on_delete((OrderedIndexItem *)x, ctx); + zslFreeNode(x); removed++; traversed++; @@ -178,11 +172,8 @@ unsigned long skiplistDeleteRangeByLex(OrderedIndex *oi, const_sds min, const_sd 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); - } else { - zslFreeNode(x); - } + if (on_delete) on_delete((OrderedIndexItem *)x, ctx); + zslFreeNode(x); removed++; x = next; diff --git a/src/t_zset.c b/src/t_zset.c index cdb5596a9bc..d6e4df290d4 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -1325,7 +1325,6 @@ static void zsetIndexDeleteCallback(OrderedIndexItem *item, void *ctx) { size_t len; orderedIndexGetElementRaw(item, &ptr, &len); hashtableDelete(ht, (sds)ptr); - orderedIndexFreeItem(item); } /* Implements ZREMRANGEBYRANK, ZREMRANGEBYSCORE, ZREMRANGEBYLEX commands. */ From e3fdcbd262b9c6161afc47483a2bfe241dff015b Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Wed, 10 Jun 2026 22:24:37 +0000 Subject: [PATCH 38/45] tests: add LexRangeSentinels test for shared.minstring/maxstring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test that countLexRange, seekToLexRange, and deleteRangeByLex handle the shared.minstring (-inf) and shared.maxstring (+inf) sentinel pointers correctly. These are distinct from literal sds strings with the same content — the interface uses pointer identity to detect them. Initialize shared.minstring/maxstring in the test fixture SetUp since createSharedObjects is not called in the unit test binary. Signed-off-by: Rain Valentine Signed-off-by: Rain Valentine --- src/unit/test_ordered_index.cpp | 42 +++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/unit/test_ordered_index.cpp b/src/unit/test_ordered_index.cpp index c7a33a03e07..68f19eefa96 100644 --- a/src/unit/test_ordered_index.cpp +++ b/src/unit/test_ordered_index.cpp @@ -52,6 +52,9 @@ class OrderedIndexTest : public ::testing::TestWithParam 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 { @@ -1063,6 +1066,41 @@ TEST_P(OrderedIndexTest, DeleteRangeByLexPreservesOutside) { } } +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"); + + /* 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, sdsnew("charlie"), 0, 0), 3UL); + ASSERT_EQ(api.countLexRange(oi, sdsnew("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, sdsnew("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); +} + /* ========== Randomized property tests ========== */ /* Default fuzz seed — overridden by --seed flag if provided. */ @@ -1493,7 +1531,7 @@ static void testOnDeleteCallback(OrderedIndexItem *item, void *ctx) { size_t len; skiplistGetElementRaw(item, &ptr, &len); rec->elements.emplace_back(ptr, len); - orderedIndexFreeItem(item); + /* Item is freed by the index after this callback returns. */ } class OnDeleteCallbackTest : public ::testing::Test { @@ -1826,7 +1864,7 @@ static void hashtableConsistencyOnDelete(OrderedIndexItem *item, void *ctx) { size_t len; skiplistGetElementRaw(item, &ptr, &len); ht->erase(std::string(ptr, len)); - orderedIndexFreeItem(item); + /* Item is freed by the index after this callback returns. */ } class RangeDeleteHashtableConsistencyTest : public ::testing::Test { From b695c0cb6dc6e05beeaab3f94f82647da15a3a47 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Thu, 11 Jun 2026 19:15:19 +0000 Subject: [PATCH 39/45] Fix ASAN memory leaks in LexRangeSentinels unit test - sdsnew("charlie") was called 3 times inline in assertions without ever being freed. Use a local sds variable and free it at end. - shared.minstring/maxstring were allocated in SetUp() but never freed on process exit. Add destructor function to clean them up. Signed-off-by: Rain Valentine --- src/unit/test_ordered_index.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/unit/test_ordered_index.cpp b/src/unit/test_ordered_index.cpp index 68f19eefa96..70409ed3c7c 100644 --- a/src/unit/test_ordered_index.cpp +++ b/src/unit/test_ordered_index.cpp @@ -23,6 +23,13 @@ extern "C" { #include #include +/* 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; } +} + /* Verify structural integrity of the ordered index after mutations. */ static ::testing::AssertionResult verifyIntegrity(OrderedIndexTestApi &api, OrderedIndex *oi) { char errmsg[256]; @@ -1074,15 +1081,17 @@ TEST_P(OrderedIndexTest, LexRangeSentinels) { 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, sdsnew("charlie"), 0, 0), 3UL); - ASSERT_EQ(api.countLexRange(oi, sdsnew("charlie"), shared.maxstring, 0, 0), 3UL); + 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, sdsnew("charlie"), shared.minstring, 0, 0), 0UL); + ASSERT_EQ(api.countLexRange(oi, charlie, shared.minstring, 0, 0), 0UL); /* Seek with sentinels - iterate all */ OrderedIndexIterator it; @@ -1099,6 +1108,8 @@ TEST_P(OrderedIndexTest, LexRangeSentinels) { /* 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 ========== */ From 64fd5902114166c5fe030f9124524022d8575c35 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Thu, 11 Jun 2026 19:34:49 +0000 Subject: [PATCH 40/45] Address review comments: underflow guard and const fix - t_zset.c: guard orderedIndexSeekToIndex with length > 0 check to prevent unsigned underflow on empty sets (eifrah-aws comment). - module.c: avoid casting away const from orderedIndexGetElementRaw by using ele/ele_len directly with createStringObject and returning early (eifrah-aws const nit). Signed-off-by: Rain Valentine --- src/module.c | 6 +++++- src/t_zset.c | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/module.c b/src/module.c index cb8281dc6da..49d196c4335 100644 --- a/src/module.c +++ b/src/module.c @@ -12024,8 +12024,12 @@ static void moduleScanKeyHashtableCallback(void *privdata, void *entry) { const char *ele; size_t ele_len; orderedIndexGetElementRaw((const OrderedIndexItem *)entry, &ele, &ele_len); - key = (sds)ele; + robj *field = createStringObject(ele, ele_len); value = createStringObjectFromLongDouble(orderedIndexGetScore((const OrderedIndexItem *)entry), 0); + data->fn(data->key, field, value, data->user_data); + decrRefCount(field); + if (value) decrRefCount(value); + return; } else if (o->type == OBJ_HASH) { key = entryGetField(entry); size_t val_len; diff --git a/src/t_zset.c b/src/t_zset.c index d6e4df290d4..f92969c3fc4 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -1527,7 +1527,8 @@ static void zuiInitIterator(zsetopsrc *op) { } else if (op->encoding == OBJ_ENCODING_SKIPLIST) { it->sl.zs = objectGetVal(op->subject); orderedIndexInitIterator(&it->sl.iter, it->sl.zs->oi); - orderedIndexSeekToIndex(&it->sl.iter, orderedIndexLength(it->sl.zs->oi) - 1); + unsigned long len = orderedIndexLength(it->sl.zs->oi); + if (len > 0) orderedIndexSeekToIndex(&it->sl.iter, len - 1); it->sl.node = NULL; } else { serverPanic("Unknown sorted set encoding"); From 4192dbe568a4e48bf00c2d62cae24b54d44b784e Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Thu, 11 Jun 2026 19:39:40 +0000 Subject: [PATCH 41/45] Add O(1) orderedIndexGetFirst/GetLast, use in ZPOP ZPOP was using orderedIndexGetByIndex(len-1) which is O(log n) in skiplist. The old code used direct zsl->tail access which was O(1). Add orderedIndexGetFirst/GetLast to restore O(1) head/tail access through the abstraction layer (eifrah-aws review comment). Signed-off-by: Rain Valentine --- src/ordered_index.c | 8 ++++++++ src/ordered_index.h | 6 ++++++ src/skiplist.c | 4 ++++ src/skiplist.h | 1 + src/skiplist_ordered_index.c | 8 ++++++++ src/skiplist_ordered_index.h | 2 ++ src/t_zset.c | 2 +- 7 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/ordered_index.c b/src/ordered_index.c index 83e6eed9951..4ffd634b000 100644 --- a/src/ordered_index.c +++ b/src/ordered_index.c @@ -81,6 +81,14 @@ 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); } diff --git a/src/ordered_index.h b/src/ordered_index.h index d4b06628069..6222837df93 100644 --- a/src/ordered_index.h +++ b/src/ordered_index.h @@ -115,6 +115,12 @@ 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); diff --git a/src/skiplist.c b/src/skiplist.c index c9c2cf28a07..ca088009241 100644 --- a/src/skiplist.c +++ b/src/skiplist.c @@ -651,6 +651,10 @@ zskiplistNode *zslGetFirst(const zskiplist *zsl) { return ((zskiplist *)zsl)->header.level[0].forward; } +zskiplistNode *zslGetLast(const zskiplist *zsl) { + return zslGetTail(zsl); +} + double zslGetScore(const zskiplistNode *node) { return node->score; } diff --git a/src/skiplist.h b/src/skiplist.h index a3a77b081fd..e13e792c6b2 100644 --- a/src/skiplist.h +++ b/src/skiplist.h @@ -174,6 +174,7 @@ void zslSeekToLexRange(zslIter *iter, const_sds min, const_sds max, int min_ex, /* Additional accessors */ zskiplistNode *zslGetFirst(const zskiplist *zsl); +zskiplistNode *zslGetLast(const zskiplist *zsl); double zslGetScore(const zskiplistNode *node); zskiplistNode *zslDetachNode(zskiplist *zsl, zskiplistNode *node); diff --git a/src/skiplist_ordered_index.c b/src/skiplist_ordered_index.c index d6f4fad8f1d..62090e54bb9 100644 --- a/src/skiplist_ordered_index.c +++ b/src/skiplist_ordered_index.c @@ -191,6 +191,14 @@ 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 *)zslGetLast((const zskiplist *)oi); +} + unsigned long skiplistGetIndex(OrderedIndex *oi, const OrderedIndexItem *node) { return zslGetRank((zskiplist *)oi, (const zskiplistNode *)node) - 1; } diff --git a/src/skiplist_ordered_index.h b/src/skiplist_ordered_index.h index 0d6727b3aae..e498e2aca28 100644 --- a/src/skiplist_ordered_index.h +++ b/src/skiplist_ordered_index.h @@ -39,6 +39,8 @@ unsigned long skiplistDeleteRangeByLex(OrderedIndex *oi, const_sds min, const_sd /* 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); diff --git a/src/t_zset.c b/src/t_zset.c index f92969c3fc4..822b772761b 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -3289,7 +3289,7 @@ void genericZpopCommand(client *c, OrderedIndexItem *zln; /* Get the first or last element in the sorted set. */ - zln = (where == ZSET_MAX ? orderedIndexGetByIndex(oi, orderedIndexLength(oi) - 1) : orderedIndexGetByIndex(oi, 0)); + zln = (where == ZSET_MAX ? orderedIndexGetLast(oi) : orderedIndexGetFirst(oi)); /* There must be an element in the sorted set. */ serverAssertWithInfo(c, zobj, zln != NULL); From 7a7c206d9f76f729abc5d98e225584d847f4f26e Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Thu, 11 Jun 2026 19:57:40 +0000 Subject: [PATCH 42/45] Eliminate C++ STL from ordered index unit tests Replace std::vector, std::string, std::set, std::sort, std::reverse, std::min/max with C-style equivalents: - Fixed-size C arrays (size always known from orderedIndexLength or insert count) instead of std::vector - sds instead of std::string for dynamic strings - snprintf instead of std::to_string - qsort + custom comparator instead of std::sort - Manual swap loop instead of std::reverse - Sorted sds arrays instead of std::set for unordered comparison - ASSERT_SDS_ARRAY_EQ macro for concise array-vs-literal assertions The only remaining std::string is in the GTest name generator function, which is required by the GTest INSTANTIATE_TEST_SUITE_P API. Addresses review comment from madolson regarding C++ construct usage in tests (per previous direction from Viktor). Signed-off-by: Rain Valentine --- src/unit/ordered_index_test.h | 17 - src/unit/test_ordered_index.cpp | 532 ++++++++++++++++++++++---------- 2 files changed, 361 insertions(+), 188 deletions(-) diff --git a/src/unit/ordered_index_test.h b/src/unit/ordered_index_test.h index 868c342d50c..e2d88c12250 100644 --- a/src/unit/ordered_index_test.h +++ b/src/unit/ordered_index_test.h @@ -15,8 +15,6 @@ extern "C" { } #include -#include -#include /* ---- Abstract interface ---- */ @@ -69,21 +67,6 @@ class OrderedIndexTestApi { OrderedIndexItem *insertSds(OrderedIndex *oi, double score, const_sds ele) { return insert(oi, score, ele, sdslen(ele)); } - - std::vector> collectAll(OrderedIndex *oi) { - std::vector> result; - OrderedIndexIterator iter; - OrderedIndexItem *pos; - initIterator(&iter, oi); - while ((pos = next(&iter)) != NULL) { - const char *ptr; - size_t len; - getElementRaw(pos, &ptr, &len); - result.emplace_back(getScore(pos), std::string(ptr, len)); - } - resetIterator(&iter); - return result; - } }; /* ---- Skiplist implementation ---- */ diff --git a/src/unit/test_ordered_index.cpp b/src/unit/test_ordered_index.cpp index 70409ed3c7c..52c3fb6d977 100644 --- a/src/unit/test_ordered_index.cpp +++ b/src/unit/test_ordered_index.cpp @@ -10,24 +10,84 @@ extern "C" { #include "server.h" } -/* Undefine min/max macros from server.h to avoid conflicts with C++ standard library */ +/* Undefine min/max macros from server.h to avoid conflicts */ #undef min #undef max #include "ordered_index_test.h" -#include #include #include -#include -#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; } + 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. */ @@ -247,8 +307,10 @@ TEST_P(OrderedIndexTest, DuplicateScores) { 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, (std::string("key") + std::to_string(i)).c_str()); + assertElement(pos, buf); } api.resetIterator(&iter); } @@ -1122,7 +1184,7 @@ static uint32_t test_fuzz_seed(void) { return s; } -/* Simple xorshift32 PRNG — deterministic, seedable, no STL. */ +/* Simple xorshift32 PRNG — deterministic and seedable. */ static uint32_t test_rand_next(uint32_t *state) { *state ^= *state << 13; *state ^= *state >> 17; @@ -1141,12 +1203,12 @@ static double test_rand_double(uint32_t *state, double lo, double hi) { struct RandomIndexEntry { OrderedIndexItem *node; double score; - std::string element; + sds element; }; -static std::string test_random_element(uint32_t *state, int maxLen) { +static sds test_random_element(uint32_t *state, int maxLen) { int len = test_rand_range(state, 1, maxLen); - std::string s(len, ' '); + sds s = sdsnewlen(NULL, len); for (int i = 0; i < len; i++) s[i] = (char)test_rand_range(state, 'a', 'z'); return s; } @@ -1155,23 +1217,33 @@ static double test_random_score(uint32_t *state) { return test_rand_double(state, -1e6, 1e6); } -static std::vector test_build_random_index(OrderedIndexTestApi &api, OrderedIndex *oi, uint32_t *state, int count) { - std::vector entries; +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); - std::string elem = test_random_element(state, 16) + std::to_string(i); - OrderedIndexItem *node = api.insert(oi, score, elem.c_str(), elem.size()); - entries.push_back({node, score, elem}); + 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); - test_build_random_index(api, oi, &rng, n); + { + RandomIndexEntry *_e = test_build_random_index(api, oi, &rng, n); + freeRandomEntries(_e, n); + } ASSERT_EQ(api.length(oi), (unsigned long)n); verifyOI(); @@ -1199,7 +1271,10 @@ TEST_P(OrderedIndexTest, RandomizedBackwardTraversal) { for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 1, 50); - test_build_random_index(api, oi, &rng, n); + { + RandomIndexEntry *_e = test_build_random_index(api, oi, &rng, n); + freeRandomEntries(_e, n); + } OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1224,11 +1299,12 @@ TEST_P(OrderedIndexTest, RandomizedScoreRetrieval) { for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 1, 50); - auto entries = test_build_random_index(api, oi, &rng, n); + RandomIndexEntry *entries = test_build_random_index(api, oi, &rng, n); - for (auto &e : entries) { - assertScore(e.node, e.score); + for (int i = 0; i < n; i++) { + assertScore(entries[i].node, entries[i].score); } + freeRandomEntries(entries, n); api.free(oi); oi = api.create(); } @@ -1239,7 +1315,10 @@ TEST_P(OrderedIndexTest, RandomizedIndexConsistency) { for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 1, 50); - test_build_random_index(api, oi, &rng, n); + { + RandomIndexEntry *_e = test_build_random_index(api, oi, &rng, n); + freeRandomEntries(_e, n); + } OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1264,7 +1343,7 @@ TEST_P(OrderedIndexTest, RandomizedDelete) { for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 2, 30); - auto entries = test_build_random_index(api, oi, &rng, n); + 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); @@ -1294,7 +1373,7 @@ TEST_P(OrderedIndexTest, RandomizedUpdateScore) { for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 2, 30); - auto entries = test_build_random_index(api, oi, &rng, n); + 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); @@ -1324,7 +1403,10 @@ TEST_P(OrderedIndexTest, RandomizedPop) { for (int trial = 0; trial < 10; trial++) { int n = test_rand_range(&rng, 3, 30); - test_build_random_index(api, oi, &rng, n); + { + RandomIndexEntry *_e = test_build_random_index(api, oi, &rng, n); + freeRandomEntries(_e, n); + } OrderedIndexIterator iter; OrderedIndexItem *pos; @@ -1369,14 +1451,14 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 5, 40); - auto entries = test_build_random_index(api, oi, &rng, n); + RandomIndexEntry *entries = test_build_random_index(api, oi, &rng, n); double s1 = test_random_score(&rng), s2 = test_random_score(&rng); - double lo = (std::min)(s1, s2), hi = (std::max)(s1, s2); + double lo = TEST_MIN(s1, s2), hi = TEST_MAX(s1, s2); int expectedDeleted = 0; - for (auto &e : entries) { - if (e.score >= lo && e.score <= hi) expectedDeleted++; + 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); @@ -1405,11 +1487,14 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByIndex) { for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 5, 40); - test_build_random_index(api, oi, &rng, n); + { + 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)(std::min)(r1, r2); - unsigned long end = (unsigned long)(std::max)(r1, r2); + 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); @@ -1439,29 +1524,36 @@ TEST_P(OrderedIndexTest, RandomizedForwardBackwardMirror) { for (int trial = 0; trial < 20; trial++) { int n = test_rand_range(&rng, 1, 50); - test_build_random_index(api, oi, &rng, n); + { + RandomIndexEntry *_e = test_build_random_index(api, oi, &rng, n); + freeRandomEntries(_e, n); + } - std::vector forwardScores; + 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.push_back(api.getScore(pos)); + forwardScores[fi++] = api.getScore(pos); } api.resetIterator(&iter); - std::vector backwardScores; + double *backwardScores = (double *)zmalloc(sizeof(double) * n); + int bi = 0; api.initIterator(&iter, oi); while (((pos = api.prev(&iter)) != NULL)) { - backwardScores.push_back(api.getScore(pos)); + backwardScores[bi++] = api.getScore(pos); } api.resetIterator(&iter); - ASSERT_EQ(forwardScores.size(), backwardScores.size()); - std::reverse(backwardScores.begin(), backwardScores.end()); - for (size_t i = 0; i < forwardScores.size(); i++) { + 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(); } @@ -1532,16 +1624,28 @@ INSTANTIATE_TEST_SUITE_P(AllImplementations, struct OnDeleteRecord { int count; - std::vector elements; + int capacity; + sds *elements; /* Fixed-size array allocated at init */ }; +static void initOnDeleteRecord(OnDeleteRecord *rec, int capacity) { + 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; - rec->count++; const char *ptr; size_t len; skiplistGetElementRaw(item, &ptr, &len); - rec->elements.emplace_back(ptr, len); + rec->elements[rec->count] = sdsnewlen(ptr, len); + rec->count++; /* Item is freed by the index after this callback returns. */ } @@ -1568,37 +1672,29 @@ class OnDeleteCallbackTest : public ::testing::Test { void insertN(int n) { for (int i = 0; i < n; i++) { - std::string name = "key" + std::to_string(i); - insert((double)i, name.c_str()); + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + insert((double)i, buf); } } - void insertLex(const std::vector &elems, double score = 1.0) { - for (auto &e : elems) { - insert(score, e.c_str()); + void insertLex(const char *elems[], int count, double score = 1.0) { + for (int i = 0; i < count; i++) { + insert(score, elems[i]); } } - std::vector collectElements(OrderedIndex *oi) { - std::vector result; - OrderedIndexIterator iter; - OrderedIndexItem *pos; - api.initIterator(&iter, oi); - while (((pos = api.next(&iter)) != NULL)) { - const char *ptr; - size_t len; - api.getElementRaw(pos, &ptr, &len); - result.emplace_back(ptr, len); - } - api.resetIterator(&iter); - return result; + /* 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_F(OnDeleteCallbackTest, DeleteRangeByScore_EmptyAndNoMatch) { - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 10); unsigned long deleted = api.deleteRangeByScore(oi, 0.0, 10.0, 0, 0, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 0UL); @@ -1617,24 +1713,30 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_EmptyAndNoMatch) { TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_Subset) { insertN(10); - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 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(); - std::sort(rec.elements.begin(), rec.elements.end()); - ASSERT_EQ(rec.elements, (std::vector{"key3", "key4", "key5", "key6"})); + sortSdsArray(rec.elements, rec.count); + ASSERT_SDS_ARRAY_EQ(rec.elements, rec.count, "key3", "key4", "key5", "key6"); - auto remaining = collectElements(oi); - ASSERT_EQ(remaining, (std::vector{"key0", "key1", "key2", "key7", "key8", "key9"})); + { + size_t _rn; + sds *_r = collectElements(oi, &_rn); + ASSERT_SDS_ARRAY_EQ(_r, _rn, "key0", "key1", "key2", "key7", "key8", "key9"); + freeSdsArray(_r, _rn); + } } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_All) { insertN(5); - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 10); unsigned long deleted = api.deleteRangeByScore(oi, NEG_INF, POS_INF, 0, 0, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 5UL); ASSERT_EQ(rec.count, 5); @@ -1653,30 +1755,33 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_NullCallback) { TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_ExclusiveBounds) { insertN(10); - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 10); unsigned long deleted = api.deleteRangeByScore(oi, 3.0, 7.0, 1, 1, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 3UL); ASSERT_EQ(rec.count, 3); - std::sort(rec.elements.begin(), rec.elements.end()); - ASSERT_EQ(rec.elements, (std::vector{"key4", "key5", "key6"})); + sortSdsArray(rec.elements, rec.count); + ASSERT_SDS_ARRAY_EQ(rec.elements, rec.count, "key4", "key5", "key6"); ASSERT_EQ(api.length(oi), 7UL); } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_SingleElement) { insertN(5); - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 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_EQ(rec.elements[0], "key2"); + ASSERT_STREQ(rec.elements[0], "key2"); ASSERT_EQ(api.length(oi), 4UL); } /* DeleteRangeByIndex */ TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_EmptyAndNoMatch) { - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 10); unsigned long deleted = api.deleteRangeByIndex(oi, 0, 4, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 0UL); @@ -1695,23 +1800,29 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_EmptyAndNoMatch) { TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_Subset) { insertN(10); - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 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); - std::sort(rec.elements.begin(), rec.elements.end()); - ASSERT_EQ(rec.elements, (std::vector{"key2", "key3", "key4"})); + sortSdsArray(rec.elements, rec.count); + ASSERT_SDS_ARRAY_EQ(rec.elements, rec.count, "key2", "key3", "key4"); - auto remaining = collectElements(oi); - ASSERT_EQ(remaining, (std::vector{"key0", "key1", "key5", "key6", "key7", "key8", "key9"})); + { + size_t _rn; + sds *_r = collectElements(oi, &_rn); + ASSERT_SDS_ARRAY_EQ(_r, _rn, "key0", "key1", "key5", "key6", "key7", "key8", "key9"); + freeSdsArray(_r, _rn); + } } TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_All) { insertN(5); - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 10); unsigned long deleted = api.deleteRangeByIndex(oi, 0, 4, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 5UL); ASSERT_EQ(rec.count, 5); @@ -1729,31 +1840,38 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_NullCallback) { TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_ExclusiveBounds) { insertN(5); - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 10); unsigned long deleted = api.deleteRangeByIndex(oi, 2, 2, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 1UL); ASSERT_EQ(rec.count, 1); - ASSERT_EQ(rec.elements[0], "key2"); + ASSERT_STREQ(rec.elements[0], "key2"); - auto remaining = collectElements(oi); - ASSERT_EQ(remaining, (std::vector{"key0", "key1", "key3", "key4"})); + { + size_t _rn; + sds *_r = collectElements(oi, &_rn); + ASSERT_SDS_ARRAY_EQ(_r, _rn, "key0", "key1", "key3", "key4"); + freeSdsArray(_r, _rn); + } } TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_SingleElement) { insertN(5); - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 10); unsigned long deleted = api.deleteRangeByIndex(oi, 0, 0, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 1UL); ASSERT_EQ(rec.count, 1); - ASSERT_EQ(rec.elements[0], "key0"); + ASSERT_STREQ(rec.elements[0], "key0"); ASSERT_EQ(api.length(oi), 4UL); } /* DeleteRangeByLex */ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 10); sds min = sdsnew("a"); sds max = sdsnew("z"); @@ -1765,7 +1883,10 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { api.free(oi); oi = api.create(); - insertLex({"apple", "banana", "cherry"}); + { + const char *_l[] = {"apple", "banana", "cherry"}; + insertLex(_l, 3); + } rec = {0, {}}; min = sdsnew("x"); max = sdsnew("z"); @@ -1778,9 +1899,13 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_Subset) { - insertLex({"apple", "banana", "cherry", "date", "elderberry"}); + { + const char *_l[] = {"apple", "banana", "cherry", "date", "elderberry"}; + insertLex(_l, 5); + } - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 10); sds min = sdsnew("banana"); sds max = sdsnew("date"); unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); @@ -1788,20 +1913,28 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_Subset) { ASSERT_EQ(rec.count, 3); ASSERT_EQ(api.length(oi), 2UL); - std::sort(rec.elements.begin(), rec.elements.end()); - ASSERT_EQ(rec.elements, (std::vector{"banana", "cherry", "date"})); + sortSdsArray(rec.elements, rec.count); + ASSERT_SDS_ARRAY_EQ(rec.elements, rec.count, "banana", "cherry", "date"); - auto remaining = collectElements(oi); - ASSERT_EQ(remaining, (std::vector{"apple", "elderberry"})); + { + size_t _rn; + sds *_r = collectElements(oi, &_rn); + ASSERT_SDS_ARRAY_EQ(_r, _rn, "apple", "elderberry"); + freeSdsArray(_r, _rn); + } sdsfree(min); sdsfree(max); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_All) { - insertLex({"apple", "banana", "cherry"}); + { + const char *_l[] = {"apple", "banana", "cherry"}; + insertLex(_l, 3); + } - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 10); sds min = sdsnew("a"); sds max = sdsnew("z"); unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); @@ -1814,7 +1947,10 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_All) { } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_NullCallback) { - insertLex({"apple", "banana", "cherry", "date"}); + { + const char *_l[] = {"apple", "banana", "cherry", "date"}; + insertLex(_l, 4); + } sds min = sdsnew("banana"); sds max = sdsnew("cherry"); @@ -1822,46 +1958,66 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_NullCallback) { ASSERT_EQ(deleted, 2UL); ASSERT_EQ(api.length(oi), 2UL); - auto remaining = collectElements(oi); - ASSERT_EQ(remaining, (std::vector{"apple", "date"})); + { + size_t _rn; + sds *_r = collectElements(oi, &_rn); + ASSERT_SDS_ARRAY_EQ(_r, _rn, "apple", "date"); + freeSdsArray(_r, _rn); + } sdsfree(min); sdsfree(max); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_ExclusiveBounds) { - insertLex({"apple", "banana", "cherry", "date", "elderberry"}); + { + const char *_l[] = {"apple", "banana", "cherry", "date", "elderberry"}; + insertLex(_l, 5); + } - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 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_EQ(rec.elements[0], "cherry"); + ASSERT_STREQ(rec.elements[0], "cherry"); ASSERT_EQ(api.length(oi), 4UL); - auto remaining = collectElements(oi); - ASSERT_EQ(remaining, (std::vector{"apple", "banana", "date", "elderberry"})); + { + 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); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_SingleElement) { - insertLex({"apple", "banana", "cherry"}); + { + const char *_l[] = {"apple", "banana", "cherry"}; + insertLex(_l, 3); + } - OnDeleteRecord rec = {0, {}}; + OnDeleteRecord rec; + initOnDeleteRecord(&rec, 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_EQ(rec.elements[0], "banana"); + ASSERT_STREQ(rec.elements[0], "banana"); ASSERT_EQ(api.length(oi), 2UL); - auto remaining = collectElements(oi); - ASSERT_EQ(remaining, (std::vector{"apple", "cherry"})); + { + size_t _rn; + sds *_r = collectElements(oi, &_rn); + ASSERT_SDS_ARRAY_EQ(_r, _rn, "apple", "cherry"); + freeSdsArray(_r, _rn); + } sdsfree(min); sdsfree(max); @@ -1869,12 +2025,48 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_SingleElement) { /* ========== 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) { - std::set *ht = (std::set *)ctx; + SimHt *ht = (SimHt *)ctx; const char *ptr; size_t len; skiplistGetElementRaw(item, &ptr, &len); - ht->erase(std::string(ptr, len)); + simHtRemove(ht, ptr, len); /* Item is freed by the index after this callback returns. */ } @@ -1894,152 +2086,150 @@ class RangeDeleteHashtableConsistencyTest : public ::testing::Test { api.insert(oi, score, ele, strlen(ele)); } - void insertN(std::set &ht, int n) { + void insertN(SimHt &ht, int n) { for (int i = 0; i < n; i++) { - std::string name = "key" + std::to_string(i); - insert((double)i, name.c_str()); - ht.insert(name); + char buf[32]; + snprintf(buf, sizeof(buf), "key%d", i); + insert((double)i, buf); + simHtAdd(&ht, buf, strlen(buf)); } } - void insertLex(std::set &ht, const std::vector &elems, double score = 1.0) { - for (auto &e : elems) { - insert(score, e.c_str()); - ht.insert(e); + 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])); } } - std::set collectIndexElements(OrderedIndex *oi) { - std::set result; - OrderedIndexIterator iter; - OrderedIndexItem *pos; - api.initIterator(&iter, oi); - while (((pos = api.next(&iter)) != NULL)) { - const char *ptr; - size_t len; - api.getElementRaw(pos, &ptr, &len); - result.insert(std::string(ptr, len)); + 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]); } - api.resetIterator(&iter); - return result; + freeSdsArray(idx_elems, idx_n); } }; /* ByScore */ TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_PartialDelete) { - std::set simulatedHt; + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); insertN(simulatedHt, 10); api.deleteRangeByScore(oi, 3.0, 6.0, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(oi); - ASSERT_EQ(indexElements, simulatedHt); - ASSERT_EQ(indexElements.size(), 6UL); + assertHtMatchesIndex(simulatedHt); } TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_FullDelete) { - std::set simulatedHt; + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); insertN(simulatedHt, 10); api.deleteRangeByScore(oi, NEG_INF, POS_INF, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(oi); - ASSERT_EQ(indexElements, simulatedHt); - ASSERT_TRUE(indexElements.empty()); + assertHtMatchesIndex(simulatedHt); } TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_EmptyRange) { - std::set simulatedHt; + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); insertN(simulatedHt, 10); api.deleteRangeByScore(oi, 20.0, 30.0, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(oi); - ASSERT_EQ(indexElements, simulatedHt); - ASSERT_EQ(indexElements.size(), 10UL); + assertHtMatchesIndex(simulatedHt); } /* ByIndex */ TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_PartialDelete) { - std::set simulatedHt; + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); insertN(simulatedHt, 10); api.deleteRangeByIndex(oi, 2, 4, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(oi); - ASSERT_EQ(indexElements, simulatedHt); - ASSERT_EQ(indexElements.size(), 7UL); + assertHtMatchesIndex(simulatedHt); } TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_FullDelete) { - std::set simulatedHt; + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); insertN(simulatedHt, 10); api.deleteRangeByIndex(oi, 0, 9, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(oi); - ASSERT_EQ(indexElements, simulatedHt); - ASSERT_TRUE(indexElements.empty()); + assertHtMatchesIndex(simulatedHt); } TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_EmptyRange) { - std::set simulatedHt; + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); insertN(simulatedHt, 10); api.deleteRangeByIndex(oi, 20, 30, hashtableConsistencyOnDelete, &simulatedHt); - std::set indexElements = collectIndexElements(oi); - ASSERT_EQ(indexElements, simulatedHt); - ASSERT_EQ(indexElements.size(), 10UL); + assertHtMatchesIndex(simulatedHt); } /* ByLex */ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_PartialDelete) { - std::set simulatedHt; - insertLex(simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); + { + 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, &simulatedHt); - std::set indexElements = collectIndexElements(oi); - ASSERT_EQ(indexElements, simulatedHt); - ASSERT_EQ(indexElements.size(), 2UL); + assertHtMatchesIndex(simulatedHt); sdsfree(min); sdsfree(max); } TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_FullDelete) { - std::set simulatedHt; - insertLex(simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); + { + 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, &simulatedHt); - std::set indexElements = collectIndexElements(oi); - ASSERT_EQ(indexElements, simulatedHt); - ASSERT_TRUE(indexElements.empty()); + assertHtMatchesIndex(simulatedHt); sdsfree(min); sdsfree(max); } TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_EmptyRange) { - std::set simulatedHt; - insertLex(simulatedHt, {"apple", "banana", "cherry", "date", "elderberry"}); + SimHt simulatedHt; + simHtInit(&simulatedHt, 20); + { + 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, &simulatedHt); - std::set indexElements = collectIndexElements(oi); - ASSERT_EQ(indexElements, simulatedHt); - ASSERT_EQ(indexElements.size(), 5UL); + assertHtMatchesIndex(simulatedHt); sdsfree(min); sdsfree(max); From a6ec45fd4de4fc85e8e782ba70ba047e81794cc9 Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Thu, 11 Jun 2026 20:24:45 +0000 Subject: [PATCH 43/45] Add unit test for orderedIndexGetFirst/GetLast Signed-off-by: Rain Valentine --- src/module.c | 20 +++++------- src/skiplist.c | 4 --- src/skiplist.h | 1 - src/skiplist_ordered_index.c | 2 +- src/t_zset.c | 2 -- src/unit/ordered_index_test.h | 8 +++++ src/unit/test_ordered_index.cpp | 55 +++++++++++++++++++++++++++++++-- 7 files changed, 69 insertions(+), 23 deletions(-) diff --git a/src/module.c b/src/module.c index 49d196c4335..ece535dce28 100644 --- a/src/module.c +++ b/src/module.c @@ -12015,30 +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) { - const char *ele; - size_t ele_len; - orderedIndexGetElementRaw((const OrderedIndexItem *)entry, &ele, &ele_len); - robj *field = createStringObject(ele, ele_len); + orderedIndexGetElementRaw((const OrderedIndexItem *)entry, &key_ptr, &key_len); value = createStringObjectFromLongDouble(orderedIndexGetScore((const OrderedIndexItem *)entry), 0); - data->fn(data->key, field, value, data->user_data); - decrRefCount(field); - if (value) decrRefCount(value); - return; } 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/skiplist.c b/src/skiplist.c index ca088009241..c9c2cf28a07 100644 --- a/src/skiplist.c +++ b/src/skiplist.c @@ -651,10 +651,6 @@ zskiplistNode *zslGetFirst(const zskiplist *zsl) { return ((zskiplist *)zsl)->header.level[0].forward; } -zskiplistNode *zslGetLast(const zskiplist *zsl) { - return zslGetTail(zsl); -} - double zslGetScore(const zskiplistNode *node) { return node->score; } diff --git a/src/skiplist.h b/src/skiplist.h index e13e792c6b2..a3a77b081fd 100644 --- a/src/skiplist.h +++ b/src/skiplist.h @@ -174,7 +174,6 @@ void zslSeekToLexRange(zslIter *iter, const_sds min, const_sds max, int min_ex, /* Additional accessors */ zskiplistNode *zslGetFirst(const zskiplist *zsl); -zskiplistNode *zslGetLast(const zskiplist *zsl); double zslGetScore(const zskiplistNode *node); zskiplistNode *zslDetachNode(zskiplist *zsl, zskiplistNode *node); diff --git a/src/skiplist_ordered_index.c b/src/skiplist_ordered_index.c index 62090e54bb9..8459c2ebb8c 100644 --- a/src/skiplist_ordered_index.c +++ b/src/skiplist_ordered_index.c @@ -196,7 +196,7 @@ OrderedIndexItem *skiplistGetFirst(OrderedIndex *oi) { } OrderedIndexItem *skiplistGetLast(OrderedIndex *oi) { - return (OrderedIndexItem *)zslGetLast((const zskiplist *)oi); + return (OrderedIndexItem *)zslGetTail((const zskiplist *)oi); } unsigned long skiplistGetIndex(OrderedIndex *oi, const OrderedIndexItem *node) { diff --git a/src/t_zset.c b/src/t_zset.c index 822b772761b..1c79a171c93 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -1527,8 +1527,6 @@ static void zuiInitIterator(zsetopsrc *op) { } else if (op->encoding == OBJ_ENCODING_SKIPLIST) { it->sl.zs = objectGetVal(op->subject); orderedIndexInitIterator(&it->sl.iter, it->sl.zs->oi); - unsigned long len = orderedIndexLength(it->sl.zs->oi); - if (len > 0) orderedIndexSeekToIndex(&it->sl.iter, len - 1); it->sl.node = NULL; } else { serverPanic("Unknown sorted set encoding"); diff --git a/src/unit/ordered_index_test.h b/src/unit/ordered_index_test.h index e2d88c12250..709e7cf8c88 100644 --- a/src/unit/ordered_index_test.h +++ b/src/unit/ordered_index_test.h @@ -40,6 +40,8 @@ class OrderedIndexTestApi { /* 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; @@ -114,6 +116,12 @@ class SkiplistOrderedIndex : public OrderedIndexTestApi { 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); } diff --git a/src/unit/test_ordered_index.cpp b/src/unit/test_ordered_index.cpp index 52c3fb6d977..2b34fca3662 100644 --- a/src/unit/test_ordered_index.cpp +++ b/src/unit/test_ordered_index.cpp @@ -400,6 +400,28 @@ TEST_P(OrderedIndexTest, PopLast) { 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"); @@ -1363,6 +1385,7 @@ TEST_P(OrderedIndexTest, RandomizedDelete) { } ASSERT_EQ(count, n - 1); api.resetIterator(&iter); + freeRandomEntries(entries, n); api.free(oi); oi = api.create(); } @@ -1394,6 +1417,7 @@ TEST_P(OrderedIndexTest, RandomizedUpdateScore) { } api.resetIterator(&iter); api.free(oi); + freeRandomEntries(entries, n); oi = api.create(); } } @@ -1478,6 +1502,7 @@ TEST_P(OrderedIndexTest, RandomizedDeleteRangeByScore) { } api.resetIterator(&iter); api.free(oi); + freeRandomEntries(entries, n); oi = api.create(); } } @@ -1703,11 +1728,12 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_EmptyAndNoMatch) { oi = api.create(); insertN(5); - rec = {0, {}}; + 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_F(OnDeleteCallbackTest, DeleteRangeByScore_Subset) { @@ -1730,6 +1756,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_Subset) { ASSERT_SDS_ARRAY_EQ(_r, _rn, "key0", "key1", "key2", "key7", "key8", "key9"); freeSdsArray(_r, _rn); } + freeOnDeleteRecord(&rec); } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_All) { @@ -1742,6 +1769,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_All) { ASSERT_EQ(rec.count, 5); ASSERT_EQ(api.length(oi), 0UL); verifyOI(); + freeOnDeleteRecord(&rec); } TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_NullCallback) { @@ -1763,6 +1791,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_ExclusiveBounds) { 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_F(OnDeleteCallbackTest, DeleteRangeByScore_SingleElement) { @@ -1775,6 +1804,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_SingleElement) { ASSERT_EQ(rec.count, 1); ASSERT_STREQ(rec.elements[0], "key2"); ASSERT_EQ(api.length(oi), 4UL); + freeOnDeleteRecord(&rec); } /* DeleteRangeByIndex */ @@ -1790,11 +1820,12 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_EmptyAndNoMatch) { oi = api.create(); insertN(3); - rec = {0, {}}; + 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_F(OnDeleteCallbackTest, DeleteRangeByIndex_Subset) { @@ -1816,6 +1847,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_Subset) { ASSERT_SDS_ARRAY_EQ(_r, _rn, "key0", "key1", "key5", "key6", "key7", "key8", "key9"); freeSdsArray(_r, _rn); } + freeOnDeleteRecord(&rec); } TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_All) { @@ -1827,6 +1859,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_All) { ASSERT_EQ(deleted, 5UL); ASSERT_EQ(rec.count, 5); ASSERT_EQ(api.length(oi), 0UL); + freeOnDeleteRecord(&rec); } TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_NullCallback) { @@ -1853,6 +1886,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_ExclusiveBounds) { ASSERT_SDS_ARRAY_EQ(_r, _rn, "key0", "key1", "key3", "key4"); freeSdsArray(_r, _rn); } + freeOnDeleteRecord(&rec); } TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_SingleElement) { @@ -1865,6 +1899,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_SingleElement) { ASSERT_EQ(rec.count, 1); ASSERT_STREQ(rec.elements[0], "key0"); ASSERT_EQ(api.length(oi), 4UL); + freeOnDeleteRecord(&rec); } /* DeleteRangeByLex */ @@ -1887,7 +1922,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { const char *_l[] = {"apple", "banana", "cherry"}; insertLex(_l, 3); } - rec = {0, {}}; + rec.count = 0; min = sdsnew("x"); max = sdsnew("z"); deleted = api.deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); @@ -1896,6 +1931,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { ASSERT_EQ(api.length(oi), 3UL); sdsfree(min); sdsfree(max); + freeOnDeleteRecord(&rec); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_Subset) { @@ -1925,6 +1961,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_Subset) { sdsfree(min); sdsfree(max); + freeOnDeleteRecord(&rec); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_All) { @@ -1944,6 +1981,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_All) { sdsfree(min); sdsfree(max); + freeOnDeleteRecord(&rec); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_NullCallback) { @@ -1994,6 +2032,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_ExclusiveBounds) { sdsfree(min); sdsfree(max); + freeOnDeleteRecord(&rec); } TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_SingleElement) { @@ -2021,6 +2060,7 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_SingleElement) { sdsfree(min); sdsfree(max); + freeOnDeleteRecord(&rec); } /* ========== Range-Delete Hashtable Consistency Tests ========== */ @@ -2125,6 +2165,7 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_PartialDelete) { api.deleteRangeByScore(oi, 3.0, 6.0, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); assertHtMatchesIndex(simulatedHt); + simHtFree(&simulatedHt); } TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_FullDelete) { @@ -2135,6 +2176,7 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_FullDelete) { api.deleteRangeByScore(oi, NEG_INF, POS_INF, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); assertHtMatchesIndex(simulatedHt); + simHtFree(&simulatedHt); } TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_EmptyRange) { @@ -2145,6 +2187,7 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_EmptyRange) { api.deleteRangeByScore(oi, 20.0, 30.0, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); assertHtMatchesIndex(simulatedHt); + simHtFree(&simulatedHt); } /* ByIndex */ @@ -2157,6 +2200,7 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_PartialDelete) { api.deleteRangeByIndex(oi, 2, 4, hashtableConsistencyOnDelete, &simulatedHt); assertHtMatchesIndex(simulatedHt); + simHtFree(&simulatedHt); } TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_FullDelete) { @@ -2167,6 +2211,7 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_FullDelete) { api.deleteRangeByIndex(oi, 0, 9, hashtableConsistencyOnDelete, &simulatedHt); assertHtMatchesIndex(simulatedHt); + simHtFree(&simulatedHt); } TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_EmptyRange) { @@ -2177,6 +2222,7 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_EmptyRange) { api.deleteRangeByIndex(oi, 20, 30, hashtableConsistencyOnDelete, &simulatedHt); assertHtMatchesIndex(simulatedHt); + simHtFree(&simulatedHt); } /* ByLex */ @@ -2197,6 +2243,7 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_PartialDelete) { sdsfree(min); sdsfree(max); + simHtFree(&simulatedHt); } TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_FullDelete) { @@ -2215,6 +2262,7 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_FullDelete) { sdsfree(min); sdsfree(max); + simHtFree(&simulatedHt); } TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_EmptyRange) { @@ -2233,4 +2281,5 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_EmptyRange) { sdsfree(min); sdsfree(max); + simHtFree(&simulatedHt); } From ec350f3e53a39042606a769f415860b38112bfeb Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Tue, 16 Jun 2026 20:00:59 +0000 Subject: [PATCH 44/45] Use explicit (ptr,len) hash/compare for zset hashtable Replace sdsHashConfigurableSeed/dictSdsKeyCompare with zsetHashFunction/zsetKeyCompare that go through zsetExtractElement to get (ptr, len) before hashing/comparing. Currently zsetExtractElement is trivial (just sdslen + return ptr) since the skiplist backend stores elements as plain sds. The fbtree backend (next PR) will extend it to skip an 8-byte score prefix on packed items using the sds aux bit. Signed-off-by: Rain Valentine --- src/server.c | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/server.c b/src/server.c index f8e97d164c9..e5c11a08412 100644 --- a/src/server.c +++ b/src/server.c @@ -626,6 +626,15 @@ 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 char *ptr; size_t len; @@ -633,11 +642,25 @@ const void *zsetHashtableGetKey(const void *element) { return ptr; } +static uint64_t zsetHashFunction(const void *key) { + size_t len; + const char *ptr = zsetExtractElement(key, &len); + return genHashFunctionConfigurableSeed(ptr, len); +} + +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) { From de108c0f15a3c7dd885cbaaacbbd0daffb8d36ae Mon Sep 17 00:00:00 2001 From: Rain Valentine Date: Wed, 17 Jun 2026 21:32:33 +0000 Subject: [PATCH 45/45] Parameterize OnDelete and RangeDelete tests over OrderedIndexTestApi Convert OnDeleteCallbackTest and RangeDeleteHashtableConsistencyTest from TEST_F (hardcoded SkiplistOrderedIndex) to TEST_P parameterized over the OrderedIndexTestApi interface. Callbacks now extract elements via api->getElementRaw() instead of calling skiplistGetElementRaw() directly. This makes the tests backend-agnostic so future backends are automatically covered when added to the Values(...) list. Signed-off-by: Rain Valentine --- src/unit/test_ordered_index.cpp | 248 +++++++++++++++++--------------- 1 file changed, 136 insertions(+), 112 deletions(-) diff --git a/src/unit/test_ordered_index.cpp b/src/unit/test_ordered_index.cpp index 2b34fca3662..8cf8a5dfe96 100644 --- a/src/unit/test_ordered_index.cpp +++ b/src/unit/test_ordered_index.cpp @@ -1648,12 +1648,14 @@ INSTANTIATE_TEST_SUITE_P(AllImplementations, /* ========== 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, int capacity) { +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); @@ -1668,31 +1670,31 @@ static void testOnDeleteCallback(OrderedIndexItem *item, void *ctx) { OnDeleteRecord *rec = (OnDeleteRecord *)ctx; const char *ptr; size_t len; - skiplistGetElementRaw(item, &ptr, &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::Test { +class OnDeleteCallbackTest : public ::testing::TestWithParam { protected: - SkiplistOrderedIndex api; + OrderedIndexTestApi *api = GetParam(); OrderedIndex *oi = nullptr; void SetUp() override { - oi = api.create(); + oi = api->create(); } void TearDown() override { - if (oi) api.free(oi); + if (oi) api->free(oi); } void verifyOI() { char errmsg[256]; - ASSERT_TRUE(api.verifyIntegrity(oi, errmsg, sizeof(errmsg))) << errmsg; + ASSERT_TRUE(api->verifyIntegrity(oi, errmsg, sizeof(errmsg))) << errmsg; } void insert(double score, const char *ele) { - api.insert(oi, score, ele, strlen(ele)); + api->insert(oi, score, ele, strlen(ele)); } void insertN(int n) { @@ -1711,40 +1713,40 @@ class OnDeleteCallbackTest : public ::testing::Test { /* Collect elements into caller-owned sds array. Caller must freeSdsArray(). */ sds *collectElements(OrderedIndex *idx, size_t *out_n) { - return collectIndexToSds(api, idx, out_n); + return collectIndexToSds(*api, idx, out_n); } }; /* DeleteRangeByScore */ -TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_EmptyAndNoMatch) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByScore_EmptyAndNoMatch) { OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); + initOnDeleteRecord(&rec, api, 10); - unsigned long deleted = api.deleteRangeByScore(oi, 0.0, 10.0, 0, 0, testOnDeleteCallback, &rec); + 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); + api->free(oi); - oi = api.create(); + oi = api->create(); insertN(5); rec.count = 0; - deleted = api.deleteRangeByScore(oi, 10.0, 20.0, 0, 0, testOnDeleteCallback, &rec); + 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); + ASSERT_EQ(api->length(oi), 5UL); freeOnDeleteRecord(&rec); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_Subset) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByScore_Subset) { insertN(10); OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); - unsigned long deleted = api.deleteRangeByScore(oi, 3.0, 6.0, 0, 0, testOnDeleteCallback, &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); + ASSERT_EQ(api->length(oi), 6UL); verifyOI(); sortSdsArray(rec.elements, rec.count); @@ -1759,84 +1761,84 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_Subset) { freeOnDeleteRecord(&rec); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_All) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByScore_All) { insertN(5); OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); - unsigned long deleted = api.deleteRangeByScore(oi, NEG_INF, POS_INF, 0, 0, testOnDeleteCallback, &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); + ASSERT_EQ(api->length(oi), 0UL); verifyOI(); freeOnDeleteRecord(&rec); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_NullCallback) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByScore_NullCallback) { insertN(5); - unsigned long deleted = api.deleteRangeByScore(oi, 1.0, 3.0, 0, 0, NULL, NULL); + unsigned long deleted = api->deleteRangeByScore(oi, 1.0, 3.0, 0, 0, NULL, NULL); ASSERT_EQ(deleted, 3UL); - ASSERT_EQ(api.length(oi), 2UL); + ASSERT_EQ(api->length(oi), 2UL); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_ExclusiveBounds) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByScore_ExclusiveBounds) { insertN(10); OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); - unsigned long deleted = api.deleteRangeByScore(oi, 3.0, 7.0, 1, 1, testOnDeleteCallback, &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); + ASSERT_EQ(api->length(oi), 7UL); freeOnDeleteRecord(&rec); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByScore_SingleElement) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByScore_SingleElement) { insertN(5); OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); - unsigned long deleted = api.deleteRangeByScore(oi, 2.0, 2.0, 0, 0, testOnDeleteCallback, &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); + ASSERT_EQ(api->length(oi), 4UL); freeOnDeleteRecord(&rec); } /* DeleteRangeByIndex */ -TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_EmptyAndNoMatch) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByIndex_EmptyAndNoMatch) { OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); + initOnDeleteRecord(&rec, api, 10); - unsigned long deleted = api.deleteRangeByIndex(oi, 0, 4, testOnDeleteCallback, &rec); + unsigned long deleted = api->deleteRangeByIndex(oi, 0, 4, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 0UL); ASSERT_EQ(rec.count, 0); - api.free(oi); + api->free(oi); - oi = api.create(); + oi = api->create(); insertN(3); rec.count = 0; - deleted = api.deleteRangeByIndex(oi, 10, 20, testOnDeleteCallback, &rec); + deleted = api->deleteRangeByIndex(oi, 10, 20, testOnDeleteCallback, &rec); ASSERT_EQ(deleted, 0UL); ASSERT_EQ(rec.count, 0); - ASSERT_EQ(api.length(oi), 3UL); + ASSERT_EQ(api->length(oi), 3UL); freeOnDeleteRecord(&rec); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_Subset) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByIndex_Subset) { insertN(10); OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); - unsigned long deleted = api.deleteRangeByIndex(oi, 2, 4, testOnDeleteCallback, &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); + ASSERT_EQ(api->length(oi), 7UL); sortSdsArray(rec.elements, rec.count); ASSERT_SDS_ARRAY_EQ(rec.elements, rec.count, "key2", "key3", "key4"); @@ -1850,32 +1852,32 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_Subset) { freeOnDeleteRecord(&rec); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_All) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByIndex_All) { insertN(5); OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); - unsigned long deleted = api.deleteRangeByIndex(oi, 0, 4, testOnDeleteCallback, &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); + ASSERT_EQ(api->length(oi), 0UL); freeOnDeleteRecord(&rec); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_NullCallback) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByIndex_NullCallback) { insertN(5); - unsigned long deleted = api.deleteRangeByIndex(oi, 2, 4, NULL, NULL); + unsigned long deleted = api->deleteRangeByIndex(oi, 2, 4, NULL, NULL); ASSERT_EQ(deleted, 3UL); - ASSERT_EQ(api.length(oi), 2UL); + ASSERT_EQ(api->length(oi), 2UL); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_ExclusiveBounds) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByIndex_ExclusiveBounds) { insertN(5); OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); - unsigned long deleted = api.deleteRangeByIndex(oi, 2, 2, testOnDeleteCallback, &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"); @@ -1889,35 +1891,35 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_ExclusiveBounds) { freeOnDeleteRecord(&rec); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByIndex_SingleElement) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByIndex_SingleElement) { insertN(5); OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); - unsigned long deleted = api.deleteRangeByIndex(oi, 0, 0, testOnDeleteCallback, &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); + ASSERT_EQ(api->length(oi), 4UL); freeOnDeleteRecord(&rec); } /* DeleteRangeByLex */ -TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); + initOnDeleteRecord(&rec, api, 10); sds min = sdsnew("a"); sds max = sdsnew("z"); - unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); + 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); + api->free(oi); - oi = api.create(); + oi = api->create(); { const char *_l[] = {"apple", "banana", "cherry"}; insertLex(_l, 3); @@ -1925,29 +1927,29 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_EmptyAndNoMatch) { rec.count = 0; min = sdsnew("x"); max = sdsnew("z"); - deleted = api.deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); + 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); + ASSERT_EQ(api->length(oi), 3UL); sdsfree(min); sdsfree(max); freeOnDeleteRecord(&rec); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_Subset) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByLex_Subset) { { const char *_l[] = {"apple", "banana", "cherry", "date", "elderberry"}; insertLex(_l, 5); } OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); + initOnDeleteRecord(&rec, api, 10); sds min = sdsnew("banana"); sds max = sdsnew("date"); - unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); + 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); + ASSERT_EQ(api->length(oi), 2UL); sortSdsArray(rec.elements, rec.count); ASSERT_SDS_ARRAY_EQ(rec.elements, rec.count, "banana", "cherry", "date"); @@ -1964,27 +1966,27 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_Subset) { freeOnDeleteRecord(&rec); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_All) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByLex_All) { { const char *_l[] = {"apple", "banana", "cherry"}; insertLex(_l, 3); } OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); + initOnDeleteRecord(&rec, api, 10); sds min = sdsnew("a"); sds max = sdsnew("z"); - unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); + 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); + ASSERT_EQ(api->length(oi), 0UL); sdsfree(min); sdsfree(max); freeOnDeleteRecord(&rec); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_NullCallback) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByLex_NullCallback) { { const char *_l[] = {"apple", "banana", "cherry", "date"}; insertLex(_l, 4); @@ -1992,9 +1994,9 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_NullCallback) { sds min = sdsnew("banana"); sds max = sdsnew("cherry"); - unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); + unsigned long deleted = api->deleteRangeByLex(oi, min, max, 0, 0, NULL, NULL); ASSERT_EQ(deleted, 2UL); - ASSERT_EQ(api.length(oi), 2UL); + ASSERT_EQ(api->length(oi), 2UL); { size_t _rn; @@ -2007,21 +2009,21 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_NullCallback) { sdsfree(max); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_ExclusiveBounds) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByLex_ExclusiveBounds) { { const char *_l[] = {"apple", "banana", "cherry", "date", "elderberry"}; insertLex(_l, 5); } OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); + initOnDeleteRecord(&rec, api, 10); sds min = sdsnew("banana"); sds max = sdsnew("date"); - unsigned long deleted = api.deleteRangeByLex(oi, min, max, 1, 1, testOnDeleteCallback, &rec); + 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); + ASSERT_EQ(api->length(oi), 4UL); { size_t _rn; @@ -2035,21 +2037,21 @@ TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_ExclusiveBounds) { freeOnDeleteRecord(&rec); } -TEST_F(OnDeleteCallbackTest, DeleteRangeByLex_SingleElement) { +TEST_P(OnDeleteCallbackTest, DeleteRangeByLex_SingleElement) { { const char *_l[] = {"apple", "banana", "cherry"}; insertLex(_l, 3); } OnDeleteRecord rec; - initOnDeleteRecord(&rec, 10); + initOnDeleteRecord(&rec, api, 10); sds min = sdsnew("banana"); sds max = sdsnew("banana"); - unsigned long deleted = api.deleteRangeByLex(oi, min, max, 0, 0, testOnDeleteCallback, &rec); + 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); + ASSERT_EQ(api->length(oi), 2UL); { size_t _rn; @@ -2102,28 +2104,31 @@ static void simHtSort(SimHt *ht) { } static void hashtableConsistencyOnDelete(OrderedIndexItem *item, void *ctx) { - SimHt *ht = (SimHt *)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; - skiplistGetElementRaw(item, &ptr, &len); + api->getElementRaw(item, &ptr, &len); simHtRemove(ht, ptr, len); /* Item is freed by the index after this callback returns. */ } -class RangeDeleteHashtableConsistencyTest : public ::testing::Test { +class RangeDeleteHashtableConsistencyTest : public ::testing::TestWithParam { protected: - SkiplistOrderedIndex api; + OrderedIndexTestApi *api = GetParam(); OrderedIndex *oi = nullptr; void SetUp() override { - oi = api.create(); + oi = api->create(); } void TearDown() override { - if (oi) api.free(oi); + if (oi) api->free(oi); } void insert(double score, const char *ele) { - api.insert(oi, score, ele, strlen(ele)); + api->insert(oi, score, ele, strlen(ele)); } void insertN(SimHt &ht, int n) { @@ -2144,7 +2149,7 @@ class RangeDeleteHashtableConsistencyTest : public ::testing::Test { void assertHtMatchesIndex(SimHt &ht) { size_t idx_n; - sds *idx_elems = collectIndexToSds(api, oi, &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); @@ -2157,34 +2162,37 @@ class RangeDeleteHashtableConsistencyTest : public ::testing::Test { /* ByScore */ -TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_PartialDelete) { +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, &simulatedHt); + api->deleteRangeByScore(oi, 3.0, 6.0, 0, 0, hashtableConsistencyOnDelete, cbCtx); assertHtMatchesIndex(simulatedHt); simHtFree(&simulatedHt); } -TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_FullDelete) { +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, &simulatedHt); + api->deleteRangeByScore(oi, NEG_INF, POS_INF, 0, 0, hashtableConsistencyOnDelete, cbCtx); assertHtMatchesIndex(simulatedHt); simHtFree(&simulatedHt); } -TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_EmptyRange) { +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, &simulatedHt); + api->deleteRangeByScore(oi, 20.0, 30.0, 0, 0, hashtableConsistencyOnDelete, cbCtx); assertHtMatchesIndex(simulatedHt); simHtFree(&simulatedHt); @@ -2192,34 +2200,37 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByScore_EmptyRange) { /* ByIndex */ -TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_PartialDelete) { +TEST_P(RangeDeleteHashtableConsistencyTest, ByIndex_PartialDelete) { SimHt simulatedHt; simHtInit(&simulatedHt, 20); + void *cbCtx[] = {&simulatedHt, api}; insertN(simulatedHt, 10); - api.deleteRangeByIndex(oi, 2, 4, hashtableConsistencyOnDelete, &simulatedHt); + api->deleteRangeByIndex(oi, 2, 4, hashtableConsistencyOnDelete, cbCtx); assertHtMatchesIndex(simulatedHt); simHtFree(&simulatedHt); } -TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_FullDelete) { +TEST_P(RangeDeleteHashtableConsistencyTest, ByIndex_FullDelete) { SimHt simulatedHt; simHtInit(&simulatedHt, 20); + void *cbCtx[] = {&simulatedHt, api}; insertN(simulatedHt, 10); - api.deleteRangeByIndex(oi, 0, 9, hashtableConsistencyOnDelete, &simulatedHt); + api->deleteRangeByIndex(oi, 0, 9, hashtableConsistencyOnDelete, cbCtx); assertHtMatchesIndex(simulatedHt); simHtFree(&simulatedHt); } -TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_EmptyRange) { +TEST_P(RangeDeleteHashtableConsistencyTest, ByIndex_EmptyRange) { SimHt simulatedHt; simHtInit(&simulatedHt, 20); + void *cbCtx[] = {&simulatedHt, api}; insertN(simulatedHt, 10); - api.deleteRangeByIndex(oi, 20, 30, hashtableConsistencyOnDelete, &simulatedHt); + api->deleteRangeByIndex(oi, 20, 30, hashtableConsistencyOnDelete, cbCtx); assertHtMatchesIndex(simulatedHt); simHtFree(&simulatedHt); @@ -2227,9 +2238,10 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByIndex_EmptyRange) { /* ByLex */ -TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_PartialDelete) { +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); @@ -2237,7 +2249,7 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_PartialDelete) { sds min = sdsnew("banana"); sds max = sdsnew("date"); - api.deleteRangeByLex(oi, min, max, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + api->deleteRangeByLex(oi, min, max, 0, 0, hashtableConsistencyOnDelete, cbCtx); assertHtMatchesIndex(simulatedHt); @@ -2246,9 +2258,10 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_PartialDelete) { simHtFree(&simulatedHt); } -TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_FullDelete) { +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); @@ -2256,7 +2269,7 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_FullDelete) { sds min = sdsnew("a"); sds max = sdsnew("z"); - api.deleteRangeByLex(oi, min, max, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + api->deleteRangeByLex(oi, min, max, 0, 0, hashtableConsistencyOnDelete, cbCtx); assertHtMatchesIndex(simulatedHt); @@ -2265,9 +2278,10 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_FullDelete) { simHtFree(&simulatedHt); } -TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_EmptyRange) { +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); @@ -2275,7 +2289,7 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_EmptyRange) { sds min = sdsnew("zzz"); sds max = sdsnew("zzzz"); - api.deleteRangeByLex(oi, min, max, 0, 0, hashtableConsistencyOnDelete, &simulatedHt); + api->deleteRangeByLex(oi, min, max, 0, 0, hashtableConsistencyOnDelete, cbCtx); assertHtMatchesIndex(simulatedHt); @@ -2283,3 +2297,13 @@ TEST_F(RangeDeleteHashtableConsistencyTest, ByLex_EmptyRange) { sdsfree(max); simHtFree(&simulatedHt); } + +INSTANTIATE_TEST_SUITE_P(AllImplementations, + OnDeleteCallbackTest, + ::testing::Values(&skiplistImpl), + orderedIndexTestName); + +INSTANTIATE_TEST_SUITE_P(AllImplementations, + RangeDeleteHashtableConsistencyTest, + ::testing::Values(&skiplistImpl), + orderedIndexTestName);