diff --git a/sources/Valkey.Glide/BaseClient.SortedSetCommands.cs b/sources/Valkey.Glide/BaseClient.SortedSetCommands.cs index 37b3dad..31fdfcb 100644 --- a/sources/Valkey.Glide/BaseClient.SortedSetCommands.cs +++ b/sources/Valkey.Glide/BaseClient.SortedSetCommands.cs @@ -108,4 +108,77 @@ public async Task SortedSetRangeByValueAsync( Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); return await Command(Request.SortedSetRangeByValueAsync(key, min, max, exclude, order, skip, take)); } + + public async Task SortedSetCombineAsync(SetOperation operation, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.SortedSetCombineAsync(operation, keys, weights, aggregate)); + } + + public async Task SortedSetCombineWithScoresAsync(SetOperation operation, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.SortedSetCombineWithScoresAsync(operation, keys, weights, aggregate)); + } + + public async Task SortedSetCombineAndStoreAsync(SetOperation operation, ValkeyKey destination, ValkeyKey first, ValkeyKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.SortedSetCombineAndStoreAsync(operation, destination, first, second, aggregate)); + } + + public async Task SortedSetCombineAndStoreAsync(SetOperation operation, ValkeyKey destination, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.SortedSetCombineAndStoreAsync(operation, destination, keys, weights, aggregate)); + } + + public async Task SortedSetIncrementAsync(ValkeyKey key, ValkeyValue member, double value, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.SortedSetIncrementAsync(key, member, value)); + } + + public async Task SortedSetIntersectionLengthAsync(ValkeyKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.SortedSetIntersectionLengthAsync(keys, limit)); + } + + public async Task SortedSetLengthByValueAsync(ValkeyKey key, ValkeyValue min, ValkeyValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.SortedSetLengthByValueAsync(key, min, max, exclude)); + } + + public async Task SortedSetPopAsync(ValkeyKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.SortedSetPopAsync(keys, count, order)); + } + + public async Task SortedSetScoresAsync(ValkeyKey key, ValkeyValue[] members, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.SortedSetScoresAsync(key, members)); + } + + public async Task SortedSetBlockingPopAsync(ValkeyKey key, Order order, double timeout, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.SortedSetBlockingPopAsync(key, order, timeout)); + } + + public async Task SortedSetBlockingPopAsync(ValkeyKey key, long count, Order order, double timeout, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + Utils.Requires(count == 1, "GLIDE does not currently support multipop BZPOPMIN or BZPOPMAX"); // TODO for the future + return await Command(Request.SortedSetBlockingPopAsync(key, count, order, timeout)); + } + + public async Task SortedSetBlockingPopAsync(ValkeyKey[] keys, long count, Order order, double timeout, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.SortedSetBlockingPopAsync(keys, count, order, timeout)); + } } diff --git a/sources/Valkey.Glide/Commands/Constants/Constants.cs b/sources/Valkey.Glide/Commands/Constants/Constants.cs index a0b187a..04a2bbb 100644 --- a/sources/Valkey.Glide/Commands/Constants/Constants.cs +++ b/sources/Valkey.Glide/Commands/Constants/Constants.cs @@ -41,6 +41,22 @@ public static class Constants public const string MinMatchLenKeyword = "MINMATCHLEN"; public const string WithMatchLenKeyword = "WITHMATCHLEN"; + /// + /// Keywords for sorted set conditional operations. + /// + public const string ExistsKeyword = "XX"; + public const string NotExistsKeyword = "NX"; + public const string GreaterThanKeyword = "GT"; + public const string LessThanKeyword = "LT"; + + /// + /// Keywords for sorted set operations. + /// + public const string WeightsKeyword = "WEIGHTS"; + public const string AggregateKeyword = "AGGREGATE"; + public const string MinKeyword = "MIN"; + public const string MaxKeyword = "MAX"; + /// /// The highest bound in the sorted set for lexicographical operations. /// @@ -60,5 +76,4 @@ public static class Constants /// The lowest bound in the sorted set for score operations. /// public const string NegativeInfinityScore = "-inf"; - } diff --git a/sources/Valkey.Glide/Commands/ISortedSetCommands.cs b/sources/Valkey.Glide/Commands/ISortedSetCommands.cs index 814ee2b..9e8c95c 100644 --- a/sources/Valkey.Glide/Commands/ISortedSetCommands.cs +++ b/sources/Valkey.Glide/Commands/ISortedSetCommands.cs @@ -362,4 +362,308 @@ Task SortedSetRangeByValueAsync( long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None); + + /// TODO: Put ref here once it is implemented in Sorted Set Commands Batch 2 + /// + /// Blocks the connection until it pops and returns a member-score pair from the sorted set stored at key. Can either pop the max or min element from the set. + /// This is the blocking variant of . + /// + /// + /// + /// This is a client blocking command. See for more details and best practices. + /// The key of the sorted set. + /// The order to sort by when popping items out of the set. + /// The timeout in seconds. A timeout of zero can be used to block indefinitely. + /// The flags to use for the operation. Currently flags are ignored. + /// A sorted set entry, or if no element could be popped and the timeout expired. + /// + /// + /// + /// SortedSetEntry? result = await client.SortedSetBlockingPopAsync(key, Order.Ascending, 5.0); + /// + /// + /// + Task SortedSetBlockingPopAsync(ValkeyKey key, Order order, double timeout, CommandFlags flags = CommandFlags.None); + + /// TODO: Put ref here once it is implemented in Sorted Set Commands Batch 2 + /// + /// Blocks the connection until it pops and returns the specified number of elements from the sorted set stored at key. Can either pop the max or min element from the set. + /// This is the blocking variant of . + /// + /// + /// + /// This is a client blocking command. See for more details and best practices. + /// The key of the sorted set. + /// The number of elements to return. + /// The order to sort by when popping items out of the set. + /// The timeout in seconds. A timeout of zero can be used to block indefinitely. + /// The flags to use for the operation. Currently flags are ignored. + /// An array of elements, or an empty array when key does not exist or timeout expired. + /// + /// + /// + /// SortedSetEntry[] result = await client.SortedSetBlockingPopAsync(key, 2, Order.Ascending, 5.0); + /// + /// + /// + Task SortedSetBlockingPopAsync(ValkeyKey key, long count, Order order, double timeout, CommandFlags flags = CommandFlags.None); + + /// + /// Blocks the connection until it pops and returns up to entries from the first non-empty sorted set. + /// The given keys are checked in the order they are provided. + /// This is the blocking variant of . + /// + /// + /// When in cluster mode, all keys must map to the same hash slot. + /// This is a client blocking command. See for more details and best practices. + /// Since Valkey 7.0 and above. + /// The keys of the sorted sets. + /// The maximum number of records to pop out of the sorted set. + /// The order to sort by when popping items out of the set. + /// The timeout in seconds. A timeout of zero can be used to block indefinitely. + /// The flags to use for the operation. Currently flags are ignored. + /// A contiguous collection of sorted set entries with the key they were popped from, or if no non-empty sorted sets are found or timeout expired. + /// + /// + /// + /// SortedSetPopResult result = await client.SortedSetBlockingPopAsync(new[] { key1, key2, key3 }, 2, Order.Ascending, 5.0); + /// + /// + /// + Task SortedSetBlockingPopAsync(ValkeyKey[] keys, long count, Order order, double timeout, CommandFlags flags = CommandFlags.None); + + /// TODO: add zunion after it has been implemented + /// + /// Computes a set operation for multiple sorted sets (optionally using per-set weights), + /// optionally performing a specific aggregation (defaults to Sum). + /// Difference operation cannot be used with weights or aggregation. + /// + /// + /// See + /// , + /// . + /// When in cluster mode, all keys must map to the same hash slot. + /// + /// The operation to perform. + /// The keys of the sorted sets. + /// The optional weights per set that correspond to keys. + /// The aggregation method (defaults to Sum). + /// The flags to use for this operation. Currently flags are ignored. + /// + /// The resulting sorted set. Depending on the operation: + /// - Union: Returns the union of members from sorted sets specified by the given keys. + /// - Intersection: Returns the intersection of members from sorted sets specified by the given keys. + /// - Difference: Returns the difference between the first sorted set and all the successive sorted sets. + /// + /// + /// + /// + /// ValkeyValue[] result = await client.SortedSetCombineAsync(SetOperation.Difference, new[] { key1, key2 }); + /// + /// + /// + Task SortedSetCombineAsync(SetOperation operation, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + + /// TODO: add zunion after it has been implemented + /// + /// Computes a set operation for multiple sorted sets (optionally using per-set weights), + /// optionally performing a specific aggregation (defaults to Sum) and returns the result with scores. + /// Difference operation cannot be used with weights or aggregation. + /// + /// + /// See + /// , + /// . + /// When in cluster mode, all keys must map to the same hash slot. + /// + /// The operation to perform. + /// The keys of the sorted sets. + /// The optional weights per set that correspond to keys. + /// The aggregation method (defaults to Sum). + /// The flags to use for this operation. Currently flags are ignored. + /// + /// The resulting sorted set with scores. Depending on the operation: + /// - Union: Returns the union of members and their scores from sorted sets specified by the given keys. + /// - Intersection: Returns the intersection of members and their scores from sorted sets specified by the given keys. + /// - Difference: Returns the difference between the first sorted set and all the successive sorted sets. + /// + /// + /// + /// + /// SortedSetEntry[] result = await client.SortedSetCombineWithScoresAsync(SetOperation.Difference, new[] { key1, key2 }); + /// + /// + /// + Task SortedSetCombineWithScoresAsync(SetOperation operation, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + + /// TODO: add zunionstore after it has been implemented + /// + /// Computes a set operation over two sorted sets, and stores the result in destination, optionally performing + /// a specific aggregation (defaults to sum). + /// Difference operation cannot be used with aggregation. + /// + /// + /// See + /// , + /// . + /// When in cluster mode, all keys must map to the same hash slot. + /// + /// The operation to perform. + /// The key to store the results in. + /// The key of the first sorted set. + /// The key of the second sorted set. + /// The aggregation method (defaults to sum). + /// The flags to use for this operation. Currently flags are ignored. + /// + /// The number of elements in the resulting sorted set at destination. Depending on the operation: + /// - Intersection: Computes the intersection of sorted sets given by the specified keys and stores the result in destination. If destination already exists, it is overwritten. + /// Otherwise, a new sorted set will be created. + /// - Difference: Calculates the difference between the first sorted set and all the successive sorted sets at keys and stores the difference as a sorted set to destination, + /// overwriting it if it already exists. Non-existent keys are treated as empty sets. + /// - Union: Computes the union of sorted sets given by the specified keys, and stores the result in destination. If destination already exists, it + /// is overwritten. Otherwise, a new sorted set will be created. + /// + /// + /// + /// + /// long result = await client.SortedSetCombineAndStoreAsync(SetOperation.Difference, destKey, key1, key2); + /// + /// + /// + Task SortedSetCombineAndStoreAsync(SetOperation operation, ValkeyKey destination, ValkeyKey first, ValkeyKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + + /// TODO: add zunionstore after it has been implemented + /// + /// 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). + /// Difference operation cannot be used with aggregation. + /// + /// + /// See + /// , + /// . + /// When in cluster mode, all keys must map to the same hash slot. + /// + /// The operation to perform. + /// The key to store the results in. + /// The keys of the sorted sets. + /// The optional weights per set that correspond to keys. + /// The aggregation method (defaults to sum). + /// The flags to use for this operation. Currently flags are ignored. + /// + /// The number of elements in the resulting sorted set at destination. Depending on the operation: + /// - Intersection: Computes the intersection of sorted sets given by the specified keys and stores the result in destination. If destination already exists, it is overwritten. + /// Otherwise, a new sorted set will be created. + /// - Difference: Calculates the difference between the first sorted set and all the successive sorted sets at keys and stores the difference as a sorted set to destination, + /// overwriting it if it already exists. Non-existent keys are treated as empty sets. + /// - Union: Computes the union of sorted sets given by the specified keys, and stores the result in destination. If destination already exists, it + /// is overwritten. Otherwise, a new sorted set will be created. + /// + /// + /// + /// + /// long result = await client.SortedSetCombineAndStoreAsync(SetOperation.Difference, destKey, new[] { key1, key2, key3 }); + /// + /// + /// + Task SortedSetCombineAndStoreAsync(SetOperation operation, ValkeyKey destination, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + + /// + /// Increments the score of member in the sorted set stored at key by increment. + /// If member does not exist in the sorted set, it is added with increment as its score. (as if its previous score was 0.0). + /// If key does not exist, a new sorted set with the specified member as its sole member is created. + /// + /// + /// The key of the sorted set. + /// The member to increment. + /// The amount to increment by. + /// The flags to use for this operation. Currently flags are ignored. + /// The new score of member. + /// + /// + /// + /// double newScore = await client.SortedSetIncrementAsync(key, "member1", 2.5); + /// + /// + /// + Task SortedSetIncrementAsync(ValkeyKey key, ValkeyValue member, double value, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the cardinality of the intersection of the sorted sets at keys. + /// + /// + /// When in cluster mode, all keys must map to the same hash slot. + /// Since Valkey 7.0 and above. + /// The keys of the sorted sets. + /// If the intersection cardinality reaches limit partway through the computation, the algorithm will exit and yield limit as the cardinality (defaults to 0 meaning unlimited). + /// The flags to use for this operation. Currently flags are ignored. + /// The number of elements in the resulting intersection. + /// + /// + /// + /// long intersectionCount = await client.SortedSetIntersectionLengthAsync(new[] { key1, key2, key3 }); + /// + /// + /// + Task SortedSetIntersectionLengthAsync(ValkeyKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the number of elements in the sorted set at key with a value between min and max. + /// + /// + /// The key of the sorted set. + /// The min value to filter by. + /// The max value to filter by. + /// Whether to exclude min and max from the range check (defaults to both inclusive). + /// The flags to use for this operation. Currently flags are ignored. + /// The number of elements in the sorted set at key with a value between min and max. + /// + /// + /// + /// long count = await client.SortedSetLengthByValueAsync(key, "a", "z"); + /// + /// + /// + Task SortedSetLengthByValueAsync(ValkeyKey key, ValkeyValue min, ValkeyValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); + + /// + /// Removes and returns up to count entries from the first non-empty sorted set in keys. + /// Returns if none of the sets exist or contain any elements. + /// + /// + /// When in cluster mode, all keys must map to the same hash slot. + /// The keys to check. + /// The maximum number of records to pop out of the sorted set. + /// The order to sort by when popping items out of the set. + /// The flags to use for the operation. Currently flags are ignored. + /// A contiguous collection of sorted set entries with the key they were popped from, or if no non-empty sorted sets are found. + /// + /// + /// + /// SortedSetPopResult result = await client.SortedSetPopAsync(new[] { key1, key2, key3 }, 2); + /// + /// + /// + Task SortedSetPopAsync(ValkeyKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the scores associated with the specified members in the sorted set stored at key. + /// + /// + /// Since Valkey 6.2.0 and above. + /// The key of the sorted set. + /// The members to get the scores for. + /// The flags to use for this operation. Currently flags are ignored. + /// + /// An array of scores corresponding to members. + /// If a member does not exist in the sorted set, the corresponding value in the list will be . + /// + /// + /// + /// + /// double?[] scores = await client.SortedSetScoresAsync(key, new[] { "member1", "member2", "member3" }); + /// + /// + /// + Task SortedSetScoresAsync(ValkeyKey key, ValkeyValue[] members, CommandFlags flags = CommandFlags.None); } diff --git a/sources/Valkey.Glide/Internals/Request.SortedSetCommands.cs b/sources/Valkey.Glide/Internals/Request.SortedSetCommands.cs index d6d3813..ac725ab 100644 --- a/sources/Valkey.Glide/Internals/Request.SortedSetCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.SortedSetCommands.cs @@ -9,25 +9,56 @@ namespace Valkey.Glide.Internals; internal partial class Request { + private static void AddSortedSetCombineOptions(List args, ValkeyKey[] keys, double[]? weights, Aggregate aggregate, SetOperation operation) + { + // Add number of keys + args.Add(keys.Length.ToGlideString()); + + // Add keys + args.AddRange(keys.Select(key => key.ToGlideString())); + + // Add weights if provided (not allowed for difference) + if (weights != null && operation != SetOperation.Difference) + { + args.Add(WeightsKeyword); + args.AddRange(weights.Select(w => w.ToGlideString())); + } + + // Add aggregate if not default (not allowed for difference) + if (aggregate != Aggregate.Sum && operation != SetOperation.Difference) + { + args.Add(AggregateKeyword); + args.Add(aggregate.ToString().ToUpper()); + } + } + + private static RequestType GetSortedSetCombineRequestType(SetOperation operation, bool isStore = false) => operation switch + { + SetOperation.Union => isStore ? RequestType.ZUnionStore : RequestType.ZUnion, + SetOperation.Intersect => isStore ? RequestType.ZInterStore : RequestType.ZInter, + SetOperation.Difference => isStore ? RequestType.ZDiffStore : RequestType.ZDiff, + _ => throw new ArgumentException($"Unsupported operation: {operation}") + }; + private static void AddSortedSetWhenOptions(List args, SortedSetWhen when) { // Add conditional options if (when.HasFlag(SortedSetWhen.Exists)) { - args.Add("XX"); + args.Add(ExistsKeyword); } else if (when.HasFlag(SortedSetWhen.NotExists)) { - args.Add("NX"); + args.Add(NotExistsKeyword); } if (when.HasFlag(SortedSetWhen.GreaterThan)) { - args.Add("GT"); + args.Add(GreaterThanKeyword); } else if (when.HasFlag(SortedSetWhen.LessThan)) { - args.Add("LT"); + args.Add(LessThanKeyword); } } @@ -122,13 +153,7 @@ public static Cmd, SortedSetEntry[]> SortedSetRa return new(RequestType.ZRange, [.. args], false, dict => { - List entries = []; - foreach (KeyValuePair kvp in dict) - { - ValkeyValue element = (ValkeyValue)kvp.Key; - double score = Convert.ToDouble(kvp.Value); - entries.Add(new SortedSetEntry(element, score)); - } + IEnumerable entries = dict.Select(kvp => new SortedSetEntry((ValkeyValue)kvp.Key, (double)kvp.Value)); // Sort by score, then by element for consistent ordering IOrderedEnumerable sortedEntries = order == Order.Ascending @@ -196,13 +221,7 @@ public static Cmd, SortedSetEntry[]> SortedSetRa return new(RequestType.ZRange, [.. args], false, dict => { - List entries = []; - foreach (KeyValuePair kvp in dict) - { - ValkeyValue element = (ValkeyValue)kvp.Key; - double score = Convert.ToDouble(kvp.Value); - entries.Add(new SortedSetEntry(element, score)); - } + IEnumerable entries = dict.Select(kvp => new SortedSetEntry((ValkeyValue)kvp.Key, (double)kvp.Value)); // Sort by score, then by element for consistent ordering IOrderedEnumerable sortedEntries = order == Order.Ascending @@ -278,4 +297,200 @@ public static Cmd SortedSetRangeByValueAsync(ValkeyKey return new(RequestType.ZRange, [.. args], false, array => [.. array.Cast().Select(gs => (ValkeyValue)gs)]); } + + public static Cmd SortedSetCombineAsync(SetOperation operation, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum) + { + List args = []; + AddSortedSetCombineOptions(args, keys, weights, aggregate, operation); + + RequestType requestType = GetSortedSetCombineRequestType(operation); + + return new(requestType, [.. args], false, array => [.. array.Cast().Select(gs => (ValkeyValue)gs)]); + } + + public static Cmd, SortedSetEntry[]> SortedSetCombineWithScoresAsync(SetOperation operation, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum) + { + List args = []; + AddSortedSetCombineOptions(args, keys, weights, aggregate, operation); + + // Add WITHSCORES + args.Add(WithScoresKeyword); + + RequestType requestType = GetSortedSetCombineRequestType(operation); + + return new(requestType, [.. args], false, dict => + { + IEnumerable entries = dict.Select(kvp => new SortedSetEntry((ValkeyValue)kvp.Key, (double)kvp.Value)); + return [.. entries.OrderBy(e => e.Score).ThenBy(e => e.Element.ToString())]; + }); + } + + public static Cmd SortedSetCombineAndStoreAsync(SetOperation operation, ValkeyKey destination, ValkeyKey first, ValkeyKey second, Aggregate aggregate = Aggregate.Sum) + => SortedSetCombineAndStoreAsync(operation, destination, [first, second], null, aggregate); + + public static Cmd SortedSetCombineAndStoreAsync(SetOperation operation, ValkeyKey destination, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum) + { + List args = [destination.ToGlideString()]; + AddSortedSetCombineOptions(args, keys, weights, aggregate, operation); + + RequestType requestType = GetSortedSetCombineRequestType(operation, isStore: true); + + return Simple(requestType, [.. args]); + } + + public static Cmd SortedSetIncrementAsync(ValkeyKey key, ValkeyValue member, double value) + { + List args = [key.ToGlideString(), value.ToGlideString(), member.ToGlideString()]; + return Simple(RequestType.ZIncrBy, [.. args], false); + } + + public static Cmd SortedSetIntersectionLengthAsync(ValkeyKey[] keys, long limit = 0) + { + List args = [keys.Length.ToGlideString()]; + args.AddRange(keys.Select(key => key.ToGlideString())); + + if (limit > 0) + { + args.Add(LimitKeyword); + args.Add(limit.ToGlideString()); + } + + return Simple(RequestType.ZInterCard, [.. args]); + } + + public static Cmd SortedSetLengthByValueAsync(ValkeyKey key, ValkeyValue min, ValkeyValue max, Exclude exclude = Exclude.None) + { + // Create lexicographical boundaries based on exclude flags + LexBoundary minBoundary = exclude.HasFlag(Exclude.Start) + ? LexBoundary.Exclusive(min) + : LexBoundary.Inclusive(min); + + LexBoundary maxBoundary = exclude.HasFlag(Exclude.Stop) + ? LexBoundary.Exclusive(max) + : LexBoundary.Inclusive(max); + + List args = [key.ToGlideString(), ((string)minBoundary).ToGlideString(), ((string)maxBoundary).ToGlideString()]; + return Simple(RequestType.ZLexCount, [.. args]); + } + + public static Cmd SortedSetPopAsync(ValkeyKey[] keys, long count, Order order = Order.Ascending) + { + List args = [keys.Length.ToGlideString()]; + args.AddRange(keys.Select(key => key.ToGlideString())); + + args.Add(order == Order.Ascending ? MinKeyword : MaxKeyword); + args.Add(CountKeyword); + args.Add(count.ToGlideString()); + + return new(RequestType.ZMPop, [.. args], true, HandleSortedSetPopResultResponse); + } + + public static Cmd SortedSetScoresAsync(ValkeyKey key, ValkeyValue[] members) + { + List args = [key.ToGlideString()]; + args.AddRange(members.Select(member => member.ToGlideString())); + + return new(RequestType.ZMScore, [.. args], false, response => + { + double?[] scores = new double?[response.Length]; + + for (int i = 0; i < response.Length; i++) + { + scores[i] = response[i] == null ? null : (double)response[i]; + } + + return scores; + }); + } + + public static Cmd SortedSetBlockingPopAsync(ValkeyKey key, Order order, double timeout) + { + List args = [key.ToGlideString(), timeout.ToGlideString()]; + + RequestType requestType = order == Order.Ascending ? RequestType.BZPopMin : RequestType.BZPopMax; + + return new(requestType, [.. args], true, response => + { + if (response == null) + { + return null; + } + + Object[] responseArray = (Object[])response; + + ValkeyValue member = (ValkeyValue)(GlideString)responseArray[1]; + double score = (double)responseArray[2]; + return new SortedSetEntry(member, score); + }, allowConverterToHandleNull: true); + } + + // Note: We keep count for the future TODO but disable the warning for now. +#pragma warning disable IDE0060 // Remove unused parameter + public static Cmd SortedSetBlockingPopAsync(ValkeyKey key, long count, Order order, double timeout) + { + // FUTURE TODO: support count > 1 requests + List args = [key.ToGlideString(), timeout.ToGlideString()]; + RequestType requestType = order == Order.Ascending ? RequestType.BZPopMin : RequestType.BZPopMax; + + return new(requestType, [.. args], true, response => + { + if (response == null) + { + return []; + } + + Object[] responseArray = (Object[])response; + + // BZPOPMIN/BZPOPMAX returns [key, member, score] - only one element + ValkeyValue member = (ValkeyValue)(GlideString)responseArray[1]; + double score = (double)responseArray[2]; + return [new SortedSetEntry(member, score)]; + }, allowConverterToHandleNull: true); + } +#pragma warning restore IDE0060 // Remove unused parameter + + public static Cmd SortedSetBlockingPopAsync(ValkeyKey[] keys, long count, Order order, double timeout) + { + List args = [timeout.ToGlideString(), keys.Length.ToGlideString()]; + args.AddRange(keys.Select(key => key.ToGlideString())); + + args.Add(order == Order.Ascending ? MinKeyword : MaxKeyword); + args.Add(CountKeyword); + args.Add(count.ToGlideString()); + + return new(RequestType.BZMPop, [.. args], true, HandleSortedSetPopResultResponse, allowConverterToHandleNull: true); + } + + /// + /// Shared response handler for sorted set pop operations (both blocking and non-blocking). + /// Handles the standard response format: [key, Dictionary<member, score>] or null. + /// + private static SortedSetPopResult HandleSortedSetPopResultResponse(object? response) + { + if (response == null) + { + return SortedSetPopResult.Null; + } + + if (response is not object[] responseArray || responseArray.Length != 2) + { + throw new InvalidOperationException($"Unexpected response format for sorted set pop operation"); + } + + ValkeyKey key = ((GlideString)responseArray[0]).ToString(); + + if (responseArray[1] is not Dictionary membersAndScores) + { + throw new InvalidOperationException($"Expected dictionary for members and scores, got {responseArray[1]?.GetType()}"); + } + + if (membersAndScores.Count == 0) + { + return SortedSetPopResult.Null; + } + + SortedSetEntry[] entries = [.. membersAndScores.Select(kvp => new SortedSetEntry((ValkeyValue)kvp.Key, (double)kvp.Value))]; + + return new SortedSetPopResult(key, entries); + } } diff --git a/sources/Valkey.Glide/Pipeline/BaseBatch.SortedSetCommands.cs b/sources/Valkey.Glide/Pipeline/BaseBatch.SortedSetCommands.cs index 578c55b..9cec6d8 100644 --- a/sources/Valkey.Glide/Pipeline/BaseBatch.SortedSetCommands.cs +++ b/sources/Valkey.Glide/Pipeline/BaseBatch.SortedSetCommands.cs @@ -58,6 +58,42 @@ public T SortedSetLength(ValkeyKey key, double min = double.NegativeInfinity, do /// public T SortedSetRangeByValue(ValkeyKey key, ValkeyValue min = default, ValkeyValue max = default, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1) => AddCmd(SortedSetRangeByValueAsync(key, min, max, exclude, order, skip, take)); + /// + public T SortedSetCombine(SetOperation operation, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum) => AddCmd(SortedSetCombineAsync(operation, keys, weights, aggregate)); + + /// + public T SortedSetCombineWithScores(SetOperation operation, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum) => AddCmd(SortedSetCombineWithScoresAsync(operation, keys, weights, aggregate)); + + /// + public T SortedSetCombineAndStore(SetOperation operation, ValkeyKey destination, ValkeyKey first, ValkeyKey second, Aggregate aggregate = Aggregate.Sum) => AddCmd(SortedSetCombineAndStoreAsync(operation, destination, first, second, aggregate)); + + /// + public T SortedSetCombineAndStore(SetOperation operation, ValkeyKey destination, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum) => AddCmd(SortedSetCombineAndStoreAsync(operation, destination, keys, weights, aggregate)); + + /// + public T SortedSetIncrement(ValkeyKey key, ValkeyValue member, double value) => AddCmd(SortedSetIncrementAsync(key, member, value)); + + /// + public T SortedSetIntersectionLength(ValkeyKey[] keys, long limit = 0) => AddCmd(SortedSetIntersectionLengthAsync(keys, limit)); + + /// + public T SortedSetLengthByValue(ValkeyKey key, ValkeyValue min, ValkeyValue max, Exclude exclude = Exclude.None) => AddCmd(SortedSetLengthByValueAsync(key, min, max, exclude)); + + /// + public T SortedSetPop(ValkeyKey[] keys, long count, Order order = Order.Ascending) => AddCmd(SortedSetPopAsync(keys, count, order)); + + /// + public T SortedSetScores(ValkeyKey key, ValkeyValue[] members) => AddCmd(SortedSetScoresAsync(key, members)); + + /// + public T SortedSetBlockingPop(ValkeyKey key, Order order, double timeout) => AddCmd(SortedSetBlockingPopAsync(key, order, timeout)); + + /// + public T SortedSetBlockingPop(ValkeyKey key, long count, Order order, double timeout) => AddCmd(SortedSetBlockingPopAsync(key, count, order, timeout)); + + /// + public T SortedSetBlockingPop(ValkeyKey[] keys, long count, Order order, double timeout) => AddCmd(SortedSetBlockingPopAsync(keys, count, order, timeout)); + // Explicit interface implementations for IBatchSortedSetCommands IBatch IBatchSortedSetCommands.SortedSetAdd(ValkeyKey key, ValkeyValue member, double score, SortedSetWhen when) => SortedSetAdd(key, member, score, when); IBatch IBatchSortedSetCommands.SortedSetAdd(ValkeyKey key, SortedSetEntry[] values, SortedSetWhen when) => SortedSetAdd(key, values, when); @@ -72,4 +108,16 @@ public T SortedSetLength(ValkeyKey key, double min = double.NegativeInfinity, do IBatch IBatchSortedSetCommands.SortedSetRangeByScoreWithScores(ValkeyKey key, double start, double stop, Exclude exclude, Order order, long skip, long take) => SortedSetRangeByScoreWithScores(key, start, stop, exclude, order, skip, take); IBatch IBatchSortedSetCommands.SortedSetRangeByValue(ValkeyKey key, ValkeyValue min, ValkeyValue max, Exclude exclude, long skip, long take) => SortedSetRangeByValue(key, min, max, exclude, skip, take); IBatch IBatchSortedSetCommands.SortedSetRangeByValue(ValkeyKey key, ValkeyValue min, ValkeyValue max, Exclude exclude, Order order, long skip, long take) => SortedSetRangeByValue(key, min, max, exclude, order, skip, take); + IBatch IBatchSortedSetCommands.SortedSetCombine(SetOperation operation, ValkeyKey[] keys, double[]? weights, Aggregate aggregate) => SortedSetCombine(operation, keys, weights, aggregate); + IBatch IBatchSortedSetCommands.SortedSetCombineWithScores(SetOperation operation, ValkeyKey[] keys, double[]? weights, Aggregate aggregate) => SortedSetCombineWithScores(operation, keys, weights, aggregate); + IBatch IBatchSortedSetCommands.SortedSetCombineAndStore(SetOperation operation, ValkeyKey destination, ValkeyKey first, ValkeyKey second, Aggregate aggregate) => SortedSetCombineAndStore(operation, destination, first, second, aggregate); + IBatch IBatchSortedSetCommands.SortedSetCombineAndStore(SetOperation operation, ValkeyKey destination, ValkeyKey[] keys, double[]? weights, Aggregate aggregate) => SortedSetCombineAndStore(operation, destination, keys, weights, aggregate); + IBatch IBatchSortedSetCommands.SortedSetIncrement(ValkeyKey key, ValkeyValue member, double value) => SortedSetIncrement(key, member, value); + IBatch IBatchSortedSetCommands.SortedSetIntersectionLength(ValkeyKey[] keys, long limit) => SortedSetIntersectionLength(keys, limit); + IBatch IBatchSortedSetCommands.SortedSetLengthByValue(ValkeyKey key, ValkeyValue min, ValkeyValue max, Exclude exclude) => SortedSetLengthByValue(key, min, max, exclude); + IBatch IBatchSortedSetCommands.SortedSetPop(ValkeyKey[] keys, long count, Order order) => SortedSetPop(keys, count, order); + IBatch IBatchSortedSetCommands.SortedSetScores(ValkeyKey key, ValkeyValue[] members) => SortedSetScores(key, members); + IBatch IBatchSortedSetCommands.SortedSetBlockingPop(ValkeyKey key, Order order, double timeout) => SortedSetBlockingPop(key, order, timeout); + IBatch IBatchSortedSetCommands.SortedSetBlockingPop(ValkeyKey key, long count, Order order, double timeout) => SortedSetBlockingPop(key, count, order, timeout); + IBatch IBatchSortedSetCommands.SortedSetBlockingPop(ValkeyKey[] keys, long count, Order order, double timeout) => SortedSetBlockingPop(keys, count, order, timeout); } diff --git a/sources/Valkey.Glide/Pipeline/IBatchSortedSetCommands.cs b/sources/Valkey.Glide/Pipeline/IBatchSortedSetCommands.cs index 70e16fc..b235c34 100644 --- a/sources/Valkey.Glide/Pipeline/IBatchSortedSetCommands.cs +++ b/sources/Valkey.Glide/Pipeline/IBatchSortedSetCommands.cs @@ -60,4 +60,52 @@ internal interface IBatchSortedSetCommands /// /// Command Response - IBatch SortedSetRangeByValue(ValkeyKey key, ValkeyValue min = default, ValkeyValue max = default, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1); + + /// + /// Command Response - + IBatch SortedSetCombine(SetOperation operation, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum); + + /// + /// Command Response - + IBatch SortedSetCombineWithScores(SetOperation operation, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum); + + /// + /// Command Response - + IBatch SortedSetCombineAndStore(SetOperation operation, ValkeyKey destination, ValkeyKey first, ValkeyKey second, Aggregate aggregate = Aggregate.Sum); + + /// + /// Command Response - + IBatch SortedSetCombineAndStore(SetOperation operation, ValkeyKey destination, ValkeyKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum); + + /// + /// Command Response - + IBatch SortedSetIncrement(ValkeyKey key, ValkeyValue member, double value); + + /// + /// Command Response - + IBatch SortedSetIntersectionLength(ValkeyKey[] keys, long limit = 0); + + /// + /// Command Response - + IBatch SortedSetLengthByValue(ValkeyKey key, ValkeyValue min, ValkeyValue max, Exclude exclude = Exclude.None); + + /// + /// Command Response - + IBatch SortedSetPop(ValkeyKey[] keys, long count, Order order = Order.Ascending); + + /// + /// Command Response - + IBatch SortedSetScores(ValkeyKey key, ValkeyValue[] members); + + /// + /// Command Response - + IBatch SortedSetBlockingPop(ValkeyKey key, Order order, double timeout); + + /// + /// Command Response - + IBatch SortedSetBlockingPop(ValkeyKey key, long count, Order order, double timeout); + + /// + /// Command Response - + IBatch SortedSetBlockingPop(ValkeyKey[] keys, long count, Order order, double timeout); } diff --git a/sources/Valkey.Glide/abstract_APITypes/SortedSetPopResult.cs b/sources/Valkey.Glide/abstract_APITypes/SortedSetPopResult.cs index d1e481a..257bf0d 100644 --- a/sources/Valkey.Glide/abstract_APITypes/SortedSetPopResult.cs +++ b/sources/Valkey.Glide/abstract_APITypes/SortedSetPopResult.cs @@ -15,7 +15,7 @@ public readonly struct SortedSetPopResult /// /// Whether this object is null/empty. /// - public bool IsNull => Key.IsNull && Entries == Array.Empty(); + public bool IsNull => Key.IsNull && (Entries == null || Entries.Length == 0); /// /// The key of the sorted set these entries came form. diff --git a/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs b/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs index bb3965e..d3f88b1 100644 --- a/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs +++ b/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs @@ -1,4 +1,4 @@ -// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 namespace Valkey.Glide.IntegrationTests; @@ -590,39 +590,57 @@ public static List CreateSortedSetTest(Pipeline.IBatch batch, bool isA testData.Add(new(2L, "SortedSetCount(key1, 1.0, 3.0, Exclude.Start)")); // Test SortedSetRangeByRank + // key1 has testMember1 (1.0), testMember2 (2.0), testMember3 (3.0) _ = batch.SortedSetRangeByRank(key1); - testData.Add(new(Array.Empty(), "SortedSetRangeByRank(key1) - all elements", true)); + testData.Add(new(new ValkeyValue[] { "testMember1", "testMember2", "testMember3" }, "SortedSetRangeByRank(key1) - all elements")); _ = batch.SortedSetRangeByRank(key1, 0, 1); - testData.Add(new(Array.Empty(), "SortedSetRangeByRank(key1, 0, 1)", true)); + testData.Add(new(new ValkeyValue[] { "testMember1", "testMember2" }, "SortedSetRangeByRank(key1, 0, 1)")); _ = batch.SortedSetRangeByRank(key1, 0, 1, Order.Descending); - testData.Add(new(Array.Empty(), "SortedSetRangeByRank(key1, 0, 1, Descending)", true)); + testData.Add(new(new ValkeyValue[] { "testMember3", "testMember2" }, "SortedSetRangeByRank(key1, 0, 1, Descending)")); // Test SortedSetRangeByRankWithScores _ = batch.SortedSetRangeByRankWithScores(key1); - testData.Add(new(Array.Empty(), "SortedSetRangeByRankWithScores(key1) - all elements", true)); + testData.Add(new(new SortedSetEntry[] + { + new("testMember1", 1.0), + new("testMember2", 2.0), + new("testMember3", 3.0) + }, "SortedSetRangeByRankWithScores(key1) - all elements")); _ = batch.SortedSetRangeByRankWithScores(key1, 0, 1); - testData.Add(new(Array.Empty(), "SortedSetRangeByRankWithScores(key1, 0, 1)", true)); + testData.Add(new(new SortedSetEntry[] + { + new("testMember1", 1.0), + new("testMember2", 2.0) + }, "SortedSetRangeByRankWithScores(key1, 0, 1)")); // Test SortedSetRangeByScore _ = batch.SortedSetRangeByScore(key1, 1.0, 3.0); - testData.Add(new(Array.Empty(), "SortedSetRangeByScore(key1, 1.0, 3.0)", true)); + testData.Add(new(new ValkeyValue[] { "testMember1", "testMember2", "testMember3" }, "SortedSetRangeByScore(key1, 1.0, 3.0)")); _ = batch.SortedSetRangeByScore(key1, 1.0, 3.0, Exclude.None, Order.Descending); - testData.Add(new(Array.Empty(), "SortedSetRangeByScore(key1, 1.0, 3.0, Descending)", true)); + testData.Add(new(new ValkeyValue[] { "testMember3", "testMember2", "testMember1" }, "SortedSetRangeByScore(key1, 1.0, 3.0, Descending)")); // Test SortedSetRangeByScoreWithScores _ = batch.SortedSetRangeByScoreWithScores(key1, 1.0, 3.0); - testData.Add(new(Array.Empty(), "SortedSetRangeByScoreWithScores(key1, 1.0, 3.0)", true)); + testData.Add(new(new SortedSetEntry[] + { + new("testMember1", 1.0), + new("testMember2", 2.0), + new("testMember3", 3.0) + }, "SortedSetRangeByScoreWithScores(key1, 1.0, 3.0)")); _ = batch.SortedSetRangeByScoreWithScores(key1, 1.0, 3.0, skip: 1, take: 1); - testData.Add(new(Array.Empty(), "SortedSetRangeByScoreWithScores(key1, 1.0, 3.0, skip: 1, take: 1)", true)); + testData.Add(new(new SortedSetEntry[] + { + new("testMember2", 2.0) + }, "SortedSetRangeByScoreWithScores(key1, 1.0, 3.0, skip: 1, take: 1)")); // Add members with same score for lexicographical ordering tests _ = batch.SortedSetAdd(key2, "apple", 0.0); - testData.Add(new(false, "SortedSetAdd(key2, apple, 0.0)")); + testData.Add(new(true, "SortedSetAdd(key2, apple, 0.0)")); _ = batch.SortedSetAdd(key2, "banana", 0.0); testData.Add(new(true, "SortedSetAdd(key2, banana, 0.0)")); @@ -631,18 +649,109 @@ public static List CreateSortedSetTest(Pipeline.IBatch batch, bool isA testData.Add(new(true, "SortedSetAdd(key2, cherry, 0.0)")); // Test SortedSetRangeByValue + // key2 has newMember (7.5), apple (0.0), banana (0.0), cherry (0.0) - lexicographically ordered for same scores _ = batch.SortedSetRangeByValue(key2, "a", "c", Exclude.None, 0, -1); - testData.Add(new(Array.Empty(), "SortedSetRangeByValue(key2, 'a', 'c', Exclude.None, 0, -1)", true)); + testData.Add(new(new ValkeyValue[] { "apple", "banana" }, "SortedSetRangeByValue(key2, 'a', 'c', Exclude.None, 0, -1)")); _ = batch.SortedSetRangeByValue(key2, "b", "d", Exclude.None, skip: 1, take: 1); - testData.Add(new(Array.Empty(), "SortedSetRangeByValue(key2, 'b', 'd', Exclude.None, skip: 1, take: 1)", true)); + testData.Add(new(new ValkeyValue[] { "cherry" }, "SortedSetRangeByValue(key2, 'b', 'd', Exclude.None, skip: 1, take: 1)")); // Test SortedSetRangeByValue _ = batch.SortedSetRangeByValue(key2, order: Order.Descending); - testData.Add(new(Array.Empty(), "SortedSetRangeByValue(key2, order: Descending)", true)); + testData.Add(new(new ValkeyValue[] { "newMember", "cherry", "banana", "apple" }, "SortedSetRangeByValue(key2, order: Descending)")); _ = batch.SortedSetRangeByValue(key2, "a", "c", order: Order.Ascending); - testData.Add(new(Array.Empty(), "SortedSetRangeByValue(key2, 'a', 'c', order: Ascending)", true)); + testData.Add(new(new ValkeyValue[] { "apple", "banana" }, "SortedSetRangeByValue(key2, 'a', 'c', order: Ascending)")); + + // Test new sorted set commands + string key3 = $"{prefix}3-{Guid.NewGuid()}"; + string destKey = $"{prefix}dest-{Guid.NewGuid()}"; + + // Test SortedSetIncrement + _ = batch.SortedSetIncrement(key1, "testMember1", 5.0); + testData.Add(new(6.0, "SortedSetIncrement(key1, testMember1, 5.0)")); + + // Test combine operations - use prefixed keys to ensure same hash slot for cluster mode + string combineKey1 = $"{{sortedSetKey}}-combine1-{Guid.NewGuid()}"; + string combineKey3 = $"{{sortedSetKey}}-combine3-{Guid.NewGuid()}"; + string combineDestKey = $"{{sortedSetKey}}-combineDest-{Guid.NewGuid()}"; + + // Setup data for combine operations + _ = batch.SortedSetAdd(combineKey1, [ + new SortedSetEntry("testMember1", 6.0), // After increment + new SortedSetEntry("testMember2", 2.0), + new SortedSetEntry("testMember3", 3.0) + ]); + testData.Add(new(3L, "SortedSetAdd(combineKey1, test data for combine)")); + + _ = batch.SortedSetAdd(combineKey3, [ + new SortedSetEntry("testMember2", 25.0), + new SortedSetEntry("testMember4", 40.0) + ]); + testData.Add(new(2L, "SortedSetAdd(combineKey3, test data for combine)")); + + _ = batch.SortedSetCombine(SetOperation.Union, [combineKey1, combineKey3]); + testData.Add(new(new ValkeyValue[] { "testMember2", "testMember3", "testMember1", "testMember4" }, "SortedSetCombine(Union, [combineKey1, combineKey3])")); + + _ = batch.SortedSetCombineWithScores(SetOperation.Intersect, [combineKey1, combineKey3]); + testData.Add(new(new SortedSetEntry[] { new("testMember2", 27.0) }, "SortedSetCombineWithScores(Intersect, [combineKey1, combineKey3])")); + + _ = batch.SortedSetCombineAndStore(SetOperation.Union, combineDestKey, combineKey1, combineKey3); + testData.Add(new(4L, "SortedSetCombineAndStore(Union, combineDestKey, combineKey1, combineKey3)")); + + // Test SortedSetIntersectionLength (ZINTERCARD) - requires Redis 7.0+ + if (TestConfiguration.SERVER_VERSION >= new Version("7.0.0")) + { + _ = batch.SortedSetIntersectionLength([combineKey1, combineKey3]); + testData.Add(new(1L, "SortedSetIntersectionLength([combineKey1, combineKey3])")); + } + + // Test SortedSetLengthByValue + _ = batch.SortedSetLengthByValue(key2, "a", "c"); + testData.Add(new(2L, "SortedSetLengthByValue(key2, a, c)")); + + // Test scores retrieval + // testMember1 was incremented from 1.0 to 6.0, testMember2 is 2.0, nonexistent should be null + _ = batch.SortedSetScores(key1, ["testMember1", "testMember2", "nonexistent"]); + testData.Add(new(new double?[] { 6.0, 2.0, null }, "SortedSetScores(key1, [testMember1, testMember2, nonexistent])")); + + // Test pop operations + // Test pop operations - add data first to prevent blocking + string popKey = $"{prefix}pop-{Guid.NewGuid()}"; + _ = batch.SortedSetAdd(popKey, [ + new SortedSetEntry("member1", 1.0), + new SortedSetEntry("member2", 2.0) + ]); + testData.Add(new(2L, "SortedSetAdd(popKey, test data for pop)")); + + // Test SortedSetPop (ZMPOP) - requires Redis 7.0+ + if (TestConfiguration.SERVER_VERSION >= new Version("7.0.0")) + { + _ = batch.SortedSetPop([popKey], 1); + testData.Add(new(new SortedSetPopResult(popKey, [ + new SortedSetEntry("member1", 1.0) + ]), "SortedSetPop([popKey], 1)")); + + // Test blocking commands with data present to prevent actual blocking + string blockingKey = $"{prefix}blocking-{Guid.NewGuid()}"; + _ = batch.SortedSetAdd(blockingKey, [ + new SortedSetEntry("block1", 10.0), + new SortedSetEntry("block2", 20.0) + ]); + testData.Add(new(2L, "SortedSetAdd(blockingKey, test data for blocking)")); + + // Test SortedSetBlockingPop (single key, single element) + _ = batch.SortedSetBlockingPop(blockingKey, Order.Ascending, 0.1); + testData.Add(new(new SortedSetEntry("block1", 10.0), "SortedSetBlockingPop(blockingKey, Ascending, 0.1s)")); + + // Test SortedSetBlockingPop (single key, multiple elements) + _ = batch.SortedSetBlockingPop(blockingKey, 1, Order.Descending, 0.1); + testData.Add(new(new SortedSetEntry[] { new("block2", 20.0) }, "SortedSetBlockingPop(blockingKey, 1, Descending, 0.1s)")); + + // Test SortedSetBlockingPop (multi-key, multiple elements) + _ = batch.SortedSetBlockingPop([blockingKey], 1, Order.Descending, 0.1); + testData.Add(new(SortedSetPopResult.Null, "SortedSetBlockingPop([blockingKey], 1, Descending, 0.1s) - should be null")); + } return testData; } @@ -972,6 +1081,7 @@ [.. TestConfiguration.TestClients.SelectMany(r => new[] { true, false }.SelectMa new("Set commands", r.Data, CreateSetTest, isAtomic), new("Generic commands", r.Data, CreateGenericTest, isAtomic), new("List commands", r.Data, CreateListTest, isAtomic), + new("Sorted Set commands", r.Data, CreateSortedSetTest, isAtomic), new("Connection Management commands", r.Data, CreateConnectionManagementTest, isAtomic), }))]; } diff --git a/tests/Valkey.Glide.IntegrationTests/SortedSetCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/SortedSetCommandTests.cs index ab48c93..d8ce1af 100644 --- a/tests/Valkey.Glide.IntegrationTests/SortedSetCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/SortedSetCommandTests.cs @@ -672,4 +672,377 @@ public async Task TestSortedSetRangeByValueWithOrderAsync(BaseClient client) Assert.Equal("cherry", result[0]); Assert.Equal("banana", result[1]); } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task TestSortedSetCombine(BaseClient client) + { + string key1 = $"{{sortedSetKey}}-{Guid.NewGuid()}"; + string key2 = $"{{sortedSetKey}}-{Guid.NewGuid()}"; + string key3 = $"{{sortedSetKey}}-{Guid.NewGuid()}"; + + // Setup test data + await client.SortedSetAddAsync(key1, [ + new("member1", 10.0), + new("member2", 20.0) + ]); + await client.SortedSetAddAsync(key2, [ + new("member2", 15.0), + new("member3", 25.0) + ]); + + // Test union + ValkeyValue[] result = await client.SortedSetCombineAsync(SetOperation.Union, [key1, key2]); + Assert.Equal(3, result.Length); + Assert.Contains("member1", result.Select(v => v.ToString())); + Assert.Contains("member2", result.Select(v => v.ToString())); + Assert.Contains("member3", result.Select(v => v.ToString())); + + // Test intersection + result = await client.SortedSetCombineAsync(SetOperation.Intersect, [key1, key2]); + Assert.Single(result); + Assert.Equal("member2", result[0]); + + // Test difference + result = await client.SortedSetCombineAsync(SetOperation.Difference, [key1, key2]); + Assert.Single(result); + Assert.Equal("member1", result[0]); + + // Test with non-existent key + result = await client.SortedSetCombineAsync(SetOperation.Union, [key1, key3]); + Assert.Equal(2, result.Length); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task TestSortedSetCombineWithScores(BaseClient client) + { + string key1 = $"{{sortedSetKey}}-{Guid.NewGuid()}"; + string key2 = $"{{sortedSetKey}}-{Guid.NewGuid()}"; + + // Setup test data + await client.SortedSetAddAsync(key1, [ + new("member1", 10.0), + new("member2", 20.0) + ]); + await client.SortedSetAddAsync(key2, [ + new("member2", 15.0), + new("member3", 25.0) + ]); + + // Test union with scores + SortedSetEntry[] result = await client.SortedSetCombineWithScoresAsync(SetOperation.Union, [key1, key2]); + Assert.Equal(3, result.Length); + + // Test intersection with scores + result = await client.SortedSetCombineWithScoresAsync(SetOperation.Intersect, [key1, key2]); + Assert.Single(result); + Assert.Equal("member2", result[0].Element); + Assert.Equal(35.0, result[0].Score); // Sum aggregation: 20 + 15 + + // Test with weights + result = await client.SortedSetCombineWithScoresAsync(SetOperation.Union, [key1, key2], [2.0, 0.5]); + Assert.Equal(3, result.Length); + var member2Entry = result.First(e => e.Element == "member2"); + Assert.Equal(47.5, member2Entry.Score); // (20 * 2) + (15 * 0.5) + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task TestSortedSetCombineAndStore(BaseClient client) + { + string key1 = $"{{sortedSetKey}}-{Guid.NewGuid()}"; + string key2 = $"{{sortedSetKey}}-{Guid.NewGuid()}"; + string destKey = $"{{sortedSetKey}}-{Guid.NewGuid()}"; + + // Setup test data + await client.SortedSetAddAsync(key1, [ + new("member1", 10.0), + new("member2", 20.0) + ]); + await client.SortedSetAddAsync(key2, [ + new("member2", 15.0), + new("member3", 25.0) + ]); + + // Test union and store + long result = await client.SortedSetCombineAndStoreAsync(SetOperation.Union, destKey, key1, key2); + Assert.Equal(3, result); + + // Verify stored result + long count = await client.SortedSetCardAsync(destKey); + Assert.Equal(3, count); + + // Test intersection and store with multiple keys + result = await client.SortedSetCombineAndStoreAsync(SetOperation.Intersect, destKey, [key1, key2]); + Assert.Equal(1, result); + + count = await client.SortedSetCardAsync(destKey); + Assert.Equal(1, count); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task TestSortedSetIncrement(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Test increment on non-existent member + double result = await client.SortedSetIncrementAsync(key, "member1", 10.5); + Assert.Equal(10.5, result); + + // Test increment on existing member + result = await client.SortedSetIncrementAsync(key, "member1", 5.0); + Assert.Equal(15.5, result); + + // Test negative increment + result = await client.SortedSetIncrementAsync(key, "member1", -3.0); + Assert.Equal(12.5, result); + + // Test increment by zero + result = await client.SortedSetIncrementAsync(key, "member1", 0.0); + Assert.Equal(12.5, result); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task TestSortedSetIntersectionLength(BaseClient client) + { + Assert.SkipWhen( + TestConfiguration.SERVER_VERSION < new Version("7.0.0"), + "ZINTERCARD is supported since 7.0.0" + ); + string key1 = $"{{sortedSetKey}}-{Guid.NewGuid()}"; + string key2 = $"{{sortedSetKey}}-{Guid.NewGuid()}"; + string key3 = $"{{sortedSetKey}}-{Guid.NewGuid()}"; + string emptyKey = $"{{sortedSetKey}}-{Guid.NewGuid()}"; + + // Setup test data + await client.SortedSetAddAsync(key1, [ + new("member1", 10.0), + new("member2", 20.0), + new("member3", 30.0) + ]); + await client.SortedSetAddAsync(key2, [ + new("member2", 15.0), + new("member3", 25.0), + new("member4", 35.0) + ]); + await client.SortedSetAddAsync(key3, [ + new("member3", 40.0), + new("member5", 50.0) + ]); + + // Test intersection of two sets + long result = await client.SortedSetIntersectionLengthAsync([key1, key2]); + Assert.Equal(2, result); // member2, member3 + + // Test intersection of three sets + result = await client.SortedSetIntersectionLengthAsync([key1, key2, key3]); + Assert.Equal(1, result); // member3 + + // Test with limit + result = await client.SortedSetIntersectionLengthAsync([key1, key2], 1); + Assert.Equal(1, result); + + // Test with non-existent key + result = await client.SortedSetIntersectionLengthAsync([key1, emptyKey]); + Assert.Equal(0, result); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task TestSortedSetLengthByValue(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Setup test data with same scores for lexicographical ordering + await client.SortedSetAddAsync(key, [ + new("apple", 0.0), + new("banana", 0.0), + new("cherry", 0.0), + new("date", 0.0) + ]); + + // Test full range + long result = await client.SortedSetLengthByValueAsync(key, "a", "z"); + Assert.Equal(4, result); + + // Test specific range + result = await client.SortedSetLengthByValueAsync(key, "b", "d"); + Assert.Equal(2, result); // banana, cherry + + // Test with exclusions + result = await client.SortedSetLengthByValueAsync(key, "b", "d", Exclude.Both); + Assert.Equal(2, result); + + // Test with exclusions + result = await client.SortedSetLengthByValueAsync(key, "banana", "date", Exclude.Both); + Assert.Equal(1, result); // cherry + + // Test with non-existent key + result = await client.SortedSetLengthByValueAsync(Guid.NewGuid().ToString(), "a", "z"); + Assert.Equal(0, result); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task TestSortedSetPop(BaseClient client) + { + Assert.SkipWhen( + TestConfiguration.SERVER_VERSION < new Version("7.0.0"), + "ZMPOP is supported since 7.0.0" + ); + string key1 = $"{{sortedSetKey}}1-{Guid.NewGuid()}"; + string key2 = $"{{sortedSetKey}}2-{Guid.NewGuid()}"; + string emptyKey = $"{{sortedSetKey}}empty-{Guid.NewGuid()}"; + + // Setup test data + await client.SortedSetAddAsync(key1, [ + new("member1", 10.0), + new("member2", 20.0), + new("member3", 30.0) + ]); + + // Test pop min (ascending) + SortedSetPopResult result = await client.SortedSetPopAsync([key1, key2], 2); + Assert.False(result.IsNull); + Assert.Equal(key1, result.Key); + Assert.Equal(2, result.Entries.Length); + Assert.Equal("member1", result.Entries[0].Element); + Assert.Equal(10.0, result.Entries[0].Score); + Assert.Equal("member2", result.Entries[1].Element); + Assert.Equal(20.0, result.Entries[1].Score); + + // Test pop max (descending) + result = await client.SortedSetPopAsync([key1], 1, Order.Descending); + Assert.False(result.IsNull); + Assert.Equal(key1, result.Key); + Assert.Single(result.Entries); + Assert.Equal("member3", result.Entries[0].Element); + Assert.Equal(30.0, result.Entries[0].Score); + + // Test pop from empty sets + result = await client.SortedSetPopAsync([emptyKey], 1); + Assert.True(result.IsNull); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task TestSortedSetScores(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Setup test data + await client.SortedSetAddAsync(key, [ + new("member1", 10.5), + new("member2", 20.0), + new("member3", 30.5) + ]); + + // Test getting scores for existing members + double?[] result = await client.SortedSetScoresAsync(key, ["member1", "member2", "member3"]); + Assert.Equal(3, result.Length); + Assert.Equal(10.5, result[0]); + Assert.Equal(20.0, result[1]); + Assert.Equal(30.5, result[2]); + + // Test getting scores for mix of existing and non-existing members + result = await client.SortedSetScoresAsync(key, ["member1", "nonexistent", "member3"]); + Assert.Equal(3, result.Length); + Assert.Equal(10.5, result[0]); + Assert.Null(result[1]); + Assert.Equal(30.5, result[2]); + + // Test with non-existent key + result = await client.SortedSetScoresAsync(Guid.NewGuid().ToString(), ["member1"]); + Assert.Single(result); + Assert.Null(result[0]); + + // Test with empty members array + await Assert.ThrowsAsync(async () => await client.SortedSetScoresAsync(key, [])); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task TestSortedSetBlockingPop(BaseClient client) + { + Assert.SkipWhen( + TestConfiguration.SERVER_VERSION < new Version("7.0.0"), + "BZMPOP is supported since 7.0.0" + ); + string key1 = $"{{testKey}}-{Guid.NewGuid()}"; + string key2 = $"{{testKey}}-{Guid.NewGuid()}"; + + // Setup test data + await client.SortedSetAddAsync(key1, [ + new("member1", 10.0), + new("member2", 20.0), + new("member3", 30.0) + ]); + + // Test single-key blocking pop with MIN order (single element) + SortedSetEntry? result = await client.SortedSetBlockingPopAsync(key1, Order.Ascending, 1.0); + Assert.NotNull(result); + Assert.Equal("member1", result.Value.Element); + Assert.Equal(10.0, result.Value.Score); + + // Test single-key blocking pop with MAX order (single element) + result = await client.SortedSetBlockingPopAsync(key1, Order.Descending, 1.0); + Assert.NotNull(result); + Assert.Equal("member3", result.Value.Element); + Assert.Equal(30.0, result.Value.Score); + + // Test single-key blocking pop with multiple elements + SortedSetEntry[] multiResult = await client.SortedSetBlockingPopAsync(key1, 1, Order.Ascending, 1.0); + Assert.Single(multiResult); + Assert.Equal("member2", multiResult[0].Element); + Assert.Equal(20.0, multiResult[0].Score); + + // Add more test data for multi-key tests + await client.SortedSetAddAsync(key2, [ + new("member4", 40.0), + new("member5", 50.0) + ]); + + // Test multi-key blocking pop with multiple elements + SortedSetPopResult popResult = await client.SortedSetBlockingPopAsync([key1, key2], 2, Order.Ascending, 1.0); + Assert.False(popResult.IsNull); + Assert.Equal(key2, popResult.Key); + Assert.Equal(2, popResult.Entries.Length); + Assert.Equal("member4", popResult.Entries[0].Element); + Assert.Equal(40.0, popResult.Entries[0].Score); + Assert.Equal("member5", popResult.Entries[1].Element); + Assert.Equal(50.0, popResult.Entries[1].Score); + + // Test timeout with empty keys + result = await client.SortedSetBlockingPopAsync(key1, Order.Ascending, 0.1); + Assert.Null(result); + + multiResult = await client.SortedSetBlockingPopAsync(key1, 1, Order.Ascending, 0.1); + Assert.Empty(multiResult); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task TestSortedSetBlockingCommands_NonExistentKeys(BaseClient client) + { + Assert.SkipWhen( + TestConfiguration.SERVER_VERSION < new Version("7.0.0"), + "BZMPOP is supported since 7.0.0" + ); + string key1 = $"{{testKey}}-{Guid.NewGuid()}"; + string key2 = $"{{testKey}}-{Guid.NewGuid()}"; + + // Test single-key blocking pop with non-existent key (should timeout) + SortedSetEntry? result = await client.SortedSetBlockingPopAsync(key1, Order.Ascending, 0.1); + Assert.Null(result); + + SortedSetEntry[] multiResult = await client.SortedSetBlockingPopAsync(key1, 1, Order.Ascending, 0.1); + Assert.Empty(multiResult); + + // Test multi-key blocking pop with non-existent keys (should timeout) + SortedSetPopResult popResult = await client.SortedSetBlockingPopAsync([key1, key2], 1, Order.Ascending, 0.1); + Assert.True(popResult.IsNull); + } } diff --git a/tests/Valkey.Glide.UnitTests/CommandTests.cs b/tests/Valkey.Glide.UnitTests/CommandTests.cs index 00fa92e..b9eda7e 100644 --- a/tests/Valkey.Glide.UnitTests/CommandTests.cs +++ b/tests/Valkey.Glide.UnitTests/CommandTests.cs @@ -3,6 +3,8 @@ using Valkey.Glide.Commands.Options; using Valkey.Glide.Internals; +using Xunit; + namespace Valkey.Glide.UnitTests; public class CommandTests @@ -306,7 +308,6 @@ public void ValidateCommandConverters() () => Assert.Equal(ValkeyType.String, Request.KeyTypeAsync("key").Converter("string")), () => Assert.Equal(ValkeyType.List, Request.KeyTypeAsync("key").Converter("list")), () => Assert.Equal(ValkeyType.Set, Request.KeyTypeAsync("key").Converter("set")), - () => Assert.Equal(ValkeyType.SortedSet, Request.KeyTypeAsync("key").Converter("zset")), () => Assert.Equal(ValkeyType.Hash, Request.KeyTypeAsync("key").Converter("hash")), () => Assert.Equal(ValkeyType.Stream, Request.KeyTypeAsync("key").Converter("stream")), () => Assert.Equal(ValkeyType.None, Request.KeyTypeAsync("key").Converter("none")), @@ -361,18 +362,6 @@ public void ValidateCommandConverters() () => Assert.Equal(5L, Request.HashLengthAsync("key").Converter(5L)), () => Assert.Equal(10L, Request.HashStringLengthAsync("key", "field").Converter(10L)), - // Sorted Set Commands - () => Assert.True(Request.SortedSetAddAsync("key", "member", 10.5).Converter(1L)), - () => Assert.False(Request.SortedSetAddAsync("key", "member", 10.5).Converter(0L)), - () => Assert.Equal(2L, Request.SortedSetAddAsync("key", [new SortedSetEntry("member1", 10.5), new SortedSetEntry("member2", 8.25)]).Converter(2L)), - () => Assert.Equal(1L, Request.SortedSetAddAsync("key", [new SortedSetEntry("member1", 10.5)]).Converter(1L)), - () => Assert.True(Request.SortedSetRemoveAsync("key", "member").Converter(1L)), - () => Assert.False(Request.SortedSetRemoveAsync("key", "member").Converter(0L)), - () => Assert.Equal(2L, Request.SortedSetRemoveAsync("key", ["member1", "member2"]).Converter(2L)), - () => Assert.Equal(5L, Request.SortedSetCardAsync("key").Converter(5L)), - () => Assert.Equal(3L, Request.SortedSetCountAsync("key", 1.0, 10.0).Converter(3L)), - () => Assert.Equal(0L, Request.SortedSetCountAsync("key").Converter(0L)), - // List Commands converters () => Assert.Equal(["key", "value"], Request.ListBlockingLeftPopAsync(["key"], TimeSpan.FromSeconds(1)).Converter([(gs)"key", (gs)"value"])), () => Assert.Null(Request.ListBlockingLeftPopAsync(["key"], TimeSpan.FromSeconds(1)).Converter(null)), @@ -567,116 +556,56 @@ public void ValidateHashCommandConverters() } [Fact] - public void ValidateSortedSetCommandArrayConverters() + public void RangeByLex_ToArgs_GeneratesCorrectArguments() { - // Test data for SortedSetRangeByRankAsync - object[] testRankArray = [ - (gs)"member1", - (gs)"member2", - (gs)"member3" - ]; + Assert.Multiple( + // Basic range + () => Assert.Equal(["[a", "[z", "BYLEX"], new RangeByLex(LexBoundary.Inclusive("a"), LexBoundary.Inclusive("z")).ToArgs()), - // Test data for SortedSetRangeByRankWithScoresAsync and SortedSetRangeByScoreWithScoresAsync - Dictionary testScoreDict = new Dictionary { - {"member1", 10.5}, - {"member2", 8.25}, - {"member3", 15.0} - }; + // Exclusive boundaries + () => Assert.Equal(["(a", "(z", "BYLEX"], new RangeByLex(LexBoundary.Exclusive("a"), LexBoundary.Exclusive("z")).ToArgs()), - Assert.Multiple( - // Test SortedSetRangeByRankAsync converter - () => - { - ValkeyValue[] result = Request.SortedSetRangeByRankAsync("key", 0, -1).Converter(testRankArray); - Assert.Equal(3, result.Length); - Assert.All(result, item => Assert.IsType(item)); - Assert.Equal("member1", result[0]); - Assert.Equal("member2", result[1]); - Assert.Equal("member3", result[2]); - }, + // Mixed boundaries + () => Assert.Equal(["[a", "(z", "BYLEX"], new RangeByLex(LexBoundary.Inclusive("a"), LexBoundary.Exclusive("z")).ToArgs()), - // Test SortedSetRangeByRankWithScoresAsync converter - () => - { - SortedSetEntry[] result = Request.SortedSetRangeByRankWithScoresAsync("key", 0, -1).Converter(testScoreDict); - Assert.Equal(3, result.Length); - Assert.All(result, entry => Assert.IsType(entry)); - Assert.Equal("member2", result[0].Element); - Assert.Equal(8.25, result[0].Score); - Assert.Equal("member1", result[1].Element); - Assert.Equal(10.5, result[1].Score); - Assert.Equal("member3", result[2].Element); - Assert.Equal(15.0, result[2].Score); + // Infinity boundaries + () => Assert.Equal(["-", "+", "BYLEX"], new RangeByLex(LexBoundary.NegativeInfinity(), LexBoundary.PositiveInfinity()).ToArgs()), - }, - // Test SortedSetRangeByScoreAsync converter - () => - { - ValkeyValue[] result = Request.SortedSetRangeByScoreAsync("key", 1.0, 20.0).Converter(testRankArray); - Assert.Equal(3, result.Length); - Assert.All(result, item => Assert.IsType(item)); - Assert.Equal("member1", result[0]); - Assert.Equal("member2", result[1]); - Assert.Equal("member3", result[2]); - }, + // With reverse + () => Assert.Equal(["[z", "[a", "BYLEX", "REV"], new RangeByLex(LexBoundary.Inclusive("a"), LexBoundary.Inclusive("z")).SetReverse().ToArgs()), - // Test SortedSetRangeByScoreWithScoresAsync converter - () => - { - SortedSetEntry[] result = Request.SortedSetRangeByScoreWithScoresAsync("key", 1.0, 20.0).Converter(testScoreDict); - Assert.Equal(3, result.Length); - Assert.All(result, entry => Assert.IsType(entry)); - // Check that entries have proper element and score values - foreach (SortedSetEntry entry in result) - { - Assert.IsType(entry.Element); - Assert.IsType(entry.Score); - } - // Validate specific values (sorted by score) - var sortedResults = result.OrderBy(e => e.Score).ToArray(); - Assert.Equal("member2", result[0].Element); - Assert.Equal(8.25, result[0].Score); - Assert.Equal("member1", result[1].Element); - Assert.Equal(10.5, result[1].Score); - Assert.Equal("member3", result[2].Element); - Assert.Equal(15.0, result[2].Score); - }, + // With limit + () => Assert.Equal(["[a", "[z", "BYLEX", "LIMIT", "10", "20"], new RangeByLex(LexBoundary.Inclusive("a"), LexBoundary.Inclusive("z")).SetLimit(10, 20).ToArgs()), - // Test SortedSetRangeByValueAsync converter - () => - { - ValkeyValue[] result = Request.SortedSetRangeByValueAsync("key", "a", "z", Exclude.None, 0, -1).Converter(testRankArray); - Assert.Equal(3, result.Length); - Assert.All(result, item => Assert.IsType(item)); - Assert.Equal("member1", result[0]); - Assert.Equal("member2", result[1]); - Assert.Equal("member3", result[2]); - }, + // With reverse and limit + () => Assert.Equal(["[z", "[a", "BYLEX", "REV", "LIMIT", "5", "15"], new RangeByLex(LexBoundary.Inclusive("a"), LexBoundary.Inclusive("z")).SetReverse().SetLimit(5, 15).ToArgs()) + ); + } - // Test SortedSetRangeByValueAsync with order converter - // Note: This test validates the converter function only, not the ordering logic. - () => - { - ValkeyValue[] result = Request.SortedSetRangeByValueAsync("key", default, default, Exclude.None, Order.Descending, 0, -1).Converter(testRankArray); - Assert.Equal(3, result.Length); - Assert.All(result, item => Assert.IsType(item)); - Assert.Equal("member1", result[0]); - Assert.Equal("member2", result[1]); - Assert.Equal("member3", result[2]); - }, + [Fact] + public void RangeByScore_ToArgs_GeneratesCorrectArguments() + { + Assert.Multiple( + // Basic range + () => Assert.Equal(["10", "20", "BYSCORE"], new RangeByScore(ScoreBoundary.Inclusive(10), ScoreBoundary.Inclusive(20)).ToArgs()), - // Test empty arrays - () => - { - ValkeyValue[] emptyResult = Request.SortedSetRangeByRankAsync("key").Converter([]); - Assert.Empty(emptyResult); - }, + // Exclusive boundaries + () => Assert.Equal(["(10", "(20", "BYSCORE"], new RangeByScore(ScoreBoundary.Exclusive(10), ScoreBoundary.Exclusive(20)).ToArgs()), - () => - { - SortedSetEntry[] emptyScoreResult = Request.SortedSetRangeByRankWithScoresAsync("key").Converter(new Dictionary()); - Assert.Empty(emptyScoreResult); - } + // Mixed boundaries + () => Assert.Equal(["10", "(20", "BYSCORE"], new RangeByScore(ScoreBoundary.Inclusive(10), ScoreBoundary.Exclusive(20)).ToArgs()), + + // Infinity boundaries + () => Assert.Equal(["-inf", "+inf", "BYSCORE"], new RangeByScore(ScoreBoundary.NegativeInfinity(), ScoreBoundary.PositiveInfinity()).ToArgs()), + + // With reverse + () => Assert.Equal(["20", "10", "BYSCORE", "REV"], new RangeByScore(ScoreBoundary.Inclusive(10), ScoreBoundary.Inclusive(20)).SetReverse().ToArgs()), + + // With limit + () => Assert.Equal(["10", "20", "BYSCORE", "LIMIT", "10", "20"], new RangeByScore(ScoreBoundary.Inclusive(10), ScoreBoundary.Inclusive(20)).SetLimit(10, 20).ToArgs()), + + // With reverse and limit + () => Assert.Equal(["20", "10", "BYSCORE", "REV", "LIMIT", "5", "15"], new RangeByScore(ScoreBoundary.Inclusive(10), ScoreBoundary.Inclusive(20)).SetReverse().SetLimit(5, 15).ToArgs()) ); } } diff --git a/tests/Valkey.Glide.UnitTests/SortedSetCommandTests.cs b/tests/Valkey.Glide.UnitTests/SortedSetCommandTests.cs index dee2d8b..955ce13 100644 --- a/tests/Valkey.Glide.UnitTests/SortedSetCommandTests.cs +++ b/tests/Valkey.Glide.UnitTests/SortedSetCommandTests.cs @@ -1,5 +1,7 @@ // Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +using System.Linq; + using Valkey.Glide.Commands.Options; using Valkey.Glide.Internals; @@ -10,7 +12,7 @@ namespace Valkey.Glide.UnitTests; public class SortedSetCommandTests { [Fact] - public void ValidateSortedSetCommandArgs() + public void SortedSetCommands_ValidateArguments() { Assert.Multiple( // SortedSetAdd - Single Member @@ -50,6 +52,138 @@ public void ValidateSortedSetCommandArgs() () => Assert.Equal(["ZREM", "key"], Request.SortedSetRemoveAsync("key", Array.Empty()).GetArgs()), () => Assert.Equal(["ZREM", "key", "", " ", "null", "0", "-1"], Request.SortedSetRemoveAsync("key", ["", " ", "null", "0", "-1"]).GetArgs()), + // SortedSetCard + () => Assert.Equal(["ZCARD", "key"], Request.SortedSetCardAsync("key").GetArgs()), + () => Assert.Equal(["ZCARD", "mykey"], Request.SortedSetCardAsync("mykey").GetArgs()), + () => Assert.Equal(["ZCARD", "test:sorted:set"], Request.SortedSetCardAsync("test:sorted:set").GetArgs()), + () => Assert.Equal(["ZCARD", ""], Request.SortedSetCardAsync("").GetArgs()), + + // SortedSetCount + () => Assert.Equal(["ZCOUNT", "key", "-inf", "+inf"], Request.SortedSetCountAsync("key").GetArgs()), + () => Assert.Equal(["ZCOUNT", "key", "1", "10"], Request.SortedSetCountAsync("key", 1.0, 10.0).GetArgs()), + () => Assert.Equal(["ZCOUNT", "key", "0", "100"], Request.SortedSetCountAsync("key", 0.0, 100.0).GetArgs()), + () => Assert.Equal(["ZCOUNT", "key", "-5", "5"], Request.SortedSetCountAsync("key", -5.0, 5.0).GetArgs()), + () => Assert.Equal(["ZCOUNT", "key", "1.5", "9.75"], Request.SortedSetCountAsync("key", 1.5, 9.75).GetArgs()), + () => Assert.Equal(["ZCOUNT", "key", "0.1", "0.9"], Request.SortedSetCountAsync("key", 0.1, 0.9).GetArgs()), + () => Assert.Equal(["ZCOUNT", "key", "-inf", "10"], Request.SortedSetCountAsync("key", double.NegativeInfinity, 10.0).GetArgs()), + () => Assert.Equal(["ZCOUNT", "key", "0", "+inf"], Request.SortedSetCountAsync("key", 0.0, double.PositiveInfinity).GetArgs()), + () => Assert.Equal(["ZCOUNT", "key", "-inf", "+inf"], Request.SortedSetCountAsync("key", double.NegativeInfinity, double.PositiveInfinity).GetArgs()), + () => Assert.Equal(["ZCOUNT", "key", "1", "10"], Request.SortedSetCountAsync("key", 1.0, 10.0, Exclude.None).GetArgs()), + () => Assert.Equal(["ZCOUNT", "key", "(1", "10"], Request.SortedSetCountAsync("key", 1.0, 10.0, Exclude.Start).GetArgs()), + () => Assert.Equal(["ZCOUNT", "key", "1", "(10"], Request.SortedSetCountAsync("key", 1.0, 10.0, Exclude.Stop).GetArgs()), + () => Assert.Equal(["ZCOUNT", "key", "(1", "(10"], Request.SortedSetCountAsync("key", 1.0, 10.0, Exclude.Both).GetArgs()), + () => Assert.Equal(["ZCOUNT", "key", "0", "0"], Request.SortedSetCountAsync("key", 0.0, 0.0).GetArgs()), + () => Assert.Equal(["ZCOUNT", "key", "(0", "(0"], Request.SortedSetCountAsync("key", 0.0, 0.0, Exclude.Both).GetArgs()), + () => Assert.Equal(["ZCOUNT", "mykey", "1", "10"], Request.SortedSetCountAsync("mykey", 1.0, 10.0).GetArgs()), + () => Assert.Equal(["ZCOUNT", "test:sorted:set", "1", "10"], Request.SortedSetCountAsync("test:sorted:set", 1.0, 10.0).GetArgs()), + + // SortedSetRangeByRank + () => Assert.Equal(["ZRANGE", "key", "0", "-1"], Request.SortedSetRangeByRankAsync("key").GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "1", "5"], Request.SortedSetRangeByRankAsync("key", 1, 5).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "-5", "-1"], Request.SortedSetRangeByRankAsync("key", -5, -1).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "0", "-1", "REV"], Request.SortedSetRangeByRankAsync("key", 0, -1, Order.Descending).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "0", "-1", "WITHSCORES"], Request.SortedSetRangeByRankWithScoresAsync("key").GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "1", "5", "WITHSCORES"], Request.SortedSetRangeByRankWithScoresAsync("key", 1, 5).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "0", "-1", "REV", "WITHSCORES"], Request.SortedSetRangeByRankWithScoresAsync("key", 0, -1, Order.Descending).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "0", "0"], Request.SortedSetRangeByRankAsync("key", 0, 0).GetArgs()), + () => Assert.Equal(["ZRANGE", "mykey", "0", "10"], Request.SortedSetRangeByRankAsync("mykey", 0, 10).GetArgs()), + + // SortedSetRangeByScore + () => Assert.Equal(["ZRANGE", "key", "-inf", "+inf", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key").GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "1", "10", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", 1.0, 10.0).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "1.5", "9.75", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", 1.5, 9.75).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "-inf", "10", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", double.NegativeInfinity, 10.0).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "(1", "10", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", 1.0, 10.0, Exclude.Start).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "1", "(10", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", 1.0, 10.0, Exclude.Stop).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "(1", "(10", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", 1.0, 10.0, Exclude.Both).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "10", "1", "BYSCORE", "REV"], Request.SortedSetRangeByScoreAsync("key", 1.0, 10.0, Exclude.None, Order.Descending).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "1", "10", "BYSCORE", "LIMIT", "2", "3"], Request.SortedSetRangeByScoreAsync("key", 1.0, 10.0, Exclude.None, Order.Ascending, 2, 3).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "(10", "(1", "BYSCORE", "REV", "LIMIT", "1", "5"], Request.SortedSetRangeByScoreAsync("key", 1.0, 10.0, Exclude.Both, Order.Descending, 1, 5).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "-inf", "+inf", "BYSCORE", "WITHSCORES"], Request.SortedSetRangeByScoreWithScoresAsync("key").GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "1", "10", "BYSCORE", "WITHSCORES"], Request.SortedSetRangeByScoreWithScoresAsync("key", 1.0, 10.0).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "10", "1", "BYSCORE", "REV", "WITHSCORES"], Request.SortedSetRangeByScoreWithScoresAsync("key", 1.0, 10.0, Exclude.None, Order.Descending).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "1", "10", "BYSCORE", "LIMIT", "2", "3", "WITHSCORES"], Request.SortedSetRangeByScoreWithScoresAsync("key", 1.0, 10.0, Exclude.None, Order.Ascending, 2, 3).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "0", "0", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", 0.0, 0.0).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "(0", "(0", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", 0.0, 0.0, Exclude.Both).GetArgs()), + + // SortedSetRangeByValue + () => Assert.Equal(["ZRANGE", "key", "[a", "[z", "BYLEX"], Request.SortedSetRangeByValueAsync("key", "a", "z", Exclude.None, 0, -1).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "[apple", "[zebra", "BYLEX"], Request.SortedSetRangeByValueAsync("key", "apple", "zebra", Exclude.None, 0, -1).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "(a", "[z", "BYLEX"], Request.SortedSetRangeByValueAsync("key", "a", "z", Exclude.Start, 0, -1).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "[a", "(z", "BYLEX"], Request.SortedSetRangeByValueAsync("key", "a", "z", Exclude.Stop, 0, -1).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "(a", "(z", "BYLEX"], Request.SortedSetRangeByValueAsync("key", "a", "z", Exclude.Both, 0, -1).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "[a", "[z", "BYLEX", "LIMIT", "2", "3"], Request.SortedSetRangeByValueAsync("key", "a", "z", Exclude.None, 2, 3).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "(a", "(z", "BYLEX", "LIMIT", "1", "5"], Request.SortedSetRangeByValueAsync("key", "a", "z", Exclude.Both, 1, 5).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "-", "+", "BYLEX"], Request.SortedSetRangeByValueAsync("key").GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "-", "+", "BYLEX"], Request.SortedSetRangeByValueAsync("key", default, default, Exclude.None, Order.Ascending, 0, -1).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "+", "-", "BYLEX", "REV"], Request.SortedSetRangeByValueAsync("key", default, default, Exclude.None, Order.Descending, 0, -1).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "-", "+", "BYLEX", "LIMIT", "2", "3"], Request.SortedSetRangeByValueAsync("key", default, default, Exclude.None, Order.Ascending, 2, 3).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "-", "+", "BYLEX"], Request.SortedSetRangeByValueAsync("key", double.NegativeInfinity, double.PositiveInfinity, Exclude.None, Order.Ascending, 0, -1).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "[a", "[a", "BYLEX"], Request.SortedSetRangeByValueAsync("key", "a", "a", Exclude.None, 0, -1).GetArgs()), + () => Assert.Equal(["ZRANGE", "key", "[", "[z", "BYLEX"], Request.SortedSetRangeByValueAsync("key", "", "z", Exclude.None, 0, -1).GetArgs()), + + // SortedSetCombine operations + () => Assert.Equal(["ZUNION", "2", "key1", "key2"], Request.SortedSetCombineAsync(SetOperation.Union, ["key1", "key2"]).GetArgs()), + () => Assert.Equal(["ZINTER", "2", "key1", "key2"], Request.SortedSetCombineAsync(SetOperation.Intersect, ["key1", "key2"]).GetArgs()), + () => Assert.Equal(["ZDIFF", "2", "key1", "key2"], Request.SortedSetCombineAsync(SetOperation.Difference, ["key1", "key2"]).GetArgs()), + () => Assert.Equal(["ZUNION", "3", "key1", "key2", "key3", "WEIGHTS", "1", "2", "3"], Request.SortedSetCombineAsync(SetOperation.Union, ["key1", "key2", "key3"], [1.0, 2.0, 3.0]).GetArgs()), + () => Assert.Equal(["ZINTER", "2", "key1", "key2", "AGGREGATE", "MAX"], Request.SortedSetCombineAsync(SetOperation.Intersect, ["key1", "key2"], null, Aggregate.Max).GetArgs()), + () => Assert.Equal(["ZUNION", "2", "key1", "key2", "WEIGHTS", "1.5", "2.5", "AGGREGATE", "MIN"], Request.SortedSetCombineAsync(SetOperation.Union, ["key1", "key2"], [1.5, 2.5], Aggregate.Min).GetArgs()), + () => Assert.Equal(["ZUNION", "2", "key1", "key2", "WITHSCORES"], Request.SortedSetCombineWithScoresAsync(SetOperation.Union, ["key1", "key2"]).GetArgs()), + () => Assert.Equal(["ZINTER", "2", "key1", "key2", "WITHSCORES"], Request.SortedSetCombineWithScoresAsync(SetOperation.Intersect, ["key1", "key2"]).GetArgs()), + () => Assert.Equal(["ZDIFF", "2", "key1", "key2", "WITHSCORES"], Request.SortedSetCombineWithScoresAsync(SetOperation.Difference, ["key1", "key2"]).GetArgs()), + () => Assert.Equal(["ZUNION", "3", "key1", "key2", "key3", "WEIGHTS", "1", "2", "3", "WITHSCORES"], Request.SortedSetCombineWithScoresAsync(SetOperation.Union, ["key1", "key2", "key3"], [1.0, 2.0, 3.0]).GetArgs()), + () => Assert.Equal(["ZINTER", "2", "key1", "key2", "AGGREGATE", "MAX", "WITHSCORES"], Request.SortedSetCombineWithScoresAsync(SetOperation.Intersect, ["key1", "key2"], null, Aggregate.Max).GetArgs()), + () => Assert.Equal(["ZUNIONSTORE", "dest", "2", "key1", "key2"], Request.SortedSetCombineAndStoreAsync(SetOperation.Union, "dest", "key1", "key2").GetArgs()), + () => Assert.Equal(["ZINTERSTORE", "dest", "2", "key1", "key2"], Request.SortedSetCombineAndStoreAsync(SetOperation.Intersect, "dest", "key1", "key2").GetArgs()), + () => Assert.Equal(["ZDIFFSTORE", "dest", "2", "key1", "key2"], Request.SortedSetCombineAndStoreAsync(SetOperation.Difference, "dest", "key1", "key2").GetArgs()), + () => Assert.Equal(["ZUNIONSTORE", "dest", "2", "key1", "key2", "AGGREGATE", "MAX"], Request.SortedSetCombineAndStoreAsync(SetOperation.Union, "dest", "key1", "key2", Aggregate.Max).GetArgs()), + () => Assert.Equal(["ZUNIONSTORE", "dest", "3", "key1", "key2", "key3"], Request.SortedSetCombineAndStoreAsync(SetOperation.Union, "dest", ["key1", "key2", "key3"]).GetArgs()), + () => Assert.Equal(["ZUNIONSTORE", "dest", "3", "key1", "key2", "key3", "WEIGHTS", "1", "2", "3"], Request.SortedSetCombineAndStoreAsync(SetOperation.Union, "dest", ["key1", "key2", "key3"], [1.0, 2.0, 3.0]).GetArgs()), + () => Assert.Equal(["ZINTERSTORE", "dest", "2", "key1", "key2", "AGGREGATE", "MIN"], Request.SortedSetCombineAndStoreAsync(SetOperation.Intersect, "dest", ["key1", "key2"], null, Aggregate.Min).GetArgs()), + + // SortedSetIncrement + () => Assert.Equal(["ZINCRBY", "key", "2.5", "member"], Request.SortedSetIncrementAsync("key", "member", 2.5).GetArgs()), + () => Assert.Equal(["ZINCRBY", "key", "-1.5", "member"], Request.SortedSetIncrementAsync("key", "member", -1.5).GetArgs()), + () => Assert.Equal(["ZINCRBY", "key", "0", "member"], Request.SortedSetIncrementAsync("key", "member", 0.0).GetArgs()), + + // SortedSetIntersectionLength + () => Assert.Equal(["ZINTERCARD", "2", "key1", "key2"], Request.SortedSetIntersectionLengthAsync(["key1", "key2"]).GetArgs()), + () => Assert.Equal(["ZINTERCARD", "3", "key1", "key2", "key3"], Request.SortedSetIntersectionLengthAsync(["key1", "key2", "key3"]).GetArgs()), + () => Assert.Equal(["ZINTERCARD", "2", "key1", "key2", "LIMIT", "10"], Request.SortedSetIntersectionLengthAsync(["key1", "key2"], 10).GetArgs()), + + // SortedSetLengthByValue + () => Assert.Equal(["ZLEXCOUNT", "key", "[a", "[z"], Request.SortedSetLengthByValueAsync("key", "a", "z").GetArgs()), + () => Assert.Equal(["ZLEXCOUNT", "key", "(a", "(z"], Request.SortedSetLengthByValueAsync("key", "a", "z", Exclude.Both).GetArgs()), + () => Assert.Equal(["ZLEXCOUNT", "key", "(a", "[z"], Request.SortedSetLengthByValueAsync("key", "a", "z", Exclude.Start).GetArgs()), + () => Assert.Equal(["ZLEXCOUNT", "key", "[a", "(z"], Request.SortedSetLengthByValueAsync("key", "a", "z", Exclude.Stop).GetArgs()), + () => Assert.Equal(["ZLEXCOUNT", "key", "[", "["], Request.SortedSetLengthByValueAsync("key", ValkeyValue.Null, ValkeyValue.Null).GetArgs()), + + // SortedSetPop + () => Assert.Equal(["ZMPOP", "2", "key1", "key2", "MIN", "COUNT", "1"], Request.SortedSetPopAsync(["key1", "key2"], 1).GetArgs()), + () => Assert.Equal(["ZMPOP", "2", "key1", "key2", "MAX", "COUNT", "3"], Request.SortedSetPopAsync(["key1", "key2"], 3, Order.Descending).GetArgs()), + () => Assert.Equal(["ZMPOP", "3", "key1", "key2", "key3", "MIN", "COUNT", "2"], Request.SortedSetPopAsync(["key1", "key2", "key3"], 2, Order.Ascending).GetArgs()), + + // SortedSetScores + () => Assert.Equal(["ZMSCORE", "key", "member1"], Request.SortedSetScoresAsync("key", ["member1"]).GetArgs()), + () => Assert.Equal(["ZMSCORE", "key", "member1", "member2", "member3"], Request.SortedSetScoresAsync("key", ["member1", "member2", "member3"]).GetArgs()), + () => Assert.Equal(["ZMSCORE", "key"], Request.SortedSetScoresAsync("key", []).GetArgs()), + + // SortedSetBlockingPopAsync - single key, single element (uses BZPOPMIN/BZPOPMAX) + () => Assert.Equal(["BZPOPMIN", "key", "5"], Request.SortedSetBlockingPopAsync("key", Order.Ascending, 5.0).GetArgs()), + () => Assert.Equal(["BZPOPMAX", "key", "0"], Request.SortedSetBlockingPopAsync("key", Order.Descending, 0.0).GetArgs()), + () => Assert.Equal(["BZPOPMIN", "key", "10.5"], Request.SortedSetBlockingPopAsync("key", Order.Ascending, 10.5).GetArgs()), + + // SortedSetBlockingPopAsync - single key, multiple elements (always uses BZPOPMIN/BZPOPMAX like SER) + () => Assert.Equal(["BZPOPMIN", "key", "5"], Request.SortedSetBlockingPopAsync("key", 3, Order.Ascending, 5.0).GetArgs()), + () => Assert.Equal(["BZPOPMAX", "key", "0"], Request.SortedSetBlockingPopAsync("key", 1, Order.Descending, 0.0).GetArgs()), + () => Assert.Equal(["BZPOPMIN", "key", "10.5"], Request.SortedSetBlockingPopAsync("key", 2, Order.Ascending, 10.5).GetArgs()), + + // SortedSetBlockingPopAsync (BZMPOP) - multi-key, multiple elements + () => Assert.Equal(["BZMPOP", "5", "2", "key1", "key2", "MIN", "COUNT", "3"], Request.SortedSetBlockingPopAsync(["key1", "key2"], 3, Order.Ascending, 5.0).GetArgs()), + () => Assert.Equal(["BZMPOP", "0", "1", "key", "MAX", "COUNT", "1"], Request.SortedSetBlockingPopAsync(["key"], 1, Order.Descending, 0.0).GetArgs()), + () => Assert.Equal(["BZMPOP", "10.5", "3", "key1", "key2", "key3", "MIN", "COUNT", "2"], Request.SortedSetBlockingPopAsync(["key1", "key2", "key3"], 2, Order.Ascending, 10.5).GetArgs()), + // Double formatting tests () => Assert.Equal("+inf", double.PositiveInfinity.ToGlideString().ToString()), () => Assert.Equal("-inf", double.NegativeInfinity.ToGlideString().ToString()), @@ -62,6 +196,286 @@ public void ValidateSortedSetCommandArgs() ); } + [Fact] + public void SortedSetCommands_ValidateConverters() + { + Assert.Multiple( + // Basic converter tests + () => Assert.True(Request.SortedSetAddAsync("key", "member", 10.5).Converter(1L)), + () => Assert.False(Request.SortedSetAddAsync("key", "member", 10.5).Converter(0L)), + () => Assert.Equal(2L, Request.SortedSetAddAsync("key", [new SortedSetEntry("member1", 10.5), new SortedSetEntry("member2", 8.25)]).Converter(2L)), + () => Assert.Equal(1L, Request.SortedSetAddAsync("key", [new SortedSetEntry("member1", 10.5)]).Converter(1L)), + () => Assert.True(Request.SortedSetRemoveAsync("key", "member").Converter(1L)), + () => Assert.False(Request.SortedSetRemoveAsync("key", "member").Converter(0L)), + () => Assert.Equal(2L, Request.SortedSetRemoveAsync("key", ["member1", "member2"]).Converter(2L)), + () => Assert.Equal(5L, Request.SortedSetCardAsync("key").Converter(5L)), + () => Assert.Equal(3L, Request.SortedSetCountAsync("key", 1.0, 10.0).Converter(3L)), + () => Assert.Equal(0L, Request.SortedSetCountAsync("key").Converter(0L)), + + // Type converter test + () => Assert.Equal(ValkeyType.SortedSet, Request.KeyTypeAsync("key").Converter("zset")) + ); + } + + [Fact] + public void SortedSetCommands_ValidateArrayConverters() + { + // Test data for SortedSetRangeByRankAsync + object[] testRankArray = [ + (GlideString)"member1", + (GlideString)"member2", + (GlideString)"member3" + ]; + + // Test data for SortedSetRangeByRankWithScoresAsync and SortedSetRangeByScoreWithScoresAsync + Dictionary testScoreDict = new Dictionary { + {"member1", 10.5}, + {"member2", 8.25}, + {"member3", 15.0} + }; + + Assert.Multiple( + // Test SortedSetRangeByRankAsync converter + () => + { + ValkeyValue[] result = Request.SortedSetRangeByRankAsync("key", 0, -1).Converter(testRankArray); + Assert.Equal(3, result.Length); + Assert.All(result, item => Assert.IsType(item)); + Assert.Equal("member1", result[0]); + Assert.Equal("member2", result[1]); + Assert.Equal("member3", result[2]); + }, + + // Test SortedSetRangeByRankWithScoresAsync converter + () => + { + SortedSetEntry[] result = Request.SortedSetRangeByRankWithScoresAsync("key", 0, -1).Converter(testScoreDict); + Assert.Equal(3, result.Length); + Assert.All(result, entry => Assert.IsType(entry)); + Assert.Equal("member2", result[0].Element); + Assert.Equal(8.25, result[0].Score); + Assert.Equal("member1", result[1].Element); + Assert.Equal(10.5, result[1].Score); + Assert.Equal("member3", result[2].Element); + Assert.Equal(15.0, result[2].Score); + }, + + // Test SortedSetRangeByScoreAsync converter + () => + { + ValkeyValue[] result = Request.SortedSetRangeByScoreAsync("key", 1.0, 20.0).Converter(testRankArray); + Assert.Equal(3, result.Length); + Assert.All(result, item => Assert.IsType(item)); + Assert.Equal("member1", result[0]); + Assert.Equal("member2", result[1]); + Assert.Equal("member3", result[2]); + }, + + // Test SortedSetRangeByScoreWithScoresAsync converter + () => + { + SortedSetEntry[] result = Request.SortedSetRangeByScoreWithScoresAsync("key", 1.0, 20.0).Converter(testScoreDict); + Assert.Equal(3, result.Length); + Assert.All(result, entry => Assert.IsType(entry)); + // Check that entries have proper element and score values + foreach (SortedSetEntry entry in result) + { + Assert.IsType(entry.Element); + Assert.IsType(entry.Score); + } + // Validate specific values (sorted by score) + var sortedResults = result.OrderBy(e => e.Score).ToArray(); + Assert.Equal("member2", result[0].Element); + Assert.Equal(8.25, result[0].Score); + Assert.Equal("member1", result[1].Element); + Assert.Equal(10.5, result[1].Score); + Assert.Equal("member3", result[2].Element); + Assert.Equal(15.0, result[2].Score); + }, + + // Test SortedSetRangeByValueAsync converter + () => + { + ValkeyValue[] result = Request.SortedSetRangeByValueAsync("key", "a", "z", Exclude.None, 0, -1).Converter(testRankArray); + Assert.Equal(3, result.Length); + Assert.All(result, item => Assert.IsType(item)); + Assert.Equal("member1", result[0]); + Assert.Equal("member2", result[1]); + Assert.Equal("member3", result[2]); + }, + + // Test SortedSetRangeByValueAsync with order converter + // Note: This test validates the converter function only, not the ordering logic. + () => + { + ValkeyValue[] result = Request.SortedSetRangeByValueAsync("key", default, default, Exclude.None, Order.Descending, 0, -1).Converter(testRankArray); + Assert.Equal(3, result.Length); + Assert.All(result, item => Assert.IsType(item)); + Assert.Equal("member1", result[0]); + Assert.Equal("member2", result[1]); + Assert.Equal("member3", result[2]); + }, + + // Test empty arrays + () => + { + ValkeyValue[] emptyResult = Request.SortedSetRangeByRankAsync("key").Converter([]); + Assert.Empty(emptyResult); + }, + + () => + { + SortedSetEntry[] emptyScoreResult = Request.SortedSetRangeByRankWithScoresAsync("key").Converter(new Dictionary()); + Assert.Empty(emptyScoreResult); + }, + + // Test SortedSetCombineWithScoresAsync converter + () => + { + SortedSetEntry[] result = Request.SortedSetCombineWithScoresAsync(SetOperation.Union, ["key1", "key2"]).Converter(testScoreDict); + Assert.Equal(3, result.Length); + Assert.All(result, entry => Assert.IsType(entry)); + // Check that entries are sorted by score + var sortedResults = result.OrderBy(e => e.Score).ToArray(); + Assert.Equal("member2", sortedResults[0].Element); + Assert.Equal(8.25, sortedResults[0].Score); + Assert.Equal("member1", sortedResults[1].Element); + Assert.Equal(10.5, sortedResults[1].Score); + Assert.Equal("member3", sortedResults[2].Element); + Assert.Equal(15.0, sortedResults[2].Score); + }, + + // Test SortedSetIncrementAsync converter + () => + { + double result = Request.SortedSetIncrementAsync("key", "member", 2.5).Converter(12.5); + Assert.Equal(12.5, result); + }, + + // Test SortedSetPopAsync converter - with result + () => + { + object[] testPopResponse = [ + (GlideString)"key1", + new Dictionary + { + { (GlideString)"member1", 10.5 }, + { (GlideString)"member2", 8.25 } + } + ]; + SortedSetPopResult result = Request.SortedSetPopAsync(["key1", "key2"], 2).Converter(testPopResponse); + Assert.False(result.IsNull); + Assert.Equal("key1", result.Key); + Assert.Equal(2, result.Entries.Length); + Assert.Equal("member1", result.Entries[0].Element); + Assert.Equal(10.5, result.Entries[0].Score); + Assert.Equal("member2", result.Entries[1].Element); + Assert.Equal(8.25, result.Entries[1].Score); + }, + + // Test SortedSetPopAsync converter - null result + () => + { + SortedSetPopResult result = Request.SortedSetPopAsync(["key1", "key2"], 2).Converter(null); + Assert.True(result.IsNull); + Assert.Equal(ValkeyKey.Null, result.Key); + Assert.Empty(result.Entries); + }, + + // Test SortedSetScoresAsync converter + () => + { + object[] testScoresResponse = [10.5, null, 8.25]; + double?[] result = Request.SortedSetScoresAsync("key", ["member1", "member2", "member3"]).Converter(testScoresResponse); + Assert.Equal(3, result.Length); + Assert.Equal(10.5, result[0]); + Assert.Null(result[1]); + Assert.Equal(8.25, result[2]); + }, + + // Test SortedSetBlockingPopAsync (single key, single element) converter + () => + { + // BZPOPMIN/BZPOPMAX returns [key, member, score] + object[] testBlockingPopResponse = [ + (GlideString)"key1", + (GlideString)"member1", + 10.5 + ]; + SortedSetEntry? result = Request.SortedSetBlockingPopAsync("key1", Order.Ascending, 5.0).Converter(testBlockingPopResponse); + Assert.NotNull(result); + Assert.Equal("member1", result.Value.Element); + Assert.Equal(10.5, result.Value.Score); + }, + + // Test SortedSetBlockingPopAsync (single key, single element) converter with null response + () => + { + SortedSetEntry? result = Request.SortedSetBlockingPopAsync("key1", Order.Ascending, 5.0).Converter(null); + Assert.Null(result); + }, + + // Test SortedSetBlockingPopAsync (single key, multiple elements) converter + () => + { + // BZPOPMIN/BZPOPMAX returns [key, member, score] - only one element + object[] testBlockingPopResponse = [ + (GlideString)"key1", + (GlideString)"member1", + 10.5 + ]; + SortedSetEntry[] result = Request.SortedSetBlockingPopAsync("key1", 2, Order.Ascending, 5.0).Converter(testBlockingPopResponse); + Assert.Single(result); + Assert.Equal("member1", result[0].Element); + Assert.Equal(10.5, result[0].Score); + }, + + // Test SortedSetBlockingPopAsync (single key, multiple elements) converter with null response + () => + { + SortedSetEntry[] result = Request.SortedSetBlockingPopAsync("key1", 2, Order.Ascending, 5.0).Converter(null); + Assert.Empty(result); + }, + + // Test SortedSetBlockingPopAsync (multi-key, multiple elements) converter + () => + { + object[] testBlockingPopResponse = [ + (GlideString)"key1", + new Dictionary + { + { (GlideString)"member1", 10.5 }, + { (GlideString)"member2", 8.25 } + } + ]; + SortedSetPopResult result = Request.SortedSetBlockingPopAsync(["key1", "key2"], 2, Order.Ascending, 5.0).Converter(testBlockingPopResponse); + Assert.False(result.IsNull); + Assert.Equal("key1", result.Key); + Assert.Equal(2, result.Entries.Length); + Assert.Equal("member1", result.Entries[0].Element); + Assert.Equal(10.5, result.Entries[0].Score); + Assert.Equal("member2", result.Entries[1].Element); + Assert.Equal(8.25, result.Entries[1].Score); + }, + + // Test SortedSetBlockingPopAsync (multi-key, multiple elements) converter with null response + () => + { + SortedSetPopResult result = Request.SortedSetBlockingPopAsync(["key1", "key2"], 2, Order.Ascending, 5.0).Converter(null); + Assert.True(result.IsNull); + Assert.Equal(ValkeyKey.Null, result.Key); + Assert.Empty(result.Entries); + }, + + // Test empty arrays + () => + { + SortedSetEntry[] emptyResult = Request.SortedSetCombineWithScoresAsync(SetOperation.Union, ["key1"]).Converter(new Dictionary()); + Assert.Empty(emptyResult); + } + ); + } + [Fact] public void RangeByLex_ToArgs_GeneratesCorrectArguments() { @@ -115,138 +529,4 @@ public void RangeByScore_ToArgs_GeneratesCorrectArguments() () => Assert.Equal(["20", "10", "BYSCORE", "REV", "LIMIT", "5", "15"], new RangeByScore(ScoreBoundary.Inclusive(10), ScoreBoundary.Inclusive(20)).SetReverse().SetLimit(5, 15).ToArgs()) ); } - - [Fact] - public void SortedSetCardAsync_ValidatesArguments() - { - Assert.Multiple( - // Basic ZCARD command - () => Assert.Equal(["ZCARD", "key"], Request.SortedSetCardAsync("key").GetArgs()), - () => Assert.Equal(["ZCARD", "mykey"], Request.SortedSetCardAsync("mykey").GetArgs()), - () => Assert.Equal(["ZCARD", "test:sorted:set"], Request.SortedSetCardAsync("test:sorted:set").GetArgs()), - - // Empty key should work - () => Assert.Equal(["ZCARD", ""], Request.SortedSetCardAsync("").GetArgs()) - ); - } - - [Fact] - public void SortedSetCountAsync_ValidatesArguments() - { - Assert.Multiple( - // Default parameters (negative infinity to positive infinity) - () => Assert.Equal(["ZCOUNT", "key", "-inf", "+inf"], Request.SortedSetCountAsync("key").GetArgs()), - - // Specific score ranges - () => Assert.Equal(["ZCOUNT", "key", "1", "10"], Request.SortedSetCountAsync("key", 1.0, 10.0).GetArgs()), - () => Assert.Equal(["ZCOUNT", "key", "0", "100"], Request.SortedSetCountAsync("key", 0.0, 100.0).GetArgs()), - () => Assert.Equal(["ZCOUNT", "key", "-5", "5"], Request.SortedSetCountAsync("key", -5.0, 5.0).GetArgs()), - - // Decimal scores - () => Assert.Equal(["ZCOUNT", "key", "1.5", "9.75"], Request.SortedSetCountAsync("key", 1.5, 9.75).GetArgs()), - () => Assert.Equal(["ZCOUNT", "key", "0.1", "0.9"], Request.SortedSetCountAsync("key", 0.1, 0.9).GetArgs()), - - // Infinity values - () => Assert.Equal(["ZCOUNT", "key", "-inf", "10"], Request.SortedSetCountAsync("key", double.NegativeInfinity, 10.0).GetArgs()), - () => Assert.Equal(["ZCOUNT", "key", "0", "+inf"], Request.SortedSetCountAsync("key", 0.0, double.PositiveInfinity).GetArgs()), - () => Assert.Equal(["ZCOUNT", "key", "-inf", "+inf"], Request.SortedSetCountAsync("key", double.NegativeInfinity, double.PositiveInfinity).GetArgs()), - - // Exclude options - () => Assert.Equal(["ZCOUNT", "key", "1", "10"], Request.SortedSetCountAsync("key", 1.0, 10.0, Exclude.None).GetArgs()), - () => Assert.Equal(["ZCOUNT", "key", "(1", "10"], Request.SortedSetCountAsync("key", 1.0, 10.0, Exclude.Start).GetArgs()), - () => Assert.Equal(["ZCOUNT", "key", "1", "(10"], Request.SortedSetCountAsync("key", 1.0, 10.0, Exclude.Stop).GetArgs()), - () => Assert.Equal(["ZCOUNT", "key", "(1", "(10"], Request.SortedSetCountAsync("key", 1.0, 10.0, Exclude.Both).GetArgs()), - - // Edge cases - () => Assert.Equal(["ZCOUNT", "key", "0", "0"], Request.SortedSetCountAsync("key", 0.0, 0.0).GetArgs()), - () => Assert.Equal(["ZCOUNT", "key", "(0", "(0"], Request.SortedSetCountAsync("key", 0.0, 0.0, Exclude.Both).GetArgs()), - - // Different key names - () => Assert.Equal(["ZCOUNT", "mykey", "1", "10"], Request.SortedSetCountAsync("mykey", 1.0, 10.0).GetArgs()), - () => Assert.Equal(["ZCOUNT", "test:sorted:set", "1", "10"], Request.SortedSetCountAsync("test:sorted:set", 1.0, 10.0).GetArgs()) - ); - } - - [Fact] - public void SortedSetRangeByRank_ValidatesArguments() - { - Assert.Multiple( - // Basic functionality - () => Assert.Equal(["ZRANGE", "key", "0", "-1"], Request.SortedSetRangeByRankAsync("key").GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "1", "5"], Request.SortedSetRangeByRankAsync("key", 1, 5).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "-5", "-1"], Request.SortedSetRangeByRankAsync("key", -5, -1).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "0", "-1", "REV"], Request.SortedSetRangeByRankAsync("key", 0, -1, Order.Descending).GetArgs()), - - // With scores - () => Assert.Equal(["ZRANGE", "key", "0", "-1", "WITHSCORES"], Request.SortedSetRangeByRankWithScoresAsync("key").GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "1", "5", "WITHSCORES"], Request.SortedSetRangeByRankWithScoresAsync("key", 1, 5).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "0", "-1", "REV", "WITHSCORES"], Request.SortedSetRangeByRankWithScoresAsync("key", 0, -1, Order.Descending).GetArgs()), - - // Edge cases - () => Assert.Equal(["ZRANGE", "key", "0", "0"], Request.SortedSetRangeByRankAsync("key", 0, 0).GetArgs()), - () => Assert.Equal(["ZRANGE", "mykey", "0", "10"], Request.SortedSetRangeByRankAsync("mykey", 0, 10).GetArgs()) - ); - } - - [Fact] - public void SortedSetRangeByScore_ValidatesArguments() - { - Assert.Multiple( - // Basic functionality - () => Assert.Equal(["ZRANGE", "key", "-inf", "+inf", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key").GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "1", "10", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", 1.0, 10.0).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "1.5", "9.75", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", 1.5, 9.75).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "-inf", "10", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", double.NegativeInfinity, 10.0).GetArgs()), - - // Exclude options - () => Assert.Equal(["ZRANGE", "key", "(1", "10", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", 1.0, 10.0, Exclude.Start).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "1", "(10", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", 1.0, 10.0, Exclude.Stop).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "(1", "(10", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", 1.0, 10.0, Exclude.Both).GetArgs()), - - // Order and limit - () => Assert.Equal(["ZRANGE", "key", "10", "1", "BYSCORE", "REV"], Request.SortedSetRangeByScoreAsync("key", 1.0, 10.0, Exclude.None, Order.Descending).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "1", "10", "BYSCORE", "LIMIT", "2", "3"], Request.SortedSetRangeByScoreAsync("key", 1.0, 10.0, Exclude.None, Order.Ascending, 2, 3).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "(10", "(1", "BYSCORE", "REV", "LIMIT", "1", "5"], Request.SortedSetRangeByScoreAsync("key", 1.0, 10.0, Exclude.Both, Order.Descending, 1, 5).GetArgs()), - - // With scores - () => Assert.Equal(["ZRANGE", "key", "-inf", "+inf", "BYSCORE", "WITHSCORES"], Request.SortedSetRangeByScoreWithScoresAsync("key").GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "1", "10", "BYSCORE", "WITHSCORES"], Request.SortedSetRangeByScoreWithScoresAsync("key", 1.0, 10.0).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "10", "1", "BYSCORE", "REV", "WITHSCORES"], Request.SortedSetRangeByScoreWithScoresAsync("key", 1.0, 10.0, Exclude.None, Order.Descending).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "1", "10", "BYSCORE", "LIMIT", "2", "3", "WITHSCORES"], Request.SortedSetRangeByScoreWithScoresAsync("key", 1.0, 10.0, Exclude.None, Order.Ascending, 2, 3).GetArgs()), - - // Edge cases - () => Assert.Equal(["ZRANGE", "key", "0", "0", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", 0.0, 0.0).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "(0", "(0", "BYSCORE"], Request.SortedSetRangeByScoreAsync("key", 0.0, 0.0, Exclude.Both).GetArgs()) - ); - } - - [Fact] - public void SortedSetRangeByValue_ValidatesArguments() - { - Assert.Multiple( - // Basic lexicographical ranges with explicit min/max - () => Assert.Equal(["ZRANGE", "key", "[a", "[z", "BYLEX"], Request.SortedSetRangeByValueAsync("key", "a", "z", Exclude.None, 0, -1).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "[apple", "[zebra", "BYLEX"], Request.SortedSetRangeByValueAsync("key", "apple", "zebra", Exclude.None, 0, -1).GetArgs()), - - // Exclude options with explicit min/max - () => Assert.Equal(["ZRANGE", "key", "(a", "[z", "BYLEX"], Request.SortedSetRangeByValueAsync("key", "a", "z", Exclude.Start, 0, -1).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "[a", "(z", "BYLEX"], Request.SortedSetRangeByValueAsync("key", "a", "z", Exclude.Stop, 0, -1).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "(a", "(z", "BYLEX"], Request.SortedSetRangeByValueAsync("key", "a", "z", Exclude.Both, 0, -1).GetArgs()), - - // Skip and take parameters with explicit min/max - () => Assert.Equal(["ZRANGE", "key", "[a", "[z", "BYLEX", "LIMIT", "2", "3"], Request.SortedSetRangeByValueAsync("key", "a", "z", Exclude.None, 2, 3).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "(a", "(z", "BYLEX", "LIMIT", "1", "5"], Request.SortedSetRangeByValueAsync("key", "a", "z", Exclude.Both, 1, 5).GetArgs()), - - // Default parameters (should use lexicographical infinity symbols) - () => Assert.Equal(["ZRANGE", "key", "-", "+", "BYLEX"], Request.SortedSetRangeByValueAsync("key").GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "-", "+", "BYLEX"], Request.SortedSetRangeByValueAsync("key", default, default, Exclude.None, Order.Ascending, 0, -1).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "+", "-", "BYLEX", "REV"], Request.SortedSetRangeByValueAsync("key", default, default, Exclude.None, Order.Descending, 0, -1).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "-", "+", "BYLEX", "LIMIT", "2", "3"], Request.SortedSetRangeByValueAsync("key", default, default, Exclude.None, Order.Ascending, 2, 3).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "-", "+", "BYLEX"], Request.SortedSetRangeByValueAsync("key", double.NegativeInfinity, double.PositiveInfinity, Exclude.None, Order.Ascending, 0, -1).GetArgs()), - - // Edge cases - () => Assert.Equal(["ZRANGE", "key", "[a", "[a", "BYLEX"], Request.SortedSetRangeByValueAsync("key", "a", "a", Exclude.None, 0, -1).GetArgs()), - () => Assert.Equal(["ZRANGE", "key", "[", "[z", "BYLEX"], Request.SortedSetRangeByValueAsync("key", "", "z", Exclude.None, 0, -1).GetArgs()) - ); - } }