From e3dd23d862109b9e57bd793e5413bd6e8d3b68fd Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:10:46 -0700 Subject: [PATCH] Add command ZRandMember (#2013) --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 91 +++++++++++++++++++++++- node/src/Commands.ts | 21 ++++++ node/src/Transaction.ts | 47 +++++++++++++ node/tests/SharedTests.ts | 133 ++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 12 ++++ 6 files changed, 304 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0370e1b3..3271ffb9d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,6 +136,7 @@ * Node: Added GeoDist command ([#1988](https://github.com/valkey-io/valkey-glide/pull/1988)) * Node: Added GeoHash command ([#1997](https://github.com/valkey-io/valkey-glide/pull/1997)) * Node: Added HStrlen command ([#2020](https://github.com/valkey-io/valkey-glide/pull/2020)) +* Node: Added ZRandMember command ([#2013](https://github.com/valkey-io/valkey-glide/pull/2013)) #### Breaking Changes * Node: Update XREAD to return a Map of Map ([#1494](https://github.com/valkey-io/valkey-glide/pull/1494)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 3d0a6c284c..78f7fc92b9 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1,7 +1,6 @@ /** * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ - import { DEFAULT_TIMEOUT_IN_MILLISECONDS, Script, @@ -144,6 +143,7 @@ import { createZMScore, createZPopMax, createZPopMin, + createZRandMember, createZRange, createZRangeWithScores, createZRank, @@ -193,6 +193,7 @@ export type ReturnType = | null | boolean | bigint + | Buffer | Set | ReturnTypeMap | ReturnTypeAttribute @@ -2840,6 +2841,94 @@ export class BaseClient { ); } + /** + * Returns a random member from the sorted set stored at `key`. + * + * See https://valkey.io/commands/zrandmember/ for more details. + * + * @param keys - The key of the sorted set. + * @returns A string representing a random member from the sorted set. + * If the sorted set does not exist or is empty, the response will be `null`. + * + * @example + * ```typescript + * const payload1 = await client.zrandmember("mySortedSet"); + * console.log(payload1); // Output: "Glide" (a random member from the set) + * ``` + * + * @example + * ```typescript + * const payload2 = await client.zrandmember("nonExistingSortedSet"); + * console.log(payload2); // Output: null since the sorted set does not exist. + * ``` + */ + public async zrandmember(key: string): Promise { + return this.createWritePromise(createZRandMember(key)); + } + + /** + * Returns random members from the sorted set stored at `key`. + * + * See https://valkey.io/commands/zrandmember/ for more details. + * + * @param keys - The key of the sorted set. + * @param count - The number of members to return. + * If `count` is positive, returns unique members. + * If negative, allows for duplicates. + * @returns An `array` of members from the sorted set. + * If the sorted set does not exist or is empty, the response will be an empty `array`. + * + * @example + * ```typescript + * const payload1 = await client.zrandmemberWithCount("mySortedSet", -3); + * console.log(payload1); // Output: ["Glide", "GLIDE", "node"] + * ``` + * + * @example + * ```typescript + * const payload2 = await client.zrandmemberWithCount("nonExistingKey", 3); + * console.log(payload1); // Output: [] since the sorted set does not exist. + * ``` + */ + public async zrandmemberWithCount( + key: string, + count: number, + ): Promise { + return this.createWritePromise(createZRandMember(key, count)); + } + + /** + * Returns random members with scores from the sorted set stored at `key`. + * + * See https://valkey.io/commands/zrandmember/ for more details. + * + * @param keys - The key of the sorted set. + * @param count - The number of members to return. + * If `count` is positive, returns unique members. + * If negative, allows for duplicates. + * @returns A 2D `array` of `[member, score]` `arrays`, where + * member is a `string` and score is a `number`. + * If the sorted set does not exist or is empty, the response will be an empty `array`. + * + * @example + * ```typescript + * const payload1 = await client.zrandmemberWithCountWithScore("mySortedSet", -3); + * console.log(payload1); // Output: [["Glide", 1.0], ["GLIDE", 1.0], ["node", 2.0]] + * ``` + * + * @example + * ```typescript + * const payload2 = await client.zrandmemberWithCountWithScore("nonExistingKey", 3); + * console.log(payload1); // Output: [] since the sorted set does not exist. + * ``` + */ + public async zrandmemberWithCountWithScores( + key: string, + count: number, + ): Promise<[string, number][]> { + return this.createWritePromise(createZRandMember(key, count, true)); + } + /** Returns the length of the string value stored at `key`. * See https://valkey.io/commands/strlen/ for more details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index e8377c9e47..19f8ceed2e 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2406,3 +2406,24 @@ export function createHStrlen( ): command_request.Command { return createCommand(RequestType.HStrlen, [key, field]); } + +/** + * @internal + */ +export function createZRandMember( + key: string, + count?: number, + withscores?: boolean, +): command_request.Command { + const args = [key]; + + if (count !== undefined) { + args.push(count.toString()); + } + + if (withscores) { + args.push("WITHSCORES"); + } + + return createCommand(RequestType.ZRandMember, args); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 24be54d0a6..073194bc20 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -160,6 +160,7 @@ import { createZMScore, createZPopMax, createZPopMin, + createZRandMember, createZRange, createZRangeWithScores, createZRank, @@ -1523,6 +1524,52 @@ export class BaseTransaction> { ); } + /** + * Returns a random member from the sorted set stored at `key`. + * + * See https://valkey.io/commands/zrandmember/ for more details. + * + * @param keys - The key of the sorted set. + * Command Response - A string representing a random member from the sorted set. + * If the sorted set does not exist or is empty, the response will be `null`. + */ + public zrandmember(key: string): T { + return this.addAndReturn(createZRandMember(key)); + } + + /** + * Returns random members from the sorted set stored at `key`. + * + * See https://valkey.io/commands/zrandmember/ for more details. + * + * @param keys - The key of the sorted set. + * @param count - The number of members to return. + * If `count` is positive, returns unique members. + * If negative, allows for duplicates. + * Command Response - An `array` of members from the sorted set. + * If the sorted set does not exist or is empty, the response will be an empty `array`. + */ + public zrandmemberWithCount(key: string, count: number): T { + return this.addAndReturn(createZRandMember(key, count)); + } + + /** + * Returns random members with scores from the sorted set stored at `key`. + * + * See https://valkey.io/commands/zrandmember/ for more details. + * + * @param keys - The key of the sorted set. + * @param count - The number of members to return. + * If `count` is positive, returns unique members. + * If negative, allows for duplicates. + * Command Response - A 2D `array` of `[member, score]` `arrays`, where + * member is a `string` and score is a `number`. + * If the sorted set does not exist or is empty, the response will be an empty `array`. + */ + public zrandmemberWithCountWithScores(key: string, count: number): T { + return this.addAndReturn(createZRandMember(key, count, true)); + } + /** Returns the string representation of the type of the value stored at `key`. * See https://valkey.io/commands/type/ for more details. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 203136507b..cb43d3ce51 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -5446,6 +5446,139 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zrandmember test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + + const memberScores = { one: 1.0, two: 2.0 }; + const elements = ["one", "two"]; + expect(await client.zadd(key1, memberScores)).toBe(2); + + // check random memember belongs to the set + const randmember = await client.zrandmember(key1); + + if (randmember !== null) { + checkSimple(randmember in elements).toEqual(true); + } + + // non existing key should return null + expect(await client.zrandmember("nonExistingKey")).toBeNull(); + + // Key exists, but is not a set + expect(await client.set(key2, "foo")).toBe("OK"); + await expect(client.zrandmember(key2)).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zrandmemberWithCount test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + + const memberScores = { one: 1.0, two: 2.0 }; + expect(await client.zadd(key1, memberScores)).toBe(2); + + // unique values are expected as count is positive + let randMembers = await client.zrandmemberWithCount(key1, 4); + expect(randMembers.length).toBe(2); + expect(randMembers.length).toEqual(new Set(randMembers).size); + + // Duplicate values are expected as count is negative + randMembers = await client.zrandmemberWithCount(key1, -4); + expect(randMembers.length).toBe(4); + const randMemberSet = new Set(); + + for (const member of randMembers) { + const memberStr = member + ""; + + if (!randMemberSet.has(memberStr)) { + randMemberSet.add(memberStr); + } + } + + expect(randMembers.length).not.toEqual(randMemberSet.size); + + // non existing key should return empty array + randMembers = await client.zrandmemberWithCount( + "nonExistingKey", + -4, + ); + expect(randMembers.length).toBe(0); + + // Key exists, but is not a set + expect(await client.set(key2, "foo")).toBe("OK"); + await expect( + client.zrandmemberWithCount(key2, 1), + ).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zrandmemberWithCountWithScores test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + + const memberScores = { one: 1.0, two: 2.0 }; + const memberScoreMap = new Map([ + ["one", 1.0], + ["two", 2.0], + ]); + expect(await client.zadd(key1, memberScores)).toBe(2); + + // unique values are expected as count is positive + let randMembers = await client.zrandmemberWithCountWithScores( + key1, + 4, + ); + + for (const member of randMembers) { + const key = String(member[0]); + const score = Number(member[1]); + expect(score).toEqual(memberScoreMap.get(key)); + } + + // Duplicate values are expected as count is negative + randMembers = await client.zrandmemberWithCountWithScores( + key1, + -4, + ); + expect(randMembers.length).toBe(4); + const keys = []; + + for (const member of randMembers) { + keys.push(String(member[0])); + } + + expect(randMembers.length).not.toEqual(new Set(keys).size); + + // non existing key should return empty array + randMembers = await client.zrandmemberWithCountWithScores( + "nonExistingKey", + -4, + ); + expect(randMembers.length).toBe(0); + + // Key exists, but is not a set + expect(await client.set(key2, "foo")).toBe("OK"); + await expect( + client.zrandmemberWithCount(key2, 1), + ).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 479c9da083..e13ba88666 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -450,6 +450,7 @@ export async function transactionTest( const key18 = "{key}" + uuidv4(); // Geospatial Data/ZSET const key19 = "{key}" + uuidv4(); // bitmap const key20 = "{key}" + uuidv4(); // list + const key21 = "{key}" + uuidv4(); // zset random const field = uuidv4(); const value = uuidv4(); // array of tuples - first element is test name/description, second - expected return value @@ -862,6 +863,17 @@ export async function transactionTest( 'geohash(key18, ["Palermo", "Catania", "NonExisting"])', ["sqc8b49rny0", "sqdtr74hyu0", null], ]); + baseTransaction.zadd(key21, { one: 1.0 }); + responseData.push(["zadd(key21, {one: 1.0}", 1]); + baseTransaction.zrandmember(key21); + responseData.push(["zrandmember(key21)", "one"]); + baseTransaction.zrandmemberWithCount(key21, 1); + responseData.push(["zrandmemberWithCountWithScores(key21, 1)", "one"]); + baseTransaction.zrandmemberWithCountWithScores(key21, 1); + responseData.push([ + "zrandmemberWithCountWithScores(key21, 1)", + [Buffer.from("one"), 1.0], + ]); if (gte("6.2.0", version)) { baseTransaction