Skip to content

Commit

Permalink
Support the NOVALUES option of HSCAN
Browse files Browse the repository at this point in the history
Issue #2705

The NOVALUES option instructs HSCAN to only return keys, without their
values.
  • Loading branch information
Gabriel Erzse committed Feb 27, 2024
1 parent dbf8f59 commit 58d419c
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 19 deletions.
20 changes: 18 additions & 2 deletions packages/client/lib/client/index.spec.ts
Expand Up @@ -8,6 +8,7 @@ import { defineScript } from '../lua-script';
import { spy } from 'sinon';
import { once } from 'events';
import { ClientKillFilters } from '../commands/CLIENT_KILL';
import { HScanTuple } from "../commands/HSCAN";
import { promisify } from 'util';

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

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

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

const results: Record<string, string> = {};
for await (const { field, value } of client.hScanIterator('key')) {
results[field] = value;
for await (const entry of client.hScanIterator('key')) {
const {field: field, value: value} = entry as HScanTuple;
results[field as string] = value as string;
}

assert.deepEqual(hash, results);

const keys: Array<string> = [];
for await (const entry of client.hScanIterator('key', { NOVALUES: true })) {
const key = entry as string;
keys.push(key);
}

function sort(a: string, b: string) {
return Number(b) - Number(a);
}

assert.deepEqual(keys.sort(sort), expectedKeys.sort(sort));
}, GLOBAL.SERVERS.OPEN);

testUtils.testWithClient('sScanIterator', async client => {
Expand Down
14 changes: 10 additions & 4 deletions packages/client/lib/client/index.ts
@@ -1,5 +1,5 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString, RedisCommands } from '../commands';
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString, RedisCommands, RedisCommandArgument } from '../commands';
import RedisSocket, { RedisSocketOptions, RedisTlsSocketOptions } from './socket';
import RedisCommandsQueue, { QueueCommandOptions } from './commands-queue';
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
Expand Down Expand Up @@ -809,13 +809,19 @@ export default class RedisClient<
} while (cursor !== 0);
}

async* hScanIterator(key: string, options?: ScanOptions): AsyncIterable<ConvertArgumentType<HScanTuple, string>> {
async* hScanIterator(key: string, options?: ScanOptions): AsyncIterable<ConvertArgumentType<HScanTuple | RedisCommandArgument, string>> {
let cursor = 0;
do {
const reply = await (this as any).hScan(key, cursor, options);
cursor = reply.cursor;
for (const tuple of reply.tuples) {
yield tuple;
if (options?.NOVALUES === true) {
for (const k of reply.keys) {
yield k;
}
} else {
for (const tuple of reply.tuples) {
yield tuple;
}
}
} while (cursor !== 0);
}
Expand Down
76 changes: 76 additions & 0 deletions packages/client/lib/commands/HSCAN.spec.ts
@@ -1,6 +1,7 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments, transformReply } from './HSCAN';
import { RedisCommandArguments } from "./index";

describe('HSCAN', () => {
describe('transformArguments', () => {
Expand Down Expand Up @@ -29,6 +30,18 @@ describe('HSCAN', () => {
);
});

it('with NOVALUES', () => {
const expectedReply: RedisCommandArguments = ['HSCAN', 'key', '0', 'NOVALUES'];
expectedReply.preserve = true;

assert.deepEqual(
transformArguments('key', 0, {
NOVALUES: true
}),
expectedReply
);
});

it('with MATCH & COUNT', () => {
assert.deepEqual(
transformArguments('key', 0, {
Expand All @@ -38,6 +51,20 @@ describe('HSCAN', () => {
['HSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1']
);
});

it('with MATCH & COUNT & NOVALUES', () => {
const expectedReply: RedisCommandArguments = ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1', 'NOVALUES'];
expectedReply.preserve = true;

assert.deepEqual(
transformArguments('key', 0, {
MATCH: 'pattern',
COUNT: 1,
NOVALUES: true
}),
expectedReply
);
});
});

describe('transformReply', () => {
Expand All @@ -51,6 +78,16 @@ describe('HSCAN', () => {
);
});

it('without keys', () => {
assert.deepEqual(
transformReply(['0', []], true),
{
cursor: 0,
keys: []
}
);
});

it('with tuples', () => {
assert.deepEqual(
transformReply(['0', ['field', 'value']]),
Expand All @@ -63,6 +100,16 @@ describe('HSCAN', () => {
}
);
});

it('with keys', () => {
assert.deepEqual(
transformReply(['0', ['key1', 'key2']], true),
{
cursor: 0,
keys: ['key1', 'key2']
}
);
});
});

testUtils.testWithClient('client.hScan', async client => {
Expand All @@ -73,5 +120,34 @@ describe('HSCAN', () => {
tuples: []
}
);

assert.deepEqual(
await client.hScan('key', 0, { NOVALUES: true }),
{
cursor: 0,
keys: []
}
);

await Promise.all([
client.hSet('key', 'a', '1'),
client.hSet('key', 'b', '2')
]);

assert.deepEqual(
await client.hScan('key', 0),
{
cursor: 0,
tuples: [{field: 'a', value: '1'}, {field: 'b', value: '2'}]
}
);

assert.deepEqual(
await client.hScan('key', 0, { NOVALUES: true }),
{
cursor: 0,
keys: ['a', 'b']
}
);
}, GLOBAL.SERVERS.OPEN);
});
33 changes: 20 additions & 13 deletions packages/client/lib/commands/HSCAN.ts
Expand Up @@ -25,20 +25,27 @@ export interface HScanTuple {

interface HScanReply {
cursor: number;
tuples: Array<HScanTuple>;
tuples?: Array<HScanTuple>;
keys?: Array<RedisCommandArgument>;
}

export function transformReply([cursor, rawTuples]: HScanRawReply): HScanReply {
const parsedTuples = [];
for (let i = 0; i < rawTuples.length; i += 2) {
parsedTuples.push({
field: rawTuples[i],
value: rawTuples[i + 1]
});
export function transformReply([cursor, rawData]: HScanRawReply, noValues?: boolean): HScanReply {
if (noValues === true) {
return {
cursor: Number(cursor),
keys: [...rawData]
};
} else {
const parsedTuples = [];
for (let i = 0; i < rawData.length; i += 2) {
parsedTuples.push({
field: rawData[i],
value: rawData[i + 1]
});
}
return {
cursor: Number(cursor),
tuples: parsedTuples
};
}

return {
cursor: Number(cursor),
tuples: parsedTuples
};
}
6 changes: 6 additions & 0 deletions packages/client/lib/commands/generic-transformers.ts
Expand Up @@ -13,6 +13,7 @@ export type BitValue = 0 | 1;
export interface ScanOptions {
MATCH?: string;
COUNT?: number;
NOVALUES?: boolean;
}

export function pushScanArguments(
Expand All @@ -30,6 +31,11 @@ export function pushScanArguments(
args.push('COUNT', options.COUNT.toString());
}

if (options?.NOVALUES === true) {
args.push('NOVALUES');
args.preserve = true;
}

return args;
}

Expand Down

0 comments on commit 58d419c

Please sign in to comment.