fakeredis 0.3.2 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +16 -2
- data/fakeredis.gemspec +2 -3
- data/lib/fake_redis.rb +1 -0
- data/lib/fakeredis/expiring_hash.rb +56 -0
- data/lib/fakeredis/sorted_set_argument_handler.rb +74 -0
- data/lib/fakeredis/sorted_set_store.rb +80 -0
- data/lib/fakeredis/version.rb +1 -1
- data/lib/fakeredis/zset.rb +4 -0
- data/lib/redis/connection/memory.rb +82 -114
- data/spec/compatibility_spec.rb +2 -2
- data/spec/connection_spec.rb +13 -3
- data/spec/hashes_spec.rb +44 -33
- data/spec/keys_spec.rb +40 -27
- data/spec/lists_spec.rb +42 -27
- data/spec/server_spec.rb +39 -2
- data/spec/sets_spec.rb +22 -16
- data/spec/sorted_sets_spec.rb +229 -58
- data/spec/spec_helper.rb +4 -0
- data/spec/spec_helper_live_redis.rb +14 -0
- data/spec/strings_spec.rb +92 -45
- metadata +14 -7
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# FakeRedis [![Build Status](
|
1
|
+
# FakeRedis [![Build Status](https://secure.travis-ci.org/guilleiguaran/fakeredis.png)](http://travis-ci.org/guilleiguaran/fakeredis)
|
2
2
|
This a fake implementation of redis-rb for machines without Redis or test environments
|
3
3
|
|
4
4
|
|
@@ -13,6 +13,18 @@ Add it to your Gemfile:
|
|
13
13
|
gem "fakeredis"
|
14
14
|
|
15
15
|
|
16
|
+
## Versions
|
17
|
+
|
18
|
+
FakeRedis currently supports redis-rb v3.0.0 or later, if you are using
|
19
|
+
redis-rb v2.2.x install the version 0.3.x:
|
20
|
+
|
21
|
+
gem install fakeredis -v "~> 0.3.0"
|
22
|
+
|
23
|
+
or use the branch 0-3-x on your Gemfile:
|
24
|
+
|
25
|
+
gem "fakeredis", :git => "git://github.com/guilleiguaran/fakeredis.git", :branch => "0-3-x"
|
26
|
+
|
27
|
+
|
16
28
|
## Usage
|
17
29
|
|
18
30
|
You can use FakeRedis without any changes:
|
@@ -38,7 +50,7 @@ Require this either in your Gemfile or in RSpec's support scripts. So either:
|
|
38
50
|
group :test do
|
39
51
|
gem "rspec"
|
40
52
|
gem "fakeredis", :require => "fakeredis/rspec"
|
41
|
-
end
|
53
|
+
end
|
42
54
|
|
43
55
|
Or:
|
44
56
|
|
@@ -52,6 +64,8 @@ Or:
|
|
52
64
|
* [obrie](https://github.com/obrie)
|
53
65
|
* [jredville](https://github.com/jredville)
|
54
66
|
* [redsquirrel](https://github.com/redsquirrel)
|
67
|
+
* [dpick](https://github.com/dpick)
|
68
|
+
* [caius](https://github.com/caius)
|
55
69
|
* [Travis-CI](http://travis-ci.org/) (Travis-CI also uses Fakeredis in its tests!!!)
|
56
70
|
|
57
71
|
|
data/fakeredis.gemspec
CHANGED
@@ -8,12 +8,11 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ["Guillermo Iguaran"]
|
10
10
|
s.email = ["guilleiguaran@gmail.com"]
|
11
|
-
s.homepage = "https://github.com/
|
11
|
+
s.homepage = "https://guilleiguaran.github.com/fakeredis"
|
12
|
+
s.license = "MIT"
|
12
13
|
s.summary = %q{Fake (In-memory) driver for redis-rb.}
|
13
14
|
s.description = %q{Fake (In-memory) driver for redis-rb. Useful for testing environment and machines without Redis.}
|
14
15
|
|
15
|
-
s.rubyforge_project = "fakeredis"
|
16
|
-
|
17
16
|
s.files = `git ls-files`.split("\n")
|
18
17
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
18
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
data/lib/fake_redis.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "fakeredis"
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module FakeRedis
|
2
|
+
# Represents a normal hash with some additional expiration information
|
3
|
+
# associated with each key
|
4
|
+
class ExpiringHash < Hash
|
5
|
+
attr_reader :expires
|
6
|
+
|
7
|
+
def initialize(*)
|
8
|
+
super
|
9
|
+
@expires = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](key)
|
13
|
+
delete(key) if expired?(key)
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def []=(key, val)
|
18
|
+
expire(key)
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete(key)
|
23
|
+
expire(key)
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def expire(key)
|
28
|
+
expires.delete(key)
|
29
|
+
end
|
30
|
+
|
31
|
+
def expired?(key)
|
32
|
+
expires.include?(key) && expires[key] < Time.now
|
33
|
+
end
|
34
|
+
|
35
|
+
def key?(key)
|
36
|
+
delete(key) if expired?(key)
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
def values_at(*keys)
|
41
|
+
keys.each {|key| delete(key) if expired?(key)}
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
def keys
|
46
|
+
super.select do |key|
|
47
|
+
if expired?(key)
|
48
|
+
delete(key)
|
49
|
+
false
|
50
|
+
else
|
51
|
+
true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
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(RuntimeError, "ERR syntax error") unless weights.size == number_of_keys
|
36
|
+
raise(RuntimeError, "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(RuntimeError, "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(RuntimeError, "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
|
data/lib/fakeredis/version.rb
CHANGED
@@ -1,69 +1,16 @@
|
|
1
1
|
require 'set'
|
2
2
|
require 'redis/connection/registry'
|
3
3
|
require 'redis/connection/command_helper'
|
4
|
+
require "fakeredis/expiring_hash"
|
5
|
+
require "fakeredis/sorted_set_argument_handler"
|
6
|
+
require "fakeredis/sorted_set_store"
|
7
|
+
require "fakeredis/zset"
|
4
8
|
|
5
9
|
class Redis
|
6
10
|
module Connection
|
7
11
|
class Memory
|
8
|
-
# Represents a normal hash with some additional expiration information
|
9
|
-
# associated with each key
|
10
|
-
class ExpiringHash < Hash
|
11
|
-
attr_reader :expires
|
12
|
-
|
13
|
-
def initialize(*)
|
14
|
-
super
|
15
|
-
@expires = {}
|
16
|
-
end
|
17
|
-
|
18
|
-
def [](key)
|
19
|
-
delete(key) if expired?(key)
|
20
|
-
super
|
21
|
-
end
|
22
|
-
|
23
|
-
def []=(key, val)
|
24
|
-
expire(key)
|
25
|
-
super
|
26
|
-
end
|
27
|
-
|
28
|
-
def delete(key)
|
29
|
-
expire(key)
|
30
|
-
super
|
31
|
-
end
|
32
|
-
|
33
|
-
def expire(key)
|
34
|
-
expires.delete(key)
|
35
|
-
end
|
36
|
-
|
37
|
-
def expired?(key)
|
38
|
-
expires.include?(key) && expires[key] < Time.now
|
39
|
-
end
|
40
|
-
|
41
|
-
def key?(key)
|
42
|
-
delete(key) if expired?(key)
|
43
|
-
super
|
44
|
-
end
|
45
|
-
|
46
|
-
def values_at(*keys)
|
47
|
-
keys.each {|key| delete(key) if expired?(key)}
|
48
|
-
super
|
49
|
-
end
|
50
|
-
|
51
|
-
def keys
|
52
|
-
super.select do |key|
|
53
|
-
if expired?(key)
|
54
|
-
delete(key)
|
55
|
-
false
|
56
|
-
else
|
57
|
-
true
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
class ZSet < Hash
|
64
|
-
end
|
65
|
-
|
66
12
|
include Redis::Connection::CommandHelper
|
13
|
+
include FakeRedis
|
67
14
|
|
68
15
|
def initialize
|
69
16
|
@data = ExpiringHash.new
|
@@ -93,8 +40,12 @@ class Redis
|
|
93
40
|
end
|
94
41
|
|
95
42
|
def write(command)
|
96
|
-
|
97
|
-
|
43
|
+
meffod = command.shift
|
44
|
+
if respond_to?(meffod)
|
45
|
+
reply = send(meffod, *command)
|
46
|
+
else
|
47
|
+
raise RuntimeError, "ERR unknown command '#{meffod}'"
|
48
|
+
end
|
98
49
|
|
99
50
|
if reply == true
|
100
51
|
reply = 1
|
@@ -103,7 +54,7 @@ class Redis
|
|
103
54
|
end
|
104
55
|
|
105
56
|
@replies << reply
|
106
|
-
@buffer << reply if @buffer &&
|
57
|
+
@buffer << reply if @buffer && meffod != :multi
|
107
58
|
nil
|
108
59
|
end
|
109
60
|
|
@@ -117,13 +68,14 @@ class Redis
|
|
117
68
|
# * brpoplpush
|
118
69
|
# * discard
|
119
70
|
# * move
|
71
|
+
# * sort
|
120
72
|
# * subscribe
|
121
73
|
# * psubscribe
|
122
74
|
# * publish
|
123
|
-
|
124
|
-
# * zunionstore
|
75
|
+
|
125
76
|
def flushdb
|
126
77
|
@data = ExpiringHash.new
|
78
|
+
"OK"
|
127
79
|
end
|
128
80
|
|
129
81
|
def flushall
|
@@ -160,6 +112,7 @@ class Redis
|
|
160
112
|
def bgreriteaof ; end
|
161
113
|
|
162
114
|
def get(key)
|
115
|
+
data_type_check(key, String)
|
163
116
|
@data[key]
|
164
117
|
end
|
165
118
|
|
@@ -175,13 +128,14 @@ class Redis
|
|
175
128
|
alias :substr :getrange
|
176
129
|
|
177
130
|
def getset(key, value)
|
178
|
-
|
179
|
-
@data[key]
|
180
|
-
|
131
|
+
data_type_check(key, String)
|
132
|
+
@data[key].tap do
|
133
|
+
set(key, value)
|
134
|
+
end
|
181
135
|
end
|
182
136
|
|
183
137
|
def mget(*keys)
|
184
|
-
raise
|
138
|
+
raise RuntimeError, "ERR wrong number of arguments for 'mget' command" if keys.empty?
|
185
139
|
@data.values_at(*keys)
|
186
140
|
end
|
187
141
|
|
@@ -254,7 +208,7 @@ class Redis
|
|
254
208
|
|
255
209
|
def lrange(key, startidx, endidx)
|
256
210
|
data_type_check(key, Array)
|
257
|
-
@data[key] && @data[key][startidx..endidx]
|
211
|
+
@data[key] && @data[key][startidx..endidx] || []
|
258
212
|
end
|
259
213
|
|
260
214
|
def ltrim(key, start, stop)
|
@@ -282,7 +236,7 @@ class Redis
|
|
282
236
|
def lset(key, index, value)
|
283
237
|
data_type_check(key, Array)
|
284
238
|
return unless @data[key]
|
285
|
-
raise RuntimeError if index >= @data[key].size
|
239
|
+
raise RuntimeError, "ERR index out of range" if index >= @data[key].size
|
286
240
|
@data[key][index] = value
|
287
241
|
end
|
288
242
|
|
@@ -307,7 +261,7 @@ class Redis
|
|
307
261
|
def rpush(key, value)
|
308
262
|
data_type_check(key, Array)
|
309
263
|
@data[key] ||= []
|
310
|
-
@data[key].push(value)
|
264
|
+
@data[key].push(value.to_s)
|
311
265
|
@data[key].size
|
312
266
|
end
|
313
267
|
|
@@ -320,7 +274,7 @@ class Redis
|
|
320
274
|
def lpush(key, value)
|
321
275
|
data_type_check(key, Array)
|
322
276
|
@data[key] ||= []
|
323
|
-
@data[key].unshift(value)
|
277
|
+
@data[key].unshift(value.to_s)
|
324
278
|
@data[key].size
|
325
279
|
end
|
326
280
|
|
@@ -338,8 +292,9 @@ class Redis
|
|
338
292
|
|
339
293
|
def rpoplpush(key1, key2)
|
340
294
|
data_type_check(key1, Array)
|
341
|
-
|
342
|
-
|
295
|
+
rpop(key1).tap do |elem|
|
296
|
+
lpush(key2, elem)
|
297
|
+
end
|
343
298
|
end
|
344
299
|
|
345
300
|
def lpop(key)
|
@@ -451,7 +406,7 @@ class Redis
|
|
451
406
|
keys.flatten.each do |key|
|
452
407
|
@data.delete(key)
|
453
408
|
end
|
454
|
-
|
409
|
+
old_count - @data.keys.size
|
455
410
|
end
|
456
411
|
|
457
412
|
def setnx(key, value)
|
@@ -523,7 +478,11 @@ class Redis
|
|
523
478
|
end
|
524
479
|
|
525
480
|
def hmset(key, *fields)
|
526
|
-
|
481
|
+
# mapped_hmset gives us [[:k1, "v1", :k2, "v2"]] for `fields`. Fix that.
|
482
|
+
fields = fields[0] if fields.size == 1 && fields[0].is_a?(Array)
|
483
|
+
fields = fields[0] if mapped_param?(fields)
|
484
|
+
raise RuntimeError, "ERR wrong number of arguments for HMSET" if fields.size > 2 && fields.size.odd?
|
485
|
+
raise RuntimeError, "ERR wrong number of arguments for 'hmset' command" if fields.empty? || fields.size.odd?
|
527
486
|
data_type_check(key, Hash)
|
528
487
|
@data[key] ||= {}
|
529
488
|
fields.each_slice(2) do |field|
|
@@ -532,9 +491,8 @@ class Redis
|
|
532
491
|
end
|
533
492
|
|
534
493
|
def hmget(key, *fields)
|
535
|
-
raise
|
494
|
+
raise RuntimeError, "ERR wrong number of arguments for 'hmget' command" if fields.empty?
|
536
495
|
data_type_check(key, Hash)
|
537
|
-
values = []
|
538
496
|
fields.map do |field|
|
539
497
|
field = field.to_s
|
540
498
|
if @data[key]
|
@@ -612,6 +570,8 @@ class Redis
|
|
612
570
|
end
|
613
571
|
|
614
572
|
def mset(*pairs)
|
573
|
+
# Handle pairs for mapped_mset command
|
574
|
+
pairs = pairs[0] if mapped_param?(pairs)
|
615
575
|
pairs.each_slice(2) do |pair|
|
616
576
|
@data[pair[0].to_s] = pair[1].to_s
|
617
577
|
end
|
@@ -619,9 +579,11 @@ class Redis
|
|
619
579
|
end
|
620
580
|
|
621
581
|
def msetnx(*pairs)
|
582
|
+
# Handle pairs for mapped_mset command
|
583
|
+
pairs = pairs[0] if mapped_param?(pairs)
|
622
584
|
keys = []
|
623
585
|
pairs.each_with_index{|item, index| keys << item.to_s if index % 2 == 0}
|
624
|
-
return if keys.any?{|key| @data.key?(key) }
|
586
|
+
return false if keys.any? {|key| @data.key?(key) }
|
625
587
|
mset(*pairs)
|
626
588
|
true
|
627
589
|
end
|
@@ -631,31 +593,27 @@ class Redis
|
|
631
593
|
end
|
632
594
|
|
633
595
|
def incr(key)
|
634
|
-
@data
|
635
|
-
@data[key] = (@data[key].to_i + 1).to_s
|
596
|
+
@data.merge!({ key => (@data[key].to_i + 1).to_s || "1"})
|
636
597
|
@data[key].to_i
|
637
598
|
end
|
638
599
|
|
639
600
|
def incrby(key, by)
|
640
|
-
@data
|
641
|
-
@data[key] = (@data[key].to_i + by.to_i).to_s
|
601
|
+
@data.merge!({ key => (@data[key].to_i + by.to_i).to_s || by })
|
642
602
|
@data[key].to_i
|
643
603
|
end
|
644
604
|
|
645
605
|
def decr(key)
|
646
|
-
@data
|
647
|
-
@data[key] = (@data[key].to_i - 1).to_s
|
606
|
+
@data.merge!({ key => (@data[key].to_i - 1).to_s || "-1"})
|
648
607
|
@data[key].to_i
|
649
608
|
end
|
650
609
|
|
651
610
|
def decrby(key, by)
|
652
|
-
@data
|
653
|
-
@data[key] = (@data[key].to_i - by.to_i).to_s
|
611
|
+
@data.merge!({ key => ((@data[key].to_i - by.to_i) || (by.to_i * -1)).to_s })
|
654
612
|
@data[key].to_i
|
655
613
|
end
|
656
614
|
|
657
615
|
def type(key)
|
658
|
-
case
|
616
|
+
case @data[key]
|
659
617
|
when nil then "none"
|
660
618
|
when String then "string"
|
661
619
|
when Hash then "hash"
|
@@ -694,6 +652,7 @@ class Redis
|
|
694
652
|
data_type_check(key, ZSet)
|
695
653
|
@data[key] ||= ZSet.new
|
696
654
|
exists = @data[key].key?(value.to_s)
|
655
|
+
score = "inf" if score == "+inf"
|
697
656
|
@data[key][value.to_s] = score
|
698
657
|
!exists
|
699
658
|
end
|
@@ -713,7 +672,8 @@ class Redis
|
|
713
672
|
|
714
673
|
def zscore(key, value)
|
715
674
|
data_type_check(key, ZSet)
|
716
|
-
@data[key] && @data[key][value.to_s]
|
675
|
+
result = @data[key] && @data[key][value.to_s]
|
676
|
+
result.to_s if result
|
717
677
|
end
|
718
678
|
|
719
679
|
def zcount(key, min, max)
|
@@ -726,7 +686,12 @@ class Redis
|
|
726
686
|
data_type_check(key, ZSet)
|
727
687
|
@data[key] ||= ZSet.new
|
728
688
|
@data[key][value.to_s] ||= 0
|
729
|
-
|
689
|
+
if %w(+inf -inf).include?(num)
|
690
|
+
num = "inf" if num == "+inf"
|
691
|
+
@data[key][value.to_s] = num
|
692
|
+
elsif ! %w(+inf -inf).include?(@data[key][value.to_s])
|
693
|
+
@data[key][value.to_s] += num
|
694
|
+
end
|
730
695
|
@data[key][value.to_s].to_s
|
731
696
|
end
|
732
697
|
|
@@ -744,11 +709,17 @@ class Redis
|
|
744
709
|
data_type_check(key, ZSet)
|
745
710
|
return [] unless @data[key]
|
746
711
|
|
747
|
-
if
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
712
|
+
# Sort by score, or if scores are equal, key alphanum
|
713
|
+
results = @data[key].sort do |(k1, v1), (k2, v2)|
|
714
|
+
if v1 == v2
|
715
|
+
k1 <=> k2
|
716
|
+
else
|
717
|
+
v1 <=> v2
|
718
|
+
end
|
719
|
+
end
|
720
|
+
# Select just the keys unless we want scores
|
721
|
+
results = results.map(&:first) unless with_scores
|
722
|
+
results[start..stop].flatten.map(&:to_s)
|
752
723
|
end
|
753
724
|
|
754
725
|
def zrevrange(key, start, stop, with_scores = nil)
|
@@ -805,31 +776,23 @@ class Redis
|
|
805
776
|
range.size
|
806
777
|
end
|
807
778
|
|
808
|
-
def zinterstore(out,
|
779
|
+
def zinterstore(out, *args)
|
809
780
|
data_type_check(out, ZSet)
|
781
|
+
args_handler = SortedSetArgumentHandler.new(args)
|
782
|
+
@data[out] = SortedSetIntersectStore.new(args_handler, @data).call
|
783
|
+
@data[out].size
|
784
|
+
end
|
810
785
|
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
when Hash
|
816
|
-
@data[src]
|
817
|
-
else
|
818
|
-
{}
|
819
|
-
end
|
820
|
-
end
|
821
|
-
|
822
|
-
@data[out] = ZSet.new
|
823
|
-
values = hashes.inject([]) {|r, h| r.empty? ? h.keys : r & h.keys }
|
824
|
-
values.each do |value|
|
825
|
-
@data[out][value] = hashes.inject(0) {|n, h| n + h[value].to_i }
|
826
|
-
end
|
827
|
-
|
786
|
+
def zunionstore(out, *args)
|
787
|
+
data_type_check(out, ZSet)
|
788
|
+
args_handler = SortedSetArgumentHandler.new(args)
|
789
|
+
@data[out] = SortedSetUnionStore.new(args_handler, @data).call
|
828
790
|
@data[out].size
|
829
791
|
end
|
830
792
|
|
831
793
|
def zremrangebyrank(key, start, stop)
|
832
|
-
sorted_elements = @data[key].sort { |(
|
794
|
+
sorted_elements = @data[key].sort { |(_, r_a), (_, r_b)| r_a <=> r_b }
|
795
|
+
start = sorted_elements.length if start > sorted_elements.length
|
833
796
|
elements_to_delete = sorted_elements[start..stop]
|
834
797
|
elements_to_delete.each { |elem, rank| @data[key].delete(elem) }
|
835
798
|
elements_to_delete.size
|
@@ -847,7 +810,8 @@ class Redis
|
|
847
810
|
|
848
811
|
def data_type_check(key, klass)
|
849
812
|
if @data[key] && !@data[key].is_a?(klass)
|
850
|
-
|
813
|
+
warn "Operation against a key holding the wrong kind of value: Expected #{klass} at #{key}."
|
814
|
+
raise RuntimeError.new("ERR Operation against a key holding the wrong kind of value")
|
851
815
|
end
|
852
816
|
end
|
853
817
|
|
@@ -863,6 +827,10 @@ class Redis
|
|
863
827
|
[offset, count]
|
864
828
|
end
|
865
829
|
end
|
830
|
+
|
831
|
+
def mapped_param? param
|
832
|
+
param.size == 1 && param[0].is_a?(Array)
|
833
|
+
end
|
866
834
|
end
|
867
835
|
end
|
868
836
|
end
|