• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NodeBB / NodeBB / 23689282802

28 Mar 2026 04:23PM UTC coverage: 85.424% (-0.01%) from 85.435%
23689282802

Pull #14129

github

web-flow
Merge 2c2fb71d3 into 6c4e92848
Pull Request #14129: Fix the saving of the statistics on PosgreSQL #14124

13444 of 18449 branches covered (72.87%)

18 of 24 new or added lines in 4 files covered. (75.0%)

2 existing lines in 2 files now uncovered.

28395 of 33240 relevant lines covered (85.42%)

3334.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.67
/src/database/postgres/sorted.js
1
'use strict';
2

3
module.exports = function (module) {
8✔
4
        const helpers = require('./helpers');
8✔
5
        const util = require('util');
8✔
6
        const Cursor = require('pg-cursor');
8✔
7
        Cursor.prototype.readAsync = util.promisify(Cursor.prototype.read);
8✔
8
        const sleep = util.promisify(setTimeout);
8✔
9

10
        require('./sorted/add')(module);
8✔
11
        require('./sorted/remove')(module);
8✔
12
        require('./sorted/union')(module);
8✔
13
        require('./sorted/intersect')(module);
8✔
14

15
        module.getSortedSetRange = async function (key, start, stop) {
8✔
16
                return await getSortedSetRange(key, start, stop, 1, false);
1,364✔
17
        };
18

19
        module.getSortedSetRevRange = async function (key, start, stop) {
8✔
20
                return await getSortedSetRange(key, start, stop, -1, false);
2,387✔
21
        };
22

23
        module.getSortedSetRangeWithScores = async function (key, start, stop) {
8✔
24
                return await getSortedSetRange(key, start, stop, 1, true);
845✔
25
        };
26

27
        module.getSortedSetRevRangeWithScores = async function (key, start, stop) {
8✔
28
                return await getSortedSetRange(key, start, stop, -1, true);
314✔
29
        };
30

31
        async function getSortedSetRange(key, start, stop, sort, withScores) {
32
                if (!key) {
4,910!
33
                        return;
×
34
                }
35

36
                if (!Array.isArray(key)) {
4,910✔
37
                        key = [key];
4,665✔
38
                }
39

40
                if (start < 0 && start > stop) {
4,910✔
41
                        return [];
1✔
42
                }
43

44
                let reverse = false;
4,909✔
45
                if (start === 0 && stop < -1) {
4,909✔
46
                        reverse = true;
1✔
47
                        sort *= -1;
1✔
48
                        start = Math.abs(stop + 1);
1✔
49
                        stop = -1;
1✔
50
                } else if (start < 0 && stop > start) {
4,908✔
51
                        const tmp1 = Math.abs(stop + 1);
3✔
52
                        stop = Math.abs(start + 1);
3✔
53
                        start = tmp1;
3✔
54
                }
55

56
                let limit = stop - start + 1;
4,909✔
57
                if (limit <= 0) {
4,909✔
58
                        limit = null;
2,286✔
59
                }
60

61
                const res = await module.pool.query({
4,909✔
62
                        name: `getSortedSetRangeWithScores${sort > 0 ? 'Asc' : 'Desc'}`,
4,909✔
63
                        text: `
64
SELECT z."value",
65
       z."score"
66
  FROM "legacy_object_live" o
67
 INNER JOIN "legacy_zset" z
68
         ON o."_key" = z."_key"
69
        AND o."type" = z."type"
70
 WHERE o."_key" = ANY($1::TEXT[])
71
 ORDER BY z."score" ${sort > 0 ? 'ASC' : 'DESC'}
4,909✔
72
 LIMIT $3::INTEGER
73
OFFSET $2::INTEGER`,
74
                        values: [key, start, limit],
75
                });
76

77
                if (reverse) {
4,909✔
78
                        res.rows.reverse();
1✔
79
                }
80

81
                if (withScores) {
4,909✔
82
                        res.rows = res.rows.map(r => ({ value: r.value, score: parseFloat(r.score) }));
1,159✔
83
                } else {
84
                        res.rows = res.rows.map(r => r.value);
46,278✔
85
                }
86

87
                return res.rows;
4,909✔
88
        }
89

90
        module.getSortedSetRangeByScore = async function (key, start, count, min, max) {
8✔
91
                return await getSortedSetRangeByScore(key, start, count, min, max, 1, false);
190✔
92
        };
93

94
        module.getSortedSetRevRangeByScore = async function (key, start, count, max, min) {
8✔
95
                return await getSortedSetRangeByScore(key, start, count, min, max, -1, false);
189✔
96
        };
97

98
        module.getSortedSetRangeByScoreWithScores = async function (key, start, count, min, max) {
8✔
99
                return await getSortedSetRangeByScore(key, start, count, min, max, 1, true);
12✔
100
        };
101

102
        module.getSortedSetRevRangeByScoreWithScores = async function (key, start, count, max, min) {
8✔
103
                return await getSortedSetRangeByScore(key, start, count, min, max, -1, true);
237✔
104
        };
105

106
        async function getSortedSetRangeByScore(key, start, count, min, max, sort, withScores) {
107
                if (!key) {
628!
108
                        return;
×
109
                }
110

111
                if (!Array.isArray(key)) {
628✔
112
                        key = [key];
523✔
113
                }
114

115
                if (parseInt(count, 10) === -1) {
628✔
116
                        count = null;
311✔
117
                }
118

119
                if (min === '-inf') {
628✔
120
                        min = null;
72✔
121
                }
122
                if (max === '+inf') {
628✔
123
                        max = null;
404✔
124
                }
125

126
                const res = await module.pool.query({
628✔
127
                        name: `getSortedSetRangeByScoreWithScores${sort > 0 ? 'Asc' : 'Desc'}`,
628✔
128
                        text: `
129
SELECT z."value",
130
       z."score"
131
  FROM "legacy_object_live" o
132
 INNER JOIN "legacy_zset" z
133
         ON o."_key" = z."_key"
134
        AND o."type" = z."type"
135
 WHERE o."_key" = ANY($1::TEXT[])
136
   AND (z."score" >= $4::NUMERIC OR $4::NUMERIC IS NULL)
137
   AND (z."score" <= $5::NUMERIC OR $5::NUMERIC IS NULL)
138
 ORDER BY z."score" ${sort > 0 ? 'ASC' : 'DESC'}
628✔
139
 LIMIT $3::INTEGER
140
OFFSET $2::INTEGER`,
141
                        values: [key, start, count, min, max],
142
                });
143

144
                if (withScores) {
628✔
145
                        res.rows = res.rows.map(r => ({ value: r.value, score: parseFloat(r.score) }));
3,785✔
146
                } else {
147
                        res.rows = res.rows.map(r => r.value);
907✔
148
                }
149

150
                return res.rows;
628✔
151
        }
152

153
        module.sortedSetCount = async function (key, min, max) {
8✔
154
                if (!key) {
117!
155
                        return;
×
156
                }
157

158
                if (min === '-inf') {
117✔
159
                        min = null;
4✔
160
                }
161
                if (max === '+inf') {
117✔
162
                        max = null;
110✔
163
                }
164

165
                const res = await module.pool.query({
117✔
166
                        name: 'sortedSetCount',
167
                        text: `
168
SELECT COUNT(*) c
169
  FROM "legacy_object_live" o
170
 INNER JOIN "legacy_zset" z
171
         ON o."_key" = z."_key"
172
        AND o."type" = z."type"
173
 WHERE o."_key" = $1::TEXT
174
   AND (z."score" >= $2::NUMERIC OR $2::NUMERIC IS NULL)
175
   AND (z."score" <= $3::NUMERIC OR $3::NUMERIC IS NULL)`,
176
                        values: [key, min, max],
177
                });
178

179
                return parseInt(res.rows[0].c, 10);
117✔
180
        };
181

182
        module.sortedSetCard = async function (key) {
8✔
183
                if (!key) {
1,731!
184
                        return 0;
×
185
                }
186

187
                const res = await module.pool.query({
1,731✔
188
                        name: 'sortedSetCard',
189
                        text: `
190
SELECT COUNT(*) c
191
  FROM "legacy_object_live" o
192
 INNER JOIN "legacy_zset" z
193
         ON o."_key" = z."_key"
194
        AND o."type" = z."type"
195
 WHERE o."_key" = $1::TEXT`,
196
                        values: [key],
197
                });
198

199
                return parseInt(res.rows[0].c, 10);
1,731✔
200
        };
201

202
        module.sortedSetsCard = async function (keys) {
8✔
203
                if (!Array.isArray(keys) || !keys.length) {
908✔
204
                        return [];
58✔
205
                }
206

207
                const res = await module.pool.query({
850✔
208
                        name: 'sortedSetsCard',
209
                        text: `
210
SELECT o."_key" k,
211
       COUNT(*) c
212
  FROM "legacy_object_live" o
213
 INNER JOIN "legacy_zset" z
214
         ON o."_key" = z."_key"
215
        AND o."type" = z."type"
216
 WHERE o."_key" = ANY($1::TEXT[])
217
 GROUP BY o."_key"`,
218
                        values: [keys],
219
                });
220

221
                return keys.map(k => parseInt((res.rows.find(r => r.k === k) || { c: 0 }).c, 10));
1,621✔
222
        };
223

224
        module.sortedSetsCardSum = async function (keys, min = '-inf', max = '+inf') {
8✔
225
                if (!keys || (Array.isArray(keys) && !keys.length)) {
216✔
226
                        return 0;
10✔
227
                }
228
                if (!Array.isArray(keys)) {
206✔
229
                        keys = [keys];
1✔
230
                }
231
                let counts;
232
                if (min !== '-inf' || max !== '+inf') {
206✔
233
                        if (min === '-inf') {
2✔
234
                                min = null;
1✔
235
                        }
236
                        if (max === '+inf') {
2✔
237
                                max = null;
1✔
238
                        }
239

240
                        const res = await module.pool.query({
2✔
241
                                name: 'sortedSetsCardSum',
242
                                text: `
243
        SELECT o."_key" k,
244
                COUNT(*) c
245
        FROM "legacy_object_live" o
246
        INNER JOIN "legacy_zset" z
247
                         ON o."_key" = z."_key"
248
                        AND o."type" = z."type"
249
        WHERE o."_key" = ANY($1::TEXT[])
250
                AND (z."score" >= $2::NUMERIC OR $2::NUMERIC IS NULL)
251
                AND (z."score" <= $3::NUMERIC OR $3::NUMERIC IS NULL)
252
        GROUP BY o."_key"`,
253
                                values: [keys, min, max],
254
                        });
255
                        counts = keys.map(k => parseInt((res.rows.find(r => r.k === k) || { c: 0 }).c, 10));
11✔
256
                } else {
257
                        counts = await module.sortedSetsCard(keys);
204✔
258
                }
259
                return counts.reduce((acc, val) => acc + val, 0);
579✔
260
        };
261

262
        module.sortedSetRank = async function (key, value) {
8✔
263
                const result = await getSortedSetRank('ASC', [key], [value]);
63✔
264
                return result ? result[0] : null;
63!
265
        };
266

267
        module.sortedSetRevRank = async function (key, value) {
8✔
268
                const result = await getSortedSetRank('DESC', [key], [value]);
4✔
269
                return result ? result[0] : null;
4!
270
        };
271

272
        async function getSortedSetRank(sort, keys, values) {
273
                values = values.map(helpers.valueToString);
82✔
274
                const res = await module.pool.query({
82✔
275
                        name: `getSortedSetRank${sort}`,
276
                        text: `
277
SELECT (SELECT r
278
          FROM (SELECT z."value" v,
279
                       RANK() OVER (PARTITION BY o."_key"
280
                                        ORDER BY z."score" ${sort},
281
                                                 z."value" ${sort}) - 1 r
282
                  FROM "legacy_object_live" o
283
                 INNER JOIN "legacy_zset" z
284
                         ON o."_key" = z."_key"
285
                        AND o."type" = z."type"
286
                 WHERE o."_key" = kvi.k) r
287
         WHERE v = kvi.v) r
288
  FROM UNNEST($1::TEXT[], $2::TEXT[]) WITH ORDINALITY kvi(k, v, i)
289
 ORDER BY kvi.i ASC`,
290
                        values: [keys, values],
291
                });
292

293
                return res.rows.map(r => (r.r === null ? null : parseFloat(r.r)));
93✔
294
        }
295

296
        module.sortedSetsRanks = async function (keys, values) {
8✔
297
                if (!Array.isArray(keys) || !keys.length) {
1!
298
                        return [];
×
299
                }
300

301
                return await getSortedSetRank('ASC', keys, values);
1✔
302
        };
303

304
        module.sortedSetsRevRanks = async function (keys, values) {
8✔
305
                if (!Array.isArray(keys) || !keys.length) {
×
306
                        return [];
×
307
                }
308

309
                return await getSortedSetRank('DESC', keys, values);
×
310
        };
311

312
        module.sortedSetRanks = async function (key, values) {
8✔
313
                if (!Array.isArray(values) || !values.length) {
13!
314
                        return [];
×
315
                }
316

317
                return await getSortedSetRank('ASC', new Array(values.length).fill(key), values);
13✔
318
        };
319

320
        module.sortedSetRevRanks = async function (key, values) {
8✔
321
                if (!Array.isArray(values) || !values.length) {
1!
322
                        return [];
×
323
                }
324

325
                return await getSortedSetRank('DESC', new Array(values.length).fill(key), values);
1✔
326
        };
327

328
        module.sortedSetScore = async function (key, value) {
8✔
329
                if (!key) {
4,992✔
330
                        return null;
1✔
331
                }
332

333
                value = helpers.valueToString(value);
4,991✔
334

335
                const res = await module.pool.query({
4,991✔
336
                        name: 'sortedSetScore',
337
                        text: `
338
SELECT z."score" s
339
  FROM "legacy_object_live" o
340
 INNER JOIN "legacy_zset" z
341
         ON o."_key" = z."_key"
342
        AND o."type" = z."type"
343
 WHERE o."_key" = $1::TEXT
344
   AND z."value" = $2::TEXT`,
345
                        values: [key, value],
346
                });
347
                if (res.rows.length) {
4,991✔
348
                        return parseFloat(res.rows[0].s);
1,238✔
349
                }
350
                return null;
3,753✔
351
        };
352

353
        module.sortedSetsScore = async function (keys, value) {
8✔
354
                if (!Array.isArray(keys) || !keys.length) {
537✔
355
                        return [];
1✔
356
                }
357

358
                value = helpers.valueToString(value);
536✔
359

360
                const res = await module.pool.query({
536✔
361
                        name: 'sortedSetsScore',
362
                        text: `
363
SELECT o."_key" k,
364
       z."score" s
365
  FROM "legacy_object_live" o
366
 INNER JOIN "legacy_zset" z
367
         ON o."_key" = z."_key"
368
        AND o."type" = z."type"
369
 WHERE o."_key" = ANY($1::TEXT[])
370
   AND z."value" = $2::TEXT`,
371
                        values: [keys, value],
372
                });
373

374
                return keys.map((k) => {
536✔
375
                        const s = res.rows.find(r => r.k === k);
1,415✔
376
                        return s ? parseFloat(s.s) : null;
1,415✔
377
                });
378
        };
379

380
        module.sortedSetScores = async function (key, values) {
8✔
381
                if (!key) {
2,770!
382
                        return null;
×
383
                }
384
                if (!values.length) {
2,770✔
385
                        return [];
44✔
386
                }
387
                values = values.map(helpers.valueToString);
2,726✔
388

389
                const res = await module.pool.query({
2,726✔
390
                        name: 'sortedSetScores',
391
                        text: `
392
SELECT z."value" v,
393
       z."score" s
394
  FROM "legacy_object_live" o
395
 INNER JOIN "legacy_zset" z
396
         ON o."_key" = z."_key"
397
        AND o."type" = z."type"
398
 WHERE o."_key" = $1::TEXT
399
   AND z."value" = ANY($2::TEXT[])`,
400
                        values: [key, values],
401
                });
402

403
                return values.map((v) => {
2,726✔
404
                        const s = res.rows.find(r => r.v === v);
33,213✔
405
                        return s ? parseFloat(s.s) : null;
33,213✔
406
                });
407
        };
408

409
        module.isSortedSetMember = async function (key, value) {
8✔
410
                if (!key) {
7,869!
411
                        return;
×
412
                }
413

414
                value = helpers.valueToString(value);
7,869✔
415

416
                const res = await module.pool.query({
7,869✔
417
                        name: 'isSortedSetMember',
418
                        text: `
419
SELECT 1
420
  FROM "legacy_object_live" o
421
 INNER JOIN "legacy_zset" z
422
         ON o."_key" = z."_key"
423
        AND o."type" = z."type"
424
 WHERE o."_key" = $1::TEXT
425
   AND z."value" = $2::TEXT`,
426
                        values: [key, value],
427
                });
428

429
                return !!res.rows.length;
7,869✔
430
        };
431

432
        module.isSortedSetMembers = async function (key, values) {
8✔
433
                if (!key) {
3,875!
434
                        return;
×
435
                }
436

437
                if (!values.length) {
3,875✔
438
                        return [];
116✔
439
                }
440
                values = values.map(helpers.valueToString);
3,759✔
441

442
                const res = await module.pool.query({
3,759✔
443
                        name: 'isSortedSetMembers',
444
                        text: `
445
SELECT z."value" v
446
  FROM "legacy_object_live" o
447
 INNER JOIN "legacy_zset" z
448
         ON o."_key" = z."_key"
449
        AND o."type" = z."type"
450
 WHERE o."_key" = $1::TEXT
451
   AND z."value" = ANY($2::TEXT[])`,
452
                        values: [key, values],
453
                });
454

455
                return values.map(v => res.rows.some(r => r.v === v));
58,276✔
456
        };
457

458
        module.isMemberOfSortedSets = async function (keys, value) {
8✔
459
                if (!Array.isArray(keys) || !keys.length) {
3,550✔
460
                        return [];
185✔
461
                }
462

463
                value = helpers.valueToString(value);
3,365✔
464

465
                const res = await module.pool.query({
3,365✔
466
                        name: 'isMemberOfSortedSets',
467
                        text: `
468
SELECT o."_key" k
469
  FROM "legacy_object_live" o
470
 INNER JOIN "legacy_zset" z
471
         ON o."_key" = z."_key"
472
        AND o."type" = z."type"
473
 WHERE o."_key" = ANY($1::TEXT[])
474
   AND z."value" = $2::TEXT`,
475
                        values: [keys, value],
476
                });
477

478
                return keys.map(k => res.rows.some(r => r.k === k));
22,464✔
479
        };
480

481
        module.getSortedSetMembers = async function (key) {
8✔
482
                const data = await module.getSortedSetsMembers([key]);
699✔
483
                return data && data[0];
699✔
484
        };
485

486
        module.getSortedSetMembersWithScores = async function (key) {
8✔
487
                const data = await module.getSortedSetsMembersWithScores([key]);
61✔
488
                return data && data[0];
61✔
489
        };
490

491
        module.getSortedSetsMembers = async function (keys) {
8✔
492
                if (!Array.isArray(keys) || !keys.length) {
1,806✔
493
                        return [];
94✔
494
                }
495

496
                const res = await module.pool.query({
1,712✔
497
                        name: 'getSortedSetsMembers',
498
                        text: `
499
SELECT "_key" k,
500
       "nodebb_get_sorted_set_members"("_key") m
501
  FROM UNNEST($1::TEXT[]) "_key";`,
502
                        values: [keys],
503
                });
504

505
                return keys.map(k => (res.rows.find(r => r.k === k) || {}).m || []);
20,928!
506
        };
507

508
        module.getSortedSetsMembersWithScores = async function (keys) {
8✔
509
                if (!Array.isArray(keys) || !keys.length) {
100!
510
                        return [];
×
511
                }
512

513
                const res = await module.pool.query({
100✔
514
                        name: 'getSortedSetsMembersWithScores',
515
                        text: `
516
SELECT "_key" k,
517
       "nodebb_get_sorted_set_members_withscores"("_key") m
518
  FROM UNNEST($1::TEXT[]) "_key";`,
519
                        values: [keys],
520
                });
521

522
                return keys.map(k => (res.rows.find(r => r.k === k) || {}).m || []);
102!
523
        };
524

525
        module.sortedSetIncrBy = async function (key, increment, value) {
8✔
526
                if (!key) {
859!
527
                        return;
×
528
                }
529

530
                value = helpers.valueToString(value);
859✔
531
                increment = parseFloat(increment);
859✔
532

533
                return await module.transaction(async (client) => {
859✔
534
                        await helpers.ensureLegacyObjectType(client, key, 'zset');
859✔
535
                        const res = await client.query({
859✔
536
                                name: 'sortedSetIncrBy',
537
                                text: `
538
INSERT INTO "legacy_zset" ("_key", "value", "score")
539
VALUES ($1::TEXT, $2::TEXT, $3::NUMERIC)
540
ON CONFLICT ("_key", "value")
541
DO UPDATE SET "score" = "legacy_zset"."score" + $3::NUMERIC
542
RETURNING "score" s`,
543
                                values: [key, value, increment],
544
                        });
545
                        return parseFloat(res.rows[0].s);
859✔
546
                });
547
        };
548

549
        module.sortedSetIncrByBulk = async function (data) {
8✔
550
                if (!data.length) {
19!
551
                        return [];
×
552
                }
553

554
                // Deduplicate by (key, value) pair, summing increments for duplicates
555
                const seen = new Map();
19✔
556
                const deduped = [];
19✔
557
                data.forEach(([key, increment, value]) => {
19✔
558
                        value = helpers.valueToString(value);
37✔
559
                        increment = parseFloat(increment);
37✔
560
                        const mapKey = `${key}\0${value}`;
37✔
561
                        if (seen.has(mapKey)) {
37!
NEW
562
                                deduped[seen.get(mapKey)][1] += increment;
×
563
                        } else {
564
                                seen.set(mapKey, deduped.length);
37✔
565
                                deduped.push([key, increment, value]);
37✔
566
                        }
567
                });
568

569
                return await module.transaction(async (client) => {
19✔
570
                        await helpers.ensureLegacyObjectsType(client, deduped.map(item => item[0]), 'zset');
37✔
571

572
                        const values = [];
19✔
573
                        const queryParams = [];
19✔
574
                        let paramIndex = 1;
19✔
575

576
                        deduped.forEach(([key, increment, value]) => {
19✔
577
                                values.push(key, value, increment);
37✔
578
                                queryParams.push(`($${paramIndex}::TEXT, $${paramIndex + 1}::TEXT, $${paramIndex + 2}::NUMERIC)`);
37✔
579
                                paramIndex += 3;
37✔
580
                        });
581

582
                        const query = `
19✔
583
INSERT INTO "legacy_zset" ("_key", "value", "score")
584
VALUES ${queryParams.join(', ')}
585
ON CONFLICT ("_key", "value")
586
DO UPDATE SET "score" = "legacy_zset"."score" + EXCLUDED."score"
587
RETURNING "value", "score"`;
588

589
                        const res = await client.query({
19✔
590
                                text: query,
591
                                values,
592
                        });
593
                        return res.rows.map(row => parseFloat(row.score));
37✔
594
                });
595
        };
596

597
        module.getSortedSetRangeByLex = async function (key, min, max, start, count) {
8✔
598
                return await sortedSetLex(key, min, max, 1, start, count);
37✔
599
        };
600

601
        module.getSortedSetRevRangeByLex = async function (key, max, min, start, count) {
8✔
602
                return await sortedSetLex(key, min, max, -1, start, count);
5✔
603
        };
604

605
        module.sortedSetLexCount = async function (key, min, max) {
8✔
606
                const q = buildLexQuery(key, min, max);
4✔
607

608
                const res = await module.pool.query({
4✔
609
                        name: `sortedSetLexCount${q.suffix}`,
610
                        text: `
611
SELECT COUNT(*) c
612
  FROM "legacy_object_live" o
613
 INNER JOIN "legacy_zset" z
614
         ON o."_key" = z."_key"
615
        AND o."type" = z."type"
616
 WHERE ${q.where}`,
617
                        values: q.values,
618
                });
619

620
                return parseInt(res.rows[0].c, 10);
4✔
621
        };
622

623
        async function sortedSetLex(key, min, max, sort, start, count) {
624
                start = start !== undefined ? start : 0;
42✔
625
                count = count !== undefined ? count : 0;
42✔
626

627
                const q = buildLexQuery(key, min, max);
42✔
628
                q.values.push(start);
42✔
629
                q.values.push(count <= 0 ? null : count);
42✔
630
                const res = await module.pool.query({
42✔
631
                        name: `sortedSetLex${sort > 0 ? 'Asc' : 'Desc'}${q.suffix}`,
42✔
632
                        text: `
633
SELECT z."value" v
634
  FROM "legacy_object_live" o
635
 INNER JOIN "legacy_zset" z
636
         ON o."_key" = z."_key"
637
        AND o."type" = z."type"
638
 WHERE ${q.where}
639
 ORDER BY z."value" ${sort > 0 ? 'ASC' : 'DESC'}
42✔
640
 LIMIT $${q.values.length}::INTEGER
641
OFFSET $${q.values.length - 1}::INTEGER`,
642
                        values: q.values,
643
                });
644

645
                return res.rows.map(r => r.v);
82✔
646
        }
647

648
        module.sortedSetRemoveRangeByLex = async function (key, min, max) {
8✔
649
                const q = buildLexQuery(key, min, max);
4✔
650
                await module.pool.query({
4✔
651
                        name: `sortedSetRemoveRangeByLex${q.suffix}`,
652
                        text: `
653
DELETE FROM "legacy_zset" z
654
 USING "legacy_object_live" o
655
 WHERE o."_key" = z."_key"
656
   AND o."type" = z."type"
657
   AND ${q.where}`,
658
                        values: q.values,
659
                });
660
        };
661

662
        function buildLexQuery(key, min, max) {
663
                const q = {
50✔
664
                        suffix: '',
665
                        where: `o."_key" = $1::TEXT`,
666
                        values: [key],
667
                };
668

669
                if (min !== '-') {
50✔
670
                        if (min.match(/^\(/)) {
35✔
671
                                q.values.push(min.slice(1));
4✔
672
                                q.suffix += 'GT';
4✔
673
                                q.where += ` AND z."value" > $${q.values.length}::TEXT COLLATE "C"`;
4✔
674
                        } else if (min.match(/^\[/)) {
31✔
675
                                q.values.push(min.slice(1));
4✔
676
                                q.suffix += 'GE';
4✔
677
                                q.where += ` AND z."value" >= $${q.values.length}::TEXT COLLATE "C"`;
4✔
678
                        } else {
679
                                q.values.push(min);
27✔
680
                                q.suffix += 'GE';
27✔
681
                                q.where += ` AND z."value" >= $${q.values.length}::TEXT COLLATE "C"`;
27✔
682
                        }
683
                }
684

685
                if (max !== '+') {
50✔
686
                        if (max.match(/^\(/)) {
35✔
687
                                q.values.push(max.slice(1));
4✔
688
                                q.suffix += 'LT';
4✔
689
                                q.where += ` AND z."value" < $${q.values.length}::TEXT COLLATE "C"`;
4✔
690
                        } else if (max.match(/^\[/)) {
31✔
691
                                q.values.push(max.slice(1));
4✔
692
                                q.suffix += 'LE';
4✔
693
                                q.where += ` AND z."value" <= $${q.values.length}::TEXT COLLATE "C"`;
4✔
694
                        } else {
695
                                q.values.push(max);
27✔
696
                                q.suffix += 'LE';
27✔
697
                                q.where += ` AND z."value" <= $${q.values.length}::TEXT COLLATE "C"`;
27✔
698
                        }
699
                }
700

701
                return q;
50✔
702
        }
703

704
        module.getSortedSetScan = async function (params) {
8✔
705
                let { match } = params;
32✔
706
                if (match.startsWith('*')) {
32✔
707
                        match = `%${match.substring(1)}`;
10✔
708
                }
709

710
                if (match.endsWith('*')) {
32✔
711
                        match = `${match.substring(0, match.length - 1)}%`;
29✔
712
                }
713

714
                const res = await module.pool.query({
32✔
715
                        text: `
716
SELECT z."value",
717
       z."score"
718
  FROM "legacy_object_live" o
719
 INNER JOIN "legacy_zset" z
720
         ON o."_key" = z."_key"
721
        AND o."type" = z."type"
722
 WHERE o."_key" = $1::TEXT
723
  AND z."value" LIKE $3
724
  LIMIT $2::INTEGER`,
725
                        values: [params.key, params.limit, match],
726
                });
727
                if (!params.withScores) {
32✔
728
                        return res.rows.map(r => r.value);
30✔
729
                }
730
                return res.rows.map(r => ({ value: r.value, score: parseFloat(r.score) }));
6✔
731
        };
732

733
        module.processSortedSet = async function (setKey, process, options) {
8✔
734
                const client = await module.pool.connect();
615✔
735
                const batchSize = (options || {}).batch || 100;
615!
736
                const sort = options.reverse ? 'DESC' : 'ASC';
615✔
737
                const min = options.min && options.min !== '-inf' ? options.min : null;
615✔
738
                const max = options.max && options.max !== '+inf' ? options.max : null;
615✔
739
                const cursor = client.query(new Cursor(`
615✔
740
SELECT z."value", z."score"
741
  FROM "legacy_object_live" o
742
 INNER JOIN "legacy_zset" z
743
         ON o."_key" = z."_key"
744
        AND o."type" = z."type"
745
 WHERE o."_key" = $1::TEXT
746
   AND (z."score" >= $2::NUMERIC OR $2::NUMERIC IS NULL)
747
   AND (z."score" <= $3::NUMERIC OR $3::NUMERIC IS NULL)
748
 ORDER BY z."score" ${sort}, z."value" ${sort}`, [setKey, min, max]));
749

750
                if (process && process.constructor && process.constructor.name !== 'AsyncFunction') {
615✔
751
                        process = util.promisify(process);
21✔
752
                }
753
                let iteration = 1;
615✔
754
                while (true) {
615✔
755
                        /* eslint-disable no-await-in-loop */
756
                        let rows = await cursor.readAsync(batchSize);
700✔
757
                        if (!rows.length) {
700✔
758
                                client.release();
615✔
759
                                return;
615✔
760
                        }
761

762
                        if (options.withScores) {
85✔
763
                                rows = rows.map(r => ({ value: r.value, score: parseFloat(r.score) }));
101✔
764
                        } else {
765
                                rows = rows.map(r => r.value);
2,282✔
766
                        }
767
                        try {
85✔
768
                                if (iteration > 1 && options.interval) {
85✔
769
                                        await sleep(options.interval);
9✔
770
                                }
771
                                await process(rows);
85✔
772
                                iteration += 1;
85✔
773
                        } catch (err) {
774
                                await client.release();
×
775
                                throw err;
×
776
                        }
777
                }
778
        };
779
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc