diff --git a/Sources/NIORedis/Commands/SortedSetCommands.swift b/Sources/NIORedis/Commands/SortedSetCommands.swift new file mode 100644 index 0000000..c0f279b --- /dev/null +++ b/Sources/NIORedis/Commands/SortedSetCommands.swift @@ -0,0 +1,603 @@ +import NIO + +// MARK: Static Helpers + +extension RedisCommandExecutor { + @usableFromInline + static func _mapSortedSetResponse(_ response: [RESPValue], scoreIsFirst: Bool) throws -> [(RESPValue, Double)] { + guard response.count > 0 else { return [] } + + var result: [(RESPValue, Double)] = [] + + var index = 0 + repeat { + let scoreItem = response[scoreIsFirst ? index : index + 1] + + guard let score = Double(scoreItem) else { + throw RedisError(identifier: #function, reason: "Unexpected response \"\(scoreItem)\"") + } + + let memberIndex = scoreIsFirst ? index + 1 : index + result.append((response[memberIndex], score)) + + index += 2 + } while (index < response.count) + + return result + } +} + +// MARK: General + +extension RedisCommandExecutor { + /// Adds elements to a sorted set, assigning their score to the values provided. + /// + /// See [https://redis.io/commands/zadd](https://redis.io/commands/zadd) + /// - Parameters: + /// - items: A list of elements and their score to add to the sorted set. + /// - to: The key of the sorted set. + /// - options: A set of options defined by Redis for this command to execute under. + /// - Returns: The number of elements added to the sorted set. + @inlinable + public func zadd( + _ items: [(element: RESPValueConvertible, score: Double)], + to key: String, + options: Set = []) -> EventLoopFuture + { + guard !options.contains("INCR") else { + return eventLoop.makeFailedFuture(RedisError(identifier: #function, reason: "INCR option is unsupported. Use zincrby(_:member:by:) instead.")) + } + + assert(options.count <= 2, "Invalid number of options provided.") + assert(options.allSatisfy(["XX", "NX", "CH"].contains), "Unsupported option provided!") + assert( + !(options.contains("XX") && options.contains("NX")), + "XX and NX options are mutually exclusive." + ) + + var args: [RESPValueConvertible] = [key] + options.map { $0 } + + for (element, score) in items { + switch score { + case .infinity: args.append("+inf") + case -.infinity: args.append("-inf") + default: args.append(score) + } + + args.append(element) + } + + return send(command: "ZADD", with: args) + .mapFromRESP() + } + + /// Adds an element to a sorted set, assigning their score to the value provided. + /// + /// See [https://redis.io/commands/zadd](https://redis.io/commands/zadd) + /// - Parameters: + /// - item: The element and its score to add to the sorted set. + /// - to: The key of the sorted set. + /// - options: A set of options defined by Redis for this command to execute under. + /// - Returns: `true` if the element was added or score was updated in the sorted set. + @inlinable + public func zadd( + _ item: (element: RESPValueConvertible, score: Double), + to key: String, + options: Set = []) -> EventLoopFuture + { + return zadd([item], to: key, options: options) + .map { return $0 == 1 } + } + + /// Returns the number of elements in a sorted set. + /// + /// See [https://redis.io/commands/zcard](https://redis.io/commands/zcard) + /// - Parameter of: The key of the sorted set. + /// - Returns: The number of elements in the sorted set. + @inlinable + public func zcard(of key: String) -> EventLoopFuture { + return send(command: "ZCARD", with: [key]) + .mapFromRESP() + } + + /// Returns the score of the specified member in a stored set. + /// + /// See [https://redis.io/commands/zscore](https://redis.io/commands/zscore) + /// - Parameters: + /// - of: The element in the sorted set to get the score for. + /// - storedAt: The key of the sorted set. + /// - Returns: The score of the element provided. + @inlinable + public func zscore(of member: RESPValueConvertible, storedAt key: String) -> EventLoopFuture { + return send(command: "ZSCORE", with: [key, member]) + .map { return Double($0) } + } + + /// Incrementally iterates over all fields in a sorted set. + /// + /// See [https://redis.io/commands/zscan](https://redis.io/commands/zscan) + /// - Parameters: + /// - key: The key identifying the sorted set. + /// - startingFrom: The position to start the scan from. + /// - count: The number of elements to advance by. Redis default is 10. + /// - matching: A glob-style pattern to filter values to be selected from the result set. + /// - Returns: A cursor position for additional invocations with a limited collection of values and their scores. + @inlinable + public func zscan( + _ key: String, + startingFrom position: Int = 0, + count: Int? = nil, + matching match: String? = nil) -> EventLoopFuture<(Int, [(RESPValue, Double)])> + { + return _scan(command: "ZSCAN", resultType: [RESPValue].self, key, position, count, match) + .flatMapThrowing { + let values = try Self._mapSortedSetResponse($0.1, scoreIsFirst: false) + return ($0.0, values) + } + } +} + +// MARK: Rank + +extension RedisCommandExecutor { + /// Returns the rank (index) of the specified element in a sorted set. + /// - Note: This treats the ordered set as ordered from low to high. + /// For the inverse, see `zrevrank(of:storedAt:)`. + /// + /// See [https://redis.io/commands/zrank](https://redis.io/commands/zrank) + /// - Parameters: + /// - of: The element in the sorted set to search for. + /// - storedAt: The key of the sorted set to search. + /// - Returns: The index of the element, or `nil` if the key was not found. + @inlinable + public func zrank(of member: RESPValueConvertible, storedAt key: String) -> EventLoopFuture { + return send(command: "ZRANK", with: [key, member]) + .mapFromRESP() + } + + /// Returns the rank (index) of the specified element in a sorted set. + /// - Note: This treats the ordered set as ordered from high to low. + /// For the inverse, see `zrank(of:storedAt:)`. + /// + /// See [https://redis.io/commands/zrevrank](https://redis.io/commands/zrevrank) + /// - Parameters: + /// - of: The element in the sorted set to search for. + /// - storedAt: The key of the sorted set to search. + /// - Returns: The index of the element, or `nil` if the key was not found. + @inlinable + public func zrevrank(of member: RESPValueConvertible, storedAt key: String) -> EventLoopFuture { + return send(command: "ZREVRANK", with: [key, member]) + .mapFromRESP() + } +} + +// MARK: Count + +extension RedisCommandExecutor { + /// Returns the number of elements in a sorted set with a score within the range specified. + /// + /// See [https://redis.io/commands/zcount](https://redis.io/commands/zcount) + /// - Parameters: + /// - of: The key of the sorted set to count. + /// - within: The min and max range of scores to filter for. + /// - Returns: The number of elements in the sorted set that fit within the score range. + @inlinable + public func zcount(of key: String, within range: (min: String, max: String)) -> EventLoopFuture { + return send(command: "ZCOUNT", with: [key, range.min, range.max]) + .mapFromRESP() + } + + /// Returns the number of elements in a sorted set whose lexiographical values are between the range specified. + /// - Important: This assumes all elements in the sorted set have the same score. If not, the returned elements are unspecified. + /// + /// See [https://redis.io/commands/zlexcount](https://redis.io/commands/zlexcount) + /// - Parameters: + /// - of: The key of the sorted set to count. + /// - within: The min and max range of values to filter for. + /// - Returns: The number of elements in the sorted set that fit within the value range. + @inlinable + public func zlexcount(of key: String, within range: (min: String, max: String)) -> EventLoopFuture { + return send(command: "ZLEXCOUNT", with: [key, range.min, range.max]) + .mapFromRESP() + } +} + +// MARK: Pop + +extension RedisCommandExecutor { + /// Removes members from a sorted set with the lowest scores. + /// + /// See [https://redis.io/commands/zpopmin](https://redis.io/commands/zpopmin) + /// - Parameters: + /// - count: The max number of elements to pop from the set. + /// - from: The key identifying the sorted set in Redis. + /// - Returns: A list of members popped from the sorted set with their associated score. + @inlinable + public func zpopmin(_ count: Int, from key: String) -> EventLoopFuture<[(RESPValue, Double)]> { + return _zpop(command: "ZPOPMIN", count, key) + } + + /// Removes a member from a sorted set with the lowest score. + /// + /// See [https://redis.io/commands/zpopmin](https://redis.io/commands/zpopmin) + /// - Parameters: + /// - from: The key identifying the sorted set in Redis. + /// - Returns: The element and its associated score that was popped from the sorted set, or `nil` if set was empty. + @inlinable + public func zpopmin(from key: String) -> EventLoopFuture<(RESPValue, Double)?> { + return _zpop(command: "ZPOPMIN", nil, key) + .map { return $0.count > 0 ? $0[0] : nil } + } + + /// Removes members from a sorted set with the highest scores. + /// + /// See [https://redis.io/commands/zpopmax](https://redis.io/commands/zpopmax) + /// - Parameters: + /// - count: The max number of elements to pop from the set. + /// - from: The key identifying the sorted set in Redis. + /// - Returns: A list of members popped from the sorted set with their associated score. + @inlinable + public func zpopmax(_ count: Int, from key: String) -> EventLoopFuture<[(RESPValue, Double)]> { + return _zpop(command: "ZPOPMAX", count, key) + } + + /// Removes a member from a sorted set with the highest score. + /// + /// See [https://redis.io/commands/zpopmax](https://redis.io/commands/zpopmax) + /// - Parameters: + /// - from: The key identifying the sorted set in Redis. + /// - Returns: The element and its associated score that was popped from the sorted set, or `nil` if set was empty. + @inlinable + public func zpopmax(from key: String) -> EventLoopFuture<(RESPValue, Double)?> { + return _zpop(command: "ZPOPMAX", nil, key) + .map { return $0.count > 0 ? $0[0] : nil } + } + + @usableFromInline + func _zpop(command: String, _ count: Int?, _ key: String) -> EventLoopFuture<[(RESPValue, Double)]> { + var args: [RESPValueConvertible] = [key] + + if let c = count { args.append(c) } + + return send(command: command, with: args) + .mapFromRESP(to: [RESPValue].self) + .flatMapThrowing { return try Self._mapSortedSetResponse($0, scoreIsFirst: true) } + } +} + +// MARK: Increment + +extension RedisCommandExecutor { + /// Increments the score of the specified member in a sorted set. + /// + /// See [https://redis.io/commands/zincrby](https://redis.io/commands/zincrby) + /// - Parameters: + /// - key: The key of the sorted set. + /// - member: The element to increment. + /// - by: The amount to increment this element's score by. + /// - Returns: The new score of the member. + @inlinable + public func zincrby(_ key: String, member: RESPValueConvertible, by amount: Int) -> EventLoopFuture { + return send(command: "ZINCRBY", with: [key, amount, member]) + .mapFromRESP() + } + + /// Increments the score of the specified member in a sorted set. + /// + /// See [https://redis.io/commands/zincrby](https://redis.io/commands/zincrby) + /// - Parameters: + /// - key: The key of the sorted set. + /// - member: The element to increment. + /// - by: The amount to increment this element's score by. + /// - Returns: The new score of the member. + @inlinable + public func zincrby(_ key: String, member: RESPValueConvertible, by amount: Double) -> EventLoopFuture { + return send(command: "ZINCRBY", with: [key, amount, member]) + .mapFromRESP() + } +} + +// MARK: Intersect and Union + +extension RedisCommandExecutor { + /// Computes a new sorted set as a union between all provided source sorted sets and stores the result at the key desired. + /// - Note: This operation overwrites any value stored at the destination key. + /// + /// See [https://redis.io/commands/zunionstore](https://redis.io/commands/zunionstore) + /// - Parameters: + /// - sources: The list of sorted set keys to treat as the source of the union. + /// - to: The key to store the union sorted set at. + /// - weights: The multiplying factor to apply to the corresponding `sources` key based on index of the two parameters. + /// - aggregateMethod: The method of aggregating the values of the union. Supported values are "SUM", "MIN", and "MAX". + /// - Returns: The number of members in the new sorted set. + @inlinable + public func zunionstore( + _ sources: [String], + to destination: String, + weights: [Int]? = nil, + aggregateMethod aggregate: String? = nil) -> EventLoopFuture + { + return _zopstore(command: "ZUNIONSTORE", sources, destination, weights, aggregate) + } + + /// Computes a new sorted set as an intersection between all provided source sorted sets and stores the result at the key desired. + /// - Note: This operation overwrites any value stored at the destination key. + /// + /// See [https://redis.io/commands/zinterstore](https://redis.io/commands/zinterstore) + /// - Parameters: + /// - sources: The list of sorted set keys to treat as the source of the intersection. + /// - to: The key to store the intersected sorted set at. + /// - weights: The multiplying factor to apply to the corresponding `sources` key based on index of the two parameters. + /// - aggregateMethod: The method of aggregating the values of the intersection. Supported values are "SUM", "MIN", and "MAX". + /// - Returns: The number of members in the new sorted set. + @inlinable + public func zinterstore( + _ sources: [String], + to destination: String, + weights: [Int]? = nil, + aggregateMethod aggregate: String? = nil) -> EventLoopFuture + { + return _zopstore(command: "ZINTERSTORE", sources, destination, weights, aggregate) + } + + @usableFromInline + func _zopstore( + command: String, + _ sources: [String], + _ destination: String, + _ weights: [Int]?, + _ aggregate: String?) -> EventLoopFuture + { + assert(sources.count > 0, "At least 1 source key should be provided.") + + var args: [RESPValueConvertible] = [destination, sources.count] + sources + + if let w = weights { + assert(w.count > 0, "When passing a value for 'weights', at least 1 value should be provided.") + assert(w.count <= sources.count, "Weights should be no larger than the amount of source keys.") + + args.append("WEIGHTS") + args.append(contentsOf: w) + } + + if let a = aggregate { + assert(a == "SUM" || a == "MIN" || a == "MAX", "Aggregate method provided is unsupported.") + + args.append("AGGREGATE") + args.append(a) + } + + return send(command: command, with: args) + .mapFromRESP() + } +} + +// MARK: Range + +extension RedisCommandExecutor { + /// Returns the specified range of elements in a sorted set. + /// - Note: This treats the ordered set as ordered from low to high. + /// For the inverse, see `zrevrange(of:startIndex:endIndex:withScores:)`. + /// + /// See [https://redis.io/commands/zrange](https://redis.io/commands/zrange) + /// - Parameters: + /// - withinIndices: The start and stop 0-based indices of the range of elements to include. + /// - from: The key of the sorted set to search. + /// - withScores: Should the list contain the items AND their scores? [Item_1, Score_1, Item_2, ...] + /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores. + @inlinable + public func zrange( + withinIndices range: (start: Int, stop: Int), + from key: String, + withScores: Bool = false) -> EventLoopFuture<[RESPValue]> + { + return _zrange(command: "ZRANGE", key, range.start, range.stop, withScores) + } + + /// Returns the specified range of elements in a sorted set. + /// - Note: This treats the ordered set as ordered from high to low. + /// For the inverse, see `zrange(of:startIndex:endIndex:withScores:)`. + /// + /// See [https://redis.io/commands/zrevrange](https://redis.io/commands/zrevrange) + /// - Parameters: + /// - withinIndices: The start and stop 0-based indices of the range of elements to include. + /// - from: The key of the sorted set to search. + /// - withScores: Should the list contain the items AND their scores? [Item_1, Score_1, Item_2, ...] + /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores. + @inlinable + public func zrevrange( + withinIndices range: (start: Int, stop: Int), + from key: String, + withScores: Bool = false) -> EventLoopFuture<[RESPValue]> + { + return _zrange(command: "ZREVRANGE", key, range.start, range.stop, withScores) + } + + @usableFromInline + func _zrange(command: String, _ key: String, _ start: Int, _ stop: Int, _ withScores: Bool) -> EventLoopFuture<[RESPValue]> { + var args: [RESPValueConvertible] = [key, start, stop] + + if withScores { args.append("WITHSCORES") } + + return send(command: command, with: args) + .mapFromRESP() + } +} + +// MARK: Range by Score + +extension RedisCommandExecutor { + /// Returns elements from a sorted set whose score fits within the range specified. + /// - Note: This treats the ordered set as ordered from low to high. + /// For the inverse, see `zrevrangebyscore(of:within:withScores:limitBy:)`. + /// + /// See [https://redis.io/commands/zrangebyscore](https://redis.io/commands/zrangebyscore) + /// - Parameters: + /// - within: The range of min and max scores to filter elements by. + /// - from: The key of the sorted set to search. + /// - withScores: Should the list contain the items AND their scores? [Item_1, Score_1, Item_2, ...] + /// - limitBy: The optional offset and count of items to query. + /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores. + @inlinable + public func zrangebyscore( + within range: (min: String, max: String), + from key: String, + withScores: Bool = false, + limitBy limit: (offset: Int, count: Int)? = nil) -> EventLoopFuture<[RESPValue]> + { + return _zrangebyscore(command: "ZRANGEBYSCORE", key, range, withScores, limit) + } + + /// Returns elements from a sorted set whose score fits within the range specified. + /// - Note: This treats the ordered set as ordered from high to low. + /// For the inverse, see `zrangebyscore(of:within:withScores:limitBy:)`. + /// + /// See [https://redis.io/commands/zrevrangebyscore](https://redis.io/commands/zrevrangebyscore) + /// - Parameters: + /// - within: The range of min and max scores to filter elements by. + /// - from: The key of the sorted set to search. + /// - withScores: Should the list contain the items AND their scores? [Item_1, Score_1, Item_2, ...] + /// - limitBy: The optional offset and count of items to query. + /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores. + @inlinable + public func zrevrangebyscore( + within range: (min: String, max: String), + from key: String, + withScores: Bool = false, + limitBy limit: (offset: Int, count: Int)? = nil) -> EventLoopFuture<[RESPValue]> + { + return _zrangebyscore(command: "ZREVRANGEBYSCORE", key, (range.max, range.min), withScores, limit) + } + + @usableFromInline + func _zrangebyscore(command: String, _ key: String, _ range: (min: String, max: String), _ withScores: Bool, _ limit: (offset: Int, count: Int)?) -> EventLoopFuture<[RESPValue]> { + var args: [RESPValueConvertible] = [key, range.min, range.max] + + if withScores { args.append("WITHSCORES") } + + if let l = limit { + args.append("LIMIT") + args.append([l.offset, l.count]) + } + + return send(command: command, with: args) + .mapFromRESP() + } +} + +// MARK: Range by Lexiographical + +extension RedisCommandExecutor { + /// Returns elements from a sorted set whose lexiographical values are between the range specified. + /// - Important: This assumes all elements in the sorted set have the same score. If not, the returned elements are unspecified. + /// - Note: This treats the ordered set as ordered from low to high. + /// For the inverse, see `zrevrangebylex(of:within:limitBy:)`. + /// + /// See [https://redis.io/commands/zrangebylex](https://redis.io/commands/zrangebylex) + /// - Parameters: + /// - within: The value range to filter elements by. + /// - from: The key of the sorted set to search. + /// - limitBy: The optional offset and count of items to query. + /// - Returns: A list of elements from the sorted set that were within the range provided. + @inlinable + public func zrangebylex( + within range: (min: String, max: String), + from key: String, + limitBy limit: (offset: Int, count: Int)? = nil) -> EventLoopFuture<[RESPValue]> + { + return _zrangebylex(command: "ZRANGEBYLEX", key, range, limit) + } + + /// Returns elements from a sorted set whose lexiographical values are between the range specified. + /// - Important: This assumes all elements in the sorted set have the same score. If not, the returned elements are unspecified. + /// - Note: This treats the ordered set as ordered from high to low. + /// For the inverse, see `zrangebylex(of:within:limitBy:)`. + /// + /// See [https://redis.io/commands/zrevrangebylex](https://redis.io/commands/zrevrangebylex) + /// - Parameters: + /// - within: The value range to filter elements by. + /// - from: The key of the sorted set to search. + /// - limitBy: The optional offset and count of items to query. + /// - Returns: A list of elements from the sorted set that were within the range provided. + @inlinable + public func zrevrangebylex( + within range: (min: String, max: String), + from key: String, + limitBy limit: (offset: Int, count: Int)? = nil) -> EventLoopFuture<[RESPValue]> + { + return _zrangebylex(command: "ZREVRANGEBYLEX", key, (range.max, range.min), limit) + } + + @usableFromInline + func _zrangebylex(command: String, _ key: String, _ range: (min: String, max: String), _ limit: (offset: Int, count: Int)?) -> EventLoopFuture<[RESPValue]> { + var args: [RESPValueConvertible] = [key, range.min, range.max] + + if let l = limit { + args.append("LIMIT") + args.append(contentsOf: [l.offset, l.count]) + } + + return send(command: command, with: args) + .mapFromRESP() + } +} + +// MARK: Remove + +extension RedisCommandExecutor { + /// Removes the specified items from a sorted set. + /// + /// See [https://redis.io/commands/zrem](https://redis.io/commands/zrem) + /// - Parameters: + /// - items: The values to remove from the sorted set. + /// - from: The key of the sorted set. + /// - Returns: The number of items removed from the set. + @inlinable + public func zrem(_ items: [RESPValueConvertible], from key: String) -> EventLoopFuture { + assert(items.count > 0, "At least 1 item should be provided.") + + return send(command: "ZREM", with: [key] + items) + .mapFromRESP() + } + + /// Removes elements from a sorted set whose lexiographical values are between the range specified. + /// - Important: This assumes all elements in the sorted set have the same score. If not, the elements selected are unspecified. + /// + /// See [https://redis.io/commands/zremrangebylex](https://redis.io/commands/zremrangebylex) + /// - Parameters: + /// - within: The value range to filter for elements to remove. + /// - from: The key of the sorted set to search. + /// - Returns: The number of elements removed from the sorted set. + @inlinable + public func zremrangebylex(within range: (min: String, max: String), from key: String) -> EventLoopFuture { + return send(command: "ZREMRANGEBYLEX", with: [key, range.min, range.max]) + .mapFromRESP() + } + + /// Removes elements from a sorted set whose index is between the provided range. + /// + /// See [https://redis.io/commands/zremrangebyrank](https://redis.io/commands/zremrangebyrank) + /// - Parameters: + /// - startingFrom: The starting index of the range. + /// - endingAt: The ending index of the range. + /// - from: The key of the sorted set to search. + /// - Returns: The number of elements removed from the sorted set. + @inlinable + public func zremrangebyrank(startingFrom start: Int, endingAt stop: Int, from key: String) -> EventLoopFuture { + return send(command: "ZREMRANGEBYRANK", with: [key, start, stop]) + .mapFromRESP() + } + + /// Removes elements from a sorted set whose score is within the range specified. + /// + /// See [https://redis.io/commands/zremrangebyscore](https://redis.io/commands/zremrangebyscore) + /// - Parameters: + /// - within: The score range to filter for elements to remove. + /// - from: The key of the sorted set to search. + /// - Returns: The number of elements removed from the sorted set. + @inlinable + public func zremrangebyscore(within range: (min: String, max: String), from key: String) -> EventLoopFuture { + return send(command: "ZREMRANGEBYSCORE", with: [key, range.min, range.max]) + .mapFromRESP() + } +} diff --git a/Tests/NIORedisTests/Commands/SortedSetCommandsTests.swift b/Tests/NIORedisTests/Commands/SortedSetCommandsTests.swift new file mode 100644 index 0000000..9dd11ca --- /dev/null +++ b/Tests/NIORedisTests/Commands/SortedSetCommandsTests.swift @@ -0,0 +1,358 @@ +@testable import NIORedis +import XCTest + +final class SortedSetCommandsTests: XCTestCase { + private static let testKey = "SortedSetCommandsTests" + + private let redis = RedisDriver(ownershipModel: .internal(threadCount: 1)) + deinit { try? redis.terminate() } + + private var connection: RedisConnection! + private var key: String { return SortedSetCommandsTests.testKey } + + override func setUp() { + do { + connection = try redis.makeConnection().wait() + + var dataset: [(RESPValueConvertible, Double)] = [] + for index in 1...10 { + dataset.append((index, Double(index))) + } + + _ = try connection.zadd(dataset, to: SortedSetCommandsTests.testKey).wait() + } catch { + XCTFail("Failed to create NIORedisConnection!") + } + } + + override func tearDown() { + _ = try? connection.send(command: "FLUSHALL").wait() + connection.close() + connection = nil + } + + func test_zadd() throws { + _ = try connection.send(command: "FLUSHALL").wait() + + XCTAssertThrowsError(try connection.zadd([(30, 2)], to: #function, options: ["INCR"]).wait()) + + var count = try connection.zadd([(30, 2)], to: #function).wait() + XCTAssertEqual(count, 1) + count = try connection.zadd([(30, 5)], to: #function).wait() + XCTAssertEqual(count, 0) + count = try connection.zadd([(30, 6), (31, 0), (32, 1)], to: #function, options: ["NX"]).wait() + XCTAssertEqual(count, 2) + count = try connection.zadd([(32, 2), (33, 3)], to: #function, options: ["XX", "CH"]).wait() + XCTAssertEqual(count, 1) + + var success = try connection.zadd((30, 7), to: #function, options: ["CH"]).wait() + XCTAssertTrue(success) + success = try connection.zadd((30, 8), to: #function, options: ["NX"]).wait() + XCTAssertFalse(success) + } + + func test_zcard() throws { + var count = try connection.zcard(of: key).wait() + XCTAssertEqual(count, 10) + + _ = try connection.zadd(("foo", 0), to: key).wait() + + count = try connection.zcard(of: key).wait() + XCTAssertEqual(count, 11) + } + + func test_zscore() throws { + _ = try connection.send(command: "FLUSHALL").wait() + + var score = try connection.zscore(of: 30, storedAt: #function).wait() + XCTAssertEqual(score, nil) + + _ = try connection.zadd((30, 1), to: #function).wait() + + score = try connection.zscore(of: 30, storedAt: #function).wait() + XCTAssertEqual(score, 1) + + _ = try connection.zincrby(#function, member: 30, by: 10).wait() + + score = try connection.zscore(of: 30, storedAt: #function).wait() + XCTAssertEqual(score, 11) + } + + func test_zscan() throws { + var (cursor, results) = try connection.zscan(key, count: 5).wait() + XCTAssertGreaterThanOrEqual(cursor, 0) + XCTAssertGreaterThanOrEqual(results.count, 5) + + (_, results) = try connection.zscan(key, startingFrom: cursor, count: 8).wait() + XCTAssertGreaterThanOrEqual(results.count, 8) + + (cursor, results) = try connection.zscan(key, matching: "1*").wait() + XCTAssertEqual(cursor, 0) + XCTAssertEqual(results.count, 2) + XCTAssertEqual(results[0].1, 1) + + (cursor, results) = try connection.zscan(key, matching: "*0").wait() + XCTAssertEqual(cursor, 0) + XCTAssertEqual(results.count, 1) + XCTAssertEqual(results[0].1, 10) + } + + func test_zrank() throws { + let futures = [ + connection.zrank(of: 1, storedAt: key), + connection.zrank(of: 2, storedAt: key), + connection.zrank(of: 3, storedAt: key), + ] + let scores = try EventLoopFuture.whenAllSucceed(futures, on: connection.eventLoop).wait() + XCTAssertEqual(scores, [0, 1, 2]) + } + + func test_zrevrank() throws { + let futures = [ + connection.zrevrank(of: 1, storedAt: key), + connection.zrevrank(of: 2, storedAt: key), + connection.zrevrank(of: 3, storedAt: key), + ] + let scores = try EventLoopFuture.whenAllSucceed(futures, on: connection.eventLoop).wait() + XCTAssertEqual(scores, [9, 8, 7]) + } + + func test_zcount() throws { + var count = try connection.zcount(of: key, within: ("1", "3")).wait() + XCTAssertEqual(count, 3) + count = try connection.zcount(of: key, within: ("(1", "(3")).wait() + XCTAssertEqual(count, 1) + } + + func test_zlexcount() throws { + var count = try connection.zlexcount(of: key, within: ("[1", "[3")).wait() + XCTAssertEqual(count, 3) + count = try connection.zlexcount(of: key, within: ("(1", "(3")).wait() + XCTAssertEqual(count, 1) + } + + func test_zpopmin() throws { + let min = try connection.zpopmin(from: key).wait() + XCTAssertEqual(min?.1, 1) + + _ = try connection.zpopmin(7, from: key).wait() + + let results = try connection.zpopmin(3, from: key).wait() + XCTAssertEqual(results.count, 2) + XCTAssertEqual(results[0].1, 9) + XCTAssertEqual(results[1].1, 10) + } + + func test_zpopmax() throws { + let min = try connection.zpopmax(from: key).wait() + XCTAssertEqual(min?.1, 10) + + _ = try connection.zpopmax(7, from: key).wait() + + let results = try connection.zpopmax(3, from: key).wait() + XCTAssertEqual(results.count, 2) + XCTAssertEqual(results[0].1, 2) + XCTAssertEqual(results[1].1, 1) + } + + func test_zincrby() throws { + var score = try connection.zincrby(key, member: 1, by: 3_00_1398.328923).wait() + XCTAssertEqual(score, 3_001_399.328923) + + score = try connection.zincrby(key, member: 1, by: -201_309.1397318).wait() + XCTAssertEqual(score, 2_800_090.1891912) + + score = try connection.zincrby(key, member: 1, by: 20).wait() + XCTAssertEqual(score, 2_800_110.1891912) + } + + func test_zunionstore() throws { + _ = try connection.zadd([(1, 1), (2, 2)], to: #function).wait() + _ = try connection.zadd([(3, 3), (4, 4)], to: #file).wait() + + let unionCount = try connection.zunionstore( + [key, #function, #file], + to: #function+#file, + weights: [3, 2, 1], + aggregateMethod: "MAX" + ).wait() + XCTAssertEqual(unionCount, 10) + let rank = try connection.zrank(of: 10, storedAt: #function+#file).wait() + XCTAssertEqual(rank, 9) + let score = try connection.zscore(of: 10, storedAt: #function+#file).wait() + XCTAssertEqual(score, 30) + } + + func test_zinterstore() throws { + _ = try connection.zadd([(3, 3), (10, 10), (11, 11)], to: #function).wait() + + let unionCount = try connection.zinterstore( + [key, #function], + to: #file, + weights: [3, 2], + aggregateMethod: "MIN" + ).wait() + XCTAssertEqual(unionCount, 2) + let rank = try connection.zrank(of: 10, storedAt: #file).wait() + XCTAssertEqual(rank, 1) + let score = try connection.zscore(of: 10, storedAt: #file).wait() + XCTAssertEqual(score, 20.0) + } + + func test_zrange() throws { + var elements = try connection.zrange(withinIndices: (1, 3), from: key).wait() + XCTAssertEqual(elements.count, 3) + elements = try connection.zrange(withinIndices: (1, 3), from: key, withScores: true).wait() + XCTAssertEqual(elements.count, 6) + + let values = try NIORedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false) + .map { (value, _) in return Int(value) } + + XCTAssertEqual(values[0], 2) + XCTAssertEqual(values[1], 3) + XCTAssertEqual(values[2], 4) + } + + func test_zrevrange() throws { + var elements = try connection.zrevrange(withinIndices: (1, 3), from: key).wait() + XCTAssertEqual(elements.count, 3) + elements = try connection.zrevrange(withinIndices: (1, 3), from: key, withScores: true).wait() + XCTAssertEqual(elements.count, 6) + + let values = try NIORedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false) + .map { (value, _) in return Int(value) } + + XCTAssertEqual(values[0], 9) + XCTAssertEqual(values[1], 8) + XCTAssertEqual(values[2], 7) + } + + func test_zrangebyscore() throws { + var elements = try connection.zrangebyscore(within: ("(1", "3"), from: key).wait() + XCTAssertEqual(elements.count, 2) + elements = try connection.zrangebyscore(within: ("1", "3"), from: key, withScores: true).wait() + XCTAssertEqual(elements.count, 6) + + let values = try NIORedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false) + .map { (_, score) in return score } + + XCTAssertEqual(values[0], 1.0) + XCTAssertEqual(values[1], 2.0) + XCTAssertEqual(values[2], 3.0) + } + + func test_zrevrangebyscore() throws { + var elements = try connection.zrevrangebyscore(within: ("(1", "3"), from: key).wait() + XCTAssertEqual(elements.count, 2) + elements = try connection.zrevrangebyscore(within: ("1", "3"), from: key, withScores: true).wait() + XCTAssertEqual(elements.count, 6) + + let values = try NIORedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false) + .map { (_, score) in return score } + + XCTAssertEqual(values[0], 3.0) + XCTAssertEqual(values[1], 2.0) + XCTAssertEqual(values[2], 1.0) + } + + func test_zrangebylex() throws { + _ = try connection.zadd([(1, 0), (2, 0), (3, 0)], to: #function).wait() + + var elements = try connection.zrangebylex(within: ("[1", "[2"), from: #function) + .wait() + .map { Int($0) } + XCTAssertEqual(elements.count, 2) + XCTAssertEqual(elements[0], 1) + XCTAssertEqual(elements[1], 2) + + elements = try connection.zrangebylex(within: ("[1", "(4"), from: #function, limitBy: (offset: 1, count: 1)) + .wait() + .map { Int($0) } + XCTAssertEqual(elements.count, 1) + XCTAssertEqual(elements[0], 2) + } + + func test_zrevrangebylex() throws { + _ = try connection.zadd([(1, 0), (2, 0), (3, 0), (4, 0)], to: #function).wait() + + var elements = try connection.zrevrangebylex(within: ("(2", "[4"), from: #function) + .wait() + .map { Int($0) } + XCTAssertEqual(elements.count, 2) + XCTAssertEqual(elements[0], 4) + XCTAssertEqual(elements[1], 3) + + elements = try connection.zrevrangebylex(within: ("[1", "(4"), from: #function, limitBy: (offset: 1, count: 2)) + .wait() + .map { Int($0) } + XCTAssertEqual(elements.count, 2) + XCTAssertEqual(elements[0], 2) + } + + func test_zrem() throws { + var count = try connection.zrem([1], from: key).wait() + XCTAssertEqual(count, 1) + count = try connection.zrem([1], from: key).wait() + XCTAssertEqual(count, 0) + + count = try connection.zrem([2, 3, 4, 5], from: key).wait() + XCTAssertEqual(count, 4) + count = try connection.zrem([5, 6, 7], from: key).wait() + XCTAssertEqual(count, 2) + } + + func test_zremrangebylex() throws { + _ = try connection.zadd([("bar", 0), ("car", 0), ("tar", 0)], to: #function).wait() + + var count = try connection.zremrangebylex(within: ("(a", "[t"), from: #function).wait() + XCTAssertEqual(count, 2) + count = try connection.zremrangebylex(within: ("-", "[t"), from: #function).wait() + XCTAssertEqual(count, 0) + count = try connection.zremrangebylex(within: ("[t", "+"), from: #function).wait() + XCTAssertEqual(count, 1) + } + + func test_zremrangebyrank() throws { + var count = try connection.zremrangebyrank(startingFrom: 0, endingAt: 3, from: key).wait() + XCTAssertEqual(count, 4) + count = try connection.zremrangebyrank(startingFrom: 0, endingAt: 10, from: key).wait() + XCTAssertEqual(count, 6) + count = try connection.zremrangebyrank(startingFrom: 0, endingAt: 3, from: key).wait() + XCTAssertEqual(count, 0) + } + + func test_zremrangebyscore() throws { + var count = try connection.zremrangebyscore(within: ("(8", "10"), from: key).wait() + XCTAssertEqual(count, 2) + count = try connection.zremrangebyscore(within: ("4", "(7"), from: key).wait() + XCTAssertEqual(count, 3) + count = try connection.zremrangebyscore(within: ("-inf", "+inf"), from: key).wait() + XCTAssertEqual(count, 5) + } + + static var allTests = [ + ("test_zadd", test_zadd), + ("test_zcard", test_zcard), + ("test_zscore", test_zscore), + ("test_zscan", test_zscan), + ("test_zrank", test_zrank), + ("test_zrevrank", test_zrevrank), + ("test_zcount", test_zcount), + ("test_zlexcount", test_zlexcount), + ("test_zpopmin", test_zpopmin), + ("test_zpopmax", test_zpopmax), + ("test_zincrby", test_zincrby), + ("test_zunionstore", test_zunionstore), + ("test_zinterstore", test_zinterstore), + ("test_zrange", test_zrange), + ("test_zrevrange", test_zrevrange), + ("test_zrangebyscore", test_zrangebyscore), + ("test_zrevrangebyscore", test_zrevrangebyscore), + ("test_zrangebylex", test_zrangebylex), + ("test_zrevrangebylex", test_zrevrangebylex), + ("test_zrem", test_zrem), + ("test_zremrangebylex", test_zremrangebylex), + ("test_zremrangebyrank", test_zremrangebyrank), + ("test_zremrangebyscore", test_zremrangebyscore), + ] +} diff --git a/Tests/NIORedisTests/XCTestManifests.swift b/Tests/NIORedisTests/XCTestManifests.swift index b60a137..a5c7748 100644 --- a/Tests/NIORedisTests/XCTestManifests.swift +++ b/Tests/NIORedisTests/XCTestManifests.swift @@ -13,7 +13,8 @@ public func allTests() -> [XCTestCaseEntry] { testCase(SetCommandsTests.allTests), testCase(RedisPipelineTests.allTests), testCase(HashCommandsTests.allTests), - testCase(ListCommandsTests.allTests) + testCase(ListCommandsTests.allTests), + testCase(SortedSetCommandsTests.allTests) ] } #endif