Skip to content

Commit

Permalink
Node: add LMOVE (valkey-io#2002)
Browse files Browse the repository at this point in the history
* implement lmove

Signed-off-by: Chloe Yip <chloe.yip@improving.com>
  • Loading branch information
cyip10 authored Jul 25, 2024
1 parent a37fe95 commit 1c70afb
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#### Changes
* Node: Added LMOVE command ([#2002](https://github.com/valkey-io/valkey-glide/pull/2002))
* Node: Added GEOPOS command ([#1991](https://github.com/valkey-io/valkey-glide/pull/1991))
* Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982))
* Node: Added FLUSHDB command ([#1986](https://github.com/valkey-io/valkey-glide/pull/1986))
Expand Down
2 changes: 2 additions & 0 deletions node/npm/glide/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function initialize() {
PeriodicChecks,
Logger,
LPosOptions,
ListDirection,
ExpireOptions,
FlushMode,
GeoUnit,
Expand Down Expand Up @@ -147,6 +148,7 @@ function initialize() {
PeriodicChecks,
Logger,
LPosOptions,
ListDirection,
ExpireOptions,
FlushMode,
GeoUnit,
Expand Down
43 changes: 43 additions & 0 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
GeoUnit,
InsertPosition,
KeyWeight,
ListDirection,
RangeByIndex,
RangeByLex,
RangeByScore,
Expand Down Expand Up @@ -63,6 +64,7 @@ import {
createLIndex,
createLInsert,
createLLen,
createLMove,
createLPop,
createLPos,
createLPush,
Expand Down Expand Up @@ -1465,6 +1467,47 @@ export class BaseClient {
return this.createWritePromise(createLLen(key));
}

/**
* Atomically pops and removes the left/right-most element to the list stored at `source`
* depending on `whereTo`, and pushes the element at the first/last element of the list
* stored at `destination` depending on `whereFrom`, see {@link ListDirection}.
*
* See https://valkey.io/commands/lmove/ for details.
*
* @param source - The key to the source list.
* @param destination - The key to the destination list.
* @param whereFrom - The {@link ListDirection} to remove the element from.
* @param whereTo - The {@link ListDirection} to add the element to.
* @returns The popped element, or `null` if `source` does not exist.
*
* since Valkey version 6.2.0.
*
* @example
* ```typescript
* await client.lpush("testKey1", ["two", "one"]);
* await client.lpush("testKey2", ["four", "three"]);
*
* const result1 = await client.lmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT);
* console.log(result1); // Output: "one".
*
* const updated_array_key1 = await client.lrange("testKey1", 0, -1);
* console.log(updated_array); // Output: "two".
*
* const updated_array_key2 = await client.lrange("testKey2", 0, -1);
* console.log(updated_array_key2); // Output: ["one", "three", "four"].
* ```
*/
public async lmove(
source: string,
destination: string,
whereFrom: ListDirection,
whereTo: ListDirection,
): Promise<string | null> {
return this.createWritePromise(
createLMove(source, destination, whereFrom, whereTo),
);
}

/**
* Sets the list element at `index` to `element`.
* The index is zero-based, so `0` means the first element, `1` the second element and so on.
Expand Down
31 changes: 31 additions & 0 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,37 @@ export function createLLen(key: string): command_request.Command {
return createCommand(RequestType.LLen, [key]);
}

/**
* Enumeration representing element popping or adding direction for the List Based Commands.
*/
export enum ListDirection {
/**
* Represents the option that elements should be popped from or added to the left side of a list.
*/
LEFT = "LEFT",
/**
* Represents the option that elements should be popped from or added to the right side of a list.
*/
RIGHT = "RIGHT",
}

/**
* @internal
*/
export function createLMove(
source: string,
destination: string,
whereFrom: ListDirection,
whereTo: ListDirection,
): command_request.Command {
return createCommand(RequestType.LMove, [
source,
destination,
whereFrom,
whereTo,
]);
}

/**
* @internal
*/
Expand Down
29 changes: 29 additions & 0 deletions node/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
InfoOptions,
InsertPosition,
KeyWeight,
ListDirection,
LolwutOptions,
RangeByIndex,
RangeByLex,
Expand Down Expand Up @@ -72,6 +73,7 @@ import {
createLIndex,
createLInsert,
createLLen,
createLMove,
createLPop,
createLPos,
createLPush,
Expand Down Expand Up @@ -707,6 +709,33 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
return this.addAndReturn(createLLen(key));
}

/**
* Atomically pops and removes the left/right-most element to the list stored at `source`
* depending on `whereFrom`, and pushes the element at the first/last element of the list
* stored at `destination` depending on `whereTo`, see {@link ListDirection}.
*
* See https://valkey.io/commands/lmove/ for details.
*
* @param source - The key to the source list.
* @param destination - The key to the destination list.
* @param whereFrom - The {@link ListDirection} to remove the element from.
* @param whereTo - The {@link ListDirection} to add the element to.
*
* Command Response - The popped element, or `null` if `source` does not exist.
*
* since Valkey version 6.2.0.
*/
public lmove(
source: string,
destination: string,
whereFrom: ListDirection,
whereTo: ListDirection,
): T {
return this.addAndReturn(
createLMove(source, destination, whereFrom, whereTo),
);
}

/**
* Sets the list element at `index` to `element`.
* The index is zero-based, so `0` means the first element, `1` the second element and so on.
Expand Down
111 changes: 111 additions & 0 deletions node/tests/SharedTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
BitwiseOperation,
ClosingError,
ExpireOptions,
ListDirection,
GlideClient,
GlideClusterClient,
InfoOptions,
Expand Down Expand Up @@ -1104,6 +1105,116 @@ export function runBaseTests<Context>(config: {
config.timeout,
);

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
`lmove list_%p`,
async (protocol) => {
await runTest(async (client: BaseClient, cluster) => {
if (cluster.checkIfServerVersionLessThan("6.2.0")) {
return;
}

const key1 = "{key}-1" + uuidv4();
const key2 = "{key}-2" + uuidv4();
const lpushArgs1 = ["2", "1"];
const lpushArgs2 = ["4", "3"];

// Initialize the tests
expect(await client.lpush(key1, lpushArgs1)).toEqual(2);
expect(await client.lpush(key2, lpushArgs2)).toEqual(2);

// Move from LEFT to LEFT
checkSimple(
await client.lmove(
key1,
key2,
ListDirection.LEFT,
ListDirection.LEFT,
),
).toEqual("1");

// Move from LEFT to RIGHT
checkSimple(
await client.lmove(
key1,
key2,
ListDirection.LEFT,
ListDirection.RIGHT,
),
).toEqual("2");

checkSimple(await client.lrange(key2, 0, -1)).toEqual([
"1",
"3",
"4",
"2",
]);
checkSimple(await client.lrange(key1, 0, -1)).toEqual([]);

// Move from RIGHT to LEFT - non-existing destination key
checkSimple(
await client.lmove(
key2,
key1,
ListDirection.RIGHT,
ListDirection.LEFT,
),
).toEqual("2");

// Move from RIGHT to RIGHT
checkSimple(
await client.lmove(
key2,
key1,
ListDirection.RIGHT,
ListDirection.RIGHT,
),
).toEqual("4");

checkSimple(await client.lrange(key2, 0, -1)).toEqual([
"1",
"3",
]);
checkSimple(await client.lrange(key1, 0, -1)).toEqual([
"2",
"4",
]);

// Non-existing source key
expect(
await client.lmove(
"{key}-non_existing_key" + uuidv4(),
key1,
ListDirection.LEFT,
ListDirection.LEFT,
),
).toEqual(null);

// Non-list source key
const key3 = "{key}-3" + uuidv4();
checkSimple(await client.set(key3, "value")).toEqual("OK");
await expect(
client.lmove(
key3,
key1,
ListDirection.LEFT,
ListDirection.LEFT,
),
).rejects.toThrow(RequestError);

// Non-list destination key
await expect(
client.lmove(
key1,
key3,
ListDirection.LEFT,
ListDirection.LEFT,
),
).rejects.toThrow(RequestError);
}, protocol);
},
config.timeout,
);

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
`lset test_%p`,
async (protocol) => {
Expand Down
24 changes: 22 additions & 2 deletions node/tests/TestUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
GlideClusterClient,
InsertPosition,
Logger,
ListDirection,
ProtocolVersion,
ReturnType,
ScoreFilter,
Expand Down Expand Up @@ -399,6 +400,7 @@ export async function transactionTest(
const key17 = "{key}" + uuidv4(); // bitmap
const key18 = "{key}" + uuidv4(); // Geospatial Data/ZSET
const key19 = "{key}" + uuidv4(); // bitmap
const key20 = "{key}" + uuidv4(); // list
const field = uuidv4();
const value = uuidv4();
// array of tuples - first element is test name/description, second - expected return value
Expand Down Expand Up @@ -476,8 +478,26 @@ export async function transactionTest(
responseData.push(['lset(key5, 0, field + "3")', "OK"]);
baseTransaction.lrange(key5, 0, -1);
responseData.push(["lrange(key5, 0, -1)", [field + "3", field + "2"]]);
baseTransaction.lpopCount(key5, 2);
responseData.push(["lpopCount(key5, 2)", [field + "3", field + "2"]]);

if (gte("6.2.0", version)) {
baseTransaction.lmove(
key5,
key20,
ListDirection.LEFT,
ListDirection.LEFT,
);
responseData.push([
"lmove(key5, key20, ListDirection.LEFT, ListDirection.LEFT)",
field + "3",
]);

baseTransaction.lpopCount(key5, 2);
responseData.push(["lpopCount(key5, 2)", [field + "2"]]);
} else {
baseTransaction.lpopCount(key5, 2);
responseData.push(["lpopCount(key5, 2)", [field + "3", field + "2"]]);
}

baseTransaction.linsert(
key5,
InsertPosition.Before,
Expand Down

0 comments on commit 1c70afb

Please sign in to comment.