Skip to content

Commit

Permalink
Node: added zrange and zrangeWithScores commands. (#1115)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Adan <[email protected]>
Co-authored-by: Shoham Elias <[email protected]>
Co-authored-by: Shoham Elias <[email protected]>
  • Loading branch information
4 people authored Apr 10, 2024
1 parent 79c49a4 commit 4e7acdc
Show file tree
Hide file tree
Showing 7 changed files with 536 additions and 37 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Python: Added HKEYS command ([#1228](https://github.com/aws/glide-for-redis/pull/1228))
* Python: Added ZREMRANGEBYSCORE command ([#1151](https://github.com/aws/glide-for-redis/pull/1151))
* Node: Added SPOP, SPOPCOUNT commands. ([#1117](https://github.com/aws/glide-for-redis/pull/1117))
* Node: Added ZRANGE command ([#1115](https://github.com/aws/glide-for-redis/pull/1115))
* Python: Added RENAME command ([#1252](https://github.com/aws/glide-for-redis/pull/1252))

#### Fixes
Expand Down
97 changes: 92 additions & 5 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import * as net from "net";
import { Buffer, BufferWriter, Reader, Writer } from "protobufjs";
import {
ExpireOptions,
ScoreLimit,
RangeByIndex,
RangeByLex,
RangeByScore,
ScoreBoundary,
SetOptions,
StreamAddOptions,
StreamReadOptions,
Expand Down Expand Up @@ -75,6 +78,8 @@ import {
createZcount,
createZpopmax,
createZpopmin,
createZrange,
createZrangeWithScores,
createZrank,
createZrem,
createZremRangeByRank,
Expand Down Expand Up @@ -1669,12 +1674,94 @@ export class BaseClient {
*/
public zcount(
key: string,
minScore: ScoreLimit,
maxScore: ScoreLimit,
minScore: ScoreBoundary<number>,
maxScore: ScoreBoundary<number>,
): Promise<number> {
return this.createWritePromise(createZcount(key, minScore, maxScore));
}

/** Returns the specified range of elements in the sorted set stored at `key`.
* ZRANGE can perform different types of range queries: by index (rank), by the score, or by lexicographical order.
*
* See https://redis.io/commands/zrange/ for more details.
* To get the elements with their scores, see `zrangeWithScores`.
*
* @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.
* @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.
*
* @example
* ```typescript
* // Example usage of zrange method to retrieve all members of a sorted set in ascending order
* const result = await client.zrange("my_sorted_set", { start: 0, stop: -1 });
* console.log(result1); // Output: ['member1', 'member2', 'member3'] - Returns all members in ascending order.
*
* @example
* // 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",
* stop: { value: 3, isInclusive: false },
* type: "byScore",
* });
* console.log(result); // Output: ['member2', 'member3'] - Returns members with scores within the range of negative infinity to 3, in ascending order.
* ```
*/
public zrange(
key: string,
rangeQuery: RangeByScore | RangeByLex | RangeByIndex,
reverse: boolean = false,
): Promise<string[]> {
return this.createWritePromise(createZrange(key, rangeQuery, reverse));
}

/** Returns the specified range of elements with their scores in the sorted set stored at `key`.
* Similar to ZRANGE but with a WITHSCORE flag.
* See https://redis.io/commands/zrange/ for more details.
*
* @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.
* @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.
*
* @example
* ```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: { value: 10, isInclusive: false },
* stop: { value: 20, isInclusive: false },
* type: "byScore",
* });
* console.log(result); // Output: {'member1': 10.5, 'member2': 15.2} - Returns members with scores between 10 and 20 with their scores.
*
* @example
* // 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",
* stop: { value: 3, isInclusive: false },
* type: "byScore",
* });
* console.log(result); // Output: {'member4': -2.0, 'member7': 1.5} - Returns members with scores within the range of negative infinity to 3, with their scores.
* ```
*/
public zrangeWithScores(
key: string,
rangeQuery: RangeByScore | RangeByLex | RangeByIndex,
reverse: boolean = false,
): Promise<Record<string, number>> {
return this.createWritePromise(
createZrangeWithScores(key, rangeQuery, reverse),
);
}

/** Returns the length of the string value stored at `key`.
* See https://redis.io/commands/strlen/ for more details.
*
Expand Down Expand Up @@ -1876,8 +1963,8 @@ export class BaseClient {
*/
public zremRangeByScore(
key: string,
minScore: ScoreLimit,
maxScore: ScoreLimit,
minScore: ScoreBoundary<number>,
maxScore: ScoreBoundary<number>,
): Promise<number> {
return this.createWritePromise(
createZremRangeByScore(key, minScore, maxScore),
Expand Down
173 changes: 156 additions & 17 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -811,42 +811,181 @@ export function createZscore(
return createCommand(RequestType.ZScore, [key, member]);
}

export type ScoreLimit =
export type ScoreBoundary<T> =
/**
* Positive infinity bound for sorted set.
*/
| `positiveInfinity`
/**
* Negative infinity bound for sorted set.
*/
| `negativeInfinity`
/**
* Represents a specific numeric score boundary in a sorted set.
*/
| {
bound: number;
/**
* The score value.
*/
value: T;
/**
* Whether the score value is inclusive. Defaults to True.
*/
isInclusive?: boolean;
};

function getScoreLimitArg(score: ScoreLimit): string {
/**
* Represents a range by index (rank) in a sorted set.
* The `start` and `stop` arguments represent zero-based indexes.
*/
export type RangeByIndex = {
/**
* The start index of the range.
*/
start: number;
/**
* The stop index of the range.
*/
stop: number;
};

/**
* Represents a range by score or a range by lex in a sorted set.
* The `start` and `stop` arguments represent score boundaries.
*/
type SortedSetRange<T> = {
/**
* The start boundary.
*/
start: ScoreBoundary<T>;
/**
* The stop boundary.
*/
stop: ScoreBoundary<T>;
/**
* The limit argument for a range query.
* Represents a limit argument for a range query in a sorted set to
* be used in [ZRANGE](https://redis.io/commands/zrange) command.
*
* The optional LIMIT argument can be used to obtain a sub-range from the matching elements
* (similar to SELECT LIMIT offset, count in SQL).
*/
limit?: {
/**
* The offset from the start of the range.
*/
offset: number;
/**
* The number of elements to include in the range.
* A negative count returns all elements from the offset.
*/
count: number;
};
};

export type RangeByScore = SortedSetRange<number> & { type: "byScore" };
export type RangeByLex = SortedSetRange<string> & { type: "byLex" };

/**
* Returns a string representation of a score boundary in Redis protocol format.
* @param score - The score boundary object containing value and inclusivity information.
* @param isLex - Indicates whether to return lexical representation for positive/negative infinity.
* @returns A string representation of the score boundary in Redis protocol format.
*/
function getScoreBoundaryArg(
score: ScoreBoundary<number> | ScoreBoundary<string>,
isLex: boolean = false,
): string {
if (score == "positiveInfinity") {
return "+inf";
return isLex ? "+" : "+inf";
} else if (score == "negativeInfinity") {
return "-inf";
return isLex ? "-" : "-inf";
}

if (score.isInclusive == false) {
return "(" + score.value.toString();
}

const value =
score.isInclusive == false
? "(" + score.bound.toString()
: score.bound.toString();
const value = isLex ? "[" + score.value.toString() : score.value.toString();
return value;
}

function createZrangeArgs(
key: string,
rangeQuery: RangeByScore | RangeByLex | RangeByIndex,
reverse: boolean,
withScores: boolean,
): string[] {
const args: string[] = [key];

if (typeof rangeQuery.start != "number") {
rangeQuery = rangeQuery as RangeByScore | RangeByLex;
const isLex = rangeQuery.type == "byLex";
args.push(getScoreBoundaryArg(rangeQuery.start, isLex));
args.push(getScoreBoundaryArg(rangeQuery.stop, isLex));
args.push(isLex == true ? "BYLEX" : "BYSCORE");
} else {
args.push(rangeQuery.start.toString());
args.push(rangeQuery.stop.toString());
}

if (reverse) {
args.push("REV");
}

if ("limit" in rangeQuery && rangeQuery.limit !== undefined) {
args.push(
"LIMIT",
String(rangeQuery.limit.offset),
String(rangeQuery.limit.count),
);
}

if (withScores) {
args.push("WITHSCORES");
}

return args;
}

/**
* @internal
*/
export function createZcount(
key: string,
minScore: ScoreLimit,
maxScore: ScoreLimit,
minScore: ScoreBoundary<number>,
maxScore: ScoreBoundary<number>,
): redis_request.Command {
const args = [key];
args.push(getScoreLimitArg(minScore));
args.push(getScoreLimitArg(maxScore));
args.push(getScoreBoundaryArg(minScore));
args.push(getScoreBoundaryArg(maxScore));
return createCommand(RequestType.Zcount, args);
}

/**
* @internal
*/
export function createZrange(
key: string,
rangeQuery: RangeByIndex | RangeByScore | RangeByLex,
reverse: boolean = false,
): redis_request.Command {
const args = createZrangeArgs(key, rangeQuery, reverse, false);
return createCommand(RequestType.Zrange, args);
}

/**
* @internal
*/
export function createZrangeWithScores(
key: string,
rangeQuery: RangeByIndex | RangeByScore | RangeByLex,
reverse: boolean = false,
): redis_request.Command {
const args = createZrangeArgs(key, rangeQuery, reverse, true);
return createCommand(RequestType.Zrange, args);
}

/**
* @internal
*/
Expand Down Expand Up @@ -927,12 +1066,12 @@ export function createZremRangeByRank(
*/
export function createZremRangeByScore(
key: string,
minScore: ScoreLimit,
maxScore: ScoreLimit,
minScore: ScoreBoundary<number>,
maxScore: ScoreBoundary<number>,
): redis_request.Command {
const args = [key];
args.push(getScoreLimitArg(minScore));
args.push(getScoreLimitArg(maxScore));
args.push(getScoreBoundaryArg(minScore));
args.push(getScoreBoundaryArg(maxScore));
return createCommand(RequestType.ZRemRangeByScore, args);
}

Expand Down
Loading

0 comments on commit 4e7acdc

Please sign in to comment.