fakeredis 0.5.0 → 0.8.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 +5 -5
- data/.gitignore +3 -0
- data/.travis.yml +14 -5
- data/LICENSE +1 -1
- data/README.md +42 -24
- data/fakeredis.gemspec +1 -1
- data/lib/fakeredis.rb +28 -0
- data/lib/fakeredis/bitop_command.rb +56 -0
- data/lib/fakeredis/command_executor.rb +6 -9
- data/lib/fakeredis/expiring_hash.rb +3 -5
- data/lib/fakeredis/geo_commands.rb +142 -0
- data/lib/fakeredis/geo_set.rb +84 -0
- data/lib/fakeredis/minitest.rb +24 -0
- data/lib/fakeredis/rspec.rb +1 -0
- data/lib/fakeredis/sort_method.rb +3 -3
- data/lib/fakeredis/sorted_set_store.rb +1 -1
- data/lib/fakeredis/transaction_commands.rb +2 -2
- data/lib/fakeredis/version.rb +1 -1
- data/lib/fakeredis/zset.rb +8 -2
- data/lib/redis/connection/memory.rb +650 -82
- data/spec/bitop_command_spec.rb +209 -0
- data/spec/command_executor_spec.rb +15 -0
- data/spec/compatibility_spec.rb +1 -1
- data/spec/connection_spec.rb +21 -21
- data/spec/fakeredis_spec.rb +73 -0
- data/spec/geo_set_spec.rb +164 -0
- data/spec/hashes_spec.rb +138 -57
- data/spec/hyper_log_logs_spec.rb +50 -0
- data/spec/keys_spec.rb +232 -90
- data/spec/lists_spec.rb +91 -35
- data/spec/memory_spec.rb +80 -7
- data/spec/server_spec.rb +38 -24
- data/spec/sets_spec.rb +112 -46
- data/spec/sort_method_spec.rb +6 -0
- data/spec/sorted_sets_spec.rb +482 -150
- data/spec/spec_helper.rb +9 -18
- data/spec/spec_helper_live_redis.rb +4 -4
- data/spec/strings_spec.rb +113 -79
- 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 +34 -13
- data/spec/upcase_method_name_spec.rb +2 -2
- metadata +23 -6
@@ -0,0 +1,84 @@
|
|
1
|
+
module FakeRedis
|
2
|
+
class GeoSet
|
3
|
+
class Point
|
4
|
+
BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz" # (geohash-specific) Base32 map
|
5
|
+
EARTH_RADIUS_IN_M = 6_378_100.0
|
6
|
+
|
7
|
+
attr_reader :lon, :lat, :name
|
8
|
+
|
9
|
+
def initialize(lon, lat, name)
|
10
|
+
@lon = Float(lon)
|
11
|
+
@lat = Float(lat)
|
12
|
+
@name = name
|
13
|
+
end
|
14
|
+
|
15
|
+
def geohash(precision = 10)
|
16
|
+
latlon = [@lat, @lon]
|
17
|
+
ranges = [[-90.0, 90.0], [-180.0, 180.0]]
|
18
|
+
coordinate = 1
|
19
|
+
|
20
|
+
(0...precision).map do
|
21
|
+
index = 0 # index into base32 map
|
22
|
+
|
23
|
+
5.times do |bit|
|
24
|
+
mid = (ranges[coordinate][0] + ranges[coordinate][1]) / 2
|
25
|
+
if latlon[coordinate] >= mid
|
26
|
+
index = index * 2 + 1
|
27
|
+
ranges[coordinate][0] = mid
|
28
|
+
else
|
29
|
+
index *= 2
|
30
|
+
ranges[coordinate][1] = mid
|
31
|
+
end
|
32
|
+
|
33
|
+
coordinate ^= 1
|
34
|
+
end
|
35
|
+
|
36
|
+
BASE32[index]
|
37
|
+
end.join
|
38
|
+
end
|
39
|
+
|
40
|
+
def distance_to(other)
|
41
|
+
lat1 = deg_to_rad(@lat)
|
42
|
+
lon1 = deg_to_rad(@lon)
|
43
|
+
lat2 = deg_to_rad(other.lat)
|
44
|
+
lon2 = deg_to_rad(other.lon)
|
45
|
+
haversine_distance(lat1, lon1, lat2, lon2)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def deg_to_rad(deg)
|
51
|
+
deg * Math::PI / 180.0
|
52
|
+
end
|
53
|
+
|
54
|
+
def haversine_distance(lat1, lon1, lat2, lon2)
|
55
|
+
h = Math.sin((lat2 - lat1) / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) *
|
56
|
+
Math.sin((lon2 - lon1) / 2) ** 2
|
57
|
+
|
58
|
+
2 * EARTH_RADIUS_IN_M * Math.asin(Math.sqrt(h))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def initialize
|
63
|
+
@points = {}
|
64
|
+
end
|
65
|
+
|
66
|
+
def size
|
67
|
+
@points.size
|
68
|
+
end
|
69
|
+
|
70
|
+
def add(lon, lat, name)
|
71
|
+
@points[name] = Point.new(lon, lat, name)
|
72
|
+
end
|
73
|
+
|
74
|
+
def get(name)
|
75
|
+
@points[name]
|
76
|
+
end
|
77
|
+
|
78
|
+
def points_within_radius(center, radius)
|
79
|
+
@points.values.select do |point|
|
80
|
+
point.distance_to(center) <= radius
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
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
|
+
Redis::Connection::Memory.reset_all_databases
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
::Minitest::Test.send(:include, self)
|
23
|
+
end
|
24
|
+
end
|
data/lib/fakeredis/rspec.rb
CHANGED
@@ -3,9 +3,9 @@ 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
|
-
warn "Operation against a key holding the wrong kind of value: Expected list, set or zset at #{key}."
|
9
9
|
raise Redis::CommandError.new("WRONGTYPE Operation against a key holding the wrong kind of value")
|
10
10
|
end
|
11
11
|
|
@@ -21,7 +21,7 @@ module FakeRedis
|
|
21
21
|
# We have to flatten it down as redis-rb adds back the array to the return value
|
22
22
|
result = sliced.flatten(1)
|
23
23
|
|
24
|
-
options[:store] ? rpush(options[:store], sliced) :
|
24
|
+
options[:store] ? rpush(options[:store], sliced) : result
|
25
25
|
end
|
26
26
|
|
27
27
|
private
|
@@ -100,7 +100,7 @@ module FakeRedis
|
|
100
100
|
skip = limit.first || 0
|
101
101
|
take = limit.last || sorted.length
|
102
102
|
|
103
|
-
sorted[skip...(skip + take)] ||
|
103
|
+
sorted[skip...(skip + take)] || []
|
104
104
|
end
|
105
105
|
|
106
106
|
def lookup_from_pattern(pattern, element)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module FakeRedis
|
2
|
-
TRANSACTION_COMMANDS = [:discard, :exec, :multi, :watch, :unwatch]
|
2
|
+
TRANSACTION_COMMANDS = [:discard, :exec, :multi, :watch, :unwatch, :client]
|
3
3
|
|
4
4
|
module TransactionCommands
|
5
5
|
def self.included(klass)
|
@@ -72,7 +72,7 @@ module FakeRedis
|
|
72
72
|
"OK"
|
73
73
|
end
|
74
74
|
|
75
|
-
def watch(_)
|
75
|
+
def watch(*_)
|
76
76
|
"OK"
|
77
77
|
end
|
78
78
|
|
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)
|
@@ -13,7 +17,7 @@ module FakeRedis
|
|
13
17
|
def select_by_score min, max
|
14
18
|
min = _floatify(min, true)
|
15
19
|
max = _floatify(max, false)
|
16
|
-
|
20
|
+
select {|_,v| v >= min && v <= max }
|
17
21
|
end
|
18
22
|
|
19
23
|
private
|
@@ -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,14 +8,21 @@ 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"
|
12
|
+
require "fakeredis/geo_commands"
|
13
|
+
require "fakeredis/version"
|
11
14
|
|
12
15
|
class Redis
|
13
16
|
module Connection
|
17
|
+
DEFAULT_REDIS_VERSION = '3.3.5'
|
18
|
+
|
14
19
|
class Memory
|
15
20
|
include Redis::Connection::CommandHelper
|
16
21
|
include FakeRedis
|
17
22
|
include SortMethod
|
18
23
|
include TransactionCommands
|
24
|
+
include BitopCommand
|
25
|
+
include GeoCommands
|
19
26
|
include CommandExecutor
|
20
27
|
|
21
28
|
attr_accessor :options
|
@@ -35,17 +42,26 @@ class Redis
|
|
35
42
|
@databases = nil
|
36
43
|
end
|
37
44
|
|
45
|
+
def self.channels
|
46
|
+
@channels ||= Hash.new {|h,k| h[k] = [] }
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.reset_all_channels
|
50
|
+
@channels = nil
|
51
|
+
end
|
52
|
+
|
38
53
|
def self.connect(options = {})
|
39
54
|
new(options)
|
40
55
|
end
|
41
56
|
|
42
57
|
def initialize(options = {})
|
43
|
-
self.options = options
|
58
|
+
self.options = self.options ? self.options.merge(options) : options
|
44
59
|
end
|
45
60
|
|
46
61
|
def database_id
|
47
62
|
@database_id ||= 0
|
48
63
|
end
|
64
|
+
|
49
65
|
attr_writer :database_id
|
50
66
|
|
51
67
|
def database_instance_key
|
@@ -79,6 +95,15 @@ class Redis
|
|
79
95
|
def disconnect
|
80
96
|
end
|
81
97
|
|
98
|
+
def client(command, _options = {})
|
99
|
+
case command
|
100
|
+
when :setname then "OK"
|
101
|
+
when :getname then nil
|
102
|
+
else
|
103
|
+
raise Redis::CommandError, "ERR unknown command '#{command}'"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
82
107
|
def timeout=(usecs)
|
83
108
|
end
|
84
109
|
|
@@ -86,14 +111,6 @@ class Redis
|
|
86
111
|
replies.shift
|
87
112
|
end
|
88
113
|
|
89
|
-
# NOT IMPLEMENTED:
|
90
|
-
# * blpop
|
91
|
-
# * brpop
|
92
|
-
# * brpoplpush
|
93
|
-
# * subscribe
|
94
|
-
# * psubscribe
|
95
|
-
# * publish
|
96
|
-
|
97
114
|
def flushdb
|
98
115
|
databases.delete_at(database_id)
|
99
116
|
"OK"
|
@@ -116,7 +133,7 @@ class Redis
|
|
116
133
|
|
117
134
|
def info
|
118
135
|
{
|
119
|
-
"redis_version" =>
|
136
|
+
"redis_version" => options[:version] || DEFAULT_REDIS_VERSION,
|
120
137
|
"connected_clients" => "1",
|
121
138
|
"connected_slaves" => "0",
|
122
139
|
"used_memory" => "3187",
|
@@ -133,9 +150,13 @@ class Redis
|
|
133
150
|
|
134
151
|
def save; end
|
135
152
|
|
136
|
-
def bgsave
|
153
|
+
def bgsave; end
|
154
|
+
|
155
|
+
def bgrewriteaof; end
|
156
|
+
|
157
|
+
def evalsha; end
|
137
158
|
|
138
|
-
def
|
159
|
+
def eval; end
|
139
160
|
|
140
161
|
def move key, destination_id
|
141
162
|
raise Redis::CommandError, "ERR source and destination objects are the same" if destination_id == database_id
|
@@ -146,6 +167,45 @@ class Redis
|
|
146
167
|
true
|
147
168
|
end
|
148
169
|
|
170
|
+
def dump(key)
|
171
|
+
return nil unless exists(key)
|
172
|
+
|
173
|
+
value = data[key]
|
174
|
+
|
175
|
+
Marshal.dump(
|
176
|
+
value: value,
|
177
|
+
version: FakeRedis::VERSION, # Redis includes the version, so we might as well
|
178
|
+
)
|
179
|
+
end
|
180
|
+
|
181
|
+
def restore(key, ttl, serialized_value)
|
182
|
+
raise Redis::CommandError, "ERR Target key name is busy." if exists(key)
|
183
|
+
|
184
|
+
raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong" if serialized_value.nil?
|
185
|
+
|
186
|
+
parsed_value = begin
|
187
|
+
Marshal.load(serialized_value)
|
188
|
+
rescue TypeError
|
189
|
+
raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong"
|
190
|
+
end
|
191
|
+
|
192
|
+
if parsed_value[:version] != FakeRedis::VERSION
|
193
|
+
raise Redis::CommandError, "ERR DUMP payload version or checksum are wrong"
|
194
|
+
end
|
195
|
+
|
196
|
+
# We could figure out what type the key was and set it with the public API here,
|
197
|
+
# or we could just assign the value. If we presume the serialized_value is only ever
|
198
|
+
# a return value from `dump` then we've only been given something that was in
|
199
|
+
# the internal data structure anyway.
|
200
|
+
data[key] = parsed_value[:value]
|
201
|
+
|
202
|
+
# Set a TTL if one has been passed
|
203
|
+
ttl = ttl.to_i # Makes nil into 0
|
204
|
+
expire(key, ttl / 1000) unless ttl.zero?
|
205
|
+
|
206
|
+
"OK"
|
207
|
+
end
|
208
|
+
|
149
209
|
def get(key)
|
150
210
|
data_type_check(key, String)
|
151
211
|
data[key]
|
@@ -161,6 +221,11 @@ class Redis
|
|
161
221
|
data[key][start_index..end_index].unpack('B*')[0].count("1")
|
162
222
|
end
|
163
223
|
|
224
|
+
def bitpos(key, bit, start_index = 0, end_index = -1)
|
225
|
+
value = data[key] || ""
|
226
|
+
value[0..end_index].unpack('B*')[0].index(bit.to_s, start_index * 8) || -1
|
227
|
+
end
|
228
|
+
|
164
229
|
def getrange(key, start, ending)
|
165
230
|
return unless data[key]
|
166
231
|
data[key][start..ending]
|
@@ -202,11 +267,22 @@ class Redis
|
|
202
267
|
end
|
203
268
|
|
204
269
|
def hdel(key, field)
|
205
|
-
field = field.to_s
|
206
270
|
data_type_check(key, Hash)
|
207
|
-
|
271
|
+
return 0 unless data[key]
|
272
|
+
|
273
|
+
if field.is_a?(Array)
|
274
|
+
old_keys_count = data[key].size
|
275
|
+
fields = field.map(&:to_s)
|
276
|
+
|
277
|
+
data[key].delete_if { |k, v| fields.include? k }
|
278
|
+
deleted = old_keys_count - data[key].size
|
279
|
+
else
|
280
|
+
field = field.to_s
|
281
|
+
deleted = data[key].delete(field) ? 1 : 0
|
282
|
+
end
|
283
|
+
|
208
284
|
remove_key_for_empty_collection(key)
|
209
|
-
deleted
|
285
|
+
deleted
|
210
286
|
end
|
211
287
|
|
212
288
|
def hkeys(key)
|
@@ -215,6 +291,44 @@ class Redis
|
|
215
291
|
data[key].keys
|
216
292
|
end
|
217
293
|
|
294
|
+
def hscan(key, start_cursor, *args)
|
295
|
+
data_type_check(key, Hash)
|
296
|
+
return ["0", []] unless data[key]
|
297
|
+
|
298
|
+
match = "*"
|
299
|
+
count = 10
|
300
|
+
|
301
|
+
if args.size.odd?
|
302
|
+
raise_argument_error('hscan')
|
303
|
+
end
|
304
|
+
|
305
|
+
if idx = args.index("MATCH")
|
306
|
+
match = args[idx + 1]
|
307
|
+
end
|
308
|
+
|
309
|
+
if idx = args.index("COUNT")
|
310
|
+
count = args[idx + 1]
|
311
|
+
end
|
312
|
+
|
313
|
+
start_cursor = start_cursor.to_i
|
314
|
+
|
315
|
+
cursor = start_cursor
|
316
|
+
next_keys = []
|
317
|
+
|
318
|
+
if start_cursor + count >= data[key].length
|
319
|
+
next_keys = (data[key].to_a)[start_cursor..-1]
|
320
|
+
cursor = 0
|
321
|
+
else
|
322
|
+
cursor = start_cursor + count
|
323
|
+
next_keys = (data[key].to_a)[start_cursor..cursor-1]
|
324
|
+
end
|
325
|
+
|
326
|
+
filtered_next_keys = next_keys.select{|k,v| File.fnmatch(match, k)}
|
327
|
+
result = filtered_next_keys.flatten.map(&:to_s)
|
328
|
+
|
329
|
+
return ["#{cursor}", result]
|
330
|
+
end
|
331
|
+
|
218
332
|
def keys(pattern = "*")
|
219
333
|
data.keys.select { |key| File.fnmatch(pattern, key) }
|
220
334
|
end
|
@@ -256,7 +370,14 @@ class Redis
|
|
256
370
|
|
257
371
|
def lrange(key, startidx, endidx)
|
258
372
|
data_type_check(key, Array)
|
259
|
-
|
373
|
+
if data[key]
|
374
|
+
# In Ruby when negative start index is out of range Array#slice returns
|
375
|
+
# nil which is not the case for lrange in Redis.
|
376
|
+
startidx = 0 if startidx < 0 && startidx.abs > data[key].size
|
377
|
+
data[key][startidx..endidx] || []
|
378
|
+
else
|
379
|
+
[]
|
380
|
+
end
|
260
381
|
end
|
261
382
|
|
262
383
|
def ltrim(key, start, stop)
|
@@ -284,10 +405,14 @@ class Redis
|
|
284
405
|
def linsert(key, where, pivot, value)
|
285
406
|
data_type_check(key, Array)
|
286
407
|
return unless data[key]
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
408
|
+
|
409
|
+
value = value.to_s
|
410
|
+
index = data[key].index(pivot.to_s)
|
411
|
+
return -1 if index.nil?
|
412
|
+
|
413
|
+
case where.to_s
|
414
|
+
when /\Abefore\z/i then data[key].insert(index, value)
|
415
|
+
when /\Aafter\z/i then data[key].insert(index + 1, value)
|
291
416
|
else raise_syntax_error
|
292
417
|
end
|
293
418
|
end
|
@@ -296,12 +421,14 @@ class Redis
|
|
296
421
|
data_type_check(key, Array)
|
297
422
|
return unless data[key]
|
298
423
|
raise Redis::CommandError, "ERR index out of range" if index >= data[key].size
|
299
|
-
data[key][index] = value
|
424
|
+
data[key][index] = value.to_s
|
300
425
|
end
|
301
426
|
|
302
427
|
def lrem(key, count, value)
|
303
428
|
data_type_check(key, Array)
|
304
|
-
return unless data[key]
|
429
|
+
return 0 unless data[key]
|
430
|
+
|
431
|
+
value = value.to_s
|
305
432
|
old_size = data[key].size
|
306
433
|
diff =
|
307
434
|
if count == 0
|
@@ -318,6 +445,7 @@ class Redis
|
|
318
445
|
end
|
319
446
|
|
320
447
|
def rpush(key, value)
|
448
|
+
raise_argument_error('rpush') if value.respond_to?(:each) && value.empty?
|
321
449
|
data_type_check(key, Array)
|
322
450
|
data[key] ||= []
|
323
451
|
[value].flatten.each do |val|
|
@@ -327,12 +455,14 @@ class Redis
|
|
327
455
|
end
|
328
456
|
|
329
457
|
def rpushx(key, value)
|
458
|
+
raise_argument_error('rpushx') if value.respond_to?(:each) && value.empty?
|
330
459
|
data_type_check(key, Array)
|
331
460
|
return unless data[key]
|
332
461
|
rpush(key, value)
|
333
462
|
end
|
334
463
|
|
335
464
|
def lpush(key, value)
|
465
|
+
raise_argument_error('lpush') if value.respond_to?(:each) && value.empty?
|
336
466
|
data_type_check(key, Array)
|
337
467
|
data[key] ||= []
|
338
468
|
[value].flatten.each do |val|
|
@@ -342,6 +472,7 @@ class Redis
|
|
342
472
|
end
|
343
473
|
|
344
474
|
def lpushx(key, value)
|
475
|
+
raise_argument_error('lpushx') if value.respond_to?(:each) && value.empty?
|
345
476
|
data_type_check(key, Array)
|
346
477
|
return unless data[key]
|
347
478
|
lpush(key, value)
|
@@ -353,6 +484,18 @@ class Redis
|
|
353
484
|
data[key].pop
|
354
485
|
end
|
355
486
|
|
487
|
+
def brpop(keys, timeout=0)
|
488
|
+
#todo threaded mode
|
489
|
+
keys = Array(keys)
|
490
|
+
keys.each do |key|
|
491
|
+
if data[key] && data[key].size > 0
|
492
|
+
return [key, data[key].pop]
|
493
|
+
end
|
494
|
+
end
|
495
|
+
sleep(timeout.to_f)
|
496
|
+
nil
|
497
|
+
end
|
498
|
+
|
356
499
|
def rpoplpush(key1, key2)
|
357
500
|
data_type_check(key1, Array)
|
358
501
|
rpop(key1).tap do |elem|
|
@@ -360,12 +503,31 @@ class Redis
|
|
360
503
|
end
|
361
504
|
end
|
362
505
|
|
506
|
+
def brpoplpush(key1, key2, opts={})
|
507
|
+
data_type_check(key1, Array)
|
508
|
+
_key, elem = brpop(key1)
|
509
|
+
lpush(key2, elem) unless elem.nil?
|
510
|
+
elem
|
511
|
+
end
|
512
|
+
|
363
513
|
def lpop(key)
|
364
514
|
data_type_check(key, Array)
|
365
515
|
return unless data[key]
|
366
516
|
data[key].shift
|
367
517
|
end
|
368
518
|
|
519
|
+
def blpop(keys, timeout=0)
|
520
|
+
#todo threaded mode
|
521
|
+
keys = Array(keys)
|
522
|
+
keys.each do |key|
|
523
|
+
if data[key] && data[key].size > 0
|
524
|
+
return [key, data[key].shift]
|
525
|
+
end
|
526
|
+
end
|
527
|
+
sleep(timeout.to_f)
|
528
|
+
nil
|
529
|
+
end
|
530
|
+
|
369
531
|
def smembers(key)
|
370
532
|
data_type_check(key, ::Set)
|
371
533
|
return [] unless data[key]
|
@@ -399,12 +561,14 @@ class Redis
|
|
399
561
|
|
400
562
|
def srem(key, value)
|
401
563
|
data_type_check(key, ::Set)
|
564
|
+
value = Array(value)
|
565
|
+
raise_argument_error('srem') if value.empty?
|
402
566
|
return false unless data[key]
|
403
567
|
|
404
568
|
if value.is_a?(Array)
|
405
569
|
old_size = data[key].size
|
406
570
|
values = value.map(&:to_s)
|
407
|
-
values.each { |
|
571
|
+
values.each { |v| data[key].delete(v) }
|
408
572
|
deleted = old_size - data[key].size
|
409
573
|
else
|
410
574
|
deleted = !!data[key].delete?(value.to_s)
|
@@ -421,11 +585,14 @@ class Redis
|
|
421
585
|
result
|
422
586
|
end
|
423
587
|
|
424
|
-
def spop(key)
|
588
|
+
def spop(key, count = nil)
|
425
589
|
data_type_check(key, ::Set)
|
426
|
-
|
427
|
-
|
428
|
-
|
590
|
+
results = (count || 1).times.map do
|
591
|
+
elem = srandmember(key)
|
592
|
+
srem(key, elem) if elem
|
593
|
+
elem
|
594
|
+
end.compact
|
595
|
+
count.nil? ? results.first : results
|
429
596
|
end
|
430
597
|
|
431
598
|
def scard(key)
|
@@ -435,6 +602,7 @@ class Redis
|
|
435
602
|
end
|
436
603
|
|
437
604
|
def sinter(*keys)
|
605
|
+
keys = keys[0] if flatten?(keys)
|
438
606
|
raise_argument_error('sinter') if keys.empty?
|
439
607
|
|
440
608
|
keys.each { |k| data_type_check(k, ::Set) }
|
@@ -452,6 +620,9 @@ class Redis
|
|
452
620
|
end
|
453
621
|
|
454
622
|
def sunion(*keys)
|
623
|
+
keys = keys[0] if flatten?(keys)
|
624
|
+
raise_argument_error('sunion') if keys.empty?
|
625
|
+
|
455
626
|
keys.each { |k| data_type_check(k, ::Set) }
|
456
627
|
keys = keys.map { |k| data[k] || ::Set.new }
|
457
628
|
keys.inject(::Set.new) do |set, key|
|
@@ -466,6 +637,7 @@ class Redis
|
|
466
637
|
end
|
467
638
|
|
468
639
|
def sdiff(key1, *keys)
|
640
|
+
keys = keys[0] if flatten?(keys)
|
469
641
|
[key1, *keys].each { |k| data_type_check(k, ::Set) }
|
470
642
|
keys = keys.map { |k| data[k] || ::Set.new }
|
471
643
|
keys.inject(data[key1] || Set.new) do |memo, set|
|
@@ -483,23 +655,58 @@ class Redis
|
|
483
655
|
number.nil? ? srandmember_single(key) : srandmember_multiple(key, number)
|
484
656
|
end
|
485
657
|
|
486
|
-
def
|
487
|
-
|
488
|
-
|
658
|
+
def sscan(key, start_cursor, *args)
|
659
|
+
data_type_check(key, ::Set)
|
660
|
+
return ["0", []] unless data[key]
|
489
661
|
|
490
|
-
|
491
|
-
|
492
|
-
|
662
|
+
match = "*"
|
663
|
+
count = 10
|
664
|
+
|
665
|
+
if args.size.odd?
|
666
|
+
raise_argument_error('sscan')
|
493
667
|
end
|
494
|
-
|
668
|
+
|
669
|
+
if idx = args.index("MATCH")
|
670
|
+
match = args[idx + 1]
|
671
|
+
end
|
672
|
+
|
673
|
+
if idx = args.index("COUNT")
|
674
|
+
count = args[idx + 1]
|
675
|
+
end
|
676
|
+
|
677
|
+
start_cursor = start_cursor.to_i
|
678
|
+
|
679
|
+
cursor = start_cursor
|
680
|
+
next_keys = []
|
681
|
+
|
682
|
+
if start_cursor + count >= data[key].length
|
683
|
+
next_keys = (data[key].to_a)[start_cursor..-1]
|
684
|
+
cursor = 0
|
685
|
+
else
|
686
|
+
cursor = start_cursor + count
|
687
|
+
next_keys = (data[key].to_a)[start_cursor..cursor-1]
|
688
|
+
end
|
689
|
+
|
690
|
+
filtered_next_keys = next_keys.select{ |k,v| File.fnmatch(match, k)}
|
691
|
+
result = filtered_next_keys.flatten.map(&:to_s)
|
692
|
+
|
693
|
+
return ["#{cursor}", result]
|
694
|
+
end
|
695
|
+
|
696
|
+
def del(*keys)
|
697
|
+
delete_keys(keys, 'del')
|
698
|
+
end
|
699
|
+
|
700
|
+
def unlink(*keys)
|
701
|
+
delete_keys(keys, 'unlink')
|
495
702
|
end
|
496
703
|
|
497
704
|
def setnx(key, value)
|
498
705
|
if exists(key)
|
499
|
-
|
706
|
+
0
|
500
707
|
else
|
501
708
|
set(key, value)
|
502
|
-
|
709
|
+
1
|
503
710
|
end
|
504
711
|
end
|
505
712
|
|
@@ -525,6 +732,12 @@ class Redis
|
|
525
732
|
1
|
526
733
|
end
|
527
734
|
|
735
|
+
def pexpire(key, ttl)
|
736
|
+
return 0 unless data[key]
|
737
|
+
data.expires[key] = Time.now + (ttl / 1000.0)
|
738
|
+
1
|
739
|
+
end
|
740
|
+
|
528
741
|
def ttl(key)
|
529
742
|
if data.expires.include?(key) && (ttl = data.expires[key].to_i - Time.now.to_i) > 0
|
530
743
|
ttl
|
@@ -533,6 +746,14 @@ class Redis
|
|
533
746
|
end
|
534
747
|
end
|
535
748
|
|
749
|
+
def pttl(key)
|
750
|
+
if data.expires.include?(key) && (ttl = data.expires[key].to_f - Time.now.to_f) > 0
|
751
|
+
ttl * 1000
|
752
|
+
else
|
753
|
+
exists(key) ? -1 : -2
|
754
|
+
end
|
755
|
+
end
|
756
|
+
|
536
757
|
def expireat(key, timestamp)
|
537
758
|
data.expires[key] = Time.at(timestamp)
|
538
759
|
true
|
@@ -548,10 +769,10 @@ class Redis
|
|
548
769
|
if data[key]
|
549
770
|
result = !data[key].include?(field)
|
550
771
|
data[key][field] = value.to_s
|
551
|
-
result
|
772
|
+
result ? 1 : 0
|
552
773
|
else
|
553
774
|
data[key] = { field => value.to_s }
|
554
|
-
|
775
|
+
1
|
555
776
|
end
|
556
777
|
end
|
557
778
|
|
@@ -584,10 +805,11 @@ class Redis
|
|
584
805
|
data[key][field[0].to_s] = field[1].to_s
|
585
806
|
end
|
586
807
|
end
|
808
|
+
"OK"
|
587
809
|
end
|
588
810
|
|
589
811
|
def hmget(key, *fields)
|
590
|
-
raise_argument_error('hmget') if fields.empty?
|
812
|
+
raise_argument_error('hmget') if fields.empty? || fields.flatten.empty?
|
591
813
|
|
592
814
|
data_type_check(key, Hash)
|
593
815
|
fields.flatten.map do |field|
|
@@ -606,6 +828,12 @@ class Redis
|
|
606
828
|
data[key].size
|
607
829
|
end
|
608
830
|
|
831
|
+
def hstrlen(key, field)
|
832
|
+
data_type_check(key, Hash)
|
833
|
+
return 0 if data[key].nil? || data[key][field].nil?
|
834
|
+
data[key][field].size
|
835
|
+
end
|
836
|
+
|
609
837
|
def hvals(key)
|
610
838
|
data_type_check(key, Hash)
|
611
839
|
return [] unless data[key]
|
@@ -642,22 +870,14 @@ class Redis
|
|
642
870
|
|
643
871
|
def sync ; end
|
644
872
|
|
645
|
-
def [](key)
|
646
|
-
get(key)
|
647
|
-
end
|
648
|
-
|
649
|
-
def []=(key, value)
|
650
|
-
set(key, value)
|
651
|
-
end
|
652
|
-
|
653
873
|
def set(key, value, *array_options)
|
654
874
|
option_nx = array_options.delete("NX")
|
655
875
|
option_xx = array_options.delete("XX")
|
656
876
|
|
657
|
-
return
|
877
|
+
return nil if option_nx && option_xx
|
658
878
|
|
659
|
-
return
|
660
|
-
return
|
879
|
+
return nil if option_nx && exists(key)
|
880
|
+
return nil if option_xx && !exists(key)
|
661
881
|
|
662
882
|
data[key] = value.to_s
|
663
883
|
|
@@ -688,6 +908,10 @@ class Redis
|
|
688
908
|
"OK"
|
689
909
|
end
|
690
910
|
|
911
|
+
def psetex(key, milliseconds, value)
|
912
|
+
setex(key, milliseconds / 1000.0, value)
|
913
|
+
end
|
914
|
+
|
691
915
|
def setrange(key, offset, value)
|
692
916
|
return unless data[key]
|
693
917
|
s = data[key][offset,value.size]
|
@@ -727,6 +951,11 @@ class Redis
|
|
727
951
|
data[key].to_i
|
728
952
|
end
|
729
953
|
|
954
|
+
def incrbyfloat(key, by)
|
955
|
+
data.merge!({ key => (data[key].to_f + by.to_f).to_s || by })
|
956
|
+
data[key]
|
957
|
+
end
|
958
|
+
|
730
959
|
def decr(key)
|
731
960
|
data.merge!({ key => (data[key].to_i - 1).to_s || "-1"})
|
732
961
|
data[key].to_i
|
@@ -758,10 +987,6 @@ class Redis
|
|
758
987
|
match = "*"
|
759
988
|
count = 10
|
760
989
|
|
761
|
-
if args.size.odd?
|
762
|
-
raise_argument_error('scan')
|
763
|
-
end
|
764
|
-
|
765
990
|
if idx = args.index("MATCH")
|
766
991
|
match = args[idx + 1]
|
767
992
|
end
|
@@ -771,23 +996,41 @@ class Redis
|
|
771
996
|
end
|
772
997
|
|
773
998
|
start_cursor = start_cursor.to_i
|
774
|
-
data_type_check(start_cursor,
|
999
|
+
data_type_check(start_cursor, Integer)
|
775
1000
|
|
776
1001
|
cursor = start_cursor
|
777
|
-
|
1002
|
+
returned_keys = []
|
1003
|
+
final_page = start_cursor + count >= keys(match).length
|
1004
|
+
|
1005
|
+
if final_page
|
1006
|
+
previous_keys_been_deleted = (count >= keys(match).length)
|
1007
|
+
start_index = previous_keys_been_deleted ? 0 : cursor
|
778
1008
|
|
779
|
-
|
780
|
-
next_keys = keys(match)[start_cursor..-1]
|
1009
|
+
returned_keys = keys(match)[start_index..-1]
|
781
1010
|
cursor = 0
|
782
1011
|
else
|
783
|
-
|
784
|
-
|
1012
|
+
end_index = start_cursor + (count - 1)
|
1013
|
+
returned_keys = keys(match)[start_cursor..end_index]
|
1014
|
+
cursor = start_cursor + count
|
785
1015
|
end
|
786
1016
|
|
787
|
-
return "#{cursor}",
|
1017
|
+
return "#{cursor}", returned_keys
|
788
1018
|
end
|
789
1019
|
|
790
1020
|
def zadd(key, *args)
|
1021
|
+
option_xx = args.delete("XX")
|
1022
|
+
option_nx = args.delete("NX")
|
1023
|
+
option_ch = args.delete("CH")
|
1024
|
+
option_incr = args.delete("INCR")
|
1025
|
+
|
1026
|
+
if option_xx && option_nx
|
1027
|
+
raise_options_error("XX", "NX")
|
1028
|
+
end
|
1029
|
+
|
1030
|
+
if option_incr && args.size > 2
|
1031
|
+
raise_options_error("INCR")
|
1032
|
+
end
|
1033
|
+
|
791
1034
|
if !args.first.is_a?(Array)
|
792
1035
|
if args.size < 2
|
793
1036
|
raise_argument_error('zadd')
|
@@ -803,18 +1046,39 @@ class Redis
|
|
803
1046
|
data_type_check(key, ZSet)
|
804
1047
|
data[key] ||= ZSet.new
|
805
1048
|
|
806
|
-
|
807
|
-
|
808
|
-
|
1049
|
+
# Turn [1, 2, 3, 4] into [[1, 2], [3, 4]] unless it is already
|
1050
|
+
args = args.each_slice(2).to_a unless args.first.is_a?(Array)
|
1051
|
+
|
1052
|
+
changed = 0
|
1053
|
+
exists = args.map(&:last).count { |el| !hexists(key, el.to_s) }
|
1054
|
+
|
1055
|
+
args.each do |score, value|
|
1056
|
+
if option_nx && hexists(key, value.to_s)
|
1057
|
+
next
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
if option_xx && !hexists(key, value.to_s)
|
1061
|
+
exists -= 1
|
1062
|
+
next
|
1063
|
+
end
|
1064
|
+
|
1065
|
+
if option_incr
|
1066
|
+
data[key][value.to_s] ||= 0
|
1067
|
+
return data[key].increment(value, score).to_s
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
if option_ch && data[key][value.to_s] != score
|
1071
|
+
changed += 1
|
1072
|
+
end
|
809
1073
|
data[key][value.to_s] = score
|
810
|
-
else
|
811
|
-
# Turn [1, 2, 3, 4] into [[1, 2], [3, 4]] unless it is already
|
812
|
-
args = args.each_slice(2).to_a unless args.first.is_a?(Array)
|
813
|
-
exists = args.map(&:last).map { |el| data[key].key?(el.to_s) }.count(false)
|
814
|
-
args.each { |s, v| data[key][v.to_s] = s }
|
815
1074
|
end
|
816
1075
|
|
817
|
-
|
1076
|
+
if option_incr
|
1077
|
+
changed = changed.zero? ? nil : changed
|
1078
|
+
exists = exists.zero? ? nil : exists
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
option_ch ? changed : exists
|
818
1082
|
end
|
819
1083
|
|
820
1084
|
def zrem(key, value)
|
@@ -830,6 +1094,36 @@ class Redis
|
|
830
1094
|
response
|
831
1095
|
end
|
832
1096
|
|
1097
|
+
def zpopmax(key, count = nil)
|
1098
|
+
data_type_check(key, ZSet)
|
1099
|
+
return [] unless data[key]
|
1100
|
+
sorted_members = sort_keys(data[key])
|
1101
|
+
results = sorted_members.last(count || 1).reverse!
|
1102
|
+
results.each do |member|
|
1103
|
+
zrem(key, member.first)
|
1104
|
+
end
|
1105
|
+
count.nil? ? results.first : results.flatten
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
def zpopmin(key, count = nil)
|
1109
|
+
data_type_check(key, ZSet)
|
1110
|
+
return [] unless data[key]
|
1111
|
+
sorted_members = sort_keys(data[key])
|
1112
|
+
results = sorted_members.first(count || 1)
|
1113
|
+
results.each do |member|
|
1114
|
+
zrem(key, member.first)
|
1115
|
+
end
|
1116
|
+
count.nil? ? results.first : results.flatten
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
def bzpopmax(*args)
|
1120
|
+
bzpop(:bzpopmax, args)
|
1121
|
+
end
|
1122
|
+
|
1123
|
+
def bzpopmin(*args)
|
1124
|
+
bzpop(:bzpopmin, args)
|
1125
|
+
end
|
1126
|
+
|
833
1127
|
def zcard(key)
|
834
1128
|
data_type_check(key, ZSet)
|
835
1129
|
data[key] ? data[key].size : 0
|
@@ -838,7 +1132,13 @@ class Redis
|
|
838
1132
|
def zscore(key, value)
|
839
1133
|
data_type_check(key, ZSet)
|
840
1134
|
value = data[key] && data[key][value.to_s]
|
841
|
-
value
|
1135
|
+
if value == Float::INFINITY
|
1136
|
+
"inf"
|
1137
|
+
elsif value == -Float::INFINITY
|
1138
|
+
"-inf"
|
1139
|
+
elsif value
|
1140
|
+
value.to_s
|
1141
|
+
end
|
842
1142
|
end
|
843
1143
|
|
844
1144
|
def zcount(key, min, max)
|
@@ -852,7 +1152,14 @@ class Redis
|
|
852
1152
|
data[key] ||= ZSet.new
|
853
1153
|
data[key][value.to_s] ||= 0
|
854
1154
|
data[key].increment(value.to_s, num)
|
855
|
-
|
1155
|
+
|
1156
|
+
if num =~ /^\+?inf/
|
1157
|
+
"inf"
|
1158
|
+
elsif num == "-inf"
|
1159
|
+
"-inf"
|
1160
|
+
else
|
1161
|
+
data[key][value.to_s].to_s
|
1162
|
+
end
|
856
1163
|
end
|
857
1164
|
|
858
1165
|
def zrank(key, value)
|
@@ -873,17 +1180,43 @@ class Redis
|
|
873
1180
|
data_type_check(key, ZSet)
|
874
1181
|
return [] unless data[key]
|
875
1182
|
|
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
|
1183
|
+
results = sort_keys(data[key])
|
884
1184
|
# Select just the keys unless we want scores
|
885
1185
|
results = results.map(&:first) unless with_scores
|
886
|
-
|
1186
|
+
start = [start, -results.size].max
|
1187
|
+
(results[start..stop] || []).flatten.map(&:to_s)
|
1188
|
+
end
|
1189
|
+
|
1190
|
+
def zrangebylex(key, start, stop, *opts)
|
1191
|
+
data_type_check(key, ZSet)
|
1192
|
+
return [] unless data[key]
|
1193
|
+
zset = data[key]
|
1194
|
+
|
1195
|
+
sorted = if zset.identical_scores?
|
1196
|
+
zset.keys.sort { |x, y| x.to_s <=> y.to_s }
|
1197
|
+
else
|
1198
|
+
zset.keys
|
1199
|
+
end
|
1200
|
+
|
1201
|
+
range = get_range start, stop, sorted.first, sorted.last
|
1202
|
+
|
1203
|
+
filtered = []
|
1204
|
+
sorted.each do |element|
|
1205
|
+
filtered << element if (range[0][:value]..range[1][:value]).cover?(element)
|
1206
|
+
end
|
1207
|
+
filtered.shift if filtered[0] == range[0][:value] && !range[0][:inclusive]
|
1208
|
+
filtered.pop if filtered.last == range[1][:value] && !range[1][:inclusive]
|
1209
|
+
|
1210
|
+
limit = get_limit(opts, filtered)
|
1211
|
+
if limit
|
1212
|
+
filtered = filtered[limit[0]..-1].take(limit[1])
|
1213
|
+
end
|
1214
|
+
|
1215
|
+
filtered
|
1216
|
+
end
|
1217
|
+
|
1218
|
+
def zrevrangebylex(key, start, stop, *args)
|
1219
|
+
zrangebylex(key, stop, start, args).reverse
|
887
1220
|
end
|
888
1221
|
|
889
1222
|
def zrevrange(key, start, stop, with_scores = nil)
|
@@ -966,6 +1299,158 @@ class Redis
|
|
966
1299
|
data[out].size
|
967
1300
|
end
|
968
1301
|
|
1302
|
+
def pfadd(key, member)
|
1303
|
+
data_type_check(key, Set)
|
1304
|
+
data[key] ||= Set.new
|
1305
|
+
previous_size = data[key].size
|
1306
|
+
data[key] |= Array(member)
|
1307
|
+
data[key].size != previous_size
|
1308
|
+
end
|
1309
|
+
|
1310
|
+
def pfcount(*keys)
|
1311
|
+
keys = keys.flatten
|
1312
|
+
raise_argument_error("pfcount") if keys.empty?
|
1313
|
+
keys.each { |key| data_type_check(key, Set) }
|
1314
|
+
if keys.count == 1
|
1315
|
+
(data[keys.first] || Set.new).size
|
1316
|
+
else
|
1317
|
+
union = keys.map { |key| data[key] }.compact.reduce(&:|)
|
1318
|
+
union.size
|
1319
|
+
end
|
1320
|
+
end
|
1321
|
+
|
1322
|
+
def pfmerge(destination, *sources)
|
1323
|
+
sources.each { |source| data_type_check(source, Set) }
|
1324
|
+
union = sources.map { |source| data[source] || Set.new }.reduce(&:|)
|
1325
|
+
data[destination] = union
|
1326
|
+
"OK"
|
1327
|
+
end
|
1328
|
+
|
1329
|
+
def subscribe(*channels)
|
1330
|
+
raise_argument_error('subscribe') if channels.empty?()
|
1331
|
+
|
1332
|
+
#Create messages for all data from the channels
|
1333
|
+
channel_replies = channels.map do |channel|
|
1334
|
+
self.class.channels[channel].slice!(0..-1).map!{|v| ["message", channel, v]}
|
1335
|
+
end
|
1336
|
+
channel_replies.flatten!(1)
|
1337
|
+
channel_replies.compact!()
|
1338
|
+
|
1339
|
+
#Put messages into the replies for the future
|
1340
|
+
channels.each_with_index do |channel,index|
|
1341
|
+
replies << ["subscribe", channel, index+1]
|
1342
|
+
end
|
1343
|
+
replies.push(*channel_replies)
|
1344
|
+
|
1345
|
+
#Add unsubscribe message to stop blocking (see https://github.com/redis/redis-rb/blob/v3.2.1/lib/redis/subscribe.rb#L38)
|
1346
|
+
replies.push(self.unsubscribe())
|
1347
|
+
|
1348
|
+
replies.pop() #Last reply will be pushed back on
|
1349
|
+
end
|
1350
|
+
|
1351
|
+
def psubscribe(*patterns)
|
1352
|
+
raise_argument_error('psubscribe') if patterns.empty?()
|
1353
|
+
|
1354
|
+
#Create messages for all data from the channels
|
1355
|
+
channel_replies = self.class.channels.keys.map do |channel|
|
1356
|
+
pattern = patterns.find{|p| File.fnmatch(p, channel) }
|
1357
|
+
unless pattern.nil?()
|
1358
|
+
self.class.channels[channel].slice!(0..-1).map!{|v| ["pmessage", pattern, channel, v]}
|
1359
|
+
end
|
1360
|
+
end
|
1361
|
+
channel_replies.flatten!(1)
|
1362
|
+
channel_replies.compact!()
|
1363
|
+
|
1364
|
+
#Put messages into the replies for the future
|
1365
|
+
patterns.each_with_index do |pattern,index|
|
1366
|
+
replies << ["psubscribe", pattern, index+1]
|
1367
|
+
end
|
1368
|
+
replies.push(*channel_replies)
|
1369
|
+
|
1370
|
+
#Add unsubscribe to stop blocking
|
1371
|
+
replies.push(self.punsubscribe())
|
1372
|
+
|
1373
|
+
replies.pop() #Last reply will be pushed back on
|
1374
|
+
end
|
1375
|
+
|
1376
|
+
def publish(channel, message)
|
1377
|
+
self.class.channels[channel] << message
|
1378
|
+
0 #Just fake number of subscribers
|
1379
|
+
end
|
1380
|
+
|
1381
|
+
def unsubscribe(*channels)
|
1382
|
+
if channels.empty?()
|
1383
|
+
replies << ["unsubscribe", nil, 0]
|
1384
|
+
else
|
1385
|
+
channels.each do |channel|
|
1386
|
+
replies << ["unsubscribe", channel, 0]
|
1387
|
+
end
|
1388
|
+
end
|
1389
|
+
replies.pop() #Last reply will be pushed back on
|
1390
|
+
end
|
1391
|
+
|
1392
|
+
def punsubscribe(*patterns)
|
1393
|
+
if patterns.empty?()
|
1394
|
+
replies << ["punsubscribe", nil, 0]
|
1395
|
+
else
|
1396
|
+
patterns.each do |pattern|
|
1397
|
+
replies << ["punsubscribe", pattern, 0]
|
1398
|
+
end
|
1399
|
+
end
|
1400
|
+
replies.pop() #Last reply will be pushed back on
|
1401
|
+
end
|
1402
|
+
|
1403
|
+
def zscan(key, start_cursor, *args)
|
1404
|
+
data_type_check(key, ZSet)
|
1405
|
+
return [] unless data[key]
|
1406
|
+
|
1407
|
+
match = "*"
|
1408
|
+
count = 10
|
1409
|
+
|
1410
|
+
if args.size.odd?
|
1411
|
+
raise_argument_error('zscan')
|
1412
|
+
end
|
1413
|
+
|
1414
|
+
if idx = args.index("MATCH")
|
1415
|
+
match = args[idx + 1]
|
1416
|
+
end
|
1417
|
+
|
1418
|
+
if idx = args.index("COUNT")
|
1419
|
+
count = args[idx + 1]
|
1420
|
+
end
|
1421
|
+
|
1422
|
+
start_cursor = start_cursor.to_i
|
1423
|
+
data_type_check(start_cursor, Integer)
|
1424
|
+
|
1425
|
+
cursor = start_cursor
|
1426
|
+
next_keys = []
|
1427
|
+
|
1428
|
+
sorted_keys = sort_keys(data[key])
|
1429
|
+
|
1430
|
+
if start_cursor + count >= sorted_keys.length
|
1431
|
+
next_keys = sorted_keys.to_a.select { |k| File.fnmatch(match, k[0]) } [start_cursor..-1]
|
1432
|
+
cursor = 0
|
1433
|
+
else
|
1434
|
+
cursor = start_cursor + count
|
1435
|
+
next_keys = sorted_keys.to_a.select { |k| File.fnmatch(match, k[0]) } [start_cursor..cursor-1]
|
1436
|
+
end
|
1437
|
+
return "#{cursor}", next_keys.flatten.map(&:to_s)
|
1438
|
+
end
|
1439
|
+
|
1440
|
+
# Originally from redis-rb
|
1441
|
+
def zscan_each(key, *args, &block)
|
1442
|
+
data_type_check(key, ZSet)
|
1443
|
+
return [] unless data[key]
|
1444
|
+
|
1445
|
+
return to_enum(:zscan_each, key, options) unless block_given?
|
1446
|
+
cursor = 0
|
1447
|
+
loop do
|
1448
|
+
cursor, values = zscan(key, cursor, options)
|
1449
|
+
values.each(&block)
|
1450
|
+
break if cursor == "0"
|
1451
|
+
end
|
1452
|
+
end
|
1453
|
+
|
969
1454
|
private
|
970
1455
|
def raise_argument_error(command, match_string=command)
|
971
1456
|
error_message = if %w(hmset mset_odd).include?(match_string.downcase)
|
@@ -981,17 +1466,61 @@ class Redis
|
|
981
1466
|
raise Redis::CommandError, "ERR syntax error"
|
982
1467
|
end
|
983
1468
|
|
1469
|
+
def raise_options_error(*options)
|
1470
|
+
if options.detect { |opt| opt.match(/incr/i) }
|
1471
|
+
error_message = "ERR INCR option supports a single increment-element pair"
|
1472
|
+
else
|
1473
|
+
error_message = "ERR #{options.join(" and ")} options at the same time are not compatible"
|
1474
|
+
end
|
1475
|
+
raise Redis::CommandError, error_message
|
1476
|
+
end
|
1477
|
+
|
1478
|
+
def raise_command_error(message)
|
1479
|
+
raise Redis::CommandError, message
|
1480
|
+
end
|
1481
|
+
|
1482
|
+
def delete_keys(keys, command)
|
1483
|
+
keys = keys.flatten(1)
|
1484
|
+
raise_argument_error(command) if keys.empty?
|
1485
|
+
|
1486
|
+
old_count = data.keys.size
|
1487
|
+
keys.each do |key|
|
1488
|
+
data.delete(key)
|
1489
|
+
end
|
1490
|
+
old_count - data.keys.size
|
1491
|
+
end
|
1492
|
+
|
984
1493
|
def remove_key_for_empty_collection(key)
|
985
1494
|
del(key) if data[key] && data[key].empty?
|
986
1495
|
end
|
987
1496
|
|
988
1497
|
def data_type_check(key, klass)
|
989
1498
|
if data[key] && !data[key].is_a?(klass)
|
990
|
-
warn "Operation against a key holding the wrong kind of value: Expected #{klass} at #{key}."
|
991
1499
|
raise Redis::CommandError.new("WRONGTYPE Operation against a key holding the wrong kind of value")
|
992
1500
|
end
|
993
1501
|
end
|
994
1502
|
|
1503
|
+
def get_range(start, stop, min = -Float::INFINITY, max = Float::INFINITY)
|
1504
|
+
range_options = []
|
1505
|
+
|
1506
|
+
[start, stop].each do |value|
|
1507
|
+
case value[0]
|
1508
|
+
when "-"
|
1509
|
+
range_options << { value: min, inclusive: true }
|
1510
|
+
when "+"
|
1511
|
+
range_options << { value: max, inclusive: true }
|
1512
|
+
when "["
|
1513
|
+
range_options << { value: value[1..-1], inclusive: true }
|
1514
|
+
when "("
|
1515
|
+
range_options << { value: value[1..-1], inclusive: false }
|
1516
|
+
else
|
1517
|
+
raise Redis::CommandError, "ERR min or max not valid string range item"
|
1518
|
+
end
|
1519
|
+
end
|
1520
|
+
|
1521
|
+
range_options
|
1522
|
+
end
|
1523
|
+
|
995
1524
|
def get_limit(opts, vals)
|
996
1525
|
index = opts.index('LIMIT')
|
997
1526
|
|
@@ -1008,6 +1537,9 @@ class Redis
|
|
1008
1537
|
def mapped_param? param
|
1009
1538
|
param.size == 1 && param[0].is_a?(Array)
|
1010
1539
|
end
|
1540
|
+
# NOTE : Redis-rb 3.x will flatten *args, so method(["a", "b", "c"])
|
1541
|
+
# should be handled the same way as method("a", "b", "c")
|
1542
|
+
alias_method :flatten?, :mapped_param?
|
1011
1543
|
|
1012
1544
|
def srandmember_single(key)
|
1013
1545
|
data_type_check(key, ::Set)
|
@@ -1027,8 +1559,44 @@ class Redis
|
|
1027
1559
|
(1..-number).map { data[key].to_a[rand(data[key].size)] }.flatten
|
1028
1560
|
end
|
1029
1561
|
end
|
1562
|
+
|
1563
|
+
def bzpop(command, args)
|
1564
|
+
timeout =
|
1565
|
+
if args.last.is_a?(Hash)
|
1566
|
+
args.pop[:timeout]
|
1567
|
+
elsif args.last.respond_to?(:to_int)
|
1568
|
+
args.pop.to_int
|
1569
|
+
end
|
1570
|
+
|
1571
|
+
timeout ||= 0
|
1572
|
+
single_pop_command = command.to_s[1..-1]
|
1573
|
+
keys = args.flatten
|
1574
|
+
keys.each do |key|
|
1575
|
+
if data[key]
|
1576
|
+
data_type_check(data[key], ZSet)
|
1577
|
+
if data[key].size > 0
|
1578
|
+
result = public_send(single_pop_command, key)
|
1579
|
+
return result.unshift(key)
|
1580
|
+
end
|
1581
|
+
end
|
1582
|
+
end
|
1583
|
+
sleep(timeout.to_f)
|
1584
|
+
nil
|
1585
|
+
end
|
1586
|
+
|
1587
|
+
def sort_keys(arr)
|
1588
|
+
# Sort by score, or if scores are equal, key alphanum
|
1589
|
+
arr.sort do |(k1, v1), (k2, v2)|
|
1590
|
+
if v1 == v2
|
1591
|
+
k1 <=> k2
|
1592
|
+
else
|
1593
|
+
v1 <=> v2
|
1594
|
+
end
|
1595
|
+
end
|
1596
|
+
end
|
1030
1597
|
end
|
1031
1598
|
end
|
1032
1599
|
end
|
1033
1600
|
|
1601
|
+
# FIXME this line should be deleted as explicit enabling is better
|
1034
1602
|
Redis::Connection.drivers << Redis::Connection::Memory
|