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.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +3 -0
  3. data/.travis.yml +14 -5
  4. data/LICENSE +1 -1
  5. data/README.md +42 -24
  6. data/fakeredis.gemspec +1 -1
  7. data/lib/fakeredis.rb +28 -0
  8. data/lib/fakeredis/bitop_command.rb +56 -0
  9. data/lib/fakeredis/command_executor.rb +6 -9
  10. data/lib/fakeredis/expiring_hash.rb +3 -5
  11. data/lib/fakeredis/geo_commands.rb +142 -0
  12. data/lib/fakeredis/geo_set.rb +84 -0
  13. data/lib/fakeredis/minitest.rb +24 -0
  14. data/lib/fakeredis/rspec.rb +1 -0
  15. data/lib/fakeredis/sort_method.rb +3 -3
  16. data/lib/fakeredis/sorted_set_store.rb +1 -1
  17. data/lib/fakeredis/transaction_commands.rb +2 -2
  18. data/lib/fakeredis/version.rb +1 -1
  19. data/lib/fakeredis/zset.rb +8 -2
  20. data/lib/redis/connection/memory.rb +650 -82
  21. data/spec/bitop_command_spec.rb +209 -0
  22. data/spec/command_executor_spec.rb +15 -0
  23. data/spec/compatibility_spec.rb +1 -1
  24. data/spec/connection_spec.rb +21 -21
  25. data/spec/fakeredis_spec.rb +73 -0
  26. data/spec/geo_set_spec.rb +164 -0
  27. data/spec/hashes_spec.rb +138 -57
  28. data/spec/hyper_log_logs_spec.rb +50 -0
  29. data/spec/keys_spec.rb +232 -90
  30. data/spec/lists_spec.rb +91 -35
  31. data/spec/memory_spec.rb +80 -7
  32. data/spec/server_spec.rb +38 -24
  33. data/spec/sets_spec.rb +112 -46
  34. data/spec/sort_method_spec.rb +6 -0
  35. data/spec/sorted_sets_spec.rb +482 -150
  36. data/spec/spec_helper.rb +9 -18
  37. data/spec/spec_helper_live_redis.rb +4 -4
  38. data/spec/strings_spec.rb +113 -79
  39. data/spec/subscription_spec.rb +107 -0
  40. data/spec/support/shared_examples/bitwise_operation.rb +59 -0
  41. data/spec/support/shared_examples/sortable.rb +20 -16
  42. data/spec/transactions_spec.rb +34 -13
  43. data/spec/upcase_method_name_spec.rb +2 -2
  44. 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
@@ -18,6 +18,7 @@ RSpec.configure do |c|
18
18
 
19
19
  c.before do
20
20
  Redis::Connection::Memory.reset_all_databases
21
+ Redis::Connection::Memory.reset_all_channels
21
22
  end
22
23
 
23
24
  end
@@ -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) : sliced.flatten(1)
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)] || sorted
103
+ sorted[skip...(skip + take)] || []
104
104
  end
105
105
 
106
106
  def lookup_from_pattern(pattern, element)
@@ -68,7 +68,7 @@ module FakeRedis
68
68
 
69
69
  class SortedSetIntersectStore < SortedSetStore
70
70
  def selected_keys
71
- @values ||= hashes.inject([]) { |r, h| r.empty? ? h.keys : (r & h.keys) }
71
+ @values ||= hashes.map(&:keys).reduce(:&)
72
72
  end
73
73
  end
74
74
 
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  module FakeRedis
2
- VERSION = "0.5.0"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -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
- reject {|_,v| v < min || v > max }
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" => "2.6.16",
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 ; end
153
+ def bgsave; end
154
+
155
+ def bgrewriteaof; end
156
+
157
+ def evalsha; end
137
158
 
138
- def bgrewriteaof ; end
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
- deleted = data[key] && data[key].delete(field)
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 ? 1 : 0
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
- (data[key] && data[key][startidx..endidx]) || []
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
- index = data[key].index(pivot)
288
- case where
289
- when :before then data[key].insert(index, value)
290
- when :after then data[key].insert(index + 1, value)
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 { |value| data[key].delete(value) }
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
- elem = srandmember(key)
427
- srem(key, elem)
428
- elem
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 del(*keys)
487
- keys = keys.flatten(1)
488
- raise_argument_error('del') if keys.empty?
658
+ def sscan(key, start_cursor, *args)
659
+ data_type_check(key, ::Set)
660
+ return ["0", []] unless data[key]
489
661
 
490
- old_count = data.keys.size
491
- keys.each do |key|
492
- data.delete(key)
662
+ match = "*"
663
+ count = 10
664
+
665
+ if args.size.odd?
666
+ raise_argument_error('sscan')
493
667
  end
494
- old_count - data.keys.size
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
- false
706
+ 0
500
707
  else
501
708
  set(key, value)
502
- true
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
- true
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 false if option_nx && option_xx
877
+ return nil if option_nx && option_xx
658
878
 
659
- return false if option_nx && exists(key)
660
- return false if option_xx && !exists(key)
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, Fixnum)
999
+ data_type_check(start_cursor, Integer)
775
1000
 
776
1001
  cursor = start_cursor
777
- next_keys = []
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
- if start_cursor + count >= data.length
780
- next_keys = keys(match)[start_cursor..-1]
1009
+ returned_keys = keys(match)[start_index..-1]
781
1010
  cursor = 0
782
1011
  else
783
- cursor = start_cursor + 10
784
- next_keys = keys(match)[start_cursor..cursor]
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}", next_keys
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
- if args.size == 2 && !(Array === args.first)
807
- score, value = args
808
- exists = !data[key].key?(value.to_s)
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
- exists
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 && value.to_s
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
- data[key][value.to_s].to_s
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
- # Sort by score, or if scores are equal, key alphanum
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
- results[start..stop].flatten.map(&:to_s)
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