lib/redis/connection/memory.rb in fakeredis-0.7.0 vs lib/redis/connection/memory.rb in fakeredis-0.8.0

- old
+ new

@@ -7,20 +7,24 @@ require "fakeredis/sorted_set_argument_handler" require "fakeredis/sorted_set_store" require "fakeredis/transaction_commands" require "fakeredis/zset" require "fakeredis/bitop_command" +require "fakeredis/geo_commands" require "fakeredis/version" class Redis module Connection + DEFAULT_REDIS_VERSION = '3.3.5' + class Memory include Redis::Connection::CommandHelper include FakeRedis include SortMethod include TransactionCommands include BitopCommand + include GeoCommands include CommandExecutor attr_accessor :options # Tracks all databases for all instances across the current process. @@ -49,11 +53,11 @@ def self.connect(options = {}) new(options) end def initialize(options = {}) - self.options = options + self.options = self.options ? self.options.merge(options) : options end def database_id @database_id ||= 0 end @@ -91,13 +95,12 @@ def disconnect end def client(command, _options = {}) case command - when :setname then true + when :setname then "OK" when :getname then nil - when :client then true else raise Redis::CommandError, "ERR unknown command '#{command}'" end end @@ -128,11 +131,11 @@ "OK" end def info { - "redis_version" => "2.6.16", + "redis_version" => options[:version] || DEFAULT_REDIS_VERSION, "connected_clients" => "1", "connected_slaves" => "0", "used_memory" => "3187", "changes_since_last_save" => "0", "last_save_time" => "1237655729", @@ -145,14 +148,18 @@ def monitor; end def save; end - def bgsave ; end + def bgsave; end - def bgrewriteaof ; end + def bgrewriteaof; end + def evalsha; end + + def eval; 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) return false if destination.has_key?(key) @@ -212,10 +219,15 @@ 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 bitpos(key, bit, start_index = 0, end_index = -1) + value = data[key] || "" + value[0..end_index].unpack('B*')[0].index(bit.to_s, start_index * 8) || -1 + end + def getrange(key, start, ending) return unless data[key] data[key][start..ending] end alias :substr :getrange @@ -491,13 +503,13 @@ end end def brpoplpush(key1, key2, opts={}) data_type_check(key1, Array) - brpop(key1).tap do |elem| - lpush(key2, elem) unless elem.nil? - end + _key, elem = brpop(key1) + lpush(key2, elem) unless elem.nil? + elem end def lpop(key) data_type_check(key, Array) return unless data[key] @@ -547,10 +559,12 @@ result end def srem(key, value) data_type_check(key, ::Set) + value = Array(value) + raise_argument_error('srem') if value.empty? return false unless data[key] if value.is_a?(Array) old_size = data[key].size values = value.map(&:to_s) @@ -573,11 +587,11 @@ def spop(key, count = nil) data_type_check(key, ::Set) results = (count || 1).times.map do elem = srandmember(key) - srem(key, elem) + srem(key, elem) if elem elem end.compact count.nil? ? results.first : results end @@ -678,18 +692,15 @@ return ["#{cursor}", result] end def del(*keys) - keys = keys.flatten(1) - raise_argument_error('del') if keys.empty? + delete_keys(keys, 'del') + end - old_count = data.keys.size - keys.each do |key| - data.delete(key) - end - old_count - data.keys.size + def unlink(*keys) + delete_keys(keys, 'unlink') end def setnx(key, value) if exists(key) 0 @@ -796,11 +807,11 @@ end "OK" end def hmget(key, *fields) - raise_argument_error('hmget') if fields.empty? + raise_argument_error('hmget') if fields.empty? || fields.flatten.empty? data_type_check(key, Hash) fields.flatten.map do |field| field = field.to_s if data[key] @@ -815,10 +826,16 @@ data_type_check(key, Hash) return 0 unless data[key] data[key].size end + def hstrlen(key, field) + data_type_check(key, Hash) + return 0 if data[key].nil? || data[key][field].nil? + data[key][field].size + end + def hvals(key) data_type_check(key, Hash) return [] unless data[key] data[key].values end @@ -851,26 +868,18 @@ data[key].key?(field.to_s) end def sync ; end - def [](key) - get(key) - end - - def []=(key, value) - set(key, value) - end - 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 nil if option_nx && option_xx - return false if option_nx && exists(key) - return false if option_xx && !exists(key) + return nil if option_nx && exists(key) + return nil 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"] @@ -897,10 +906,14 @@ data[key] = value.to_s expire(key, seconds) "OK" end + def psetex(key, milliseconds, value) + setex(key, milliseconds / 1000.0, value) + end + def setrange(key, offset, value) return unless data[key] s = data[key][offset,value.size] data[key][s] = value end @@ -1003,10 +1016,23 @@ return "#{cursor}", returned_keys end def zadd(key, *args) + option_xx = args.delete("XX") + option_nx = args.delete("NX") + option_ch = args.delete("CH") + option_incr = args.delete("INCR") + + if option_xx && option_nx + raise_options_error("XX", "NX") + end + + if option_incr && args.size > 2 + raise_options_error("INCR") + end + if !args.first.is_a?(Array) if args.size < 2 raise_argument_error('zadd') elsif args.size.odd? raise_syntax_error @@ -1018,22 +1044,43 @@ end data_type_check(key, ZSet) data[key] ||= ZSet.new - if args.size == 2 && !(Array === args.first) - score, value = args - exists = !data[key].key?(value.to_s) + # Turn [1, 2, 3, 4] into [[1, 2], [3, 4]] unless it is already + args = args.each_slice(2).to_a unless args.first.is_a?(Array) + + changed = 0 + exists = args.map(&:last).count { |el| !hexists(key, el.to_s) } + + args.each do |score, value| + if option_nx && hexists(key, value.to_s) + next + end + + if option_xx && !hexists(key, value.to_s) + exists -= 1 + next + end + + if option_incr + data[key][value.to_s] ||= 0 + return data[key].increment(value, score).to_s + end + + if option_ch && data[key][value.to_s] != score + changed += 1 + end data[key][value.to_s] = score - else - # Turn [1, 2, 3, 4] into [[1, 2], [3, 4]] unless it is already - args = args.each_slice(2).to_a unless args.first.is_a?(Array) - exists = args.map(&:last).map { |el| data[key].key?(el.to_s) }.count(false) - args.each { |s, v| data[key][v.to_s] = s } end - exists + if option_incr + changed = changed.zero? ? nil : changed + exists = exists.zero? ? nil : exists + end + + option_ch ? changed : exists end def zrem(key, value) data_type_check(key, ZSet) values = Array(value) @@ -1045,19 +1092,55 @@ remove_key_for_empty_collection(key) response end + def zpopmax(key, count = nil) + data_type_check(key, ZSet) + return [] unless data[key] + sorted_members = sort_keys(data[key]) + results = sorted_members.last(count || 1).reverse! + results.each do |member| + zrem(key, member.first) + end + count.nil? ? results.first : results.flatten + end + + def zpopmin(key, count = nil) + data_type_check(key, ZSet) + return [] unless data[key] + sorted_members = sort_keys(data[key]) + results = sorted_members.first(count || 1) + results.each do |member| + zrem(key, member.first) + end + count.nil? ? results.first : results.flatten + end + + def bzpopmax(*args) + bzpop(:bzpopmax, args) + end + + def bzpopmin(*args) + bzpop(:bzpopmin, args) + end + def zcard(key) data_type_check(key, ZSet) data[key] ? data[key].size : 0 end def zscore(key, value) data_type_check(key, ZSet) value = data[key] && data[key][value.to_s] - value && value.to_s + if value == Float::INFINITY + "inf" + elsif value == -Float::INFINITY + "-inf" + elsif value + value.to_s + end end def zcount(key, min, max) data_type_check(key, ZSet) return 0 unless data[key] @@ -1067,11 +1150,18 @@ def zincrby(key, num, value) data_type_check(key, ZSet) data[key] ||= ZSet.new data[key][value.to_s] ||= 0 data[key].increment(value.to_s, num) - data[key][value.to_s].to_s + + if num =~ /^\+?inf/ + "inf" + elsif num == "-inf" + "-inf" + else + data[key][value.to_s].to_s + end end def zrank(key, value) data_type_check(key, ZSet) z = data[key] @@ -1091,10 +1181,11 @@ return [] unless data[key] results = sort_keys(data[key]) # Select just the keys unless we want scores results = results.map(&:first) unless with_scores + start = [start, -results.size].max (results[start..stop] || []).flatten.map(&:to_s) end def zrangebylex(key, start, stop, *opts) data_type_check(key, ZSet) @@ -1206,10 +1297,37 @@ args_handler = SortedSetArgumentHandler.new(args) data[out] = SortedSetUnionStore.new(args_handler, data).call data[out].size end + def pfadd(key, member) + data_type_check(key, Set) + data[key] ||= Set.new + previous_size = data[key].size + data[key] |= Array(member) + data[key].size != previous_size + end + + def pfcount(*keys) + keys = keys.flatten + raise_argument_error("pfcount") if keys.empty? + keys.each { |key| data_type_check(key, Set) } + if keys.count == 1 + (data[keys.first] || Set.new).size + else + union = keys.map { |key| data[key] }.compact.reduce(&:|) + union.size + end + end + + def pfmerge(destination, *sources) + sources.each { |source| data_type_check(source, Set) } + union = sources.map { |source| data[source] || Set.new }.reduce(&:|) + data[destination] = union + "OK" + end + def subscribe(*channels) raise_argument_error('subscribe') if channels.empty?() #Create messages for all data from the channels channel_replies = channels.map do |channel| @@ -1346,17 +1464,40 @@ def raise_syntax_error raise Redis::CommandError, "ERR syntax error" end + def raise_options_error(*options) + if options.detect { |opt| opt.match(/incr/i) } + error_message = "ERR INCR option supports a single increment-element pair" + else + error_message = "ERR #{options.join(" and ")} options at the same time are not compatible" + end + raise Redis::CommandError, error_message + end + + def raise_command_error(message) + raise Redis::CommandError, message + end + + def delete_keys(keys, command) + keys = keys.flatten(1) + raise_argument_error(command) if keys.empty? + + old_count = data.keys.size + keys.each do |key| + data.delete(key) + end + old_count - data.keys.size + end + def remove_key_for_empty_collection(key) del(key) if data[key] && data[key].empty? end def data_type_check(key, klass) if data[key] && !data[key].is_a?(klass) - warn "Operation against a key holding the wrong kind of value: Expected #{klass} at #{key}." raise Redis::CommandError.new("WRONGTYPE Operation against a key holding the wrong kind of value") end end def get_range(start, stop, min = -Float::INFINITY, max = Float::INFINITY) @@ -1417,12 +1558,36 @@ else (1..-number).map { data[key].to_a[rand(data[key].size)] }.flatten end end + def bzpop(command, args) + timeout = + if args.last.is_a?(Hash) + args.pop[:timeout] + elsif args.last.respond_to?(:to_int) + args.pop.to_int + end + + timeout ||= 0 + single_pop_command = command.to_s[1..-1] + keys = args.flatten + keys.each do |key| + if data[key] + data_type_check(data[key], ZSet) + if data[key].size > 0 + result = public_send(single_pop_command, key) + return result.unshift(key) + end + end + end + sleep(timeout.to_f) + nil + end + def sort_keys(arr) # Sort by score, or if scores are equal, key alphanum - sorted_keys = arr.sort do |(k1, v1), (k2, v2)| + arr.sort do |(k1, v1), (k2, v2)| if v1 == v2 k1 <=> k2 else v1 <=> v2 end