From c101a6d92a5bec09efee0eb6f80fecfad642abc7 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Fri, 9 Aug 2024 09:50:49 -0700 Subject: [PATCH] Node: Add `ZRANGESTORE` command (#2068) * Node: Add ZRANGESTORE command --------- Signed-off-by: Andrew Carbonetto --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 68 ++++++- node/src/Commands.ts | 16 ++ node/src/Transaction.ts | 59 ++++-- node/tests/GlideClusterClient.test.ts | 1 + node/tests/SharedTests.ts | 259 +++++++++++++++++++++++++- node/tests/TestUtilities.ts | 5 + 7 files changed, 380 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa7f24b6d7..83751c2ea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ * Node: Added WATCH and UNWATCH commands ([#2076](https://github.com/valkey-io/valkey-glide/pull/2076)) * Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022)) * Node: Added ZREMRANGEBYLEX command ([#2025](https://github.com/valkey-io/valkey-glide/pull/2025)) +* Node: Added ZRANGESTORE command ([#2068](https://github.com/valkey-io/valkey-glide/pull/2068)) * Node: Added SRANDMEMBER command ([#2067](https://github.com/valkey-io/valkey-glide/pull/2067)) * Node: Added XINFO STREAM command ([#2083](https://github.com/valkey-io/valkey-glide/pull/2083)) * Node: Added ZSCAN command ([#2061](https://github.com/valkey-io/valkey-glide/pull/2061)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index e93bcd2c2d..f2bbe54e4a 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -46,6 +46,7 @@ import { SearchOrigin, SetOptions, StreamAddOptions, + StreamClaimOptions, StreamGroupOptions, StreamReadOptions, StreamTrimOptions, @@ -160,6 +161,7 @@ import { createUnlink, createWatch, createXAdd, + createXClaim, createXDel, createXGroupCreate, createXGroupDestroy, @@ -186,6 +188,7 @@ import { createZPopMin, createZRandMember, createZRange, + createZRangeStore, createZRangeWithScores, createZRank, createZRem, @@ -196,8 +199,6 @@ import { createZRevRankWithScore, createZScan, createZScore, - StreamClaimOptions, - createXClaim, } from "./Commands"; import { ClosingError, @@ -3139,10 +3140,10 @@ export class BaseClient { * * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. - * For range queries by index (rank), use RangeByIndex. - * For range queries by lexicographical order, use RangeByLex. - * For range queries by score, use RangeByScore. - * @param reverse - If true, reverses the sorted set, with index 0 as the element with the highest score. + * - For range queries by index (rank), use {@link RangeByIndex}. + * - For range queries by lexicographical order, use {@link RangeByLex}. + * - For range queries by score, use {@link RangeByScore}. + * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. * @returns A list of elements within the specified range. * If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty array. * @@ -3177,10 +3178,10 @@ export class BaseClient { * * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. - * For range queries by index (rank), use RangeByIndex. - * For range queries by lexicographical order, use RangeByLex. - * For range queries by score, use RangeByScore. - * @param reverse - If true, reverses the sorted set, with index 0 as the element with the highest score. + * - For range queries by index (rank), use {@link RangeByIndex}. + * - For range queries by lexicographical order, use {@link RangeByLex}. + * - For range queries by score, use {@link RangeByScore}. + * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. * @returns A map of elements and their scores within the specified range. * If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. * @@ -3215,6 +3216,53 @@ export class BaseClient { ); } + /** + * Stores a specified range of elements from the sorted set at `source`, into a new + * sorted set at `destination`. If `destination` doesn't exist, a new sorted + * set is created; if it exists, it's overwritten. + * + * See https://valkey.io/commands/zrangestore/ for more details. + * + * @remarks When in cluster mode, `destination` and `source` must map to the same hash slot. + * @param destination - The key for the destination sorted set. + * @param source - The key of the source sorted set. + * @param rangeQuery - The range query object representing the type of range query to perform. + * - For range queries by index (rank), use {@link RangeByIndex}. + * - For range queries by lexicographical order, use {@link RangeByLex}. + * - For range queries by score, use {@link RangeByScore}. + * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. + * @returns The number of elements in the resulting sorted set. + * + * since - Redis version 6.2.0. + * + * @example + * ```typescript + * // Example usage of zrangeStore to retrieve and store all members of a sorted set in ascending order. + * const result = await client.zrangeStore("destination_key", "my_sorted_set", { start: 0, stop: -1 }); + * console.log(result); // Output: 7 - "destination_key" contains a sorted set with the 7 members from "my_sorted_set". + * ``` + * @example + * ```typescript + * // Example usage of zrangeStore method to retrieve members within a score range in ascending order and store in "destination_key" + * const result = await client.zrangeStore("destination_key", "my_sorted_set", { + * start: InfScoreBoundary.NegativeInfinity, + * stop: { value: 3, isInclusive: false }, + * type: "byScore", + * }); + * console.log(result); // Output: 5 - Stores 5 members with scores within the range of negative infinity to 3, in ascending order, in "destination_key". + * ``` + */ + public zrangeStore( + destination: string, + source: string, + rangeQuery: RangeByScore | RangeByLex | RangeByIndex, + reverse: boolean = false, + ): Promise { + return this.createWritePromise( + createZRangeStore(destination, source, rangeQuery, reverse), + ); + } + /** * 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. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 6f9715ddf4..d13c047461 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1737,6 +1737,22 @@ export function createZRangeWithScores( return createCommand(RequestType.ZRange, args); } +/** + * @internal + */ +export function createZRangeStore( + destination: string, + source: string, + rangeQuery: RangeByIndex | RangeByScore | RangeByLex, + reverse: boolean = false, +): command_request.Command { + const args = [ + destination, + ...createZRangeArgs(source, rangeQuery, reverse, false), + ]; + return createCommand(RequestType.ZRangeStore, args); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 937909f15b..0004c600a8 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -31,6 +31,7 @@ import { GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars GeoSearchResultOptions, GeoSearchShape, + GeoSearchStoreResultOptions, GeoUnit, GeospatialData, InfoOptions, @@ -99,6 +100,7 @@ import { createGeoHash, createGeoPos, createGeoSearch, + createGeoSearchStore, createGet, createGetBit, createGetDel, @@ -195,15 +197,15 @@ import { createXAdd, createXClaim, createXDel, + createXGroupCreate, + createXGroupCreateConsumer, + createXGroupDelConsumer, + createXGroupDestroy, createXInfoConsumers, createXInfoStream, createXLen, createXRead, createXTrim, - createXGroupCreate, - createXGroupDestroy, - createXGroupCreateConsumer, - createXGroupDelConsumer, createZAdd, createZCard, createZCount, @@ -220,6 +222,7 @@ import { createZPopMin, createZRandMember, createZRange, + createZRangeStore, createZRangeWithScores, createZRank, createZRem, @@ -230,8 +233,6 @@ import { createZRevRankWithScore, createZScan, createZScore, - createGeoSearchStore, - GeoSearchStoreResultOptions, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -1743,10 +1744,10 @@ export class BaseTransaction> { * * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. - * For range queries by index (rank), use RangeByIndex. - * For range queries by lexicographical order, use RangeByLex. - * For range queries by score, use RangeByScore. - * @param reverse - If true, reverses the sorted set, with index 0 as the element with the highest score. + * - For range queries by index (rank), use {@link RangeByIndex}. + * - For range queries by lexicographical order, use {@link RangeByLex}. + * - For range queries by score, use {@link RangeByScore}. + * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. * * Command Response - A list of elements within the specified range. * If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty array. @@ -1765,10 +1766,10 @@ export class BaseTransaction> { * * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. - * For range queries by index (rank), use RangeByIndex. - * For range queries by lexicographical order, use RangeByLex. - * For range queries by score, use RangeByScore. - * @param reverse - If true, reverses the sorted set, with index 0 as the element with the highest score. + * - For range queries by index (rank), use {@link RangeByIndex}. + * - For range queries by lexicographical order, use {@link RangeByLex}. + * - For range queries by score, use {@link RangeByScore}. + * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. * * Command Response - A map of elements and their scores within the specified range. * If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. @@ -1783,6 +1784,36 @@ export class BaseTransaction> { ); } + /** + * Stores a specified range of elements from the sorted set at `source`, into a new + * sorted set at `destination`. If `destination` doesn't exist, a new sorted + * set is created; if it exists, it's overwritten. + * + * See https://valkey.io/commands/zrangestore/ for more details. + * + * @param destination - The key for the destination sorted set. + * @param source - The key of the source sorted set. + * @param rangeQuery - The range query object representing the type of range query to perform. + * - For range queries by index (rank), use {@link RangeByIndex}. + * - For range queries by lexicographical order, use {@link RangeByLex}. + * - For range queries by score, use {@link RangeByScore}. + * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. + * + * Command Response - The number of elements in the resulting sorted set. + * + * since - Redis version 6.2.0. + */ + public zrangeStore( + destination: string, + source: string, + rangeQuery: RangeByScore | RangeByLex | RangeByIndex, + reverse: boolean = false, + ): T { + return this.addAndReturn( + createZRangeStore(destination, source, rangeQuery, reverse), + ); + } + /** * 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. diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 7165837804..db4b9f3282 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -368,6 +368,7 @@ describe("GlideClusterClient", () => { { member: "_" }, { radius: 5, unit: GeoUnit.METERS }, ), + client.zrangeStore("abc", "zyx", { start: 0, stop: -1 }), ); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index db27f80007..1bfdbada9d 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -3702,24 +3702,257 @@ export function runBaseTests(config: { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `zrange different typesn of keys test_%p`, + `zrangeStore by index test_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { - const key = uuidv4(); + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + + const key = "{testKey}:1-" + uuidv4(); + const destkey = "{testKey}:2-" + uuidv4(); + const membersScores = { one: 1, two: 2, three: 3 }; + expect(await client.zadd(key, membersScores)).toEqual(3); + expect( - await client.zrange("nonExistingKey", { + await client.zrangeStore(destkey, key, { + start: 0, + stop: 1, + }), + ).toEqual(2); + expect( + await client.zrange(destkey, { + start: 0, + stop: -1, + }), + ).toEqual(["one", "two"]); + + expect( + await client.zrangeStore( + destkey, + key, + { start: 0, stop: 1 }, + true, + ), + ).toEqual(2); + expect( + await client.zrange( + destkey, + { + start: 0, + stop: -1, + }, + true, + ), + ).toEqual(["three", "two"]); + + expect( + await client.zrangeStore(destkey, key, { + start: 3, + stop: 1, + }), + ).toEqual(0); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zrangeStore by score test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key = "{testKey}:1-" + uuidv4(); + const destkey = "{testKey}:2-" + uuidv4(); + const membersScores = { one: 1, two: 2, three: 3 }; + expect(await client.zadd(key, membersScores)).toEqual(3); + + expect( + await client.zrangeStore(destkey, key, { + start: InfScoreBoundary.NegativeInfinity, + stop: { value: 3, isInclusive: false }, + type: "byScore", + }), + ).toEqual(2); + expect( + await client.zrange(destkey, { + start: 0, + stop: -1, + }), + ).toEqual(["one", "two"]); + + expect( + await client.zrangeStore( + destkey, + key, + { + start: { value: 3, isInclusive: false }, + stop: InfScoreBoundary.NegativeInfinity, + type: "byScore", + }, + true, + ), + ).toEqual(2); + expect( + await client.zrange( + destkey, + { + start: 0, + stop: -1, + }, + true, + ), + ).toEqual(["two", "one"]); + + expect( + await client.zrangeStore(destkey, key, { + start: InfScoreBoundary.NegativeInfinity, + stop: InfScoreBoundary.PositiveInfinity, + limit: { offset: 1, count: 2 }, + type: "byScore", + }), + ).toEqual(2); + expect( + await client.zrange(destkey, { + start: 0, + stop: -1, + }), + ).toEqual(["two", "three"]); + + expect( + await client.zrangeStore( + destkey, + key, + { + start: InfScoreBoundary.NegativeInfinity, + stop: { value: 3, isInclusive: false }, + type: "byScore", + }, + true, + ), + ).toEqual(0); + + expect( + await client.zrangeStore(destkey, key, { + start: InfScoreBoundary.PositiveInfinity, + stop: { value: 3, isInclusive: false }, + type: "byScore", + }), + ).toEqual(0); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zrangeStore by lex test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + const key = "{testKey}:1-" + uuidv4(); + const destkey = "{testKey}:2-" + uuidv4(); + const membersScores = { a: 1, b: 2, c: 3 }; + expect(await client.zadd(key, membersScores)).toEqual(3); + + expect( + await client.zrangeStore(destkey, key, { + start: InfScoreBoundary.NegativeInfinity, + stop: { value: "c", isInclusive: false }, + type: "byLex", + }), + ).toEqual(2); + expect( + await client.zrange(destkey, { + start: 0, + stop: -1, + }), + ).toEqual(["a", "b"]); + + expect( + await client.zrangeStore(destkey, key, { + start: InfScoreBoundary.NegativeInfinity, + stop: InfScoreBoundary.PositiveInfinity, + limit: { offset: 1, count: 2 }, + type: "byLex", + }), + ).toEqual(2); + expect( + await client.zrange(destkey, { + start: 0, + stop: -1, + }), + ).toEqual(["b", "c"]); + + expect( + await client.zrangeStore( + destkey, + key, + { + start: { value: "c", isInclusive: false }, + stop: InfScoreBoundary.NegativeInfinity, + type: "byLex", + }, + true, + ), + ).toEqual(2); + expect( + await client.zrange( + destkey, + { + start: 0, + stop: -1, + }, + true, + ), + ).toEqual(["b", "a"]); + + expect( + await client.zrangeStore( + destkey, + key, + { + start: InfScoreBoundary.NegativeInfinity, + stop: { value: "c", isInclusive: false }, + type: "byLex", + }, + true, + ), + ).toEqual(0); + + expect( + await client.zrangeStore(destkey, key, { + start: InfScoreBoundary.PositiveInfinity, + stop: { value: "c", isInclusive: false }, + type: "byLex", + }), + ).toEqual(0); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zrange and zrangeStore different types of keys test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + const key = "{testKey}:1-" + uuidv4(); + const nonExistingKey = "{testKey}:2-" + uuidv4(); + const destkey = "{testKey}:3-" + uuidv4(); + + // test non-existing key - return an empty set + expect( + await client.zrange(nonExistingKey, { start: 0, stop: 1, }), ).toEqual([]); expect( - await client.zrangeWithScores("nonExistingKey", { + await client.zrangeWithScores(nonExistingKey, { start: 0, stop: 1, }), ).toEqual({}); + // test against a non-sorted set - throw RequestError expect(await client.set(key, "value")).toEqual("OK"); await expect( @@ -3729,6 +3962,22 @@ export function runBaseTests(config: { await expect( client.zrangeWithScores(key, { start: 0, stop: 1 }), ).rejects.toThrow(); + + // test zrangeStore - added in version 6.2.0 + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + + // test non-existing key - stores an empty set + expect( + await client.zrangeStore(destkey, nonExistingKey, { + start: 0, + stop: 1, + }), + ).toEqual(0); + + // test against a non-sorted set - throw RequestError + await expect( + client.zrangeStore(destkey, key, { start: 0, stop: 1 }), + ).rejects.toThrow(); }, protocol); }, config.timeout, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index ef044bd008..ec2090f520 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -825,6 +825,11 @@ export async function transactionTest( responseData.push(["zadd(key13, { one: 1, two: 2, three: 3.5 })", 3]); if (gte(version, "6.2.0")) { + baseTransaction.zrangeStore(key8, key8, { start: 0, stop: -1 }); + responseData.push([ + "zrangeStore(key8, key8, { start: 0, stop: -1 })", + 4, + ]); baseTransaction.zdiff([key13, key12]); responseData.push(["zdiff([key13, key12])", ["three"]]); baseTransaction.zdiffWithScores([key13, key12]);