lib/redis/connection/memory.rb in fakeredis-0.4.3 vs lib/redis/connection/memory.rb in fakeredis-0.5.0

- old
+ new

@@ -1,20 +1,26 @@ require 'set' require 'redis/connection/registry' require 'redis/connection/command_helper' +require "fakeredis/command_executor" require "fakeredis/expiring_hash" +require "fakeredis/sort_method" require "fakeredis/sorted_set_argument_handler" require "fakeredis/sorted_set_store" +require "fakeredis/transaction_commands" require "fakeredis/zset" class Redis module Connection class Memory include Redis::Connection::CommandHelper include FakeRedis + include SortMethod + include TransactionCommands + include CommandExecutor - attr_accessor :buffer, :options + attr_accessor :options # Tracks all databases for all instances across the current process. # We have to be able to handle two clients with the same host/port accessing # different databases at once without overwriting each other. So we store our # "data" outside the client instances, in this class level instance method. @@ -74,39 +80,18 @@ end def timeout=(usecs) end - def write(command) - meffod = command.shift.to_s.downcase.to_sym - if respond_to?(meffod) - reply = send(meffod, *command) - else - raise Redis::CommandError, "ERR unknown command '#{meffod}'" - end - - if reply == true - reply = 1 - elsif reply == false - reply = 0 - end - - replies << reply - buffer << reply if buffer && meffod != :multi - nil - end - def read replies.shift end # NOT IMPLEMENTED: # * blpop # * brpop # * brpoplpush - # * discard - # * sort # * subscribe # * psubscribe # * publish def flushdb @@ -148,11 +133,11 @@ def save; end def bgsave ; end - def bgreriteaof ; end + def bgrewriteaof ; end def move key, destination_id raise Redis::CommandError, "ERR source and destination objects are the same" if destination_id == database_id destination = find_database(destination_id) return false unless data.has_key?(key) @@ -169,10 +154,15 @@ def getbit(key, offset) return unless data[key] data[key].unpack('B*')[0].split("")[offset].to_i end + def bitcount(key, start_index = 0, end_index = -1) + return 0 unless data[key] + data[key][start_index..end_index].unpack('B*')[0].count("1") + end + def getrange(key, start, ending) return unless data[key] data[key][start..ending] end alias :substr :getrange @@ -212,12 +202,13 @@ end def hdel(key, field) field = field.to_s data_type_check(key, Hash) - data[key] && data[key].delete(field) + deleted = data[key] && data[key].delete(field) remove_key_for_empty_collection(key) + deleted ? 1 : 0 end def hkeys(key) data_type_check(key, Hash) return [] if data[key].nil? @@ -242,10 +233,15 @@ def lastsave Time.now.to_i end + def time + microseconds = (Time.now.to_f * 1000000).to_i + [ microseconds / 1000000, microseconds % 1000000 ] + end + def dbsize data.keys.count end def exists(key) @@ -265,21 +261,21 @@ def ltrim(key, start, stop) data_type_check(key, Array) return unless data[key] - if start < 0 && data[key].count < start.abs - # Example: we have a list of 3 elements and - # we give it a ltrim list, -5, -1. This means - # it should trim to a max of 5. Since 3 < 5 - # we should not touch the list. This is consistent - # with behavior of real Redis's ltrim with a negative - # start argument. - data[key] - else + # Example: we have a list of 3 elements and + # we give it a ltrim list, -5, -1. This means + # it should trim to a max of 5. Since 3 < 5 + # we should not touch the list. This is consistent + # with behavior of real Redis's ltrim with a negative + # start argument. + unless start < 0 && data[key].count < start.abs data[key] = data[key][start..stop] end + + "OK" end def lindex(key, index) data_type_check(key, Array) data[key] && data[key][index] @@ -358,11 +354,11 @@ end def rpoplpush(key1, key2) data_type_check(key1, Array) rpop(key1).tap do |elem| - lpush(key2, elem) + lpush(key2, elem) unless elem.nil? end end def lpop(key) data_type_check(key, Array) @@ -401,11 +397,21 @@ result end def srem(key, value) data_type_check(key, ::Set) - deleted = !!(data[key] && data[key].delete?(value.to_s)) + return false unless data[key] + + if value.is_a?(Array) + old_size = data[key].size + values = value.map(&:to_s) + values.each { |value| data[key].delete(value) } + deleted = old_size - data[key].size + else + deleted = !!data[key].delete?(value.to_s) + end + remove_key_for_empty_collection(key) deleted end def smove(source, destination, value) @@ -471,14 +477,12 @@ data_type_check(destination, ::Set) result = sdiff(key1, *keys) data[destination] = ::Set.new(result) end - def srandmember(key) - data_type_check(key, ::Set) - return nil unless data[key] - data[key].to_a[rand(data[key].size)] + def srandmember(key, number=nil) + number.nil? ? srandmember_single(key) : srandmember_multiple(key, number) end def del(*keys) keys = keys.flatten(1) raise_argument_error('del') if keys.empty? @@ -514,13 +518,13 @@ true end end def expire(key, ttl) - return unless data[key] + return 0 unless data[key] data.expires[key] = Time.now + ttl - true + 1 end def ttl(key) if data.expires.include?(key) && (ttl = data.expires[key].to_i - Time.now.to_i) > 0 ttl @@ -584,11 +588,11 @@ def hmget(key, *fields) raise_argument_error('hmget') if fields.empty? data_type_check(key, Hash) - fields.map do |field| + fields.flatten.map do |field| field = field.to_s if data[key] data[key][field] else nil @@ -617,10 +621,21 @@ data[key] = { field => increment.to_s } end data[key][field].to_i end + def hincrbyfloat(key, field, increment) + data_type_check(key, Hash) + field = field.to_s + if data[key] + data[key][field] = (data[key][field].to_f + increment.to_f).to_s + else + data[key] = { field => increment.to_s } + end + data[key][field] + end + def hexists(key, field) data_type_check(key, Hash) return false unless data[key] data[key].key?(field.to_s) end @@ -633,20 +648,35 @@ def []=(key, value) set(key, value) end - def set(key, value) + def set(key, value, *array_options) + option_nx = array_options.delete("NX") + option_xx = array_options.delete("XX") + + return false if option_nx && option_xx + + return false if option_nx && exists(key) + return false if option_xx && !exists(key) + data[key] = value.to_s + + options = Hash[array_options.each_slice(2).to_a] + ttl_in_seconds = options["EX"] if options["EX"] + ttl_in_seconds = options["PX"] / 1000.0 if options["PX"] + + expire(key, ttl_in_seconds) if ttl_in_seconds + "OK" end def setbit(key, offset, bit) old_val = data[key] ? data[key].unpack('B*')[0].split("") : [] size_increment = [((offset/8)+1)*8-old_val.length, 0].max old_val += Array.new(size_increment).map{"0"} - original_val = old_val[offset] + original_val = old_val[offset].to_i old_val[offset] = bit.to_s new_val = "" old_val.each_slice(8){|b| new_val = new_val + b.join("").to_i(2).chr } data[key] = new_val original_val @@ -685,14 +715,10 @@ return false if keys.any?{|key| data.key?(key) } mset(*pairs) true end - def sort(key) - # TODO: Implement - end - def incr(key) data.merge!({ key => (data[key].to_i + 1).to_s || "1"}) data[key].to_i end @@ -726,26 +752,41 @@ def shutdown; end def slaveof(host, port) ; end - def exec - buffer.tap {|x| self.buffer = nil } - end + def scan(start_cursor, *args) + match = "*" + count = 10 - def multi - self.buffer = [] - yield if block_given? - "OK" - end + if args.size.odd? + raise_argument_error('scan') + end - def watch(_) - "OK" - end + if idx = args.index("MATCH") + match = args[idx + 1] + end - def unwatch - "OK" + if idx = args.index("COUNT") + count = args[idx + 1] + end + + start_cursor = start_cursor.to_i + data_type_check(start_cursor, Fixnum) + + cursor = start_cursor + next_keys = [] + + if start_cursor + count >= data.length + next_keys = keys(match)[start_cursor..-1] + cursor = 0 + else + cursor = start_cursor + 10 + next_keys = keys(match)[start_cursor..cursor] + end + + return "#{cursor}", next_keys end def zadd(key, *args) if !args.first.is_a?(Array) if args.size < 2 @@ -898,10 +939,21 @@ range = data[key].select_by_score(min, max) range.each {|k,_| data[key].delete(k) } range.size end + def zremrangebyrank(key, start, stop) + data_type_check(key, ZSet) + return 0 unless data[key] + + sorted_elements = data[key].sort_by { |k, v| v } + start = sorted_elements.length if start > sorted_elements.length + elements_to_delete = sorted_elements[start..stop] + elements_to_delete.each { |elem, rank| data[key].delete(elem) } + elements_to_delete.size + end + def zinterstore(out, *args) data_type_check(out, ZSet) args_handler = SortedSetArgumentHandler.new(args) data[out] = SortedSetIntersectStore.new(args_handler, data).call data[out].size @@ -912,18 +964,10 @@ args_handler = SortedSetArgumentHandler.new(args) data[out] = SortedSetUnionStore.new(args_handler, data).call data[out].size end - def zremrangebyrank(key, start, stop) - sorted_elements = data[key].sort_by { |k, v| v } - start = sorted_elements.length if start > sorted_elements.length - elements_to_delete = sorted_elements[start..stop] - elements_to_delete.each { |elem, rank| data[key].delete(elem) } - elements_to_delete.size - end - private def raise_argument_error(command, match_string=command) error_message = if %w(hmset mset_odd).include?(match_string.downcase) "ERR wrong number of arguments for #{command.upcase}" else @@ -961,9 +1005,28 @@ end end def mapped_param? param param.size == 1 && param[0].is_a?(Array) + end + + def srandmember_single(key) + data_type_check(key, ::Set) + return nil unless data[key] + data[key].to_a[rand(data[key].size)] + end + + def srandmember_multiple(key, number) + return [] unless data[key] + if number >= 0 + # replace with `data[key].to_a.sample(number)` when 1.8.7 is deprecated + (1..number).inject([]) do |selected, _| + available_elements = data[key].to_a - selected + selected << available_elements[rand(available_elements.size)] + end.compact + else + (1..-number).map { data[key].to_a[rand(data[key].size)] }.flatten + end end end end end