diff --git a/libs/server/API/GarnetApiObjectCommands.cs b/libs/server/API/GarnetApiObjectCommands.cs index a635efcaef..4acf8f41b1 100644 --- a/libs/server/API/GarnetApiObjectCommands.cs +++ b/libs/server/API/GarnetApiObjectCommands.cs @@ -327,6 +327,14 @@ public GarnetStatus SetDiff(ArgSlice[] keys, out HashSet members) public GarnetStatus SetDiffStore(byte[] key, ArgSlice[] keys, out int count) => storageSession.SetDiffStore(key, keys, out count); + /// + public GarnetStatus SetIntersect(ArgSlice[] keys, out HashSet output) + => storageSession.SetIntersect(keys, out output); + + /// + public GarnetStatus SetIntersectStore(byte[] key, ArgSlice[] keys, out int count) + => storageSession.SetIntersectStore(key, keys, out count); + #endregion #region Hash Methods diff --git a/libs/server/API/GarnetWatchApi.cs b/libs/server/API/GarnetWatchApi.cs index 919b34c8fb..258b6e0ecd 100644 --- a/libs/server/API/GarnetWatchApi.cs +++ b/libs/server/API/GarnetWatchApi.cs @@ -261,6 +261,15 @@ public GarnetStatus SetUnion(ArgSlice[] keys, out HashSet output) return garnetApi.SetUnion(keys, out output); } + /// + public GarnetStatus SetIntersect(ArgSlice[] keys, out HashSet output) + { + foreach (var key in keys) + { + garnetApi.WATCH(key, StoreType.Object); + } + return garnetApi.SetIntersect(keys, out output); + } /// public GarnetStatus SetDiff(ArgSlice[] keys, out HashSet output) diff --git a/libs/server/API/IGarnetApi.cs b/libs/server/API/IGarnetApi.cs index 73019e8da7..096c0621bf 100644 --- a/libs/server/API/IGarnetApi.cs +++ b/libs/server/API/IGarnetApi.cs @@ -556,6 +556,16 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi /// GarnetStatus SetUnionStore(byte[] key, ArgSlice[] keys, out int count); + /// + /// This command is equal to SINTER, but instead of returning the resulting set, it is stored in destination. + /// If destination already exists, it is overwritten. + /// + /// + /// + /// + /// + GarnetStatus SetIntersectStore(byte[] key, ArgSlice[] keys, out int count); + /// /// This command is equal to SDIFF, but instead of returning the resulting set, it is stored in destination. /// If destination already exists, it is overwritten. @@ -1248,6 +1258,15 @@ public interface IGarnetReadApi /// GarnetStatus SetUnion(ArgSlice[] keys, out HashSet output); + /// + /// Returns the members of the set resulting from the intersection of all the given sets. + /// Keys that do not exist are considered to be empty sets. + /// + /// + /// + /// + GarnetStatus SetIntersect(ArgSlice[] keys, out HashSet output); + /// /// Returns the members of the set resulting from the difference between the first set and all the successive sets. /// diff --git a/libs/server/Objects/Set/SetObject.cs b/libs/server/Objects/Set/SetObject.cs index f85dee478b..ae0e3e295d 100644 --- a/libs/server/Objects/Set/SetObject.cs +++ b/libs/server/Objects/Set/SetObject.cs @@ -30,6 +30,8 @@ public enum SetOperation : byte SUNIONSTORE, SDIFF, SDIFFSTORE, + SINTER, + SINTERSTORE } diff --git a/libs/server/Resp/Objects/SetCommands.cs b/libs/server/Resp/Objects/SetCommands.cs index 84ca427f56..0c18a56736 100644 --- a/libs/server/Resp/Objects/SetCommands.cs +++ b/libs/server/Resp/Objects/SetCommands.cs @@ -96,6 +96,129 @@ private unsafe bool SetAdd(int count, byte* ptr, ref TGarnetApi stor return true; } + /// + /// Returns the members of the set resulting from the intersection of all the given sets. + /// Keys that do not exist are considered to be empty sets. + /// + /// + /// + /// + /// + /// + private bool SetIntersect(int count, byte* ptr, ref TGarnetApi storageApi) + where TGarnetApi : IGarnetApi + { + if (count < 1) + { + return AbortWithWrongNumberOfArguments("SINTER", count); + } + + // Read all the keys + ArgSlice[] keys = new ArgSlice[count]; + + for (int i = 0; i < keys.Length; i++) + { + keys[i] = default; + if (!RespReadUtils.ReadPtrWithLengthHeader(ref keys[i].ptr, ref keys[i].length, ref ptr, recvBufferPtr + bytesRead)) + return false; + } + + if (NetworkKeyArraySlotVerify(ref keys, true)) + { + var bufSpan = new ReadOnlySpan(recvBufferPtr, bytesRead); + if (!DrainCommands(bufSpan, count)) return false; + return true; + } + + var status = storageApi.SetIntersect(keys, out var result); + + if (status == GarnetStatus.OK) + { + // write the size of result + int resultCount = 0; + if (result != null) + { + resultCount = result.Count; + while (!RespWriteUtils.WriteArrayLength(resultCount, ref dcurr, dend)) + SendAndReset(); + + foreach (var item in result) + { + while (!RespWriteUtils.WriteBulkString(item, ref dcurr, dend)) + SendAndReset(); + } + } + else + { + while (!RespWriteUtils.WriteArrayLength(resultCount, ref dcurr, dend)) + SendAndReset(); + } + } + + // update read pointers + readHead = (int)(ptr - recvBufferPtr); + return true; + } + + /// + /// This command is equal to SINTER, but instead of returning the resulting set, it is stored in destination. + /// If destination already exists, it is overwritten. + /// + /// + /// + /// + /// + /// + private bool SetIntersectStore(int count, byte* ptr, ref TGarnetApi storageApi) + where TGarnetApi : IGarnetApi + { + if (count < 2) + { + return AbortWithWrongNumberOfArguments("SINTERSTORE", count); + } + + // Get the key + if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var key, ref ptr, recvBufferPtr + bytesRead)) + return false; + + if (NetworkSingleKeySlotVerify(key, false)) + { + var bufSpan = new ReadOnlySpan(recvBufferPtr, bytesRead); + if (!DrainCommands(bufSpan, count)) + return false; + return true; + } + + var keys = new ArgSlice[count - 1]; + for (var i = 0; i < count - 1; i++) + { + keys[i] = default; + if (!RespReadUtils.ReadPtrWithLengthHeader(ref keys[i].ptr, ref keys[i].length, ref ptr, recvBufferPtr + bytesRead)) + return false; + } + + if (NetworkKeyArraySlotVerify(ref keys, true)) + { + var bufSpan = new ReadOnlySpan(recvBufferPtr, bytesRead); + if (!DrainCommands(bufSpan, count)) return false; + return true; + } + + var status = storageApi.SetIntersectStore(key, keys, out var output); + + if (status == GarnetStatus.OK) + { + while (!RespWriteUtils.WriteInteger(output, ref dcurr, dend)) + SendAndReset(); + } + + // Move input head + readHead = (int)(ptr - recvBufferPtr); + + return true; + } + + /// /// Returns the members of the set resulting from the union of all the given sets. /// Keys that do not exist are considered to be empty sets. diff --git a/libs/server/Resp/RespCommand.cs b/libs/server/Resp/RespCommand.cs index 9b2affb0a5..56258ae552 100644 --- a/libs/server/Resp/RespCommand.cs +++ b/libs/server/Resp/RespCommand.cs @@ -708,6 +708,10 @@ static RespCommand MatchedNone(RespServerSession session, int oldReadHead) { return (RespCommand.Set, (byte)SetOperation.SUNION); } + else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("SINTER\r\n"u8)) + { + return (RespCommand.Set, (byte)SetOperation.SINTER); + } break; case 'U': @@ -915,6 +919,10 @@ static RespCommand MatchedNone(RespServerSession session, int oldReadHead) { return (RespCommand.Set, (byte)SetOperation.SUNIONSTORE); } + else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read("1\r\nSINTE"u8) && *(ulong*)(ptr + 10) == MemoryMarshal.Read("RSTORE\r\n"u8)) + { + return (RespCommand.Set, (byte)SetOperation.SINTERSTORE); + } break; case 12: diff --git a/libs/server/Resp/RespCommandsInfo.json b/libs/server/Resp/RespCommandsInfo.json index 0b762e6c63..c73569f25a 100644 --- a/libs/server/Resp/RespCommandsInfo.json +++ b/libs/server/Resp/RespCommandsInfo.json @@ -3887,6 +3887,38 @@ ], "SubCommands": null }, + { + "Command": "Set", + "ArrayCommand": 13, + "Name": "SINTER", + "IsInternal": false, + "Arity": -2, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "Read, Set, Slow", + "Tips": [ + "nondeterministic_output_order" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, { "Command": "Set", "ArrayCommand": 10, @@ -3931,6 +3963,50 @@ ], "SubCommands": null }, + { + "Command": "Set", + "ArrayCommand": 14, + "Name": "SINTERSTORE", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "Set, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "OW, Update" + }, + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 2 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, { "Command": "TIME", "ArrayCommand": null, diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index a14c99226f..29e0654580 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -529,6 +529,8 @@ private bool ProcessArrayCommands(RespCommand cmd, byte subcmd, int (RespCommand.Set, (byte)SetOperation.SUNIONSTORE) => SetUnionStore(count, ptr, ref storageApi), (RespCommand.Set, (byte)SetOperation.SDIFF) => SetDiff(count, ptr, ref storageApi), (RespCommand.Set, (byte)SetOperation.SDIFFSTORE) => SetDiffStore(count, ptr, ref storageApi), + (RespCommand.Set, (byte)SetOperation.SINTER) => SetIntersect(count, ptr, ref storageApi), + (RespCommand.Set, (byte)SetOperation.SINTERSTORE) => SetIntersectStore(count, ptr, ref storageApi), _ => ProcessOtherCommands(cmd, subcmd, count, ref storageApi), }; return success; diff --git a/libs/server/Storage/Session/ObjectStore/SetOps.cs b/libs/server/Storage/Session/ObjectStore/SetOps.cs index 03ad1f93f9..d682df4a72 100644 --- a/libs/server/Storage/Session/ObjectStore/SetOps.cs +++ b/libs/server/Storage/Session/ObjectStore/SetOps.cs @@ -421,6 +421,147 @@ internal unsafe GarnetStatus SetMove(ArgSlice sourceKey, ArgSlice destinationKey return GarnetStatus.OK; } + + /// + /// Returns the members of the set resulting from the intersection of all the given sets. + /// Keys that do not exist are considered to be empty sets. + /// + /// + /// + /// + public GarnetStatus SetIntersect(ArgSlice[] keys, out HashSet output) + { + output = default; + + if (keys.Length == 0) + return GarnetStatus.OK; + + var createTransaction = false; + + if (txnManager.state != TxnState.Running) + { + Debug.Assert(txnManager.state == TxnState.None); + createTransaction = true; + foreach (var item in keys) + txnManager.SaveKeyEntryToLock(item, true, LockType.Shared); + _ = txnManager.Run(true); + } + + // SetObject + var setObjectStoreLockableContext = txnManager.ObjectStoreLockableContext; + + try + { + output = SetIntersect(keys, ref setObjectStoreLockableContext); + } + finally + { + if (createTransaction) + txnManager.Commit(true); + } + + return GarnetStatus.OK; + } + + /// + /// This command is equal to SINTER, but instead of returning the resulting set, it is stored in destination. + /// If destination already exists, it is overwritten. + /// + /// + /// + /// + /// + public GarnetStatus SetIntersectStore(byte[] key, ArgSlice[] keys, out int count) + { + count = default; + + if (keys.Length == 0) + { + return GarnetStatus.OK; + } + + var destination = scratchBufferManager.CreateArgSlice(key); + + var createTransaction = false; + + if (txnManager.state != TxnState.Running) + { + Debug.Assert(txnManager.state == TxnState.None); + createTransaction = true; + txnManager.SaveKeyEntryToLock(destination, true, LockType.Exclusive); + foreach (var item in keys) + txnManager.SaveKeyEntryToLock(item, true, LockType.Shared); + _ = txnManager.Run(true); + } + + // SetObject + var setObjectStoreLockableContext = txnManager.ObjectStoreLockableContext; + + try + { + var members = SetIntersect(keys, ref setObjectStoreLockableContext); + + var newSetObject = new SetObject(); + foreach (var item in members) + { + _ = newSetObject.Set.Add(item); + newSetObject.UpdateSize(item); + } + _ = SET(key, newSetObject, ref setObjectStoreLockableContext); + count = members.Count; + } + finally + { + if (createTransaction) + txnManager.Commit(true); + } + + return GarnetStatus.OK; + } + + + private HashSet SetIntersect(ArgSlice[] keys, ref TObjectContext objectContext) + where TObjectContext : ITsavoriteContext + { + if (keys.Length == 0) + { + return new HashSet(ByteArrayComparer.Instance); + } + + HashSet result; + var status = GET(keys[0].ToArray(), out var first, ref objectContext); + if (status == GarnetStatus.OK && first.garnetObject is SetObject firstObject) + { + result = new HashSet(firstObject.Set, ByteArrayComparer.Instance); + } + else + { + return new HashSet(ByteArrayComparer.Instance); + } + + + for (var i = 1; i < keys.Length; i++) + { + // intersection of anything with empty set is empty set + if (result.Count == 0) + { + return result; + } + + status = GET(keys[i].ToArray(), out var next, ref objectContext); + if (status == GarnetStatus.OK && next.garnetObject is SetObject nextObject) + { + result.IntersectWith(nextObject.Set); + } + else + { + return new HashSet(ByteArrayComparer.Instance); + } + } + + return result; + } + /// /// Returns the members of the set resulting from the union of all the given sets. /// Keys that do not exist are considered to be empty sets. diff --git a/libs/server/Transaction/TxnKeyManager.cs b/libs/server/Transaction/TxnKeyManager.cs index a16512ef69..8145960b5d 100644 --- a/libs/server/Transaction/TxnKeyManager.cs +++ b/libs/server/Transaction/TxnKeyManager.cs @@ -189,6 +189,8 @@ private int SetObjectKeys(byte subCommand, int inputCount) (byte)SetOperation.SDIFF => ListKeys(inputCount, true, LockType.Shared), (byte)SetOperation.SDIFFSTORE => XSTOREKeys(inputCount, true), (byte)SetOperation.SMOVE => ListKeys(inputCount, true, LockType.Exclusive), + (byte)SetOperation.SINTER => ListKeys(inputCount, true, LockType.Shared), + (byte)SetOperation.SINTERSTORE => XSTOREKeys(inputCount, true), _ => -1 }; } diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs index 9b04d60c9b..129d95c228 100644 --- a/playground/CommandInfoUpdater/SupportedCommand.cs +++ b/playground/CommandInfoUpdater/SupportedCommand.cs @@ -187,6 +187,8 @@ public class SupportedCommand new("SUBSCRIBE", RespCommand.SUBSCRIBE), new("SUNION", RespCommand.Set, (byte)SetOperation.SUNION), new("SUNIONSTORE", RespCommand.Set, (byte)SetOperation.SUNIONSTORE), + new("SINTER", RespCommand.Set, (byte)SetOperation.SINTER), + new("SINTERSTORE", RespCommand.Set, (byte)SetOperation.SINTERSTORE), new("TIME", RespCommand.TIME), new("TTL", RespCommand.TTL), new("TYPE", RespCommand.TYPE), diff --git a/test/Garnet.test/RespSetTest.cs b/test/Garnet.test/RespSetTest.cs index 257a4244f2..1f0793d7c8 100644 --- a/test/Garnet.test/RespSetTest.cs +++ b/test/Garnet.test/RespSetTest.cs @@ -383,6 +383,75 @@ public void CanDoSetUnionStore(string key) Assert.IsTrue(expectedResult.OrderBy(t => t).SequenceEqual(strResult.OrderBy(t => t))); } + + [Test] + public void CanDoSetInter() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var redisValues1 = new RedisValue[] { "item-a", "item-b", "item-c", "item-d" }; + var result = db.SetAdd(new RedisKey("key1"), redisValues1); + Assert.AreEqual(4, result); + + result = db.SetAdd(new RedisKey("key2"), ["item-c"]); + Assert.AreEqual(1, result); + + result = db.SetAdd(new RedisKey("key3"), ["item-a", "item-c", "item-e"]); + Assert.AreEqual(3, result); + + var members = db.SetCombine(SetOperation.Intersect, ["key1", "key2", "key3"]); + RedisValue[] entries = ["item-c"]; + Assert.AreEqual(1, members.Length); + // assert two arrays are equal ignoring order + Assert.IsTrue(members.OrderBy(x => x).SequenceEqual(entries.OrderBy(x => x))); + + members = db.SetCombine(SetOperation.Intersect, ["key1", "key2", "key3", "_not_exists"]); + Assert.IsEmpty(members); + + members = db.SetCombine(SetOperation.Intersect, ["_not_exists_1", "_not_exists_2", "_not_exists_3"]); + Assert.IsEmpty(members); + + + try + { + db.SetCombine(SetOperation.Intersect, []); + Assert.Fail(); + } + catch (RedisServerException e) + { + Assert.AreEqual(string.Format(CmdStrings.GenericErrWrongNumArgs, "SINTER"), e.Message); + } + } + + [Test] + public void CanDoSetInterStore() + { + string key = "key"; + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key1 = "key1"; + var key1Value = new RedisValue[] { "a", "b", "c" }; + + var key2 = "key2"; + var key2Value = new RedisValue[] { "c", "d", "e" }; + + var addResult = db.SetAdd(key1, key1Value); + Assert.AreEqual(key1Value.Length, addResult); + addResult = db.SetAdd(key2, key2Value); + Assert.AreEqual(key2Value.Length, addResult); + + var result = (int)db.Execute("SINTERSTORE", key, key1, key2); + Assert.AreEqual(1, result); + + var membersResult = db.SetMembers(key); + Assert.AreEqual(1, membersResult.Length); + var strResult = membersResult.Select(m => m.ToString()).ToArray(); + var expectedResult = new[] { "c" }; + Assert.IsTrue(expectedResult.SequenceEqual(strResult)); + } + + [Test] [TestCase("key1", "key2")] [TestCase("", "key2")] @@ -977,6 +1046,90 @@ public void CanDoSdiffLC() Assert.AreEqual(expectedResponse, response.AsSpan().Slice(0, expectedResponse.Length).ToArray()); } + + [Test] + public void CanDoSinterLC() + { + var lightClientRequest = TestUtils.CreateRequest(); + lightClientRequest.SendCommand("SADD key1 a b c d"); + lightClientRequest.SendCommand("SADD key2 c"); + lightClientRequest.SendCommand("SADD key3 a c e"); + var response = lightClientRequest.SendCommand("SINTER key1 key2 key3"); + var expectedResponse = "*1\r\n$1\r\nc\r\n"; + Assert.AreEqual(expectedResponse, response.AsSpan().Slice(0, expectedResponse.Length).ToArray()); + } + + [Test] + public void IntersectWithEmptySetReturnEmptySet() + { + var lightClientRequest = TestUtils.CreateRequest(); + lightClientRequest.SendCommand("SADD key1 a"); + + var response = lightClientRequest.SendCommand("SINTER key1 key2"); + var expectedResponse = "*0\r\n"; + Assert.AreEqual(expectedResponse, response.AsSpan().Slice(0, expectedResponse.Length).ToArray()); + } + + [Test] + public void IntersectWithNoKeysReturnError() + { + var lightClientRequest = TestUtils.CreateRequest(); + var response = lightClientRequest.SendCommand("SINTER"); + var expectedResponse = "-ERR wrong number of arguments for 'SINTER' command\r\n"; + Assert.AreEqual(expectedResponse, response.AsSpan().Slice(0, expectedResponse.Length).ToArray()); + } + + [Test] + public void IntersectAndStoreWithNoKeysReturnError() + { + var lightClientRequest = TestUtils.CreateRequest(); + var response = lightClientRequest.SendCommand("SINTERSTORE"); + var expectedResponse = "-ERR wrong number of arguments for 'SINTERSTORE' command\r\n"; + Assert.AreEqual(expectedResponse, response.AsSpan().Slice(0, expectedResponse.Length).ToArray()); + } + + + [Test] + public void IntersectAndStoreWithNotExisingSetsOverwitesDestinationSet() + { + var lightClientRequest = TestUtils.CreateRequest(); + lightClientRequest.SendCommand("SADD key a"); + + var SINTERSTOREResponse = lightClientRequest.SendCommand("SINTERSTORE key key1 key2 key3"); + var expectedSINTERSTOREResponse = ":0\r\n"; + Assert.AreEqual(expectedSINTERSTOREResponse, SINTERSTOREResponse.AsSpan().Slice(0, expectedSINTERSTOREResponse.Length).ToArray()); + + var membersResponse = lightClientRequest.SendCommand("SMEMBERS key"); + var expectedResponse = "*0\r\n"; + Assert.AreEqual(expectedResponse, membersResponse.AsSpan().Slice(0, expectedResponse.Length).ToArray()); + } + + [Test] + public void IntersectAndStoreWithNoSetsReturnErrWrongNumArgs() + { + var lightClientRequest = TestUtils.CreateRequest(); + var SINTERSTOREResponse = lightClientRequest.SendCommand("SINTERSTORE key"); + var expectedSINTERSTOREResponse = $"-{string.Format(CmdStrings.GenericErrWrongNumArgs, "SINTERSTORE")}\r\n"; + Assert.AreEqual(expectedSINTERSTOREResponse, SINTERSTOREResponse.AsSpan().Slice(0, expectedSINTERSTOREResponse.Length).ToArray()); + } + + + [Test] + public void CanDoSinterStoreLC() + { + var lightClientRequest = TestUtils.CreateRequest(); + lightClientRequest.SendCommand("SADD key1 a b c d"); + lightClientRequest.SendCommand("SADD key2 c"); + lightClientRequest.SendCommand("SADD key3 a c e"); + var response = lightClientRequest.SendCommand("SINTERSTORE key key1 key2 key3"); + var expectedResponse = ":1\r\n"; + Assert.AreEqual(expectedResponse, response.AsSpan().Slice(0, expectedResponse.Length).ToArray()); + + var membersResponse = lightClientRequest.SendCommand("SMEMBERS key"); + expectedResponse = "*1\r\n$1\r\nc\r\n"; + Assert.AreEqual(expectedResponse, membersResponse.AsSpan().Slice(0, expectedResponse.Length).ToArray()); + } + [Test] [TestCase("")] [TestCase("key")] @@ -1069,6 +1222,22 @@ public void CanDoSunionStoreWhenMemberKeysNotExisting() strResponse = Encoding.ASCII.GetString(membersResponse).Substring(0, expectedResponse.Length); Assert.AreEqual(expectedResponse, strResponse); } + + [Test] + public void CanDoSinterStoreWhenMemberKeysNotExisting() + { + using var lightClientRequest = TestUtils.CreateRequest(); + var response = lightClientRequest.SendCommand("SINTERSTORE key key1 key2 key3"); + var expectedResponse = ":0\r\n"; + var strResponse = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + Assert.AreEqual(expectedResponse, strResponse); + + var membersResponse = lightClientRequest.SendCommand("SMEMBERS key"); + expectedResponse = "*0\r\n"; + strResponse = Encoding.ASCII.GetString(membersResponse).Substring(0, expectedResponse.Length); + Assert.AreEqual(expectedResponse, strResponse); + } + #endregion diff --git a/website/docs/commands/api-compatibility.md b/website/docs/commands/api-compatibility.md index d14f0b42c5..2119a9e661 100644 --- a/website/docs/commands/api-compatibility.md +++ b/website/docs/commands/api-compatibility.md @@ -234,9 +234,9 @@ Note that this list is subject to change as we continue to expand our API comman | | [SCARD](data-structures.md#scard) | ➕ | | | | [SDIFF](data-structures.md#sdiff) | ➕ | | | | [SDIFFSTORE](data-structures.md#sdiffstore) | ➕ | | -| | SINTER | ➖ | | +| | [SINTER](data-structures.md#sinter) | ➕ | | +| | [SINTERSTORE](data-structures.md#sinterstore) | ➕ | | | | SINTERCARD | ➖ | | -| | SINTERSTORE | ➖ | | | | SISMEMBER | ➕ | | | | [SMEMBERS](data-structures.md#smembers) | ➕ | | | | SMISMEMBER | ➖ | | diff --git a/website/docs/commands/data-structures.md b/website/docs/commands/data-structures.md index fe56d83182..4e1b41c2b5 100644 --- a/website/docs/commands/data-structures.md +++ b/website/docs/commands/data-structures.md @@ -550,6 +550,33 @@ If **destination** already exists, it is overwritten. --- +### SINTER + +#### Syntax + +```bash + SINTER key [key ...] +``` + +Returns the members of the set resulting from the intersection of all the given sets. +Keys that do not exist are considered to be empty sets. + +--- + +### SINTERSTORE + +#### Syntax + +```bash + SINTERSTORE destination key [key ...] +``` + +This command is equal to [SINTER](#INTER), but instead of returning the resulting set, it is stored in **destination**. + +If **destination** already exists, it is overwritten. + +--- + ### SDIFF #### Syntax