From d0a850d7a6a36f5e7867e06c931dcf81869dccc5 Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:42:30 -0700 Subject: [PATCH] Node: add SUNIONSTORE command (#1549) --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 22 ++++++++++ node/src/Commands.ts | 10 +++++ node/src/Transaction.ts | 16 +++++++ node/tests/RedisClusterClient.test.ts | 1 + node/tests/SharedTests.ts | 60 +++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 2 + 7 files changed, 112 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cccb3c3026..2d3325a4ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * Python: Added SORT command ([#1439](https://github.com/aws/glide-for-redis/pull/1439)) * Node: Added OBJECT ENCODING command ([#1518](https://github.com/aws/glide-for-redis/pull/1518)) * Python: Added LMOVE and BLMOVE commands ([#1536](https://github.com/aws/glide-for-redis/pull/1536)) +* Node: Added SUNIONSTORE command ([#1549](https://github.com/aws/glide-for-redis/pull/1549)) ### Breaking Changes * Node: Update XREAD to return a Map of Map ([#1494](https://github.com/aws/glide-for-redis/pull/1494)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 8a5476c0c4..c748707efe 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -94,6 +94,7 @@ import { createZRemRangeByRank, createZRemRangeByScore, createZScore, + createSUnionStore, } from "./Commands"; import { ClosingError, @@ -1303,6 +1304,27 @@ export class BaseClient { ); } + /** + * Stores the members of the union of all given sets specified by `keys` into a new set + * at `destination`. + * + * See https://valkey.io/commands/sunionstore/ for details. + * + * @remarks When in cluster mode, `destination` and all `keys` must map to the same hash slot. + * @param destination - The key of the destination set. + * @param keys - The keys from which to retrieve the set members. + * @returns The number of elements in the resulting set. + * + * @example + * ```typescript + * const length = await client.sunionstore("mySet", ["set1", "set2"]); + * console.log(length); // Output: 2 - Two elements were stored in "mySet", and those two members are the union of "set1" and "set2". + * ``` + */ + public sunionstore(destination: string, keys: string[]): Promise { + return this.createWritePromise(createSUnionStore(destination, keys)); + } + /** Returns if `member` is a member of the set stored at `key`. * See https://redis.io/commands/sismember/ for more details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index a4e898d3bf..68a8a4ac23 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -599,6 +599,16 @@ export function createSInter(keys: string[]): redis_request.Command { return createCommand(RequestType.SInter, keys); } +/** + * @internal + */ +export function createSUnionStore( + destination: string, + keys: string[], +): redis_request.Command { + return createCommand(RequestType.SUnionStore, [destination].concat(keys)); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index c429743bfc..09d4f65c28 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -99,6 +99,7 @@ import { createZRemRangeByRank, createZRemRangeByScore, createZScore, + createSUnionStore, } from "./Commands"; import { redis_request } from "./ProtobufMessage"; @@ -723,6 +724,21 @@ export class BaseTransaction> { return this.addAndReturn(createSInter(keys), true); } + /** + * Stores the members of the union of all given sets specified by `keys` into a new set + * at `destination`. + * + * See https://valkey.io/commands/sunionstore/ for details. + * + * @param destination - The key of the destination set. + * @param keys - The keys from which to retrieve the set members. + * + * Command Response - The number of elements in the resulting set. + */ + public sunionstore(destination: string, keys: string[]): T { + return this.addAndReturn(createSUnionStore(destination, keys)); + } + /** Returns if `member` is a member of the set stored at `key`. * See https://redis.io/commands/sismember/ for more details. * diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index cbe5254b44..894df81d08 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -289,6 +289,7 @@ describe("RedisClusterClient", () => { client.renamenx("abc", "zxy"), client.sinter(["abc", "zxy", "lkn"]), client.zinterstore("abc", ["zxy", "lkn"]), + client.sunionstore("abc", ["zxy", "lkn"]), // TODO all rest multi-key commands except ones tested below ]; diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 52b43589af..35630c58a4 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1160,6 +1160,66 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `sunionstore test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = `{key}:${uuidv4()}`; + const key2 = `{key}:${uuidv4()}`; + const key3 = `{key}:${uuidv4()}`; + const key4 = `{key}:${uuidv4()}`; + const stringKey = `{key}:${uuidv4()}`; + const nonExistingKey = `{key}:${uuidv4()}`; + + expect(await client.sadd(key1, ["a", "b", "c"])).toEqual(3); + expect(await client.sadd(key2, ["c", "d", "e"])).toEqual(3); + expect(await client.sadd(key3, ["e", "f", "g"])).toEqual(3); + + // store union in new key + expect(await client.sunionstore(key4, [key1, key2])).toEqual(5); + expect(await client.smembers(key4)).toEqual( + new Set(["a", "b", "c", "d", "e"]), + ); + + // overwrite existing set + expect(await client.sunionstore(key1, [key4, key2])).toEqual(5); + expect(await client.smembers(key1)).toEqual( + new Set(["a", "b", "c", "d", "e"]), + ); + + // overwrite one of the source keys + expect(await client.sunionstore(key2, [key4, key2])).toEqual(5); + expect(await client.smembers(key2)).toEqual( + new Set(["a", "b", "c", "d", "e"]), + ); + + // union with a non-existing key + expect( + await client.sunionstore(key2, [nonExistingKey]), + ).toEqual(0); + expect(await client.smembers(key2)).toEqual(new Set()); + + // invalid argument - key list must not be empty + await expect(client.sunionstore(key4, [])).rejects.toThrow(); + + // key exists, but it is not a set + expect(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.sunionstore(key4, [stringKey, key1]), + ).rejects.toThrow(); + + // overwrite destination when destination is not a set + expect( + await client.sunionstore(stringKey, [key1, key3]), + ).toEqual(7); + expect(await client.smembers(stringKey)).toEqual( + new Set(["a", "b", "c", "d", "e", "f", "g"]), + ); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `sismember test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index e39831c193..011b09ce34 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -305,6 +305,8 @@ export async function transactionTest( args.push([field + "2", field + "1"]); baseTransaction.sadd(key7, ["bar", "foo"]); args.push(2); + baseTransaction.sunionstore(key7, [key7, key7]); + args.push(2); baseTransaction.sinter([key7, key7]); args.push(new Set(["bar", "foo"])); baseTransaction.srem(key7, ["foo"]);