fakeredis 0.5.0 → 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 +4 -4
- data/.gitignore +4 -0
- data/.travis.yml +16 -5
- data/LICENSE +1 -1
- data/README.md +18 -2
- data/fakeredis.gemspec +1 -1
- data/gemfiles/redisrb-master.gemfile +14 -0
- data/lib/fakeredis/bitop_command.rb +56 -0
- data/lib/fakeredis/expiring_hash.rb +1 -1
- data/lib/fakeredis/minitest.rb +24 -0
- data/lib/fakeredis/rspec.rb +1 -0
- data/lib/fakeredis/sort_method.rb +3 -2
- data/lib/fakeredis/transaction_commands.rb +1 -1
- data/lib/fakeredis/version.rb +1 -1
- data/lib/fakeredis/zset.rb +7 -1
- data/lib/redis/connection/memory.rb +424 -38
- data/spec/bitop_command_spec.rb +209 -0
- data/spec/compatibility_spec.rb +1 -1
- data/spec/connection_spec.rb +20 -20
- data/spec/hashes_spec.rb +123 -57
- data/spec/keys_spec.rb +197 -80
- data/spec/lists_spec.rb +61 -34
- data/spec/memory_spec.rb +60 -7
- data/spec/server_spec.rb +24 -24
- data/spec/sets_spec.rb +95 -46
- data/spec/sort_method_spec.rb +6 -0
- data/spec/sorted_sets_spec.rb +288 -150
- data/spec/spec_helper.rb +1 -0
- data/spec/strings_spec.rb +83 -78
- data/spec/subscription_spec.rb +107 -0
- data/spec/support/shared_examples/bitwise_operation.rb +59 -0
- data/spec/support/shared_examples/sortable.rb +20 -16
- data/spec/transactions_spec.rb +20 -12
- data/spec/upcase_method_name_spec.rb +2 -2
- metadata +14 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 30c50ee5b3390908b28564542cf9d6ad6c1dfb20
|
4
|
+
data.tar.gz: 30c85e12c5689721c1ead41da72a91c47a010a0b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 62ec40ecbbf36d198b0a8b95d5772f6d1e79a9709378a67a9e5aaba8cc15f6122fba3966524831ce5a8e7b6ebdbceaf620a4320f338497deff52c8361d8c41c7
|
7
|
+
data.tar.gz: 7d7c9066f97f7accfc95e40819b726ffcbf318e5e6f1af9fda405ce3104c56e546e2a2c6b5e61cac58a283a4d56f868c8c1c6bb5ca811ca440cc0eb62adf599a
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,8 +1,19 @@
|
|
1
1
|
language: ruby
|
2
|
+
before_install:
|
3
|
+
- travis_retry gem install bundler
|
2
4
|
rvm:
|
3
|
-
- 1
|
4
|
-
-
|
5
|
-
- 2.
|
6
|
-
-
|
7
|
-
- jruby
|
5
|
+
- 2.1
|
6
|
+
- 2.2
|
7
|
+
- 2.3.1
|
8
|
+
- ruby-head
|
9
|
+
- jruby
|
8
10
|
- rbx-2
|
11
|
+
gemfile:
|
12
|
+
- Gemfile
|
13
|
+
- gemfiles/redisrb-master.gemfile
|
14
|
+
matrix:
|
15
|
+
allow_failures:
|
16
|
+
- rvm: rbx-2
|
17
|
+
# Use the faster container based infrastructure
|
18
|
+
# http://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/
|
19
|
+
sudo: false
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -39,7 +39,7 @@ You can use FakeRedis without any changes:
|
|
39
39
|
>> redis.get "foo"
|
40
40
|
=> "bar"
|
41
41
|
|
42
|
-
Read [redis-rb](https://github.com/
|
42
|
+
Read [redis-rb](https://github.com/redis/redis-rb) documentation and
|
43
43
|
[Redis](http://redis.io) homepage for more info about commands
|
44
44
|
|
45
45
|
## Usage with RSpec
|
@@ -57,6 +57,22 @@ Or:
|
|
57
57
|
# spec/support/fakeredis.rb
|
58
58
|
require 'fakeredis/rspec'
|
59
59
|
|
60
|
+
## Usage with Minitest
|
61
|
+
|
62
|
+
Require this either in your Gemfile or in Minitest's support scripts. So
|
63
|
+
either:
|
64
|
+
|
65
|
+
# Gemfile
|
66
|
+
group :test do
|
67
|
+
gem "minitest"
|
68
|
+
gem "fakeredis", :require => "fakeredis/minitest"
|
69
|
+
end
|
70
|
+
|
71
|
+
Or:
|
72
|
+
|
73
|
+
# test/test_helper.rb (or test/minitest_config.rb)
|
74
|
+
require 'fakeredis/minitest'
|
75
|
+
|
60
76
|
## Acknowledgements
|
61
77
|
|
62
78
|
* [dim](https://github.com/dim)
|
@@ -82,5 +98,5 @@ Or:
|
|
82
98
|
|
83
99
|
## Copyright
|
84
100
|
|
85
|
-
Copyright (c) 2011-
|
101
|
+
Copyright (c) 2011-2016 Guillermo Iguaran. See LICENSE for
|
86
102
|
further details.
|
data/fakeredis.gemspec
CHANGED
@@ -18,6 +18,6 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
19
|
s.require_paths = ["lib"]
|
20
20
|
|
21
|
-
s.add_runtime_dependency(%q<redis>, ["~> 3.
|
21
|
+
s.add_runtime_dependency(%q<redis>, ["~> 3.2"])
|
22
22
|
s.add_development_dependency(%q<rspec>, ["~> 3.0"])
|
23
23
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gem 'rake'
|
4
|
+
gem 'rdoc'
|
5
|
+
gem "redis", github: "redis/redis-rb"
|
6
|
+
|
7
|
+
platforms :rbx do
|
8
|
+
gem 'racc'
|
9
|
+
gem 'rubysl', '~> 2.0'
|
10
|
+
gem 'psych'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Specify your gem's dependencies in fakeredis.gemspec
|
14
|
+
gemspec :path => ".."
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module FakeRedis
|
2
|
+
module BitopCommand
|
3
|
+
BIT_OPERATORS = {
|
4
|
+
'or' => :|,
|
5
|
+
'and' => :&,
|
6
|
+
'xor' => :'^',
|
7
|
+
'not' => :~,
|
8
|
+
}
|
9
|
+
|
10
|
+
def bitop(operation, destkey, *keys)
|
11
|
+
if result = apply(operator(operation), keys)
|
12
|
+
set(destkey, result)
|
13
|
+
result.length
|
14
|
+
else
|
15
|
+
0
|
16
|
+
end
|
17
|
+
rescue ArgumentError => _
|
18
|
+
raise_argument_error('bitop')
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def operator(operation)
|
24
|
+
BIT_OPERATORS[operation.to_s.downcase]
|
25
|
+
end
|
26
|
+
|
27
|
+
def apply(operator, keys)
|
28
|
+
case operator
|
29
|
+
when :~
|
30
|
+
raise ArgumentError if keys.count != 1
|
31
|
+
bitwise_not(keys.first)
|
32
|
+
when :&, :|, :'^'
|
33
|
+
raise ArgumentError if keys.empty?
|
34
|
+
bitwise_operation(operator, keys)
|
35
|
+
else
|
36
|
+
raise ArgumentError
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def bitwise_not(key)
|
41
|
+
if value = get(keys.first)
|
42
|
+
value.bytes.map { |byte| ~ byte }.pack('c*')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def bitwise_operation(operation, keys)
|
47
|
+
apply_onto, *values = keys.map { |key| get(key) }.reject(&:nil?)
|
48
|
+
values.reduce(apply_onto) do |memo, value|
|
49
|
+
shorter, longer = [memo, value].sort_by(&:length).map(&:bytes).map(&:to_a)
|
50
|
+
longer.each_with_index.map do |byte, index|
|
51
|
+
byte.send(operation, shorter[index] || 0)
|
52
|
+
end.pack('c*')
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Require this either in your Gemfile or in your minitest configuration.
|
2
|
+
# Examples:
|
3
|
+
#
|
4
|
+
# # Gemfile
|
5
|
+
# group :test do
|
6
|
+
# gem 'minitest'
|
7
|
+
# gem 'fakeredis', :require => 'fakeredis/minitest'
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# # test/test_helper.rb (or test/minitest_config.rb)
|
11
|
+
# require 'fakeredis/minitest'
|
12
|
+
|
13
|
+
require 'fakeredis'
|
14
|
+
|
15
|
+
module FakeRedis
|
16
|
+
module Minitest
|
17
|
+
def setup
|
18
|
+
super
|
19
|
+
Redis::Connection::Memory.reset_all_databases
|
20
|
+
end
|
21
|
+
|
22
|
+
::Minitest::Test.send(:include, self)
|
23
|
+
end
|
24
|
+
end
|
data/lib/fakeredis/rspec.rb
CHANGED
@@ -3,6 +3,7 @@ module FakeRedis
|
|
3
3
|
module SortMethod
|
4
4
|
def sort(key, *redis_options_array)
|
5
5
|
return [] unless key
|
6
|
+
return [] if type(key) == 'none'
|
6
7
|
|
7
8
|
unless %w(list set zset).include? type(key)
|
8
9
|
warn "Operation against a key holding the wrong kind of value: Expected list, set or zset at #{key}."
|
@@ -21,7 +22,7 @@ module FakeRedis
|
|
21
22
|
# We have to flatten it down as redis-rb adds back the array to the return value
|
22
23
|
result = sliced.flatten(1)
|
23
24
|
|
24
|
-
options[:store] ? rpush(options[:store], sliced) :
|
25
|
+
options[:store] ? rpush(options[:store], sliced) : result
|
25
26
|
end
|
26
27
|
|
27
28
|
private
|
@@ -100,7 +101,7 @@ module FakeRedis
|
|
100
101
|
skip = limit.first || 0
|
101
102
|
take = limit.last || sorted.length
|
102
103
|
|
103
|
-
sorted[skip...(skip + take)] ||
|
104
|
+
sorted[skip...(skip + take)] || []
|
104
105
|
end
|
105
106
|
|
106
107
|
def lookup_from_pattern(pattern, element)
|
data/lib/fakeredis/version.rb
CHANGED
data/lib/fakeredis/zset.rb
CHANGED
@@ -5,6 +5,10 @@ module FakeRedis
|
|
5
5
|
super(key, _floatify(val))
|
6
6
|
end
|
7
7
|
|
8
|
+
def identical_scores?
|
9
|
+
values.uniq.size == 1
|
10
|
+
end
|
11
|
+
|
8
12
|
# Increments the value of key by val
|
9
13
|
def increment(key, val)
|
10
14
|
self[key] += _floatify(val)
|
@@ -25,8 +29,10 @@ module FakeRedis
|
|
25
29
|
elsif (( number = str.to_s.match(/^\((\d+)/i) ))
|
26
30
|
number[1].to_i + (increment ? 1 : -1)
|
27
31
|
else
|
28
|
-
Float str
|
32
|
+
Float str.to_s
|
29
33
|
end
|
34
|
+
rescue ArgumentError
|
35
|
+
raise Redis::CommandError, "ERR value is not a valid float"
|
30
36
|
end
|
31
37
|
|
32
38
|
end
|
@@ -8,6 +8,7 @@ require "fakeredis/sorted_set_argument_handler"
|
|
8
8
|
require "fakeredis/sorted_set_store"
|
9
9
|
require "fakeredis/transaction_commands"
|
10
10
|
require "fakeredis/zset"
|
11
|
+
require "fakeredis/bitop_command"
|
11
12
|
|
12
13
|
class Redis
|
13
14
|
module Connection
|
@@ -16,6 +17,7 @@ class Redis
|
|
16
17
|
include FakeRedis
|
17
18
|
include SortMethod
|
18
19
|
include TransactionCommands
|
20
|
+
include BitopCommand
|
19
21
|
include CommandExecutor
|
20
22
|
|
21
23
|
attr_accessor :options
|
@@ -35,6 +37,14 @@ class Redis
|
|
35
37
|
@databases = nil
|
36
38
|
end
|
37
39
|
|
40
|
+
def self.channels
|
41
|
+
@channels ||= Hash.new {|h,k| h[k] = [] }
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.reset_all_channels
|
45
|
+
@channels = nil
|
46
|
+
end
|
47
|
+
|
38
48
|
def self.connect(options = {})
|
39
49
|
new(options)
|
40
50
|
end
|
@@ -86,14 +96,6 @@ class Redis
|
|
86
96
|
replies.shift
|
87
97
|
end
|
88
98
|
|
89
|
-
# NOT IMPLEMENTED:
|
90
|
-
# * blpop
|
91
|
-
# * brpop
|
92
|
-
# * brpoplpush
|
93
|
-
# * subscribe
|
94
|
-
# * psubscribe
|
95
|
-
# * publish
|
96
|
-
|
97
99
|
def flushdb
|
98
100
|
databases.delete_at(database_id)
|
99
101
|
"OK"
|
@@ -146,6 +148,45 @@ class Redis
|
|
146
148
|
true
|
147
149
|
end
|
148
150
|
|
151
|
+
def dump(key)
|
152
|
+
return nil unless exists(key)
|
153
|
+
|
154
|
+
value = data[key]
|
155
|
+
|
156
|
+
Marshal.dump(
|
157
|
+
value: value,
|
158
|
+
version: FakeRedis::VERSION, # Redis includes the version, so we might as well
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
def restore(key, ttl, serialized_value)
|
163
|
+
raise Redis::CommandError, "ERR Target key name is busy." if exists(key)
|
164
|
+
|
165
|
+
raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong" if serialized_value.nil?
|
166
|
+
|
167
|
+
parsed_value = begin
|
168
|
+
Marshal.load(serialized_value)
|
169
|
+
rescue TypeError
|
170
|
+
raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong"
|
171
|
+
end
|
172
|
+
|
173
|
+
if parsed_value[:version] != FakeRedis::VERSION
|
174
|
+
raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong"
|
175
|
+
end
|
176
|
+
|
177
|
+
# We could figure out what type the key was and set it with the public API here,
|
178
|
+
# or we could just assign the value. If we presume the serialized_value is only ever
|
179
|
+
# a return value from `dump` then we've only been given something that was in
|
180
|
+
# the internal data structure anyway.
|
181
|
+
data[key] = parsed_value[:value]
|
182
|
+
|
183
|
+
# Set a TTL if one has been passed
|
184
|
+
ttl = ttl.to_i # Makes nil into 0
|
185
|
+
expire(key, ttl / 1000) unless ttl.zero?
|
186
|
+
|
187
|
+
"OK"
|
188
|
+
end
|
189
|
+
|
149
190
|
def get(key)
|
150
191
|
data_type_check(key, String)
|
151
192
|
data[key]
|
@@ -202,11 +243,22 @@ class Redis
|
|
202
243
|
end
|
203
244
|
|
204
245
|
def hdel(key, field)
|
205
|
-
field = field.to_s
|
206
246
|
data_type_check(key, Hash)
|
207
|
-
|
247
|
+
return 0 unless data[key]
|
248
|
+
|
249
|
+
if field.is_a?(Array)
|
250
|
+
old_keys_count = data[key].size
|
251
|
+
fields = field.map(&:to_s)
|
252
|
+
|
253
|
+
data[key].delete_if { |k, v| fields.include? k }
|
254
|
+
deleted = old_keys_count - data[key].size
|
255
|
+
else
|
256
|
+
field = field.to_s
|
257
|
+
deleted = data[key].delete(field) ? 1 : 0
|
258
|
+
end
|
259
|
+
|
208
260
|
remove_key_for_empty_collection(key)
|
209
|
-
deleted
|
261
|
+
deleted
|
210
262
|
end
|
211
263
|
|
212
264
|
def hkeys(key)
|
@@ -215,6 +267,44 @@ class Redis
|
|
215
267
|
data[key].keys
|
216
268
|
end
|
217
269
|
|
270
|
+
def hscan(key, start_cursor, *args)
|
271
|
+
data_type_check(key, Hash)
|
272
|
+
return ["0", []] unless data[key]
|
273
|
+
|
274
|
+
match = "*"
|
275
|
+
count = 10
|
276
|
+
|
277
|
+
if args.size.odd?
|
278
|
+
raise_argument_error('hscan')
|
279
|
+
end
|
280
|
+
|
281
|
+
if idx = args.index("MATCH")
|
282
|
+
match = args[idx + 1]
|
283
|
+
end
|
284
|
+
|
285
|
+
if idx = args.index("COUNT")
|
286
|
+
count = args[idx + 1]
|
287
|
+
end
|
288
|
+
|
289
|
+
start_cursor = start_cursor.to_i
|
290
|
+
|
291
|
+
cursor = start_cursor
|
292
|
+
next_keys = []
|
293
|
+
|
294
|
+
if start_cursor + count >= data[key].length
|
295
|
+
next_keys = (data[key].to_a)[start_cursor..-1]
|
296
|
+
cursor = 0
|
297
|
+
else
|
298
|
+
cursor = start_cursor + count
|
299
|
+
next_keys = (data[key].to_a)[start_cursor..cursor-1]
|
300
|
+
end
|
301
|
+
|
302
|
+
filtered_next_keys = next_keys.select{|k,v| File.fnmatch(match, k)}
|
303
|
+
result = filtered_next_keys.flatten.map(&:to_s)
|
304
|
+
|
305
|
+
return ["#{cursor}", result]
|
306
|
+
end
|
307
|
+
|
218
308
|
def keys(pattern = "*")
|
219
309
|
data.keys.select { |key| File.fnmatch(pattern, key) }
|
220
310
|
end
|
@@ -256,7 +346,14 @@ class Redis
|
|
256
346
|
|
257
347
|
def lrange(key, startidx, endidx)
|
258
348
|
data_type_check(key, Array)
|
259
|
-
|
349
|
+
if data[key]
|
350
|
+
# In Ruby when negative start index is out of range Array#slice returns
|
351
|
+
# nil which is not the case for lrange in Redis.
|
352
|
+
startidx = 0 if startidx < 0 && startidx.abs > data[key].size
|
353
|
+
data[key][startidx..endidx] || []
|
354
|
+
else
|
355
|
+
[]
|
356
|
+
end
|
260
357
|
end
|
261
358
|
|
262
359
|
def ltrim(key, start, stop)
|
@@ -284,7 +381,11 @@ class Redis
|
|
284
381
|
def linsert(key, where, pivot, value)
|
285
382
|
data_type_check(key, Array)
|
286
383
|
return unless data[key]
|
287
|
-
|
384
|
+
|
385
|
+
value = value.to_s
|
386
|
+
index = data[key].index(pivot.to_s)
|
387
|
+
return -1 if index.nil?
|
388
|
+
|
288
389
|
case where
|
289
390
|
when :before then data[key].insert(index, value)
|
290
391
|
when :after then data[key].insert(index + 1, value)
|
@@ -296,12 +397,14 @@ class Redis
|
|
296
397
|
data_type_check(key, Array)
|
297
398
|
return unless data[key]
|
298
399
|
raise Redis::CommandError, "ERR index out of range" if index >= data[key].size
|
299
|
-
data[key][index] = value
|
400
|
+
data[key][index] = value.to_s
|
300
401
|
end
|
301
402
|
|
302
403
|
def lrem(key, count, value)
|
303
404
|
data_type_check(key, Array)
|
304
|
-
return unless data[key]
|
405
|
+
return 0 unless data[key]
|
406
|
+
|
407
|
+
value = value.to_s
|
305
408
|
old_size = data[key].size
|
306
409
|
diff =
|
307
410
|
if count == 0
|
@@ -318,6 +421,7 @@ class Redis
|
|
318
421
|
end
|
319
422
|
|
320
423
|
def rpush(key, value)
|
424
|
+
raise_argument_error('rpush') if value.respond_to?(:each) && value.empty?
|
321
425
|
data_type_check(key, Array)
|
322
426
|
data[key] ||= []
|
323
427
|
[value].flatten.each do |val|
|
@@ -327,12 +431,14 @@ class Redis
|
|
327
431
|
end
|
328
432
|
|
329
433
|
def rpushx(key, value)
|
434
|
+
raise_argument_error('rpushx') if value.respond_to?(:each) && value.empty?
|
330
435
|
data_type_check(key, Array)
|
331
436
|
return unless data[key]
|
332
437
|
rpush(key, value)
|
333
438
|
end
|
334
439
|
|
335
440
|
def lpush(key, value)
|
441
|
+
raise_argument_error('lpush') if value.respond_to?(:each) && value.empty?
|
336
442
|
data_type_check(key, Array)
|
337
443
|
data[key] ||= []
|
338
444
|
[value].flatten.each do |val|
|
@@ -342,6 +448,7 @@ class Redis
|
|
342
448
|
end
|
343
449
|
|
344
450
|
def lpushx(key, value)
|
451
|
+
raise_argument_error('lpushx') if value.respond_to?(:each) && value.empty?
|
345
452
|
data_type_check(key, Array)
|
346
453
|
return unless data[key]
|
347
454
|
lpush(key, value)
|
@@ -353,6 +460,18 @@ class Redis
|
|
353
460
|
data[key].pop
|
354
461
|
end
|
355
462
|
|
463
|
+
def brpop(keys, timeout=0)
|
464
|
+
#todo threaded mode
|
465
|
+
keys = Array(keys)
|
466
|
+
keys.each do |key|
|
467
|
+
if data[key] && data[key].size > 0
|
468
|
+
return [key, data[key].pop]
|
469
|
+
end
|
470
|
+
end
|
471
|
+
sleep(timeout.to_f)
|
472
|
+
nil
|
473
|
+
end
|
474
|
+
|
356
475
|
def rpoplpush(key1, key2)
|
357
476
|
data_type_check(key1, Array)
|
358
477
|
rpop(key1).tap do |elem|
|
@@ -360,12 +479,31 @@ class Redis
|
|
360
479
|
end
|
361
480
|
end
|
362
481
|
|
482
|
+
def brpoplpush(key1, key2, opts={})
|
483
|
+
data_type_check(key1, Array)
|
484
|
+
brpop(key1).tap do |elem|
|
485
|
+
lpush(key2, elem) unless elem.nil?
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
363
489
|
def lpop(key)
|
364
490
|
data_type_check(key, Array)
|
365
491
|
return unless data[key]
|
366
492
|
data[key].shift
|
367
493
|
end
|
368
494
|
|
495
|
+
def blpop(keys, timeout=0)
|
496
|
+
#todo threaded mode
|
497
|
+
keys = Array(keys)
|
498
|
+
keys.each do |key|
|
499
|
+
if data[key] && data[key].size > 0
|
500
|
+
return [key, data[key].shift]
|
501
|
+
end
|
502
|
+
end
|
503
|
+
sleep(timeout.to_f)
|
504
|
+
nil
|
505
|
+
end
|
506
|
+
|
369
507
|
def smembers(key)
|
370
508
|
data_type_check(key, ::Set)
|
371
509
|
return [] unless data[key]
|
@@ -404,7 +542,7 @@ class Redis
|
|
404
542
|
if value.is_a?(Array)
|
405
543
|
old_size = data[key].size
|
406
544
|
values = value.map(&:to_s)
|
407
|
-
values.each { |
|
545
|
+
values.each { |v| data[key].delete(v) }
|
408
546
|
deleted = old_size - data[key].size
|
409
547
|
else
|
410
548
|
deleted = !!data[key].delete?(value.to_s)
|
@@ -435,6 +573,7 @@ class Redis
|
|
435
573
|
end
|
436
574
|
|
437
575
|
def sinter(*keys)
|
576
|
+
keys = keys[0] if flatten?(keys)
|
438
577
|
raise_argument_error('sinter') if keys.empty?
|
439
578
|
|
440
579
|
keys.each { |k| data_type_check(k, ::Set) }
|
@@ -452,6 +591,9 @@ class Redis
|
|
452
591
|
end
|
453
592
|
|
454
593
|
def sunion(*keys)
|
594
|
+
keys = keys[0] if flatten?(keys)
|
595
|
+
raise_argument_error('sunion') if keys.empty?
|
596
|
+
|
455
597
|
keys.each { |k| data_type_check(k, ::Set) }
|
456
598
|
keys = keys.map { |k| data[k] || ::Set.new }
|
457
599
|
keys.inject(::Set.new) do |set, key|
|
@@ -466,6 +608,7 @@ class Redis
|
|
466
608
|
end
|
467
609
|
|
468
610
|
def sdiff(key1, *keys)
|
611
|
+
keys = keys[0] if flatten?(keys)
|
469
612
|
[key1, *keys].each { |k| data_type_check(k, ::Set) }
|
470
613
|
keys = keys.map { |k| data[k] || ::Set.new }
|
471
614
|
keys.inject(data[key1] || Set.new) do |memo, set|
|
@@ -483,6 +626,44 @@ class Redis
|
|
483
626
|
number.nil? ? srandmember_single(key) : srandmember_multiple(key, number)
|
484
627
|
end
|
485
628
|
|
629
|
+
def sscan(key, start_cursor, *args)
|
630
|
+
data_type_check(key, ::Set)
|
631
|
+
return ["0", []] unless data[key]
|
632
|
+
|
633
|
+
match = "*"
|
634
|
+
count = 10
|
635
|
+
|
636
|
+
if args.size.odd?
|
637
|
+
raise_argument_error('sscan')
|
638
|
+
end
|
639
|
+
|
640
|
+
if idx = args.index("MATCH")
|
641
|
+
match = args[idx + 1]
|
642
|
+
end
|
643
|
+
|
644
|
+
if idx = args.index("COUNT")
|
645
|
+
count = args[idx + 1]
|
646
|
+
end
|
647
|
+
|
648
|
+
start_cursor = start_cursor.to_i
|
649
|
+
|
650
|
+
cursor = start_cursor
|
651
|
+
next_keys = []
|
652
|
+
|
653
|
+
if start_cursor + count >= data[key].length
|
654
|
+
next_keys = (data[key].to_a)[start_cursor..-1]
|
655
|
+
cursor = 0
|
656
|
+
else
|
657
|
+
cursor = start_cursor + count
|
658
|
+
next_keys = (data[key].to_a)[start_cursor..cursor-1]
|
659
|
+
end
|
660
|
+
|
661
|
+
filtered_next_keys = next_keys.select{ |k,v| File.fnmatch(match, k)}
|
662
|
+
result = filtered_next_keys.flatten.map(&:to_s)
|
663
|
+
|
664
|
+
return ["#{cursor}", result]
|
665
|
+
end
|
666
|
+
|
486
667
|
def del(*keys)
|
487
668
|
keys = keys.flatten(1)
|
488
669
|
raise_argument_error('del') if keys.empty?
|
@@ -496,10 +677,10 @@ class Redis
|
|
496
677
|
|
497
678
|
def setnx(key, value)
|
498
679
|
if exists(key)
|
499
|
-
|
680
|
+
0
|
500
681
|
else
|
501
682
|
set(key, value)
|
502
|
-
|
683
|
+
1
|
503
684
|
end
|
504
685
|
end
|
505
686
|
|
@@ -525,6 +706,12 @@ class Redis
|
|
525
706
|
1
|
526
707
|
end
|
527
708
|
|
709
|
+
def pexpire(key, ttl)
|
710
|
+
return 0 unless data[key]
|
711
|
+
data.expires[key] = Time.now + (ttl / 1000.0)
|
712
|
+
1
|
713
|
+
end
|
714
|
+
|
528
715
|
def ttl(key)
|
529
716
|
if data.expires.include?(key) && (ttl = data.expires[key].to_i - Time.now.to_i) > 0
|
530
717
|
ttl
|
@@ -533,6 +720,14 @@ class Redis
|
|
533
720
|
end
|
534
721
|
end
|
535
722
|
|
723
|
+
def pttl(key)
|
724
|
+
if data.expires.include?(key) && (ttl = data.expires[key].to_f - Time.now.to_f) > 0
|
725
|
+
ttl * 1000
|
726
|
+
else
|
727
|
+
exists(key) ? -1 : -2
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
536
731
|
def expireat(key, timestamp)
|
537
732
|
data.expires[key] = Time.at(timestamp)
|
538
733
|
true
|
@@ -548,10 +743,10 @@ class Redis
|
|
548
743
|
if data[key]
|
549
744
|
result = !data[key].include?(field)
|
550
745
|
data[key][field] = value.to_s
|
551
|
-
result
|
746
|
+
result ? 1 : 0
|
552
747
|
else
|
553
748
|
data[key] = { field => value.to_s }
|
554
|
-
|
749
|
+
1
|
555
750
|
end
|
556
751
|
end
|
557
752
|
|
@@ -727,6 +922,11 @@ class Redis
|
|
727
922
|
data[key].to_i
|
728
923
|
end
|
729
924
|
|
925
|
+
def incrbyfloat(key, by)
|
926
|
+
data.merge!({ key => (data[key].to_f + by.to_f).to_s || by })
|
927
|
+
data[key]
|
928
|
+
end
|
929
|
+
|
730
930
|
def decr(key)
|
731
931
|
data.merge!({ key => (data[key].to_i - 1).to_s || "-1"})
|
732
932
|
data[key].to_i
|
@@ -758,10 +958,6 @@ class Redis
|
|
758
958
|
match = "*"
|
759
959
|
count = 10
|
760
960
|
|
761
|
-
if args.size.odd?
|
762
|
-
raise_argument_error('scan')
|
763
|
-
end
|
764
|
-
|
765
961
|
if idx = args.index("MATCH")
|
766
962
|
match = args[idx + 1]
|
767
963
|
end
|
@@ -774,17 +970,22 @@ class Redis
|
|
774
970
|
data_type_check(start_cursor, Fixnum)
|
775
971
|
|
776
972
|
cursor = start_cursor
|
777
|
-
|
973
|
+
returned_keys = []
|
974
|
+
final_page = start_cursor + count >= keys(match).length
|
975
|
+
|
976
|
+
if final_page
|
977
|
+
previous_keys_been_deleted = (count >= keys(match).length)
|
978
|
+
start_index = previous_keys_been_deleted ? 0 : cursor
|
778
979
|
|
779
|
-
|
780
|
-
next_keys = keys(match)[start_cursor..-1]
|
980
|
+
returned_keys = keys(match)[start_index..-1]
|
781
981
|
cursor = 0
|
782
982
|
else
|
783
|
-
|
784
|
-
|
983
|
+
end_index = start_cursor + (count - 1)
|
984
|
+
returned_keys = keys(match)[start_cursor..end_index]
|
985
|
+
cursor = start_cursor + count
|
785
986
|
end
|
786
987
|
|
787
|
-
return "#{cursor}",
|
988
|
+
return "#{cursor}", returned_keys
|
788
989
|
end
|
789
990
|
|
790
991
|
def zadd(key, *args)
|
@@ -873,19 +1074,44 @@ class Redis
|
|
873
1074
|
data_type_check(key, ZSet)
|
874
1075
|
return [] unless data[key]
|
875
1076
|
|
876
|
-
|
877
|
-
results = data[key].sort do |(k1, v1), (k2, v2)|
|
878
|
-
if v1 == v2
|
879
|
-
k1 <=> k2
|
880
|
-
else
|
881
|
-
v1 <=> v2
|
882
|
-
end
|
883
|
-
end
|
1077
|
+
results = sort_keys(data[key])
|
884
1078
|
# Select just the keys unless we want scores
|
885
1079
|
results = results.map(&:first) unless with_scores
|
886
1080
|
results[start..stop].flatten.map(&:to_s)
|
887
1081
|
end
|
888
1082
|
|
1083
|
+
def zrangebylex(key, start, stop, *opts)
|
1084
|
+
data_type_check(key, ZSet)
|
1085
|
+
return [] unless data[key]
|
1086
|
+
zset = data[key]
|
1087
|
+
|
1088
|
+
sorted = if zset.identical_scores?
|
1089
|
+
zset.keys.sort { |x, y| x.to_s <=> y.to_s }
|
1090
|
+
else
|
1091
|
+
zset.keys
|
1092
|
+
end
|
1093
|
+
|
1094
|
+
range = get_range start, stop, sorted.first, sorted.last
|
1095
|
+
|
1096
|
+
filtered = []
|
1097
|
+
sorted.each do |element|
|
1098
|
+
filtered << element if (range[0][:value]..range[1][:value]).cover?(element)
|
1099
|
+
end
|
1100
|
+
filtered.shift if filtered[0] == range[0][:value] && !range[0][:inclusive]
|
1101
|
+
filtered.pop if filtered.last == range[1][:value] && !range[1][:inclusive]
|
1102
|
+
|
1103
|
+
limit = get_limit(opts, filtered)
|
1104
|
+
if limit
|
1105
|
+
filtered = filtered[limit[0]..-1].take(limit[1])
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
filtered
|
1109
|
+
end
|
1110
|
+
|
1111
|
+
def zrevrangebylex(key, start, stop, *args)
|
1112
|
+
zrangebylex(key, stop, start, args).reverse
|
1113
|
+
end
|
1114
|
+
|
889
1115
|
def zrevrange(key, start, stop, with_scores = nil)
|
890
1116
|
data_type_check(key, ZSet)
|
891
1117
|
return [] unless data[key]
|
@@ -966,6 +1192,131 @@ class Redis
|
|
966
1192
|
data[out].size
|
967
1193
|
end
|
968
1194
|
|
1195
|
+
def subscribe(*channels)
|
1196
|
+
raise_argument_error('subscribe') if channels.empty?()
|
1197
|
+
|
1198
|
+
#Create messages for all data from the channels
|
1199
|
+
channel_replies = channels.map do |channel|
|
1200
|
+
self.class.channels[channel].slice!(0..-1).map!{|v| ["message", channel, v]}
|
1201
|
+
end
|
1202
|
+
channel_replies.flatten!(1)
|
1203
|
+
channel_replies.compact!()
|
1204
|
+
|
1205
|
+
#Put messages into the replies for the future
|
1206
|
+
channels.each_with_index do |channel,index|
|
1207
|
+
replies << ["subscribe", channel, index+1]
|
1208
|
+
end
|
1209
|
+
replies.push(*channel_replies)
|
1210
|
+
|
1211
|
+
#Add unsubscribe message to stop blocking (see https://github.com/redis/redis-rb/blob/v3.2.1/lib/redis/subscribe.rb#L38)
|
1212
|
+
replies.push(self.unsubscribe())
|
1213
|
+
|
1214
|
+
replies.pop() #Last reply will be pushed back on
|
1215
|
+
end
|
1216
|
+
|
1217
|
+
def psubscribe(*patterns)
|
1218
|
+
raise_argument_error('psubscribe') if patterns.empty?()
|
1219
|
+
|
1220
|
+
#Create messages for all data from the channels
|
1221
|
+
channel_replies = self.class.channels.keys.map do |channel|
|
1222
|
+
pattern = patterns.find{|p| File.fnmatch(p, channel) }
|
1223
|
+
unless pattern.nil?()
|
1224
|
+
self.class.channels[channel].slice!(0..-1).map!{|v| ["pmessage", pattern, channel, v]}
|
1225
|
+
end
|
1226
|
+
end
|
1227
|
+
channel_replies.flatten!(1)
|
1228
|
+
channel_replies.compact!()
|
1229
|
+
|
1230
|
+
#Put messages into the replies for the future
|
1231
|
+
patterns.each_with_index do |pattern,index|
|
1232
|
+
replies << ["psubscribe", pattern, index+1]
|
1233
|
+
end
|
1234
|
+
replies.push(*channel_replies)
|
1235
|
+
|
1236
|
+
#Add unsubscribe to stop blocking
|
1237
|
+
replies.push(self.punsubscribe())
|
1238
|
+
|
1239
|
+
replies.pop() #Last reply will be pushed back on
|
1240
|
+
end
|
1241
|
+
|
1242
|
+
def publish(channel, message)
|
1243
|
+
self.class.channels[channel] << message
|
1244
|
+
0 #Just fake number of subscribers
|
1245
|
+
end
|
1246
|
+
|
1247
|
+
def unsubscribe(*channels)
|
1248
|
+
if channels.empty?()
|
1249
|
+
replies << ["unsubscribe", nil, 0]
|
1250
|
+
else
|
1251
|
+
channels.each do |channel|
|
1252
|
+
replies << ["unsubscribe", channel, 0]
|
1253
|
+
end
|
1254
|
+
end
|
1255
|
+
replies.pop() #Last reply will be pushed back on
|
1256
|
+
end
|
1257
|
+
|
1258
|
+
def punsubscribe(*patterns)
|
1259
|
+
if patterns.empty?()
|
1260
|
+
replies << ["punsubscribe", nil, 0]
|
1261
|
+
else
|
1262
|
+
patterns.each do |pattern|
|
1263
|
+
replies << ["punsubscribe", pattern, 0]
|
1264
|
+
end
|
1265
|
+
end
|
1266
|
+
replies.pop() #Last reply will be pushed back on
|
1267
|
+
end
|
1268
|
+
|
1269
|
+
def zscan(key, start_cursor, *args)
|
1270
|
+
data_type_check(key, ZSet)
|
1271
|
+
return [] unless data[key]
|
1272
|
+
|
1273
|
+
match = "*"
|
1274
|
+
count = 10
|
1275
|
+
|
1276
|
+
if args.size.odd?
|
1277
|
+
raise_argument_error('zscan')
|
1278
|
+
end
|
1279
|
+
|
1280
|
+
if idx = args.index("MATCH")
|
1281
|
+
match = args[idx + 1]
|
1282
|
+
end
|
1283
|
+
|
1284
|
+
if idx = args.index("COUNT")
|
1285
|
+
count = args[idx + 1]
|
1286
|
+
end
|
1287
|
+
|
1288
|
+
start_cursor = start_cursor.to_i
|
1289
|
+
data_type_check(start_cursor, Fixnum)
|
1290
|
+
|
1291
|
+
cursor = start_cursor
|
1292
|
+
next_keys = []
|
1293
|
+
|
1294
|
+
sorted_keys = sort_keys(data[key])
|
1295
|
+
|
1296
|
+
if start_cursor + count >= sorted_keys.length
|
1297
|
+
next_keys = sorted_keys.to_a.select { |k| File.fnmatch(match, k[0]) } [start_cursor..-1]
|
1298
|
+
cursor = 0
|
1299
|
+
else
|
1300
|
+
cursor = start_cursor + count
|
1301
|
+
next_keys = sorted_keys.to_a.select { |k| File.fnmatch(match, k[0]) } [start_cursor..cursor-1]
|
1302
|
+
end
|
1303
|
+
return "#{cursor}", next_keys.flatten.map(&:to_s)
|
1304
|
+
end
|
1305
|
+
|
1306
|
+
# Originally from redis-rb
|
1307
|
+
def zscan_each(key, *args, &block)
|
1308
|
+
data_type_check(key, ZSet)
|
1309
|
+
return [] unless data[key]
|
1310
|
+
|
1311
|
+
return to_enum(:zscan_each, key, options) unless block_given?
|
1312
|
+
cursor = 0
|
1313
|
+
loop do
|
1314
|
+
cursor, values = zscan(key, cursor, options)
|
1315
|
+
values.each(&block)
|
1316
|
+
break if cursor == "0"
|
1317
|
+
end
|
1318
|
+
end
|
1319
|
+
|
969
1320
|
private
|
970
1321
|
def raise_argument_error(command, match_string=command)
|
971
1322
|
error_message = if %w(hmset mset_odd).include?(match_string.downcase)
|
@@ -992,6 +1343,27 @@ class Redis
|
|
992
1343
|
end
|
993
1344
|
end
|
994
1345
|
|
1346
|
+
def get_range(start, stop, min = -Float::INFINITY, max = Float::INFINITY)
|
1347
|
+
range_options = []
|
1348
|
+
|
1349
|
+
[start, stop].each do |value|
|
1350
|
+
case value[0]
|
1351
|
+
when "-"
|
1352
|
+
range_options << { value: min, inclusive: true }
|
1353
|
+
when "+"
|
1354
|
+
range_options << { value: max, inclusive: true }
|
1355
|
+
when "["
|
1356
|
+
range_options << { value: value[1..-1], inclusive: true }
|
1357
|
+
when "("
|
1358
|
+
range_options << { value: value[1..-1], inclusive: false }
|
1359
|
+
else
|
1360
|
+
raise Redis::CommandError, "ERR min or max not valid string range item"
|
1361
|
+
end
|
1362
|
+
end
|
1363
|
+
|
1364
|
+
range_options
|
1365
|
+
end
|
1366
|
+
|
995
1367
|
def get_limit(opts, vals)
|
996
1368
|
index = opts.index('LIMIT')
|
997
1369
|
|
@@ -1008,6 +1380,9 @@ class Redis
|
|
1008
1380
|
def mapped_param? param
|
1009
1381
|
param.size == 1 && param[0].is_a?(Array)
|
1010
1382
|
end
|
1383
|
+
# NOTE : Redis-rb 3.x will flatten *args, so method(["a", "b", "c"])
|
1384
|
+
# should be handled the same way as method("a", "b", "c")
|
1385
|
+
alias_method :flatten?, :mapped_param?
|
1011
1386
|
|
1012
1387
|
def srandmember_single(key)
|
1013
1388
|
data_type_check(key, ::Set)
|
@@ -1027,6 +1402,17 @@ class Redis
|
|
1027
1402
|
(1..-number).map { data[key].to_a[rand(data[key].size)] }.flatten
|
1028
1403
|
end
|
1029
1404
|
end
|
1405
|
+
|
1406
|
+
def sort_keys(arr)
|
1407
|
+
# Sort by score, or if scores are equal, key alphanum
|
1408
|
+
sorted_keys = arr.sort do |(k1, v1), (k2, v2)|
|
1409
|
+
if v1 == v2
|
1410
|
+
k1 <=> k2
|
1411
|
+
else
|
1412
|
+
v1 <=> v2
|
1413
|
+
end
|
1414
|
+
end
|
1415
|
+
end
|
1030
1416
|
end
|
1031
1417
|
end
|
1032
1418
|
end
|