fakeredis 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 1be2134927d3b7838ee3e2fd4376791bea516561
4
- data.tar.gz: 2b3e0c64b79df1358151bbd1405000395fa06e86
2
+ SHA256:
3
+ metadata.gz: 7c0b77a04c1c761edb3a6544aca213f373d57bb485d17f9e7e0f2c63bfd7e660
4
+ data.tar.gz: 57975993a6664bc469ee297a6a36d75ac2e4e429972aee6e50a2f518f93ca580
5
5
  SHA512:
6
- metadata.gz: cfb37be84fd30d941fadc44a177e2029cd2de063fc090bc3004166a72f5dba6e7378e9c918aa1d156d44b8277f830dfedbd34aad57142770512f420f4f12fae3
7
- data.tar.gz: 2f841badb4c16e9ff6d3c35b697b9ee262022bc8286553e7187a47173af742114eacd5cccdb3d0477c111457fc9a7f50fdb7de970a25c04ba1be33318c84a1ac
6
+ metadata.gz: ba35de9b7e297eca69cb7652d7817f257c447d7533d8c259c47215b15355ec49e5c2584907f0726f97327c443de122bd240cbca6fa194222f114bc99ffd0dbb9
7
+ data.tar.gz: 8c4e340be842d1fe966a3a4c6f63987bc8af346da74ced914cdc77488e08a6d15101ee6625097337287ec55c19f41118da4a79892b7fdd00bbebff1d103a813a
data/.gitignore CHANGED
@@ -1,7 +1,6 @@
1
1
  *.gem
2
2
  .bundle
3
3
  Gemfile.lock
4
- gemfiles/*.gemfile.lock
5
4
  pkg/*
6
5
  .rvmrc
7
6
  *.rbc
@@ -5,18 +5,13 @@ cache: bundler
5
5
  sudo: false
6
6
 
7
7
  rvm:
8
- - 2.2
9
- - 2.3.6
10
8
  - 2.4
11
9
  - 2.5
10
+ - 2.6
12
11
  - ruby-head
13
12
  - jruby
14
13
  - rbx-2
15
14
 
16
- gemfile:
17
- - Gemfile
18
- - gemfiles/redisrb-master.gemfile
19
-
20
15
  matrix:
21
16
  allow_failures:
22
17
  - rvm: rbx-2
@@ -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.2", "< 5.0"])
21
+ s.add_runtime_dependency(%q<redis>, ["~> 4.1"])
22
22
  s.add_development_dependency(%q<rspec>, ["~> 3.0"])
23
23
  end
@@ -15,4 +15,20 @@ module FakeRedis
15
15
  def self.disable
16
16
  Redis::Connection.drivers.delete_if {|driver| Redis::Connection::Memory == driver }
17
17
  end
18
+
19
+ def self.disabling
20
+ return yield unless enabled?
21
+
22
+ disable
23
+ yield
24
+ enable
25
+ end
26
+
27
+ def self.enabling
28
+ return yield if enabled?
29
+
30
+ enable
31
+ yield
32
+ disable
33
+ end
18
34
  end
@@ -15,12 +15,6 @@ module FakeRedis
15
15
  raise Redis::CommandError, "ERR unknown command '#{meffod}'"
16
16
  end
17
17
 
18
- if reply == true
19
- reply = 1
20
- elsif reply == false
21
- reply = 0
22
- end
23
-
24
18
  replies << reply
25
19
  nil
26
20
  end
@@ -44,10 +44,8 @@ module FakeRedis
44
44
  end
45
45
 
46
46
  def values_at(*keys)
47
- keys.each do |key|
48
- key = normalize(key)
49
- delete(key) if expired?(key)
50
- end
47
+ keys = keys.map { |key| normalize(key) }
48
+ keys.each { |key| delete(key) if expired?(key) }
51
49
  super
52
50
  end
53
51
 
@@ -0,0 +1,142 @@
1
+ require "fakeredis/geo_set"
2
+
3
+ module FakeRedis
4
+ module GeoCommands
5
+ DISTANCE_UNITS = {
6
+ "m" => 1,
7
+ "km" => 1000,
8
+ "ft" => 0.3048,
9
+ "mi" => 1609.34
10
+ }
11
+
12
+ REDIS_DOUBLE_PRECISION = 4
13
+ REDIS_GEOHASH_SIZE = 10
14
+
15
+ def geoadd(key, *members)
16
+ raise_argument_error("geoadd") if members.empty? || members.size % 3 != 0
17
+
18
+ set = (data[key] ||= GeoSet.new)
19
+ prev_size = set.size
20
+ members.each_slice(3) do |member|
21
+ set.add(*member)
22
+ end
23
+ set.size - prev_size
24
+ end
25
+
26
+ def geodist(key, member1, member2, unit = "m")
27
+ unit = unit.to_s
28
+ raise_command_error("ERR unsupported unit provided. please use #{DISTANCE_UNITS.keys.join(', ')}") unless DISTANCE_UNITS.include?(unit)
29
+
30
+ set = (data[key] || GeoSet.new)
31
+ point1 = set.get(member1)
32
+ point2 = set.get(member2)
33
+ if point1 && point2
34
+ distance = point1.distance_to(point2)
35
+ distance_in_units = distance / DISTANCE_UNITS[unit]
36
+ distance_in_units.round(REDIS_DOUBLE_PRECISION).to_s
37
+ end
38
+ end
39
+
40
+ def geohash(key, member)
41
+ members = Array(member)
42
+ raise_argument_error("geohash") if members.empty?
43
+ set = (data[key] || GeoSet.new)
44
+ members.map do |member|
45
+ point = set.get(member)
46
+ point.geohash(REDIS_GEOHASH_SIZE) if point
47
+ end
48
+ end
49
+
50
+ def geopos(key, member)
51
+ return nil unless data[key]
52
+
53
+ members = Array(member)
54
+ set = (data[key] || GeoSet.new)
55
+ members.map do |member|
56
+ point = set.get(member)
57
+ [point.lon.to_s, point.lat.to_s] if point
58
+ end
59
+ end
60
+
61
+ def georadius(*args)
62
+ args = args.dup
63
+ raise_argument_error("georadius") if args.size < 5
64
+ key, lon, lat, radius, unit, *rest = args
65
+ raise_argument_error("georadius") unless DISTANCE_UNITS.has_key?(unit)
66
+ radius *= DISTANCE_UNITS[unit]
67
+
68
+ set = (data[key] || GeoSet.new)
69
+ center = GeoSet::Point.new(lon, lat, nil)
70
+
71
+ do_georadius(set, center, radius, unit, rest)
72
+ end
73
+
74
+ def georadiusbymember(*args)
75
+ args = args.dup
76
+ raise_argument_error("georadiusbymember") if args.size < 4
77
+ key, member, radius, unit, *rest = args
78
+ raise_argument_error("georadiusbymember") unless DISTANCE_UNITS.has_key?(unit)
79
+ radius *= DISTANCE_UNITS[unit]
80
+
81
+ set = (data[key] || GeoSet.new)
82
+ center = set.get(member)
83
+ raise_command_error("ERR could not decode requested zset member") unless center
84
+
85
+ do_georadius(set, center, radius, unit, args)
86
+ end
87
+
88
+ private
89
+
90
+ def do_georadius(set, center, radius, unit, args)
91
+ points = set.points_within_radius(center, radius)
92
+
93
+ options = georadius_options(args)
94
+
95
+ if options[:asc]
96
+ points.sort_by! { |p| p.distance_to(center) }
97
+ elsif options[:desc]
98
+ points.sort_by! { |p| -p.distance_to(center) }
99
+ end
100
+
101
+ points = points.take(options[:count]) if options[:count]
102
+ extras = options[:extras]
103
+ return points.map(&:name) if extras.empty?
104
+
105
+ points.map do |point|
106
+ member = [point.name]
107
+
108
+ extras.each do |extra|
109
+ case extra
110
+ when "WITHCOORD"
111
+ member << [point.lon.to_s, point.lat.to_s]
112
+ when "WITHDIST"
113
+ distance = point.distance_to(center)
114
+ distance_in_units = distance / DISTANCE_UNITS[unit]
115
+ member << distance_in_units.round(REDIS_DOUBLE_PRECISION).to_s
116
+ when "WITHHASH"
117
+ member << point.geohash(REDIS_GEOHASH_SIZE)
118
+ end
119
+ end
120
+
121
+ member
122
+ end
123
+ end
124
+
125
+ def georadius_options(args)
126
+ options = {}
127
+ args = args.map { |arg| arg.to_s.upcase }
128
+
129
+ if idx = args.index("COUNT")
130
+ options[:count] = Integer(args[idx + 1])
131
+ end
132
+
133
+ options[:asc] = true if args.include?("ASC")
134
+ options[:desc] = true if args.include?("DESC")
135
+
136
+ extras = args & ["WITHCOORD", "WITHDIST", "WITHHASH"]
137
+ options[:extras] = extras
138
+
139
+ options
140
+ end
141
+ end
142
+ end
@@ -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
@@ -15,8 +15,8 @@ require 'fakeredis'
15
15
  module FakeRedis
16
16
  module Minitest
17
17
  def setup
18
- super
19
18
  Redis::Connection::Memory.reset_all_databases
19
+ super
20
20
  end
21
21
 
22
22
  ::Minitest::Test.send(:include, self)
@@ -6,7 +6,6 @@ module FakeRedis
6
6
  return [] if type(key) == 'none'
7
7
 
8
8
  unless %w(list set zset).include? type(key)
9
- warn "Operation against a key holding the wrong kind of value: Expected list, set or zset at #{key}."
10
9
  raise Redis::CommandError.new("WRONGTYPE Operation against a key holding the wrong kind of value")
11
10
  end
12
11
 
@@ -1,3 +1,3 @@
1
1
  module FakeRedis
2
- VERSION = "0.7.0"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -17,7 +17,7 @@ module FakeRedis
17
17
  def select_by_score min, max
18
18
  min = _floatify(min, true)
19
19
  max = _floatify(max, false)
20
- reject {|_,v| v < min || v > max }
20
+ select {|_,v| v >= min && v <= max }
21
21
  end
22
22
 
23
23
  private
@@ -9,16 +9,20 @@ require "fakeredis/sorted_set_store"
9
9
  require "fakeredis/transaction_commands"
10
10
  require "fakeredis/zset"
11
11
  require "fakeredis/bitop_command"
12
+ require "fakeredis/geo_commands"
12
13
  require "fakeredis/version"
13
14
 
14
15
  class Redis
15
16
  module Connection
17
+ DEFAULT_REDIS_VERSION = '3.3.5'
18
+
16
19
  class Memory
17
20
  include Redis::Connection::CommandHelper
18
21
  include FakeRedis
19
22
  include SortMethod
20
23
  include TransactionCommands
21
24
  include BitopCommand
25
+ include GeoCommands
22
26
  include CommandExecutor
23
27
 
24
28
  attr_accessor :options
@@ -51,7 +55,7 @@ class Redis
51
55
  end
52
56
 
53
57
  def initialize(options = {})
54
- self.options = options
58
+ self.options = self.options ? self.options.merge(options) : options
55
59
  end
56
60
 
57
61
  def database_id
@@ -93,9 +97,8 @@ class Redis
93
97
 
94
98
  def client(command, _options = {})
95
99
  case command
96
- when :setname then true
100
+ when :setname then "OK"
97
101
  when :getname then nil
98
- when :client then true
99
102
  else
100
103
  raise Redis::CommandError, "ERR unknown command '#{command}'"
101
104
  end
@@ -130,7 +133,7 @@ class Redis
130
133
 
131
134
  def info
132
135
  {
133
- "redis_version" => "2.6.16",
136
+ "redis_version" => options[:version] || DEFAULT_REDIS_VERSION,
134
137
  "connected_clients" => "1",
135
138
  "connected_slaves" => "0",
136
139
  "used_memory" => "3187",
@@ -147,9 +150,13 @@ class Redis
147
150
 
148
151
  def save; end
149
152
 
150
- def bgsave ; end
153
+ def bgsave; end
154
+
155
+ def bgrewriteaof; end
156
+
157
+ def evalsha; end
151
158
 
152
- def bgrewriteaof ; end
159
+ def eval; end
153
160
 
154
161
  def move key, destination_id
155
162
  raise Redis::CommandError, "ERR source and destination objects are the same" if destination_id == database_id
@@ -214,6 +221,11 @@ class Redis
214
221
  data[key][start_index..end_index].unpack('B*')[0].count("1")
215
222
  end
216
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
+
217
229
  def getrange(key, start, ending)
218
230
  return unless data[key]
219
231
  data[key][start..ending]
@@ -493,9 +505,9 @@ class Redis
493
505
 
494
506
  def brpoplpush(key1, key2, opts={})
495
507
  data_type_check(key1, Array)
496
- brpop(key1).tap do |elem|
497
- lpush(key2, elem) unless elem.nil?
498
- end
508
+ _key, elem = brpop(key1)
509
+ lpush(key2, elem) unless elem.nil?
510
+ elem
499
511
  end
500
512
 
501
513
  def lpop(key)
@@ -549,6 +561,8 @@ class Redis
549
561
 
550
562
  def srem(key, value)
551
563
  data_type_check(key, ::Set)
564
+ value = Array(value)
565
+ raise_argument_error('srem') if value.empty?
552
566
  return false unless data[key]
553
567
 
554
568
  if value.is_a?(Array)
@@ -575,7 +589,7 @@ class Redis
575
589
  data_type_check(key, ::Set)
576
590
  results = (count || 1).times.map do
577
591
  elem = srandmember(key)
578
- srem(key, elem)
592
+ srem(key, elem) if elem
579
593
  elem
580
594
  end.compact
581
595
  count.nil? ? results.first : results
@@ -680,14 +694,11 @@ class Redis
680
694
  end
681
695
 
682
696
  def del(*keys)
683
- keys = keys.flatten(1)
684
- raise_argument_error('del') if keys.empty?
697
+ delete_keys(keys, 'del')
698
+ end
685
699
 
686
- old_count = data.keys.size
687
- keys.each do |key|
688
- data.delete(key)
689
- end
690
- old_count - data.keys.size
700
+ def unlink(*keys)
701
+ delete_keys(keys, 'unlink')
691
702
  end
692
703
 
693
704
  def setnx(key, value)
@@ -798,7 +809,7 @@ class Redis
798
809
  end
799
810
 
800
811
  def hmget(key, *fields)
801
- raise_argument_error('hmget') if fields.empty?
812
+ raise_argument_error('hmget') if fields.empty? || fields.flatten.empty?
802
813
 
803
814
  data_type_check(key, Hash)
804
815
  fields.flatten.map do |field|
@@ -817,6 +828,12 @@ class Redis
817
828
  data[key].size
818
829
  end
819
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
+
820
837
  def hvals(key)
821
838
  data_type_check(key, Hash)
822
839
  return [] unless data[key]
@@ -853,22 +870,14 @@ class Redis
853
870
 
854
871
  def sync ; end
855
872
 
856
- def [](key)
857
- get(key)
858
- end
859
-
860
- def []=(key, value)
861
- set(key, value)
862
- end
863
-
864
873
  def set(key, value, *array_options)
865
874
  option_nx = array_options.delete("NX")
866
875
  option_xx = array_options.delete("XX")
867
876
 
868
- return false if option_nx && option_xx
877
+ return nil if option_nx && option_xx
869
878
 
870
- return false if option_nx && exists(key)
871
- return false if option_xx && !exists(key)
879
+ return nil if option_nx && exists(key)
880
+ return nil if option_xx && !exists(key)
872
881
 
873
882
  data[key] = value.to_s
874
883
 
@@ -899,6 +908,10 @@ class Redis
899
908
  "OK"
900
909
  end
901
910
 
911
+ def psetex(key, milliseconds, value)
912
+ setex(key, milliseconds / 1000.0, value)
913
+ end
914
+
902
915
  def setrange(key, offset, value)
903
916
  return unless data[key]
904
917
  s = data[key][offset,value.size]
@@ -1005,6 +1018,19 @@ class Redis
1005
1018
  end
1006
1019
 
1007
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
+
1008
1034
  if !args.first.is_a?(Array)
1009
1035
  if args.size < 2
1010
1036
  raise_argument_error('zadd')
@@ -1020,18 +1046,39 @@ class Redis
1020
1046
  data_type_check(key, ZSet)
1021
1047
  data[key] ||= ZSet.new
1022
1048
 
1023
- if args.size == 2 && !(Array === args.first)
1024
- score, value = args
1025
- 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
1026
1073
  data[key][value.to_s] = score
1027
- else
1028
- # Turn [1, 2, 3, 4] into [[1, 2], [3, 4]] unless it is already
1029
- args = args.each_slice(2).to_a unless args.first.is_a?(Array)
1030
- exists = args.map(&:last).map { |el| data[key].key?(el.to_s) }.count(false)
1031
- args.each { |s, v| data[key][v.to_s] = s }
1032
1074
  end
1033
1075
 
1034
- 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
1035
1082
  end
1036
1083
 
1037
1084
  def zrem(key, value)
@@ -1047,6 +1094,36 @@ class Redis
1047
1094
  response
1048
1095
  end
1049
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
+
1050
1127
  def zcard(key)
1051
1128
  data_type_check(key, ZSet)
1052
1129
  data[key] ? data[key].size : 0
@@ -1055,7 +1132,13 @@ class Redis
1055
1132
  def zscore(key, value)
1056
1133
  data_type_check(key, ZSet)
1057
1134
  value = data[key] && data[key][value.to_s]
1058
- 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
1059
1142
  end
1060
1143
 
1061
1144
  def zcount(key, min, max)
@@ -1069,7 +1152,14 @@ class Redis
1069
1152
  data[key] ||= ZSet.new
1070
1153
  data[key][value.to_s] ||= 0
1071
1154
  data[key].increment(value.to_s, num)
1072
- 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
1073
1163
  end
1074
1164
 
1075
1165
  def zrank(key, value)
@@ -1093,6 +1183,7 @@ class Redis
1093
1183
  results = sort_keys(data[key])
1094
1184
  # Select just the keys unless we want scores
1095
1185
  results = results.map(&:first) unless with_scores
1186
+ start = [start, -results.size].max
1096
1187
  (results[start..stop] || []).flatten.map(&:to_s)
1097
1188
  end
1098
1189
 
@@ -1208,6 +1299,33 @@ class Redis
1208
1299
  data[out].size
1209
1300
  end
1210
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
+
1211
1329
  def subscribe(*channels)
1212
1330
  raise_argument_error('subscribe') if channels.empty?()
1213
1331
 
@@ -1348,13 +1466,36 @@ class Redis
1348
1466
  raise Redis::CommandError, "ERR syntax error"
1349
1467
  end
1350
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
+
1351
1493
  def remove_key_for_empty_collection(key)
1352
1494
  del(key) if data[key] && data[key].empty?
1353
1495
  end
1354
1496
 
1355
1497
  def data_type_check(key, klass)
1356
1498
  if data[key] && !data[key].is_a?(klass)
1357
- warn "Operation against a key holding the wrong kind of value: Expected #{klass} at #{key}."
1358
1499
  raise Redis::CommandError.new("WRONGTYPE Operation against a key holding the wrong kind of value")
1359
1500
  end
1360
1501
  end
@@ -1419,9 +1560,33 @@ class Redis
1419
1560
  end
1420
1561
  end
1421
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
+
1422
1587
  def sort_keys(arr)
1423
1588
  # Sort by score, or if scores are equal, key alphanum
1424
- sorted_keys = arr.sort do |(k1, v1), (k2, v2)|
1589
+ arr.sort do |(k1, v1), (k2, v2)|
1425
1590
  if v1 == v2
1426
1591
  k1 <=> k2
1427
1592
  else