lib/redis.rb in redis-4.4.0 vs lib/redis.rb in redis-4.5.0

- old
+ new

@@ -2,10 +2,12 @@ require "monitor" require_relative "redis/errors" class Redis + @exists_returns_integer = true + class << self attr_reader :exists_returns_integer def exists_returns_integer=(value) unless value @@ -832,21 +834,27 @@ # @param [String] key # @param [String] value # @param [Hash] options # - `:ex => Integer`: Set the specified expire time, in seconds. # - `:px => Integer`: Set the specified expire time, in milliseconds. + # - `:exat => Integer` : Set the specified Unix time at which the key will expire, in seconds. + # - `:pxat => Integer` : Set the specified Unix time at which the key will expire, in milliseconds. # - `:nx => true`: Only set the key if it does not already exist. # - `:xx => true`: Only set the key if it already exist. # - `:keepttl => true`: Retain the time to live associated with the key. + # - `:get => true`: Return the old string stored at key, or nil if key did not exist. # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true` - def set(key, value, ex: nil, px: nil, nx: nil, xx: nil, keepttl: nil) + def set(key, value, ex: nil, px: nil, exat: nil, pxat: nil, nx: nil, xx: nil, keepttl: nil, get: nil) args = [:set, key, value.to_s] args << "EX" << ex if ex args << "PX" << px if px + args << "EXAT" << exat if exat + args << "PXAT" << pxat if pxat args << "NX" if nx args << "XX" if xx args << "KEEPTTL" if keepttl + args << "GET" if get synchronize do |client| if nx || xx client.call(args, &BoolifySet) else @@ -1108,10 +1116,49 @@ synchronize do |client| client.call([:getset, key, value.to_s]) end end + # Get the value of key and delete the key. This command is similar to GET, + # except for the fact that it also deletes the key on success. + # + # @param [String] key + # @return [String] the old value stored in the key, or `nil` if the key + # did not exist + def getdel(key) + synchronize do |client| + client.call([:getdel, key]) + end + end + + # Get the value of key and optionally set its expiration. GETEX is similar to + # GET, but is a write command with additional options. When no options are + # provided, GETEX behaves like GET. + # + # @param [String] key + # @param [Hash] options + # - `:ex => Integer`: Set the specified expire time, in seconds. + # - `:px => Integer`: Set the specified expire time, in milliseconds. + # - `:exat => true`: Set the specified Unix time at which the key will + # expire, in seconds. + # - `:pxat => true`: Set the specified Unix time at which the key will + # expire, in milliseconds. + # - `:persist => true`: Remove the time to live associated with the key. + # @return [String] The value of key, or nil when key does not exist. + def getex(key, ex: nil, px: nil, exat: nil, pxat: nil, persist: false) + args = [:getex, key] + args << "EX" << ex if ex + args << "PX" << px if px + args << "EXAT" << exat if exat + args << "PXAT" << pxat if pxat + args << "PERSIST" if persist + + synchronize do |client| + client.call(args) + end + end + # Get the length of the value stored in a key. # # @param [String] key # @return [Integer] the length of the value stored in the key, or 0 # if the key does not exist @@ -1129,10 +1176,63 @@ synchronize do |client| client.call([:llen, key]) end end + # Remove the first/last element in a list, append/prepend it to another list and return it. + # + # @param [String] source source key + # @param [String] destination destination key + # @param [String, Symbol] where_source from where to remove the element from the source list + # e.g. 'LEFT' - from head, 'RIGHT' - from tail + # @param [String, Symbol] where_destination where to push the element to the source list + # e.g. 'LEFT' - to head, 'RIGHT' - to tail + # + # @return [nil, String] the element, or nil when the source key does not exist + # + # @note This command comes in place of the now deprecated RPOPLPUSH. + # Doing LMOVE RIGHT LEFT is equivalent. + def lmove(source, destination, where_source, where_destination) + where_source, where_destination = _normalize_move_wheres(where_source, where_destination) + + synchronize do |client| + client.call([:lmove, source, destination, where_source, where_destination]) + end + end + + # Remove the first/last element in a list and append/prepend it + # to another list and return it, or block until one is available. + # + # @example With timeout + # element = redis.blmove("foo", "bar", "LEFT", "RIGHT", timeout: 5) + # # => nil on timeout + # # => "element" on success + # @example Without timeout + # element = redis.blmove("foo", "bar", "LEFT", "RIGHT") + # # => "element" + # + # @param [String] source source key + # @param [String] destination destination key + # @param [String, Symbol] where_source from where to remove the element from the source list + # e.g. 'LEFT' - from head, 'RIGHT' - from tail + # @param [String, Symbol] where_destination where to push the element to the source list + # e.g. 'LEFT' - to head, 'RIGHT' - to tail + # @param [Hash] options + # - `:timeout => Numeric`: timeout in seconds, defaults to no timeout + # + # @return [nil, String] the element, or nil when the source key does not exist or the timeout expired + # + def blmove(source, destination, where_source, where_destination, timeout: 0) + where_source, where_destination = _normalize_move_wheres(where_source, where_destination) + + synchronize do |client| + command = [:blmove, source, destination, where_source, where_destination, timeout] + timeout += client.timeout if timeout > 0 + client.call_with_timeout(command, timeout) + end + end + # Prepend one or more values to a list, creating the list if it doesn't exist # # @param [String] key # @param [String, Array<String>] value string value, or array of string values to push # @return [Integer] the length of the list after the push operation @@ -1477,10 +1577,23 @@ synchronize do |client| client.call([:sismember, key, member], &Boolify) end end + # Determine if multiple values are members of a set. + # + # @param [String] key + # @param [String, Array<String>] members + # @return [Array<Boolean>] + def smismember(key, *members) + synchronize do |client| + client.call([:smismember, key, *members]) do |reply| + reply.map(&Boolify) + end + end + end + # Get all the members in a set. # # @param [String] key # @return [Array<String>] def smembers(key) @@ -1581,10 +1694,14 @@ # @param [Hash] options # - `:xx => true`: Only update elements that already exist (never # add elements) # - `:nx => true`: Don't update already existing elements (always # add new elements) + # - `:lt => true`: Only update existing elements if the new score + # is less than the current score + # - `:gt => true`: Only update existing elements if the new score + # is greater than the current score # - `:ch => true`: Modify the return value from the number of new # elements added, to the total number of elements changed (CH is an # abbreviation of changed); changed elements are new elements added # and elements already existing for which the score was updated # - `:incr => true`: When this option is specified ZADD acts like @@ -1595,14 +1712,16 @@ # **added** to the sorted set. # - `Integer` when an array of pairs is specified, holding the number of # pairs that were **added** to the sorted set. # - `Float` when option :incr is specified, holding the score of the member # after incrementing it. - def zadd(key, *args, nx: nil, xx: nil, ch: nil, incr: nil) + def zadd(key, *args, nx: nil, xx: nil, lt: nil, gt: nil, ch: nil, incr: nil) command = [:zadd, key] command << "NX" if nx command << "XX" if xx + command << "LT" if lt + command << "GT" if gt command << "CH" if ch command << "INCR" if incr synchronize do |client| if args.size == 1 && args[0].is_a?(Array) @@ -1761,10 +1880,67 @@ synchronize do |client| client.call([:zscore, key, member], &Floatify) end end + # Get the scores associated with the given members in a sorted set. + # + # @example Get the scores for members "a" and "b" + # redis.zmscore("zset", "a", "b") + # # => [32.0, 48.0] + # + # @param [String] key + # @param [String, Array<String>] members + # @return [Array<Float>] scores of the members + def zmscore(key, *members) + synchronize do |client| + client.call([:zmscore, key, *members]) do |reply| + reply.map(&Floatify) + end + end + end + + # Get one or more random members from a sorted set. + # + # @example Get one random member + # redis.zrandmember("zset") + # # => "a" + # @example Get multiple random members + # redis.zrandmember("zset", 2) + # # => ["a", "b"] + # @example Gem multiple random members with scores + # redis.zrandmember("zset", 2, with_scores: true) + # # => [["a", 2.0], ["b", 3.0]] + # + # @param [String] key + # @param [Integer] count + # @param [Hash] options + # - `:with_scores => true`: include scores in output + # + # @return [nil, String, Array<String>, Array<[String, Float]>] + # - when `key` does not exist or set is empty, `nil` + # - when `count` is not specified, a member + # - when `count` is specified and `:with_scores` is not specified, an array of members + # - when `:with_scores` is specified, an array with `[member, score]` pairs + def zrandmember(key, count = nil, withscores: false, with_scores: withscores) + if with_scores && count.nil? + raise ArgumentError, "count argument must be specified" + end + + args = [:zrandmember, key] + args << count if count + + if with_scores + args << "WITHSCORES" + block = FloatifyPairs + end + + synchronize do |client| + client.call(args, &block) + end + end + # Return a range of members in a sorted set, by index. # # @example Retrieve all members from a sorted set # redis.zrange("zset", 0, -1) # # => ["a", "b"] @@ -3516,11 +3692,11 @@ EMPTY_STREAM_RESPONSE = [nil].freeze private_constant :EMPTY_STREAM_RESPONSE HashifyStreamEntries = lambda { |reply| reply.compact.map do |entry_id, values| - [entry_id, values.each_slice(2).to_h] + [entry_id, values&.each_slice(2)&.to_h] end } HashifyStreamAutoclaim = lambda { |reply| { @@ -3632,9 +3808,24 @@ else timeout = client.timeout.to_f + blocking_timeout_msec.to_f / 1000.0 client.call_with_timeout(args, timeout, &HashifyStreams) end end + end + + def _normalize_move_wheres(where_source, where_destination) + where_source = where_source.to_s.upcase + where_destination = where_destination.to_s.upcase + + if where_source != "LEFT" && where_source != "RIGHT" + raise ArgumentError, "where_source must be 'LEFT' or 'RIGHT'" + end + + if where_destination != "LEFT" && where_destination != "RIGHT" + raise ArgumentError, "where_destination must be 'LEFT' or 'RIGHT'" + end + + [where_source, where_destination] end end require_relative "redis/version" require_relative "redis/connection"