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

Add SMOVE command for node #1476

Merged
merged 5 commits into from
May 30, 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
28 changes: 28 additions & 0 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
createSCard,
createSIsMember,
createSMembers,
createSMove,
createSPop,
createSRem,
createSet,
Expand Down Expand Up @@ -1225,6 +1226,33 @@ export class BaseClient {
);
}

/** Moves `member` from the set at `source` to the set at `destination`, removing it from the source set.
* Creates a new destination set if needed. The operation is atomic.
* See https://valkey.io/commands/smove for more details.
*
adarovadya marked this conversation as resolved.
Show resolved Hide resolved
* @remarks When in cluster mode, `source` and `destination` must map to the same hash slot.
*
* @param source - The key of the set to remove the element from.
* @param destination - The key of the set to add the element to.
* @param member - The set element to move.
* @returns `true` on success, or `false` if the `source` set does not exist or the element is not a member of the source set.
*
* @example
* ```typescript
* const result = await client.smove("set1", "set2", "member1");
* console.log(result); // Output: true - "member1" was moved from "set1" to "set2".
* ```
*/
public smove(
source: string,
destination: string,
member: string,
): Promise<boolean> {
return this.createWritePromise(
createSMove(source, destination, member),
);
}

/** Returns the set cardinality (number of elements) of the set stored at `key`.
* See https://redis.io/commands/scard/ for details.
*
Expand Down
12 changes: 12 additions & 0 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,18 @@ export function createSMembers(key: string): redis_request.Command {
return createCommand(RequestType.SMembers, [key]);
}

/**
*
* @internal
*/
export function createSMove(
source: string,
destination: string,
member: string,
): redis_request.Command {
return createCommand(RequestType.SMove, [source, destination, member]);
}

/**
* @internal
*/
Expand Down
15 changes: 15 additions & 0 deletions node/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
createSCard,
createSIsMember,
createSMembers,
createSMove,
createSPop,
createSRem,
createSelect,
Expand Down Expand Up @@ -679,6 +680,20 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
return this.addAndReturn(createSMembers(key), true);
}

/** Moves `member` from the set at `source` to the set at `destination`, removing it from the source set.
* Creates a new destination set if needed. The operation is atomic.
* See https://valkey.io/commands/smove for more details.
*
adarovadya marked this conversation as resolved.
Show resolved Hide resolved
* @param source - The key of the set to remove the element from.
* @param destination - The key of the set to add the element to.
* @param member - The set element to move.
*
* Command Response - `true` on success, or `false` if the `source` set does not exist or the element is not a member of the source set.
*/
public smove(source: string, destination: string, member: string): T {
return this.addAndReturn(createSMove(source, destination, member));
}

/** Returns the set cardinality (number of elements) of the set stored at `key`.
* See https://redis.io/commands/scard/ for details.
*
Expand Down
1 change: 1 addition & 0 deletions node/tests/RedisClusterClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ describe("RedisClusterClient", () => {
client.blpop(["abc", "zxy", "lkn"], 0.1),
client.rename("abc", "zxy"),
client.brpop(["abc", "zxy", "lkn"], 0.1),
client.smove("abc", "zxy", "value"),
client.renamenx("abc", "zxy"),
// TODO all rest multi-key commands except ones tested below
];
Expand Down
66 changes: 66 additions & 0 deletions node/tests/SharedTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,72 @@ export function runBaseTests<Context>(config: {
config.timeout,
);

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
Copy link
Collaborator

Choose a reason for hiding this comment

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

`smove test_%p`,
async (protocol) => {
await runTest(async (client: BaseClient) => {
const key1 = "{key}" + uuidv4();
const key2 = "{key}" + uuidv4();
const key3 = "{key}" + uuidv4();
const string_key = "{key}" + uuidv4();
const non_existing_key = "{key}" + uuidv4();

expect(await client.sadd(key1, ["1", "2", "3"])).toEqual(3);
expect(await client.sadd(key2, ["2", "3"])).toEqual(2);

// move an element
expect(await client.smove(key1, key2, "1"));
expect(await client.smembers(key1)).toEqual(
new Set(["2", "3"]),
);
expect(await client.smembers(key2)).toEqual(
new Set(["1", "2", "3"]),
);

// moved element already exists in the destination set
expect(await client.smove(key2, key1, "2"));
expect(await client.smembers(key1)).toEqual(
new Set(["2", "3"]),
);
expect(await client.smembers(key2)).toEqual(
new Set(["1", "3"]),
);

// attempt to move from a non-existing key
expect(await client.smove(non_existing_key, key1, "4")).toEqual(
false,
);
expect(await client.smembers(key1)).toEqual(
new Set(["2", "3"]),
);

// move to a new set
expect(await client.smove(key1, key3, "2"));
expect(await client.smembers(key1)).toEqual(new Set(["3"]));
expect(await client.smembers(key3)).toEqual(new Set(["2"]));

// attempt to move a missing element
expect(await client.smove(key1, key3, "42")).toEqual(false);
expect(await client.smembers(key1)).toEqual(new Set(["3"]));
expect(await client.smembers(key3)).toEqual(new Set(["2"]));

// move missing element to missing key
expect(
await client.smove(key1, non_existing_key, "42"),
).toEqual(false);
expect(await client.smembers(key1)).toEqual(new Set(["3"]));
expect(await client.type(non_existing_key)).toEqual("none");

// key exists, but it is not a set
expect(await client.set(string_key, "value")).toEqual("OK");
await expect(
client.smove(string_key, key1, "_"),
).rejects.toThrow();
}, protocol);
},
config.timeout,
);

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
`srem, scard and smembers with non existing key_%p`,
async (protocol) => {
Expand Down
2 changes: 2 additions & 0 deletions node/tests/TestUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ export async function transactionTest(
args.push("bar");
baseTransaction.spopCount(key7, 2);
args.push(new Set());
baseTransaction.smove(key7, key7, "non_existing_member");
args.push(false);
baseTransaction.scard(key7);
args.push(0);
baseTransaction.zadd(key8, {
Expand Down
Loading