Skip to content

Commit 58d419c

Browse files
committed
Support the NOVALUES option of HSCAN
Issue #2705 The NOVALUES option instructs HSCAN to only return keys, without their values.
1 parent dbf8f59 commit 58d419c

File tree

5 files changed

+130
-19
lines changed

5 files changed

+130
-19
lines changed

Diff for: packages/client/lib/client/index.spec.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { defineScript } from '../lua-script';
88
import { spy } from 'sinon';
99
import { once } from 'events';
1010
import { ClientKillFilters } from '../commands/CLIENT_KILL';
11+
import { HScanTuple } from "../commands/HSCAN";
1112
import { promisify } from 'util';
1213

1314
import {version} from '../../package.json';
@@ -774,18 +775,33 @@ describe('Client', () => {
774775

775776
testUtils.testWithClient('hScanIterator', async client => {
776777
const hash: Record<string, string> = {};
778+
const expectedKeys: Array<string> = [];
777779
for (let i = 0; i < 100; i++) {
778780
hash[i.toString()] = i.toString();
781+
expectedKeys.push(i.toString());
779782
}
780783

781784
await client.hSet('key', hash);
782785

783786
const results: Record<string, string> = {};
784-
for await (const { field, value } of client.hScanIterator('key')) {
785-
results[field] = value;
787+
for await (const entry of client.hScanIterator('key')) {
788+
const {field: field, value: value} = entry as HScanTuple;
789+
results[field as string] = value as string;
786790
}
787791

788792
assert.deepEqual(hash, results);
793+
794+
const keys: Array<string> = [];
795+
for await (const entry of client.hScanIterator('key', { NOVALUES: true })) {
796+
const key = entry as string;
797+
keys.push(key);
798+
}
799+
800+
function sort(a: string, b: string) {
801+
return Number(b) - Number(a);
802+
}
803+
804+
assert.deepEqual(keys.sort(sort), expectedKeys.sort(sort));
789805
}, GLOBAL.SERVERS.OPEN);
790806

791807
testUtils.testWithClient('sScanIterator', async client => {

Diff for: packages/client/lib/client/index.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import COMMANDS from './commands';
2-
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString, RedisCommands } from '../commands';
2+
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString, RedisCommands, RedisCommandArgument } from '../commands';
33
import RedisSocket, { RedisSocketOptions, RedisTlsSocketOptions } from './socket';
44
import RedisCommandsQueue, { QueueCommandOptions } from './commands-queue';
55
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
@@ -809,13 +809,19 @@ export default class RedisClient<
809809
} while (cursor !== 0);
810810
}
811811

812-
async* hScanIterator(key: string, options?: ScanOptions): AsyncIterable<ConvertArgumentType<HScanTuple, string>> {
812+
async* hScanIterator(key: string, options?: ScanOptions): AsyncIterable<ConvertArgumentType<HScanTuple | RedisCommandArgument, string>> {
813813
let cursor = 0;
814814
do {
815815
const reply = await (this as any).hScan(key, cursor, options);
816816
cursor = reply.cursor;
817-
for (const tuple of reply.tuples) {
818-
yield tuple;
817+
if (options?.NOVALUES === true) {
818+
for (const k of reply.keys) {
819+
yield k;
820+
}
821+
} else {
822+
for (const tuple of reply.tuples) {
823+
yield tuple;
824+
}
819825
}
820826
} while (cursor !== 0);
821827
}

Diff for: packages/client/lib/commands/HSCAN.spec.ts

+76
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { strict as assert } from 'assert';
22
import testUtils, { GLOBAL } from '../test-utils';
33
import { transformArguments, transformReply } from './HSCAN';
4+
import { RedisCommandArguments } from "./index";
45

56
describe('HSCAN', () => {
67
describe('transformArguments', () => {
@@ -29,6 +30,18 @@ describe('HSCAN', () => {
2930
);
3031
});
3132

33+
it('with NOVALUES', () => {
34+
const expectedReply: RedisCommandArguments = ['HSCAN', 'key', '0', 'NOVALUES'];
35+
expectedReply.preserve = true;
36+
37+
assert.deepEqual(
38+
transformArguments('key', 0, {
39+
NOVALUES: true
40+
}),
41+
expectedReply
42+
);
43+
});
44+
3245
it('with MATCH & COUNT', () => {
3346
assert.deepEqual(
3447
transformArguments('key', 0, {
@@ -38,6 +51,20 @@ describe('HSCAN', () => {
3851
['HSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1']
3952
);
4053
});
54+
55+
it('with MATCH & COUNT & NOVALUES', () => {
56+
const expectedReply: RedisCommandArguments = ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1', 'NOVALUES'];
57+
expectedReply.preserve = true;
58+
59+
assert.deepEqual(
60+
transformArguments('key', 0, {
61+
MATCH: 'pattern',
62+
COUNT: 1,
63+
NOVALUES: true
64+
}),
65+
expectedReply
66+
);
67+
});
4168
});
4269

4370
describe('transformReply', () => {
@@ -51,6 +78,16 @@ describe('HSCAN', () => {
5178
);
5279
});
5380

81+
it('without keys', () => {
82+
assert.deepEqual(
83+
transformReply(['0', []], true),
84+
{
85+
cursor: 0,
86+
keys: []
87+
}
88+
);
89+
});
90+
5491
it('with tuples', () => {
5592
assert.deepEqual(
5693
transformReply(['0', ['field', 'value']]),
@@ -63,6 +100,16 @@ describe('HSCAN', () => {
63100
}
64101
);
65102
});
103+
104+
it('with keys', () => {
105+
assert.deepEqual(
106+
transformReply(['0', ['key1', 'key2']], true),
107+
{
108+
cursor: 0,
109+
keys: ['key1', 'key2']
110+
}
111+
);
112+
});
66113
});
67114

68115
testUtils.testWithClient('client.hScan', async client => {
@@ -73,5 +120,34 @@ describe('HSCAN', () => {
73120
tuples: []
74121
}
75122
);
123+
124+
assert.deepEqual(
125+
await client.hScan('key', 0, { NOVALUES: true }),
126+
{
127+
cursor: 0,
128+
keys: []
129+
}
130+
);
131+
132+
await Promise.all([
133+
client.hSet('key', 'a', '1'),
134+
client.hSet('key', 'b', '2')
135+
]);
136+
137+
assert.deepEqual(
138+
await client.hScan('key', 0),
139+
{
140+
cursor: 0,
141+
tuples: [{field: 'a', value: '1'}, {field: 'b', value: '2'}]
142+
}
143+
);
144+
145+
assert.deepEqual(
146+
await client.hScan('key', 0, { NOVALUES: true }),
147+
{
148+
cursor: 0,
149+
keys: ['a', 'b']
150+
}
151+
);
76152
}, GLOBAL.SERVERS.OPEN);
77153
});

Diff for: packages/client/lib/commands/HSCAN.ts

+20-13
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,27 @@ export interface HScanTuple {
2525

2626
interface HScanReply {
2727
cursor: number;
28-
tuples: Array<HScanTuple>;
28+
tuples?: Array<HScanTuple>;
29+
keys?: Array<RedisCommandArgument>;
2930
}
3031

31-
export function transformReply([cursor, rawTuples]: HScanRawReply): HScanReply {
32-
const parsedTuples = [];
33-
for (let i = 0; i < rawTuples.length; i += 2) {
34-
parsedTuples.push({
35-
field: rawTuples[i],
36-
value: rawTuples[i + 1]
37-
});
32+
export function transformReply([cursor, rawData]: HScanRawReply, noValues?: boolean): HScanReply {
33+
if (noValues === true) {
34+
return {
35+
cursor: Number(cursor),
36+
keys: [...rawData]
37+
};
38+
} else {
39+
const parsedTuples = [];
40+
for (let i = 0; i < rawData.length; i += 2) {
41+
parsedTuples.push({
42+
field: rawData[i],
43+
value: rawData[i + 1]
44+
});
45+
}
46+
return {
47+
cursor: Number(cursor),
48+
tuples: parsedTuples
49+
};
3850
}
39-
40-
return {
41-
cursor: Number(cursor),
42-
tuples: parsedTuples
43-
};
4451
}

Diff for: packages/client/lib/commands/generic-transformers.ts

+6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type BitValue = 0 | 1;
1313
export interface ScanOptions {
1414
MATCH?: string;
1515
COUNT?: number;
16+
NOVALUES?: boolean;
1617
}
1718

1819
export function pushScanArguments(
@@ -30,6 +31,11 @@ export function pushScanArguments(
3031
args.push('COUNT', options.COUNT.toString());
3132
}
3233

34+
if (options?.NOVALUES === true) {
35+
args.push('NOVALUES');
36+
args.preserve = true;
37+
}
38+
3339
return args;
3440
}
3541

0 commit comments

Comments
 (0)