diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b3bc9845e..b53a3beb9 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ - Adds: Support for `COPY` with `.KeyCopy()`/`.KeyCopyAsync()` ([#2064 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2064)) - Adds: Support for `LMOVE` with `.ListMove()`/`.ListMoveAsync()` ([#2065 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2065)) - Adds: Support for `SMISMEMBER` with `.SetContains()`/`.SetContainsAsync()` ([#2077 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2077)) +- Adds: Support for `ZDIFF`, `ZDIFFSTORE`, `ZINTER`, `ZINTERCARD`, and `ZUNION` with `.SortedSetCombine()`/`.SortedSetCombineAsync()`, `.SortedSetCombineWithScores()`/`.SortedSetCombineWithScoresAsync()`, and `.SortedSetIntersectionLength()`/`.SortedSetIntersectionLengthAsync()` ([#2075 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2075)) - Adds: Support for `SINTERCARD` with `.SetIntersectionLength()`/`.SetIntersectionLengthAsync()` ([#2078 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2078)) ## 2.5.61 diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index b4d3c5fd7..27e1e32ff 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -196,7 +196,11 @@ internal enum RedisCommand ZADD, ZCARD, ZCOUNT, + ZDIFF, + ZDIFFSTORE, ZINCRBY, + ZINTER, + ZINTERCARD, ZINTERSTORE, ZLEXCOUNT, ZPOPMAX, @@ -216,6 +220,7 @@ internal enum RedisCommand ZREVRANK, ZSCAN, ZSCORE, + ZUNION, ZUNIONSTORE, UNKNOWN, diff --git a/src/StackExchange.Redis/Enums/SetOperation.cs b/src/StackExchange.Redis/Enums/SetOperation.cs index b88b0c8f0..f7cf4f1df 100644 --- a/src/StackExchange.Redis/Enums/SetOperation.cs +++ b/src/StackExchange.Redis/Enums/SetOperation.cs @@ -1,4 +1,6 @@ -namespace StackExchange.Redis +using System; + +namespace StackExchange.Redis { /// /// Describes an algebraic set operation that can be performed to combine multiple sets. @@ -18,4 +20,18 @@ public enum SetOperation /// Difference, } + + internal static class SetOperationExtensions + { + public static RedisCommand ToCommand(this SetOperation operation, bool store) => operation switch + { + SetOperation.Intersect when store => RedisCommand.ZINTERSTORE, + SetOperation.Intersect => RedisCommand.ZINTER, + SetOperation.Union when store => RedisCommand.ZUNIONSTORE, + SetOperation.Union => RedisCommand.ZUNION, + SetOperation.Difference when store => RedisCommand.ZDIFFSTORE, + SetOperation.Difference => RedisCommand.ZDIFF, + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + } } diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index c6887608f..02d530bbe 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1355,9 +1355,42 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/zadd long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); + /// + /// Computes a set operation for multiple sorted sets (optionally using per-set ), + /// optionally performing a specific aggregation (defaults to ). + /// cannot be used with weights or aggregation. + /// + /// The operation to perform. + /// The keys of the sorted sets. + /// The optional weights per set that correspond to . + /// The aggregation method (defaults to ). + /// The flags to use for this operation. + /// https://redis.io/commands/zunion + /// https://redis.io/commands/zinter + /// https://redis.io/commands/zdiff + /// The resulting sorted set. + RedisValue[] SortedSetCombine(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + + /// + /// Computes a set operation for multiple sorted sets (optionally using per-set ), + /// optionally performing a specific aggregation (defaults to ). + /// cannot be used with weights or aggregation. + /// + /// The operation to perform. + /// The keys of the sorted sets. + /// The optional weights per set that correspond to . + /// The aggregation method (defaults to ). + /// The flags to use for this operation. + /// https://redis.io/commands/zunion + /// https://redis.io/commands/zinter + /// https://redis.io/commands/zdiff + /// The resulting sorted set with scores. + SortedSetEntry[] SortedSetCombineWithScores(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + /// /// Computes a set operation over two sorted sets, and stores the result in destination, optionally performing /// a specific aggregation (defaults to sum). + /// cannot be used with aggregation. /// /// The operation to perform. /// The key to store the results in. @@ -1367,12 +1400,14 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// https://redis.io/commands/zunionstore /// https://redis.io/commands/zinterstore + /// https://redis.io/commands/zdiffstore /// The number of elements in the resulting sorted set at destination. long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// /// Computes a set operation over multiple sorted sets (optionally using per-set weights), and stores the result in destination, optionally performing /// a specific aggregation (defaults to sum). + /// cannot be used with aggregation. /// /// The operation to perform. /// The key to store the results in. @@ -1382,6 +1417,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// https://redis.io/commands/zunionstore /// https://redis.io/commands/zinterstore + /// https://redis.io/commands/zdiffstore /// The number of elements in the resulting sorted set at destination. long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); @@ -1408,6 +1444,16 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/zincrby double SortedSetIncrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None); + /// + /// Returns the cardinality of the intersection of the sorted sets at . + /// + /// The keys of the sorted sets. + /// If the intersection cardinality reaches partway through the computation, the algorithm will exit and yield as the cardinality (defaults to 0 meaning unlimited). + /// The flags to use for this operation. + /// The number of elements in the resulting intersection. + /// https://redis.io/commands/zintercard + long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None); + /// /// Returns the sorted set cardinality (number of elements) of the sorted set stored at key. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 45074dd6d..e5febbcbd 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1319,9 +1319,42 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/zadd Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); + /// + /// Computes a set operation for multiple sorted sets (optionally using per-set ), + /// optionally performing a specific aggregation (defaults to ). + /// cannot be used with weights or aggregation. + /// + /// The operation to perform. + /// The keys of the sorted sets. + /// The optional weights per set that correspond to . + /// The aggregation method (defaults to ). + /// The flags to use for this operation. + /// https://redis.io/commands/zunion + /// https://redis.io/commands/zinter + /// https://redis.io/commands/zdiff + /// The resulting sorted set. + Task SortedSetCombineAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + + /// + /// Computes a set operation for multiple sorted sets (optionally using per-set ), + /// optionally performing a specific aggregation (defaults to ). + /// cannot be used with weights or aggregation. + /// + /// The operation to perform. + /// The keys of the sorted sets. + /// The optional weights per set that correspond to . + /// The aggregation method (defaults to ). + /// The flags to use for this operation. + /// https://redis.io/commands/zunion + /// https://redis.io/commands/zinter + /// https://redis.io/commands/zdiff + /// The resulting sorted set with scores. + Task SortedSetCombineWithScoresAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + /// /// Computes a set operation over two sorted sets, and stores the result in destination, optionally performing /// a specific aggregation (defaults to sum). + /// cannot be used with aggregation. /// /// The operation to perform. /// The key to store the results in. @@ -1331,12 +1364,14 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// https://redis.io/commands/zunionstore /// https://redis.io/commands/zinterstore + /// https://redis.io/commands/zdiffstore /// The number of elements in the resulting sorted set at destination. Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// /// Computes a set operation over multiple sorted sets (optionally using per-set weights), and stores the result in destination, optionally performing /// a specific aggregation (defaults to sum). + /// cannot be used with aggregation. /// /// The operation to perform. /// The key to store the results in. @@ -1346,6 +1381,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// https://redis.io/commands/zunionstore /// https://redis.io/commands/zinterstore + /// https://redis.io/commands/zdiffstore /// The number of elements in the resulting sorted set at destination. Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); @@ -1372,6 +1408,16 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/zincrby Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None); + /// + /// Returns the cardinality of the intersection of the sorted sets at . + /// + /// The keys of the sorted sets. + /// If the intersection cardinality reaches partway through the computation, the algorithm will exit and yield as the cardinality (defaults to 0 meaning unlimited). + /// The flags to use for this operation. + /// The number of elements in the resulting intersection. + /// https://redis.io/commands/zintercard + Task SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None); + /// /// Returns the sorted set cardinality (number of elements) of the sorted set stored at key. /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 9563d4d13..c2e79b1d0 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -345,6 +345,12 @@ public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandF public bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetAdd(ToInner(key), member, score, when, flags); + public RedisValue[] SortedSetCombine(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetCombine(operation, keys, weights, aggregate, flags); + + public SortedSetEntry[] SortedSetCombineWithScores(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetCombineWithScores(operation, keys, weights, aggregate, flags); + public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => Inner.SortedSetCombineAndStore(operation, ToInner(destination), ToInner(keys), weights, aggregate, flags); @@ -357,6 +363,9 @@ public double SortedSetDecrement(RedisKey key, RedisValue member, double value, public double SortedSetIncrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => Inner.SortedSetIncrement(ToInner(key), member, value, flags); + public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetIntersectionLength(keys, limit, flags); + public long SortedSetLength(RedisKey key, double min = -1.0 / 0.0, double max = 1.0 / 0.0, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => Inner.SortedSetLength(ToInner(key), min, max, exclude, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 87f40b608..a8fa91649 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -358,6 +358,12 @@ public Task SortedSetAddAsync(RedisKey key, RedisValue member, double scor public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetAddAsync(ToInner(key), member, score, when, flags); + public Task SortedSetCombineAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetCombineAsync(operation, keys, weights, aggregate, flags); + + public Task SortedSetCombineWithScoresAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetCombineWithScoresAsync(operation, keys, weights, aggregate, flags); + public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => Inner.SortedSetCombineAndStoreAsync(operation, ToInner(destination), ToInner(keys), weights, aggregate, flags); @@ -370,6 +376,9 @@ public Task SortedSetDecrementAsync(RedisKey key, RedisValue member, dou public Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => Inner.SortedSetIncrementAsync(ToInner(key), member, value, flags); + public Task SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetIntersectionLengthAsync(keys, limit, flags); + public Task SortedSetLengthAsync(RedisKey key, double min = -1.0 / 0.0, double max = 1.0 / 0.0, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => Inner.SortedSetLengthAsync(ToInner(key), min, max, exclude, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 4f07e98e2..ade9fdd5f 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -592,10 +592,13 @@ StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, Sta StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.CommandFlags flags) -> long StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetCombine(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.IDatabase.SortedSetCombineWithScores(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! StackExchange.Redis.IDatabase.SortedSetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetDecrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double StackExchange.Redis.IDatabase.SortedSetIncrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double +StackExchange.Redis.IDatabase.SortedSetIntersectionLength(StackExchange.Redis.RedisKey[]! keys, long limit = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetLength(StackExchange.Redis.RedisKey key, double min = -Infinity, double max = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetLengthByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! @@ -783,10 +786,13 @@ StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKe StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetCombineWithScoresAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetDecrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetIncrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetIntersectionLengthAsync(StackExchange.Redis.RedisKey[]! keys, long limit = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetLengthAsync(StackExchange.Redis.RedisKey key, double min = -Infinity, double max = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetLengthByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 54181f982..269211ee1 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1616,6 +1616,30 @@ public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When return ExecuteAsync(msg, ResultProcessor.Int64); } + public RedisValue[] SortedSetCombine(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetCombineCommandMessage(operation, keys, weights, aggregate, withScores: false, flags); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public Task SortedSetCombineAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetCombineCommandMessage(operation, keys, weights, aggregate, withScores: false, flags); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public SortedSetEntry[] SortedSetCombineWithScores(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetCombineCommandMessage(operation, keys, weights, aggregate, withScores: true, flags); + return ExecuteSync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); + } + + public Task SortedSetCombineWithScoresAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetCombineCommandMessage(operation, keys, weights, aggregate, withScores: true, flags); + return ExecuteAsync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); + } + public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) { var msg = GetSortedSetCombineAndStoreCommandMessage(operation, destination, new[] { first, second }, null, aggregate, flags); @@ -1662,6 +1686,18 @@ public Task SortedSetIncrementAsync(RedisKey key, RedisValue member, dou return ExecuteAsync(msg, ResultProcessor.Double); } + public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetIntersectionLengthMessage(keys, limit, flags); + return ExecuteSync(msg, ResultProcessor.Int64); + } + + public Task SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetIntersectionLengthMessage(keys, limit, flags); + return ExecuteAsync(msg, ResultProcessor.Int64); + } + public long SortedSetLength(RedisKey key, double min = double.NegativeInfinity, double max = double.PositiveInfinity, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) { var msg = GetSortedSetLengthMessage(key, min, max, exclude, flags); @@ -3168,36 +3204,91 @@ private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long private Message GetSortedSetCombineAndStoreCommandMessage(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights, Aggregate aggregate, CommandFlags flags) { - var command = operation switch + var command = operation.ToCommand(store: true); + if (keys == null) { - SetOperation.Intersect => RedisCommand.ZINTERSTORE, - SetOperation.Union => RedisCommand.ZUNIONSTORE, - _ => throw new ArgumentOutOfRangeException(nameof(operation)), - }; - if (keys == null) throw new ArgumentNullException(nameof(keys)); + throw new ArgumentNullException(nameof(keys)); + } + if (command == RedisCommand.ZDIFFSTORE && (weights != null || aggregate != Aggregate.Sum)) + { + throw new ArgumentException("ZDIFFSTORE cannot be used with weights or aggregation."); + } + if (weights != null && keys.Length != weights.Length) + { + throw new ArgumentException("Keys and weights should have the same number of elements.", nameof(weights)); + } - List? values = null; - if (weights != null && weights.Length != 0) + RedisValue[] values = RedisValue.EmptyArray; + + var argsLength = (weights?.Length > 0 ? 1 + weights.Length : 0) + (aggregate != Aggregate.Sum ? 2 : 0); + if (argsLength > 0) { - (values ??= new List()).Add(RedisLiterals.WEIGHTS); + values = new RedisValue[argsLength]; + AddWeightsAggregationAndScore(values, weights, aggregate); + } + return new SortedSetCombineAndStoreCommandMessage(Database, flags, command, destination, keys, values); + } + + private Message GetSortedSetCombineCommandMessage(SetOperation operation, RedisKey[] keys, double[]? weights, Aggregate aggregate, bool withScores, CommandFlags flags) + { + var command = operation.ToCommand(store: false); + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + if (command == RedisCommand.ZDIFF && (weights != null || aggregate != Aggregate.Sum)) + { + throw new ArgumentException("ZDIFF cannot be used with weights or aggregation."); + } + if (weights != null && keys.Length != weights.Length) + { + throw new ArgumentException("Keys and weights should have the same number of elements.", nameof(weights)); + } + + var i = 0; + var values = new RedisValue[1 + keys.Length + + (weights?.Length > 0 ? 1 + weights.Length : 0) + + (aggregate != Aggregate.Sum ? 2 : 0) + + (withScores ? 1 : 0)]; + values[i++] = keys.Length; + foreach (var key in keys) + { + values[i++] = key.AsRedisValue(); + } + AddWeightsAggregationAndScore(values.AsSpan(i), weights, aggregate, withScores: withScores); + return Message.Create(Database, flags, command, values ?? RedisValue.EmptyArray); + } + + private void AddWeightsAggregationAndScore(Span values, double[]? weights, Aggregate aggregate, bool withScores = false) + { + int i = 0; + if (weights?.Length > 0) + { + values[i++] = RedisLiterals.WEIGHTS; foreach (var weight in weights) - values.Add(weight); + { + values[i++] = weight; + } } switch (aggregate) { - case Aggregate.Sum: break; // default + case Aggregate.Sum: + break; // add nothing - Redis default case Aggregate.Min: - (values ??= new List()).Add(RedisLiterals.AGGREGATE); - values.Add(RedisLiterals.MIN); + values[i++] = RedisLiterals.AGGREGATE; + values[i++] = RedisLiterals.MIN; break; case Aggregate.Max: - (values ??= new List()).Add(RedisLiterals.AGGREGATE); - values.Add(RedisLiterals.MAX); + values[i++] = RedisLiterals.AGGREGATE; + values[i++] = RedisLiterals.MAX; break; default: throw new ArgumentOutOfRangeException(nameof(aggregate)); } - return new SortedSetCombineAndStoreCommandMessage(Database, flags, command, destination, keys, values?.ToArray() ?? RedisValue.EmptyArray); + if (withScores) + { + values[i++] = RedisLiterals.WITHSCORES; + } } private Message GetSortedSetLengthMessage(RedisKey key, double min, double max, Exclude exclude, CommandFlags flags) @@ -3210,6 +3301,25 @@ private Message GetSortedSetLengthMessage(RedisKey key, double min, double max, return Message.Create(Database, flags, RedisCommand.ZCOUNT, key, from, to); } + private Message GetSortedSetIntersectionLengthMessage(RedisKey[] keys, long limit, CommandFlags flags) + { + if (keys == null) throw new ArgumentNullException(nameof(keys)); + + var i = 0; + var values = new RedisValue[1 + keys.Length + (limit > 0 ? 2 : 0)]; + values[i++] = keys.Length; + foreach (var key in keys) + { + values[i++] = key.AsRedisValue(); + } + if (limit > 0) + { + values[i++] = RedisLiterals.LIMIT; + values[i++] = limit; + } + return Message.Create(Database, flags, RedisCommand.ZINTERCARD, values); + } + private Message GetSortedSetRangeByScoreMessage(RedisKey key, double start, double stop, Exclude exclude, Order order, long skip, long take, CommandFlags flags, bool withScores) { // usage: {ZRANGEBYSCORE|ZREVRANGEBYSCORE} key from to [WITHSCORES] [LIMIT offset count] diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 78db3c07a..f8ca3f325 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -741,6 +741,22 @@ public void SortedSetAdd_2() mock.Verify(_ => _.SortedSetAdd("prefix:key", values, When.Exists, CommandFlags.None)); } + [Fact] + public void SortedSetCombine() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetCombine(SetOperation.Intersect, keys); + mock.Verify(_ => _.SortedSetCombine(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); + } + + [Fact] + public void SortedSetCombineWithScores() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetCombineWithScores(SetOperation.Intersect, keys); + mock.Verify(_ => _.SortedSetCombineWithScores(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); + } + [Fact] public void SortedSetCombineAndStore_1() { @@ -771,6 +787,14 @@ public void SortedSetIncrement() mock.Verify(_ => _.SortedSetIncrement("prefix:key", "member", 1.23, CommandFlags.None)); } + [Fact] + public void SortedSetIntersectionLength() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetIntersectionLength(keys, 1, CommandFlags.None); + mock.Verify(_ => _.SortedSetIntersectionLength(keys, 1, CommandFlags.None)); + } + [Fact] public void SortedSetLength() { diff --git a/tests/StackExchange.Redis.Tests/SortedSets.cs b/tests/StackExchange.Redis.Tests/SortedSets.cs index e7cdcc59c..fec0d82ac 100644 --- a/tests/StackExchange.Redis.Tests/SortedSets.cs +++ b/tests/StackExchange.Redis.Tests/SortedSets.cs @@ -38,6 +38,15 @@ public SortedSets(ITestOutputHelper output, SharedConnectionFixture fixture) : b new SortedSetEntry("j", 512) }; + private static readonly SortedSetEntry[] entriesPow3 = new SortedSetEntry[] + { + new SortedSetEntry("a", 1), + new SortedSetEntry("c", 4), + new SortedSetEntry("e", 16), + new SortedSetEntry("g", 64), + new SortedSetEntry("i", 256), + }; + private static readonly SortedSetEntry[] lexEntries = new SortedSetEntry[] { new SortedSetEntry("a", 0), @@ -52,6 +61,284 @@ public SortedSets(ITestOutputHelper output, SharedConnectionFixture fixture) : b new SortedSetEntry("j", 0) }; + [Fact] + public void SortedSetCombine() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var diff = db.SortedSetCombine(SetOperation.Difference, new RedisKey[]{ key1, key2}); + Assert.Equal(5, diff.Length); + Assert.Equal("b", diff[0]); + + var inter = db.SortedSetCombine(SetOperation.Intersect, new RedisKey[]{ key1, key2}); + Assert.Equal(5, inter.Length); + Assert.Equal("a", inter[0]); + + var union = db.SortedSetCombine(SetOperation.Union, new RedisKey[]{ key1, key2}); + Assert.Equal(10, union.Length); + Assert.Equal("a", union[0]); + } + + + [Fact] + public async Task SortedSetCombineAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var diff = await db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }); + Assert.Equal(5, diff.Length); + Assert.Equal("b", diff[0]); + + var inter = await db.SortedSetCombineAsync(SetOperation.Intersect, new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter.Length); + Assert.Equal("a", inter[0]); + + var union = await db.SortedSetCombineAsync(SetOperation.Union, new RedisKey[] { key1, key2 }); + Assert.Equal(10, union.Length); + Assert.Equal("a", union[0]); + } + + [Fact] + public void SortedSetCombineWithScores() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var diff = db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[]{ key1, key2}); + Assert.Equal(5, diff.Length); + Assert.Equal(new SortedSetEntry("b", 2), diff[0]); + + var inter = db.SortedSetCombineWithScores(SetOperation.Intersect, new RedisKey[]{ key1, key2}); + Assert.Equal(5, inter.Length); + Assert.Equal(new SortedSetEntry("a", 2), inter[0]); + + var union = db.SortedSetCombineWithScores(SetOperation.Union, new RedisKey[]{ key1, key2}); + Assert.Equal(10, union.Length); + Assert.Equal(new SortedSetEntry("a", 2), union[0]); + } + + + [Fact] + public async Task SortedSetCombineWithScoresAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var diff = await db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }); + Assert.Equal(5, diff.Length); + Assert.Equal(new SortedSetEntry("b", 2), diff[0]); + + var inter = await db.SortedSetCombineWithScoresAsync(SetOperation.Intersect, new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter.Length); + Assert.Equal(new SortedSetEntry("a", 2), inter[0]); + + var union = await db.SortedSetCombineWithScoresAsync(SetOperation.Union, new RedisKey[] { key1, key2 }); + Assert.Equal(10, union.Length); + Assert.Equal(new SortedSetEntry("a", 2), union[0]); + } + + [Fact] + public void SortedSetCombineAndStore() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + var destination = Me() + "dest"; + db.KeyDelete(destination, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var diff = db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[]{ key1, key2}); + Assert.Equal(5, diff); + + var inter = db.SortedSetCombineAndStore(SetOperation.Intersect, destination, new RedisKey[]{ key1, key2}); + Assert.Equal(5, inter); + + var union = db.SortedSetCombineAndStore(SetOperation.Union, destination, new RedisKey[]{ key1, key2}); + Assert.Equal(10, union); + } + + + [Fact] + public async Task SortedSetCombineAndStoreAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + var destination = Me() + "dest"; + db.KeyDelete(destination, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var diff = await db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }); + Assert.Equal(5, diff); + + var inter = await db.SortedSetCombineAndStoreAsync(SetOperation.Intersect, destination, new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter); + + var union = await db.SortedSetCombineAndStoreAsync(SetOperation.Union, destination, new RedisKey[] { key1, key2 }); + Assert.Equal(10, union); + } + + [Fact] + public async Task SortedSetCombineErrors() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + var destination = Me() + "dest"; + db.KeyDelete(destination, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + // ZDIFF can't be used with weights + var ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); + // and Async... + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); + + // ZDIFF can't be used with aggregation + ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); + // and Async... + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); + + // Too many weights + ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Union, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + // and Async... + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Union, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + } + + [Fact] + public void SortedSetIntersectionLength() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var inter = db.SortedSetIntersectionLength(new RedisKey[]{ key1, key2}); + Assert.Equal(5, inter); + + // with limit + inter = db.SortedSetIntersectionLength(new RedisKey[]{ key1, key2}, 3); + Assert.Equal(3, inter); + } + + [Fact] + public async Task SortedSetIntersectionLengthAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var inter = await db.SortedSetIntersectionLengthAsync(new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter); + + // with limit + inter = await db.SortedSetIntersectionLengthAsync(new RedisKey[] { key1, key2 }, 3); + Assert.Equal(3, inter); + } + [Fact] public void SortedSetPopMulti_Multi() { diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index 322f71a3f..c8d83b309 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -687,6 +687,22 @@ public void SortedSetAddAsync_2() mock.Verify(_ => _.SortedSetAddAsync("prefix:key", values, When.Exists, CommandFlags.None)); } + [Fact] + public void SortedSetCombineAsync() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetCombineAsync(SetOperation.Intersect, keys); + mock.Verify(_ => _.SortedSetCombineAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); + } + + [Fact] + public void SortedSetCombineWithScoresAsync() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys); + mock.Verify(_ => _.SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); + } + [Fact] public void SortedSetCombineAndStoreAsync_1() { @@ -717,6 +733,14 @@ public void SortedSetIncrementAsync() mock.Verify(_ => _.SortedSetIncrementAsync("prefix:key", "member", 1.23, CommandFlags.None)); } + [Fact] + public void SortedSetIntersectionLengthAsync() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None); + mock.Verify(_ => _.SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None)); + } + [Fact] public void SortedSetLengthAsync() {