kuende-fakeredis 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +19 -0
- data/Gemfile +13 -0
- data/Guardfile +8 -0
- data/LICENSE +21 -0
- data/README.md +102 -0
- data/Rakefile +27 -0
- data/fakeredis.gemspec +24 -0
- data/gemfiles/redisrb-master.gemfile +14 -0
- data/lib/fake_redis.rb +1 -0
- data/lib/fakeredis.rb +6 -0
- data/lib/fakeredis/bitop_command.rb +56 -0
- data/lib/fakeredis/command_executor.rb +25 -0
- data/lib/fakeredis/expiring_hash.rb +70 -0
- data/lib/fakeredis/minitest.rb +24 -0
- data/lib/fakeredis/rspec.rb +24 -0
- data/lib/fakeredis/sort_method.rb +117 -0
- data/lib/fakeredis/sorted_set_argument_handler.rb +74 -0
- data/lib/fakeredis/sorted_set_store.rb +80 -0
- data/lib/fakeredis/transaction_commands.rb +83 -0
- data/lib/fakeredis/version.rb +3 -0
- data/lib/fakeredis/zset.rb +39 -0
- data/lib/redis/connection/memory.rb +1375 -0
- data/spec/bitop_command_spec.rb +209 -0
- data/spec/compatibility_spec.rb +9 -0
- data/spec/connection_spec.rb +85 -0
- data/spec/hashes_spec.rb +261 -0
- data/spec/keys_spec.rb +488 -0
- data/spec/lists_spec.rb +229 -0
- data/spec/memory_spec.rb +28 -0
- data/spec/server_spec.rb +100 -0
- data/spec/sets_spec.rb +280 -0
- data/spec/sort_method_spec.rb +74 -0
- data/spec/sorted_sets_spec.rb +578 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/spec_helper_live_redis.rb +14 -0
- data/spec/strings_spec.rb +289 -0
- data/spec/subscription_spec.rb +107 -0
- data/spec/support/shared_examples/bitwise_operation.rb +59 -0
- data/spec/support/shared_examples/sortable.rb +69 -0
- data/spec/transactions_spec.rb +92 -0
- data/spec/upcase_method_name_spec.rb +18 -0
- metadata +148 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Require this either in your Gemfile or in RSpec's
|
|
2
|
+
# support scripts. Examples:
|
|
3
|
+
#
|
|
4
|
+
# # Gemfile
|
|
5
|
+
# group :test do
|
|
6
|
+
# gem "rspec"
|
|
7
|
+
# gem "fakeredis", :require => "fakeredis/rspec"
|
|
8
|
+
# end
|
|
9
|
+
#
|
|
10
|
+
# # spec/support/fakeredis.rb
|
|
11
|
+
# require 'fakeredis/rspec'
|
|
12
|
+
#
|
|
13
|
+
|
|
14
|
+
require 'rspec/core'
|
|
15
|
+
require 'fakeredis'
|
|
16
|
+
|
|
17
|
+
RSpec.configure do |c|
|
|
18
|
+
|
|
19
|
+
c.before do
|
|
20
|
+
Redis::Connection::Memory.reset_all_databases
|
|
21
|
+
Redis::Connection::Memory.reset_all_channels
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Codes are mostly referenced from MockRedis' implementation.
|
|
2
|
+
module FakeRedis
|
|
3
|
+
module SortMethod
|
|
4
|
+
def sort(key, *redis_options_array)
|
|
5
|
+
return [] unless key
|
|
6
|
+
return [] if type(key) == 'none'
|
|
7
|
+
|
|
8
|
+
unless %w(list set zset).include? type(key)
|
|
9
|
+
warn "Operation against a key holding the wrong kind of value: Expected list, set or zset at #{key}."
|
|
10
|
+
raise Redis::CommandError.new("WRONGTYPE Operation against a key holding the wrong kind of value")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# redis_options is an array of format [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]
|
|
14
|
+
# Lets nibble it back into a hash
|
|
15
|
+
options = extract_options_from(redis_options_array)
|
|
16
|
+
|
|
17
|
+
# And now to actually do the work of this method
|
|
18
|
+
|
|
19
|
+
projected = project(data[key], options[:by], options[:get])
|
|
20
|
+
sorted = sort_by(projected, options[:order])
|
|
21
|
+
sliced = slice(sorted, options[:limit])
|
|
22
|
+
# We have to flatten it down as redis-rb adds back the array to the return value
|
|
23
|
+
result = sliced.flatten(1)
|
|
24
|
+
|
|
25
|
+
options[:store] ? rpush(options[:store], sliced) : result
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
ASCENDING_SORT = Proc.new { |a, b| a.first <=> b.first }
|
|
31
|
+
DESCENDING_SORT = Proc.new { |a, b| b.first <=> a.first }
|
|
32
|
+
|
|
33
|
+
def extract_options_from(options_array)
|
|
34
|
+
# Defaults
|
|
35
|
+
options = {
|
|
36
|
+
:limit => [],
|
|
37
|
+
:order => "ASC",
|
|
38
|
+
:get => []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if options_array.first == "BY"
|
|
42
|
+
options_array.shift
|
|
43
|
+
options[:by] = options_array.shift
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if options_array.first == "LIMIT"
|
|
47
|
+
options_array.shift
|
|
48
|
+
options[:limit] = [options_array.shift, options_array.shift]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
while options_array.first == "GET"
|
|
52
|
+
options_array.shift
|
|
53
|
+
options[:get] << options_array.shift
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if %w(ASC DESC ALPHA).include?(options_array.first)
|
|
57
|
+
options[:order] = options_array.shift
|
|
58
|
+
options[:order] = "ASC" if options[:order] == "ALPHA"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if options_array.first == "STORE"
|
|
62
|
+
options_array.shift
|
|
63
|
+
options[:store] = options_array.shift
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
options
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def project(enumerable, by, get_patterns)
|
|
70
|
+
enumerable.map do |*elements|
|
|
71
|
+
element = elements.flatten.first
|
|
72
|
+
weight = by ? lookup_from_pattern(by, element) : element
|
|
73
|
+
value = element
|
|
74
|
+
|
|
75
|
+
if get_patterns.length > 0
|
|
76
|
+
value = get_patterns.map do |pattern|
|
|
77
|
+
pattern == "#" ? element : lookup_from_pattern(pattern, element)
|
|
78
|
+
end
|
|
79
|
+
value = value.first if value.length == 1
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
[weight, value]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def sort_by(projected, direction)
|
|
87
|
+
sorter =
|
|
88
|
+
case direction.upcase
|
|
89
|
+
when "DESC"
|
|
90
|
+
DESCENDING_SORT
|
|
91
|
+
when "ASC", "ALPHA"
|
|
92
|
+
ASCENDING_SORT
|
|
93
|
+
else
|
|
94
|
+
raise "Invalid direction '#{direction}'"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
projected.sort(&sorter).map(&:last)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def slice(sorted, limit)
|
|
101
|
+
skip = limit.first || 0
|
|
102
|
+
take = limit.last || sorted.length
|
|
103
|
+
|
|
104
|
+
sorted[skip...(skip + take)] || sorted
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def lookup_from_pattern(pattern, element)
|
|
108
|
+
key = pattern.sub('*', element)
|
|
109
|
+
|
|
110
|
+
if (hash_parts = key.split('->')).length > 1
|
|
111
|
+
hget hash_parts.first, hash_parts.last
|
|
112
|
+
else
|
|
113
|
+
get key
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module FakeRedis
|
|
2
|
+
# Takes in the variable length array of arguments for a zinterstore/zunionstore method
|
|
3
|
+
# and parses them into a few attributes for the method to access.
|
|
4
|
+
#
|
|
5
|
+
# Handles throwing errors for various scenarios (matches redis):
|
|
6
|
+
# * Custom weights specified, but not enough or too many given
|
|
7
|
+
# * Invalid aggregate value given
|
|
8
|
+
# * Multiple aggregate values given
|
|
9
|
+
class SortedSetArgumentHandler
|
|
10
|
+
# [Symbol] The aggregate method to use for the output values. One of %w(sum min max) expected
|
|
11
|
+
attr_reader :aggregate
|
|
12
|
+
# [Integer] Number of keys in the argument list
|
|
13
|
+
attr_accessor :number_of_keys
|
|
14
|
+
# [Array] The actual keys in the argument list
|
|
15
|
+
attr_accessor :keys
|
|
16
|
+
# [Array] integers for weighting the values of each key - one number per key expected
|
|
17
|
+
attr_accessor :weights
|
|
18
|
+
|
|
19
|
+
# Used internally
|
|
20
|
+
attr_accessor :type
|
|
21
|
+
|
|
22
|
+
# Expects all the argments for the method to be passed as an array
|
|
23
|
+
def initialize args
|
|
24
|
+
# Pull out known lengths of data
|
|
25
|
+
self.number_of_keys = args.shift
|
|
26
|
+
self.keys = args.shift(number_of_keys)
|
|
27
|
+
# Handle the variable lengths of data (WEIGHTS/AGGREGATE)
|
|
28
|
+
args.inject(self) {|handler, item| handler.handle(item) }
|
|
29
|
+
|
|
30
|
+
# Defaults for unspecified things
|
|
31
|
+
self.weights ||= Array.new(number_of_keys) { 1 }
|
|
32
|
+
self.aggregate ||= :sum
|
|
33
|
+
|
|
34
|
+
# Validate values
|
|
35
|
+
raise(Redis::CommandError, "ERR syntax error") unless weights.size == number_of_keys
|
|
36
|
+
raise(Redis::CommandError, "ERR syntax error") unless [:min, :max, :sum].include?(aggregate)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Only allows assigning a value *once* - raises Redis::CommandError if a second is given
|
|
40
|
+
def aggregate=(str)
|
|
41
|
+
raise(Redis::CommandError, "ERR syntax error") if (defined?(@aggregate) && @aggregate)
|
|
42
|
+
@aggregate = str.to_s.downcase.to_sym
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Decides how to handle an item, depending on where we are in the arguments
|
|
46
|
+
def handle(item)
|
|
47
|
+
case item
|
|
48
|
+
when "WEIGHTS"
|
|
49
|
+
self.type = :weights
|
|
50
|
+
self.weights = []
|
|
51
|
+
when "AGGREGATE"
|
|
52
|
+
self.type = :aggregate
|
|
53
|
+
when nil
|
|
54
|
+
# This should never be called, raise a syntax error if we manage to hit it
|
|
55
|
+
raise(Redis::CommandError, "ERR syntax error")
|
|
56
|
+
else
|
|
57
|
+
send "handle_#{type}", item
|
|
58
|
+
end
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def handle_weights(item)
|
|
63
|
+
self.weights << item
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def handle_aggregate(item)
|
|
67
|
+
self.aggregate = item
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def inject_block
|
|
71
|
+
lambda { |handler, item| handler.handle(item) }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module FakeRedis
|
|
2
|
+
class SortedSetStore
|
|
3
|
+
attr_accessor :data, :weights, :aggregate, :keys
|
|
4
|
+
|
|
5
|
+
def initialize params, data
|
|
6
|
+
self.data = data
|
|
7
|
+
self.weights = params.weights
|
|
8
|
+
self.aggregate = params.aggregate
|
|
9
|
+
self.keys = params.keys
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def hashes
|
|
13
|
+
@hashes ||= keys.map do |src|
|
|
14
|
+
case data[src]
|
|
15
|
+
when ::Set
|
|
16
|
+
# Every value has a score of 1
|
|
17
|
+
Hash[data[src].map {|k,v| [k, 1]}]
|
|
18
|
+
when Hash
|
|
19
|
+
data[src]
|
|
20
|
+
else
|
|
21
|
+
{}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Apply the weightings to the hashes
|
|
27
|
+
def computed_values
|
|
28
|
+
unless defined?(@computed_values) && @computed_values
|
|
29
|
+
# Do nothing if all weights are 1, as n * 1 is n
|
|
30
|
+
@computed_values = hashes if weights.all? {|weight| weight == 1 }
|
|
31
|
+
# Otherwise, multiply the values in each hash by that hash's weighting
|
|
32
|
+
@computed_values ||= hashes.each_with_index.map do |hash, index|
|
|
33
|
+
weight = weights[index]
|
|
34
|
+
Hash[hash.map {|k, v| [k, (v * weight)]}]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
@computed_values
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def aggregate_sum out
|
|
41
|
+
selected_keys.each do |key|
|
|
42
|
+
out[key] = computed_values.inject(0) do |n, hash|
|
|
43
|
+
n + (hash[key] || 0)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def aggregate_min out
|
|
49
|
+
selected_keys.each do |key|
|
|
50
|
+
out[key] = computed_values.map {|h| h[key] }.compact.min
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def aggregate_max out
|
|
55
|
+
selected_keys.each do |key|
|
|
56
|
+
out[key] = computed_values.map {|h| h[key] }.compact.max
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def selected_keys
|
|
61
|
+
raise NotImplemented, "subclass needs to implement #selected_keys"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def call
|
|
65
|
+
ZSet.new.tap {|out| send("aggregate_#{aggregate}", out) }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class SortedSetIntersectStore < SortedSetStore
|
|
70
|
+
def selected_keys
|
|
71
|
+
@values ||= hashes.inject([]) { |r, h| r.empty? ? h.keys : (r & h.keys) }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class SortedSetUnionStore < SortedSetStore
|
|
76
|
+
def selected_keys
|
|
77
|
+
@values ||= hashes.map(&:keys).flatten.uniq
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module FakeRedis
|
|
2
|
+
TRANSACTION_COMMANDS = [:discard, :exec, :multi, :watch, :unwatch]
|
|
3
|
+
|
|
4
|
+
module TransactionCommands
|
|
5
|
+
def self.included(klass)
|
|
6
|
+
klass.class_eval do
|
|
7
|
+
def self.queued_commands
|
|
8
|
+
@queued_commands ||= Hash.new {|h,k| h[k] = [] }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.in_multi
|
|
12
|
+
@in_multi ||= Hash.new{|h,k| h[k] = false}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def queued_commands
|
|
16
|
+
self.class.queued_commands[database_instance_key]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def queued_commands=(cmds)
|
|
20
|
+
self.class.queued_commands[database_instance_key] = cmds
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def in_multi
|
|
24
|
+
self.class.in_multi[database_instance_key]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def in_multi=(multi_state)
|
|
28
|
+
self.class.in_multi[database_instance_key] = multi_state
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def discard
|
|
34
|
+
unless in_multi
|
|
35
|
+
raise Redis::CommandError, "ERR DISCARD without MULTI"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
self.in_multi = false
|
|
39
|
+
self.queued_commands = []
|
|
40
|
+
|
|
41
|
+
'OK'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def exec
|
|
45
|
+
unless in_multi
|
|
46
|
+
raise Redis::CommandError, "ERR EXEC without MULTI"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
responses = queued_commands.map do |cmd|
|
|
50
|
+
begin
|
|
51
|
+
send(*cmd)
|
|
52
|
+
rescue => e
|
|
53
|
+
e
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
self.queued_commands = [] # reset queued_commands
|
|
58
|
+
self.in_multi = false # reset in_multi state
|
|
59
|
+
|
|
60
|
+
responses
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def multi
|
|
64
|
+
if in_multi
|
|
65
|
+
raise Redis::CommandError, "ERR MULTI calls can not be nested"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
self.in_multi = true
|
|
69
|
+
|
|
70
|
+
yield(self) if block_given?
|
|
71
|
+
|
|
72
|
+
"OK"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def watch(*_)
|
|
76
|
+
"OK"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def unwatch
|
|
80
|
+
"OK"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module FakeRedis
|
|
2
|
+
class ZSet < Hash
|
|
3
|
+
|
|
4
|
+
def []=(key, val)
|
|
5
|
+
super(key, _floatify(val))
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def identical_scores?
|
|
9
|
+
values.uniq.size == 1
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Increments the value of key by val
|
|
13
|
+
def increment(key, val)
|
|
14
|
+
self[key] += _floatify(val)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def select_by_score min, max
|
|
18
|
+
min = _floatify(min, true)
|
|
19
|
+
max = _floatify(max, false)
|
|
20
|
+
reject {|_,v| v < min || v > max }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Originally lifted from redis-rb
|
|
26
|
+
def _floatify(str, increment = true)
|
|
27
|
+
if (( inf = str.to_s.match(/^([+-])?inf/i) ))
|
|
28
|
+
(inf[1] == "-" ? -1.0 : 1.0) / 0.0
|
|
29
|
+
elsif (( number = str.to_s.match(/^\((\d+)/i) ))
|
|
30
|
+
number[1].to_i + (increment ? 1 : -1)
|
|
31
|
+
else
|
|
32
|
+
Float str.to_s
|
|
33
|
+
end
|
|
34
|
+
rescue ArgumentError
|
|
35
|
+
raise Redis::CommandError, "ERR value is not a valid float"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,1375 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
require 'redis/connection/registry'
|
|
3
|
+
require 'redis/connection/command_helper'
|
|
4
|
+
require "fakeredis/command_executor"
|
|
5
|
+
require "fakeredis/expiring_hash"
|
|
6
|
+
require "fakeredis/sort_method"
|
|
7
|
+
require "fakeredis/sorted_set_argument_handler"
|
|
8
|
+
require "fakeredis/sorted_set_store"
|
|
9
|
+
require "fakeredis/transaction_commands"
|
|
10
|
+
require "fakeredis/zset"
|
|
11
|
+
require "fakeredis/bitop_command"
|
|
12
|
+
require "fakeredis/version"
|
|
13
|
+
|
|
14
|
+
class Redis
|
|
15
|
+
module Connection
|
|
16
|
+
class Memory
|
|
17
|
+
include Redis::Connection::CommandHelper
|
|
18
|
+
include FakeRedis
|
|
19
|
+
include SortMethod
|
|
20
|
+
include TransactionCommands
|
|
21
|
+
include BitopCommand
|
|
22
|
+
include CommandExecutor
|
|
23
|
+
|
|
24
|
+
attr_accessor :options
|
|
25
|
+
|
|
26
|
+
# Tracks all databases for all instances across the current process.
|
|
27
|
+
# We have to be able to handle two clients with the same host/port accessing
|
|
28
|
+
# different databases at once without overwriting each other. So we store our
|
|
29
|
+
# "data" outside the client instances, in this class level instance method.
|
|
30
|
+
# Client instances access it with a key made up of their host/port, and then select
|
|
31
|
+
# which DB out of the array of them they want. Allows the access we need.
|
|
32
|
+
def self.databases
|
|
33
|
+
@databases ||= Hash.new {|h,k| h[k] = [] }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Used for resetting everything in specs
|
|
37
|
+
def self.reset_all_databases
|
|
38
|
+
@databases = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.channels
|
|
42
|
+
@channels ||= Hash.new {|h,k| h[k] = [] }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.reset_all_channels
|
|
46
|
+
@channels = nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.connect(options = {})
|
|
50
|
+
new(options)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def initialize(options = {})
|
|
54
|
+
self.options = options
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def database_id
|
|
58
|
+
@database_id ||= 0
|
|
59
|
+
end
|
|
60
|
+
attr_writer :database_id
|
|
61
|
+
|
|
62
|
+
def database_instance_key
|
|
63
|
+
[options[:host], options[:port]].hash
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def databases
|
|
67
|
+
self.class.databases[database_instance_key]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def find_database id=database_id
|
|
71
|
+
databases[id] ||= ExpiringHash.new
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def data
|
|
75
|
+
find_database
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def replies
|
|
79
|
+
@replies ||= []
|
|
80
|
+
end
|
|
81
|
+
attr_writer :replies
|
|
82
|
+
|
|
83
|
+
def connected?
|
|
84
|
+
true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def connect_unix(path, timeout)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def disconnect
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def timeout=(usecs)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def read
|
|
97
|
+
replies.shift
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def flushdb
|
|
101
|
+
databases.delete_at(database_id)
|
|
102
|
+
"OK"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def flushall
|
|
106
|
+
self.class.databases[database_instance_key] = []
|
|
107
|
+
"OK"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def auth(password)
|
|
111
|
+
"OK"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def select(index)
|
|
115
|
+
data_type_check(index, Integer)
|
|
116
|
+
self.database_id = index
|
|
117
|
+
"OK"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def info
|
|
121
|
+
{
|
|
122
|
+
"redis_version" => "2.6.16",
|
|
123
|
+
"connected_clients" => "1",
|
|
124
|
+
"connected_slaves" => "0",
|
|
125
|
+
"used_memory" => "3187",
|
|
126
|
+
"changes_since_last_save" => "0",
|
|
127
|
+
"last_save_time" => "1237655729",
|
|
128
|
+
"total_connections_received" => "1",
|
|
129
|
+
"total_commands_processed" => "1",
|
|
130
|
+
"uptime_in_seconds" => "36000",
|
|
131
|
+
"uptime_in_days" => 0
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def monitor; end
|
|
136
|
+
|
|
137
|
+
def save; end
|
|
138
|
+
|
|
139
|
+
def bgsave ; end
|
|
140
|
+
|
|
141
|
+
def bgrewriteaof ; end
|
|
142
|
+
|
|
143
|
+
def move key, destination_id
|
|
144
|
+
raise Redis::CommandError, "ERR source and destination objects are the same" if destination_id == database_id
|
|
145
|
+
destination = find_database(destination_id)
|
|
146
|
+
return false unless data.has_key?(key)
|
|
147
|
+
return false if destination.has_key?(key)
|
|
148
|
+
destination[key] = data.delete(key)
|
|
149
|
+
true
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def dump(key)
|
|
153
|
+
return nil unless exists(key)
|
|
154
|
+
|
|
155
|
+
value = data[key]
|
|
156
|
+
|
|
157
|
+
Marshal.dump(
|
|
158
|
+
value: value,
|
|
159
|
+
version: FakeRedis::VERSION, # Redis includes the version, so we might as well
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def restore(key, ttl, serialized_value)
|
|
164
|
+
raise Redis::CommandError, "ERR Target key name is busy." if exists(key)
|
|
165
|
+
|
|
166
|
+
raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong" if serialized_value.nil?
|
|
167
|
+
|
|
168
|
+
parsed_value = begin
|
|
169
|
+
Marshal.load(serialized_value)
|
|
170
|
+
rescue TypeError
|
|
171
|
+
raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
if parsed_value[:version] != FakeRedis::VERSION
|
|
175
|
+
raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# We could figure out what type the key was and set it with the public API here,
|
|
179
|
+
# or we could just assign the value. If we presume the serialized_value is only ever
|
|
180
|
+
# a return value from `dump` then we've only been given something that was in
|
|
181
|
+
# the internal data structure anyway.
|
|
182
|
+
data[key] = parsed_value[:value]
|
|
183
|
+
|
|
184
|
+
# Set a TTL if one has been passed
|
|
185
|
+
ttl = ttl.to_i # Makes nil into 0
|
|
186
|
+
expire(key, ttl / 1000) unless ttl.zero?
|
|
187
|
+
|
|
188
|
+
"OK"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def get(key)
|
|
192
|
+
data_type_check(key, String)
|
|
193
|
+
data[key]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def getbit(key, offset)
|
|
197
|
+
return unless data[key]
|
|
198
|
+
data[key].unpack('B*')[0].split("")[offset].to_i
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def bitcount(key, start_index = 0, end_index = -1)
|
|
202
|
+
return 0 unless data[key]
|
|
203
|
+
data[key][start_index..end_index].unpack('B*')[0].count("1")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def getrange(key, start, ending)
|
|
207
|
+
return unless data[key]
|
|
208
|
+
data[key][start..ending]
|
|
209
|
+
end
|
|
210
|
+
alias :substr :getrange
|
|
211
|
+
|
|
212
|
+
def getset(key, value)
|
|
213
|
+
data_type_check(key, String)
|
|
214
|
+
data[key].tap do
|
|
215
|
+
set(key, value)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def mget(*keys)
|
|
220
|
+
raise_argument_error('mget') if keys.empty?
|
|
221
|
+
# We work with either an array, or list of arguments
|
|
222
|
+
keys = keys.first if keys.size == 1
|
|
223
|
+
data.values_at(*keys)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def append(key, value)
|
|
227
|
+
data[key] = (data[key] || "")
|
|
228
|
+
data[key] = data[key] + value.to_s
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def strlen(key)
|
|
232
|
+
return unless data[key]
|
|
233
|
+
data[key].size
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def hgetall(key)
|
|
237
|
+
data_type_check(key, Hash)
|
|
238
|
+
data[key].to_a.flatten || {}
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def hget(key, field)
|
|
242
|
+
data_type_check(key, Hash)
|
|
243
|
+
data[key] && data[key][field.to_s]
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def hdel(key, field)
|
|
247
|
+
data_type_check(key, Hash)
|
|
248
|
+
return 0 unless data[key]
|
|
249
|
+
|
|
250
|
+
if field.is_a?(Array)
|
|
251
|
+
old_keys_count = data[key].size
|
|
252
|
+
fields = field.map(&:to_s)
|
|
253
|
+
|
|
254
|
+
data[key].delete_if { |k, v| fields.include? k }
|
|
255
|
+
deleted = old_keys_count - data[key].size
|
|
256
|
+
else
|
|
257
|
+
field = field.to_s
|
|
258
|
+
deleted = data[key].delete(field) ? 1 : 0
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
remove_key_for_empty_collection(key)
|
|
262
|
+
deleted
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def hkeys(key)
|
|
266
|
+
data_type_check(key, Hash)
|
|
267
|
+
return [] if data[key].nil?
|
|
268
|
+
data[key].keys
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def hscan(key, start_cursor, *args)
|
|
272
|
+
data_type_check(key, Hash)
|
|
273
|
+
return ["0", []] unless data[key]
|
|
274
|
+
|
|
275
|
+
match = "*"
|
|
276
|
+
count = 10
|
|
277
|
+
|
|
278
|
+
if args.size.odd?
|
|
279
|
+
raise_argument_error('hscan')
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
if idx = args.index("MATCH")
|
|
283
|
+
match = args[idx + 1]
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
if idx = args.index("COUNT")
|
|
287
|
+
count = args[idx + 1]
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
start_cursor = start_cursor.to_i
|
|
291
|
+
|
|
292
|
+
cursor = start_cursor
|
|
293
|
+
next_keys = []
|
|
294
|
+
|
|
295
|
+
if start_cursor + count >= data[key].length
|
|
296
|
+
next_keys = (data[key].to_a)[start_cursor..-1]
|
|
297
|
+
cursor = 0
|
|
298
|
+
else
|
|
299
|
+
cursor = start_cursor + count
|
|
300
|
+
next_keys = (data[key].to_a)[start_cursor..cursor-1]
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
filtered_next_keys = next_keys.select{|k,v| File.fnmatch(match, k)}
|
|
304
|
+
result = filtered_next_keys.flatten.map(&:to_s)
|
|
305
|
+
|
|
306
|
+
return ["#{cursor}", result]
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def keys(pattern = "*")
|
|
310
|
+
data.keys.select { |key| File.fnmatch(pattern, key) }
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def randomkey
|
|
314
|
+
data.keys[rand(dbsize)]
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def echo(string)
|
|
318
|
+
string
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def ping
|
|
322
|
+
"PONG"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def lastsave
|
|
326
|
+
Time.now.to_i
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def time
|
|
330
|
+
microseconds = (Time.now.to_f * 1000000).to_i
|
|
331
|
+
[ microseconds / 1000000, microseconds % 1000000 ]
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def dbsize
|
|
335
|
+
data.keys.count
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def exists(key)
|
|
339
|
+
data.key?(key)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def llen(key)
|
|
343
|
+
data_type_check(key, Array)
|
|
344
|
+
return 0 unless data[key]
|
|
345
|
+
data[key].size
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def lrange(key, startidx, endidx)
|
|
349
|
+
data_type_check(key, Array)
|
|
350
|
+
(data[key] && data[key][startidx..endidx]) || []
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def ltrim(key, start, stop)
|
|
354
|
+
data_type_check(key, Array)
|
|
355
|
+
return unless data[key]
|
|
356
|
+
|
|
357
|
+
# Example: we have a list of 3 elements and
|
|
358
|
+
# we give it a ltrim list, -5, -1. This means
|
|
359
|
+
# it should trim to a max of 5. Since 3 < 5
|
|
360
|
+
# we should not touch the list. This is consistent
|
|
361
|
+
# with behavior of real Redis's ltrim with a negative
|
|
362
|
+
# start argument.
|
|
363
|
+
unless start < 0 && data[key].count < start.abs
|
|
364
|
+
data[key] = data[key][start..stop]
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
"OK"
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def lindex(key, index)
|
|
371
|
+
data_type_check(key, Array)
|
|
372
|
+
data[key] && data[key][index]
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def linsert(key, where, pivot, value)
|
|
376
|
+
data_type_check(key, Array)
|
|
377
|
+
return unless data[key]
|
|
378
|
+
|
|
379
|
+
value = value.to_s
|
|
380
|
+
index = data[key].index(pivot.to_s)
|
|
381
|
+
return -1 if index.nil?
|
|
382
|
+
|
|
383
|
+
case where
|
|
384
|
+
when :before then data[key].insert(index, value)
|
|
385
|
+
when :after then data[key].insert(index + 1, value)
|
|
386
|
+
else raise_syntax_error
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def lset(key, index, value)
|
|
391
|
+
data_type_check(key, Array)
|
|
392
|
+
return unless data[key]
|
|
393
|
+
raise Redis::CommandError, "ERR index out of range" if index >= data[key].size
|
|
394
|
+
data[key][index] = value.to_s
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def lrem(key, count, value)
|
|
398
|
+
data_type_check(key, Array)
|
|
399
|
+
return 0 unless data[key]
|
|
400
|
+
|
|
401
|
+
value = value.to_s
|
|
402
|
+
old_size = data[key].size
|
|
403
|
+
diff =
|
|
404
|
+
if count == 0
|
|
405
|
+
data[key].delete(value)
|
|
406
|
+
old_size - data[key].size
|
|
407
|
+
else
|
|
408
|
+
array = count > 0 ? data[key].dup : data[key].reverse
|
|
409
|
+
count.abs.times{ array.delete_at(array.index(value) || array.length) }
|
|
410
|
+
data[key] = count > 0 ? array.dup : array.reverse
|
|
411
|
+
old_size - data[key].size
|
|
412
|
+
end
|
|
413
|
+
remove_key_for_empty_collection(key)
|
|
414
|
+
diff
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def rpush(key, value)
|
|
418
|
+
raise_argument_error('rpush') if value.respond_to?(:each) && value.empty?
|
|
419
|
+
data_type_check(key, Array)
|
|
420
|
+
data[key] ||= []
|
|
421
|
+
[value].flatten.each do |val|
|
|
422
|
+
data[key].push(val.to_s)
|
|
423
|
+
end
|
|
424
|
+
data[key].size
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def rpushx(key, value)
|
|
428
|
+
raise_argument_error('rpushx') if value.respond_to?(:each) && value.empty?
|
|
429
|
+
data_type_check(key, Array)
|
|
430
|
+
return unless data[key]
|
|
431
|
+
rpush(key, value)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def lpush(key, value)
|
|
435
|
+
raise_argument_error('lpush') if value.respond_to?(:each) && value.empty?
|
|
436
|
+
data_type_check(key, Array)
|
|
437
|
+
data[key] ||= []
|
|
438
|
+
[value].flatten.each do |val|
|
|
439
|
+
data[key].unshift(val.to_s)
|
|
440
|
+
end
|
|
441
|
+
data[key].size
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def lpushx(key, value)
|
|
445
|
+
raise_argument_error('lpushx') if value.respond_to?(:each) && value.empty?
|
|
446
|
+
data_type_check(key, Array)
|
|
447
|
+
return unless data[key]
|
|
448
|
+
lpush(key, value)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def rpop(key)
|
|
452
|
+
data_type_check(key, Array)
|
|
453
|
+
return unless data[key]
|
|
454
|
+
data[key].pop
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def brpop(keys, timeout=0)
|
|
458
|
+
#todo threaded mode
|
|
459
|
+
keys = Array(keys)
|
|
460
|
+
keys.each do |key|
|
|
461
|
+
if data[key] && data[key].size > 0
|
|
462
|
+
return [key, data[key].pop]
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
sleep(timeout.to_f)
|
|
466
|
+
nil
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def rpoplpush(key1, key2)
|
|
470
|
+
data_type_check(key1, Array)
|
|
471
|
+
rpop(key1).tap do |elem|
|
|
472
|
+
lpush(key2, elem) unless elem.nil?
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def brpoplpush(key1, key2, opts={})
|
|
477
|
+
data_type_check(key1, Array)
|
|
478
|
+
brpop(key1).tap do |elem|
|
|
479
|
+
lpush(key2, elem) unless elem.nil?
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def lpop(key)
|
|
484
|
+
data_type_check(key, Array)
|
|
485
|
+
return unless data[key]
|
|
486
|
+
data[key].shift
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def blpop(keys, timeout=0)
|
|
490
|
+
#todo threaded mode
|
|
491
|
+
keys = Array(keys)
|
|
492
|
+
keys.each do |key|
|
|
493
|
+
if data[key] && data[key].size > 0
|
|
494
|
+
return [key, data[key].shift]
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
sleep(timeout.to_f)
|
|
498
|
+
nil
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def smembers(key)
|
|
502
|
+
data_type_check(key, ::Set)
|
|
503
|
+
return [] unless data[key]
|
|
504
|
+
data[key].to_a.reverse
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def sismember(key, value)
|
|
508
|
+
data_type_check(key, ::Set)
|
|
509
|
+
return false unless data[key]
|
|
510
|
+
data[key].include?(value.to_s)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def sadd(key, value)
|
|
514
|
+
data_type_check(key, ::Set)
|
|
515
|
+
value = Array(value)
|
|
516
|
+
raise_argument_error('sadd') if value.empty?
|
|
517
|
+
|
|
518
|
+
result = if data[key]
|
|
519
|
+
old_set = data[key].dup
|
|
520
|
+
data[key].merge(value.map(&:to_s))
|
|
521
|
+
(data[key] - old_set).size
|
|
522
|
+
else
|
|
523
|
+
data[key] = ::Set.new(value.map(&:to_s))
|
|
524
|
+
data[key].size
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# 0 = false, 1 = true, 2+ untouched
|
|
528
|
+
return result == 1 if result < 2
|
|
529
|
+
result
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def srem(key, value)
|
|
533
|
+
data_type_check(key, ::Set)
|
|
534
|
+
return false unless data[key]
|
|
535
|
+
|
|
536
|
+
if value.is_a?(Array)
|
|
537
|
+
old_size = data[key].size
|
|
538
|
+
values = value.map(&:to_s)
|
|
539
|
+
values.each { |v| data[key].delete(v) }
|
|
540
|
+
deleted = old_size - data[key].size
|
|
541
|
+
else
|
|
542
|
+
deleted = !!data[key].delete?(value.to_s)
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
remove_key_for_empty_collection(key)
|
|
546
|
+
deleted
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def smove(source, destination, value)
|
|
550
|
+
data_type_check(destination, ::Set)
|
|
551
|
+
result = self.srem(source, value)
|
|
552
|
+
self.sadd(destination, value) if result
|
|
553
|
+
result
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def spop(key)
|
|
557
|
+
data_type_check(key, ::Set)
|
|
558
|
+
elem = srandmember(key)
|
|
559
|
+
srem(key, elem)
|
|
560
|
+
elem
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def scard(key)
|
|
564
|
+
data_type_check(key, ::Set)
|
|
565
|
+
return 0 unless data[key]
|
|
566
|
+
data[key].size
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def sinter(*keys)
|
|
570
|
+
keys = keys[0] if flatten?(keys)
|
|
571
|
+
raise_argument_error('sinter') if keys.empty?
|
|
572
|
+
|
|
573
|
+
keys.each { |k| data_type_check(k, ::Set) }
|
|
574
|
+
return ::Set.new if keys.any? { |k| data[k].nil? }
|
|
575
|
+
keys = keys.map { |k| data[k] || ::Set.new }
|
|
576
|
+
keys.inject do |set, key|
|
|
577
|
+
set & key
|
|
578
|
+
end.to_a
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def sinterstore(destination, *keys)
|
|
582
|
+
data_type_check(destination, ::Set)
|
|
583
|
+
result = sinter(*keys)
|
|
584
|
+
data[destination] = ::Set.new(result)
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def sunion(*keys)
|
|
588
|
+
keys = keys[0] if flatten?(keys)
|
|
589
|
+
raise_argument_error('sunion') if keys.empty?
|
|
590
|
+
|
|
591
|
+
keys.each { |k| data_type_check(k, ::Set) }
|
|
592
|
+
keys = keys.map { |k| data[k] || ::Set.new }
|
|
593
|
+
keys.inject(::Set.new) do |set, key|
|
|
594
|
+
set | key
|
|
595
|
+
end.to_a
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def sunionstore(destination, *keys)
|
|
599
|
+
data_type_check(destination, ::Set)
|
|
600
|
+
result = sunion(*keys)
|
|
601
|
+
data[destination] = ::Set.new(result)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def sdiff(key1, *keys)
|
|
605
|
+
keys = keys[0] if flatten?(keys)
|
|
606
|
+
[key1, *keys].each { |k| data_type_check(k, ::Set) }
|
|
607
|
+
keys = keys.map { |k| data[k] || ::Set.new }
|
|
608
|
+
keys.inject(data[key1] || Set.new) do |memo, set|
|
|
609
|
+
memo - set
|
|
610
|
+
end.to_a
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def sdiffstore(destination, key1, *keys)
|
|
614
|
+
data_type_check(destination, ::Set)
|
|
615
|
+
result = sdiff(key1, *keys)
|
|
616
|
+
data[destination] = ::Set.new(result)
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def srandmember(key, number=nil)
|
|
620
|
+
number.nil? ? srandmember_single(key) : srandmember_multiple(key, number)
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def del(*keys)
|
|
624
|
+
keys = keys.flatten(1)
|
|
625
|
+
raise_argument_error('del') if keys.empty?
|
|
626
|
+
|
|
627
|
+
old_count = data.keys.size
|
|
628
|
+
keys.each do |key|
|
|
629
|
+
data.delete(key)
|
|
630
|
+
end
|
|
631
|
+
old_count - data.keys.size
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def setnx(key, value)
|
|
635
|
+
if exists(key)
|
|
636
|
+
0
|
|
637
|
+
else
|
|
638
|
+
set(key, value)
|
|
639
|
+
1
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def rename(key, new_key)
|
|
644
|
+
return unless data[key]
|
|
645
|
+
data[new_key] = data[key]
|
|
646
|
+
data.expires[new_key] = data.expires[key] if data.expires.include?(key)
|
|
647
|
+
data.delete(key)
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def renamenx(key, new_key)
|
|
651
|
+
if exists(new_key)
|
|
652
|
+
false
|
|
653
|
+
else
|
|
654
|
+
rename(key, new_key)
|
|
655
|
+
true
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def expire(key, ttl)
|
|
660
|
+
return 0 unless data[key]
|
|
661
|
+
data.expires[key] = Time.now + ttl
|
|
662
|
+
1
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def pexpire(key, ttl)
|
|
666
|
+
return 0 unless data[key]
|
|
667
|
+
data.expires[key] = Time.now + (ttl / 1000.0)
|
|
668
|
+
1
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def ttl(key)
|
|
672
|
+
if data.expires.include?(key) && (ttl = data.expires[key].to_i - Time.now.to_i) > 0
|
|
673
|
+
ttl
|
|
674
|
+
else
|
|
675
|
+
exists(key) ? -1 : -2
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def pttl(key)
|
|
680
|
+
if data.expires.include?(key) && (ttl = data.expires[key].to_f - Time.now.to_f) > 0
|
|
681
|
+
ttl * 1000
|
|
682
|
+
else
|
|
683
|
+
exists(key) ? -1 : -2
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def expireat(key, timestamp)
|
|
688
|
+
data.expires[key] = Time.at(timestamp)
|
|
689
|
+
true
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def persist(key)
|
|
693
|
+
!!data.expires.delete(key)
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def hset(key, field, value)
|
|
697
|
+
data_type_check(key, Hash)
|
|
698
|
+
field = field.to_s
|
|
699
|
+
if data[key]
|
|
700
|
+
result = !data[key].include?(field)
|
|
701
|
+
data[key][field] = value.to_s
|
|
702
|
+
result ? 1 : 0
|
|
703
|
+
else
|
|
704
|
+
data[key] = { field => value.to_s }
|
|
705
|
+
1
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def hsetnx(key, field, value)
|
|
710
|
+
data_type_check(key, Hash)
|
|
711
|
+
field = field.to_s
|
|
712
|
+
return false if data[key] && data[key][field]
|
|
713
|
+
hset(key, field, value)
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
def hmset(key, *fields)
|
|
717
|
+
# mapped_hmset gives us [[:k1, "v1", :k2, "v2"]] for `fields`. Fix that.
|
|
718
|
+
fields = fields[0] if mapped_param?(fields)
|
|
719
|
+
raise_argument_error('hmset') if fields.empty?
|
|
720
|
+
|
|
721
|
+
is_list_of_arrays = fields.all?{|field| field.instance_of?(Array)}
|
|
722
|
+
|
|
723
|
+
raise_argument_error('hmset') if fields.size.odd? and !is_list_of_arrays
|
|
724
|
+
raise_argument_error('hmset') if is_list_of_arrays and !fields.all?{|field| field.length == 2}
|
|
725
|
+
|
|
726
|
+
data_type_check(key, Hash)
|
|
727
|
+
data[key] ||= {}
|
|
728
|
+
|
|
729
|
+
if is_list_of_arrays
|
|
730
|
+
fields.each do |pair|
|
|
731
|
+
data[key][pair[0].to_s] = pair[1].to_s
|
|
732
|
+
end
|
|
733
|
+
else
|
|
734
|
+
fields.each_slice(2) do |field|
|
|
735
|
+
data[key][field[0].to_s] = field[1].to_s
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
def hmget(key, *fields)
|
|
741
|
+
raise_argument_error('hmget') if fields.empty?
|
|
742
|
+
|
|
743
|
+
data_type_check(key, Hash)
|
|
744
|
+
fields.flatten.map do |field|
|
|
745
|
+
field = field.to_s
|
|
746
|
+
if data[key]
|
|
747
|
+
data[key][field]
|
|
748
|
+
else
|
|
749
|
+
nil
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
def hlen(key)
|
|
755
|
+
data_type_check(key, Hash)
|
|
756
|
+
return 0 unless data[key]
|
|
757
|
+
data[key].size
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def hvals(key)
|
|
761
|
+
data_type_check(key, Hash)
|
|
762
|
+
return [] unless data[key]
|
|
763
|
+
data[key].values
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
def hincrby(key, field, increment)
|
|
767
|
+
data_type_check(key, Hash)
|
|
768
|
+
field = field.to_s
|
|
769
|
+
if data[key]
|
|
770
|
+
data[key][field] = (data[key][field].to_i + increment.to_i).to_s
|
|
771
|
+
else
|
|
772
|
+
data[key] = { field => increment.to_s }
|
|
773
|
+
end
|
|
774
|
+
data[key][field].to_i
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
def hincrbyfloat(key, field, increment)
|
|
778
|
+
data_type_check(key, Hash)
|
|
779
|
+
field = field.to_s
|
|
780
|
+
if data[key]
|
|
781
|
+
data[key][field] = (data[key][field].to_f + increment.to_f).to_s
|
|
782
|
+
else
|
|
783
|
+
data[key] = { field => increment.to_s }
|
|
784
|
+
end
|
|
785
|
+
data[key][field]
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
def hexists(key, field)
|
|
789
|
+
data_type_check(key, Hash)
|
|
790
|
+
return false unless data[key]
|
|
791
|
+
data[key].key?(field.to_s)
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def sync ; end
|
|
795
|
+
|
|
796
|
+
def [](key)
|
|
797
|
+
get(key)
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
def []=(key, value)
|
|
801
|
+
set(key, value)
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
def set(key, value, *array_options)
|
|
805
|
+
option_nx = array_options.delete("NX")
|
|
806
|
+
option_xx = array_options.delete("XX")
|
|
807
|
+
|
|
808
|
+
return false if option_nx && option_xx
|
|
809
|
+
|
|
810
|
+
return false if option_nx && exists(key)
|
|
811
|
+
return false if option_xx && !exists(key)
|
|
812
|
+
|
|
813
|
+
data[key] = value.to_s
|
|
814
|
+
|
|
815
|
+
options = Hash[array_options.each_slice(2).to_a]
|
|
816
|
+
ttl_in_seconds = options["EX"] if options["EX"]
|
|
817
|
+
ttl_in_seconds = options["PX"] / 1000.0 if options["PX"]
|
|
818
|
+
|
|
819
|
+
expire(key, ttl_in_seconds) if ttl_in_seconds
|
|
820
|
+
|
|
821
|
+
"OK"
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
def setbit(key, offset, bit)
|
|
825
|
+
old_val = data[key] ? data[key].unpack('B*')[0].split("") : []
|
|
826
|
+
size_increment = [((offset/8)+1)*8-old_val.length, 0].max
|
|
827
|
+
old_val += Array.new(size_increment).map{"0"}
|
|
828
|
+
original_val = old_val[offset].to_i
|
|
829
|
+
old_val[offset] = bit.to_s
|
|
830
|
+
new_val = ""
|
|
831
|
+
old_val.each_slice(8){|b| new_val = new_val + b.join("").to_i(2).chr }
|
|
832
|
+
data[key] = new_val
|
|
833
|
+
original_val
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def setex(key, seconds, value)
|
|
837
|
+
data[key] = value.to_s
|
|
838
|
+
expire(key, seconds)
|
|
839
|
+
"OK"
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
def setrange(key, offset, value)
|
|
843
|
+
return unless data[key]
|
|
844
|
+
s = data[key][offset,value.size]
|
|
845
|
+
data[key][s] = value
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
def mset(*pairs)
|
|
849
|
+
# Handle pairs for mapped_mset command
|
|
850
|
+
pairs = pairs[0] if mapped_param?(pairs)
|
|
851
|
+
raise_argument_error('mset') if pairs.empty? || pairs.size == 1
|
|
852
|
+
# We have to reply with a different error message here to be consistent with redis-rb 3.0.6 / redis-server 2.8.1
|
|
853
|
+
raise_argument_error("mset", "mset_odd") if pairs.size.odd?
|
|
854
|
+
|
|
855
|
+
pairs.each_slice(2) do |pair|
|
|
856
|
+
data[pair[0].to_s] = pair[1].to_s
|
|
857
|
+
end
|
|
858
|
+
"OK"
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
def msetnx(*pairs)
|
|
862
|
+
# Handle pairs for mapped_msetnx command
|
|
863
|
+
pairs = pairs[0] if mapped_param?(pairs)
|
|
864
|
+
keys = []
|
|
865
|
+
pairs.each_with_index{|item, index| keys << item.to_s if index % 2 == 0}
|
|
866
|
+
return false if keys.any?{|key| data.key?(key) }
|
|
867
|
+
mset(*pairs)
|
|
868
|
+
true
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
def incr(key)
|
|
872
|
+
data.merge!({ key => (data[key].to_i + 1).to_s || "1"})
|
|
873
|
+
data[key].to_i
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
def incrby(key, by)
|
|
877
|
+
data.merge!({ key => (data[key].to_i + by.to_i).to_s || by })
|
|
878
|
+
data[key].to_i
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
def incrbyfloat(key, by)
|
|
882
|
+
data.merge!({ key => (data[key].to_f + by.to_f).to_s || by })
|
|
883
|
+
data[key]
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
def decr(key)
|
|
887
|
+
data.merge!({ key => (data[key].to_i - 1).to_s || "-1"})
|
|
888
|
+
data[key].to_i
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
def decrby(key, by)
|
|
892
|
+
data.merge!({ key => ((data[key].to_i - by.to_i) || (by.to_i * -1)).to_s })
|
|
893
|
+
data[key].to_i
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
def type(key)
|
|
897
|
+
case data[key]
|
|
898
|
+
when nil then "none"
|
|
899
|
+
when String then "string"
|
|
900
|
+
when ZSet then "zset"
|
|
901
|
+
when Hash then "hash"
|
|
902
|
+
when Array then "list"
|
|
903
|
+
when ::Set then "set"
|
|
904
|
+
end
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
def quit ; end
|
|
908
|
+
|
|
909
|
+
def shutdown; end
|
|
910
|
+
|
|
911
|
+
def slaveof(host, port) ; end
|
|
912
|
+
|
|
913
|
+
def scan(start_cursor, *args)
|
|
914
|
+
match = "*"
|
|
915
|
+
count = 10
|
|
916
|
+
|
|
917
|
+
if args.size.odd?
|
|
918
|
+
raise_argument_error('scan')
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
if idx = args.index("MATCH")
|
|
922
|
+
match = args[idx + 1]
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
if idx = args.index("COUNT")
|
|
926
|
+
count = args[idx + 1]
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
start_cursor = start_cursor.to_i
|
|
930
|
+
data_type_check(start_cursor, Fixnum)
|
|
931
|
+
|
|
932
|
+
cursor = start_cursor
|
|
933
|
+
next_keys = []
|
|
934
|
+
|
|
935
|
+
if start_cursor + count >= data.length
|
|
936
|
+
next_keys = keys(match)[start_cursor..-1]
|
|
937
|
+
cursor = 0
|
|
938
|
+
else
|
|
939
|
+
cursor = start_cursor + 10
|
|
940
|
+
next_keys = keys(match)[start_cursor..cursor]
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
return "#{cursor}", next_keys
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
def zadd(key, *args)
|
|
947
|
+
if !args.first.is_a?(Array)
|
|
948
|
+
if args.size < 2
|
|
949
|
+
raise_argument_error('zadd')
|
|
950
|
+
elsif args.size.odd?
|
|
951
|
+
raise_syntax_error
|
|
952
|
+
end
|
|
953
|
+
else
|
|
954
|
+
unless args.all? {|pair| pair.size == 2 }
|
|
955
|
+
raise_syntax_error
|
|
956
|
+
end
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
data_type_check(key, ZSet)
|
|
960
|
+
data[key] ||= ZSet.new
|
|
961
|
+
|
|
962
|
+
if args.size == 2 && !(Array === args.first)
|
|
963
|
+
score, value = args
|
|
964
|
+
exists = !data[key].key?(value.to_s)
|
|
965
|
+
data[key][value.to_s] = score
|
|
966
|
+
else
|
|
967
|
+
# Turn [1, 2, 3, 4] into [[1, 2], [3, 4]] unless it is already
|
|
968
|
+
args = args.each_slice(2).to_a unless args.first.is_a?(Array)
|
|
969
|
+
exists = args.map(&:last).map { |el| data[key].key?(el.to_s) }.count(false)
|
|
970
|
+
args.each { |s, v| data[key][v.to_s] = s }
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
exists
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
def zrem(key, value)
|
|
977
|
+
data_type_check(key, ZSet)
|
|
978
|
+
values = Array(value)
|
|
979
|
+
return 0 unless data[key]
|
|
980
|
+
|
|
981
|
+
response = values.map do |v|
|
|
982
|
+
data[key].delete(v.to_s) if data[key].has_key?(v.to_s)
|
|
983
|
+
end.compact.size
|
|
984
|
+
|
|
985
|
+
remove_key_for_empty_collection(key)
|
|
986
|
+
response
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
def zcard(key)
|
|
990
|
+
data_type_check(key, ZSet)
|
|
991
|
+
data[key] ? data[key].size : 0
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
def zscore(key, value)
|
|
995
|
+
data_type_check(key, ZSet)
|
|
996
|
+
value = data[key] && data[key][value.to_s]
|
|
997
|
+
value && value.to_s
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
def zcount(key, min, max)
|
|
1001
|
+
data_type_check(key, ZSet)
|
|
1002
|
+
return 0 unless data[key]
|
|
1003
|
+
data[key].select_by_score(min, max).size
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
def zincrby(key, num, value)
|
|
1007
|
+
data_type_check(key, ZSet)
|
|
1008
|
+
data[key] ||= ZSet.new
|
|
1009
|
+
data[key][value.to_s] ||= 0
|
|
1010
|
+
data[key].increment(value.to_s, num)
|
|
1011
|
+
data[key][value.to_s].to_s
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
def zrank(key, value)
|
|
1015
|
+
data_type_check(key, ZSet)
|
|
1016
|
+
z = data[key]
|
|
1017
|
+
return unless z
|
|
1018
|
+
z.keys.sort_by {|k| z[k] }.index(value.to_s)
|
|
1019
|
+
end
|
|
1020
|
+
|
|
1021
|
+
def zrevrank(key, value)
|
|
1022
|
+
data_type_check(key, ZSet)
|
|
1023
|
+
z = data[key]
|
|
1024
|
+
return unless z
|
|
1025
|
+
z.keys.sort_by {|k| -z[k] }.index(value.to_s)
|
|
1026
|
+
end
|
|
1027
|
+
|
|
1028
|
+
def zrange(key, start, stop, with_scores = nil)
|
|
1029
|
+
data_type_check(key, ZSet)
|
|
1030
|
+
return [] unless data[key]
|
|
1031
|
+
|
|
1032
|
+
results = sort_keys(data[key])
|
|
1033
|
+
# Select just the keys unless we want scores
|
|
1034
|
+
results = results.map(&:first) unless with_scores
|
|
1035
|
+
results[start..stop].flatten.map(&:to_s)
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
def zrangebylex(key, start, stop, *opts)
|
|
1039
|
+
data_type_check(key, ZSet)
|
|
1040
|
+
return [] unless data[key]
|
|
1041
|
+
zset = data[key]
|
|
1042
|
+
|
|
1043
|
+
sorted = if zset.identical_scores?
|
|
1044
|
+
zset.keys.sort { |x, y| x.to_s <=> y.to_s }
|
|
1045
|
+
else
|
|
1046
|
+
zset.keys
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
range = get_range start, stop, sorted.first, sorted.last
|
|
1050
|
+
|
|
1051
|
+
filtered = []
|
|
1052
|
+
sorted.each do |element|
|
|
1053
|
+
filtered << element if (range[0][:value]..range[1][:value]).cover?(element)
|
|
1054
|
+
end
|
|
1055
|
+
filtered.shift if filtered[0] == range[0][:value] && !range[0][:inclusive]
|
|
1056
|
+
filtered.pop if filtered.last == range[1][:value] && !range[1][:inclusive]
|
|
1057
|
+
|
|
1058
|
+
limit = get_limit(opts, filtered)
|
|
1059
|
+
if limit
|
|
1060
|
+
filtered = filtered[limit[0]..-1].take(limit[1])
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
filtered
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
def zrevrangebylex(key, start, stop, *args)
|
|
1067
|
+
zrangebylex(key, stop, start, args).reverse
|
|
1068
|
+
end
|
|
1069
|
+
|
|
1070
|
+
def zrevrange(key, start, stop, with_scores = nil)
|
|
1071
|
+
data_type_check(key, ZSet)
|
|
1072
|
+
return [] unless data[key]
|
|
1073
|
+
|
|
1074
|
+
if with_scores
|
|
1075
|
+
data[key].sort_by {|_,v| -v }
|
|
1076
|
+
else
|
|
1077
|
+
data[key].keys.sort_by {|k| -data[key][k] }
|
|
1078
|
+
end[start..stop].flatten.map(&:to_s)
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
def zrangebyscore(key, min, max, *opts)
|
|
1082
|
+
data_type_check(key, ZSet)
|
|
1083
|
+
return [] unless data[key]
|
|
1084
|
+
|
|
1085
|
+
range = data[key].select_by_score(min, max)
|
|
1086
|
+
vals = if opts.include?('WITHSCORES')
|
|
1087
|
+
range.sort_by {|_,v| v }
|
|
1088
|
+
else
|
|
1089
|
+
range.keys.sort_by {|k| range[k] }
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
limit = get_limit(opts, vals)
|
|
1093
|
+
vals = vals[*limit] if limit
|
|
1094
|
+
|
|
1095
|
+
vals.flatten.map(&:to_s)
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
def zrevrangebyscore(key, max, min, *opts)
|
|
1099
|
+
opts = opts.flatten
|
|
1100
|
+
data_type_check(key, ZSet)
|
|
1101
|
+
return [] unless data[key]
|
|
1102
|
+
|
|
1103
|
+
range = data[key].select_by_score(min, max)
|
|
1104
|
+
vals = if opts.include?('WITHSCORES')
|
|
1105
|
+
range.sort_by {|_,v| -v }
|
|
1106
|
+
else
|
|
1107
|
+
range.keys.sort_by {|k| -range[k] }
|
|
1108
|
+
end
|
|
1109
|
+
|
|
1110
|
+
limit = get_limit(opts, vals)
|
|
1111
|
+
vals = vals[*limit] if limit
|
|
1112
|
+
|
|
1113
|
+
vals.flatten.map(&:to_s)
|
|
1114
|
+
end
|
|
1115
|
+
|
|
1116
|
+
def zremrangebyscore(key, min, max)
|
|
1117
|
+
data_type_check(key, ZSet)
|
|
1118
|
+
return 0 unless data[key]
|
|
1119
|
+
|
|
1120
|
+
range = data[key].select_by_score(min, max)
|
|
1121
|
+
range.each {|k,_| data[key].delete(k) }
|
|
1122
|
+
range.size
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
def zremrangebyrank(key, start, stop)
|
|
1126
|
+
data_type_check(key, ZSet)
|
|
1127
|
+
return 0 unless data[key]
|
|
1128
|
+
|
|
1129
|
+
sorted_elements = data[key].sort_by { |k, v| v }
|
|
1130
|
+
start = sorted_elements.length if start > sorted_elements.length
|
|
1131
|
+
elements_to_delete = sorted_elements[start..stop]
|
|
1132
|
+
elements_to_delete.each { |elem, rank| data[key].delete(elem) }
|
|
1133
|
+
elements_to_delete.size
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
def zinterstore(out, *args)
|
|
1137
|
+
data_type_check(out, ZSet)
|
|
1138
|
+
args_handler = SortedSetArgumentHandler.new(args)
|
|
1139
|
+
data[out] = SortedSetIntersectStore.new(args_handler, data).call
|
|
1140
|
+
data[out].size
|
|
1141
|
+
end
|
|
1142
|
+
|
|
1143
|
+
def zunionstore(out, *args)
|
|
1144
|
+
data_type_check(out, ZSet)
|
|
1145
|
+
args_handler = SortedSetArgumentHandler.new(args)
|
|
1146
|
+
data[out] = SortedSetUnionStore.new(args_handler, data).call
|
|
1147
|
+
data[out].size
|
|
1148
|
+
end
|
|
1149
|
+
|
|
1150
|
+
def subscribe(*channels)
|
|
1151
|
+
raise_argument_error('subscribe') if channels.empty?()
|
|
1152
|
+
|
|
1153
|
+
#Create messages for all data from the channels
|
|
1154
|
+
channel_replies = channels.map do |channel|
|
|
1155
|
+
self.class.channels[channel].slice!(0..-1).map!{|v| ["message", channel, v]}
|
|
1156
|
+
end
|
|
1157
|
+
channel_replies.flatten!(1)
|
|
1158
|
+
channel_replies.compact!()
|
|
1159
|
+
|
|
1160
|
+
#Put messages into the replies for the future
|
|
1161
|
+
channels.each_with_index do |channel,index|
|
|
1162
|
+
replies << ["subscribe", channel, index+1]
|
|
1163
|
+
end
|
|
1164
|
+
replies.push(*channel_replies)
|
|
1165
|
+
|
|
1166
|
+
#Add unsubscribe message to stop blocking (see https://github.com/redis/redis-rb/blob/v3.2.1/lib/redis/subscribe.rb#L38)
|
|
1167
|
+
replies.push(self.unsubscribe())
|
|
1168
|
+
|
|
1169
|
+
replies.pop() #Last reply will be pushed back on
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
def psubscribe(*patterns)
|
|
1173
|
+
raise_argument_error('psubscribe') if patterns.empty?()
|
|
1174
|
+
|
|
1175
|
+
#Create messages for all data from the channels
|
|
1176
|
+
channel_replies = self.class.channels.keys.map do |channel|
|
|
1177
|
+
pattern = patterns.find{|p| File.fnmatch(p, channel) }
|
|
1178
|
+
unless pattern.nil?()
|
|
1179
|
+
self.class.channels[channel].slice!(0..-1).map!{|v| ["pmessage", pattern, channel, v]}
|
|
1180
|
+
end
|
|
1181
|
+
end
|
|
1182
|
+
channel_replies.flatten!(1)
|
|
1183
|
+
channel_replies.compact!()
|
|
1184
|
+
|
|
1185
|
+
#Put messages into the replies for the future
|
|
1186
|
+
patterns.each_with_index do |pattern,index|
|
|
1187
|
+
replies << ["psubscribe", pattern, index+1]
|
|
1188
|
+
end
|
|
1189
|
+
replies.push(*channel_replies)
|
|
1190
|
+
|
|
1191
|
+
#Add unsubscribe to stop blocking
|
|
1192
|
+
replies.push(self.punsubscribe())
|
|
1193
|
+
|
|
1194
|
+
replies.pop() #Last reply will be pushed back on
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
def publish(channel, message)
|
|
1198
|
+
self.class.channels[channel] << message
|
|
1199
|
+
0 #Just fake number of subscribers
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
def unsubscribe(*channels)
|
|
1203
|
+
if channels.empty?()
|
|
1204
|
+
replies << ["unsubscribe", nil, 0]
|
|
1205
|
+
else
|
|
1206
|
+
channels.each do |channel|
|
|
1207
|
+
replies << ["unsubscribe", channel, 0]
|
|
1208
|
+
end
|
|
1209
|
+
end
|
|
1210
|
+
replies.pop() #Last reply will be pushed back on
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
def punsubscribe(*patterns)
|
|
1214
|
+
if patterns.empty?()
|
|
1215
|
+
replies << ["punsubscribe", nil, 0]
|
|
1216
|
+
else
|
|
1217
|
+
patterns.each do |pattern|
|
|
1218
|
+
replies << ["punsubscribe", pattern, 0]
|
|
1219
|
+
end
|
|
1220
|
+
end
|
|
1221
|
+
replies.pop() #Last reply will be pushed back on
|
|
1222
|
+
end
|
|
1223
|
+
|
|
1224
|
+
def zscan(key, start_cursor, *args)
|
|
1225
|
+
data_type_check(key, ZSet)
|
|
1226
|
+
return [] unless data[key]
|
|
1227
|
+
|
|
1228
|
+
match = "*"
|
|
1229
|
+
count = 10
|
|
1230
|
+
|
|
1231
|
+
if args.size.odd?
|
|
1232
|
+
raise_argument_error('zscan')
|
|
1233
|
+
end
|
|
1234
|
+
|
|
1235
|
+
if idx = args.index("MATCH")
|
|
1236
|
+
match = args[idx + 1]
|
|
1237
|
+
end
|
|
1238
|
+
|
|
1239
|
+
if idx = args.index("COUNT")
|
|
1240
|
+
count = args[idx + 1]
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1243
|
+
start_cursor = start_cursor.to_i
|
|
1244
|
+
data_type_check(start_cursor, Fixnum)
|
|
1245
|
+
|
|
1246
|
+
cursor = start_cursor
|
|
1247
|
+
next_keys = []
|
|
1248
|
+
|
|
1249
|
+
sorted_keys = sort_keys(data[key])
|
|
1250
|
+
|
|
1251
|
+
if start_cursor + count >= sorted_keys.length
|
|
1252
|
+
next_keys = sorted_keys.to_a.select { |k| File.fnmatch(match, k[0]) } [start_cursor..-1]
|
|
1253
|
+
cursor = 0
|
|
1254
|
+
else
|
|
1255
|
+
cursor = start_cursor + count
|
|
1256
|
+
next_keys = sorted_keys.to_a.select { |k| File.fnmatch(match, k[0]) } [start_cursor..cursor-1]
|
|
1257
|
+
end
|
|
1258
|
+
return "#{cursor}", next_keys.flatten.map(&:to_s)
|
|
1259
|
+
end
|
|
1260
|
+
|
|
1261
|
+
# Originally from redis-rb
|
|
1262
|
+
def zscan_each(key, *args, &block)
|
|
1263
|
+
data_type_check(key, ZSet)
|
|
1264
|
+
return [] unless data[key]
|
|
1265
|
+
|
|
1266
|
+
return to_enum(:zscan_each, key, options) unless block_given?
|
|
1267
|
+
cursor = 0
|
|
1268
|
+
loop do
|
|
1269
|
+
cursor, values = zscan(key, cursor, options)
|
|
1270
|
+
values.each(&block)
|
|
1271
|
+
break if cursor == "0"
|
|
1272
|
+
end
|
|
1273
|
+
end
|
|
1274
|
+
|
|
1275
|
+
private
|
|
1276
|
+
def raise_argument_error(command, match_string=command)
|
|
1277
|
+
error_message = if %w(hmset mset_odd).include?(match_string.downcase)
|
|
1278
|
+
"ERR wrong number of arguments for #{command.upcase}"
|
|
1279
|
+
else
|
|
1280
|
+
"ERR wrong number of arguments for '#{command}' command"
|
|
1281
|
+
end
|
|
1282
|
+
|
|
1283
|
+
raise Redis::CommandError, error_message
|
|
1284
|
+
end
|
|
1285
|
+
|
|
1286
|
+
def raise_syntax_error
|
|
1287
|
+
raise Redis::CommandError, "ERR syntax error"
|
|
1288
|
+
end
|
|
1289
|
+
|
|
1290
|
+
def remove_key_for_empty_collection(key)
|
|
1291
|
+
del(key) if data[key] && data[key].empty?
|
|
1292
|
+
end
|
|
1293
|
+
|
|
1294
|
+
def data_type_check(key, klass)
|
|
1295
|
+
if data[key] && !data[key].is_a?(klass)
|
|
1296
|
+
warn "Operation against a key holding the wrong kind of value: Expected #{klass} at #{key}."
|
|
1297
|
+
raise Redis::CommandError.new("WRONGTYPE Operation against a key holding the wrong kind of value")
|
|
1298
|
+
end
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
def get_range(start, stop, min = -Float::INFINITY, max = Float::INFINITY)
|
|
1302
|
+
range_options = []
|
|
1303
|
+
|
|
1304
|
+
[start, stop].each do |value|
|
|
1305
|
+
case value[0]
|
|
1306
|
+
when "-"
|
|
1307
|
+
range_options << { value: min, inclusive: true }
|
|
1308
|
+
when "+"
|
|
1309
|
+
range_options << { value: max, inclusive: true }
|
|
1310
|
+
when "["
|
|
1311
|
+
range_options << { value: value[1..-1], inclusive: true }
|
|
1312
|
+
when "("
|
|
1313
|
+
range_options << { value: value[1..-1], inclusive: false }
|
|
1314
|
+
else
|
|
1315
|
+
raise Redis::CommandError, "ERR min or max not valid string range item"
|
|
1316
|
+
end
|
|
1317
|
+
end
|
|
1318
|
+
|
|
1319
|
+
range_options
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1322
|
+
def get_limit(opts, vals)
|
|
1323
|
+
index = opts.index('LIMIT')
|
|
1324
|
+
|
|
1325
|
+
if index
|
|
1326
|
+
offset = opts[index + 1]
|
|
1327
|
+
|
|
1328
|
+
count = opts[index + 2]
|
|
1329
|
+
count = vals.size if count < 0
|
|
1330
|
+
|
|
1331
|
+
[offset, count]
|
|
1332
|
+
end
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
def mapped_param? param
|
|
1336
|
+
param.size == 1 && param[0].is_a?(Array)
|
|
1337
|
+
end
|
|
1338
|
+
# NOTE : Redis-rb 3.x will flatten *args, so method(["a", "b", "c"])
|
|
1339
|
+
# should be handled the same way as method("a", "b", "c")
|
|
1340
|
+
alias_method :flatten?, :mapped_param?
|
|
1341
|
+
|
|
1342
|
+
def srandmember_single(key)
|
|
1343
|
+
data_type_check(key, ::Set)
|
|
1344
|
+
return nil unless data[key]
|
|
1345
|
+
data[key].to_a[rand(data[key].size)]
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
def srandmember_multiple(key, number)
|
|
1349
|
+
return [] unless data[key]
|
|
1350
|
+
if number >= 0
|
|
1351
|
+
# replace with `data[key].to_a.sample(number)` when 1.8.7 is deprecated
|
|
1352
|
+
(1..number).inject([]) do |selected, _|
|
|
1353
|
+
available_elements = data[key].to_a - selected
|
|
1354
|
+
selected << available_elements[rand(available_elements.size)]
|
|
1355
|
+
end.compact
|
|
1356
|
+
else
|
|
1357
|
+
(1..-number).map { data[key].to_a[rand(data[key].size)] }.flatten
|
|
1358
|
+
end
|
|
1359
|
+
end
|
|
1360
|
+
|
|
1361
|
+
def sort_keys(arr)
|
|
1362
|
+
# Sort by score, or if scores are equal, key alphanum
|
|
1363
|
+
sorted_keys = arr.sort do |(k1, v1), (k2, v2)|
|
|
1364
|
+
if v1 == v2
|
|
1365
|
+
k1 <=> k2
|
|
1366
|
+
else
|
|
1367
|
+
v1 <=> v2
|
|
1368
|
+
end
|
|
1369
|
+
end
|
|
1370
|
+
end
|
|
1371
|
+
end
|
|
1372
|
+
end
|
|
1373
|
+
end
|
|
1374
|
+
|
|
1375
|
+
Redis::Connection.drivers << Redis::Connection::Memory
|