Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node: added zrange and zrangeWithScores commands. #1115

Merged
merged 4 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:/aws/glide-for-redis/pull/1228))
* Python: Added ZREMRANGEBYSCORE command ([#1151](https:/aws/glide-for-redis/pull/1151))
* Node: Added SPOP, SPOPCOUNT commands. ([#1117](https:/aws/glide-for-redis/pull/1117))
* Node: Added ZRANGE command ([#1115](https:/aws/glide-for-redis/pull/1115))
* Python: Added RENAME command ([#1252](https:/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.
shohamazon marked this conversation as resolved.
Show resolved Hide resolved
*
* @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.
shohamazon marked this conversation as resolved.
Show resolved Hide resolved
*
* @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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add documentation, this function isn't trivial

): 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
Loading