From 6460ad68a7763f84b6601eb2104afdc6b3139f29 Mon Sep 17 00:00:00 2001 From: Vijay-Nirmal Date: Tue, 17 Dec 2024 12:08:16 +0530 Subject: [PATCH 1/7] Added COMMAND GETKEYS and GETKEYSANDFLAGS command --- libs/server/Resp/BasicCommands.cs | 155 ++++++++++++++++++ libs/server/Resp/CmdStrings.cs | 4 + libs/server/Resp/Parser/RespCommand.cs | 14 ++ libs/server/Resp/RespServerSession.cs | 2 + .../CommandInfoUpdater/SupportedCommand.cs | 2 + 5 files changed, 177 insertions(+) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 860dd691b2..e0fc25e314 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; @@ -1149,6 +1150,160 @@ private bool NetworkCOMMAND_INFO() return true; } + /// + /// Processes COMMAND GETKEYS subcommand. + /// + private bool NetworkCOMMAND_GETKEYS() + { + if (parseState.Count == 0) + { + return AbortWithWrongNumberOfArguments(nameof(RespCommand.COMMAND_GETKEYS)); + } + + var cmdName = parseState.GetString(0).ToUpperInvariant(); + bool cmdFound = RespCommandsInfo.TryGetRespCommandInfo(cmdName, out var cmdInfo, true, true, logger) || + storeWrapper.customCommandManager.TryGetCustomCommandInfo(cmdName, out cmdInfo); + + if (!cmdFound) + { + return AbortWithErrorMessage(CmdStrings.RESP_INVALID_COMMAND_SPECIFIED); + } + + if (cmdInfo.KeySpecifications == null || cmdInfo.KeySpecifications.Length == 0) + { + return AbortWithErrorMessage(CmdStrings.RESP_COMMAND_HAS_NO_KEY_ARGS); + } + + var keyList = new List(); + foreach (var spec in cmdInfo.KeySpecifications) + { + ExtractKeys(spec, keyList); + } + + while (!RespWriteUtils.WriteArrayLength(keyList.Count, ref dcurr, dend)) + SendAndReset(); + + foreach (var key in keyList) + { + while (!RespWriteUtils.WriteBulkString(key, ref dcurr, dend)) + SendAndReset(); + } + + return true; + } + + /// + /// Processes COMMAND GETKEYSANDFLAGS subcommand. + /// + private bool NetworkCOMMAND_GETKEYSANDFLAGS() + { + if (parseState.Count == 0) + { + return AbortWithWrongNumberOfArguments(nameof(RespCommand.COMMAND_GETKEYSANDFLAGS)); + } + + var cmdName = parseState.GetString(0).ToUpperInvariant(); + bool cmdFound = RespCommandsInfo.TryGetRespCommandInfo(cmdName, out var cmdInfo, true, true, logger) || + storeWrapper.customCommandManager.TryGetCustomCommandInfo(cmdName, out cmdInfo); + + if (!cmdFound) + { + return AbortWithErrorMessage(CmdStrings.RESP_INVALID_COMMAND_SPECIFIED); + } + + if (cmdInfo.KeySpecifications == null || cmdInfo.KeySpecifications.Length == 0) + { + return AbortWithErrorMessage(CmdStrings.RESP_COMMAND_HAS_NO_KEY_ARGS); + } + + var keyList = new List(); + var flagsList = new List(); + + foreach (var spec in cmdInfo.KeySpecifications) + { + var keyCount = keyList.Count; + ExtractKeys(spec, keyList); + var flags = EnumUtils.GetEnumDescriptions(spec.Flags); + for (int i = keyCount; i < keyList.Count; i++) + { + flagsList.Add(flags); + } + } + + while (!RespWriteUtils.WriteArrayLength(keyList.Count, ref dcurr, dend)) + SendAndReset(); + + for (int i = 0; i < keyList.Count; i++) + { + while (!RespWriteUtils.WriteArrayLength(2, ref dcurr, dend)) + SendAndReset(); + + while (!RespWriteUtils.WriteBulkString(keyList[i], ref dcurr, dend)) + SendAndReset(); + + while (!RespWriteUtils.WriteArrayLength(flagsList[i].Length, ref dcurr, dend)) + SendAndReset(); + + foreach (var flag in flagsList[i]) + { + while (!RespWriteUtils.WriteBulkString(Encoding.ASCII.GetBytes(flag), ref dcurr, dend)) + SendAndReset(); + } + } + + return true; + } + + private void ExtractKeys(RespCommandKeySpecification spec, List keyList) + { + int startIndex = 0; + + if (spec.BeginSearch is BeginSearchIndex bsIndex) + { + startIndex = bsIndex.Index; + } + else if (spec.BeginSearch is BeginSearchKeyword bsKeyword) + { + for (int i = bsKeyword.StartFrom; i < parseState.Count; i++) + { + if (parseState.GetArgSliceByRef(i).ReadOnlySpan.EqualsUpperCaseSpanIgnoringCase(Encoding.ASCII.GetBytes(bsKeyword.Keyword))) + { + startIndex = i + 1; + break; + } + } + } + + if (startIndex >= parseState.Count) + return; + + if (spec.FindKeys is FindKeysRange range) + { + var lastKey = range.LastKey == -1 ? parseState.Count - 1 : + Math.Min(startIndex + range.LastKey, parseState.Count - 1); + + for (int i = startIndex; i <= lastKey; i += range.KeyStep) + { + if (i < parseState.Count) + { + keyList.Add(parseState.GetArgSliceByRef(i).ToArray()); + } + } + } + else if (spec.FindKeys is FindKeysKeyNum keyNum) + { + if (keyNum.KeyNumIdx >= 0 && keyNum.KeyNumIdx < parseState.Count && + parseState.TryGetInt(startIndex + keyNum.KeyNumIdx, out var count)) + { + var firstKey = startIndex + keyNum.FirstKey; + for (int i = 0; i < count && firstKey + i * keyNum.KeyStep < parseState.Count; i++) + { + keyList.Add(parseState.GetArgSliceByRef(firstKey + i * keyNum.KeyStep).ToArray()); + } + } + } + } + private bool NetworkECHO() { if (parseState.Count != 1) diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index aad5e5e43f..cc5c8c35fc 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -36,6 +36,8 @@ static partial class CmdStrings public static ReadOnlySpan info => "info"u8; public static ReadOnlySpan DOCS => "DOCS"u8; public static ReadOnlySpan docs => "docs"u8; + public static ReadOnlySpan GETKEYS => "GETKEYS"u8; + public static ReadOnlySpan GETKEYSANDFLAGS => "GETKEYSANDFLAGS"u8; public static ReadOnlySpan COMMAND => "COMMAND"u8; public static ReadOnlySpan LATENCY => "LATENCY"u8; public static ReadOnlySpan CLUSTER => "CLUSTER"u8; @@ -212,6 +214,8 @@ static partial class CmdStrings public static ReadOnlySpan RESP_ERR_INCR_SUPPORTS_ONLY_SINGLE_PAIR => "ERR INCR option supports a single increment-element pair"u8; public static ReadOnlySpan 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 RESP_ERR_SCRIPT_FLUSH_OPTIONS => "ERR SCRIPT FLUSH only support SYNC|ASYNC option"u8; + public static ReadOnlySpan RESP_INVALID_COMMAND_SPECIFIED => "Invalid command specified"u8; + public static ReadOnlySpan RESP_COMMAND_HAS_NO_KEY_ARGS => "The command has no key arguments"u8; /// /// Response string templates diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index 5cf5077592..64ddc108d9 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -261,6 +261,8 @@ public enum RespCommand : ushort COMMAND_COUNT, COMMAND_DOCS, COMMAND_INFO, + COMMAND_GETKEYS, + COMMAND_GETKEYSANDFLAGS, MEMORY, // MEMORY_USAGE is a read-only command, so moved up @@ -370,6 +372,8 @@ public static class RespCommandExtensions RespCommand.COMMAND_COUNT, RespCommand.COMMAND_DOCS, RespCommand.COMMAND_INFO, + RespCommand.COMMAND_GETKEYS, + RespCommand.COMMAND_GETKEYSANDFLAGS, RespCommand.MEMORY_USAGE, // Config RespCommand.CONFIG_GET, @@ -1704,6 +1708,16 @@ private RespCommand SlowParseCommand(ref int count, ref ReadOnlySpan speci { return RespCommand.COMMAND_DOCS; } + + if (subCommand.EqualsUpperCaseSpanIgnoringCase(CmdStrings.GETKEYS)) + { + return RespCommand.COMMAND_GETKEYS; + } + + if (subCommand.EqualsUpperCaseSpanIgnoringCase(CmdStrings.GETKEYSANDFLAGS)) + { + return RespCommand.COMMAND_GETKEYSANDFLAGS; + } } else if (command.SequenceEqual(CmdStrings.PING)) { diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index a5b00f020c..bafcdff1d9 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -726,6 +726,8 @@ private bool ProcessOtherCommands(RespCommand command, ref TGarnetAp RespCommand.COMMAND_COUNT => NetworkCOMMAND_COUNT(), RespCommand.COMMAND_DOCS => NetworkCOMMAND_DOCS(), RespCommand.COMMAND_INFO => NetworkCOMMAND_INFO(), + RespCommand.COMMAND_GETKEYS => NetworkCOMMAND_GETKEYS(), + RespCommand.COMMAND_GETKEYSANDFLAGS => NetworkCOMMAND_GETKEYSANDFLAGS(), RespCommand.ECHO => NetworkECHO(), RespCommand.HELLO => NetworkHELLO(), RespCommand.TIME => NetworkTIME(), diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs index c40c688f96..7bbfbea39b 100644 --- a/playground/CommandInfoUpdater/SupportedCommand.cs +++ b/playground/CommandInfoUpdater/SupportedCommand.cs @@ -96,6 +96,8 @@ public class SupportedCommand new("COMMAND|INFO", RespCommand.COMMAND_INFO), new("COMMAND|COUNT", RespCommand.COMMAND_COUNT), new("COMMAND|DOCS", RespCommand.COMMAND_DOCS), + new("COMMAND|COMMAND_GETKEYS", RespCommand.COMMAND_GETKEYS), + new("COMMAND|COMMAND_GETKEYSANDFLAGS", RespCommand.COMMAND_GETKEYSANDFLAGS), ]), new("COMMITAOF", RespCommand.COMMITAOF), new("CONFIG", RespCommand.CONFIG, From c6d06337c7d4fd481d8fbebdef76cd818d455078 Mon Sep 17 00:00:00 2001 From: Vijay-Nirmal Date: Thu, 19 Dec 2024 02:14:59 +0530 Subject: [PATCH 2/7] Added COMMAND GETKEYS and GETKEYSANDFLAGS commands --- libs/resources/RespCommandsDocs.json | 44 ++++++++ libs/resources/RespCommandsInfo.json | 14 +++ libs/server/Resp/BasicCommands.cs | 97 ++++++++++++++--- .../CommandInfoUpdater/CommandDocsUpdater.cs | 2 +- .../CommandInfoUpdater/SupportedCommand.cs | 4 +- test/Garnet.test/RespCommandTests.cs | 102 ++++++++++++++++++ website/docs/commands/api-compatibility.md | 4 +- website/docs/commands/server.md | 36 +++++++ 8 files changed, 284 insertions(+), 19 deletions(-) diff --git a/libs/resources/RespCommandsDocs.json b/libs/resources/RespCommandsDocs.json index b9701595ad..4220f1cf3e 100644 --- a/libs/resources/RespCommandsDocs.json +++ b/libs/resources/RespCommandsDocs.json @@ -1485,6 +1485,50 @@ "ArgumentFlags": "Optional, Multiple" } ] + }, + { + "Command": "COMMAND_GETKEYS", + "Name": "COMMAND|GETKEYS", + "Summary": "Extracts the key names from an arbitrary command.", + "Group": "Server", + "Complexity": "O(N) where N is the number of arguments to the command", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "COMMAND", + "DisplayText": "command", + "Type": "String" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "ARG", + "DisplayText": "arg", + "Type": "String", + "ArgumentFlags": "Optional, Multiple" + } + ] + }, + { + "Command": "COMMAND_GETKEYSANDFLAGS", + "Name": "COMMAND|GETKEYSANDFLAGS", + "Summary": "Extracts the key names and access flags for an arbitrary command.", + "Group": "Server", + "Complexity": "O(N) where N is the number of arguments to the command", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "COMMAND", + "DisplayText": "command", + "Type": "String" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "ARG", + "DisplayText": "arg", + "Type": "String", + "ArgumentFlags": "Optional, Multiple" + } + ] } ] }, diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index b131275ee7..7be37d9214 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -849,6 +849,20 @@ "Tips": [ "nondeterministic_output_order" ] + }, + { + "Command": "COMMAND_GETKEYS", + "Name": "COMMAND|GETKEYS", + "Arity": -3, + "Flags": "Loading, Stale", + "AclCategories": "Connection, Slow" + }, + { + "Command": "COMMAND_GETKEYSANDFLAGS", + "Name": "COMMAND|GETKEYSANDFLAGS", + "Arity": -3, + "Flags": "Loading, Stale", + "AclCategories": "Connection, Slow" } ] }, diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index e0fc25e314..a8c20866e8 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -1261,44 +1261,113 @@ private void ExtractKeys(RespCommandKeySpecification spec, List keyList) if (spec.BeginSearch is BeginSearchIndex bsIndex) { startIndex = bsIndex.Index; + if (startIndex < 0) + startIndex = parseState.Count + startIndex; } else if (spec.BeginSearch is BeginSearchKeyword bsKeyword) { - for (int i = bsKeyword.StartFrom; i < parseState.Count; i++) + // Handle negative StartFrom by converting to positive index from end + int searchStartIndex = bsKeyword.StartFrom; + if (searchStartIndex < 0) { - if (parseState.GetArgSliceByRef(i).ReadOnlySpan.EqualsUpperCaseSpanIgnoringCase(Encoding.ASCII.GetBytes(bsKeyword.Keyword))) + searchStartIndex = parseState.Count + searchStartIndex; // Convert negative index to positive from end + + // Search backwards from the calculated start position for the keyword + for (int i = searchStartIndex; i >= 0; i--) { - startIndex = i + 1; - break; + if (parseState.GetArgSliceByRef(i).ReadOnlySpan.EqualsUpperCaseSpanIgnoringCase(Encoding.ASCII.GetBytes(bsKeyword.Keyword))) + { + startIndex = i + 1; + break; + } + } + } + else + { + // Search forwards from start position for the keyword + for (int i = searchStartIndex; i < parseState.Count; i++) + { + if (parseState.GetArgSliceByRef(i).ReadOnlySpan.EqualsUpperCaseSpanIgnoringCase(Encoding.ASCII.GetBytes(bsKeyword.Keyword))) + { + startIndex = i + 1; + break; + } } } } - if (startIndex >= parseState.Count) + if (startIndex < 0 || startIndex >= parseState.Count) return; if (spec.FindKeys is FindKeysRange range) { - var lastKey = range.LastKey == -1 ? parseState.Count - 1 : - Math.Min(startIndex + range.LastKey, parseState.Count - 1); - + int lastKey; + if (range.LastKey < 0) + { + // For negative LastKey, calculate limit based on the factor + int availableArgs = parseState.Count - startIndex; + int limitFactor = range.Limit <= 1 ? availableArgs : availableArgs / range.Limit; + + // Calculate available slots based on keyStep + int slotsAvailable = (limitFactor + range.KeyStep - 1) / range.KeyStep; + lastKey = startIndex + (slotsAvailable * range.KeyStep) - range.KeyStep; + } + else + { + lastKey = Math.Min(startIndex + range.LastKey, parseState.Count - 1); + } + for (int i = startIndex; i <= lastKey; i += range.KeyStep) { if (i < parseState.Count) { - keyList.Add(parseState.GetArgSliceByRef(i).ToArray()); + var value = parseState.GetArgSliceByRef(i).ToArray(); + if (value.Length != 0) + { + keyList.Add(value); + } } } } else if (spec.FindKeys is FindKeysKeyNum keyNum) { - if (keyNum.KeyNumIdx >= 0 && keyNum.KeyNumIdx < parseState.Count && - parseState.TryGetInt(startIndex + keyNum.KeyNumIdx, out var count)) + int numKeys = 0; + int firstKey = startIndex + keyNum.FirstKey; + + // Handle negative FirstKey + if (keyNum.FirstKey < 0) + firstKey = parseState.Count + keyNum.FirstKey; + + // Get number of keys from the KeyNumIdx + if (keyNum.KeyNumIdx >= 0) { - var firstKey = startIndex + keyNum.FirstKey; - for (int i = 0; i < count && firstKey + i * keyNum.KeyStep < parseState.Count; i++) + var keyNumPos = startIndex + keyNum.KeyNumIdx; + if (keyNumPos < parseState.Count && parseState.TryGetInt(keyNumPos, out var count)) { - keyList.Add(parseState.GetArgSliceByRef(firstKey + i * keyNum.KeyStep).ToArray()); + numKeys = count; + } + } + else + { + // Negative KeyNumIdx means count from the end + var keyNumPos = parseState.Count + keyNum.KeyNumIdx; + if (keyNumPos >= 0 && parseState.TryGetInt(keyNumPos, out var count)) + { + numKeys = count; + } + } + + // Extract keys based on numKeys, firstKey, and keyStep + if (numKeys > 0 && firstKey >= 0) + { + for (int i = 0; i < numKeys && firstKey + i * keyNum.KeyStep < parseState.Count; i++) + { + var keyIndex = firstKey + i * keyNum.KeyStep; + var value = parseState.GetArgSliceByRef(keyIndex).ToArray(); + if (value.Length != 0) + { + keyList.Add(value); + } } } } diff --git a/playground/CommandInfoUpdater/CommandDocsUpdater.cs b/playground/CommandInfoUpdater/CommandDocsUpdater.cs index 51c7fb7586..8775b3e625 100644 --- a/playground/CommandInfoUpdater/CommandDocsUpdater.cs +++ b/playground/CommandInfoUpdater/CommandDocsUpdater.cs @@ -233,7 +233,7 @@ private static IReadOnlyDictionary GetUpdatedCommandsDo } // Update commands docs with commands to add - foreach (var command in commandsToAdd.Keys) + foreach (var command in commandsToAdd.Keys.Where(x => x.Command.StartsWith("COMMAND"))) { RespCommandDocs baseCommandDocs; List updatedSubCommandsDocs; diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs index a738c4182c..3b59e38207 100644 --- a/playground/CommandInfoUpdater/SupportedCommand.cs +++ b/playground/CommandInfoUpdater/SupportedCommand.cs @@ -96,8 +96,8 @@ public class SupportedCommand new("COMMAND|INFO", RespCommand.COMMAND_INFO), new("COMMAND|COUNT", RespCommand.COMMAND_COUNT), new("COMMAND|DOCS", RespCommand.COMMAND_DOCS), - new("COMMAND|COMMAND_GETKEYS", RespCommand.COMMAND_GETKEYS), - new("COMMAND|COMMAND_GETKEYSANDFLAGS", RespCommand.COMMAND_GETKEYSANDFLAGS), + new("COMMAND|GETKEYS", RespCommand.COMMAND_GETKEYS), + new("COMMAND|GETKEYSANDFLAGS", RespCommand.COMMAND_GETKEYSANDFLAGS), ]), new("COMMITAOF", RespCommand.COMMITAOF), new("CONFIG", RespCommand.CONFIG, diff --git a/test/Garnet.test/RespCommandTests.cs b/test/Garnet.test/RespCommandTests.cs index 8059ee3c25..5b2071f91b 100644 --- a/test/Garnet.test/RespCommandTests.cs +++ b/test/Garnet.test/RespCommandTests.cs @@ -535,5 +535,107 @@ private void VerifyCommandDocs(string cmdName, RedisResult result) } } } + + /// + /// Test COMMAND GETKEYS command with various command signatures + /// + [Test] + [TestCase("SET", new[] { "mykey" }, new[] { "mykey", "value" }, Description = "Simple SET command")] + [TestCase("MSET", new[] { "key1", "key2" }, new[] { "key1", "value1", "key2", "value2" }, Description = "Multiple SET pairs")] + [TestCase("MGET", new[] { "key1", "key2", "key3" }, new[] { "key1", "key2", "key3" }, Description = "Multiple GET keys")] + [TestCase("ZUNIONSTORE", new[] { "destination", "key1", "key2" }, new[] { "destination", "2", "key1", "key2" }, Description = "ZUNIONSTORE with multiple source keys")] + [TestCase("EVAL", new[] { "key1", "key2" }, new[] { "key1", "key2" }, new[] { "return redis.call('GET', KEYS[1])", "2", "key1", "key2" }, Description = "EVAL with multiple keys")] + [TestCase("EXPIRE", new[] { "mykey" }, new[] { "mykey", "100", "NX" }, Description = "EXPIRE with NX option")] + [TestCase("MIGRATE", new[] { "key1", "key2" }, new[] { "127.0.0.1", "6379", "", "0", "5000", "KEYS", "key1", "key2" }, Description = "MIGRATE with multiple keys")] + [TestCase("GEOSEARCHSTORE", new[] { "dst", "src" }, new[] { "dst", "src", "FROMMEMBER", "member", "COUNT", "10", "ASC" }, Description = "GEOSEARCHSTORE with options")] + public void CommandGetKeysTest(string command, string[] expectedKeys, string[] args) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var cmdArgs = new object[] { "GETKEYS", command }.Union(args).ToArray(); + var results = (RedisResult[])db.Execute("COMMAND", cmdArgs); + + ClassicAssert.IsNotNull(results); + ClassicAssert.AreEqual(expectedKeys.Length, results.Length); + + for (var i = 0; i < expectedKeys.Length; i++) + { + ClassicAssert.AreEqual(expectedKeys[i], results[i].ToString()); + } + } + + /// + /// Test COMMAND GETKEYSANDFLAGS command with various command signatures + /// + [Test] + [TestCase("SET", "mykey", "RW access update variable_flags", new[] { "mykey", "value" }, Description = "Simple SET command")] + [TestCase("MSET", "key1,key2", "OW update|OW update", new[] { "key1", "value1", "key2", "value2" }, Description = "Multiple SET pairs")] + [TestCase("MGET", "key1,key2,key3", "RO access|RO access|RO access", new[] { "key1", "key2", "key3" }, Description = "Multiple GET keys")] + [TestCase("ZUNIONSTORE", "destination,key1,key2", "OW update|RO access|RO access", new[] { "destination", "2", "key1", "key2" }, Description = "ZUNIONSTORE with multiple source keys")] + [TestCase("EVAL", "key1,key2", "RW access update|RW access update", new[] { "return redis.call('GET', KEYS[1])", "2", "key1", "key2" }, Description = "EVAL with multiple keys")] + [TestCase("EXPIRE", "mykey", "RW update", new[] { "mykey", "100", "NX" }, Description = "EXPIRE with NX option")] + [TestCase("MIGRATE", "key1,key2", "RW access delete incomplete|RW access delete incomplete", new[] { "127.0.0.1", "6379", "", "0", "5000", "KEYS", "key1", "key2" }, Description = "MIGRATE with multiple keys")] + [TestCase("GEOSEARCHSTORE", "dst,src", "OW update|RO access", new[] { "dst", "src", "FROMMEMBER", "member", "COUNT", "10", "ASC" }, Description = "GEOSEARCHSTORE with options")] + public void CommandGetKeysAndFlagsTest(string command, string expectedKeysStr, string expectedFlagsStr, string[] args) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var expectedKeys = expectedKeysStr.Split(','); + var expectedFlags = expectedFlagsStr.Split('|') + .Select(f => f.Split(' ', StringSplitOptions.RemoveEmptyEntries)) + .ToArray(); + + var cmdArgs = new object[] { "GETKEYSANDFLAGS", command }.Union(args).ToArray(); + var results = (RedisResult[])db.Execute("COMMAND", cmdArgs); + + ClassicAssert.IsNotNull(results); + ClassicAssert.AreEqual(expectedKeys.Length, results.Length); + + for (var i = 0; i < expectedKeys.Length; i++) + { + var keyInfo = (RedisResult[])results[i]; + ClassicAssert.AreEqual(2, keyInfo.Length); + ClassicAssert.AreEqual(expectedKeys[i], keyInfo[0].ToString()); + + var flags = ((RedisResult[])keyInfo[1]).Select(f => f.ToString()).ToArray(); + CollectionAssert.AreEquivalent(expectedFlags[i], flags); + } + } + + /// + /// Test COMMAND GETKEYS and GETKEYSANDFLAGS with invalid input + /// + [Test] + [TestCase("GETKEYS", Description = "GETKEYS with no command")] + [TestCase("GETKEYSANDFLAGS", Description = "GETKEYSANDFLAGS with no command")] + public void CommandGetKeysInvalidInputTest(string subcommand) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + // Test with no arguments + try + { + db.Execute("COMMAND", subcommand); + Assert.Fail("Should throw exception for missing command argument"); + } + catch (RedisServerException e) + { + ClassicAssert.AreEqual("ERR Invalid arguments specified for command", e.Message); + } + + // Test with invalid command + try + { + db.Execute("COMMAND", subcommand, "INVALIDCOMMAND", "key1"); + Assert.Fail("Should throw exception for invalid command"); + } + catch (RedisServerException e) + { + ClassicAssert.AreEqual("ERR Invalid command specified", e.Message); + } + } } } \ No newline at end of file diff --git a/website/docs/commands/api-compatibility.md b/website/docs/commands/api-compatibility.md index 3e2e1f3d36..9aaaab218d 100644 --- a/website/docs/commands/api-compatibility.md +++ b/website/docs/commands/api-compatibility.md @@ -111,8 +111,8 @@ Note that this list is subject to change as we continue to expand our API comman | **COMMAND** | [COMMAND](server.md#command) | ➕ | | | | [COUNT](server.md#command-count) | ➕ | | | | [DOCS](server.md#command-docs) | ➕ | | -| | GETKEYS | ➖ | | -| | GETKEYSANDFLAGS | ➖ | | +| | [GETKEYS](server.md#command-getkeys) | ➖ | | +| | [GETKEYSANDFLAGS](server.md#command-getkeysandflags) | ➖ | | | | HELP | ➖ | | | | [INFO](server.md#command-info) | ➕ | | | | LIST | ➖ | | diff --git a/website/docs/commands/server.md b/website/docs/commands/server.md index f7a9a92c8b..17eec8c916 100644 --- a/website/docs/commands/server.md +++ b/website/docs/commands/server.md @@ -49,6 +49,42 @@ By default, the reply includes all of the server's commands. You can use the opt Array reply: a map, as a flattened array, where each key is a command name, and each value is the documentary information. +--- +### COMMAND GETKEYS +#### Syntax + +```bash +COMMAND GETKEYS command-name [arg [arg ...]] +``` + +Returns an array of keys that would be accessed by the given command. + +* `command-name`: The name of the command to analyze +* `arg`: The arguments that would be passed to the command + +#### Resp Reply + +Array reply: a list of keys that the command would access. + +--- +### COMMAND GETKEYSANDFLAGS +#### Syntax + +```bash +COMMAND GETKEYSANDFLAGS command-name [arg [arg ...]] +``` + +Returns an array of key names and access flags for keys that would be accessed by the given command. + +* `command-name`: The name of the command to analyze +* `arg`: The arguments that would be passed to the command + +#### Resp Reply + +Array reply: a nested array where each item contains: +1. The key name +2. An array of access flag strings that apply to that key + --- ### COMMAND INFO #### Syntax From f5ec1cd694a1458ed4eedc30f5df34628844ffd7 Mon Sep 17 00:00:00 2001 From: Vijay-Nirmal Date: Thu, 19 Dec 2024 12:25:04 +0530 Subject: [PATCH 3/7] Fixed code format --- libs/server/Resp/BasicCommands.cs | 7 ++-- test/Garnet.test/Resp/ACL/RespCommandTests.cs | 32 +++++++++++++++++++ test/Garnet.test/RespCommandTests.cs | 6 ++-- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index a8c20866e8..672321d5d1 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -1270,8 +1270,9 @@ private void ExtractKeys(RespCommandKeySpecification spec, List keyList) int searchStartIndex = bsKeyword.StartFrom; if (searchStartIndex < 0) { - searchStartIndex = parseState.Count + searchStartIndex; // Convert negative index to positive from end - + // Convert negative index to positive from end + searchStartIndex = parseState.Count + searchStartIndex; + // Search backwards from the calculated start position for the keyword for (int i = searchStartIndex; i >= 0; i--) { @@ -1307,7 +1308,7 @@ private void ExtractKeys(RespCommandKeySpecification spec, List keyList) // For negative LastKey, calculate limit based on the factor int availableArgs = parseState.Count - startIndex; int limitFactor = range.Limit <= 1 ? availableArgs : availableArgs / range.Limit; - + // Calculate available slots based on keyStep int slotsAvailable = (limitFactor + range.KeyStep - 1) / range.KeyStep; lastKey = startIndex + (slotsAvailable * range.KeyStep) - range.KeyStep; diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index bf47124bdb..1d4b6a58dd 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -12,6 +12,7 @@ using Garnet.server.ACL; using NUnit.Framework; using NUnit.Framework.Legacy; +using StackExchange.Redis; namespace Garnet.test.Resp.ACL { @@ -2259,6 +2260,37 @@ static async Task DoCommandDocsMultiAsync(GarnetClient client) } } + [Test] + public async Task CommandGetKeysAsync() + { + await CheckCommandsAsync( + "COMMAND GETKEYS", + [DoCommandGetKeysAsync] + ); + + static async Task DoCommandGetKeysAsync(GarnetClient client) + { + string[] res = await client.ExecuteForStringArrayResultAsync("COMMAND", ["GETKEYS", "SET", "mykey", "value"]); + ClassicAssert.IsNotNull(res); + ClassicAssert.Contains("mykey", res); + } + } + + [Test] + public async Task CommandGetKeysAndFlagsAsync() + { + await CheckCommandsAsync( + "COMMAND GETKEYSANDFLAGS", + [DoCommandGetKeysAndFlagsAsync] + ); + + static async Task DoCommandGetKeysAndFlagsAsync(GarnetClient client) + { + var res = await client.ExecuteForStringArrayResultAsync("COMMAND", ["GETKEYSANDFLAGS", "EVAL", "return redis.call('TIME')", "0"]); + ClassicAssert.AreEqual(0, res.Length); + } + } + [Test] public async Task CommitAOFACLsAsync() { diff --git a/test/Garnet.test/RespCommandTests.cs b/test/Garnet.test/RespCommandTests.cs index 5b2071f91b..7d9499d539 100644 --- a/test/Garnet.test/RespCommandTests.cs +++ b/test/Garnet.test/RespCommandTests.cs @@ -558,7 +558,7 @@ public void CommandGetKeysTest(string command, string[] expectedKeys, string[] a ClassicAssert.IsNotNull(results); ClassicAssert.AreEqual(expectedKeys.Length, results.Length); - + for (var i = 0; i < expectedKeys.Length; i++) { ClassicAssert.AreEqual(expectedKeys[i], results[i].ToString()); @@ -592,13 +592,13 @@ public void CommandGetKeysAndFlagsTest(string command, string expectedKeysStr, s ClassicAssert.IsNotNull(results); ClassicAssert.AreEqual(expectedKeys.Length, results.Length); - + for (var i = 0; i < expectedKeys.Length; i++) { var keyInfo = (RedisResult[])results[i]; ClassicAssert.AreEqual(2, keyInfo.Length); ClassicAssert.AreEqual(expectedKeys[i], keyInfo[0].ToString()); - + var flags = ((RedisResult[])keyInfo[1]).Select(f => f.ToString()).ToArray(); CollectionAssert.AreEquivalent(expectedFlags[i], flags); } From 3516f7ac4f0bb757e4df29a08beea62ac3b5aecd Mon Sep 17 00:00:00 2001 From: Vijay-Nirmal Date: Thu, 19 Dec 2024 13:05:38 +0530 Subject: [PATCH 4/7] Fixed test failures --- test/Garnet.test/Resp/ACL/RespCommandTests.cs | 4 +-- test/Garnet.test/RespCommandTests.cs | 27 ++++--------------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index 1d4b6a58dd..f6fbb46e30 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -2261,7 +2261,7 @@ static async Task DoCommandDocsMultiAsync(GarnetClient client) } [Test] - public async Task CommandGetKeysAsync() + public async Task CommandGetKeysACLsAsync() { await CheckCommandsAsync( "COMMAND GETKEYS", @@ -2277,7 +2277,7 @@ static async Task DoCommandGetKeysAsync(GarnetClient client) } [Test] - public async Task CommandGetKeysAndFlagsAsync() + public async Task CommandGetKeysAndFlagsACLsAsync() { await CheckCommandsAsync( "COMMAND GETKEYSANDFLAGS", diff --git a/test/Garnet.test/RespCommandTests.cs b/test/Garnet.test/RespCommandTests.cs index 7d9499d539..4e343bc8d0 100644 --- a/test/Garnet.test/RespCommandTests.cs +++ b/test/Garnet.test/RespCommandTests.cs @@ -373,6 +373,8 @@ public void AofIndependentCommandsTest() RespCommand.COMMAND_COUNT, RespCommand.COMMAND_DOCS, RespCommand.COMMAND_INFO, + RespCommand.COMMAND_GETKEYS, + RespCommand.COMMAND_GETKEYSANDFLAGS, RespCommand.MEMORY_USAGE, // Config RespCommand.CONFIG_GET, @@ -544,7 +546,7 @@ private void VerifyCommandDocs(string cmdName, RedisResult result) [TestCase("MSET", new[] { "key1", "key2" }, new[] { "key1", "value1", "key2", "value2" }, Description = "Multiple SET pairs")] [TestCase("MGET", new[] { "key1", "key2", "key3" }, new[] { "key1", "key2", "key3" }, Description = "Multiple GET keys")] [TestCase("ZUNIONSTORE", new[] { "destination", "key1", "key2" }, new[] { "destination", "2", "key1", "key2" }, Description = "ZUNIONSTORE with multiple source keys")] - [TestCase("EVAL", new[] { "key1", "key2" }, new[] { "key1", "key2" }, new[] { "return redis.call('GET', KEYS[1])", "2", "key1", "key2" }, Description = "EVAL with multiple keys")] + [TestCase("EVAL", new[] { "key1", "key2" }, new[] { "return redis.call('GET', KEYS[1])", "2", "key1", "key2" }, Description = "EVAL with multiple keys")] [TestCase("EXPIRE", new[] { "mykey" }, new[] { "mykey", "100", "NX" }, Description = "EXPIRE with NX option")] [TestCase("MIGRATE", new[] { "key1", "key2" }, new[] { "127.0.0.1", "6379", "", "0", "5000", "KEYS", "key1", "key2" }, Description = "MIGRATE with multiple keys")] [TestCase("GEOSEARCHSTORE", new[] { "dst", "src" }, new[] { "dst", "src", "FROMMEMBER", "member", "COUNT", "10", "ASC" }, Description = "GEOSEARCHSTORE with options")] @@ -615,27 +617,8 @@ public void CommandGetKeysInvalidInputTest(string subcommand) using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - // Test with no arguments - try - { - db.Execute("COMMAND", subcommand); - Assert.Fail("Should throw exception for missing command argument"); - } - catch (RedisServerException e) - { - ClassicAssert.AreEqual("ERR Invalid arguments specified for command", e.Message); - } - - // Test with invalid command - try - { - db.Execute("COMMAND", subcommand, "INVALIDCOMMAND", "key1"); - Assert.Fail("Should throw exception for invalid command"); - } - catch (RedisServerException e) - { - ClassicAssert.AreEqual("ERR Invalid command specified", e.Message); - } + Assert.Throws(() => db.Execute("COMMAND", subcommand)); + Assert.Throws(() => db.Execute("COMMAND", subcommand, "INVALIDCOMMAND", "key1")); } } } \ No newline at end of file From 9d4c36d81763123acd3196d8a6a3d9481d6eb76f Mon Sep 17 00:00:00 2001 From: Vijay-Nirmal Date: Tue, 14 Jan 2025 22:44:53 +0530 Subject: [PATCH 5/7] Fixed review commands --- libs/server/Resp/BasicCommands.cs | 157 ++---------------- libs/server/Resp/Parser/SessionParseState.cs | 1 + .../Resp/Parser/SessionParseStateExtension.cs | 89 ++++++++++ .../Resp/RespCommandKeySpecification.cs | 110 ++++++++++++ .../CommandInfoUpdater/CommandDocsUpdater.cs | 2 +- 5 files changed, 212 insertions(+), 147 deletions(-) create mode 100644 libs/server/Resp/Parser/SessionParseStateExtension.cs diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 672321d5d1..ad0fc935a2 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -1174,18 +1174,15 @@ private bool NetworkCOMMAND_GETKEYS() return AbortWithErrorMessage(CmdStrings.RESP_COMMAND_HAS_NO_KEY_ARGS); } - var keyList = new List(); - foreach (var spec in cmdInfo.KeySpecifications) - { - ExtractKeys(spec, keyList); - } + parseState.TryExtractKeysFromSpecs(cmdInfo.KeySpecifications, out var keys); + - while (!RespWriteUtils.WriteArrayLength(keyList.Count, ref dcurr, dend)) + while (!RespWriteUtils.WriteArrayLength(keys.Count, ref dcurr, dend)) SendAndReset(); - foreach (var key in keyList) + foreach (var key in keys) { - while (!RespWriteUtils.WriteBulkString(key, ref dcurr, dend)) + while (!RespWriteUtils.WriteBulkString(key.Span, ref dcurr, dend)) SendAndReset(); } @@ -1216,35 +1213,23 @@ private bool NetworkCOMMAND_GETKEYSANDFLAGS() return AbortWithErrorMessage(CmdStrings.RESP_COMMAND_HAS_NO_KEY_ARGS); } - var keyList = new List(); - var flagsList = new List(); + parseState.TryExtractKeyandFlagsFromSpecs(cmdInfo.KeySpecifications, out var keys, out var flags); - foreach (var spec in cmdInfo.KeySpecifications) - { - var keyCount = keyList.Count; - ExtractKeys(spec, keyList); - var flags = EnumUtils.GetEnumDescriptions(spec.Flags); - for (int i = keyCount; i < keyList.Count; i++) - { - flagsList.Add(flags); - } - } - - while (!RespWriteUtils.WriteArrayLength(keyList.Count, ref dcurr, dend)) + while (!RespWriteUtils.WriteArrayLength(keys.Count, ref dcurr, dend)) SendAndReset(); - for (int i = 0; i < keyList.Count; i++) + for (int i = 0; i < keys.Count; i++) { while (!RespWriteUtils.WriteArrayLength(2, ref dcurr, dend)) SendAndReset(); - while (!RespWriteUtils.WriteBulkString(keyList[i], ref dcurr, dend)) + while (!RespWriteUtils.WriteBulkString(keys[i].Span, ref dcurr, dend)) SendAndReset(); - while (!RespWriteUtils.WriteArrayLength(flagsList[i].Length, ref dcurr, dend)) + while (!RespWriteUtils.WriteArrayLength(flags[i].Length, ref dcurr, dend)) SendAndReset(); - foreach (var flag in flagsList[i]) + foreach (var flag in flags[i]) { while (!RespWriteUtils.WriteBulkString(Encoding.ASCII.GetBytes(flag), ref dcurr, dend)) SendAndReset(); @@ -1254,126 +1239,6 @@ private bool NetworkCOMMAND_GETKEYSANDFLAGS() return true; } - private void ExtractKeys(RespCommandKeySpecification spec, List keyList) - { - int startIndex = 0; - - if (spec.BeginSearch is BeginSearchIndex bsIndex) - { - startIndex = bsIndex.Index; - if (startIndex < 0) - startIndex = parseState.Count + startIndex; - } - else if (spec.BeginSearch is BeginSearchKeyword bsKeyword) - { - // Handle negative StartFrom by converting to positive index from end - int searchStartIndex = bsKeyword.StartFrom; - if (searchStartIndex < 0) - { - // Convert negative index to positive from end - searchStartIndex = parseState.Count + searchStartIndex; - - // Search backwards from the calculated start position for the keyword - for (int i = searchStartIndex; i >= 0; i--) - { - if (parseState.GetArgSliceByRef(i).ReadOnlySpan.EqualsUpperCaseSpanIgnoringCase(Encoding.ASCII.GetBytes(bsKeyword.Keyword))) - { - startIndex = i + 1; - break; - } - } - } - else - { - // Search forwards from start position for the keyword - for (int i = searchStartIndex; i < parseState.Count; i++) - { - if (parseState.GetArgSliceByRef(i).ReadOnlySpan.EqualsUpperCaseSpanIgnoringCase(Encoding.ASCII.GetBytes(bsKeyword.Keyword))) - { - startIndex = i + 1; - break; - } - } - } - } - - if (startIndex < 0 || startIndex >= parseState.Count) - return; - - if (spec.FindKeys is FindKeysRange range) - { - int lastKey; - if (range.LastKey < 0) - { - // For negative LastKey, calculate limit based on the factor - int availableArgs = parseState.Count - startIndex; - int limitFactor = range.Limit <= 1 ? availableArgs : availableArgs / range.Limit; - - // Calculate available slots based on keyStep - int slotsAvailable = (limitFactor + range.KeyStep - 1) / range.KeyStep; - lastKey = startIndex + (slotsAvailable * range.KeyStep) - range.KeyStep; - } - else - { - lastKey = Math.Min(startIndex + range.LastKey, parseState.Count - 1); - } - - for (int i = startIndex; i <= lastKey; i += range.KeyStep) - { - if (i < parseState.Count) - { - var value = parseState.GetArgSliceByRef(i).ToArray(); - if (value.Length != 0) - { - keyList.Add(value); - } - } - } - } - else if (spec.FindKeys is FindKeysKeyNum keyNum) - { - int numKeys = 0; - int firstKey = startIndex + keyNum.FirstKey; - - // Handle negative FirstKey - if (keyNum.FirstKey < 0) - firstKey = parseState.Count + keyNum.FirstKey; - - // Get number of keys from the KeyNumIdx - if (keyNum.KeyNumIdx >= 0) - { - var keyNumPos = startIndex + keyNum.KeyNumIdx; - if (keyNumPos < parseState.Count && parseState.TryGetInt(keyNumPos, out var count)) - { - numKeys = count; - } - } - else - { - // Negative KeyNumIdx means count from the end - var keyNumPos = parseState.Count + keyNum.KeyNumIdx; - if (keyNumPos >= 0 && parseState.TryGetInt(keyNumPos, out var count)) - { - numKeys = count; - } - } - - // Extract keys based on numKeys, firstKey, and keyStep - if (numKeys > 0 && firstKey >= 0) - { - for (int i = 0; i < numKeys && firstKey + i * keyNum.KeyStep < parseState.Count; i++) - { - var keyIndex = firstKey + i * keyNum.KeyStep; - var value = parseState.GetArgSliceByRef(keyIndex).ToArray(); - if (value.Length != 0) - { - keyList.Add(value); - } - } - } - } - } - private bool NetworkECHO() { if (parseState.Count != 1) diff --git a/libs/server/Resp/Parser/SessionParseState.cs b/libs/server/Resp/Parser/SessionParseState.cs index 3211d75179..bc76fbd477 100644 --- a/libs/server/Resp/Parser/SessionParseState.cs +++ b/libs/server/Resp/Parser/SessionParseState.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; using Garnet.common; using Garnet.common.Parsing; using Tsavorite.core; diff --git a/libs/server/Resp/Parser/SessionParseStateExtension.cs b/libs/server/Resp/Parser/SessionParseStateExtension.cs new file mode 100644 index 0000000000..78447839ae --- /dev/null +++ b/libs/server/Resp/Parser/SessionParseStateExtension.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.Collections.Generic; + +namespace Garnet.server +{ + /// + /// Extension methods for the SessionParseState struct. + /// + internal static class SessionParseStateExtension + { + /// + /// Tries to extract keys from the key specifications in the given RespCommandsInfo. + /// + /// The SessionParseState instance. + /// The RespCommandKeySpecification array contains the key specification + /// The list to store extracted keys. + /// True if keys were successfully extracted, otherwise false. + internal static bool TryExtractKeysFromSpecs(this ref SessionParseState state, RespCommandKeySpecification[] keySpecs, out List keys) + { + keys = new(); + + foreach (var spec in keySpecs) + { + if(!ExtractKeysFromSpec(ref state, keys, spec)) + { + return false; + } + } + + return true; + } + + internal static bool TryExtractKeyandFlagsFromSpecs(this ref SessionParseState state, RespCommandKeySpecification[] keySpecs, out List keys, out List flags) + { + keys = new(); + flags = new(); + + foreach (var spec in keySpecs) + { + var prevKeyCount = keys.Count; + if (!ExtractKeysFromSpec(ref state, keys, spec)) + { + return false; + } + + var keyFlags = spec.RespFormatFlags; + for (int i = prevKeyCount; i < keys.Count; i++) + { + flags.Add(keyFlags); + } + } + + return true; + } + + private static bool ExtractKeysFromSpec(ref SessionParseState state, List keys, RespCommandKeySpecification spec) + { + int startIndex = 0; + + if (spec.BeginSearch is BeginSearchIndex bsIndex) + { + startIndex = bsIndex.GetIndex(ref state); + } + else if (spec.BeginSearch is BeginSearchKeyword bsKeyword) + { + if (!bsKeyword.TryGetStartFrom(ref state, out startIndex)) + { + return false; + } + } + + if (startIndex < 0 || startIndex >= state.Count) + return false; + + if (spec.FindKeys is FindKeysRange range) + { + range.ExtractKeys(ref state, startIndex, keys); + } + else if (spec.FindKeys is FindKeysKeyNum keyNum) + { + keyNum.ExtractKeys(ref state, startIndex, keys); + } + + return true; + } + } +} \ No newline at end of file diff --git a/libs/server/Resp/RespCommandKeySpecification.cs b/libs/server/Resp/RespCommandKeySpecification.cs index f11292b8a9..3b399e02f4 100644 --- a/libs/server/Resp/RespCommandKeySpecification.cs +++ b/libs/server/Resp/RespCommandKeySpecification.cs @@ -2,10 +2,13 @@ // Licensed under the MIT license. using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Reflection; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using Azure; using Garnet.common; namespace Garnet.server @@ -51,6 +54,9 @@ public KeySpecificationFlags Flags [JsonIgnore] public string RespFormat => respFormat ??= ToRespFormat(); + [JsonIgnore] + public string[] RespFormatFlags => respFormatFlags; + private string respFormat; private readonly KeySpecificationFlags flags; private readonly string[] respFormatFlags; @@ -223,6 +229,16 @@ public BeginSearchIndex(int index) : this() { this.Index = index; } + + public int GetIndex(ref SessionParseState parseState) + { + if (Index < 0) + { + return parseState.Count + Index; + } + + return Index; + } } /// @@ -263,6 +279,32 @@ public BeginSearchKeyword(string keyword, int startFrom) : this() this.Keyword = keyword; this.StartFrom = startFrom; } + + public bool TryGetStartFrom(ref SessionParseState parseState, out int index) + { + var keyword = Encoding.UTF8.GetBytes(Keyword); + + // Handle negative StartFrom by converting to positive index from end + int searchStartIndex = StartFrom < 0 ? parseState.Count + StartFrom : StartFrom; + + // Determine the search direction + int increment = StartFrom < 0 ? -1 : 1; + int start = StartFrom < 0 ? searchStartIndex : searchStartIndex; + int end = StartFrom < 0 ? -1 : parseState.Count; + + // Search for the keyword + for (int i = start; i != end; i += increment) + { + if (parseState.GetArgSliceByRef(i).ReadOnlySpan.EqualsUpperCaseSpanIgnoringCase(keyword)) + { + index = i + 1; + return true; + } + } + + index = default; + return false; + } } /// @@ -339,6 +381,35 @@ public FindKeysRange(int lastKey, int keyStep, int limit) : this() this.KeyStep = keyStep; this.Limit = limit; } + + public void ExtractKeys(ref SessionParseState state, int startIndex, List keys) + { + int lastKey; + if (LastKey < 0) + { + // For negative LastKey, calculate limit based on the factor + int availableArgs = state.Count - startIndex; + int limitFactor = Limit <= 1 ? availableArgs : availableArgs / Limit; + + // Calculate available slots based on keyStep + int slotsAvailable = (limitFactor + KeyStep - 1) / KeyStep; + lastKey = startIndex + (slotsAvailable * KeyStep) - KeyStep; + lastKey = Math.Min(lastKey, state.Count - 1); + } + else + { + lastKey = Math.Min(startIndex + LastKey, state.Count - 1); + } + + for (int i = startIndex; i <= lastKey; i += KeyStep) + { + var argSlice = state.GetArgSliceByRef(i); + if (argSlice.length > 0) + { + keys.Add(argSlice); + } + } + } } /// @@ -385,6 +456,45 @@ public FindKeysKeyNum(int keyNumIdx, int firstKey, int keyStep) : this() this.FirstKey = firstKey; this.KeyStep = keyStep; } + + public void ExtractKeys(ref SessionParseState state, int startIndex, List keys) + { + int numKeys = 0; + int firstKey = startIndex + FirstKey; + + // Handle negative FirstKey + if (FirstKey < 0) + firstKey = state.Count + FirstKey; + + // Get number of keys from the KeyNumIdx + if (KeyNumIdx >= 0) + { + var keyNumPos = startIndex + KeyNumIdx; + if (keyNumPos < state.Count && state.TryGetInt(keyNumPos, out var count)) + { + numKeys = count; + } + } + else + { + // Negative KeyNumIdx means count from the end + var keyNumPos = state.Count + KeyNumIdx; + if (keyNumPos >= 0 && state.TryGetInt(keyNumPos, out var count)) + { + numKeys = count; + } + } + + // Extract keys based on numKeys, firstKey, and keyStep + if (numKeys > 0 && firstKey >= 0) + { + for (int i = 0; i < numKeys && firstKey + i * KeyStep < state.Count; i++) + { + var keyIndex = firstKey + i * KeyStep; + keys.Add(state.GetArgSliceByRef(keyIndex)); + } + } + } } /// diff --git a/playground/CommandInfoUpdater/CommandDocsUpdater.cs b/playground/CommandInfoUpdater/CommandDocsUpdater.cs index 8775b3e625..51c7fb7586 100644 --- a/playground/CommandInfoUpdater/CommandDocsUpdater.cs +++ b/playground/CommandInfoUpdater/CommandDocsUpdater.cs @@ -233,7 +233,7 @@ private static IReadOnlyDictionary GetUpdatedCommandsDo } // Update commands docs with commands to add - foreach (var command in commandsToAdd.Keys.Where(x => x.Command.StartsWith("COMMAND"))) + foreach (var command in commandsToAdd.Keys) { RespCommandDocs baseCommandDocs; List updatedSubCommandsDocs; From f503361416c768bc911db5c30f75bece40ae2480 Mon Sep 17 00:00:00 2001 From: Vijay-Nirmal Date: Tue, 14 Jan 2025 22:46:09 +0530 Subject: [PATCH 6/7] Fixed all review commands --- libs/server/Resp/BasicCommands.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index ad0fc935a2..bf7c46ca4c 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -1160,7 +1160,7 @@ private bool NetworkCOMMAND_GETKEYS() return AbortWithWrongNumberOfArguments(nameof(RespCommand.COMMAND_GETKEYS)); } - var cmdName = parseState.GetString(0).ToUpperInvariant(); + var cmdName = parseState.GetString(0); bool cmdFound = RespCommandsInfo.TryGetRespCommandInfo(cmdName, out var cmdInfo, true, true, logger) || storeWrapper.customCommandManager.TryGetCustomCommandInfo(cmdName, out cmdInfo); @@ -1199,7 +1199,7 @@ private bool NetworkCOMMAND_GETKEYSANDFLAGS() return AbortWithWrongNumberOfArguments(nameof(RespCommand.COMMAND_GETKEYSANDFLAGS)); } - var cmdName = parseState.GetString(0).ToUpperInvariant(); + var cmdName = parseState.GetString(0); bool cmdFound = RespCommandsInfo.TryGetRespCommandInfo(cmdName, out var cmdInfo, true, true, logger) || storeWrapper.customCommandManager.TryGetCustomCommandInfo(cmdName, out cmdInfo); From 50ac2be1cf1e5af66aeb2eb9d3d2bf664b8df7e1 Mon Sep 17 00:00:00 2001 From: Vijay-Nirmal Date: Tue, 14 Jan 2025 22:59:11 +0530 Subject: [PATCH 7/7] Code format fix --- libs/server/Resp/Parser/SessionParseStateExtension.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server/Resp/Parser/SessionParseStateExtension.cs b/libs/server/Resp/Parser/SessionParseStateExtension.cs index 78447839ae..238899467e 100644 --- a/libs/server/Resp/Parser/SessionParseStateExtension.cs +++ b/libs/server/Resp/Parser/SessionParseStateExtension.cs @@ -23,7 +23,7 @@ internal static bool TryExtractKeysFromSpecs(this ref SessionParseState state, R foreach (var spec in keySpecs) { - if(!ExtractKeysFromSpec(ref state, keys, spec)) + if (!ExtractKeysFromSpec(ref state, keys, spec)) { return false; }