From d91fbabd206683f7c541ab5cab79b41976c4db36 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:56:05 -0700 Subject: [PATCH] Node: add ZINTER and ZUNION commands (#2146) * Node: add ZINTER and ZUNION commands Signed-off-by: TJ Zhang * update tsdoc Signed-off-by: TJ Zhang * address commentes Signed-off-by: TJ Zhang * Address PR comments Signed-off-by: Jonathan Louie * Fix ESLint issues Signed-off-by: Jonathan Louie * Use createZCmdArgs instead of createZCmdStoreArgs for createZUnionStore Signed-off-by: Jonathan Louie * Fix Prettier suggestions Signed-off-by: Jonathan Louie --------- Signed-off-by: TJ Zhang Signed-off-by: Jonathan Louie Signed-off-by: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> Co-authored-by: TJ Zhang Co-authored-by: Jonathan Louie Co-authored-by: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 137 ++++++++- node/src/Commands.ts | 59 +++- node/src/Transaction.ts | 90 +++++- node/tests/GlideClusterClient.test.ts | 4 + node/tests/SharedTests.ts | 412 ++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 24 ++ 7 files changed, 717 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79c4a598eb..817b975922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ * Node: Added BZPOPMAX & BZPOPMIN command ([#2077]((https://github.com/valkey-io/valkey-glide/pull/2077)) * Node: Added XGROUP CREATECONSUMER & XGROUP DELCONSUMER commands ([#2088](https://github.com/valkey-io/valkey-glide/pull/2088)) * Node: Added GETEX command ([#2107]((https://github.com/valkey-io/valkey-glide/pull/2107)) +* Node: Added ZINTER and ZUNION commands ([#2146](https://github.com/aws/glide-for-redis/pull/2146)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index b231738d76..450eb9f774 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -196,6 +196,7 @@ import { createZDiffStore, createZDiffWithScores, createZIncrBy, + createZInter, createZInterCard, createZInterstore, createZLexCount, @@ -216,6 +217,7 @@ import { createZRevRankWithScore, createZScan, createZScore, + createZUnion, createZUnionStore, } from "./Commands"; import { @@ -3808,7 +3810,7 @@ export class BaseClient { /** * Computes the intersection of sorted sets given by the specified `keys` and stores the result in `destination`. * If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. - * To get the result directly, see `zinter_withscores`. + * To get the result directly, see {@link zinterWithScores}. * * @see {@link https://valkey.io/commands/zinterstore/|valkey.io} for more details. * @remarks When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. @@ -3817,7 +3819,8 @@ export class BaseClient { * @param keys - The keys of the sorted sets with possible formats: * string[] - for keys only. * KeyWeight[] - for weighted keys with score multipliers. - * @param aggregationType - Specifies the aggregation strategy to apply when combining the scores of elements. See `AggregationType`. + * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. + * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. * @returns The number of elements in the resulting sorted set stored at `destination`. * * @example @@ -3826,9 +3829,9 @@ export class BaseClient { * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}) * await client.zadd("key2", {"member1": 9.5}) * await client.zinterstore("my_sorted_set", ["key1", "key2"]) // Output: 1 - Indicates that the sorted set "my_sorted_set" contains one element. - * await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 20} - "member1" is now stored in "my_sorted_set" with score of 20. + * await client.zrangeWithScores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 20} - "member1" is now stored in "my_sorted_set" with score of 20. * await client.zinterstore("my_sorted_set", ["key1", "key2"] , AggregationType.MAX ) // Output: 1 - Indicates that the sorted set "my_sorted_set" contains one element, and it's score is the maximum score between the sets. - * await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 10.5} - "member1" is now stored in "my_sorted_set" with score of 10.5. + * await client.zrangeWithScores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 10.5} - "member1" is now stored in "my_sorted_set" with score of 10.5. * ``` */ public async zinterstore( @@ -3841,6 +3844,132 @@ export class BaseClient { ); } + /** + * Computes the intersection of sorted sets given by the specified `keys` and returns a list of intersecting elements. + * To get the scores as well, see {@link zinterWithScores}. + * To store the result in a key as a sorted set, see {@link zinterStore}. + * + * @remarks When in cluster mode, all keys in `keys` must map to the same hash slot. + * + * @remarks Since Valkey version 6.2.0. + * + * @see {@link https://valkey.io/commands/zinter/|valkey.io} for details. + * + * @param keys - The keys of the sorted sets. + * @returns The resulting array of intersecting elements. + * + * @example + * ```typescript + * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}); + * await client.zadd("key2", {"member1": 9.5}); + * const result = await client.zinter(["key1", "key2"]); + * console.log(result); // Output: ['member1'] + * ``` + */ + public zinter(keys: string[]): Promise { + return this.createWritePromise(createZInter(keys)); + } + + /** + * Computes the intersection of sorted sets given by the specified `keys` and returns a list of intersecting elements with scores. + * To get the elements only, see {@link zinter}. + * To store the result in a key as a sorted set, see {@link zinterStore}. + * + * @remarks When in cluster mode, all keys in `keys` must map to the same hash slot. + * + * @see {@link https://valkey.io/commands/zinter/|valkey.io} for details. + * + * @remarks Since Valkey version 6.2.0. + * + * @param keys - The keys of the sorted sets with possible formats: + * - string[] - for keys only. + * - KeyWeight[] - for weighted keys with score multipliers. + * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. + * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. + * @returns The resulting sorted set with scores. + * + * @example + * ```typescript + * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}); + * await client.zadd("key2", {"member1": 9.5}); + * const result1 = await client.zinterWithScores(["key1", "key2"]); + * console.log(result1); // Output: {'member1': 20} - "member1" with score of 20 is the result + * const result2 = await client.zinterWithScores(["key1", "key2"], AggregationType.MAX) + * console.log(result2); // Output: {'member1': 10.5} - "member1" with score of 10.5 is the result. + * ``` + */ + public zinterWithScores( + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): Promise> { + return this.createWritePromise( + createZInter(keys, aggregationType, true), + ); + } + + /** + * Computes the union of sorted sets given by the specified `keys` and returns a list of union elements. + * To get the scores as well, see {@link zunionWithScores}. + * + * To store the result in a key as a sorted set, see {@link zunionStore}. + * + * @remarks When in cluster mode, all keys in `keys` must map to the same hash slot. + * + * @remarks Since Valkey version 6.2.0. + * + * @see {@link https://valkey.io/commands/zunion/|valkey.io} for details. + * + * @param keys - The keys of the sorted sets. + * @returns The resulting array of union elements. + * + * @example + * ```typescript + * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}); + * await client.zadd("key2", {"member1": 9.5}); + * const result = await client.zunion(["key1", "key2"]); + * console.log(result); // Output: ['member1', 'member2'] + * ``` + */ + public zunion(keys: string[]): Promise { + return this.createWritePromise(createZUnion(keys)); + } + + /** + * Computes the intersection of sorted sets given by the specified `keys` and returns a list of union elements with scores. + * To get the elements only, see {@link zunion}. + * + * @remarks When in cluster mode, all keys in `keys` must map to the same hash slot. + * + * @see {@link https://valkey.io/commands/zunion/|valkey.io} for details. + * + * @remarks Since Valkey version 6.2.0. + * + * @param keys - The keys of the sorted sets with possible formats: + * - string[] - for keys only. + * - KeyWeight[] - for weighted keys with score multipliers. + * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. + * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. + * @returns The resulting sorted set with scores. + * + * @example + * ```typescript + * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}); + * await client.zadd("key2", {"member1": 9.5}); + * const result1 = await client.zunionWithScores(["key1", "key2"]); + * console.log(result1); // {'member1': 20, 'member2': 8.2} + * const result2 = await client.zunionWithScores(["key1", "key2"], "MAX"); + * console.log(result2); // {'member1': 10.5, 'member2': 8.2} + * ``` + */ + public zunionWithScores( + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): Promise> { + return this.createWritePromise( + createZUnion(keys, aggregationType, true), + ); + } + /** * Returns a random member from the sorted set stored at `key`. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index f5e5ac55a8..ba07f24a41 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1431,16 +1431,59 @@ export function createZInterstore( keys: string[] | KeyWeight[], aggregationType?: AggregationType, ): command_request.Command { - const args = createZCmdStoreArgs(destination, keys, aggregationType); + const args = createZCmdArgs(keys, { + aggregationType, + withScores: false, + destination, + }); return createCommand(RequestType.ZInterStore, args); } -function createZCmdStoreArgs( - destination: string, +/** + * @internal + */ +export function createZInter( keys: string[] | KeyWeight[], aggregationType?: AggregationType, + withScores?: boolean, +): command_request.Command { + const args = createZCmdArgs(keys, { aggregationType, withScores }); + return createCommand(RequestType.ZInter, args); +} + +/** + * @internal + */ +export function createZUnion( + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + withScores?: boolean, +): command_request.Command { + const args = createZCmdArgs(keys, { aggregationType, withScores }); + return createCommand(RequestType.ZUnion, args); +} + +/** + * @internal + * Helper function for Zcommands (ZInter, ZinterStore, ZUnion..) that arranges arguments in the server's required order. + */ +function createZCmdArgs( + keys: string[] | KeyWeight[], + options: { + aggregationType?: AggregationType; + withScores?: boolean; + destination?: string; + }, ): string[] { - const args: string[] = [destination, keys.length.toString()]; + const args = []; + + const destination = options.destination; + + if (destination) { + args.push(destination); + } + + args.push(keys.length.toString()); if (typeof keys[0] === "string") { args.push(...(keys as string[])); @@ -1451,10 +1494,16 @@ function createZCmdStoreArgs( args.push("WEIGHTS", ...weights); } + const aggregationType = options.aggregationType; + if (aggregationType) { args.push("AGGREGATE", aggregationType); } + if (options.withScores) { + args.push("WITHSCORES"); + } + return args; } @@ -1540,7 +1589,7 @@ export function createZUnionStore( keys: string[] | KeyWeight[], aggregationType?: AggregationType, ): command_request.Command { - const args = createZCmdStoreArgs(destination, keys, aggregationType); + const args = createZCmdArgs(keys, { destination, aggregationType }); return createCommand(RequestType.ZUnionStore, args); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 90f719bae2..df89635de6 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -232,6 +232,7 @@ import { createZDiffStore, createZDiffWithScores, createZIncrBy, + createZInter, createZInterCard, createZInterstore, createZLexCount, @@ -252,6 +253,7 @@ import { createZRevRankWithScore, createZScan, createZScore, + createZUnion, createZUnionStore, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -1954,11 +1956,15 @@ export class BaseTransaction> { * * @see {@link https://valkey.io/commands/zinterstore/|valkey.io} for details. * + * @remarks Since Valkey version 6.2.0. + * * @param destination - The key of the destination sorted set. * @param keys - The keys of the sorted sets with possible formats: * string[] - for keys only. * KeyWeight[] - for weighted keys with score multipliers. - * @param aggregationType - Specifies the aggregation strategy to apply when combining the scores of elements. See `AggregationType`. + * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. + * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. + * * Command Response - The number of elements in the resulting sorted set stored at `destination`. */ public zinterstore( @@ -1971,6 +1977,88 @@ export class BaseTransaction> { ); } + /** + * Computes the intersection of sorted sets given by the specified `keys` and returns a list of intersecting elements. + * To get the scores as well, see {@link zinterWithScores}. + * To store the result in a key as a sorted set, see {@link zinterStore}. + * + * @remarks Since Valkey version 6.2.0. + * + * @see {@link https://valkey.io/commands/zinter/|valkey.io} for details. + * + * @param keys - The keys of the sorted sets. + * + * Command Response - The resulting array of intersecting elements. + */ + public zinter(keys: string[]): T { + return this.addAndReturn(createZInter(keys)); + } + + /** + * Computes the intersection of sorted sets given by the specified `keys` and returns a list of intersecting elements with scores. + * To get the elements only, see {@link zinter}. + * To store the result in a key as a sorted set, see {@link zinterStore}. + * + * @see {@link https://valkey.io/commands/zinter/|valkey.io} for details. + * + * @remarks Since Valkey version 6.2.0. + * + * @param keys - The keys of the sorted sets with possible formats: + * - string[] - for keys only. + * - KeyWeight[] - for weighted keys with score multipliers. + * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. + * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. + * + * Command Response - The resulting sorted set with scores. + */ + public zinterWithScores( + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): T { + return this.addAndReturn(createZInter(keys, aggregationType, true)); + } + + /** + * Computes the union of sorted sets given by the specified `keys` and returns a list of union elements. + * To get the scores as well, see {@link zunionWithScores}. + * + * To store the result in a key as a sorted set, see {@link zunionStore}. + * + * @remarks Since Valkey version 6.2.0. + * + * @see {@link https://valkey.io/commands/zunion/|valkey.io} for details. + * + * @param keys - The keys of the sorted sets. + * + * Command Response - The resulting array of union elements. + */ + public zunion(keys: string[]): T { + return this.addAndReturn(createZUnion(keys)); + } + + /** + * Computes the intersection of sorted sets given by the specified `keys` and returns a list of union elements with scores. + * To get the elements only, see {@link zunion}. + * + * @see {@link https://valkey.io/commands/zunion/|valkey.io} for details. + * + * @remarks Since Valkey version 6.2.0. + * + * @param keys - The keys of the sorted sets with possible formats: + * - string[] - for keys only. + * - KeyWeight[] - for weighted keys with score multipliers. + * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. + * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. + * + * Command Response - The resulting sorted set with scores. + */ + public zunionWithScores( + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): T { + return this.addAndReturn(createZUnion(keys, aggregationType, true)); + } + /** * Returns a random member from the sorted set stored at `key`. * diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 65d39b1703..a4669086b5 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -406,6 +406,10 @@ describe("GlideClusterClient", () => { { radius: 5, unit: GeoUnit.METERS }, ), client.zrangeStore("abc", "zyx", { start: 0, stop: -1 }), + client.zinter(["abc", "zxy", "lkn"]), + client.zinterWithScores(["abc", "zxy", "lkn"]), + client.zunion(["abc", "zxy", "lkn"]), + client.zunionWithScores(["abc", "zxy", "lkn"]), ); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 23c65ba5ae..0443ccb837 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -4768,6 +4768,418 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinter basic test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + const resultZinter = await client.zinter([key1, key2]); + const expectedZinter = ["one", "two"]; + expect(resultZinter).toEqual(expectedZinter); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinter with scores basic test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + const resultZinterWithScores = await client.zinterWithScores([ + key1, + key2, + ]); + const expectedZinterWithScores = { + one: 2.5, + two: 4.5, + }; + expect(resultZinterWithScores).toEqual( + expectedZinterWithScores, + ); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinter with scores with max aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Intersection results are aggregated by the MAX score of elements + const zinterWithScoresResults = await client.zinterWithScores( + [key1, key2], + "MAX", + ); + const expectedMapMax = { + one: 1.5, + two: 2.5, + }; + expect(zinterWithScoresResults).toEqual(expectedMapMax); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinter with scores with min aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Intersection results are aggregated by the MIN score of elements + const zinterWithScoresResults = await client.zinterWithScores( + [key1, key2], + "MIN", + ); + const expectedMapMin = { + one: 1.0, + two: 2.0, + }; + expect(zinterWithScoresResults).toEqual(expectedMapMin); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinter with scores with sum aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Intersection results are aggregated by the SUM score of elements + const zinterWithScoresResults = await client.zinterWithScores( + [key1, key2], + "SUM", + ); + const expectedMapSum = { + one: 2.5, + two: 4.5, + }; + expect(zinterWithScoresResults).toEqual(expectedMapSum); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinter with scores with weights and aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Intersection results are aggregated by the SUM score of elements with weights + const zinterWithScoresResults = await client.zinterWithScores( + [ + [key1, 3], + [key2, 2], + ], + "SUM", + ); + const expectedMapSum = { + one: 6, + two: 11, + }; + expect(zinterWithScoresResults).toEqual(expectedMapSum); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinter empty test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + + // Non existing key zinter + expect( + await client.zinter([key1, "{testKey}-non_existing_key"]), + ).toEqual([]); + + // Non existing key zinterWithScores + expect( + await client.zinterWithScores([ + key1, + "{testKey}-non_existing_key", + ]), + ).toEqual({}); + + // Empty list check zinter + await expect(client.zinter([])).rejects.toThrow(); + + // Empty list check zinterWithScores + await expect(client.zinterWithScores([])).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunion basic test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + const resultZunion = await client.zunion([key1, key2]); + const expectedZunion = ["one", "two", "three"]; + + expect(resultZunion.sort()).toEqual(expectedZunion.sort()); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunion with scores basic test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + const resultZunionWithScores = await client.zunionWithScores([ + key1, + key2, + ]); + const expectedZunionWithScores = { + one: 2.5, + two: 4.5, + three: 3.5, + }; + expect(resultZunionWithScores).toEqual( + expectedZunionWithScores, + ); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunion with scores with max aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Union results are aggregated by the MAX score of elements + const zunionWithScoresResults = await client.zunionWithScores( + [key1, key2], + "MAX", + ); + const expectedMapMax = { + one: 1.5, + two: 2.5, + three: 3.5, + }; + expect(zunionWithScoresResults).toEqual(expectedMapMax); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunion with scores with min aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Union results are aggregated by the MIN score of elements + const zunionWithScoresResults = await client.zunionWithScores( + [key1, key2], + "MIN", + ); + const expectedMapMin = { + one: 1.0, + two: 2.0, + three: 3.5, + }; + expect(zunionWithScoresResults).toEqual(expectedMapMin); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunion with scores with sum aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Union results are aggregated by the SUM score of elements + const zunionWithScoresResults = await client.zunionWithScores( + [key1, key2], + "SUM", + ); + const expectedMapSum = { + one: 2.5, + two: 4.5, + three: 3.5, + }; + expect(zunionWithScoresResults).toEqual(expectedMapSum); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunion with scores with weights and aggregation test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Union results are aggregated by the SUM score of elements with weights + const zunionWithScoresResults = await client.zunionWithScores( + [ + [key1, 3], + [key2, 2], + ], + "SUM", + ); + const expectedMapSum = { + one: 6, + two: 11, + three: 7, + }; + expect(zunionWithScoresResults).toEqual(expectedMapSum); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zunion empty test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key1 = "{testKey}:1-" + uuidv4(); + + const membersScores1 = { one: 1.0, two: 2.0 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + + // Non existing key zunion + expect( + await client.zunion([key1, "{testKey}-non_existing_key"]), + ).toEqual(["one", "two"]); + + // Non existing key zunionWithScores + expect( + await client.zunionWithScores([ + key1, + "{testKey}-non_existing_key", + ]), + ).toEqual(membersScores1); + + // Empty list check zunion + await expect(client.zunion([])).rejects.toThrow(); + + // Empty list check zunionWithScores + await expect(client.zunionWithScores([])).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `type test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 2ac2cfda7a..1e6ada1a34 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -636,6 +636,8 @@ export async function transactionTest( const key23 = "{key}" + uuidv4(); // zset random const key24 = "{key}" + uuidv4(); // list value const key25 = "{key}" + uuidv4(); // Geospatial Data/ZSET + const key26 = "{key}" + uuidv4(); // sorted set + const key27 = "{key}" + uuidv4(); // sorted set const field = uuidv4(); const value = uuidv4(); const groupName1 = uuidv4(); @@ -996,6 +998,28 @@ export async function transactionTest( responseData.push(['zmscore(key12, ["two", "one"]', [2.0, 1.0]]); baseTransaction.zinterstore(key12, [key12, key13]); responseData.push(["zinterstore(key12, [key12, key13])", 0]); + + if (gte(version, "6.2.0")) { + baseTransaction.zadd(key26, { one: 1, two: 2 }); + responseData.push(["zadd(key26, { one: 1, two: 2 })", 2]); + baseTransaction.zadd(key27, { one: 1, two: 2, three: 3.5 }); + responseData.push([ + "zadd(key27, { one: 1, two: 2, three: 3.5 })", + 3, + ]); + baseTransaction.zinter([key27, key26]); + responseData.push(["zinter([key27, key26])", ["one", "two"]]); + baseTransaction.zinterWithScores([key27, key26]); + responseData.push([ + "zinterWithScores([key27, key26])", + { one: 2, two: 4 }, + ]); + baseTransaction.zunionWithScores([key27, key26]); + responseData.push([ + "zunionWithScores([key27, key26])", + { one: 2, two: 4, three: 3.5 }, + ]); + } } else { baseTransaction.zinterstore(key12, [key12, key13]); responseData.push(["zinterstore(key12, [key12, key13])", 2]);