Skip to content

Commit

Permalink
[Compatibility] Added LCS command (microsoft#843)
Browse files Browse the repository at this point in the history
* Added LCS command

* Format fix

* Reverted CommandDocsUpdater.cs

* Fix cluster test

* Fixed wrong change

* Moved to constant

* Review command fixes

* Fixed review comment

* Fixed test issue

---------

Co-authored-by: Vasileios Zois <96085550+vazois@users.noreply.github.com>
Co-authored-by: Tal Zaccai <talzacc@microsoft.com>
  • Loading branch information
3 people authored Jan 7, 2025
1 parent ce21c24 commit 56394d8
Show file tree
Hide file tree
Showing 17 changed files with 758 additions and 1 deletion.
55 changes: 55 additions & 0 deletions libs/resources/RespCommandsDocs.json
Original file line number Diff line number Diff line change
Expand Up @@ -3337,6 +3337,61 @@
}
]
},
{
"Command": "LCS",
"Name": "LCS",
"Summary": "Finds the longest common substring.",
"Group": "String",
"Complexity": "O(N*M) where N and M are the lengths of s1 and s2, respectively",
"Arguments": [
{
"TypeDiscriminator": "RespCommandKeyArgument",
"Name": "KEY1",
"DisplayText": "key1",
"Type": "Key",
"KeySpecIndex": 0
},
{
"TypeDiscriminator": "RespCommandKeyArgument",
"Name": "KEY2",
"DisplayText": "key2",
"Type": "Key",
"KeySpecIndex": 0
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "LEN",
"DisplayText": "len",
"Type": "PureToken",
"Token": "LEN",
"ArgumentFlags": "Optional"
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "IDX",
"DisplayText": "idx",
"Type": "PureToken",
"Token": "IDX",
"ArgumentFlags": "Optional"
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "MIN-MATCH-LEN",
"DisplayText": "min-match-len",
"Type": "Integer",
"Token": "MINMATCHLEN",
"ArgumentFlags": "Optional"
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "WITHMATCHLEN",
"DisplayText": "withmatchlen",
"Type": "PureToken",
"Token": "WITHMATCHLEN",
"ArgumentFlags": "Optional"
}
]
},
{
"Command": "LINDEX",
"Name": "LINDEX",
Expand Down
25 changes: 25 additions & 0 deletions libs/resources/RespCommandsInfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -2113,6 +2113,31 @@
}
]
},
{
"Command": "LCS",
"Name": "LCS",
"Arity": -3,
"Flags": "ReadOnly",
"FirstKey": 1,
"LastKey": 2,
"Step": 1,
"AclCategories": "Read, Slow, String",
"KeySpecifications": [
{
"BeginSearch": {
"TypeDiscriminator": "BeginSearchIndex",
"Index": 1
},
"FindKeys": {
"TypeDiscriminator": "FindKeysRange",
"LastKey": 1,
"KeyStep": 1,
"Limit": 0
},
"Flags": "RO, Access"
}
]
},
{
"Command": "LINDEX",
"Name": "LINDEX",
Expand Down
4 changes: 4 additions & 0 deletions libs/server/API/GarnetApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ public unsafe GarnetStatus GET(ArgSlice key, out ArgSlice value)
/// <inheritdoc />
public GarnetStatus GET(byte[] key, out GarnetObjectStoreOutput value)
=> storageSession.GET(key, out value, ref objectContext);

/// <inheritdoc />
public GarnetStatus LCS(ArgSlice key1, ArgSlice key2, ref SpanByteAndMemory output, bool lenOnly = false, bool withIndices = false, bool withMatchLen = false, int minMatchLen = 0)
=> storageSession.LCS(key1, key2, ref output, lenOnly, withIndices, withMatchLen, minMatchLen);
#endregion

#region GETEX
Expand Down
8 changes: 8 additions & 0 deletions libs/server/API/GarnetWatchApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ public GarnetStatus GET(byte[] key, out GarnetObjectStoreOutput value)
garnetApi.WATCH(key, StoreType.Object);
return garnetApi.GET(key, out value);
}

/// <inheritdoc />
public GarnetStatus LCS(ArgSlice key1, ArgSlice key2, ref SpanByteAndMemory output, bool lenOnly = false, bool withIndices = false, bool withMatchLen = false, int minMatchLen = 0)
{
garnetApi.WATCH(key1, StoreType.Object);
garnetApi.WATCH(key2, StoreType.Object);
return garnetApi.LCS(key1, key2, ref output, lenOnly, withIndices, withMatchLen, minMatchLen);
}
#endregion

#region GETRANGE
Expand Down
13 changes: 13 additions & 0 deletions libs/server/API/IGarnetApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,19 @@ public interface IGarnetReadApi
/// <param name="value"></param>
/// <returns></returns>
GarnetStatus GET(byte[] key, out GarnetObjectStoreOutput value);

/// <summary>
/// Finds the longest common subsequence (LCS) between two keys.
/// </summary>
/// <param name="key1">The first key to compare.</param>
/// <param name="key2">The second key to compare.</param>
/// <param name="output">The output containing the LCS result.</param>
/// <param name="lenOnly">If true, only the length of the LCS is returned.</param>
/// <param name="withIndices">If true, the indices of the LCS in both keys are returned.</param>
/// <param name="withMatchLen">If true, the length of each match is returned.</param>
/// <param name="minMatchLen">The minimum length of a match to be considered.</param>
/// <returns>The status of the operation.</returns>
GarnetStatus LCS(ArgSlice key1, ArgSlice key2, ref SpanByteAndMemory output, bool lenOnly = false, bool withIndices = false, bool withMatchLen = false, int minMatchLen = 0);
#endregion

#region GETRANGE
Expand Down
72 changes: 72 additions & 0 deletions libs/server/Resp/ArrayCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -450,5 +450,77 @@ private bool NetworkArrayPING()
WriteDirectLargeRespString(message);
return true;
}

private bool NetworkLCS<TGarnetApi>(ref TGarnetApi storageApi)
where TGarnetApi : IGarnetApi
{
if (parseState.Count < 2)
return AbortWithWrongNumberOfArguments(nameof(RespCommand.LCS));

var key1 = parseState.GetArgSliceByRef(0);
var key2 = parseState.GetArgSliceByRef(1);

// Parse options
var lenOnly = false;
var withIndices = false;
var minMatchLen = 0;
var withMatchLen = false;
var tokenIdx = 2;
while (tokenIdx < parseState.Count)
{
var option = parseState.GetArgSliceByRef(tokenIdx++).ReadOnlySpan;

if (option.EqualsUpperCaseSpanIgnoringCase(CmdStrings.LEN))
{
lenOnly = true;
}
else if (option.EqualsUpperCaseSpanIgnoringCase(CmdStrings.IDX))
{
withIndices = true;
}
else if (option.EqualsUpperCaseSpanIgnoringCase(CmdStrings.MINMATCHLEN))
{
if (tokenIdx + 1 > parseState.Count)
{
return AbortWithErrorMessage(CmdStrings.RESP_SYNTAX_ERROR);
}

if (!parseState.TryGetInt(tokenIdx++, out var minLen))
{
return AbortWithErrorMessage(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER);
}

if (minLen < 0)
{
minLen = 0;
}

minMatchLen = minLen;
}
else if (option.EqualsUpperCaseSpanIgnoringCase(CmdStrings.WITHMATCHLEN))
{
withMatchLen = true;
}
else
{
return AbortWithErrorMessage(CmdStrings.RESP_SYNTAX_ERROR);
}
}

if (lenOnly && withIndices)
{
return AbortWithErrorMessage(CmdStrings.RESP_ERR_LENGTH_AND_INDEXES);
}

var output = new SpanByteAndMemory(dcurr, (int)(dend - dcurr));
var status = storageApi.LCS(key1, key2, ref output, lenOnly, withIndices, withMatchLen, minMatchLen);

if (!output.IsSpanByte)
SendAndReset(output.Memory, output.Length);
else
dcurr += output.Length;

return true;
}
}
}
7 changes: 7 additions & 0 deletions libs/server/Resp/CmdStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ static partial class CmdStrings
public static ReadOnlySpan<byte> WEIGHTS => "WEIGHTS"u8;
public static ReadOnlySpan<byte> AGGREGATE => "AGGREGATE"u8;
public static ReadOnlySpan<byte> SUM => "SUM"u8;
public static ReadOnlySpan<byte> LEN => "LEN"u8;
public static ReadOnlySpan<byte> IDX => "IDX"u8;
public static ReadOnlySpan<byte> MINMATCHLEN => "MINMATCHLEN"u8;
public static ReadOnlySpan<byte> WITHMATCHLEN => "WITHMATCHLEN"u8;

/// <summary>
/// Response strings
Expand All @@ -140,6 +144,8 @@ static partial class CmdStrings
public static ReadOnlySpan<byte> RESP_PONG => "+PONG\r\n"u8;
public static ReadOnlySpan<byte> RESP_EMPTY => "$0\r\n\r\n"u8;
public static ReadOnlySpan<byte> RESP_QUEUED => "+QUEUED\r\n"u8;
public static ReadOnlySpan<byte> matches => "matches"u8;
public static ReadOnlySpan<byte> len => "len"u8;

/// <summary>
/// Simple error response strings, i.e. these are of the form "-errorString\r\n"
Expand Down Expand Up @@ -215,6 +221,7 @@ static partial class CmdStrings
public static ReadOnlySpan<byte> RESP_ERR_INCR_SUPPORTS_ONLY_SINGLE_PAIR => "ERR INCR option supports a single increment-element pair"u8;
public static ReadOnlySpan<byte> RESP_ERR_INVALID_BITFIELD_TYPE => "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is"u8;
public static ReadOnlySpan<byte> RESP_ERR_SCRIPT_FLUSH_OPTIONS => "ERR SCRIPT FLUSH only support SYNC|ASYNC option"u8;
public static ReadOnlySpan<byte> RESP_ERR_LENGTH_AND_INDEXES => "If you want both the length and indexes, please just use IDX."u8;

/// <summary>
/// Response string templates
Expand Down
5 changes: 5 additions & 0 deletions libs/server/Resp/Parser/RespCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public enum RespCommand : ushort
HSTRLEN,
HVALS,
KEYS,
LCS,
LINDEX,
LLEN,
LPOS,
Expand Down Expand Up @@ -772,6 +773,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan<byte>
{
return RespCommand.DEL;
}
else if (*(ulong*)(ptr + 1) == MemoryMarshal.Read<ulong>("3\r\nLCS\r\n"u8))
{
return RespCommand.LCS;
}

break;

Expand Down
2 changes: 2 additions & 0 deletions libs/server/Resp/RespServerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,8 @@ private bool ProcessOtherCommands<TGarnetApi>(RespCommand command, ref TGarnetAp

RespCommand.EVAL => TryEVAL(),
RespCommand.EVALSHA => TryEVALSHA(),
// Slow commands
RespCommand.LCS => NetworkLCS(ref storageApi),
_ => Process(command)
};

Expand Down
Loading

0 comments on commit 56394d8

Please sign in to comment.