Skip to content

Commit

Permalink
feat: add sismember command (#174)
Browse files Browse the repository at this point in the history
* feat: add SISMEMBER command

* feat: add SISMEMBER command

* feat: add SISMEMBER command

* fix format

* feat: add SISMEMBER command

* feat: add SISMEMBER command

* feat: add SISMEMBER command

* fix format

* feat: add SISMEMBER command

* fix format

* fix testcase

* fix conflict

* fix conflict

* fix conflict

* sync new update in main branch
  • Loading branch information
Zzhiter authored Apr 6, 2024
1 parent f2ee39a commit 5675a98
Show file tree
Hide file tree
Showing 15 changed files with 229 additions and 2 deletions.
4 changes: 4 additions & 0 deletions libs/server/API/GarnetApiObjectCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ public GarnetStatus SetMembers(ArgSlice key, out ArgSlice[] members)
public GarnetStatus SetMembers(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter)
=> storageSession.SetMembers(key, input, ref outputFooter, ref objectContext);

/// <inheritdoc />
public GarnetStatus SetIsMember(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter)
=> storageSession.SetIsMember(key, input, ref outputFooter, ref objectContext);

/// <inheritdoc />
public GarnetStatus SetPop(ArgSlice key, out ArgSlice member)
=> storageSession.SetPop(key, out member, ref objectContext);
Expand Down
7 changes: 7 additions & 0 deletions libs/server/API/GarnetWatchApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,13 @@ public GarnetStatus SetMembers(ArgSlice key, out ArgSlice[] members)
return garnetApi.SetMembers(key, out members);
}

/// <inheritdoc />
public GarnetStatus SetIsMember(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter)
{
garnetApi.WATCH(key, StoreType.Object);
return garnetApi.SetIsMember(key, input, ref outputFooter);
}

/// <inheritdoc />
public GarnetStatus SetMembers(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter)
{
Expand Down
9 changes: 9 additions & 0 deletions libs/server/API/IGarnetApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,15 @@ public interface IGarnetReadApi
/// <returns></returns>
GarnetStatus SetMembers(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter);

/// <summary>
/// Returns if member is a member of the set stored at key.
/// </summary>
/// <param name="key"></param>
/// <param name="input"></param>
/// <param name="outputFooter"></param>
/// <returns></returns>
GarnetStatus SetIsMember(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter);

/// <summary>
/// Iterates over the members of the Set with the given key using a cursor,
/// a match pattern and count parameters.
Expand Down
4 changes: 4 additions & 0 deletions libs/server/Objects/Set/SetObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public enum SetOperation : byte
SMEMBERS,
SCARD,
SSCAN,
SISMEMBER,
}


Expand Down Expand Up @@ -111,6 +112,9 @@ public override unsafe bool Operate(ref SpanByte input, ref SpanByteAndMemory ou
case SetOperation.SMEMBERS:
SetMembers(_input, input.Length, ref output);
break;
case SetOperation.SISMEMBER:
SetIsMember(_input, input.Length, ref output);
break;
case SetOperation.SREM:
SetRemove(_input, input.Length, _output);
break;
Expand Down
37 changes: 37 additions & 0 deletions libs/server/Objects/Set/SetObjectImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,43 @@ private void SetMembers(byte* input, int length, ref SpanByteAndMemory output)
}
}

private void SetIsMember(byte* input, int length, ref SpanByteAndMemory output)
{
byte* input_startptr = input + sizeof(ObjectInputHeader);
byte* input_currptr = input_startptr;

bool isMemory = false;
MemoryHandle ptrHandle = default;
byte* ptr = output.SpanByte.ToPointer();

var curr = ptr;
var end = curr + output.Length;

ObjectOutputHeader _output = default;
try
{
if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var member, ref input_currptr, input + length))
return;

bool 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);

_output.opsDone = 1;
_output.bytesDone = (int)(input_currptr - input_startptr);
_output.countDone = 1;
}
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(byte* input, int length, byte* output)
{
var _input = (ObjectInputHeader*)input;
Expand Down
69 changes: 69 additions & 0 deletions libs/server/Resp/Objects/SetCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,75 @@ private unsafe bool SetMembers<TGarnetApi>(int count, byte* ptr, ref TGarnetApi
return true;
}

private unsafe bool SetIsMember<TGarnetApi>(int count, byte* ptr, ref TGarnetApi storageApi)
where TGarnetApi : IGarnetApi
{
if (count != 2)
{
setItemsDoneCount = setOpsCount = 0;
return AbortWithWrongNumberOfArguments("SISMEMBER", count);
}

// Get the key
if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var key, ref ptr, recvBufferPtr + bytesRead))
return false;

if (NetworkSingleKeySlotVerify(key, true))
{
var bufSpan = new ReadOnlySpan<byte>(recvBufferPtr, bytesRead);
if (!DrainCommands(bufSpan, count))
return false;
return true;
}

// Prepare input
var inputPtr = (ObjectInputHeader*)(ptr - sizeof(ObjectInputHeader));

// Save old values
var save = *inputPtr;

// Prepare length of header in input buffer
var inputLength = (int)(recvBufferPtr + bytesRead - (byte*)inputPtr);

// Prepare header in input buffer
inputPtr->header.type = GarnetObjectType.Set;
inputPtr->header.SetOp = SetOperation.SISMEMBER;
inputPtr->count = count - 2;
inputPtr->done = 0;

// Prepare GarnetObjectStore output
var outputFooter = new GarnetObjectStoreOutput { spanByteAndMemory = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)) };

var status = storageApi.SetIsMember(key, new ArgSlice((byte*)inputPtr, inputLength), ref outputFooter);

// Restore input buffer
*inputPtr = save;

switch (status)
{
case GarnetStatus.OK:
// Process output
var objOutputHeader = ProcessOutputWithHeader(outputFooter.spanByteAndMemory);
ptr += objOutputHeader.bytesDone;
setItemsDoneCount += objOutputHeader.countDone;
if (setItemsDoneCount > objOutputHeader.opsDone)
return false;
break;
case GarnetStatus.NOTFOUND:
while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_RETURN_VAL_0, ref dcurr, dend))
SendAndReset();
ReadLeftToken(count - 1, ref ptr);
break;
}

// Reset session counters
setItemsDoneCount = setOpsCount = 0;

// Move input head
readHead = (int)(ptr - recvBufferPtr);
return true;
}

/// <summary>
/// Removes and returns one or more random members from the set at key.
/// </summary>
Expand Down
4 changes: 4 additions & 0 deletions libs/server/Resp/RespCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,10 @@ static RespCommand MatchedNone(RespServerSession session, int oldReadHead)
{
return (RespCommand.SUBSCRIBE, 0);
}
else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read<ulong>("SISMEMBE"u8) && *(uint*)(ptr + 11) == MemoryMarshal.Read<uint>("ER\r\n"u8))
{
return (RespCommand.Set, (byte)SetOperation.SISMEMBER);
}
else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read<ulong>("ZLEXCOUN"u8) && *(uint*)(ptr + 11) == MemoryMarshal.Read<uint>("NT\r\n"u8))
{
return (RespCommand.SortedSet, (byte)SortedSetOperation.ZLEXCOUNT);
Expand Down
1 change: 1 addition & 0 deletions libs/server/Resp/RespCommandsInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ public static RespCommandsInfo findCommand(RespCommand cmd, byte subCmd = 0)
{(byte)SetOperation.SCARD, new RespCommandsInfo("SCARD", RespCommand.Set, 1, null, (byte)SetOperation.SCARD)},
{(byte)SetOperation.SPOP, new RespCommandsInfo("SPOP", RespCommand.Set, -1, null, (byte)SetOperation.SPOP) },
{(byte)SetOperation.SSCAN, new RespCommandsInfo("SSCAN", RespCommand.Set, -2, null, (byte)SetOperation.SSCAN) },
{(byte)SetOperation.SISMEMBER, new RespCommandsInfo("SISMEMBER",RespCommand.Set, 2, null, (byte)SetOperation.SISMEMBER) },
};

private static readonly Dictionary<RespCommand, RespCommandsInfo> customCommandsInfoMap = new Dictionary<RespCommand, RespCommandsInfo>
Expand Down
2 changes: 1 addition & 1 deletion libs/server/Resp/RespInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static HashSet<string> GetCommands()
// Pub/sub
"PUBLISH", "SUBSCRIBE", "PSUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE",
// Set
"SADD", "SREM", "SPOP", "SMEMBERS", "SCARD", "SSCAN",
"SADD", "SREM", "SPOP", "SMEMBERS", "SCARD", "SSCAN", "SISMEMBER",
//Scan ops
"DBSIZE", "KEYS","SCAN",
// Geospatial commands
Expand Down
1 change: 1 addition & 0 deletions libs/server/Resp/RespServerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ private bool ProcessArrayCommands<TGarnetApi>(RespCommand cmd, byte subcmd, int
// Set Commands
(RespCommand.Set, (byte)SetOperation.SADD) => SetAdd(count, ptr, ref storageApi),
(RespCommand.Set, (byte)SetOperation.SMEMBERS) => SetMembers(count, ptr, ref storageApi),
(RespCommand.Set, (byte)SetOperation.SISMEMBER) => SetIsMember(count, ptr, ref storageApi),
(RespCommand.Set, (byte)SetOperation.SREM) => SetRemove(count, ptr, ref storageApi),
(RespCommand.Set, (byte)SetOperation.SCARD) => SetLength(count, ptr, ref storageApi),
(RespCommand.Set, (byte)SetOperation.SPOP) => SetPop(count, ptr, ref storageApi),
Expand Down
13 changes: 13 additions & 0 deletions libs/server/Storage/Session/ObjectStore/SetOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,19 @@ public GarnetStatus SetMembers<TObjectContext>(byte[] key, ArgSlice input, ref G
where TObjectContext : ITsavoriteContext<byte[], IGarnetObject, SpanByte, GarnetObjectStoreOutput, long>
=> ReadObjectStoreOperationWithOutput(key, input, ref objectContext, ref outputFooter);

/// <summary>
/// Returns if member is a member of the set stored at key.
/// </summary>
/// <typeparam name="TObjectContext"></typeparam>
/// <param name="key"></param>
/// <param name="input"></param>
/// <param name="outputFooter"></param>
/// <param name="objectContext"></param>
/// <returns></returns>
public GarnetStatus SetIsMember<TObjectContext>(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter, ref TObjectContext objectContext)
where TObjectContext : ITsavoriteContext<byte[], IGarnetObject, SpanByte, GarnetObjectStoreOutput, long>
=> ReadObjectStoreOperationWithOutput(key, input, ref objectContext, ref outputFooter);

/// <summary>
/// Removes and returns one or more random members from the set at key.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions libs/server/Transaction/TxnKeyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ private int SetObjectKeys(byte subCommand)
(byte)SetOperation.SREM => SingleKey(1, true, LockType.Exclusive),
(byte)SetOperation.SCARD => SingleKey(1, true, LockType.Exclusive),
(byte)SetOperation.SPOP => SingleKey(1, true, LockType.Exclusive),
(byte)SetOperation.SISMEMBER => SingleKey(1, true, LockType.Shared),
_ => -1
};
}
Expand Down
65 changes: 65 additions & 0 deletions test/Garnet.test/RespSetTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Garnet.server;
using NUnit.Framework;
using StackExchange.Redis;

Expand Down Expand Up @@ -50,6 +51,27 @@ public void CanAddAndListMembers()
Assert.AreEqual(expectedResponse, actualValue);
}

[Test]
public void CanCheckIfMemberExistsInSet()
{
using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig());
var db = redis.GetDatabase(0);
var key = new RedisKey("user1:set");

db.KeyDelete(key);

db.SetAdd(key, new RedisValue[] { "Hello", "World" });

var existingMemberExists = db.SetContains(key, "Hello");
Assert.IsTrue(existingMemberExists);

var nonExistingMemberExists = db.SetContains(key, "NonExistingMember");
Assert.IsFalse(nonExistingMemberExists);

var setDoesNotExist = db.SetContains("NonExistingSet", "AnyMember");
Assert.IsFalse(setDoesNotExist);
}


[Test]
public void CanAddAndGetAllMembersWithPendingStatus()
Expand Down Expand Up @@ -96,6 +118,9 @@ public void CanRemoveField()
var result = db.SetAdd(new RedisKey("user1:set"), new RedisValue[] { "ItemOne", "ItemTwo", "ItemThree", "ItemFour" });
Assert.AreEqual(4, result);

var existingMemberExists = db.SetContains(new RedisKey("user1:set"), "ItemOne");
Assert.IsTrue(existingMemberExists, "Existing member 'ItemOne' does not exist in the set.");

var memresponse = db.Execute("MEMORY", "USAGE", "user1:set");
var actualValue = ResultType.Integer == memresponse.Type ? Int32.Parse(memresponse.ToString()) : -1;
var expectedResponse = 424;
Expand Down Expand Up @@ -281,6 +306,46 @@ public void CanAddAndListMembersLC()

}

[Test]
public void CanCheckIfMemberExistsInSetLC()
{
using var lightClientRequest = TestUtils.CreateRequest();

var response = lightClientRequest.SendCommand("SADD myset \"Hello\"");
var expectedResponse = ":1\r\n";
var strResponse = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length);
Assert.AreEqual(expectedResponse, strResponse);

response = lightClientRequest.SendCommand("SADD myset \"World\"");
strResponse = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length);
Assert.AreEqual(expectedResponse, strResponse);

response = lightClientRequest.SendCommand("SISMEMBER myset \"Hello\"");
expectedResponse = ":1\r\n";
strResponse = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length);
Assert.AreEqual(expectedResponse, strResponse);

response = lightClientRequest.SendCommand("SISMEMBER myset \"NonExistingMember\"");
expectedResponse = ":0\r\n";
strResponse = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length);
Assert.AreEqual(expectedResponse, strResponse);

response = lightClientRequest.SendCommand("SISMEMBER NonExistingSet \"AnyMember\"");
expectedResponse = ":0\r\n";
strResponse = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length);
Assert.AreEqual(expectedResponse, strResponse);

// Missing arguments
response = lightClientRequest.SendCommand("SISMEMBER myset");
expectedResponse = string.Format(CmdStrings.ErrWrongNumArgs, "SISMEMBER");
strResponse = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length);
Assert.AreEqual(expectedResponse, strResponse);

// Extra arguments
response = lightClientRequest.SendCommand("SISMEMBER myset \"Hello\" \"ExtraArg\"");
strResponse = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length);
Assert.AreEqual(expectedResponse, strResponse);
}

[Test]
public void CanDoSCARDCommandLC()
Expand Down
2 changes: 1 addition & 1 deletion website/docs/commands/api-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ Note that this list is subject to change as we continue to expand our API comman
| | SINTER || |
| | SINTERCARD || |
| | SINTERSTORE || |
| | SISMEMBER | | |
| | SISMEMBER | | |
| | [SMEMBERS](data-structures.md#smembers) || |
| | SMISMEMBER || |
| | SMOVE || |
Expand Down
12 changes: 12 additions & 0 deletions website/docs/commands/data-structures.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,18 @@ Removes and returns one or more random members from the set value stored at **ke
---
### SISMEMBER
#### Syntax
```bash
SISMEMBER key member
```
Returns if **member** is a member of the set stored at **key**.
---
### SREM
#### Syntax
Expand Down

0 comments on commit 5675a98

Please sign in to comment.