resplite 1.2.10 → 1.2.14
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/client.js +35 -0
- package/src/commands/registry.js +12 -2
- package/src/commands/unlink.js +12 -0
- package/src/commands/zrank.js +18 -0
- package/src/commands/zrevrangebyscore.js +46 -0
- package/src/commands/zrevrank.js +18 -0
- package/src/engine/engine.js +28 -0
- package/src/server/connection.js +1 -0
- package/src/storage/sqlite/zsets.js +61 -0
- package/test/integration/client.test.js +103 -0
- package/test/integration/embed.test.js +5 -5
- package/test/integration/strings.test.js +11 -0
- package/test/integration/zsets.test.js +50 -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
|
];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLIENT - connection introspection (SETNAME, GETNAME, ID).
|
|
3
|
+
* Requires connection context.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function argStr(buf) {
|
|
7
|
+
return Buffer.isBuffer(buf) ? buf.toString('utf8') : String(buf);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function handleClient(engine, args, context) {
|
|
11
|
+
if (!args || args.length < 1) {
|
|
12
|
+
return { error: 'ERR wrong number of arguments for \'CLIENT\' command' };
|
|
13
|
+
}
|
|
14
|
+
const sub = argStr(args[0]).toUpperCase();
|
|
15
|
+
if (sub === 'SETNAME') {
|
|
16
|
+
if (args.length !== 2) {
|
|
17
|
+
return { error: 'ERR wrong number of arguments for \'CLIENT SETNAME\' command' };
|
|
18
|
+
}
|
|
19
|
+
context.connectionName = argStr(args[1]);
|
|
20
|
+
return { simple: 'OK' };
|
|
21
|
+
}
|
|
22
|
+
if (sub === 'GETNAME') {
|
|
23
|
+
if (args.length !== 1) {
|
|
24
|
+
return { error: 'ERR wrong number of arguments for \'CLIENT GETNAME\' command' };
|
|
25
|
+
}
|
|
26
|
+
return context.connectionName ?? null;
|
|
27
|
+
}
|
|
28
|
+
if (sub === 'ID') {
|
|
29
|
+
if (args.length !== 1) {
|
|
30
|
+
return { error: 'ERR wrong number of arguments for \'CLIENT ID\' command' };
|
|
31
|
+
}
|
|
32
|
+
return context.connectionId;
|
|
33
|
+
}
|
|
34
|
+
return { error: 'ERR unknown subcommand or wrong number of arguments for \'CLIENT\'. Try CLIENT HELP.' };
|
|
35
|
+
}
|
package/src/commands/registry.js
CHANGED
|
@@ -9,6 +9,7 @@ import * as quit from './quit.js';
|
|
|
9
9
|
import * as get from './get.js';
|
|
10
10
|
import * as set from './set.js';
|
|
11
11
|
import * as del from './del.js';
|
|
12
|
+
import * as unlink from './unlink.js';
|
|
12
13
|
import * as exists from './exists.js';
|
|
13
14
|
import * as type from './type.js';
|
|
14
15
|
import * as object from './object.js';
|
|
@@ -53,8 +54,11 @@ import * as zrem from './zrem.js';
|
|
|
53
54
|
import * as zcard from './zcard.js';
|
|
54
55
|
import * as zscore from './zscore.js';
|
|
55
56
|
import * as zrange from './zrange.js';
|
|
56
|
-
import * as zrevrange from './zrevrange.js';
|
|
57
57
|
import * as zrangebyscore from './zrangebyscore.js';
|
|
58
|
+
import * as zrevrange from './zrevrange.js';
|
|
59
|
+
import * as zrevrangebyscore from './zrevrangebyscore.js';
|
|
60
|
+
import * as zrevrank from './zrevrank.js';
|
|
61
|
+
import * as zrank from './zrank.js';
|
|
58
62
|
import * as sqliteInfo from './sqlite-info.js';
|
|
59
63
|
import * as cacheInfo from './cache-info.js';
|
|
60
64
|
import * as memoryInfo from './memory-info.js';
|
|
@@ -67,6 +71,7 @@ import * as ftSugadd from './ft-sugadd.js';
|
|
|
67
71
|
import * as ftSugget from './ft-sugget.js';
|
|
68
72
|
import * as ftSugdel from './ft-sugdel.js';
|
|
69
73
|
import * as monitor from './monitor.js';
|
|
74
|
+
import * as client from './client.js';
|
|
70
75
|
|
|
71
76
|
const HANDLERS = new Map([
|
|
72
77
|
['PING', (e, a) => ping.handlePing()],
|
|
@@ -75,6 +80,7 @@ const HANDLERS = new Map([
|
|
|
75
80
|
['GET', (e, a) => get.handleGet(e, a)],
|
|
76
81
|
['SET', (e, a) => set.handleSet(e, a)],
|
|
77
82
|
['DEL', (e, a) => del.handleDel(e, a)],
|
|
83
|
+
['UNLINK', (e, a) => unlink.handleUnlink(e, a)],
|
|
78
84
|
['EXISTS', (e, a) => exists.handleExists(e, a)],
|
|
79
85
|
['TYPE', (e, a) => type.handleType(e, a)],
|
|
80
86
|
['OBJECT', (e, a) => object.handleObject(e, a)],
|
|
@@ -119,8 +125,11 @@ const HANDLERS = new Map([
|
|
|
119
125
|
['ZCARD', (e, a) => zcard.handleZcard(e, a)],
|
|
120
126
|
['ZSCORE', (e, a) => zscore.handleZscore(e, a)],
|
|
121
127
|
['ZRANGE', (e, a) => zrange.handleZrange(e, a)],
|
|
122
|
-
['ZREVRANGE', (e, a) => zrevrange.handleZrevrange(e, a)],
|
|
123
128
|
['ZRANGEBYSCORE', (e, a) => zrangebyscore.handleZrangebyscore(e, a)],
|
|
129
|
+
['ZREVRANGE', (e, a) => zrevrange.handleZrevrange(e, a)],
|
|
130
|
+
['ZREVRANGEBYSCORE', (e, a) => zrevrangebyscore.handleZrevrangebyscore(e, a)],
|
|
131
|
+
['ZREVRANK', (e, a) => zrevrank.handleZrevrank(e, a)],
|
|
132
|
+
['ZRANK', (e, a) => zrank.handleZrank(e, a)],
|
|
124
133
|
['SQLITE.INFO', (e, a) => sqliteInfo.handleSqliteInfo(e, a)],
|
|
125
134
|
['CACHE.INFO', (e, a) => cacheInfo.handleCacheInfo(e, a)],
|
|
126
135
|
['MEMORY.INFO', (e, a) => memoryInfo.handleMemoryInfo(e, a)],
|
|
@@ -133,6 +142,7 @@ const HANDLERS = new Map([
|
|
|
133
142
|
['FT.SUGGET', (e, a) => ftSugget.handleFtSugget(e, a)],
|
|
134
143
|
['FT.SUGDEL', (e, a) => ftSugdel.handleFtSugdel(e, a)],
|
|
135
144
|
['MONITOR', (e, a, ctx) => monitor.handleMonitor(a, ctx)],
|
|
145
|
+
['CLIENT', (e, a, ctx) => client.handleClient(e, a, ctx)],
|
|
136
146
|
]);
|
|
137
147
|
|
|
138
148
|
/**
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UNLINK key [key ...] - same as DEL; returns count of removed keys.
|
|
3
|
+
* In Redis, UNLINK is non-blocking; in RESPlite we delegate to DEL.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function handleUnlink(engine, args) {
|
|
7
|
+
if (!args || args.length < 1) {
|
|
8
|
+
return { error: 'ERR wrong number of arguments for \'UNLINK\' command' };
|
|
9
|
+
}
|
|
10
|
+
const n = engine.del(args);
|
|
11
|
+
return n;
|
|
12
|
+
}
|
|
@@ -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,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
|
@@ -395,6 +395,22 @@ export function createEngine(opts = {}) {
|
|
|
395
395
|
return zsets.rangeByRankReverse(k, start, stop, { withScores: options.withScores });
|
|
396
396
|
},
|
|
397
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
|
+
|
|
398
414
|
zrangebyscore(key, min, max, options = {}) {
|
|
399
415
|
const k = asKey(key);
|
|
400
416
|
const meta = getKeyMeta(key);
|
|
@@ -407,6 +423,18 @@ export function createEngine(opts = {}) {
|
|
|
407
423
|
});
|
|
408
424
|
},
|
|
409
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
|
+
|
|
410
438
|
type(key) {
|
|
411
439
|
const meta = getKeyMeta(key);
|
|
412
440
|
return typeName(meta);
|
package/src/server/connection.js
CHANGED
|
@@ -48,6 +48,20 @@ export function createZsetsStorage(db, keys) {
|
|
|
48
48
|
ORDER BY score ASC, member ASC
|
|
49
49
|
LIMIT ? OFFSET ?`
|
|
50
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
|
+
);
|
|
51
65
|
|
|
52
66
|
return {
|
|
53
67
|
/**
|
|
@@ -195,5 +209,52 @@ export function createZsetsStorage(db, keys) {
|
|
|
195
209
|
}
|
|
196
210
|
return out;
|
|
197
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
|
+
},
|
|
198
259
|
};
|
|
199
260
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import { createTestServer } from '../helpers/server.js';
|
|
5
|
+
import { sendCommand, argv } from '../helpers/client.js';
|
|
6
|
+
import { encode } from '../../src/resp/encoder.js';
|
|
7
|
+
import { tryParseValue } from '../../src/resp/parser.js';
|
|
8
|
+
|
|
9
|
+
function sendTwoCommands(port, argv1, argv2) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
let recv = Buffer.alloc(0);
|
|
12
|
+
const results = [];
|
|
13
|
+
const socket = net.createConnection({ port, host: '127.0.0.1' }, () => {
|
|
14
|
+
socket.write(encode(argv1));
|
|
15
|
+
socket.write(encode(argv2));
|
|
16
|
+
});
|
|
17
|
+
const t = setTimeout(() => {
|
|
18
|
+
socket.destroy();
|
|
19
|
+
reject(new Error('sendTwoCommands timeout'));
|
|
20
|
+
}, 3000);
|
|
21
|
+
socket.on('data', (chunk) => {
|
|
22
|
+
recv = Buffer.concat([recv, chunk]);
|
|
23
|
+
while (results.length < 2) {
|
|
24
|
+
const parsed = tryParseValue(recv, 0);
|
|
25
|
+
if (parsed === null) break;
|
|
26
|
+
results.push(parsed.value);
|
|
27
|
+
recv = recv.subarray(parsed.end);
|
|
28
|
+
}
|
|
29
|
+
if (results.length === 2) {
|
|
30
|
+
clearTimeout(t);
|
|
31
|
+
socket.destroy();
|
|
32
|
+
resolve(results);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
socket.on('error', (err) => {
|
|
36
|
+
clearTimeout(t);
|
|
37
|
+
reject(err);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('CLIENT integration', () => {
|
|
43
|
+
let s;
|
|
44
|
+
let port;
|
|
45
|
+
|
|
46
|
+
before(async () => {
|
|
47
|
+
s = await createTestServer();
|
|
48
|
+
port = s.port;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
after(async () => {
|
|
52
|
+
await s.closeAsync();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('CLIENT ID returns connection id', async () => {
|
|
56
|
+
const reply = await sendCommand(port, argv('CLIENT', 'ID'));
|
|
57
|
+
const v = tryParseValue(reply, 0).value;
|
|
58
|
+
assert.strictEqual(typeof v, 'number');
|
|
59
|
+
assert.ok(Number.isInteger(v) && v >= 1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('CLIENT GETNAME returns null when no name set', async () => {
|
|
63
|
+
const reply = await sendCommand(port, argv('CLIENT', 'GETNAME'));
|
|
64
|
+
const v = tryParseValue(reply, 0).value;
|
|
65
|
+
assert.strictEqual(v, null);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('CLIENT SETNAME then GETNAME roundtrip', async () => {
|
|
69
|
+
const [setResult, getResult] = await sendTwoCommands(
|
|
70
|
+
port,
|
|
71
|
+
argv('CLIENT', 'SETNAME', 'my-conn'),
|
|
72
|
+
argv('CLIENT', 'GETNAME')
|
|
73
|
+
);
|
|
74
|
+
assert.equal(setResult, 'OK');
|
|
75
|
+
assert.ok(Buffer.isBuffer(getResult));
|
|
76
|
+
assert.equal(getResult.toString('utf8'), 'my-conn');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('CLIENT SETNAME with empty name is allowed', async () => {
|
|
80
|
+
const [setResult, getResult] = await sendTwoCommands(
|
|
81
|
+
port,
|
|
82
|
+
argv('CLIENT', 'SETNAME', ''),
|
|
83
|
+
argv('CLIENT', 'GETNAME')
|
|
84
|
+
);
|
|
85
|
+
assert.equal(setResult, 'OK');
|
|
86
|
+
assert.ok(Buffer.isBuffer(getResult));
|
|
87
|
+
assert.equal(getResult.length, 0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('CLIENT wrong subcommand returns error', async () => {
|
|
91
|
+
const reply = await sendCommand(port, argv('CLIENT', 'LIST'));
|
|
92
|
+
const v = tryParseValue(reply, 0).value;
|
|
93
|
+
assert.ok(v && v.error);
|
|
94
|
+
assert.match(v.error, /unknown subcommand|CLIENT HELP/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('CLIENT without subcommand returns error', async () => {
|
|
98
|
+
const reply = await sendCommand(port, argv('CLIENT'));
|
|
99
|
+
const v = tryParseValue(reply, 0).value;
|
|
100
|
+
assert.ok(v && v.error);
|
|
101
|
+
assert.match(v.error, /wrong number of arguments/);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -147,10 +147,10 @@ describe('createRESPlite', () => {
|
|
|
147
147
|
} catch (_) {}
|
|
148
148
|
await client.quit();
|
|
149
149
|
await srv.close();
|
|
150
|
-
|
|
151
|
-
assert.
|
|
152
|
-
assert.ok(
|
|
153
|
-
assert.equal(typeof
|
|
154
|
-
assert.ok(
|
|
150
|
+
const hgetError = errorCalls.find((c) => c.command === 'HGET');
|
|
151
|
+
assert.ok(hgetError, 'expected at least one HGET error (got: ' + errorCalls.map((c) => c.command).join(', ') + ')');
|
|
152
|
+
assert.ok(hgetError.error.includes('WRONGTYPE'));
|
|
153
|
+
assert.equal(typeof hgetError.connectionId, 'number');
|
|
154
|
+
assert.ok(hgetError.clientAddress.length > 0);
|
|
155
155
|
});
|
|
156
156
|
});
|
|
@@ -33,6 +33,17 @@ describe('Strings integration', () => {
|
|
|
33
33
|
assert.equal(reply.toString('ascii'), ':1\r\n');
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
it('UNLINK returns count like DEL', async () => {
|
|
37
|
+
await sendCommand(port, argv('SET', 'u1', 'a'));
|
|
38
|
+
await sendCommand(port, argv('SET', 'u2', 'b'));
|
|
39
|
+
const one = await sendCommand(port, argv('UNLINK', 'u1'));
|
|
40
|
+
assert.equal(one.toString('ascii'), ':1\r\n');
|
|
41
|
+
const two = await sendCommand(port, argv('UNLINK', 'u2', 'u3'));
|
|
42
|
+
assert.equal(two.toString('ascii'), ':1\r\n');
|
|
43
|
+
const zero = await sendCommand(port, argv('UNLINK', 'u1'));
|
|
44
|
+
assert.equal(zero.toString('ascii'), ':0\r\n');
|
|
45
|
+
});
|
|
46
|
+
|
|
36
47
|
it('EXISTS returns count', async () => {
|
|
37
48
|
await sendCommand(port, argv('SET', 'ex1', 'a'));
|
|
38
49
|
const r = await sendCommand(port, argv('EXISTS', 'ex1', 'ex2'));
|
|
@@ -69,6 +69,56 @@ describe('ZSET integration', () => {
|
|
|
69
69
|
assert.equal(withArr[3].toString('utf8'), '2');
|
|
70
70
|
});
|
|
71
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
|
+
|
|
72
122
|
it('ZRANGE negative indices and start > stop', async () => {
|
|
73
123
|
await sendCommand(port, argv('ZADD', 'z4', '1', 'a', '2', 'b', '3', 'c'));
|
|
74
124
|
const last = await sendCommand(port, argv('ZRANGE', 'z4', '-1', '-1'));
|