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