resplite 1.2.8 → 1.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -16
- package/package.json +1 -1
- package/scripts/benchmark-redis-vs-resplite.js +31 -0
- package/src/commands/registry.js +8 -0
- package/src/commands/zrank.js +18 -0
- package/src/commands/zrevrange.js +27 -0
- package/src/commands/zrevrangebyscore.js +46 -0
- package/src/commands/zrevrank.js +18 -0
- package/src/engine/engine.js +36 -0
- package/src/storage/sqlite/zsets.js +95 -0
- package/test/integration/zsets.test.js +68 -0
package/README.md
CHANGED
|
@@ -515,21 +515,24 @@ A typical comparison is **Redis (for example, in Docker)** on one side and **RES
|
|
|
515
515
|
|
|
516
516
|
**Example results (Redis vs RESPLite, default pragma, 10k iterations):**
|
|
517
517
|
|
|
518
|
-
| Suite
|
|
519
|
-
|
|
520
|
-
| PING
|
|
521
|
-
| SET+GET
|
|
522
|
-
| MSET+MGET(10)
|
|
523
|
-
| INCR
|
|
524
|
-
| HSET+HGET
|
|
525
|
-
| HGETALL(50)
|
|
526
|
-
| HLEN(50)
|
|
527
|
-
| SADD+SMEMBERS
|
|
528
|
-
| LPUSH+LRANGE
|
|
529
|
-
| LREM
|
|
530
|
-
| ZADD+ZRANGE
|
|
531
|
-
|
|
|
532
|
-
|
|
|
518
|
+
| Suite | Redis (Docker) | RESPLite (default) |
|
|
519
|
+
|-------------------|----------------|--------------------|
|
|
520
|
+
| PING | 9.72K/s | 37.66K/s |
|
|
521
|
+
| SET+GET | 4.60K/s | 11.96K/s |
|
|
522
|
+
| MSET+MGET(10) | 4.38K/s | 5.71K/s |
|
|
523
|
+
| INCR | 9.76K/s | 19.15K/s |
|
|
524
|
+
| HSET+HGET | 4.42K/s | 11.71K/s |
|
|
525
|
+
| HGETALL(50) | 8.42K/s | 11.12K/s |
|
|
526
|
+
| HLEN(50) | 8.88K/s | 30.72K/s |
|
|
527
|
+
| SADD+SMEMBERS | 8.33K/s | 18.19K/s |
|
|
528
|
+
| LPUSH+LRANGE | 8.29K/s | 14.78K/s |
|
|
529
|
+
| LREM | 4.85K/s | 6.35K/s |
|
|
530
|
+
| ZADD+ZRANGE | 9.37K/s | 16.43K/s |
|
|
531
|
+
| ZADD+ZREVRANGE | 8.22K/s | 16.82K/s |
|
|
532
|
+
| ZRANK+ZREVRANK | 4.56K/s | 13.03K/s |
|
|
533
|
+
| ZREVRANGEBYSCORE | 8.88K/s | 16.88K/s |
|
|
534
|
+
| SET+DEL | 4.75K/s | 9.99K/s |
|
|
535
|
+
| FT.SEARCH | 8.39K/s | 8.81K/s |
|
|
533
536
|
|
|
534
537
|
To reproduce the benchmark, run `npm run benchmark -- --template default`. Numbers depend on host and whether Redis is native or in Docker.
|
|
535
538
|
|
|
@@ -545,7 +548,7 @@ To reproduce the benchmark, run `npm run benchmark -- --template default`. Numbe
|
|
|
545
548
|
| **Hashes** | HSET, HGET, HMGET, HGETALL, HDEL, HEXISTS, HINCRBY |
|
|
546
549
|
| **Sets** | SADD, SREM, SMEMBERS, SISMEMBER, SCARD |
|
|
547
550
|
| **Lists** | LPUSH, RPUSH, LLEN, LRANGE, LINDEX, LPOP, RPOP, BLPOP, BRPOP |
|
|
548
|
-
| **Sorted sets** | ZADD, ZREM, ZCARD, ZSCORE, ZRANGE, ZRANGEBYSCORE |
|
|
551
|
+
| **Sorted sets** | ZADD, ZREM, ZCARD, ZSCORE, ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZRANK, ZREVRANK |
|
|
549
552
|
| **Search (FT.\*)** | FT.CREATE, FT.INFO, FT.ADD, FT.DEL, FT.SEARCH, FT.SUGADD, FT.SUGGET, FT.SUGDEL |
|
|
550
553
|
| **Introspection** | TYPE, OBJECT IDLETIME, SCAN, KEYS, MONITOR |
|
|
551
554
|
| **Admin** | SQLITE.INFO, CACHE.INFO, MEMORY.INFO |
|
package/package.json
CHANGED
|
@@ -258,6 +258,34 @@ async function benchZaddZrange(client, n) {
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
async function benchZaddZrevrange(client, n) {
|
|
262
|
+
const key = 'bm:zset:rev';
|
|
263
|
+
for (let i = 0; i < n; i++) {
|
|
264
|
+
await client.zAdd(key, { score: i, value: `m${i}` });
|
|
265
|
+
if (i % 10 === 0) await client.zRange(key, 0, 49, { REV: true });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function benchZrankZrevrank(client, n) {
|
|
270
|
+
const key = 'bm:zset:rank';
|
|
271
|
+
await client.del(key);
|
|
272
|
+
await client.zAdd(key, Array.from({ length: 100 }, (_, i) => ({ score: i, value: `m${i}` })));
|
|
273
|
+
for (let i = 0; i < n; i++) {
|
|
274
|
+
const member = `m${i % 100}`;
|
|
275
|
+
await client.zRank(key, member);
|
|
276
|
+
await client.zRevRank(key, member);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function benchZrevrangebyscore(client, n) {
|
|
281
|
+
const key = 'bm:zset:byscore';
|
|
282
|
+
await client.del(key);
|
|
283
|
+
await client.zAdd(key, Array.from({ length: 100 }, (_, i) => ({ score: i, value: `m${i}` })));
|
|
284
|
+
for (let i = 0; i < n; i++) {
|
|
285
|
+
await client.sendCommand(['ZREVRANGEBYSCORE', key, '99', '0', 'LIMIT', '0', '20']);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
261
289
|
async function benchDel(client, n) {
|
|
262
290
|
for (let i = 0; i < n; i++) {
|
|
263
291
|
await client.set(`bm:del:${i}`, 'x');
|
|
@@ -311,6 +339,9 @@ const SUITES = [
|
|
|
311
339
|
{ name: 'LPUSH+LRANGE', fn: benchLpushLrange, iterScale: 1 },
|
|
312
340
|
{ name: 'LREM', fn: benchLrem, iterScale: 1 },
|
|
313
341
|
{ name: 'ZADD+ZRANGE', fn: benchZaddZrange, iterScale: 1 },
|
|
342
|
+
{ name: 'ZADD+ZREVRANGE', fn: benchZaddZrevrange, iterScale: 1 },
|
|
343
|
+
{ name: 'ZRANK+ZREVRANK', fn: benchZrankZrevrank, iterScale: 1 },
|
|
344
|
+
{ name: 'ZREVRANGEBYSCORE', fn: benchZrevrangebyscore, iterScale: 1 },
|
|
314
345
|
{ name: 'SET+DEL', fn: benchDel, iterScale: 1 },
|
|
315
346
|
{ name: 'FT.SEARCH', fn: benchFtSearch, iterScale: 1 },
|
|
316
347
|
];
|
package/src/commands/registry.js
CHANGED
|
@@ -54,6 +54,10 @@ import * as zcard from './zcard.js';
|
|
|
54
54
|
import * as zscore from './zscore.js';
|
|
55
55
|
import * as zrange from './zrange.js';
|
|
56
56
|
import * as zrangebyscore from './zrangebyscore.js';
|
|
57
|
+
import * as zrevrange from './zrevrange.js';
|
|
58
|
+
import * as zrevrangebyscore from './zrevrangebyscore.js';
|
|
59
|
+
import * as zrevrank from './zrevrank.js';
|
|
60
|
+
import * as zrank from './zrank.js';
|
|
57
61
|
import * as sqliteInfo from './sqlite-info.js';
|
|
58
62
|
import * as cacheInfo from './cache-info.js';
|
|
59
63
|
import * as memoryInfo from './memory-info.js';
|
|
@@ -119,6 +123,10 @@ const HANDLERS = new Map([
|
|
|
119
123
|
['ZSCORE', (e, a) => zscore.handleZscore(e, a)],
|
|
120
124
|
['ZRANGE', (e, a) => zrange.handleZrange(e, a)],
|
|
121
125
|
['ZRANGEBYSCORE', (e, a) => zrangebyscore.handleZrangebyscore(e, a)],
|
|
126
|
+
['ZREVRANGE', (e, a) => zrevrange.handleZrevrange(e, a)],
|
|
127
|
+
['ZREVRANGEBYSCORE', (e, a) => zrevrangebyscore.handleZrevrangebyscore(e, a)],
|
|
128
|
+
['ZREVRANK', (e, a) => zrevrank.handleZrevrank(e, a)],
|
|
129
|
+
['ZRANK', (e, a) => zrank.handleZrank(e, a)],
|
|
122
130
|
['SQLITE.INFO', (e, a) => sqliteInfo.handleSqliteInfo(e, a)],
|
|
123
131
|
['CACHE.INFO', (e, a) => cacheInfo.handleCacheInfo(e, a)],
|
|
124
132
|
['MEMORY.INFO', (e, a) => memoryInfo.handleMemoryInfo(e, a)],
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZRANK key member
|
|
3
|
+
* Returns the 0-based rank of member in the sorted set (low to high score).
|
|
4
|
+
* Returns null if key does not exist or member is not in the set.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function handleZrank(engine, args) {
|
|
8
|
+
if (!args || args.length < 2) {
|
|
9
|
+
return { error: 'ERR wrong number of arguments for \'ZRANK\' command' };
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const rank = engine.zrank(args[0], args[1]);
|
|
13
|
+
return rank;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
const msg = e && e.message ? e.message : String(e);
|
|
16
|
+
return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZREVRANGE key start stop [WITHSCORES]
|
|
3
|
+
* Same as ZRANGE but ordered from highest to lowest score (rank 0 = highest).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function handleZrevrange(engine, args) {
|
|
7
|
+
if (!args || args.length < 3) {
|
|
8
|
+
return { error: 'ERR wrong number of arguments for \'ZREVRANGE\' command' };
|
|
9
|
+
}
|
|
10
|
+
let withScores = false;
|
|
11
|
+
for (let i = 3; i < args.length; i++) {
|
|
12
|
+
const a = (Buffer.isBuffer(args[i]) ? args[i].toString('utf8') : String(args[i])).toUpperCase();
|
|
13
|
+
if (a === 'WITHSCORES') withScores = true;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const start = parseInt(String(args[1]), 10);
|
|
17
|
+
const stop = parseInt(String(args[2]), 10);
|
|
18
|
+
if (Number.isNaN(start) || Number.isNaN(stop)) {
|
|
19
|
+
return { error: 'ERR value is not an integer or out of range' };
|
|
20
|
+
}
|
|
21
|
+
const arr = engine.zrevrange(args[0], start, stop, { withScores });
|
|
22
|
+
return arr;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
const msg = e && e.message ? e.message : String(e);
|
|
25
|
+
return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
|
|
3
|
+
* Same as ZRANGEBYSCORE but ordered from highest to lowest score.
|
|
4
|
+
* Note: Redis uses max min (first score is upper bound, second is lower bound).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function handleZrevrangebyscore(engine, args) {
|
|
8
|
+
if (!args || args.length < 3) {
|
|
9
|
+
return { error: 'ERR wrong number of arguments for \'ZREVRANGEBYSCORE\' command' };
|
|
10
|
+
}
|
|
11
|
+
let withScores = false;
|
|
12
|
+
let limitOffset = null;
|
|
13
|
+
let limitCount = null;
|
|
14
|
+
const raw = args.slice(3);
|
|
15
|
+
for (let i = 0; i < raw.length; i++) {
|
|
16
|
+
const a = (Buffer.isBuffer(raw[i]) ? raw[i].toString('utf8') : String(raw[i])).toUpperCase();
|
|
17
|
+
if (a === 'WITHSCORES') {
|
|
18
|
+
withScores = true;
|
|
19
|
+
} else if (a === 'LIMIT' && i + 2 < raw.length) {
|
|
20
|
+
const off = parseInt(String(raw[i + 1]), 10);
|
|
21
|
+
const cnt = parseInt(String(raw[i + 2]), 10);
|
|
22
|
+
if (!Number.isNaN(off) && !Number.isNaN(cnt)) {
|
|
23
|
+
limitOffset = off;
|
|
24
|
+
limitCount = cnt;
|
|
25
|
+
}
|
|
26
|
+
i += 2;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const max = parseFloat(String(args[1]));
|
|
31
|
+
const min = parseFloat(String(args[2]));
|
|
32
|
+
if (Number.isNaN(max) || Number.isNaN(min)) {
|
|
33
|
+
return { error: 'ERR value is not a valid float' };
|
|
34
|
+
}
|
|
35
|
+
const options = { withScores };
|
|
36
|
+
if (limitOffset != null && limitCount != null) {
|
|
37
|
+
options.offset = limitOffset;
|
|
38
|
+
options.limit = limitCount;
|
|
39
|
+
}
|
|
40
|
+
const arr = engine.zrevrangebyscore(args[0], max, min, options);
|
|
41
|
+
return arr;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
const msg = e && e.message ? e.message : String(e);
|
|
44
|
+
return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZREVRANK key member
|
|
3
|
+
* Returns the 0-based rank of member in the sorted set (high to low score).
|
|
4
|
+
* Returns null if key does not exist or member is not in the set.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function handleZrevrank(engine, args) {
|
|
8
|
+
if (!args || args.length < 2) {
|
|
9
|
+
return { error: 'ERR wrong number of arguments for \'ZREVRANK\' command' };
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const rank = engine.zrevrank(args[0], args[1]);
|
|
13
|
+
return rank;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
const msg = e && e.message ? e.message : String(e);
|
|
16
|
+
return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/engine/engine.js
CHANGED
|
@@ -387,6 +387,30 @@ export function createEngine(opts = {}) {
|
|
|
387
387
|
return zsets.rangeByRank(k, start, stop, { withScores: options.withScores });
|
|
388
388
|
},
|
|
389
389
|
|
|
390
|
+
zrevrange(key, start, stop, options = {}) {
|
|
391
|
+
const k = asKey(key);
|
|
392
|
+
const meta = getKeyMeta(key);
|
|
393
|
+
if (!meta) return [];
|
|
394
|
+
expectZset(meta);
|
|
395
|
+
return zsets.rangeByRankReverse(k, start, stop, { withScores: options.withScores });
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
zrevrank(key, member) {
|
|
399
|
+
const k = asKey(key);
|
|
400
|
+
const meta = getKeyMeta(key);
|
|
401
|
+
if (!meta) return null;
|
|
402
|
+
expectZset(meta);
|
|
403
|
+
return zsets.rankReverse(k, asKey(member));
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
zrank(key, member) {
|
|
407
|
+
const k = asKey(key);
|
|
408
|
+
const meta = getKeyMeta(key);
|
|
409
|
+
if (!meta) return null;
|
|
410
|
+
expectZset(meta);
|
|
411
|
+
return zsets.rank(k, asKey(member));
|
|
412
|
+
},
|
|
413
|
+
|
|
390
414
|
zrangebyscore(key, min, max, options = {}) {
|
|
391
415
|
const k = asKey(key);
|
|
392
416
|
const meta = getKeyMeta(key);
|
|
@@ -399,6 +423,18 @@ export function createEngine(opts = {}) {
|
|
|
399
423
|
});
|
|
400
424
|
},
|
|
401
425
|
|
|
426
|
+
zrevrangebyscore(key, max, min, options = {}) {
|
|
427
|
+
const k = asKey(key);
|
|
428
|
+
const meta = getKeyMeta(key);
|
|
429
|
+
if (!meta) return [];
|
|
430
|
+
expectZset(meta);
|
|
431
|
+
return zsets.rangeByScoreReverse(k, max, min, {
|
|
432
|
+
withScores: options.withScores,
|
|
433
|
+
offset: options.offset ?? 0,
|
|
434
|
+
limit: options.limit,
|
|
435
|
+
});
|
|
436
|
+
},
|
|
437
|
+
|
|
402
438
|
type(key) {
|
|
403
439
|
const meta = getKeyMeta(key);
|
|
404
440
|
return typeName(meta);
|
|
@@ -36,12 +36,32 @@ export function createZsetsStorage(db, keys) {
|
|
|
36
36
|
ORDER BY score ASC, member ASC
|
|
37
37
|
LIMIT ? OFFSET ?`
|
|
38
38
|
);
|
|
39
|
+
const rangeByRankReverseStmt = db.prepare(
|
|
40
|
+
`SELECT member, score FROM redis_zsets
|
|
41
|
+
WHERE key = ?
|
|
42
|
+
ORDER BY score DESC, member DESC
|
|
43
|
+
LIMIT ? OFFSET ?`
|
|
44
|
+
);
|
|
39
45
|
const rangeByScoreStmt = db.prepare(
|
|
40
46
|
`SELECT member, score FROM redis_zsets
|
|
41
47
|
WHERE key = ? AND score >= ? AND score <= ?
|
|
42
48
|
ORDER BY score ASC, member ASC
|
|
43
49
|
LIMIT ? OFFSET ?`
|
|
44
50
|
);
|
|
51
|
+
const rankReverseStmt = db.prepare(
|
|
52
|
+
`SELECT COUNT(*) AS n FROM redis_zsets
|
|
53
|
+
WHERE key = ? AND (score > ? OR (score = ? AND member > ?))`
|
|
54
|
+
);
|
|
55
|
+
const rankStmt = db.prepare(
|
|
56
|
+
`SELECT COUNT(*) AS n FROM redis_zsets
|
|
57
|
+
WHERE key = ? AND (score < ? OR (score = ? AND member < ?))`
|
|
58
|
+
);
|
|
59
|
+
const rangeByScoreReverseStmt = db.prepare(
|
|
60
|
+
`SELECT member, score FROM redis_zsets
|
|
61
|
+
WHERE key = ? AND score <= ? AND score >= ?
|
|
62
|
+
ORDER BY score DESC, member DESC
|
|
63
|
+
LIMIT ? OFFSET ?`
|
|
64
|
+
);
|
|
45
65
|
|
|
46
66
|
return {
|
|
47
67
|
/**
|
|
@@ -143,6 +163,34 @@ export function createZsetsStorage(db, keys) {
|
|
|
143
163
|
return out;
|
|
144
164
|
},
|
|
145
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Range by rank in reverse order (ZREVRANGE). Rank 0 = highest score.
|
|
168
|
+
* Same start/stop semantics as rangeByRank; order is score DESC, member DESC.
|
|
169
|
+
* @param {Buffer} key
|
|
170
|
+
* @param {number} start 0-based inclusive (0 = highest score)
|
|
171
|
+
* @param {number} stop 0-based inclusive
|
|
172
|
+
* @param {{ withScores?: boolean }} options
|
|
173
|
+
* @returns {Buffer[] | Array<Buffer|string>} members or [member, score, ...]
|
|
174
|
+
*/
|
|
175
|
+
rangeByRankReverse(key, start, stop, options = {}) {
|
|
176
|
+
const len = this.count(key);
|
|
177
|
+
if (len === 0) return [];
|
|
178
|
+
let s = start >= 0 ? start : Math.max(0, len + start);
|
|
179
|
+
let e = stop >= 0 ? stop : Math.max(0, len + stop);
|
|
180
|
+
if (s > e) return [];
|
|
181
|
+
s = Math.min(s, len - 1);
|
|
182
|
+
e = Math.min(e, len - 1);
|
|
183
|
+
const limit = e - s + 1;
|
|
184
|
+
const offset = s;
|
|
185
|
+
const rows = rangeByRankReverseStmt.all(key, limit, offset);
|
|
186
|
+
if (!options.withScores) return rows.map((r) => r.member);
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const r of rows) {
|
|
189
|
+
out.push(r.member, formatScore(r.score));
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
},
|
|
193
|
+
|
|
146
194
|
/**
|
|
147
195
|
* Range by score (min/max inclusive). Order: score ASC, member ASC.
|
|
148
196
|
* @param {Buffer} key
|
|
@@ -161,5 +209,52 @@ export function createZsetsStorage(db, keys) {
|
|
|
161
209
|
}
|
|
162
210
|
return out;
|
|
163
211
|
},
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Rank of member in reverse order (ZREVRANK). Rank 0 = highest score.
|
|
215
|
+
* Returns null if key does not exist or member not in set.
|
|
216
|
+
* @param {Buffer} key
|
|
217
|
+
* @param {Buffer} member
|
|
218
|
+
* @returns {number | null} 0-based rank or null
|
|
219
|
+
*/
|
|
220
|
+
rankReverse(key, member) {
|
|
221
|
+
const scoreRow = scoreStmt.get(key, member);
|
|
222
|
+
if (scoreRow == null) return null;
|
|
223
|
+
const row = rankReverseStmt.get(key, scoreRow.score, scoreRow.score, member);
|
|
224
|
+
return row ? row.n : 0;
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Rank of member in ascending order (ZRANK). Rank 0 = lowest score.
|
|
229
|
+
* Returns null if key does not exist or member not in set.
|
|
230
|
+
* @param {Buffer} key
|
|
231
|
+
* @param {Buffer} member
|
|
232
|
+
* @returns {number | null} 0-based rank or null
|
|
233
|
+
*/
|
|
234
|
+
rank(key, member) {
|
|
235
|
+
const scoreRow = scoreStmt.get(key, member);
|
|
236
|
+
if (scoreRow == null) return null;
|
|
237
|
+
const row = rankStmt.get(key, scoreRow.score, scoreRow.score, member);
|
|
238
|
+
return row ? row.n : 0;
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Range by score in reverse order (ZREVRANGEBYSCORE). max/min inclusive, order score DESC, member DESC.
|
|
243
|
+
* @param {Buffer} key
|
|
244
|
+
* @param {number} max
|
|
245
|
+
* @param {number} min
|
|
246
|
+
* @param {{ withScores?: boolean, limit?: number, offset?: number }} options
|
|
247
|
+
*/
|
|
248
|
+
rangeByScoreReverse(key, max, min, options = {}) {
|
|
249
|
+
const limit = options.limit ?? -1;
|
|
250
|
+
const offset = options.offset ?? 0;
|
|
251
|
+
const rows = rangeByScoreReverseStmt.all(key, max, min, limit < 0 ? 1e9 : limit, offset);
|
|
252
|
+
if (!options.withScores) return rows.map((r) => r.member);
|
|
253
|
+
const out = [];
|
|
254
|
+
for (const r of rows) {
|
|
255
|
+
out.push(r.member, formatScore(r.score));
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
},
|
|
164
259
|
};
|
|
165
260
|
}
|
|
@@ -51,6 +51,74 @@ describe('ZSET integration', () => {
|
|
|
51
51
|
assert.equal(arr[3].toString('utf8'), '2');
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
it('ZREVRANGE returns order highest to lowest and supports WITHSCORES', async () => {
|
|
55
|
+
await sendCommand(port, argv('ZADD', 'zrev', '1', 'a', '2', 'b', '3', 'c'));
|
|
56
|
+
const rev = await sendCommand(port, argv('ZREVRANGE', 'zrev', '0', '-1'));
|
|
57
|
+
const arr = tryParseValue(rev, 0).value;
|
|
58
|
+
assert.equal(arr.length, 3);
|
|
59
|
+
assert.equal(arr[0].toString('utf8'), 'c');
|
|
60
|
+
assert.equal(arr[1].toString('utf8'), 'b');
|
|
61
|
+
assert.equal(arr[2].toString('utf8'), 'a');
|
|
62
|
+
|
|
63
|
+
const withScores = await sendCommand(port, argv('ZREVRANGE', 'zrev', '0', '30', 'WITHSCORES'));
|
|
64
|
+
const withArr = tryParseValue(withScores, 0).value;
|
|
65
|
+
assert.equal(withArr.length, 6);
|
|
66
|
+
assert.equal(withArr[0].toString('utf8'), 'c');
|
|
67
|
+
assert.equal(withArr[1].toString('utf8'), '3');
|
|
68
|
+
assert.equal(withArr[2].toString('utf8'), 'b');
|
|
69
|
+
assert.equal(withArr[3].toString('utf8'), '2');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('ZREVRANK returns 0-based rank high-to-low and nil when missing', async () => {
|
|
73
|
+
await sendCommand(port, argv('ZADD', 'zrevrank', '1', 'a', '2', 'b', '3', 'c'));
|
|
74
|
+
const rankC = await sendCommand(port, argv('ZREVRANK', 'zrevrank', 'c'));
|
|
75
|
+
assert.equal(tryParseValue(rankC, 0).value, 0);
|
|
76
|
+
const rankB = await sendCommand(port, argv('ZREVRANK', 'zrevrank', 'b'));
|
|
77
|
+
assert.equal(tryParseValue(rankB, 0).value, 1);
|
|
78
|
+
const rankA = await sendCommand(port, argv('ZREVRANK', 'zrevrank', 'a'));
|
|
79
|
+
assert.equal(tryParseValue(rankA, 0).value, 2);
|
|
80
|
+
|
|
81
|
+
const noMember = await sendCommand(port, argv('ZREVRANK', 'zrevrank', 'x'));
|
|
82
|
+
assert.ok(noMember.toString('ascii').startsWith('$-1'));
|
|
83
|
+
const noKey = await sendCommand(port, argv('ZREVRANK', 'nokey', 'a'));
|
|
84
|
+
assert.ok(noKey.toString('ascii').startsWith('$-1'));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('ZRANK returns 0-based rank low-to-high and nil when missing', async () => {
|
|
88
|
+
await sendCommand(port, argv('ZADD', 'zrank', '1', 'a', '2', 'b', '3', 'c'));
|
|
89
|
+
const rankA = await sendCommand(port, argv('ZRANK', 'zrank', 'a'));
|
|
90
|
+
assert.equal(tryParseValue(rankA, 0).value, 0);
|
|
91
|
+
const rankB = await sendCommand(port, argv('ZRANK', 'zrank', 'b'));
|
|
92
|
+
assert.equal(tryParseValue(rankB, 0).value, 1);
|
|
93
|
+
const rankC = await sendCommand(port, argv('ZRANK', 'zrank', 'c'));
|
|
94
|
+
assert.equal(tryParseValue(rankC, 0).value, 2);
|
|
95
|
+
|
|
96
|
+
const noMember = await sendCommand(port, argv('ZRANK', 'zrank', 'x'));
|
|
97
|
+
assert.ok(noMember.toString('ascii').startsWith('$-1'));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('ZREVRANGEBYSCORE returns score range high-to-low and supports WITHSCORES and LIMIT', async () => {
|
|
101
|
+
await sendCommand(port, argv('ZADD', 'zrevscore', '1', 'a', '2', 'b', '3', 'c', '4', 'd'));
|
|
102
|
+
const rev = await sendCommand(port, argv('ZREVRANGEBYSCORE', 'zrevscore', '4', '2'));
|
|
103
|
+
const arr = tryParseValue(rev, 0).value;
|
|
104
|
+
assert.equal(arr.length, 3);
|
|
105
|
+
assert.equal(arr[0].toString('utf8'), 'd');
|
|
106
|
+
assert.equal(arr[1].toString('utf8'), 'c');
|
|
107
|
+
assert.equal(arr[2].toString('utf8'), 'b');
|
|
108
|
+
|
|
109
|
+
const withScores = await sendCommand(port, argv('ZREVRANGEBYSCORE', 'zrevscore', '10', '0', 'WITHSCORES'));
|
|
110
|
+
const withArr = tryParseValue(withScores, 0).value;
|
|
111
|
+
assert.equal(withArr.length, 8);
|
|
112
|
+
assert.equal(withArr[0].toString('utf8'), 'd');
|
|
113
|
+
assert.equal(withArr[1].toString('utf8'), '4');
|
|
114
|
+
|
|
115
|
+
const limitReply = await sendCommand(port, argv('ZREVRANGEBYSCORE', 'zrevscore', '10', '0', 'LIMIT', '1', '2'));
|
|
116
|
+
const limited = tryParseValue(limitReply, 0).value;
|
|
117
|
+
assert.equal(limited.length, 2);
|
|
118
|
+
assert.equal(limited[0].toString('utf8'), 'c');
|
|
119
|
+
assert.equal(limited[1].toString('utf8'), 'b');
|
|
120
|
+
});
|
|
121
|
+
|
|
54
122
|
it('ZRANGE negative indices and start > stop', async () => {
|
|
55
123
|
await sendCommand(port, argv('ZADD', 'z4', '1', 'a', '2', 'b', '3', 'c'));
|
|
56
124
|
const last = await sendCommand(port, argv('ZRANGE', 'z4', '-1', '-1'));
|