diff --git a/CHANGELOG.md b/CHANGELOG.md
index b30c888b02..bf092e926d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,5 @@
#### Changes
+* Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049))
* Node: Added MSETNX command ([#2046](https://github.com/valkey-io/valkey-glide/pull/2046))
* Node: Added BLMOVE command ([#2027](https://github.com/valkey-io/valkey-glide/pull/2027))
* Node: Exported client configuration types ([#2023](https://github.com/valkey-io/valkey-glide/pull/2023))
diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts
index 08dcbb8021..23fd2c3caa 100644
--- a/node/npm/glide/index.ts
+++ b/node/npm/glide/index.ts
@@ -116,7 +116,6 @@ function initialize() {
ListDirection,
ExpireOptions,
FlushMode,
- GeoUnit,
InfoOptions,
InsertPosition,
SetOptions,
@@ -138,6 +137,7 @@ function initialize() {
ConfigurationError,
ExecAbortError,
RedisError,
+ ReturnType,
RequestError,
TimeoutError,
ConnectionError,
@@ -199,7 +199,6 @@ function initialize() {
ListDirection,
ExpireOptions,
FlushMode,
- GeoUnit,
InfoOptions,
InsertPosition,
SetOptions,
@@ -221,6 +220,7 @@ function initialize() {
ConfigurationError,
ExecAbortError,
RedisError,
+ ReturnType,
RequestError,
TimeoutError,
ConnectionError,
diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts
index 0dacbff526..1c69b15643 100644
--- a/node/src/BaseClient.ts
+++ b/node/src/BaseClient.ts
@@ -31,7 +31,7 @@ import {
GeoUnit,
GeospatialData,
InsertPosition,
- KeyWeight,
+ KeyWeight, // eslint-disable-line @typescript-eslint/no-unused-vars
LPosOptions,
ListDirection,
MemberOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars
@@ -84,6 +84,7 @@ import {
createIncr,
createIncrBy,
createIncrByFloat,
+ createLCS,
createLIndex,
createLInsert,
createLLen,
@@ -3940,11 +3941,8 @@ export class BaseClient {
* where each sub-array represents a single item in the following order:
*
* - The member (location) name.
- *
* - The distance from the center as a floating point `number`, in the same unit specified for `searchBy`, if `withDist` is set to `true`.
- *
* - The geohash of the location as a integer `number`, if `withHash` is set to `true`.
- *
* - The coordinates as a two item `array` of floating point `number`s, if `withCoord` is set to `true`.
*
* @example
@@ -4173,7 +4171,7 @@ export class BaseClient {
* See https://valkey.io/commands/geohash/ for more details.
*
* @param key - The key of the sorted set.
- * @param members - The array of members whose GeoHash
strings are to be retrieved.
+ * @param members - The array of members whose `GeoHash` strings are to be retrieved.
* @returns An array of `GeoHash` strings representing the positions of the specified members stored at `key`.
* If a member does not exist in the sorted set, a `null` value is returned for that member.
*
@@ -4191,6 +4189,111 @@ export class BaseClient {
);
}
+ /**
+ * Returns all the longest common subsequences combined between strings stored at `key1` and `key2`.
+ *
+ * since Valkey version 7.0.0.
+ *
+ * @remarks When in cluster mode, `key1` and `key2` must map to the same hash slot.
+ *
+ * See https://valkey.io/commands/lcs/ for more details.
+ *
+ * @param key1 - The key that stores the first string.
+ * @param key2 - The key that stores the second string.
+ * @returns A `String` containing all the longest common subsequence combined between the 2 strings.
+ * An empty `String` is returned if the keys do not exist or have no common subsequences.
+ *
+ * @example
+ * ```typescript
+ * await client.mset({"testKey1": "abcd", "testKey2": "axcd"});
+ * const result = await client.lcs("testKey1", "testKey2");
+ * console.log(result); // Output: 'cd'
+ * ```
+ */
+ public async lcs(key1: string, key2: string): Promise {
+ return this.createWritePromise(createLCS(key1, key2));
+ }
+
+ /**
+ * Returns the total length of all the longest common subsequences between strings stored at `key1` and `key2`.
+ *
+ * since Valkey version 7.0.0.
+ *
+ * @remarks When in cluster mode, `key1` and `key2` must map to the same hash slot.
+ *
+ * See https://valkey.io/commands/lcs/ for more details.
+ *
+ * @param key1 - The key that stores the first string.
+ * @param key2 - The key that stores the second string.
+ * @returns The total length of all the longest common subsequences between the 2 strings.
+ *
+ * @example
+ * ```typescript
+ * await client.mset({"testKey1": "abcd", "testKey2": "axcd"});
+ * const result = await client.lcsLen("testKey1", "testKey2");
+ * console.log(result); // Output: 2
+ * ```
+ */
+ public async lcsLen(key1: string, key2: string): Promise {
+ return this.createWritePromise(createLCS(key1, key2, { len: true }));
+ }
+
+ /**
+ * Returns the indices and lengths of the longest common subsequences between strings stored at
+ * `key1` and `key2`.
+ *
+ * since Valkey version 7.0.0.
+ *
+ * @remarks When in cluster mode, `key1` and `key2` must map to the same hash slot.
+ *
+ * See https://valkey.io/commands/lcs/ for more details.
+ *
+ * @param key1 - The key that stores the first string.
+ * @param key2 - The key that stores the second string.
+ * @param withMatchLen - (Optional) If `true`, include the length of the substring matched for the each match.
+ * @param minMatchLen - (Optional) The minimum length of matches to include in the result.
+ * @returns A `Record` containing the indices of the longest common subsequences between the
+ * 2 strings and the lengths of the longest common subsequences. The resulting map contains two
+ * keys, "matches" and "len":
+ * - `"len"` is mapped to the total length of the all longest common subsequences between the 2 strings
+ * stored as an integer. This value doesn't count towards the `minMatchLen` filter.
+ * - `"matches"` is mapped to a three dimensional array of integers that stores pairs
+ * of indices that represent the location of the common subsequences in the strings held
+ * by `key1` and `key2`.
+ *
+ * @example
+ * ```typescript
+ * await client.mset({"key1": "ohmytext", "key2": "mynewtext"});
+ * const result = await client.lcsIdx("key1", "key2");
+ * console.log(result); // Output:
+ * {
+ * "matches" :
+ * [
+ * [ // first substring match is "text"
+ * [4, 7], // in `key1` it is located between indices 4 and 7
+ * [5, 8], // and in `key2` - in between 5 and 8
+ * 4 // the match length, returned if `withMatchLen` set to `true`
+ * ],
+ * [ // second substring match is "my"
+ * [2, 3], // in `key1` it is located between indices 2 and 3
+ * [0, 1], // and in `key2` - in between 0 and 1
+ * 2 // the match length, returned if `withMatchLen` set to `true`
+ * ]
+ * ],
+ * "len" : 6 // total length of the all matches found
+ * }
+ * ```
+ */
+ public async lcsIdx(
+ key1: string,
+ key2: string,
+ options?: { withMatchLen?: boolean; minMatchLen?: number },
+ ): Promise> {
+ return this.createWritePromise(
+ createLCS(key1, key2, { idx: options ?? {} }),
+ );
+ }
+
/**
* @internal
*/
diff --git a/node/src/Commands.ts b/node/src/Commands.ts
index 1805b2a7f8..44af182559 100644
--- a/node/src/Commands.ts
+++ b/node/src/Commands.ts
@@ -2776,3 +2776,27 @@ export function createZRandMember(
return createCommand(RequestType.ZRandMember, args);
}
+
+/** @internal */
+export function createLCS(
+ key1: string,
+ key2: string,
+ options?: {
+ len?: boolean;
+ idx?: { withMatchLen?: boolean; minMatchLen?: number };
+ },
+): command_request.Command {
+ const args = [key1, key2];
+
+ if (options) {
+ if (options.len) args.push("LEN");
+ else if (options.idx) {
+ args.push("IDX");
+ if (options.idx.withMatchLen) args.push("WITHMATCHLEN");
+ if (options.idx.minMatchLen !== undefined)
+ args.push("MINMATCHLEN", options.idx.minMatchLen.toString());
+ }
+ }
+
+ return createCommand(RequestType.LCS, args);
+}
diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts
index 36b93f3e87..de661be35a 100644
--- a/node/src/Transaction.ts
+++ b/node/src/Transaction.ts
@@ -99,6 +99,7 @@ import {
createIncrBy,
createIncrByFloat,
createInfo,
+ createLCS,
createLIndex,
createLInsert,
createLLen,
@@ -2364,11 +2365,8 @@ export class BaseTransaction> {
* where each sub-array represents a single item in the following order:
*
* - The member (location) name.
- *
* - The distance from the center as a floating point `number`, in the same unit specified for `searchBy`.
- *
* - The geohash of the location as a integer `number`.
- *
* - The coordinates as a two item `array` of floating point `number`s.
*/
public geosearch(
@@ -2496,7 +2494,7 @@ export class BaseTransaction> {
* See https://valkey.io/commands/geohash/ for more details.
*
* @param key - The key of the sorted set.
- * @param members - The array of members whose GeoHash
strings are to be retrieved.
+ * @param members - The array of members whose `GeoHash` strings are to be retrieved.
*
* Command Response - An array of `GeoHash` strings representing the positions of the specified members stored at `key`.
* If a member does not exist in the sorted set, a `null` value is returned for that member.
@@ -2504,6 +2502,69 @@ export class BaseTransaction> {
public geohash(key: string, members: string[]): T {
return this.addAndReturn(createGeoHash(key, members));
}
+
+ /**
+ * Returns all the longest common subsequences combined between strings stored at `key1` and `key2`.
+ *
+ * since Valkey version 7.0.0.
+ *
+ * See https://valkey.io/commands/lcs/ for more details.
+ *
+ * @param key1 - The key that stores the first string.
+ * @param key2 - The key that stores the second string.
+ *
+ * Command Response - A `String` containing all the longest common subsequence combined between the 2 strings.
+ * An empty `String` is returned if the keys do not exist or have no common subsequences.
+ */
+ public lcs(key1: string, key2: string): T {
+ return this.addAndReturn(createLCS(key1, key2));
+ }
+
+ /**
+ * Returns the total length of all the longest common subsequences between strings stored at `key1` and `key2`.
+ *
+ * since Valkey version 7.0.0.
+ *
+ * See https://valkey.io/commands/lcs/ for more details.
+ *
+ * @param key1 - The key that stores the first string.
+ * @param key2 - The key that stores the second string.
+ *
+ * Command Response - The total length of all the longest common subsequences between the 2 strings.
+ */
+ public lcsLen(key1: string, key2: string): T {
+ return this.addAndReturn(createLCS(key1, key2, { len: true }));
+ }
+
+ /**
+ * Returns the indices and lengths of the longest common subsequences between strings stored at
+ * `key1` and `key2`.
+ *
+ * since Valkey version 7.0.0.
+ *
+ * See https://valkey.io/commands/lcs/ for more details.
+ *
+ * @param key1 - The key that stores the first string.
+ * @param key2 - The key that stores the second string.
+ * @param withMatchLen - (Optional) If `true`, include the length of the substring matched for the each match.
+ * @param minMatchLen - (Optional) The minimum length of matches to include in the result.
+ *
+ * Command Response - A `Record` containing the indices of the longest common subsequences between the
+ * 2 strings and the lengths of the longest common subsequences. The resulting map contains two
+ * keys, "matches" and "len":
+ * - `"len"` is mapped to the total length of the all longest common subsequences between the 2 strings
+ * stored as an integer. This value doesn't count towards the `minMatchLen` filter.
+ * - `"matches"` is mapped to a three dimensional array of integers that stores pairs
+ * of indices that represent the location of the common subsequences in the strings held
+ * by `key1` and `key2`.
+ */
+ public lcsIdx(
+ key1: string,
+ key2: string,
+ options?: { withMatchLen?: boolean; minMatchLen?: number },
+ ): T {
+ return this.addAndReturn(createLCS(key1, key2, { idx: options ?? {} }));
+ }
}
/**
diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts
index 00fe077201..02e6f74943 100644
--- a/node/tests/RedisClusterClient.test.ts
+++ b/node/tests/RedisClusterClient.test.ts
@@ -338,17 +338,14 @@ describe("GlideClusterClient", () => {
client.zintercard(["abc", "zxy", "lkn"]),
client.zmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX),
client.bzmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX, 0.1),
+ client.lcs("abc", "xyz"),
+ client.lcsLen("abc", "xyz"),
+ client.lcsIdx("abc", "xyz"),
);
}
for (const promise of promises) {
- try {
- await promise;
- } catch (e) {
- expect((e as Error).message.toLowerCase()).toContain(
- "crossslot",
- );
- }
+ await expect(promise).rejects.toThrowError(/crossslot/i);
}
client.close();
diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts
index 1f62c7f1c8..2cfc25b4ca 100644
--- a/node/tests/SharedTests.ts
+++ b/node/tests/SharedTests.ts
@@ -5991,6 +5991,143 @@ export function runBaseTests(config: {
},
config.timeout,
);
+
+ it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
+ `lcs %p`,
+ async (protocol) => {
+ await runTest(async (client: BaseClient, cluster) => {
+ if (cluster.checkIfServerVersionLessThan("7.0.0")) return;
+
+ const key1 = "{lcs}" + uuidv4();
+ const key2 = "{lcs}" + uuidv4();
+ const key3 = "{lcs}" + uuidv4();
+ const key4 = "{lcs}" + uuidv4();
+
+ // keys does not exist or is empty
+ expect(await client.lcs(key1, key2)).toEqual("");
+ expect(await client.lcsLen(key1, key2)).toEqual(0);
+ expect(await client.lcsIdx(key1, key2)).toEqual({
+ matches: [],
+ len: 0,
+ });
+
+ // LCS with some strings
+ expect(
+ await client.mset({
+ [key1]: "abcdefghijk",
+ [key2]: "defjkjuighijk",
+ [key3]: "123",
+ }),
+ ).toEqual("OK");
+ expect(await client.lcs(key1, key2)).toEqual("defghijk");
+ expect(await client.lcsLen(key1, key2)).toEqual(8);
+
+ // LCS with only IDX
+ expect(await client.lcsIdx(key1, key2)).toEqual({
+ matches: [
+ [
+ [6, 10],
+ [8, 12],
+ ],
+ [
+ [3, 5],
+ [0, 2],
+ ],
+ ],
+ len: 8,
+ });
+ expect(await client.lcsIdx(key1, key2, {})).toEqual({
+ matches: [
+ [
+ [6, 10],
+ [8, 12],
+ ],
+ [
+ [3, 5],
+ [0, 2],
+ ],
+ ],
+ len: 8,
+ });
+ expect(
+ await client.lcsIdx(key1, key2, { withMatchLen: false }),
+ ).toEqual({
+ matches: [
+ [
+ [6, 10],
+ [8, 12],
+ ],
+ [
+ [3, 5],
+ [0, 2],
+ ],
+ ],
+ len: 8,
+ });
+
+ // LCS with IDX and WITHMATCHLEN
+ expect(
+ await client.lcsIdx(key1, key2, { withMatchLen: true }),
+ ).toEqual({
+ matches: [
+ [[6, 10], [8, 12], 5],
+ [[3, 5], [0, 2], 3],
+ ],
+ len: 8,
+ });
+
+ // LCS with IDX and MINMATCHLEN
+ expect(
+ await client.lcsIdx(key1, key2, { minMatchLen: 4 }),
+ ).toEqual({
+ matches: [
+ [
+ [6, 10],
+ [8, 12],
+ ],
+ ],
+ len: 8,
+ });
+ // LCS with IDX and a negative MINMATCHLEN
+ expect(
+ await client.lcsIdx(key1, key2, { minMatchLen: -1 }),
+ ).toEqual({
+ matches: [
+ [
+ [6, 10],
+ [8, 12],
+ ],
+ [
+ [3, 5],
+ [0, 2],
+ ],
+ ],
+ len: 8,
+ });
+
+ // LCS with IDX, MINMATCHLEN, and WITHMATCHLEN
+ expect(
+ await client.lcsIdx(key1, key2, {
+ minMatchLen: 4,
+ withMatchLen: true,
+ }),
+ ).toEqual({ matches: [[[6, 10], [8, 12], 5]], len: 8 });
+
+ // non-string keys are used
+ expect(await client.sadd(key4, ["_"])).toEqual(1);
+ await expect(client.lcs(key1, key4)).rejects.toThrow(
+ RequestError,
+ );
+ await expect(client.lcsLen(key1, key4)).rejects.toThrow(
+ RequestError,
+ );
+ await expect(client.lcsIdx(key1, key4)).rejects.toThrow(
+ RequestError,
+ );
+ }, protocol);
+ },
+ config.timeout,
+ );
}
export function runCommonTests(config: {
diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts
index f7f189fc74..0267e02263 100644
--- a/node/tests/TestUtilities.ts
+++ b/node/tests/TestUtilities.ts
@@ -447,9 +447,9 @@ export async function transactionTest(
baseTransaction: Transaction | ClusterTransaction,
version: string,
): Promise<[string, ReturnType][]> {
- const key1 = "{key}" + uuidv4();
- const key2 = "{key}" + uuidv4();
- const key3 = "{key}" + uuidv4();
+ const key1 = "{key}" + uuidv4(); // string
+ const key2 = "{key}" + uuidv4(); // string
+ const key3 = "{key}" + uuidv4(); // string
const key4 = "{key}" + uuidv4();
const key5 = "{key}" + uuidv4();
const key6 = "{key}" + uuidv4();
@@ -1046,6 +1046,59 @@ export async function transactionTest(
withCode: true,
});
responseData.push(["functionList({ libName, true})", []]);
+
+ baseTransaction
+ .mset({ [key1]: "abcd", [key2]: "bcde", [key3]: "wxyz" })
+ .lcs(key1, key2)
+ .lcs(key1, key3)
+ .lcsLen(key1, key2)
+ .lcsLen(key1, key3)
+ .lcsIdx(key1, key2)
+ .lcsIdx(key1, key2, { minMatchLen: 1 })
+ .lcsIdx(key1, key2, { withMatchLen: true })
+ .lcsIdx(key1, key2, { withMatchLen: true, minMatchLen: 1 })
+ .del([key1, key2, key3]);
+
+ responseData.push(
+ ['mset({[key1]: "abcd", [key2]: "bcde", [key3]: "wxyz"})', "OK"],
+ ["lcs(key1, key2)", "bcd"],
+ ["lcs(key1, key3)", ""],
+ ["lcsLen(key1, key2)", 3],
+ ["lcsLen(key1, key3)", 0],
+ [
+ "lcsIdx(key1, key2)",
+ {
+ matches: [
+ [
+ [1, 3],
+ [0, 2],
+ ],
+ ],
+ len: 3,
+ },
+ ],
+ [
+ "lcsIdx(key1, key2, {minMatchLen: 1})",
+ {
+ matches: [
+ [
+ [1, 3],
+ [0, 2],
+ ],
+ ],
+ len: 3,
+ },
+ ],
+ [
+ "lcsIdx(key1, key2, {withMatchLen: true})",
+ { matches: [[[1, 3], [0, 2], 3]], len: 3 },
+ ],
+ [
+ "lcsIdx(key1, key2, {withMatchLen: true, minMatchLen: 1})",
+ { matches: [[[1, 3], [0, 2], 3]], len: 3 },
+ ],
+ ["del([key1, key2, key3])", 3],
+ );
}
return responseData;