From a4b47b4da2126aea3a14acdcc9fe4d76ed8d22d4 Mon Sep 17 00:00:00 2001 From: Vijay Nirmal Date: Fri, 18 Oct 2024 20:06:07 +0530 Subject: [PATCH] [Compatibility] Added SMISMEMBER command (#713) * Added SMISMEMBER command * Added doc for SMISMEMBER * Code format fix * Fixed test issue * Fixed test issue finally * Finial fix for the previous final fix for test case * Added Garnet API * Fixed code format * Fixed build issue --------- Co-authored-by: Yoganand Rajasekaran <60369795+yrajas@users.noreply.github.com> --- libs/resources/RespCommandsDocs.json | 23 ++++++++ libs/resources/RespCommandsInfo.json | 25 +++++++++ libs/server/API/GarnetApiObjectCommands.cs | 4 ++ libs/server/API/GarnetWatchApi.cs | 7 +++ libs/server/API/IGarnetApi.cs | 8 +++ libs/server/Objects/Set/SetObject.cs | 4 ++ libs/server/Objects/Set/SetObjectImpl.cs | 39 +++++++++++++ libs/server/Resp/Objects/SetCommands.cs | 42 ++++++++++++-- libs/server/Resp/Parser/RespCommand.cs | 9 ++- libs/server/Resp/RespServerSession.cs | 3 +- .../Storage/Session/ObjectStore/Common.cs | 56 +++++++++++++++++++ .../Storage/Session/ObjectStore/SetOps.cs | 35 ++++++++++++ libs/server/Transaction/TxnKeyManager.cs | 2 + .../CommandInfoUpdater/SupportedCommand.cs | 1 + .../RedirectTests/BaseCommand.cs | 17 ++++++ .../ClusterSlotVerificationTests.cs | 7 +++ test/Garnet.test/Resp/ACL/RespCommandTests.cs | 15 +++++ test/Garnet.test/RespSetTest.cs | 52 +++++++++++++++++ test/Garnet.test/TestProcedureSet.cs | 4 ++ website/docs/commands/api-compatibility.md | 2 +- website/docs/commands/data-structures.md | 16 ++++++ 21 files changed, 361 insertions(+), 10 deletions(-) diff --git a/libs/resources/RespCommandsDocs.json b/libs/resources/RespCommandsDocs.json index 064dc87708..2315aff5b0 100644 --- a/libs/resources/RespCommandsDocs.json +++ b/libs/resources/RespCommandsDocs.json @@ -4747,6 +4747,29 @@ } ] }, + { + "Command": "SMISMEMBER", + "Name": "SMISMEMBER", + "Summary": "Determines whether multiple members belong to a set.", + "Group": "Set", + "Complexity": "O(N) where N is the number of elements being checked for membership", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "DisplayText": "key", + "Type": "Key", + "KeySpecIndex": 0 + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "MEMBER", + "DisplayText": "member", + "Type": "String", + "ArgumentFlags": "Multiple" + } + ] + }, { "Command": "SMOVE", "Name": "SMOVE", diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index 91fa19c4c4..9ff0a95864 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -3479,6 +3479,31 @@ } ] }, + { + "Command": "SMISMEMBER", + "Name": "SMISMEMBER", + "Arity": -3, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Read, Set", + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "RO, Access" + } + ] + }, { "Command": "SMOVE", "Name": "SMOVE", diff --git a/libs/server/API/GarnetApiObjectCommands.cs b/libs/server/API/GarnetApiObjectCommands.cs index 7d5043c06f..e045363cb3 100644 --- a/libs/server/API/GarnetApiObjectCommands.cs +++ b/libs/server/API/GarnetApiObjectCommands.cs @@ -301,6 +301,10 @@ public GarnetStatus SetMembers(byte[] key, ref ObjectInput input, ref GarnetObje public GarnetStatus SetIsMember(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter) => storageSession.SetIsMember(key, ref input, ref outputFooter, ref objectContext); + /// + public GarnetStatus SetIsMember(ArgSlice key, ArgSlice[] members, out int[] result) + => storageSession.SetIsMember(key, members, out result, ref objectContext); + /// public GarnetStatus SetPop(ArgSlice key, out ArgSlice member) => storageSession.SetPop(key, out member, ref objectContext); diff --git a/libs/server/API/GarnetWatchApi.cs b/libs/server/API/GarnetWatchApi.cs index 277d1e2cec..ee0896ac56 100644 --- a/libs/server/API/GarnetWatchApi.cs +++ b/libs/server/API/GarnetWatchApi.cs @@ -262,6 +262,13 @@ public GarnetStatus SetIsMember(byte[] key, ref ObjectInput input, ref GarnetObj return garnetApi.SetIsMember(key, ref input, ref outputFooter); } + /// + public GarnetStatus SetIsMember(ArgSlice key, ArgSlice[] members, out int[] result) + { + garnetApi.WATCH(key, StoreType.Object); + return garnetApi.SetIsMember(key, members, out result); + } + /// public GarnetStatus SetMembers(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter) { diff --git a/libs/server/API/IGarnetApi.cs b/libs/server/API/IGarnetApi.cs index 5343f35a70..a10d0add73 100644 --- a/libs/server/API/IGarnetApi.cs +++ b/libs/server/API/IGarnetApi.cs @@ -1358,6 +1358,14 @@ public interface IGarnetReadApi /// GarnetStatus SetIsMember(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter); + /// + /// Returns whether each member is a member of the set stored at key. + /// + /// + /// + /// + GarnetStatus SetIsMember(ArgSlice key, ArgSlice[] members, out int[] result); + /// /// Iterates over the members of the Set with the given key using a cursor, /// a match pattern and count parameters. diff --git a/libs/server/Objects/Set/SetObject.cs b/libs/server/Objects/Set/SetObject.cs index 99f20de115..1de3662dc6 100644 --- a/libs/server/Objects/Set/SetObject.cs +++ b/libs/server/Objects/Set/SetObject.cs @@ -26,6 +26,7 @@ public enum SetOperation : byte SMOVE, SRANDMEMBER, SISMEMBER, + SMISMEMBER, SUNION, SUNIONSTORE, SDIFF, @@ -129,6 +130,9 @@ public override unsafe bool Operate(ref ObjectInput input, ref SpanByteAndMemory case SetOperation.SISMEMBER: SetIsMember(ref input, ref output); break; + case SetOperation.SMISMEMBER: + SetMultiIsMember(ref input, ref output); + break; case SetOperation.SREM: SetRemove(ref input, _output); break; diff --git a/libs/server/Objects/Set/SetObjectImpl.cs b/libs/server/Objects/Set/SetObjectImpl.cs index b6eef96165..fc20f57218 100644 --- a/libs/server/Objects/Set/SetObjectImpl.cs +++ b/libs/server/Objects/Set/SetObjectImpl.cs @@ -93,6 +93,45 @@ private void SetIsMember(ref ObjectInput input, ref SpanByteAndMemory output) } } + private void SetMultiIsMember(ref ObjectInput input, ref SpanByteAndMemory output) + { + var isMemory = false; + MemoryHandle ptrHandle = default; + var ptr = output.SpanByte.ToPointer(); + + var curr = ptr; + var end = curr + output.Length; + + ObjectOutputHeader _output = default; + try + { + var totalCount = input.parseState.Count - input.parseStateFirstArgIdx; + while (!RespWriteUtils.WriteArrayLength(totalCount, ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); + + var argCurr = input.parseStateFirstArgIdx; + while (argCurr < input.parseState.Count) + { + var member = input.parseState.GetArgSliceByRef(argCurr).SpanByte.ToByteArray(); + var isMember = set.Contains(member); + + while (!RespWriteUtils.WriteInteger(isMember ? 1 : 0, ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); + + argCurr++; + } + _output.result1 = totalCount; + } + finally + { + while (!RespWriteUtils.WriteDirect(ref _output, ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); + + if (isMemory) ptrHandle.Dispose(); + output.Length = (int)(curr - ptr); + } + } + private void SetRemove(ref ObjectInput input, byte* output) { var _output = (ObjectOutputHeader*)output; diff --git a/libs/server/Resp/Objects/SetCommands.cs b/libs/server/Resp/Objects/SetCommands.cs index 16625237f3..619c86f4d4 100644 --- a/libs/server/Resp/Objects/SetCommands.cs +++ b/libs/server/Resp/Objects/SetCommands.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.Diagnostics; using Garnet.common; using Tsavorite.core; @@ -374,12 +375,25 @@ private unsafe bool SetMembers(ref TGarnetApi storageApi) return true; } - private unsafe bool SetIsMember(ref TGarnetApi storageApi) + private unsafe bool SetIsMember(RespCommand cmd, ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { - if (parseState.Count != 2) + Debug.Assert(cmd == RespCommand.SISMEMBER || cmd == RespCommand.SMISMEMBER); + + var isSingle = cmd == RespCommand.SISMEMBER; + if (isSingle) { - return AbortWithWrongNumberOfArguments("SISMEMBER"); + if (parseState.Count != 2) + { + return AbortWithWrongNumberOfArguments("SISMEMBER"); + } + } + else + { + if (parseState.Count < 2) + { + return AbortWithWrongNumberOfArguments("SMISMEMBER"); + } } // Get the key @@ -387,7 +401,7 @@ private unsafe bool SetIsMember(ref TGarnetApi storageApi) var keyBytes = sbKey.ToByteArray(); // Prepare input - var header = new RespInputHeader(GarnetObjectType.Set) { SetOp = SetOperation.SISMEMBER }; + var header = new RespInputHeader(GarnetObjectType.Set) { SetOp = isSingle ? SetOperation.SISMEMBER : SetOperation.SMISMEMBER }; var input = new ObjectInput(header, ref parseState, 1); // Prepare GarnetObjectStore output @@ -402,8 +416,24 @@ private unsafe bool SetIsMember(ref TGarnetApi storageApi) ProcessOutputWithHeader(outputFooter.spanByteAndMemory); break; case GarnetStatus.NOTFOUND: - while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_RETURN_VAL_0, ref dcurr, dend)) - SendAndReset(); + if (isSingle) + { + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_RETURN_VAL_0, ref dcurr, dend)) + SendAndReset(); + } + else + { + var count = parseState.Count - 1; // Remove key + while (!RespWriteUtils.WriteArrayLength(count, ref dcurr, dend)) + SendAndReset(); + + for (var i = 0; i < count; i++) + { + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_RETURN_VAL_0, ref dcurr, dend)) + SendAndReset(); + } + } + break; case GarnetStatus.WRONGTYPE: while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index 3be145ddd6..a6b94e6b34 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -59,6 +59,7 @@ public enum RespCommand : byte SINTER, SISMEMBER, SMEMBERS, + SMISMEMBER, SRANDMEMBER, SSCAN, STRLEN, @@ -1331,10 +1332,14 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan { return RespCommand.SDIFFSTORE; } - else if (*(ulong*)(ptr + 1) == MemoryMarshal.Read("10\r\nEXPI"u8) && *(uint*)(ptr + 9) == MemoryMarshal.Read("RETIME\r\n"u8)) + else if (*(ulong*)(ptr + 1) == MemoryMarshal.Read("10\r\nEXPI"u8) && *(ulong*)(ptr + 9) == MemoryMarshal.Read("RETIME\r\n"u8)) { return RespCommand.EXPIRETIME; } + else if (*(ulong*)(ptr + 1) == MemoryMarshal.Read("10\r\nSMIS"u8) && *(ulong*)(ptr + 9) == MemoryMarshal.Read("MEMBER\r\n"u8)) + { + return RespCommand.SMISMEMBER; + } break; case 11: @@ -1362,7 +1367,7 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan { return RespCommand.SINTERSTORE; } - else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read("1\r\nPEXPI"u8) && *(uint*)(ptr + 10) == MemoryMarshal.Read("RETIME\r\n"u8)) + else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read("1\r\nPEXPI"u8) && *(ulong*)(ptr + 10) == MemoryMarshal.Read("RETIME\r\n"u8)) { return RespCommand.PEXPIRETIME; } diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index ab89ea9c0f..91332dbdfd 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -663,7 +663,8 @@ private bool ProcessArrayCommands(RespCommand cmd, ref TGarnetApi st // Set Commands RespCommand.SADD => SetAdd(ref storageApi), RespCommand.SMEMBERS => SetMembers(ref storageApi), - RespCommand.SISMEMBER => SetIsMember(ref storageApi), + RespCommand.SISMEMBER => SetIsMember(cmd, ref storageApi), + RespCommand.SMISMEMBER => SetIsMember(cmd, ref storageApi), RespCommand.SREM => SetRemove(ref storageApi), RespCommand.SCARD => SetLength(ref storageApi), RespCommand.SPOP => SetPop(ref storageApi), diff --git a/libs/server/Storage/Session/ObjectStore/Common.cs b/libs/server/Storage/Session/ObjectStore/Common.cs index 89e3f6c932..b2cb99ff7c 100644 --- a/libs/server/Storage/Session/ObjectStore/Common.cs +++ b/libs/server/Storage/Session/ObjectStore/Common.cs @@ -296,6 +296,62 @@ unsafe ArgSlice[] ProcessRespArrayOutput(GarnetObjectStoreOutput outputFooter, o return elements; } + /// + /// Converts an array of elements in RESP format to ArgSlice[] type + /// + /// The RESP format output object + /// A description of the error, if there is any + /// + unsafe int[] ProcessRespIntegerArrayOutput(GarnetObjectStoreOutput outputFooter, out string error) + { + int[] elements = default; + error = default; + + // For reading the elements in the outputFooter + byte* element = null; + + var outputSpan = outputFooter.spanByteAndMemory.IsSpanByte ? + outputFooter.spanByteAndMemory.SpanByte.AsReadOnlySpan() : outputFooter.spanByteAndMemory.AsMemoryReadOnlySpan(); + + try + { + fixed (byte* outputPtr = outputSpan) + { + var refPtr = outputPtr; + + if (*refPtr == '-') + { + if (!RespReadUtils.ReadErrorAsString(out error, ref refPtr, outputPtr + outputSpan.Length)) + return default; + } + else if (*refPtr == '*') + { + // Get the number of elements + if (!RespReadUtils.ReadUnsignedArrayLength(out var arraySize, ref refPtr, outputPtr + outputSpan.Length)) + return default; + + // Create the argslice[] + elements = new int[arraySize]; + for (int i = 0; i < elements.Length; i++) + { + element = null; + if (RespReadUtils.TryReadInt(ref refPtr, outputPtr + outputSpan.Length, out var number, out var _)) + { + elements[i] = number; + } + } + } + } + } + finally + { + if (!outputFooter.spanByteAndMemory.IsSpanByte) + outputFooter.spanByteAndMemory.Memory.Dispose(); + } + + return elements; + } + /// /// Processes RESP output as pairs of score and member. /// diff --git a/libs/server/Storage/Session/ObjectStore/SetOps.cs b/libs/server/Storage/Session/ObjectStore/SetOps.cs index 4e022b4a72..fc3a998185 100644 --- a/libs/server/Storage/Session/ObjectStore/SetOps.cs +++ b/libs/server/Storage/Session/ObjectStore/SetOps.cs @@ -711,6 +711,41 @@ public GarnetStatus SetIsMember(byte[] key, ref ObjectInput inpu where TObjectContext : ITsavoriteContext => ReadObjectStoreOperationWithOutput(key, ref input, ref objectContext, ref outputFooter); + /// + /// Returns whether each member is a member of the set stored at key. + /// + /// + /// + /// + /// + /// + public unsafe GarnetStatus SetIsMember(ArgSlice key, ArgSlice[] members, out int[] result, ref TObjectContext objectContext) + where TObjectContext : ITsavoriteContext + { + result = default; + + if (key.Length == 0) + return GarnetStatus.OK; + + var parseState = new SessionParseState(); + parseState.InitializeWithArguments(members); + + // Prepare the input + var input = new ObjectInput(new RespInputHeader + { + type = GarnetObjectType.Set, + SetOp = SetOperation.SMISMEMBER, + }, ref parseState, 0); + + var outputFooter = new GarnetObjectStoreOutput { spanByteAndMemory = new SpanByteAndMemory(null) }; + var status = ReadObjectStoreOperationWithOutput(key.ToArray(), ref input, ref objectContext, ref outputFooter); + + if (status == GarnetStatus.OK) + result = ProcessRespIntegerArrayOutput(outputFooter, out _); + + return status; + } + /// /// Removes and returns one or more random members from the set at key. /// diff --git a/libs/server/Transaction/TxnKeyManager.cs b/libs/server/Transaction/TxnKeyManager.cs index d0c16af360..54f6cff259 100644 --- a/libs/server/Transaction/TxnKeyManager.cs +++ b/libs/server/Transaction/TxnKeyManager.cs @@ -75,6 +75,7 @@ internal int GetKeys(RespCommand command, int inputCount, out ReadOnlySpan RespCommand.SMOVE => SetObjectKeys(SetOperation.SMOVE, inputCount), RespCommand.SRANDMEMBER => SetObjectKeys(SetOperation.SRANDMEMBER, inputCount), RespCommand.SISMEMBER => SetObjectKeys(SetOperation.SISMEMBER, inputCount), + RespCommand.SMISMEMBER => SetObjectKeys(SetOperation.SMISMEMBER, inputCount), RespCommand.SUNION => SetObjectKeys(SetOperation.SUNION, inputCount), RespCommand.SUNIONSTORE => SetObjectKeys(SetOperation.SUNIONSTORE, inputCount), RespCommand.SDIFF => SetObjectKeys(SetOperation.SDIFF, inputCount), @@ -276,6 +277,7 @@ private int SetObjectKeys(SetOperation subCommand, int inputCount) SetOperation.SRANDMEMBER => SingleKey(1, true, LockType.Shared), SetOperation.SPOP => SingleKey(1, true, LockType.Exclusive), SetOperation.SISMEMBER => SingleKey(1, true, LockType.Shared), + SetOperation.SMISMEMBER => SingleKey(1, true, LockType.Shared), SetOperation.SUNION => ListKeys(inputCount, true, LockType.Shared), SetOperation.SUNIONSTORE => XSTOREKeys(inputCount, true), SetOperation.SDIFF => ListKeys(inputCount, true, LockType.Shared), diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs index 10dc9455b7..5ccf20af5e 100644 --- a/playground/CommandInfoUpdater/SupportedCommand.cs +++ b/playground/CommandInfoUpdater/SupportedCommand.cs @@ -228,6 +228,7 @@ public class SupportedCommand new("SISMEMBER", RespCommand.SISMEMBER), new("SLAVEOF", RespCommand.SECONDARYOF), new("SMEMBERS", RespCommand.SMEMBERS), + new("SMISMEMBER", RespCommand.SMISMEMBER), new("SMOVE", RespCommand.SMOVE), new("SPOP", RespCommand.SPOP), new("SRANDMEMBER", RespCommand.SRANDMEMBER), diff --git a/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs b/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs index f91a5d0fae..dcd12f1f6a 100644 --- a/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs +++ b/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs @@ -1176,6 +1176,23 @@ public override string[] GetSingleSlotRequest() public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); } + internal class SMISMEMBER : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => true; + public override string Command => nameof(SMISMEMBER); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], "0", "1"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + internal class SPOP : BaseCommand { public override bool IsArrayCommand => false; diff --git a/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs b/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs index e37590973c..429eb6543e 100644 --- a/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs +++ b/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs @@ -85,6 +85,7 @@ public class ClusterSlotVerificationTests new SCARD(), new SMEMBERS(), new SISMEMBER(), + new SMISMEMBER(), new SPOP(), new SRANDMEMBER(), new GEOADD(), @@ -261,6 +262,7 @@ public virtual void OneTimeTearDown() [TestCase("SCARD")] [TestCase("SMEMBERS")] [TestCase("SISMEMBER")] + [TestCase("SMISMEMBER")] [TestCase("SPOP")] [TestCase("SRANDMEMBER")] [TestCase("GEOADD")] @@ -397,6 +399,7 @@ void GarnetClientSessionClusterDown(BaseCommand command) [TestCase("SCARD")] [TestCase("SMEMBERS")] [TestCase("SISMEMBER")] + [TestCase("SMISMEMBER")] [TestCase("SPOP")] [TestCase("SRANDMEMBER")] [TestCase("GEOADD")] @@ -543,6 +546,7 @@ void GarnetClientSessionOK(BaseCommand command) [TestCase("SCARD")] [TestCase("SMEMBERS")] [TestCase("SISMEMBER")] + [TestCase("SMISMEMBER")] [TestCase("SPOP")] [TestCase("SRANDMEMBER")] [TestCase("GEOADD")] @@ -681,6 +685,7 @@ void GarnetClientSessionCrossslotTest(BaseCommand command) [TestCase("SCARD")] [TestCase("SMEMBERS")] [TestCase("SISMEMBER")] + [TestCase("SMISMEMBER")] [TestCase("SPOP")] [TestCase("SRANDMEMBER")] [TestCase("GEOADD")] @@ -826,6 +831,7 @@ void GarnetClientSessionMOVEDTest(BaseCommand command) [TestCase("SCARD")] [TestCase("SMEMBERS")] [TestCase("SISMEMBER")] + [TestCase("SMISMEMBER")] [TestCase("SPOP")] [TestCase("SRANDMEMBER")] [TestCase("GEOADD")] @@ -988,6 +994,7 @@ void GarnetClientSessionASKTest(BaseCommand command) [TestCase("SCARD")] [TestCase("SMEMBERS")] [TestCase("SISMEMBER")] + [TestCase("SMISMEMBER")] [TestCase("SPOP")] [TestCase("SRANDMEMBER")] [TestCase("GEOADD")] diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index 9d96ec096d..c71932497f 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -5168,6 +5168,21 @@ static async Task DoSIsMemberAsync(GarnetClient client) } } + [Test] + public async Task SMIsMemberACLsAsync() + { + await CheckCommandsAsync( + "SMISMEMBER", + [DoSMultiIsMemberAsync] + ); + + static async Task DoSMultiIsMemberAsync(GarnetClient client) + { + string[] val = await client.ExecuteForStringArrayResultAsync("SMISMEMBER", ["foo", "5"]); + ClassicAssert.IsNotNull(val); + } + } + [Test] public async Task SubscribeACLsAsync() { diff --git a/test/Garnet.test/RespSetTest.cs b/test/Garnet.test/RespSetTest.cs index 925170fdc3..ae072ff17d 100644 --- a/test/Garnet.test/RespSetTest.cs +++ b/test/Garnet.test/RespSetTest.cs @@ -1467,5 +1467,57 @@ private static void CreateLongSet() response = lightClientRequest.SendCommand("SADD myset five", 1); } #endregion + + #region SMISMEMBER + + [Test] + [TestCase("Value1,Value2,Value3,Value4,Value5", "Value3,Value6", "true,false")] + [TestCase("Value1,Value2,Value5", "InvalidA,Value1,Value5,InvalidB", "false,true,true,false")] + [TestCase("Value1", "Value1", "true")] + [TestCase("Value1", "Value2", "false")] + public void CheckIfMemberExistsInSetLC(string valuesInput, string findInput, string expectedInput) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "KeyA"; + var values = valuesInput.Split(","); + var find = findInput.Split(","); + var expectedResult = expectedInput.Split(",").Select(x => bool.Parse(x)).ToArray(); + + foreach (var value in values) + { + db.SetAdd(key, value); + } + + var actualResult = db.SetContains(key, find.Select(x => (RedisValue)x).ToArray()); + + CollectionAssert.AreEqual(expectedResult, actualResult); + } + + [Test] + public void CheckIfMemberExistsWithNoExistKey() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "KeyA"; + RedisValue[] find = ["Value1", "Value2"]; + bool[] expectedResult = [false, false]; + + var actualResult = db.SetContains(key, find); + + CollectionAssert.AreEqual(expectedResult, actualResult); + } + + [Test] + public void CheckIfMemberExistsWithInvalidParam() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "KeyA"; + + Assert.Throws(() => db.Execute("SMISMEMBER", key)); + } + + #endregion } } \ No newline at end of file diff --git a/test/Garnet.test/TestProcedureSet.cs b/test/Garnet.test/TestProcedureSet.cs index 17d6f44ab7..17f782a8f8 100644 --- a/test/Garnet.test/TestProcedureSet.cs +++ b/test/Garnet.test/TestProcedureSet.cs @@ -80,6 +80,10 @@ private static bool TestAPI(TGarnetApi api, ref CustomProcedureInput if (status != GarnetStatus.OK || members.Length != 5) return false; + status = api.SetIsMember(setA, elements[0..5], out var result); + if (status != GarnetStatus.OK || result.Length != 5) + return false; + status = api.SetPop(setA, out var member); if (status != GarnetStatus.OK) return false; diff --git a/website/docs/commands/api-compatibility.md b/website/docs/commands/api-compatibility.md index 467cbdbcbd..fa6b2bb6d8 100644 --- a/website/docs/commands/api-compatibility.md +++ b/website/docs/commands/api-compatibility.md @@ -303,7 +303,7 @@ Note that this list is subject to change as we continue to expand our API comman | | SINTERCARD | ➖ | | | | [SISMEMBER](data-structures.md#sismember) | ➕ | | | | [SMEMBERS](data-structures.md#smembers) | ➕ | | -| | SMISMEMBER | ➖ | | +| | [SMISMEMBER](data-structures.md#smismember) | ➕ | | | | [SMOVE](data-structures.md#smove) | ➕ | | | | [SPOP](data-structures.md#spop) | ➕ | | | | SPUBLISH | ➖ | | diff --git a/website/docs/commands/data-structures.md b/website/docs/commands/data-structures.md index e2aae7d1fb..7c5a4d4a4c 100644 --- a/website/docs/commands/data-structures.md +++ b/website/docs/commands/data-structures.md @@ -559,6 +559,22 @@ Returns if **member** is a member of the set stored at **key**. --- +### SMISMEMBER + +#### Syntax + +```bash + SMISMEMBER key member [member ...] +``` + +Returns whether each **member** is a member of the set stored at **key**. + +#### Resp Reply + +Array reply: a list representing the membership of the given elements, in the same order as they are requested. + +--- + ### SRANDMEMBER #### Syntax