Skip to content

Commit

Permalink
Node: add SUNIONSTORE command (valkey-io#1549)
Browse files Browse the repository at this point in the history
  • Loading branch information
aaron-congo authored Jun 10, 2024
1 parent 9caf33d commit d0a850d
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
22 changes: 22 additions & 0 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import {
createZRemRangeByRank,
createZRemRangeByScore,
createZScore,
createSUnionStore,
} from "./Commands";
import {
ClosingError,
Expand Down Expand Up @@ -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<number> {
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.
*
Expand Down
10 changes: 10 additions & 0 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
16 changes: 16 additions & 0 deletions node/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import {
createZRemRangeByRank,
createZRemRangeByScore,
createZScore,
createSUnionStore,
} from "./Commands";
import { redis_request } from "./ProtobufMessage";

Expand Down Expand Up @@ -723,6 +724,21 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
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.
*
Expand Down
1 change: 1 addition & 0 deletions node/tests/RedisClusterClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
];

Expand Down
60 changes: 60 additions & 0 deletions node/tests/SharedTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,66 @@ export function runBaseTests<Context>(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) => {
Expand Down
2 changes: 2 additions & 0 deletions node/tests/TestUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down

0 comments on commit d0a850d

Please sign in to comment.