diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md
index 463da7c7f..9b366cd7e 100644
--- a/docs/ReleaseNotes.md
+++ b/docs/ReleaseNotes.md
@@ -27,6 +27,7 @@
- Adds: Support for `ZMPOP` with `.SortedSetPop()`/`.SortedSetPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094))
- Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2095))
- Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105))
+- Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111))
## 2.5.61
diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs
index 9c8a6aa16..786316303 100644
--- a/src/StackExchange.Redis/Enums/RedisCommand.cs
+++ b/src/StackExchange.Redis/Enums/RedisCommand.cs
@@ -166,6 +166,7 @@ internal enum RedisCommand
SMISMEMBER,
SMOVE,
SORT,
+ SORT_RO,
SPOP,
SRANDMEMBER,
SREM,
@@ -320,6 +321,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.SETRANGE:
case RedisCommand.SINTERSTORE:
case RedisCommand.SMOVE:
+ case RedisCommand.SORT:
case RedisCommand.SPOP:
case RedisCommand.SREM:
case RedisCommand.SUNIONSTORE:
@@ -428,7 +430,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.SLOWLOG:
case RedisCommand.SMEMBERS:
case RedisCommand.SMISMEMBER:
- case RedisCommand.SORT:
+ case RedisCommand.SORT_RO:
case RedisCommand.SRANDMEMBER:
case RedisCommand.STRLEN:
case RedisCommand.SUBSCRIBE:
diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs
index 5ca31542f..2485ad27f 100644
--- a/src/StackExchange.Redis/Interfaces/IDatabase.cs
+++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs
@@ -1551,6 +1551,7 @@ public interface IDatabase : IRedis, IDatabaseAsync
/// the get parameter (note that # specifies the element itself, when used in get).
/// Referring to the redis SORT documentation for examples is recommended.
/// When used in hashes, by and get can be used to specify fields using -> notation (again, refer to redis documentation).
+ /// Uses SORT_RO when possible.
///
/// The key of the list, set, or sorted set.
/// How many entries to skip on the return.
@@ -1562,6 +1563,7 @@ public interface IDatabase : IRedis, IDatabaseAsync
/// The flags to use for this operation.
/// The sorted elements, or the external values if get is specified.
///
+ ///
RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None);
///
diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
index 724be9caa..50d129eea 100644
--- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
+++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
@@ -1516,6 +1516,7 @@ public interface IDatabaseAsync : IRedisAsync
/// the get parameter (note that # specifies the element itself, when used in get).
/// Referring to the redis SORT documentation for examples is recommended.
/// When used in hashes, by and get can be used to specify fields using -> notation (again, refer to redis documentation).
+ /// Uses SORT_RO when possible.
///
/// The key of the list, set, or sorted set.
/// How many entries to skip on the return.
diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs
index 47553653e..c3c189909 100644
--- a/src/StackExchange.Redis/RedisDatabase.cs
+++ b/src/StackExchange.Redis/RedisDatabase.cs
@@ -1775,26 +1775,26 @@ private CursorEnumerable SetScanAsync(RedisKey key, RedisValue patte
public RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None)
{
- var msg = GetSortedSetAddMessage(default(RedisKey), key, skip, take, order, sortType, by, get, flags);
- return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ var msg = GetSortMessage(RedisKey.Null, key, skip, take, order, sortType, by, get, flags, out var server);
+ return ExecuteSync(msg, ResultProcessor.RedisValueArray, server: server, defaultValue: Array.Empty());
}
public long SortAndStore(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None)
{
- var msg = GetSortedSetAddMessage(destination, key, skip, take, order, sortType, by, get, flags);
- return ExecuteSync(msg, ResultProcessor.Int64);
+ var msg = GetSortMessage(destination, key, skip, take, order, sortType, by, get, flags, out var server);
+ return ExecuteSync(msg, ResultProcessor.Int64, server);
}
public Task SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None)
{
- var msg = GetSortedSetAddMessage(destination, key, skip, take, order, sortType, by, get, flags);
- return ExecuteAsync(msg, ResultProcessor.Int64);
+ var msg = GetSortMessage(destination, key, skip, take, order, sortType, by, get, flags, out var server);
+ return ExecuteAsync(msg, ResultProcessor.Int64, server);
}
public Task SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None)
{
- var msg = GetSortedSetAddMessage(default(RedisKey), key, skip, take, order, sortType, by, get, flags);
- return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ var msg = GetSortMessage(RedisKey.Null, key, skip, take, order, sortType, by, get, flags, out var server);
+ return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty(), server: server);
}
public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags)
@@ -3513,28 +3513,25 @@ private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double s
}
}
- private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long skip, long take, Order order, SortType sortType, RedisValue by, RedisValue[]? get, CommandFlags flags)
+ private Message GetSortMessage(RedisKey destination, RedisKey key, long skip, long take, Order order, SortType sortType, RedisValue by, RedisValue[]? get, CommandFlags flags, out ServerEndPoint? server)
{
+ server = null;
+ var command = destination.IsNull && GetFeatures(key, flags, out server).ReadOnlySort
+ ? RedisCommand.SORT_RO
+ : RedisCommand.SORT;
+
// most common cases; no "get", no "by", no "destination", no "skip", no "take"
if (destination.IsNull && skip == 0 && take == -1 && by.IsNull && (get == null || get.Length == 0))
{
- switch (order)
+ return order switch
{
- case Order.Ascending:
- switch (sortType)
- {
- case SortType.Numeric: return Message.Create(Database, flags, RedisCommand.SORT, key);
- case SortType.Alphabetic: return Message.Create(Database, flags, RedisCommand.SORT, key, RedisLiterals.ALPHA);
- }
- break;
- case Order.Descending:
- switch (sortType)
- {
- case SortType.Numeric: return Message.Create(Database, flags, RedisCommand.SORT, key, RedisLiterals.DESC);
- case SortType.Alphabetic: return Message.Create(Database, flags, RedisCommand.SORT, key, RedisLiterals.DESC, RedisLiterals.ALPHA);
- }
- break;
- }
+ Order.Ascending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key),
+ Order.Ascending when sortType == SortType.Alphabetic => Message.Create(Database, flags, command, key, RedisLiterals.ALPHA),
+ Order.Descending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key, RedisLiterals.DESC),
+ Order.Descending when sortType == SortType.Alphabetic => Message.Create(Database, flags, command, key, RedisLiterals.DESC, RedisLiterals.ALPHA),
+ Order.Ascending or Order.Descending => throw new ArgumentOutOfRangeException(nameof(sortType)),
+ _ => throw new ArgumentOutOfRangeException(nameof(order)),
+ };
}
// and now: more complicated scenarios...
@@ -3578,7 +3575,7 @@ private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long
values.Add(item);
}
}
- if (destination.IsNull) return Message.Create(Database, flags, RedisCommand.SORT, key, values.ToArray());
+ if (destination.IsNull) return Message.Create(Database, flags, command, key, values.ToArray());
// Because we are using STORE, we need to push this to a primary
if (Message.GetPrimaryReplicaFlags(flags) == CommandFlags.DemandReplica)
diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs
index a2dcee19e..1a5bd9b98 100644
--- a/src/StackExchange.Redis/RedisFeatures.cs
+++ b/src/StackExchange.Redis/RedisFeatures.cs
@@ -132,6 +132,11 @@ public RedisFeatures(Version version)
///
public bool PushIfNotExists => Version >= v2_1_1;
+ ///
+ /// Does this support SORT_RO?
+ ///
+ internal bool ReadOnlySort => Version >= v7_0_0_rc1;
+
///
/// Is SCAN (cursor-based scanning) available?
///
diff --git a/tests/StackExchange.Redis.Tests/Sets.cs b/tests/StackExchange.Redis.Tests/Sets.cs
index d32e12573..436691eff 100644
--- a/tests/StackExchange.Redis.Tests/Sets.cs
+++ b/tests/StackExchange.Redis.Tests/Sets.cs
@@ -343,4 +343,50 @@ public void SetPopMulti_Nil()
var arr = db.SetPop(key, 1);
Assert.Empty(arr);
}
+
+ [Fact]
+ public async Task TestSortReadonlyPrimary()
+ {
+ using var conn = Create();
+
+ var db = conn.GetDatabase();
+ var key = Me();
+ await db.KeyDeleteAsync(key);
+
+ var random = new Random();
+ var items = Enumerable.Repeat(0, 200).Select(_ => random.Next()).ToList();
+ await db.SetAddAsync(key, items.Select(x=>(RedisValue)x).ToArray());
+ items.Sort();
+
+ var result = db.Sort(key).Select(x=>(int)x);
+ Assert.Equal(items, result);
+
+ result = (await db.SortAsync(key)).Select(x => (int)x);
+ Assert.Equal(items, result);
+ }
+
+ [Fact]
+ public async Task TestSortReadonlyReplica()
+ {
+ using var conn = Create(require: RedisFeatures.v7_0_0_rc1);
+
+ var db = conn.GetDatabase();
+ var key = Me();
+ await db.KeyDeleteAsync(key);
+
+ var random = new Random();
+ var items = Enumerable.Repeat(0, 200).Select(_ => random.Next()).ToList();
+ await db.SetAddAsync(key, items.Select(x=>(RedisValue)x).ToArray());
+
+ using var readonlyConn = Create(configuration: TestConfig.Current.ReplicaServerAndPort, require: RedisFeatures.v7_0_0_rc1);
+ var readonlyDb = conn.GetDatabase();
+
+ items.Sort();
+
+ var result = readonlyDb.Sort(key).Select(x => (int)x);
+ Assert.Equal(items, result);
+
+ result = (await readonlyDb.SortAsync(key)).Select(x => (int)x);
+ Assert.Equal(items, result);
+ }
}