diff --git a/libs/resources/RespCommandsDocs.json b/libs/resources/RespCommandsDocs.json
index 1c6b34e89f..55cd618133 100644
--- a/libs/resources/RespCommandsDocs.json
+++ b/libs/resources/RespCommandsDocs.json
@@ -6040,6 +6040,92 @@
}
]
},
+ {
+ "Command": "ZRANGESTORE",
+ "Name": "ZRANGESTORE",
+ "Summary": "Stores a range of members from sorted set in a key.",
+ "Group": "SortedSet",
+ "Complexity": "O(log(N)\u002BM) with N being the number of elements in the sorted set and M the number of elements stored into the destination key.",
+ "Arguments": [
+ {
+ "TypeDiscriminator": "RespCommandKeyArgument",
+ "Name": "DST",
+ "DisplayText": "dst",
+ "Type": "Key",
+ "KeySpecIndex": 0
+ },
+ {
+ "TypeDiscriminator": "RespCommandKeyArgument",
+ "Name": "SRC",
+ "DisplayText": "src",
+ "Type": "Key",
+ "KeySpecIndex": 1
+ },
+ {
+ "TypeDiscriminator": "RespCommandBasicArgument",
+ "Name": "MIN",
+ "DisplayText": "min",
+ "Type": "String"
+ },
+ {
+ "TypeDiscriminator": "RespCommandBasicArgument",
+ "Name": "MAX",
+ "DisplayText": "max",
+ "Type": "String"
+ },
+ {
+ "TypeDiscriminator": "RespCommandContainerArgument",
+ "Name": "SORTBY",
+ "Type": "OneOf",
+ "ArgumentFlags": "Optional",
+ "Arguments": [
+ {
+ "TypeDiscriminator": "RespCommandBasicArgument",
+ "Name": "BYSCORE",
+ "DisplayText": "byscore",
+ "Type": "PureToken",
+ "Token": "BYSCORE"
+ },
+ {
+ "TypeDiscriminator": "RespCommandBasicArgument",
+ "Name": "BYLEX",
+ "DisplayText": "bylex",
+ "Type": "PureToken",
+ "Token": "BYLEX"
+ }
+ ]
+ },
+ {
+ "TypeDiscriminator": "RespCommandBasicArgument",
+ "Name": "REV",
+ "DisplayText": "rev",
+ "Type": "PureToken",
+ "Token": "REV",
+ "ArgumentFlags": "Optional"
+ },
+ {
+ "TypeDiscriminator": "RespCommandContainerArgument",
+ "Name": "LIMIT",
+ "Type": "Block",
+ "Token": "LIMIT",
+ "ArgumentFlags": "Optional",
+ "Arguments": [
+ {
+ "TypeDiscriminator": "RespCommandBasicArgument",
+ "Name": "OFFSET",
+ "DisplayText": "offset",
+ "Type": "Integer"
+ },
+ {
+ "TypeDiscriminator": "RespCommandBasicArgument",
+ "Name": "COUNT",
+ "DisplayText": "count",
+ "Type": "Integer"
+ }
+ ]
+ }
+ ]
+ },
{
"Command": "ZRANK",
"Name": "ZRANK",
diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json
index ccaa73b7d2..583ae9ec6a 100644
--- a/libs/resources/RespCommandsInfo.json
+++ b/libs/resources/RespCommandsInfo.json
@@ -4453,6 +4453,44 @@
}
]
},
+ {
+ "Command": "ZRANGESTORE",
+ "Name": "ZRANGESTORE",
+ "Arity": -5,
+ "Flags": "DenyOom, Write",
+ "FirstKey": 1,
+ "LastKey": 2,
+ "Step": 1,
+ "AclCategories": "SortedSet, Slow, Write",
+ "KeySpecifications": [
+ {
+ "BeginSearch": {
+ "TypeDiscriminator": "BeginSearchIndex",
+ "Index": 1
+ },
+ "FindKeys": {
+ "TypeDiscriminator": "FindKeysRange",
+ "LastKey": 0,
+ "KeyStep": 1,
+ "Limit": 0
+ },
+ "Flags": "OW, Update"
+ },
+ {
+ "BeginSearch": {
+ "TypeDiscriminator": "BeginSearchIndex",
+ "Index": 2
+ },
+ "FindKeys": {
+ "TypeDiscriminator": "FindKeysRange",
+ "LastKey": 0,
+ "KeyStep": 1,
+ "Limit": 0
+ },
+ "Flags": "RO, Access"
+ }
+ ]
+ },
{
"Command": "ZRANK",
"Name": "ZRANK",
diff --git a/libs/server/API/GarnetApiObjectCommands.cs b/libs/server/API/GarnetApiObjectCommands.cs
index 86bc00a04f..927107c3e0 100644
--- a/libs/server/API/GarnetApiObjectCommands.cs
+++ b/libs/server/API/GarnetApiObjectCommands.cs
@@ -34,6 +34,10 @@ public GarnetStatus SortedSetAdd(ArgSlice key, (ArgSlice score, ArgSlice member)
public GarnetStatus SortedSetAdd(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput output)
=> storageSession.SortedSetAdd(key, ref input, ref output, ref objectContext);
+ ///
+ public GarnetStatus SortedSetRangeStore(ArgSlice dstKey, ArgSlice srcKey, ref ObjectInput input, out int result)
+ => storageSession.SortedSetRangeStore(dstKey, srcKey, ref input, out result, ref objectContext);
+
///
public GarnetStatus SortedSetRemove(ArgSlice key, ArgSlice member, out int zremCount)
=> storageSession.SortedSetRemove(key.ToArray(), member, out zremCount, ref objectContext);
diff --git a/libs/server/API/IGarnetApi.cs b/libs/server/API/IGarnetApi.cs
index b1676d1391..14851c1148 100644
--- a/libs/server/API/IGarnetApi.cs
+++ b/libs/server/API/IGarnetApi.cs
@@ -355,6 +355,16 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi
///
GarnetStatus SortedSetAdd(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput output);
+ ///
+ /// Stores a range of sorted set elements in the specified key space.
+ ///
+ /// The distribution key for the sorted set.
+ /// The sub-key for the sorted set.
+ /// The input object containing the elements to store.
+ /// The result of the store operation.
+ /// A indicating the status of the operation.
+ GarnetStatus SortedSetRangeStore(ArgSlice dstKey, ArgSlice srcKey, ref ObjectInput input, out int result);
+
///
/// Removes the specified member from the sorted set stored at key.
///
diff --git a/libs/server/Objects/SortedSet/SortedSetObject.cs b/libs/server/Objects/SortedSet/SortedSetObject.cs
index 4042441287..ab6bf7d4e5 100644
--- a/libs/server/Objects/SortedSet/SortedSetObject.cs
+++ b/libs/server/Objects/SortedSet/SortedSetObject.cs
@@ -28,6 +28,7 @@ public enum SortedSetOperation : byte
ZRANK,
ZRANGE,
ZRANGEBYSCORE,
+ ZRANGESTORE,
GEOADD,
GEOHASH,
GEODIST,
@@ -257,6 +258,7 @@ public override unsafe bool Operate(ref ObjectInput input, ref SpanByteAndMemory
SortedSetRank(ref input, ref output);
break;
case SortedSetOperation.ZRANGE:
+ case SortedSetOperation.ZRANGESTORE:
SortedSetRange(ref input, ref output);
break;
case SortedSetOperation.ZRANGEBYSCORE:
diff --git a/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs b/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs
index 13ab47eeae..b1480f455e 100644
--- a/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs
+++ b/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs
@@ -435,6 +435,9 @@ private void SortedSetRange(ref ObjectInput input, ref SpanByteAndMemory output)
ZRangeOptions options = new();
switch (input.header.SortedSetOp)
{
+ case SortedSetOperation.ZRANGESTORE:
+ options.WithScores = true;
+ break;
case SortedSetOperation.ZRANGEBYSCORE:
options.ByScore = true;
break;
diff --git a/libs/server/Resp/Objects/SortedSetCommands.cs b/libs/server/Resp/Objects/SortedSetCommands.cs
index 09fc435351..4c7c7f60b5 100644
--- a/libs/server/Resp/Objects/SortedSetCommands.cs
+++ b/libs/server/Resp/Objects/SortedSetCommands.cs
@@ -194,6 +194,38 @@ private unsafe bool SortedSetRange(RespCommand command, ref TGarnetA
return true;
}
+ private unsafe bool SortedSetRangeStore(ref TGarnetApi storageApi)
+ where TGarnetApi : IGarnetApi
+ {
+ // ZRANGESTORE dst src min max [BYSCORE | BYLEX] [REV] [LIMIT offset count]
+ if (parseState.Count is < 4 or > 9)
+ {
+ return AbortWithWrongNumberOfArguments(nameof(RespCommand.ZRANGESTORE));
+ }
+
+ var dstKey = parseState.GetArgSliceByRef(0);
+ var srcKey = parseState.GetArgSliceByRef(1);
+
+ var header = new RespInputHeader(GarnetObjectType.SortedSet) { SortedSetOp = SortedSetOperation.ZRANGESTORE };
+ var input = new ObjectInput(header, ref parseState, startIdx: 2, arg1: respProtocolVersion);
+
+ var status = storageApi.SortedSetRangeStore(dstKey, srcKey, ref input, out int result);
+
+ switch (status)
+ {
+ case GarnetStatus.OK:
+ while (!RespWriteUtils.WriteInteger(result, 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 score of member in the sorted set at key.
/// If member does not exist in the sorted set, or key does not exist, nil is returned.
diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs
index 336c7d821c..c01ce43bae 100644
--- a/libs/server/Resp/Parser/RespCommand.cs
+++ b/libs/server/Resp/Parser/RespCommand.cs
@@ -162,6 +162,7 @@ public enum RespCommand : ushort
ZINCRBY,
ZPOPMAX,
ZPOPMIN,
+ ZRANGESTORE,
ZREM,
ZREMRANGEBYLEX,
ZREMRANGEBYRANK,
@@ -1418,6 +1419,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan
{
return RespCommand.INCRBYFLOAT;
}
+ else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read("1\r\nZRANG"u8) && *(ulong*)(ptr + 10) == MemoryMarshal.Read("ESTORE\r\n"u8))
+ {
+ return RespCommand.ZRANGESTORE;
+ }
break;
case 12:
diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs
index f025e6cb3c..edccbb070d 100644
--- a/libs/server/Resp/RespServerSession.cs
+++ b/libs/server/Resp/RespServerSession.cs
@@ -607,6 +607,7 @@ private bool ProcessArrayCommands(RespCommand cmd, ref TGarnetApi st
RespCommand.ZINCRBY => SortedSetIncrement(ref storageApi),
RespCommand.ZRANK => SortedSetRank(cmd, ref storageApi),
RespCommand.ZRANGE => SortedSetRange(cmd, ref storageApi),
+ RespCommand.ZRANGESTORE => SortedSetRangeStore(ref storageApi),
RespCommand.ZRANGEBYSCORE => SortedSetRange(cmd, ref storageApi),
RespCommand.ZREVRANK => SortedSetRank(cmd, ref storageApi),
RespCommand.ZREMRANGEBYLEX => SortedSetLengthByValue(cmd, ref storageApi),
diff --git a/libs/server/Storage/Session/ObjectStore/SortedSetOps.cs b/libs/server/Storage/Session/ObjectStore/SortedSetOps.cs
index eb517ad668..1c6df20744 100644
--- a/libs/server/Storage/Session/ObjectStore/SortedSetOps.cs
+++ b/libs/server/Storage/Session/ObjectStore/SortedSetOps.cs
@@ -692,6 +692,111 @@ public GarnetStatus SortedSetAdd(byte[] key, ref ObjectInput inp
where TObjectContext : ITsavoriteContext
=> RMWObjectStoreOperationWithOutput(key, ref input, ref objectStoreContext, ref output);
+ ///
+ /// ZRANGESTORE - Stores a range of sorted set elements into a destination key.
+ ///
+ /// The type of the object context.
+ /// The destination key where the range will be stored.
+ /// The source key from which the range will be taken.
+ /// The input object containing range parameters.
+ /// The result of the operation, indicating the number of elements stored.
+ /// The context of the object store.
+ /// Returns a GarnetStatus indicating the success or failure of the operation.
+ public unsafe GarnetStatus SortedSetRangeStore(ArgSlice dstKey, ArgSlice srcKey, ref ObjectInput input, out int result, ref TObjectContext objectStoreContext)
+ where TObjectContext : ITsavoriteContext
+ {
+ if (txnManager.ObjectStoreLockableContext.Session is null)
+ ThrowObjectStoreUninitializedException();
+
+ result = 0;
+
+ if (dstKey.Length == 0 || srcKey.Length == 0)
+ return GarnetStatus.OK;
+
+ var createTransaction = false;
+
+ if (txnManager.state != TxnState.Running)
+ {
+ Debug.Assert(txnManager.state == TxnState.None);
+ createTransaction = true;
+ txnManager.SaveKeyEntryToLock(dstKey, true, LockType.Exclusive);
+ txnManager.SaveKeyEntryToLock(srcKey, true, LockType.Shared);
+ _ = txnManager.Run(true);
+ }
+
+ // SetObject
+ var objectStoreLockableContext = txnManager.ObjectStoreLockableContext;
+
+ try
+ {
+ SpanByteAndMemory rangeOutputMem = default;
+ var rangeOutput = new GarnetObjectStoreOutput() { spanByteAndMemory = rangeOutputMem };
+ var status = SortedSetRange(srcKey.ToArray(), ref input, ref rangeOutput, ref objectStoreLockableContext);
+ rangeOutputMem = rangeOutput.spanByteAndMemory;
+
+ if (status == GarnetStatus.WRONGTYPE)
+ {
+ return GarnetStatus.WRONGTYPE;
+ }
+
+ if (status == GarnetStatus.NOTFOUND)
+ {
+ // Expire/Delete the destination key if the source key is not found
+ _ = EXPIRE(dstKey, TimeSpan.Zero, out _, StoreType.Object, ExpireOption.None, ref lockableContext, ref objectStoreLockableContext);
+ return GarnetStatus.OK;
+ }
+
+ Debug.Assert(!rangeOutputMem.IsSpanByte, "Output should not be in SpanByte format when the status is OK");
+
+ var rangeOutputHandler = rangeOutputMem.Memory.Memory.Pin();
+ try
+ {
+ var rangeOutPtr = (byte*)rangeOutputHandler.Pointer;
+ ref var currOutPtr = ref rangeOutPtr;
+ var endOutPtr = rangeOutPtr + rangeOutputMem.Length;
+
+ var destinationKey = dstKey.ToArray();
+ objectStoreLockableContext.Delete(ref destinationKey);
+
+ RespReadUtils.ReadUnsignedArrayLength(out var arrayLen, ref currOutPtr, endOutPtr);
+ Debug.Assert(arrayLen % 2 == 0, "Should always contain element and its score");
+ result = arrayLen / 2;
+
+ if (result > 0)
+ {
+ parseState.Initialize(arrayLen); // 2 elements per pair (result * 2)
+
+ for (int j = 0; j < result; j++)
+ {
+ // Read member/element into parse state
+ parseState.Read((2 * j) + 1, ref currOutPtr, endOutPtr);
+ // Read score into parse state
+ parseState.Read(2 * j, ref currOutPtr, endOutPtr);
+ }
+
+ var zAddInput = new ObjectInput(new RespInputHeader
+ {
+ type = GarnetObjectType.SortedSet,
+ SortedSetOp = SortedSetOperation.ZADD,
+ }, ref parseState);
+
+ var zAddOutput = new GarnetObjectStoreOutput { spanByteAndMemory = new SpanByteAndMemory(null) };
+ RMWObjectStoreOperationWithOutput(destinationKey, ref zAddInput, ref objectStoreLockableContext, ref zAddOutput);
+ }
+ }
+ finally
+ {
+ rangeOutputHandler.Dispose();
+ }
+ return status;
+ }
+ finally
+ {
+ if (createTransaction)
+ txnManager.Commit(true);
+ }
+ }
+
///
/// Removes the specified members from the sorted set stored at key.
/// Non existing members are ignored.
diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs
index f57a69e4d3..ddd6f5172d 100644
--- a/playground/CommandInfoUpdater/SupportedCommand.cs
+++ b/playground/CommandInfoUpdater/SupportedCommand.cs
@@ -275,6 +275,7 @@ public class SupportedCommand
new("ZRANDMEMBER", RespCommand.ZRANDMEMBER),
new("ZRANGE", RespCommand.ZRANGE),
new("ZRANGEBYSCORE", RespCommand.ZRANGEBYSCORE),
+ new("ZRANGESTORE", RespCommand.ZRANGESTORE),
new("ZRANK", RespCommand.ZRANK),
new("ZREM", RespCommand.ZREM),
new("ZREMRANGEBYLEX", RespCommand.ZREMRANGEBYLEX),
diff --git a/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs b/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs
index 215d6b86ec..b52911236b 100644
--- a/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs
+++ b/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs
@@ -1997,6 +1997,35 @@ public override ArraySegment[] SetupSingleSlotRequest()
}
}
+ internal class ZRANGESTORE : BaseCommand
+ {
+ public override bool IsArrayCommand => true;
+ public override bool ArrayResponse => false;
+ public override string Command => nameof(ZRANGESTORE);
+
+ public override string[] GetSingleSlotRequest()
+ {
+ var ssk = GetSingleSlotKeys;
+ // ZRANGESTORE dst src 0 -1
+ return [ssk[0], ssk[1], "0", "-1"];
+ }
+
+ public override string[] GetCrossSlotRequest()
+ {
+ var csk = GetCrossSlotKeys;
+ return [csk[0], csk[1], "0", "-1"];
+ }
+
+ public override ArraySegment[] SetupSingleSlotRequest()
+ {
+ var ssk = GetSingleSlotKeys;
+ var setup = new ArraySegment[2];
+ setup[0] = new(["ZADD", ssk[1], "1", "a", "2", "b", "3", "c"]);
+ setup[1] = new(["ZADD", ssk[2], "4", "d", "5", "e", "6", "f"]);
+ return setup;
+ }
+ }
+
#endregion
#region HashCommands
diff --git a/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs b/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs
index 54bf2a318d..b569a0aa8b 100644
--- a/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs
+++ b/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs
@@ -97,6 +97,7 @@ public class ClusterSlotVerificationTests
new ZCARD(),
new ZRANGE(),
new ZREVRANGEBYLEX(),
+ new ZRANGESTORE(),
new ZSCORE(),
new ZMSCORE(),
new ZPOPMAX(),
@@ -279,6 +280,7 @@ public virtual void OneTimeTearDown()
[TestCase("ZCARD")]
[TestCase("ZRANGE")]
[TestCase("ZREVRANGEBYLEX")]
+ [TestCase("ZRANGESTORE")]
[TestCase("ZSCORE")]
[TestCase("ZMSCORE")]
[TestCase("ZPOPMAX")]
@@ -421,6 +423,7 @@ void GarnetClientSessionClusterDown(BaseCommand command)
[TestCase("ZCARD")]
[TestCase("ZRANGE")]
[TestCase("ZREVRANGEBYLEX")]
+ [TestCase("ZRANGESTORE")]
[TestCase("ZSCORE")]
[TestCase("ZMSCORE")]
[TestCase("ZPOPMAX")]
@@ -573,6 +576,7 @@ void GarnetClientSessionOK(BaseCommand command)
[TestCase("ZCARD")]
[TestCase("ZRANGE")]
[TestCase("ZREVRANGEBYLEX")]
+ [TestCase("ZRANGESTORE")]
[TestCase("ZSCORE")]
[TestCase("ZMSCORE")]
[TestCase("ZPOPMAX")]
@@ -717,6 +721,7 @@ void GarnetClientSessionCrossslotTest(BaseCommand command)
[TestCase("ZCARD")]
[TestCase("ZRANGE")]
[TestCase("ZREVRANGEBYLEX")]
+ [TestCase("ZRANGESTORE")]
[TestCase("ZSCORE")]
[TestCase("ZMSCORE")]
[TestCase("ZPOPMAX")]
@@ -868,6 +873,7 @@ void GarnetClientSessionMOVEDTest(BaseCommand command)
[TestCase("ZCARD")]
[TestCase("ZRANGE")]
[TestCase("ZREVRANGEBYLEX")]
+ [TestCase("ZRANGESTORE")]
[TestCase("ZSCORE")]
[TestCase("ZMSCORE")]
[TestCase("ZPOPMAX")]
@@ -1036,6 +1042,7 @@ void GarnetClientSessionASKTest(BaseCommand command)
[TestCase("ZCARD")]
[TestCase("ZRANGE")]
[TestCase("ZREVRANGEBYLEX")]
+ [TestCase("ZRANGESTORE")]
[TestCase("ZSCORE")]
[TestCase("ZMSCORE")]
[TestCase("ZPOPMAX")]
diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs
index cf8f3a0311..a4f74634ef 100644
--- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs
+++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs
@@ -5795,6 +5795,21 @@ static async Task DoZRevRangeByLexAsync(GarnetClient client)
}
}
+ [Test]
+ public async Task ZRangeStoreACLsAsync()
+ {
+ await CheckCommandsAsync(
+ "ZRANGESTORE",
+ [DoZRangeStoreAsync]
+ );
+
+ static async Task DoZRangeStoreAsync(GarnetClient client)
+ {
+ var val = await client.ExecuteForLongResultAsync("ZRANGESTORE", ["dkey", "key", "0", "-1"]);
+ ClassicAssert.AreEqual(0, val);
+ }
+ }
+
[Test]
public async Task ZRangeByScoreACLsAsync()
{
diff --git a/test/Garnet.test/RespSortedSetTests.cs b/test/Garnet.test/RespSortedSetTests.cs
index cb70206712..90ef72febc 100644
--- a/test/Garnet.test/RespSortedSetTests.cs
+++ b/test/Garnet.test/RespSortedSetTests.cs
@@ -1126,6 +1126,130 @@ public void CanDoZRevRangeByLexWithoutLimit(string min, string max, string[] exp
ClassicAssert.AreEqual(expected, result);
}
+ [Test]
+ [TestCase("user1:obj1", "user1:objA", new[] { "Hello", "World" }, new[] { 1.0, 2.0 }, new[] { "Hello", "World" }, new[] { 1.0, 2.0 })] // Normal case
+ [TestCase("user1:emptySet", "user1:objB", new string[] { }, new double[] { }, new string[] { }, new double[] { })] // Empty set
+ [TestCase("user1:nonExistingKey", "user1:objC", new string[] { }, new double[] { }, new string[] { }, new double[] { })] // Non-existing key
+ [TestCase("user1:obj2", "user1:objD", new[] { "Alpha", "Beta", "Gamma" }, new[] { 1.0, 2.0, 3.0 }, new[] { "Beta", "Gamma" }, new[] { 2.0, 3.0 }, -2, -1)] // Negative range
+ public void CheckSortedSetRangeStoreSE(string key, string destinationKey, string[] elements, double[] scores, string[] expectedElements, double[] expectedScores, int start = 0, int stop = -1)
+ {
+ int expectedCount = expectedElements.Length;
+
+ using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig());
+ var db = redis.GetDatabase(0);
+
+ var keyValues = elements.Zip(scores, (e, s) => new SortedSetEntry(e, s)).ToArray();
+
+ // Set up sorted set if elements are provided
+ if (keyValues.Length > 0)
+ {
+ db.SortedSetAdd(key, keyValues);
+ }
+
+ var actualCount = db.SortedSetRangeAndStore(key, destinationKey, start, stop);
+ ClassicAssert.AreEqual(expectedCount, actualCount);
+
+ var actualMembers = db.SortedSetRangeByScoreWithScores(destinationKey);
+ ClassicAssert.AreEqual(expectedCount, actualMembers.Length);
+
+ for (int i = 0; i < expectedCount; i++)
+ {
+ ClassicAssert.AreEqual(expectedElements[i], actualMembers[i].Element.ToString());
+ ClassicAssert.AreEqual(expectedScores[i], actualMembers[i].Score);
+ }
+ }
+
+ [Test]
+ [TestCase("set1", "dest1", new[] { "a", "b", "c", "d" }, new[] { 1.0, 2.0, 3.0, 4.0 }, "BYSCORE", "(2", "4", "", 2, new[] { "c", "d" }, new[] { 3.0, 4.0 }, Description = "ZRANGESTORE BYSCORE with exclusive min")]
+ [TestCase("set1", "dest1", new[] { "a", "b", "c", "d" }, new[] { 1.0, 2.0, 3.0, 4.0 }, "BYSCORE", "2", "(4", "", 2, new[] { "b", "c" }, new[] { 2.0, 3.0 }, Description = "ZRANGESTORE BYSCORE with exclusive max")]
+ [TestCase("set1", "dest1", new[] { "a", "b", "c", "d" }, new[] { 1.0, 2.0, 3.0, 4.0 }, "BYSCORE REV", "4", "1", "", 4, new[] { "a", "b", "c", "d" }, new[] { 1.0, 2.0, 3.0, 4.0 }, Description = "ZRANGESTORE BYSCORE with REV")]
+ [TestCase("set1", "dest1", new[] { "a", "b", "c", "d" }, new[] { 1.0, 2.0, 3.0, 4.0 }, "BYSCORE", "2", "4", "LIMIT 1 1", 1, new[] { "c" }, new[] { 3.0 }, Description = "ZRANGESTORE BYSCORE with LIMIT")]
+ public void CheckSortedSetRangeStoreByScoreSE(string sourceKey, string destKey, string[] sourceElements, double[] sourceScores, string options, string min, string max, string limit,
+ int expectedCount, string[] expectedElements, double[] expectedScores)
+ {
+ using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig());
+ var db = redis.GetDatabase(0);
+
+ var entries = sourceElements.Zip(sourceScores, (e, s) => new SortedSetEntry(e, s)).ToArray();
+ db.SortedSetAdd(sourceKey, entries);
+
+ var command = $"{destKey} {sourceKey} {min} {max} {options} {limit}".Trim().Split(" ");
+ var result = db.Execute("ZRANGESTORE", command);
+ ClassicAssert.AreEqual(expectedCount, int.Parse(result.ToString()));
+
+ var actualMembers = db.SortedSetRangeByScoreWithScores(destKey);
+ ClassicAssert.AreEqual(expectedElements.Length, actualMembers.Length);
+
+ for (int i = 0; i < expectedElements.Length; i++)
+ {
+ ClassicAssert.AreEqual(expectedElements[i], actualMembers[i].Element.ToString());
+ ClassicAssert.AreEqual(expectedScores[i], actualMembers[i].Score);
+ }
+ }
+
+ [Test]
+ [TestCase("set1", "dest1", new[] { "a", "b", "c", "d" }, new[] { 1.0, 1.0, 1.0, 1.0 }, "BYLEX", "[b", "[d", "", 3, new[] { "b", "c", "d" }, Description = "ZRANGESTORE BYLEX with inclusive range")]
+ [TestCase("set1", "dest1", new[] { "a", "b", "c", "d" }, new[] { 1.0, 1.0, 1.0, 1.0 }, "BYLEX", "(b", "(d", "", 1, new[] { "c" }, Description = "ZRANGESTORE BYLEX with exclusive range")]
+ [TestCase("set1", "dest1", new[] { "a", "b", "c", "d" }, new[] { 1.0, 1.0, 1.0, 1.0 }, "BYLEX REV", "[d", "[b", "", 3, new[] { "b", "c", "d" }, Description = "ZRANGESTORE BYLEX with REV")]
+ [TestCase("set1", "dest1", new[] { "a", "b", "c", "d" }, new[] { 1.0, 1.0, 1.0, 1.0 }, "BYLEX", "[b", "[d", "LIMIT 1 1", 1, new[] { "c" }, Description = "ZRANGESTORE BYLEX with LIMIT")]
+ public void CheckSortedSetRangeStoreByLexSE(string sourceKey, string destKey, string[] sourceElements, double[] sourceScores, string options, string min, string max, string limit,
+ int expectedCount, string[] expectedElements)
+ {
+ using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig());
+ var db = redis.GetDatabase(0);
+
+ var entries = sourceElements.Zip(sourceScores, (e, s) => new SortedSetEntry(e, s)).ToArray();
+ db.SortedSetAdd(sourceKey, entries);
+
+ var command = $"{destKey} {sourceKey} {min} {max} {options} {limit}".Trim().Split();
+ var result = db.Execute("ZRANGESTORE", command);
+ ClassicAssert.AreEqual(expectedCount, int.Parse(result.ToString()));
+
+ var actualMembers = db.SortedSetRangeByScore(destKey);
+ ClassicAssert.AreEqual(expectedElements.Length, actualMembers.Length);
+
+ for (int i = 0; i < expectedElements.Length; i++)
+ {
+ ClassicAssert.AreEqual(expectedElements[i], actualMembers[i].ToString());
+ }
+ }
+
+ [Test]
+ public void TestCheckSortedSetRangeStoreWithExistingDestinationKeySE()
+ {
+ using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig());
+ var db = redis.GetDatabase(0);
+
+ var sourceKey = "sourceKey";
+ var destinationKey = "destKey";
+
+ // Set up source sorted set
+ var sourceElements = new[] { "a", "b", "c", "d" };
+ var sourceScores = new[] { 1.0, 2.0, 3.0, 4.0 };
+ var sourceEntries = sourceElements.Zip(sourceScores, (e, s) => new SortedSetEntry(e, s)).ToArray();
+ db.SortedSetAdd(sourceKey, sourceEntries);
+
+ // Set up existing destination sorted set
+ db.StringSet(destinationKey, "dummy");
+
+ // Expected elements after range store
+ var expectedElements = new[] { "b", "c" };
+ var expectedScores = new[] { 2.0, 3.0 };
+
+ var actualCount = db.SortedSetRangeAndStore(sourceKey, destinationKey, 1, 2);
+
+ Assert.That(actualCount, Is.EqualTo(expectedElements.Length));
+
+ var actualMembers = db.SortedSetRangeByScoreWithScores(destinationKey);
+ Assert.That(actualMembers.Length, Is.EqualTo(expectedElements.Length));
+
+ for (int i = 0; i < expectedElements.Length; i++)
+ {
+ Assert.That(actualMembers[i].Element.ToString(), Is.EqualTo(expectedElements[i]));
+ Assert.That(actualMembers[i].Score, Is.EqualTo(expectedScores[i]));
+ }
+ }
+
#endregion
#region LightClientTests
@@ -1250,6 +1374,61 @@ public void CanDoZRangeByIndexLC(int bytesSent)
ClassicAssert.AreEqual(expectedResponse, actualValue);
}
+ // ZRANGEBSTORE
+ [Test]
+ [TestCase("user1:obj1", "user1:objA", new[] { "Hello", "World" }, new[] { 1.0, 2.0 }, new[] { "Hello", "World" }, new[] { 1.0, 2.0 })] // Normal case
+ [TestCase("user1:emptySet", "user1:objB", new string[] { }, new double[] { }, new string[] { }, new double[] { })] // Empty set
+ [TestCase("user1:nonExistingKey", "user1:objC", new string[] { }, new double[] { }, new string[] { }, new double[] { })] // Non-existing key
+ [TestCase("user1:obj2", "user1:objD", new[] { "Alpha", "Beta", "Gamma" }, new[] { 1.0, 2.0, 3.0 }, new[] { "Beta", "Gamma" }, new[] { 2.0, 3.0 }, -2, -1)] // Negative range
+ public void CheckSortedSetRangeStoreLC(string key, string destinationKey, string[] elements, double[] scores, string[] expectedElements, double[] expectedScores, int start = 0, int stop = -1)
+ {
+ using var lightClientRequest = TestUtils.CreateRequest();
+
+ // Setup initial sorted set if elements exist
+ if (elements.Length > 0)
+ {
+ var addCommand = $"ZADD {key} " + string.Join(" ", elements.Zip(scores, (e, s) => $"{s} {e}"));
+ var response = lightClientRequest.SendCommand(addCommand);
+ var expectedResponse = $":{elements.Length}\r\n";
+ var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length);
+ ClassicAssert.AreEqual(expectedResponse, actualValue);
+ }
+
+ // Execute ZRANGESTORE
+ var rangeStoreCommand = $"ZRANGESTORE {destinationKey} {key} {start} {stop}";
+ var response2 = lightClientRequest.SendCommand(rangeStoreCommand);
+ var expectedResponse2 = $":{expectedElements.Length}\r\n";
+ var actualValue2 = Encoding.ASCII.GetString(response2).Substring(0, expectedResponse2.Length);
+ ClassicAssert.AreEqual(expectedResponse2, actualValue2);
+
+ // Verify stored result using ZRANGE
+ if (expectedElements.Length > 0)
+ {
+ var verifyCommand = $"ZRANGE {destinationKey} 0 -1 WITHSCORES";
+ var response3 = lightClientRequest.SendCommand(verifyCommand, expectedElements.Length * 2 + 1);
+ var expectedItems = new List();
+ expectedItems.Add($"*{expectedElements.Length * 2}");
+ for (int i = 0; i < expectedElements.Length; i++)
+ {
+ expectedItems.Add($"${expectedElements[i].Length}");
+ expectedItems.Add(expectedElements[i]);
+ expectedItems.Add($"${expectedScores[i].ToString().Length}");
+ expectedItems.Add(expectedScores[i].ToString());
+ }
+ var expectedResponse3 = string.Join("\r\n", expectedItems) + "\r\n";
+ var actualValue3 = Encoding.ASCII.GetString(response3).Substring(0, expectedResponse3.Length);
+ ClassicAssert.AreEqual(expectedResponse3, actualValue3);
+ }
+ else
+ {
+ var verifyCommand = $"ZRANGE {destinationKey} 0 -1";
+ var response3 = lightClientRequest.SendCommand(verifyCommand);
+ var expectedResponse3 = "*0\r\n";
+ var actualValue3 = Encoding.ASCII.GetString(response3).Substring(0, expectedResponse3.Length);
+ ClassicAssert.AreEqual(expectedResponse3, actualValue3);
+ }
+ }
+
[Test]
[TestCase(10)]
[TestCase(50)]
diff --git a/website/docs/commands/api-compatibility.md b/website/docs/commands/api-compatibility.md
index 697d316f67..944a325f46 100644
--- a/website/docs/commands/api-compatibility.md
+++ b/website/docs/commands/api-compatibility.md
@@ -339,7 +339,7 @@ Note that this list is subject to change as we continue to expand our API comman
| | [ZRANGE](data-structures.md#zrange) | ➕ | |
| | [ZRANGEBYLEX](data-structures.md#zrangebylex) | ➕ | (Deprecated) |
| | [ZRANGEBYSCORE](data-structures.md#zrangebyscore) | ➕ | (Deprecated) |
-| | ZRANGESTORE | ➖ | |
+| | [ZRANGESTORE](data-structures.md#zrangestore) | ➕ | |
| | [ZRANK](data-structures.md#zrank) | ➕ | |
| | [ZREM](data-structures.md#zrem) | ➕ | |
| | [ZREMRANGEBYLEX](data-structures.md#zremrangebylex) | ➕ | |
diff --git a/website/docs/commands/data-structures.md b/website/docs/commands/data-structures.md
index 0570a87605..4eeec3aaae 100644
--- a/website/docs/commands/data-structures.md
+++ b/website/docs/commands/data-structures.md
@@ -1115,6 +1115,18 @@ If member does not exist in the sorted set, or **key** does not exist, nil is re
---
+### ZRANGESTORE
+
+#### Syntax
+
+```bash
+ ZRANGESTORE dst src min max [BYSCORE|BYLEX] [REV] [LIMIT offset count]
+```
+
+Stores the specified range of elements in the sorted set stored at **src** into the sorted set stored at **dst**.
+
+---
+
## Geospatial indices
### GEOADD