Skip to content

Commit

Permalink
Added zadd and zaddIncr in node.
Browse files Browse the repository at this point in the history
  • Loading branch information
Adan committed Jan 17, 2024
1 parent f610957 commit 46b184c
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 4 deletions.
68 changes: 68 additions & 0 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Buffer, BufferWriter, Reader, Writer } from "protobufjs";
import {
ExpireOptions,
SetOptions,
ZaddOptions,
createDecr,
createDecrBy,
createDel,
Expand Down Expand Up @@ -46,6 +47,7 @@ import {
createSet,
createTTL,
createUnlink,
createZadd,
} from "./Commands";
import {
ClosingError,
Expand Down Expand Up @@ -945,6 +947,72 @@ export class BaseClient {
return this.createWritePromise(scriptInvocation);
}

/** Adds members with their scores to the sorted set stored at `key`.
* If a member is already a part of the sorted set, its score is updated.
* See https://redis.io/commands/zadd/ for more details.
*
* @param key - The key of the sorted set.
* @param membersScoresMap - A mapping of members to their corresponding scores.
* @param options - The Zadd options.
* @param changed - Modify the return value from the number of new elements added, to the total number of elements changed.
* @returns The number of elements added to the sorted set.
* If `changed` is set, returns the number of elements updated in the sorted set.
* If there was a conflict with the options, the operation aborts and null is returned.
*
* @example
* await zadd("mySortedSet", \{ "member1": 10.5, "member2": 8.2 \})
* 2 (Indicates that two elements have been added or updated in the sorted set "mySortedSet".)
*
* await zadd("existingSortedSet", \{ member1: 15.0, member2: 5.5 \}, \{ conditionalChange: "onlyIfExists" \});
* 2 (Updates the scores of two existing members in the sorted set "existingSortedSet".)
*
*/
public zadd(
key: string,
membersScoresMap: Record<string, number>,
options?: ZaddOptions,
changed?: boolean
): Promise<number | null> {
return this.createWritePromise(
createZadd(
key,
membersScoresMap,
options,
changed ? "CH" : undefined
)
);
}

/** Increments the score of member in the sorted set stored at `key` by `increment`.
* If `member` does not exist in the sorted set, it is added with `increment` as its score (as if its previous score was 0.0).
* If `key` does not exist, a new sorted set with the specified member as its sole member is created.
* See https://redis.io/commands/zadd/ for more details.
*
* @param key - The key of the sorted set.
* @param member - A member in the sorted set to increment.
* @param increment - The score to increment the member.
* @param options - The Zadd options.
* @returns The score of the member.
* If there was a conflict with the options, the operation aborts and null is returned.
*
* @example
* await zaddIncr("mySortedSet", member , 5.0)
* 5.0 (Indicates that two elements have been added or updated in the sorted set "mySortedSet.")
*
* await zaddIncr("existingSortedSet", member , "3.0" , \{ UpdateOptions: "ScoreLessThanCurrent" \})
* null (Updates the scores of two existing members in the sorted set "existingSortedSet.")
*/
public zaddIncr(
key: string,
member: string,
increment: number,
options?: ZaddOptions
): Promise<number | null> {
return this.createWritePromise(
createZadd(key, { [member]: increment }, options, "INCR")
);
}

private readonly MAP_READ_FROM_STRATEGY: Record<
ReadFrom,
connection_request.ReadFrom
Expand Down
60 changes: 56 additions & 4 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
MAX_REQUEST_ARGS_LEN,
createLeakedStringVec,
} from "glide-rs";
import { MAX_REQUEST_ARGS_LEN, createLeakedStringVec } from "glide-rs";
import Long from "long";
import { redis_request } from "./ProtobufMessage";
import RequestType = redis_request.RequestType;
Expand Down Expand Up @@ -680,3 +677,58 @@ export function createPExpireAt(
export function createTTL(key: string): redis_request.Command {
return createCommand(RequestType.TTL, [key]);
}

export type ZaddOptions = {
/**
* `onlyIfDoesNotExist` - Only add new elements. Don't update already existing elements. Equivalent to `NX` in the Redis API.
* `onlyIfExists` - Only update elements that already exist. Don't add new elements. Equivalent to `XX` in the Redis API.
*/
conditionalChange?: "onlyIfExists" | "onlyIfDoesNotExist";
/**
* `ScoreLessThanCurrent` - Only update existing elements if the new score is less than the current score.
* Equivalent to `LT` in the Redis API.
* `ScoreGreaterThanCurrent` - Only update existing elements if the new score is greater than the current score.
* Equivalent to `GT` in the Redis API.
*/
UpdateOptions?: "ScoreLessThanCurrent" | "ScoreGreaterThanCurrent";
};

/**
* @internal
*/
export function createZadd(
key: string,
membersScoresMap: Record<string, number>,
options?: ZaddOptions,
changedOrIncr?: "CH" | "INCR"
): redis_request.Command {
let args = [key];
if (options) {
if (options.conditionalChange === "onlyIfExists") {
args.push("XX");
} else if (options.conditionalChange === "onlyIfDoesNotExist") {
if (options.UpdateOptions) {
throw new Error(
`The GT, LT, and NX options are mutually exclusive. Cannot choose both ${options.UpdateOptions} and NX.`
);
}
args.push("NX");
}

if (options.UpdateOptions === "ScoreLessThanCurrent") {
args.push("LT");
} else if (options.UpdateOptions === "ScoreGreaterThanCurrent") {
args.push("GT");
}
}
if (changedOrIncr) {
args.push(changedOrIncr);
}
args = args.concat(
Object.entries(membersScoresMap).flatMap(([key, value]) => [
value.toString(),
key,
])
);
return createCommand(RequestType.Zadd, args);
}
96 changes: 96 additions & 0 deletions node/tests/SharedTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,102 @@ export function runBaseTests<Context>(config: {
},
config.timeout
);

it(
"zadd and zaddIncr test",
async () => {
await runTest(async (client: BaseClient) => {
const key = uuidv4();
const membersScores = { one: 1, two: 2, three: 3 };

expect(await client.zadd(key, membersScores)).toEqual(3);
expect(await client.zaddIncr(key, "one", 2)).toEqual(3.0);
});
},
config.timeout
);

it(
"zadd and zaddIncr with NX XX test",
async () => {
await runTest(async (client: BaseClient) => {
const key = uuidv4();
const membersScores = { one: 1, two: 2, three: 3 };
expect(
await client.zadd(key, membersScores, {
conditionalChange: "onlyIfExists",
})
).toEqual(0);

expect(
await client.zadd(key, membersScores, {
conditionalChange: "onlyIfDoesNotExist",
})
).toEqual(3);

expect(
await client.zaddIncr(key, "one", 5.0, {
conditionalChange: "onlyIfDoesNotExist",
})
).toEqual(null);

expect(
await client.zaddIncr(key, "one", 5.0, {
conditionalChange: "onlyIfExists",
})
).toEqual(6.0);
});
},
config.timeout
);

it(
"zadd and zaddIncr with GT LT test",
async () => {
await runTest(async (client: BaseClient) => {
const key = uuidv4();
const membersScores = { one: -3, two: 2, three: 3 };

expect(await client.zadd(key, membersScores)).toEqual(3);
membersScores["one"] = 10;

expect(
await client.zadd(
key,
membersScores,
{
UpdateOptions: "ScoreGreaterThanCurrent",
},
true
)
).toEqual(1);

expect(
await client.zadd(
key,
membersScores,
{
UpdateOptions: "ScoreLessThanCurrent",
},
true
)
).toEqual(0);

expect(
await client.zaddIncr(key, "one", -3.0, {
UpdateOptions: "ScoreLessThanCurrent",
})
).toEqual(7.0);

expect(
await client.zaddIncr(key, "one", -3.0, {
UpdateOptions: "ScoreGreaterThanCurrent",
})
).toEqual(null);
});
},
config.timeout
);
}

export function runCommonTests<Context>(config: {
Expand Down

0 comments on commit 46b184c

Please sign in to comment.