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: Add LCS command. #2049

Merged
merged 5 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -1,4 +1,5 @@
#### Changes
* Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049))
* Node: Exported client configuration types ([#2023](https://github.com/valkey-io/valkey-glide/pull/2023))
* Java, Python: Update docs for GEOSEARCH command ([#2017](https://github.com/valkey-io/valkey-glide/pull/2017))
* Node: Added FUNCTION LIST command ([#2019](https://github.com/valkey-io/valkey-glide/pull/2019))
Expand Down
4 changes: 2 additions & 2 deletions node/npm/glide/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ function initialize() {
ListDirection,
ExpireOptions,
FlushMode,
GeoUnit,
InfoOptions,
InsertPosition,
SetOptions,
Expand All @@ -128,6 +127,7 @@ function initialize() {
ConfigurationError,
ExecAbortError,
RedisError,
ReturnType,
RequestError,
TimeoutError,
ConnectionError,
Expand Down Expand Up @@ -177,7 +177,6 @@ function initialize() {
ListDirection,
ExpireOptions,
FlushMode,
GeoUnit,
InfoOptions,
InsertPosition,
SetOptions,
Expand All @@ -199,6 +198,7 @@ function initialize() {
ConfigurationError,
ExecAbortError,
RedisError,
ReturnType,
RequestError,
TimeoutError,
ConnectionError,
Expand Down
129 changes: 118 additions & 11 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import * as net from "net";
import { Buffer, BufferWriter, Reader, Writer } from "protobufjs";
import {
AggregationType,
BitmapIndexType,
BitOffsetOptions,
BitmapIndexType,
BitwiseOperation,
CoordOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars
ExpireOptions,
Expand All @@ -21,13 +21,13 @@ import {
GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars
GeoSearchResultOptions,
GeoSearchShape,
GeospatialData,
GeoUnit,
GeospatialData,
InsertPosition,
KeyWeight,
MemberOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars
KeyWeight, // eslint-disable-line @typescript-eslint/no-unused-vars
LPosOptions,
ListDirection,
MemberOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars
RangeByIndex,
RangeByLex,
RangeByScore,
Expand All @@ -41,9 +41,9 @@ import {
ZAddOptions,
createBLPop,
createBRPop,
createBZMPop,
createBitCount,
createBitOp,
createBZMPop,
createBitPos,
createDecr,
createDecrBy,
Expand Down Expand Up @@ -76,6 +76,7 @@ import {
createIncr,
createIncrBy,
createIncrByFloat,
createLCS,
createLIndex,
createLInsert,
createLLen,
Expand Down Expand Up @@ -175,10 +176,11 @@ import {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
type PromiseFunction = (value?: any) => void;
type ErrorFunction = (error: RedisError) => void;
export type ReturnTypeMap = { [key: string]: ReturnType };
export type ReturnTypeRecord = { [key: string]: ReturnType };
export type ReturnTypeMap = Map<string, ReturnType>;
export type ReturnTypeAttribute = {
value: ReturnType;
attributes: ReturnTypeMap;
attributes: ReturnTypeRecord;
};
export enum ProtocolVersion {
/** Use RESP2 to communicate with the server nodes. */
Expand All @@ -195,6 +197,7 @@ export type ReturnType =
| bigint
| Buffer
| Set<ReturnType>
| ReturnTypeRecord
| ReturnTypeMap
| ReturnTypeAttribute
| ReturnType[];
Expand Down Expand Up @@ -3789,11 +3792,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
Expand Down Expand Up @@ -4022,7 +4022,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 <code>GeoHash</code> 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.
*
Expand All @@ -4040,6 +4040,113 @@ 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<string> {
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<number> {
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.
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
* @param minMatchLen - (Optional) The minimum length of matches to include in the result.
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
* @returns A `Map` 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":
*
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
* - `"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 `minMatchLen` filter.
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
*
* - `"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<Map<string, (number | [number, number])[][] | number>> {
return this.createWritePromise(
createLCS(key1, key2, { idx: options ?? {} }),
);
}

/**
* @internal
*/
Expand Down
25 changes: 25 additions & 0 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2427,3 +2427,28 @@ 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");

if (options.idx) {
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
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);
}
71 changes: 67 additions & 4 deletions node/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ import {
createZRevRankWithScore,
createZScore,
createZIncrBy,
createLCS,
} from "./Commands";
import { command_request } from "./ProtobufMessage";

Expand Down Expand Up @@ -2261,11 +2262,8 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
* 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(
Expand Down Expand Up @@ -2393,14 +2391,79 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
* See https://valkey.io/commands/geohash/ for more details.
*
* @param key - The key of the sorted set.
* @param members - The array of members whose <code>GeoHash</code> 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.
*/
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.
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
* @param minMatchLen - (Optional) The minimum length of matches to include in the result.
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
*
* Command Response - A `Map` 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 `minMatchLen` filter.
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
*
* - `"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 ?? {} }));
}
}

/**
Expand Down
11 changes: 4 additions & 7 deletions node/tests/RedisClusterClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,17 +336,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();
Expand Down
Loading
Loading