diff --git a/libs/resources/RespCommandsDocs.json b/libs/resources/RespCommandsDocs.json
index 55cd618133..c7f576aad0 100644
--- a/libs/resources/RespCommandsDocs.json
+++ b/libs/resources/RespCommandsDocs.json
@@ -5787,6 +5787,58 @@
}
]
},
+ {
+ "Command": "ZMPOP",
+ "Name": "ZMPOP",
+ "Summary": "Returns the highest- or lowest-scoring members from one or more sorted sets after removing them. Deletes the sorted set if the last member was popped.",
+ "Group": "SortedSet",
+ "Complexity": "O(K) \u002B O(M*log(N)) where K is the number of provided keys, N being the number of elements in the sorted set, and M being the number of elements popped.",
+ "Arguments": [
+ {
+ "TypeDiscriminator": "RespCommandBasicArgument",
+ "Name": "NUMKEYS",
+ "DisplayText": "numkeys",
+ "Type": "Integer"
+ },
+ {
+ "TypeDiscriminator": "RespCommandKeyArgument",
+ "Name": "KEY",
+ "DisplayText": "key",
+ "Type": "Key",
+ "ArgumentFlags": "Multiple",
+ "KeySpecIndex": 0
+ },
+ {
+ "TypeDiscriminator": "RespCommandContainerArgument",
+ "Name": "WHERE",
+ "Type": "OneOf",
+ "Arguments": [
+ {
+ "TypeDiscriminator": "RespCommandBasicArgument",
+ "Name": "MIN",
+ "DisplayText": "min",
+ "Type": "PureToken",
+ "Token": "MIN"
+ },
+ {
+ "TypeDiscriminator": "RespCommandBasicArgument",
+ "Name": "MAX",
+ "DisplayText": "max",
+ "Type": "PureToken",
+ "Token": "MAX"
+ }
+ ]
+ },
+ {
+ "TypeDiscriminator": "RespCommandBasicArgument",
+ "Name": "COUNT",
+ "DisplayText": "count",
+ "Type": "Integer",
+ "Token": "COUNT",
+ "ArgumentFlags": "Optional"
+ }
+ ]
+ },
{
"Command": "ZMSCORE",
"Name": "ZMSCORE",
diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json
index 583ae9ec6a..0216f7f582 100644
--- a/libs/resources/RespCommandsInfo.json
+++ b/libs/resources/RespCommandsInfo.json
@@ -4300,6 +4300,28 @@
}
]
},
+ {
+ "Command": "ZMPOP",
+ "Name": "ZMPOP",
+ "Arity": -4,
+ "Flags": "MovableKeys, Write",
+ "AclCategories": "SortedSet, Slow, Write",
+ "KeySpecifications": [
+ {
+ "BeginSearch": {
+ "TypeDiscriminator": "BeginSearchIndex",
+ "Index": 1
+ },
+ "FindKeys": {
+ "TypeDiscriminator": "FindKeysKeyNum",
+ "KeyNumIdx": 0,
+ "FirstKey": 1,
+ "KeyStep": 1
+ },
+ "Flags": "RW, Access, Delete"
+ }
+ ]
+ },
{
"Command": "ZMSCORE",
"Name": "ZMSCORE",
diff --git a/libs/server/API/GarnetApiObjectCommands.cs b/libs/server/API/GarnetApiObjectCommands.cs
index 927107c3e0..670db210ee 100644
--- a/libs/server/API/GarnetApiObjectCommands.cs
+++ b/libs/server/API/GarnetApiObjectCommands.cs
@@ -74,6 +74,10 @@ public GarnetStatus SortedSetScores(byte[] key, ref ObjectInput input, ref Garne
public GarnetStatus SortedSetPop(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter)
=> storageSession.SortedSetPop(key, ref input, ref outputFooter, ref objectContext);
+ ///
+ public GarnetStatus SortedSetMPop(ReadOnlySpan keys, int count, bool lowScoresFirst, out ArgSlice poppedKey, out (ArgSlice member, ArgSlice score)[] pairs)
+ => storageSession.SortedSetMPop(keys, count, lowScoresFirst, out poppedKey, out pairs);
+
///
public GarnetStatus SortedSetPop(ArgSlice key, out (ArgSlice member, ArgSlice score)[] pairs, int count = 1, bool lowScoresFirst = true)
=> storageSession.SortedSetPop(key, count, lowScoresFirst, out pairs, ref objectContext);
diff --git a/libs/server/API/IGarnetApi.cs b/libs/server/API/IGarnetApi.cs
index 14851c1148..15d9c2255a 100644
--- a/libs/server/API/IGarnetApi.cs
+++ b/libs/server/API/IGarnetApi.cs
@@ -410,6 +410,17 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi
///
GarnetStatus SortedSetPop(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter);
+ ///
+ /// Removes and returns multiple elements from a sorted set.
+ ///
+ /// The keys of the sorted set.
+ /// The number of elements to pop.
+ /// If true, elements with the lowest scores are popped first; otherwise, elements with the highest scores are popped first.
+ /// The key of the popped element.
+ /// An array of tuples containing the member and score of each popped element.
+ /// A indicating the result of the operation.
+ GarnetStatus SortedSetMPop(ReadOnlySpan keys, int count, bool lowScoresFirst, out ArgSlice poppedKey, out (ArgSlice member, ArgSlice score)[] pairs);
+
///
/// Removes and returns up to count members with the highest or lowest scores in the sorted set stored at key.
///
diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs
index e2c48bf690..e0e40afddf 100644
--- a/libs/server/Resp/CmdStrings.cs
+++ b/libs/server/Resp/CmdStrings.cs
@@ -120,6 +120,8 @@ static partial class CmdStrings
public static ReadOnlySpan BYLEX => "BYLEX"u8;
public static ReadOnlySpan REV => "REV"u8;
public static ReadOnlySpan LIMIT => "LIMIT"u8;
+ public static ReadOnlySpan MIN => "MIN"u8;
+ public static ReadOnlySpan MAX => "MAX"u8;
///
/// Response strings
diff --git a/libs/server/Resp/Objects/SortedSetCommands.cs b/libs/server/Resp/Objects/SortedSetCommands.cs
index 4c7c7f60b5..6289f23d9e 100644
--- a/libs/server/Resp/Objects/SortedSetCommands.cs
+++ b/libs/server/Resp/Objects/SortedSetCommands.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using System;
+using System.Text;
using Garnet.common;
using Tsavorite.core;
@@ -389,6 +390,122 @@ private unsafe bool SortedSetPop(RespCommand command, ref TGarnetApi
return true;
}
+ ///
+ /// Removes and returns up to count members from the first non-empty sorted set key from the list of keys.
+ ///
+ private unsafe bool SortedSetMPop(ref TGarnetApi storageApi)
+ where TGarnetApi : IGarnetApi
+ {
+ // ZMPOP requires at least 3 args: numkeys, key [key...], [COUNT count]
+ if (parseState.Count < 3)
+ {
+ return AbortWithWrongNumberOfArguments(nameof(RespCommand.ZMPOP));
+ }
+
+ // Get number of keys
+ if (!parseState.TryGetInt(0, out var numKeys) || numKeys < 1)
+ {
+ return AbortWithErrorMessage(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER);
+ }
+
+ if (numKeys < 0)
+ {
+ return AbortWithErrorMessage(Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericParamShouldBeGreaterThanZero, "numkeys")));
+ }
+
+ // Validate we have enough arguments (no of keys + (MIN or MAX))
+ if (parseState.Count < numKeys + 2)
+ {
+ return AbortWithErrorMessage(CmdStrings.RESP_SYNTAX_ERROR);
+ }
+
+ // Get MIN/MAX argument
+ var orderArg = parseState.GetArgSliceByRef(numKeys + 1);
+ var orderSpan = orderArg.ReadOnlySpan;
+ var lowScoresFirst = true;
+
+ if (orderSpan.EqualsUpperCaseSpanIgnoringCase(CmdStrings.MIN))
+ lowScoresFirst = true;
+ else if (orderSpan.EqualsUpperCaseSpanIgnoringCase(CmdStrings.MAX))
+ lowScoresFirst = false;
+ else
+ {
+ return AbortWithErrorMessage(CmdStrings.RESP_SYNTAX_ERROR);
+ }
+
+ // Parse optional COUNT argument
+ var count = 1;
+ if (parseState.Count > numKeys + 2)
+ {
+ if (parseState.Count != numKeys + 4)
+ {
+ return AbortWithErrorMessage(CmdStrings.RESP_SYNTAX_ERROR);
+ }
+
+ var countArg = parseState.GetArgSliceByRef(numKeys + 2);
+ if (!countArg.ReadOnlySpan.EqualsUpperCaseSpanIgnoringCase(CmdStrings.COUNT))
+ {
+ return AbortWithErrorMessage(CmdStrings.RESP_SYNTAX_ERROR);
+ }
+
+ if (!parseState.TryGetInt(numKeys + 3, out count) || count < 1)
+ {
+ return AbortWithErrorMessage(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER);
+ }
+
+ if (count < 0)
+ {
+ return AbortWithErrorMessage(Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericParamShouldBeGreaterThanZero, "count")));
+ }
+ }
+
+ var keys = parseState.Parameters.Slice(1, numKeys);
+ var status = storageApi.SortedSetMPop(keys, count, lowScoresFirst, out var poppedKey, out var pairs);
+
+ switch (status)
+ {
+ case GarnetStatus.OK:
+ if (pairs == null || pairs.Length == 0)
+ {
+ // No elements found
+ while (!RespWriteUtils.WriteNull(ref dcurr, dend))
+ SendAndReset();
+ }
+ else
+ {
+ // Write array with 2 elements: key and array of elements
+ while (!RespWriteUtils.WriteArrayLength(2, ref dcurr, dend))
+ SendAndReset();
+
+ // Write key
+ while (!RespWriteUtils.WriteBulkString(poppedKey.ReadOnlySpan, ref dcurr, dend))
+ SendAndReset();
+
+ // Write array of member-score pairs
+ while (!RespWriteUtils.WriteArrayLength(pairs.Length, ref dcurr, dend))
+ SendAndReset();
+
+ foreach (var (member, score) in pairs)
+ {
+ while (!RespWriteUtils.WriteArrayLength(2, ref dcurr, dend))
+ SendAndReset();
+ while (!RespWriteUtils.WriteBulkString(member.ReadOnlySpan, ref dcurr, dend))
+ SendAndReset();
+ while (!RespWriteUtils.WriteBulkString(score.ReadOnlySpan, ref dcurr, dend))
+ SendAndReset();
+ }
+ }
+ break;
+
+ case GarnetStatus.WRONGTYPE:
+ while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend))
+ SendAndReset();
+ break;
+ }
+
+ return true;
+ }
+
///
/// Returns the number of elements in the sorted set at key with a score between min and max.
///
diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs
index c01ce43bae..a7916d157e 100644
--- a/libs/server/Resp/Parser/RespCommand.cs
+++ b/libs/server/Resp/Parser/RespCommand.cs
@@ -160,6 +160,7 @@ public enum RespCommand : ushort
ZADD,
ZDIFFSTORE,
ZINCRBY,
+ ZMPOP,
ZPOPMAX,
ZPOPMIN,
ZRANGESTORE,
@@ -1044,6 +1045,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan
{
return RespCommand.ZSCAN;
}
+ else if (*(ulong*)(ptr + 3) == MemoryMarshal.Read("\nZMPOP\r\n"u8))
+ {
+ return RespCommand.ZMPOP;
+ }
break;
}
break;
diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs
index edccbb070d..fe14d4b157 100644
--- a/libs/server/Resp/RespServerSession.cs
+++ b/libs/server/Resp/RespServerSession.cs
@@ -615,6 +615,7 @@ private bool ProcessArrayCommands(RespCommand cmd, ref TGarnetApi st
RespCommand.ZREMRANGEBYSCORE => SortedSetRemoveRange(cmd, ref storageApi),
RespCommand.ZLEXCOUNT => SortedSetLengthByValue(cmd, ref storageApi),
RespCommand.ZPOPMIN => SortedSetPop(cmd, ref storageApi),
+ RespCommand.ZMPOP => SortedSetMPop(ref storageApi),
RespCommand.ZRANDMEMBER => SortedSetRandomMember(ref storageApi),
RespCommand.ZDIFF => SortedSetDifference(ref storageApi),
RespCommand.ZDIFFSTORE => SortedSetDifferenceStore(ref storageApi),
diff --git a/libs/server/Storage/Session/ObjectStore/SortedSetOps.cs b/libs/server/Storage/Session/ObjectStore/SortedSetOps.cs
index 1c6df20744..72b7896be9 100644
--- a/libs/server/Storage/Session/ObjectStore/SortedSetOps.cs
+++ b/libs/server/Storage/Session/ObjectStore/SortedSetOps.cs
@@ -1032,5 +1032,61 @@ private GarnetStatus SortedSetDifference(ReadOnlySpan
return GarnetStatus.OK;
}
+
+ ///
+ /// Removes and returns up to count members and their scores from the first sorted set that contains a member.
+ ///
+ public unsafe GarnetStatus SortedSetMPop(ReadOnlySpan keys, int count, bool lowScoresFirst, out ArgSlice poppedKey, out (ArgSlice member, ArgSlice score)[] pairs)
+ {
+ if (txnManager.ObjectStoreLockableContext.Session is null)
+ ThrowObjectStoreUninitializedException();
+
+ pairs = default;
+ poppedKey = 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 key in keys)
+ txnManager.SaveKeyEntryToLock(key, true, LockType.Exclusive);
+ txnManager.Run(true);
+ }
+
+ var storeLockableContext = txnManager.ObjectStoreLockableContext;
+
+ try
+ {
+ // Try popping from each key until we find one with members
+ foreach (var key in keys)
+ {
+ if (key.Length == 0) continue;
+
+ var status = SortedSetPop(key, count, lowScoresFirst, out pairs, ref storeLockableContext);
+ if (status == GarnetStatus.OK && pairs != null && pairs.Length > 0)
+ {
+ poppedKey = key;
+ return status;
+ }
+
+ if (status != GarnetStatus.OK && status != GarnetStatus.NOTFOUND)
+ {
+ return status;
+ }
+ }
+
+ return GarnetStatus.OK;
+ }
+ finally
+ {
+ if (createTransaction)
+ txnManager.Commit(true);
+ }
+ }
}
}
\ No newline at end of file
diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs
index ddd6f5172d..40d6ca496d 100644
--- a/playground/CommandInfoUpdater/SupportedCommand.cs
+++ b/playground/CommandInfoUpdater/SupportedCommand.cs
@@ -270,6 +270,7 @@ public class SupportedCommand
new("ZINCRBY", RespCommand.ZINCRBY),
new("ZLEXCOUNT", RespCommand.ZLEXCOUNT),
new("ZMSCORE", RespCommand.ZMSCORE),
+ new("ZMPOP", RespCommand.ZMPOP),
new("ZPOPMAX", RespCommand.ZPOPMAX),
new("ZPOPMIN", RespCommand.ZPOPMIN),
new("ZRANDMEMBER", RespCommand.ZRANDMEMBER),
diff --git a/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs b/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs
index b52911236b..e256ede56c 100644
--- a/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs
+++ b/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs
@@ -2026,6 +2026,35 @@ public override ArraySegment[] SetupSingleSlotRequest()
}
}
+ internal class ZMPOP : BaseCommand
+ {
+ public override bool IsArrayCommand => true;
+ public override bool ArrayResponse => false;
+ public override string Command => nameof(ZMPOP);
+
+ public override string[] GetSingleSlotRequest()
+ {
+ var ssk = GetSingleSlotKeys;
+ return ["3", ssk[0], ssk[1], ssk[2], "MIN", "COUNT", "1"];
+ }
+
+ public override string[] GetCrossSlotRequest()
+ {
+ var csk = GetCrossSlotKeys;
+ return ["3", csk[0], csk[1], csk[2], "MIN", "COUNT", "1"];
+ }
+
+ public override ArraySegment[] SetupSingleSlotRequest()
+ {
+ var ssk = GetSingleSlotKeys;
+ var setup = new ArraySegment[3];
+ setup[0] = new ArraySegment(["ZADD", ssk[1], "1", "a"]);
+ setup[1] = new ArraySegment(["ZADD", ssk[2], "2", "b"]);
+ setup[2] = new ArraySegment(["ZADD", ssk[3], "3", "c"]);
+ return setup;
+ }
+ }
+
#endregion
#region HashCommands
diff --git a/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs b/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs
index b569a0aa8b..59c6bb488a 100644
--- a/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs
+++ b/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs
@@ -70,6 +70,7 @@ public class ClusterSlotVerificationTests
new LPUSH(),
new LPOP(),
new LMPOP(),
+ new ZMPOP(),
new BLPOP(),
new BLMOVE(),
new BRPOPLPUSH(),
@@ -253,6 +254,7 @@ public virtual void OneTimeTearDown()
[TestCase("LPUSH")]
[TestCase("LPOP")]
[TestCase("LMPOP")]
+ [TestCase("ZMPOP")]
[TestCase("BLPOP")]
[TestCase("BLMOVE")]
[TestCase("BRPOPLPUSH")]
@@ -396,6 +398,7 @@ void GarnetClientSessionClusterDown(BaseCommand command)
[TestCase("LPUSH")]
[TestCase("LPOP")]
[TestCase("LMPOP")]
+ [TestCase("ZMPOP")]
[TestCase("BLPOP")]
[TestCase("BLMOVE")]
[TestCase("BRPOPLPUSH")]
@@ -549,6 +552,7 @@ void GarnetClientSessionOK(BaseCommand command)
[TestCase("LPUSH")]
[TestCase("LPOP")]
[TestCase("LMPOP")]
+ [TestCase("ZMPOP")]
[TestCase("BLPOP")]
[TestCase("BLMOVE")]
[TestCase("BRPOPLPUSH")]
@@ -694,6 +698,7 @@ void GarnetClientSessionCrossslotTest(BaseCommand command)
[TestCase("LPUSH")]
[TestCase("LPOP")]
[TestCase("LMPOP")]
+ [TestCase("ZMPOP")]
[TestCase("BLPOP")]
[TestCase("BLMOVE")]
[TestCase("BRPOPLPUSH")]
@@ -846,6 +851,7 @@ void GarnetClientSessionMOVEDTest(BaseCommand command)
[TestCase("LPUSH")]
[TestCase("LPOP")]
[TestCase("LMPOP")]
+ [TestCase("ZMPOP")]
[TestCase("BLPOP")]
[TestCase("BLMOVE")]
[TestCase("BRPOPLPUSH")]
@@ -1015,6 +1021,7 @@ void GarnetClientSessionASKTest(BaseCommand command)
[TestCase("LPUSH")]
[TestCase("LPOP")]
[TestCase("LMPOP")]
+ [TestCase("ZMPOP")]
[TestCase("BLPOP")]
[TestCase("BLMOVE")]
[TestCase("BRPOPLPUSH")]
diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs
index a4f74634ef..02c2aa37e7 100644
--- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs
+++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs
@@ -5658,6 +5658,31 @@ static async Task DoZCardAsync(GarnetClient client)
}
}
+
+
+ [Test]
+ public async Task ZMPopACLsAsync()
+ {
+ await CheckCommandsAsync(
+ "ZMPOP",
+ [DoZMPopAsync, DoZMPopCountAsync]
+ );
+
+ static async Task DoZMPopAsync(GarnetClient client)
+ {
+ string[] val = await client.ExecuteForStringArrayResultAsync("ZMPOP", ["2", "foo", "bar", "MIN"]);
+ ClassicAssert.AreEqual(1, val.Length);
+ ClassicAssert.IsNull(val[0]);
+ }
+
+ static async Task DoZMPopCountAsync(GarnetClient client)
+ {
+ string[] val = await client.ExecuteForStringArrayResultAsync("ZMPOP", ["2", "foo", "bar", "MAX", "COUNT", "10"]);
+ ClassicAssert.AreEqual(1, val.Length);
+ ClassicAssert.IsNull(val[0]);
+ }
+ }
+
[Test]
public async Task ZPopMaxACLsAsync()
{
diff --git a/test/Garnet.test/RespSortedSetTests.cs b/test/Garnet.test/RespSortedSetTests.cs
index 90ef72febc..13da291281 100644
--- a/test/Garnet.test/RespSortedSetTests.cs
+++ b/test/Garnet.test/RespSortedSetTests.cs
@@ -1250,6 +1250,103 @@ public void TestCheckSortedSetRangeStoreWithExistingDestinationKeySE()
}
}
+ [Test]
+ [TestCase("board1", 1, Description = "Pop from single key")]
+ [TestCase("board2", 3, Description = "Pop multiple elements")]
+ public void SortedSetMultiPopTest(string key, int count)
+ {
+ using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig());
+ var db = redis.GetDatabase(0);
+ db.SortedSetAdd(key, entries);
+
+ var result = db.Execute("ZMPOP", 1, key, "MIN", "COUNT", count);
+ ClassicAssert.IsNotNull(result);
+ var popResult = (RedisResult[])result;
+ ClassicAssert.AreEqual(key, (string)popResult[0]);
+
+ var poppedItems = (RedisResult[])popResult[1];
+ ClassicAssert.AreEqual(Math.Min(count, entries.Length), poppedItems.Length);
+
+ if (count == 1)
+ {
+ var element = poppedItems[0];
+ ClassicAssert.AreEqual("a", (string)element[0]);
+ ClassicAssert.AreEqual("1", (string)element[1]);
+ }
+ }
+
+ [Test]
+ [TestCase(new string[] { "board1" }, "MAX", 1, new string[] { "j" }, new double[] { 10.0 }, Description = "Pop maximum element from single key with count")]
+ [TestCase(new string[] { "board1" }, "MIN", 1, new string[] { "a" }, new double[] { 1.0 }, Description = "Pop minimum element from single key with count")]
+ [TestCase(new string[] { "board1" }, "MAX", 3, new string[] { "j", "i", "h" }, new double[] { 10.0, 9.0, 8.0 }, Description = "Pop multiple maximum elements from single key with count")]
+ [TestCase(new string[] { "board1" }, "MIN", 3, new string[] { "a", "b", "c" }, new double[] { 1.0, 2.0, 3.0 }, Description = "Pop multiple minimum elements from single key with count")]
+ [TestCase(new string[] { "board1", "nokey1" }, "MAX", 1, new string[] { "j" }, new double[] { 10.0 }, Description = "Pop maximum element from mixed existing and missing keys with count")]
+ [TestCase(new string[] { "board1", "nokey1" }, "MIN", 1, new string[] { "a" }, new double[] { 1.0 }, Description = "Pop minimum element from mixed existing and missing keys with count")]
+ [TestCase(new string[] { "nokey1", "nokey2" }, "MAX", 1, new string[] { }, new double[] { }, Description = "Pop maximum element from all missing keys with count")]
+ [TestCase(new string[] { "nokey1", "nokey2" }, "MIN", 1, new string[] { }, new double[] { }, Description = "Pop minimum element from all missing keys with count")]
+ [TestCase(new string[] { "board1", "nokey1" }, "MAX", null, new string[] { "j" }, new double[] { 10.0 }, Description = "Pop maximum element from mixed existing and missing keys without count")]
+ [TestCase(new string[] { "board1", "nokey1" }, "MIN", null, new string[] { "a" }, new double[] { 1.0 }, Description = "Pop minimum element from mixed existing and missing keys without count")]
+ public void SortedSetMultiPopWithOptionsTest(string[] keys, string direction, int? count, string[] expectedValues, double[] expectedScores)
+ {
+ using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig());
+ var db = redis.GetDatabase(0);
+
+ if (keys[0] == "board1")
+ {
+ db.SortedSetAdd(keys[0], entries);
+ }
+
+ List