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