diff --git a/CHANGELOG.md b/CHANGELOG.md index 205a3f91c5..427e34e3a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ * Node: Added ZINCRBY command ([#2009](https://github.com/valkey-io/valkey-glide/pull/2009)) * Node: Added BZMPOP command ([#2018](https://github.com/valkey-io/valkey-glide/pull/2018)) * Node: Added PFMERGE command ([#2053](https://github.com/valkey-io/valkey-glide/pull/2053)) +* Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 12997f2547..b64b44aa86 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -121,7 +121,8 @@ function initialize() { InsertPosition, SetOptions, ZaddOptions, - ScoreBoundry, + InfScoreBoundary, + ScoreBoundary, UpdateOptions, ProtocolVersion, RangeByIndex, @@ -204,7 +205,8 @@ function initialize() { InsertPosition, SetOptions, ZaddOptions, - ScoreBoundry, + InfScoreBoundary, + ScoreBoundary, UpdateOptions, ProtocolVersion, RangeByIndex, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 9e6a35e768..d87696ec90 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -152,6 +152,7 @@ import { createZIncrBy, createZInterCard, createZInterstore, + createZLexCount, createZMPop, createZMScore, createZPopMax, @@ -2855,7 +2856,7 @@ export class BaseClient { * @example * ```typescript * // Example usage of the zcount method to count members in a sorted set within a score range - * const result = await client.zcount("my_sorted_set", { bound: 5.0, isInclusive: true }, "positiveInfinity"); + * const result = await client.zcount("my_sorted_set", { bound: 5.0, isInclusive: true }, InfScoreBoundary.PositiveInfinity); * console.log(result); // Output: 2 - Indicates that there are 2 members with scores between 5.0 (inclusive) and +inf in the sorted set "my_sorted_set". * ``` * @@ -2899,7 +2900,7 @@ export class BaseClient { * ```typescript * // Example usage of zrange method to retrieve members within a score range in ascending order * const result = await client.zrange("my_sorted_set", { - * start: "negativeInfinity", + * start: InfScoreBoundary.NegativeInfinity, * stop: { value: 3, isInclusive: false }, * type: "byScore", * }); @@ -2941,7 +2942,7 @@ export class BaseClient { * ```typescript * // Example usage of zrangeWithScores method to retrieve members within a score range with their scores * const result = await client.zrangeWithScores("my_sorted_set", { - * start: "negativeInfinity", + * start: InfScoreBoundary.NegativeInfinity, * stop: { value: 3, isInclusive: false }, * type: "byScore", * }); @@ -3271,7 +3272,7 @@ export class BaseClient { * @example * ```typescript * // Example usage of zremRangeByScore method to remove members from a sorted set based on score range - * const result = await client.zremRangeByScore("my_sorted_set", { bound: 5.0, isInclusive: true }, "positiveInfinity"); + * const result = await client.zremRangeByScore("my_sorted_set", { bound: 5.0, isInclusive: true }, InfScoreBoundary.PositiveInfinity); * console.log(result); // Output: 2 - Indicates that 2 members with scores between 5.0 (inclusive) and +inf have been removed from the sorted set "my_sorted_set". * ``` * @@ -3292,6 +3293,38 @@ export class BaseClient { ); } + /** + * Returns the number of members in the sorted set stored at 'key' with scores between 'minLex' and 'maxLex'. + * + * See https://valkey.io/commands/zlexcount/ for more details. + * + * @param key - The key of the sorted set. + * @param minLex - The minimum lex to count from. Can be positive/negative infinity, or a specific lex and inclusivity. + * @param maxLex - The maximum lex to count up to. Can be positive/negative infinity, or a specific lex and inclusivity. + * @returns The number of members in the specified lex range. + * If 'key' does not exist, it is treated as an empty sorted set, and the command returns '0'. + * If maxLex is less than minLex, '0' is returned. + * + * @example + * ```typescript + * const result = await client.zlexcount("my_sorted_set", {value: "c"}, InfScoreBoundary.PositiveInfinity); + * console.log(result); // Output: 2 - Indicates that there are 2 members with lex scores between "c" (inclusive) and positive infinity in the sorted set "my_sorted_set". + * ``` + * + * @example + * ```typescript + * const result = await client.zlexcount("my_sorted_set", {value: "c"}, {value: "k", isInclusive: false}); + * console.log(result); // Output: 1 - Indicates that there is one member with a lex score between "c" (inclusive) and "k" (exclusive) in the sorted set "my_sorted_set". + * ``` + */ + public async zlexcount( + key: string, + minLex: ScoreBoundary, + maxLex: ScoreBoundary, + ): Promise { + return this.createWritePromise(createZLexCount(key, minLex, maxLex)); + } + /** Returns the rank of `member` in the sorted set stored at `key`, with scores ordered from low to high. * See https://valkey.io/commands/zrank for more details. * To get the rank of `member` with its score, see `zrankWithScore`. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index d8125adc36..0ce84f71b3 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1503,15 +1503,25 @@ export function createZMScore( return createCommand(RequestType.ZMScore, [key, ...members]); } -export type ScoreBoundary = +export enum InfScoreBoundary { /** * Positive infinity bound for sorted set. */ - | `positiveInfinity` + PositiveInfinity = "+", /** * Negative infinity bound for sorted set. */ - | `negativeInfinity` + NegativeInfinity = "-", +} + +/** + * Defines where to insert new elements into a list. + */ +export type ScoreBoundary = + /** + * Represents an lower/upper boundary in a sorted set. + */ + | InfScoreBoundary /** * Represents a specific numeric score boundary in a sorted set. */ @@ -1591,10 +1601,16 @@ function getScoreBoundaryArg( score: ScoreBoundary | ScoreBoundary, isLex: boolean = false, ): string { - if (score == "positiveInfinity") { - return isLex ? "+" : "+inf"; - } else if (score == "negativeInfinity") { - return isLex ? "-" : "-inf"; + if (score == InfScoreBoundary.PositiveInfinity) { + return ( + InfScoreBoundary.PositiveInfinity.toString() + (isLex ? "" : "inf") + ); + } + + if (score == InfScoreBoundary.NegativeInfinity) { + return ( + InfScoreBoundary.NegativeInfinity.toString() + (isLex ? "" : "inf") + ); } if (score.isInclusive == false) { @@ -1800,6 +1816,22 @@ export function createPersist(key: string): command_request.Command { return createCommand(RequestType.Persist, [key]); } +/** + * @internal + */ +export function createZLexCount( + key: string, + minLex: ScoreBoundary, + maxLex: ScoreBoundary, +): command_request.Command { + const args = [ + key, + getScoreBoundaryArg(minLex, true), + getScoreBoundaryArg(maxLex, true), + ]; + return createCommand(RequestType.ZLexCount, args); +} + export function createZRank( key: string, member: string, diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 1ab380a103..038f549a1c 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -172,6 +172,7 @@ import { createZIncrBy, createZInterCard, createZInterstore, + createZLexCount, createZMPop, createZMScore, createZPopMax, @@ -1793,6 +1794,27 @@ export class BaseTransaction> { ); } + /** + * Returns the number of members in the sorted set stored at 'key' with scores between 'minLex' and 'maxLex'. + * + * See https://valkey.io/commands/zlexcount/ for more details. + * + * @param key - The key of the sorted set. + * @param minLex - The minimum lex to count from. Can be positive/negative infinity, or a specific lex and inclusivity. + * @param maxLex - The maximum lex to count up to. Can be positive/negative infinity, or a specific lex and inclusivity. + * + * Command Response - The number of members in the specified lex range. + * If 'key' does not exist, it is treated as an empty sorted set, and the command returns '0'. + * If maxLex is less than minLex, '0' is returned. + */ + public zlexcount( + key: string, + minLex: ScoreBoundary, + maxLex: ScoreBoundary, + ): T { + return this.addAndReturn(createZLexCount(key, minLex, maxLex)); + } + /** Returns the rank of `member` in the sorted set stored at `key`, with scores ordered from low to high. * See https://valkey.io/commands/zrank for more details. * To get the rank of `member` with its score, see `zrankWithScore`. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index ffbcddcfa8..d2e45fb13d 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -27,6 +27,7 @@ import { GeospatialData, GlideClient, GlideClusterClient, + InfScoreBoundary, InfoOptions, InsertPosition, ListDirection, @@ -3261,8 +3262,8 @@ export function runBaseTests(config: { expect( await client.zcount( key1, - "negativeInfinity", - "positiveInfinity", + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, ), ).toEqual(3); expect( @@ -3280,26 +3281,38 @@ export function runBaseTests(config: { ), ).toEqual(2); expect( - await client.zcount(key1, "negativeInfinity", { - value: 3, - }), + await client.zcount( + key1, + InfScoreBoundary.NegativeInfinity, + { + value: 3, + }, + ), ).toEqual(3); expect( - await client.zcount(key1, "positiveInfinity", { - value: 3, - }), + await client.zcount( + key1, + InfScoreBoundary.PositiveInfinity, + { + value: 3, + }, + ), ).toEqual(0); expect( await client.zcount( "nonExistingKey", - "negativeInfinity", - "positiveInfinity", + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, ), ).toEqual(0); expect(await client.set(key2, "foo")).toEqual("OK"); await expect( - client.zcount(key2, "negativeInfinity", "positiveInfinity"), + client.zcount( + key2, + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, + ), ).rejects.toThrow(); }, protocol); }, @@ -3353,14 +3366,14 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: "negativeInfinity", + start: InfScoreBoundary.NegativeInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }), ).toEqual(["one", "two"]); const result = await client.zrangeWithScores(key, { - start: "negativeInfinity", - stop: "positiveInfinity", + start: InfScoreBoundary.NegativeInfinity, + stop: InfScoreBoundary.PositiveInfinity, type: "byScore", }); @@ -3376,7 +3389,7 @@ export function runBaseTests(config: { key, { start: { value: 3, isInclusive: false }, - stop: "negativeInfinity", + stop: InfScoreBoundary.NegativeInfinity, type: "byScore", }, true, @@ -3385,8 +3398,8 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: "negativeInfinity", - stop: "positiveInfinity", + start: InfScoreBoundary.NegativeInfinity, + stop: InfScoreBoundary.PositiveInfinity, limit: { offset: 1, count: 2 }, type: "byScore", }), @@ -3396,7 +3409,7 @@ export function runBaseTests(config: { await client.zrange( key, { - start: "negativeInfinity", + start: InfScoreBoundary.NegativeInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }, @@ -3406,7 +3419,7 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: "positiveInfinity", + start: InfScoreBoundary.PositiveInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }), @@ -3416,7 +3429,7 @@ export function runBaseTests(config: { await client.zrangeWithScores( key, { - start: "negativeInfinity", + start: InfScoreBoundary.NegativeInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }, @@ -3426,7 +3439,7 @@ export function runBaseTests(config: { expect( await client.zrangeWithScores(key, { - start: "positiveInfinity", + start: InfScoreBoundary.PositiveInfinity, stop: { value: 3, isInclusive: false }, type: "byScore", }), @@ -3446,7 +3459,7 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: "negativeInfinity", + start: InfScoreBoundary.NegativeInfinity, stop: { value: "c", isInclusive: false }, type: "byLex", }), @@ -3454,8 +3467,8 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: "negativeInfinity", - stop: "positiveInfinity", + start: InfScoreBoundary.NegativeInfinity, + stop: InfScoreBoundary.PositiveInfinity, limit: { offset: 1, count: 2 }, type: "byLex", }), @@ -3466,7 +3479,7 @@ export function runBaseTests(config: { key, { start: { value: "c", isInclusive: false }, - stop: "negativeInfinity", + stop: InfScoreBoundary.NegativeInfinity, type: "byLex", }, true, @@ -3477,7 +3490,7 @@ export function runBaseTests(config: { await client.zrange( key, { - start: "negativeInfinity", + start: InfScoreBoundary.NegativeInfinity, stop: { value: "c", isInclusive: false }, type: "byLex", }, @@ -3487,7 +3500,7 @@ export function runBaseTests(config: { expect( await client.zrange(key, { - start: "positiveInfinity", + start: InfScoreBoundary.PositiveInfinity, stop: { value: "c", isInclusive: false }, type: "byLex", }), @@ -4240,15 +4253,15 @@ export function runBaseTests(config: { await client.zremRangeByScore( key, { value: 1 }, - "negativeInfinity", + InfScoreBoundary.NegativeInfinity, ), ).toEqual(0); expect( await client.zremRangeByScore( "nonExistingKey", - "negativeInfinity", - "positiveInfinity", + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, ), ).toEqual(0); }, protocol); @@ -4256,6 +4269,80 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zlexcount test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const stringKey = uuidv4(); + const membersScores = { a: 1, b: 2, c: 3 }; + expect(await client.zadd(key, membersScores)).toEqual(3); + + // In range negative to positive infinity. + expect( + await client.zlexcount( + key, + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, + ), + ).toEqual(3); + + // In range a (exclusive) to positive infinity + expect( + await client.zlexcount( + key, + { value: "a", isInclusive: false }, + InfScoreBoundary.PositiveInfinity, + ), + ).toEqual(2); + + // In range negative infinity to c (inclusive) + expect( + await client.zlexcount( + key, + InfScoreBoundary.NegativeInfinity, + { + value: "c", + isInclusive: true, + }, + ), + ).toEqual(3); + + // Incorrect range start > end + expect( + await client.zlexcount( + key, + InfScoreBoundary.PositiveInfinity, + { + value: "c", + isInclusive: true, + }, + ), + ).toEqual(0); + + // Non-existing key + expect( + await client.zlexcount( + "non_existing_key", + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, + ), + ).toEqual(0); + + // Key exists, but it is not a set + expect(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.zlexcount( + stringKey, + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, + ), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "time test_%p", async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index af9c812825..610da75184 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -23,6 +23,7 @@ import { GeospatialData, GlideClient, GlideClusterClient, + InfScoreBoundary, InsertPosition, ListDirection, ProtocolVersion, @@ -727,8 +728,24 @@ export async function transactionTest( responseData.push(["zinterstore(key12, [key12, key13])", 2]); } - baseTransaction.zcount(key8, { value: 2 }, "positiveInfinity"); - responseData.push(['zcount(key8, { value: 2 }, "positiveInfinity")', 4]); + baseTransaction.zcount( + key8, + { value: 2 }, + InfScoreBoundary.PositiveInfinity, + ); + responseData.push([ + "zcount(key8, { value: 2 }, InfScoreBoundary.PositiveInfinity)", + 4, + ]); + baseTransaction.zlexcount( + key8, + { value: "a" }, + InfScoreBoundary.PositiveInfinity, + ); + responseData.push([ + 'zlexcount(key8, { value: "a" }, InfScoreBoundary.PositiveInfinity)', + 4, + ]); baseTransaction.zpopmin(key8); responseData.push(["zpopmin(key8)", { member2: 3.0 }]); baseTransaction.zpopmax(key8); @@ -737,8 +754,8 @@ export async function transactionTest( responseData.push(["zremRangeByRank(key8, 1, 1)", 1]); baseTransaction.zremRangeByScore( key8, - "negativeInfinity", - "positiveInfinity", + InfScoreBoundary.NegativeInfinity, + InfScoreBoundary.PositiveInfinity, ); responseData.push(["zremRangeByScore(key8, -Inf, +Inf)", 1]); // key8 is now empty