redis_migrator 0.0.1
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.
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/README.md +35 -0
- data/lib/redis_migrator/redis_helper.rb +52 -0
- data/lib/redis_migrator/redis_populator.rb +63 -0
- data/lib/redis_migrator.rb +96 -0
- data/migrator_benchmark.rb +22 -0
- data/redis_migrator.gemspec +20 -0
- data/spec/migrator_spec.rb +63 -0
- data/spec/mock_redis/lib/mock_redis/assertions.rb +13 -0
- data/spec/mock_redis/lib/mock_redis/database.rb +432 -0
- data/spec/mock_redis/lib/mock_redis/distributed.rb +6 -0
- data/spec/mock_redis/lib/mock_redis/exceptions.rb +3 -0
- data/spec/mock_redis/lib/mock_redis/expire_wrapper.rb +25 -0
- data/spec/mock_redis/lib/mock_redis/hash_methods.rb +118 -0
- data/spec/mock_redis/lib/mock_redis/list_methods.rb +187 -0
- data/spec/mock_redis/lib/mock_redis/multi_db_wrapper.rb +86 -0
- data/spec/mock_redis/lib/mock_redis/set_methods.rb +126 -0
- data/spec/mock_redis/lib/mock_redis/string_methods.rb +203 -0
- data/spec/mock_redis/lib/mock_redis/transaction_wrapper.rb +80 -0
- data/spec/mock_redis/lib/mock_redis/undef_redis_methods.rb +11 -0
- data/spec/mock_redis/lib/mock_redis/utility_methods.rb +25 -0
- data/spec/mock_redis/lib/mock_redis/version.rb +3 -0
- data/spec/mock_redis/lib/mock_redis/zset.rb +110 -0
- data/spec/mock_redis/lib/mock_redis/zset_methods.rb +210 -0
- data/spec/mock_redis/lib/mock_redis.rb +119 -0
- data/spec/redis_helper_spec.rb +58 -0
- data/spec/spec_helper.rb +29 -0
- metadata +107 -0
@@ -0,0 +1,432 @@
|
|
1
|
+
require 'mock_redis/assertions'
|
2
|
+
require 'mock_redis/exceptions'
|
3
|
+
require 'mock_redis/hash_methods'
|
4
|
+
require 'mock_redis/list_methods'
|
5
|
+
require 'mock_redis/set_methods'
|
6
|
+
require 'mock_redis/string_methods'
|
7
|
+
require 'mock_redis/zset_methods'
|
8
|
+
|
9
|
+
class MockRedis
|
10
|
+
class Database
|
11
|
+
include HashMethods
|
12
|
+
include ListMethods
|
13
|
+
include SetMethods
|
14
|
+
include StringMethods
|
15
|
+
include ZsetMethods
|
16
|
+
|
17
|
+
attr_reader :data, :expire_times
|
18
|
+
|
19
|
+
def initialize(*args)
|
20
|
+
@data = {}
|
21
|
+
@expire_times = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize_copy(source)
|
25
|
+
@data = @data.clone
|
26
|
+
@data.keys.each {|k| @data[k] = @data[k].clone}
|
27
|
+
@expire_times = @expire_times.map{|x| x.clone}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Redis commands go below this line and above 'private'
|
31
|
+
|
32
|
+
def auth(_) 'OK' end
|
33
|
+
|
34
|
+
def bgrewriteaof() "Background append only file rewriting started" end
|
35
|
+
|
36
|
+
def bgsave() "Background saving started" end
|
37
|
+
|
38
|
+
def dbsize
|
39
|
+
data.keys.length
|
40
|
+
end
|
41
|
+
|
42
|
+
def del(*keys)
|
43
|
+
keys.
|
44
|
+
find_all{|key| data[key]}.
|
45
|
+
each {|k| persist(k)}.
|
46
|
+
each {|k| data.delete(k)}.
|
47
|
+
length
|
48
|
+
end
|
49
|
+
|
50
|
+
def echo(msg)
|
51
|
+
msg.to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
def expire(key, seconds)
|
55
|
+
expireat(key, Time.now.to_i + seconds.to_i)
|
56
|
+
end
|
57
|
+
|
58
|
+
def expireat(key, timestamp)
|
59
|
+
unless looks_like_integer?(timestamp.to_s)
|
60
|
+
raise RuntimeError, "ERR value is not an integer or out of range"
|
61
|
+
end
|
62
|
+
|
63
|
+
if exists(key)
|
64
|
+
set_expiration(key, Time.at(timestamp.to_i))
|
65
|
+
true
|
66
|
+
else
|
67
|
+
false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def exists(key)
|
72
|
+
data.has_key?(key)
|
73
|
+
end
|
74
|
+
|
75
|
+
def flushdb
|
76
|
+
data.keys.each {|k| del(k)}
|
77
|
+
'OK'
|
78
|
+
end
|
79
|
+
|
80
|
+
def info
|
81
|
+
astats = [
|
82
|
+
["2", "2699"],
|
83
|
+
["6", "1"],
|
84
|
+
["7", "1"],
|
85
|
+
["8", "17197"],
|
86
|
+
["9", "109875"],
|
87
|
+
["10", "94348"],
|
88
|
+
["11", "32580"],
|
89
|
+
["12", "52347"],
|
90
|
+
["13", "86475"],
|
91
|
+
["14", "58175"],
|
92
|
+
["15", "53408"],
|
93
|
+
["16", "876949"],
|
94
|
+
["17", "71157"],
|
95
|
+
["18", "5104"],
|
96
|
+
["19", "2705"],
|
97
|
+
["20", "2903"],
|
98
|
+
["21", "1024"],
|
99
|
+
["22", "2546"],
|
100
|
+
["23", "952"],
|
101
|
+
["24", "186080"],
|
102
|
+
["25", "611"],
|
103
|
+
["26", "40936"],
|
104
|
+
["27", "960"],
|
105
|
+
["28", "1323"],
|
106
|
+
["29", "14216"],
|
107
|
+
["30", "52412"],
|
108
|
+
["31", "21130"],
|
109
|
+
["32", "47959"],
|
110
|
+
["33", "6891"],
|
111
|
+
["34", "9712"],
|
112
|
+
["35", "3366"],
|
113
|
+
["36", "5737"],
|
114
|
+
["37", "11274"],
|
115
|
+
["38", "8057"],
|
116
|
+
["39", "2957"],
|
117
|
+
["40", "51200"],
|
118
|
+
["42", "8220"],
|
119
|
+
["43", "8278"],
|
120
|
+
["44", "6539"],
|
121
|
+
["45", "764"],
|
122
|
+
["47", "1018"],
|
123
|
+
["48", "19250"],
|
124
|
+
["49", "713"],
|
125
|
+
["51", "51"],
|
126
|
+
["53", "2"],
|
127
|
+
["55", "3922"],
|
128
|
+
["56", "153"],
|
129
|
+
["57", "614"],
|
130
|
+
["58", "1"],
|
131
|
+
["59", "1775"],
|
132
|
+
["61", "32865"],
|
133
|
+
["63", "2530"],
|
134
|
+
["64", "565"],
|
135
|
+
["65", "1322"],
|
136
|
+
["67", "1572"],
|
137
|
+
["69", "1421"],
|
138
|
+
["71", "1220"],
|
139
|
+
["72", "241"],
|
140
|
+
["73", "5432"],
|
141
|
+
["74", "1122"],
|
142
|
+
["75", "2555"],
|
143
|
+
["77", "1539"],
|
144
|
+
["78", "612"],
|
145
|
+
["79", "902"],
|
146
|
+
["81", "1678"],
|
147
|
+
["83", "51"],
|
148
|
+
["84", "612"],
|
149
|
+
["85", "706"],
|
150
|
+
["87", "410"],
|
151
|
+
["88", "5435"],
|
152
|
+
["89", "813"],
|
153
|
+
["90", "612"],
|
154
|
+
["93", "153"],
|
155
|
+
["94", "612"],
|
156
|
+
["96", "159"],
|
157
|
+
["97", "306"],
|
158
|
+
["99", "153"],
|
159
|
+
["101", "456"],
|
160
|
+
["103", "741"],
|
161
|
+
["105", "447"],
|
162
|
+
["107", "754"],
|
163
|
+
["109", "414"],
|
164
|
+
["111", "475"],
|
165
|
+
["113", "757"],
|
166
|
+
["115", "287"],
|
167
|
+
["117", "420"],
|
168
|
+
["118", "765"],
|
169
|
+
["119", "642"],
|
170
|
+
["120", "159"],
|
171
|
+
["121", "926"],
|
172
|
+
["122", "612"],
|
173
|
+
["123", "251"],
|
174
|
+
["125", "390"],
|
175
|
+
["127", "354"],
|
176
|
+
["128", "617"],
|
177
|
+
["129", "528"],
|
178
|
+
["131", "298"],
|
179
|
+
["132", "612"],
|
180
|
+
["133", "809"],
|
181
|
+
["135", "244"],
|
182
|
+
["136", "306"],
|
183
|
+
["137", "504"],
|
184
|
+
["139", "201"],
|
185
|
+
["141", "1124"],
|
186
|
+
["143", "139"],
|
187
|
+
["144", "159"],
|
188
|
+
["145", "1322"],
|
189
|
+
["147", "410"],
|
190
|
+
["149", "253"],
|
191
|
+
["151", "304"],
|
192
|
+
["153", "312"],
|
193
|
+
["155", "249"],
|
194
|
+
["157", "306"],
|
195
|
+
["159", "348"],
|
196
|
+
["161", "255"],
|
197
|
+
["163", "458"],
|
198
|
+
["165", "5"],
|
199
|
+
["167", "306"],
|
200
|
+
["168", "47"],
|
201
|
+
["169", "214"],
|
202
|
+
["171", "250"],
|
203
|
+
["173", "5"],
|
204
|
+
["177", "10"],
|
205
|
+
["179", "158"],
|
206
|
+
["181", "5"],
|
207
|
+
["183", "10"],
|
208
|
+
["185", "51"],
|
209
|
+
["187", "49"],
|
210
|
+
["191", "5"],
|
211
|
+
["192", "47"],
|
212
|
+
["193", "51"],
|
213
|
+
["197", "112"],
|
214
|
+
["199", "5"],
|
215
|
+
["201", "5"],
|
216
|
+
["203", "5"],
|
217
|
+
["209", "5"],
|
218
|
+
["213", "51"],
|
219
|
+
["217", "102"],
|
220
|
+
["225", "357"],
|
221
|
+
["229", "51"],
|
222
|
+
["233", "204"],
|
223
|
+
["237", "51"],
|
224
|
+
["239", "1"],
|
225
|
+
["247", "46"],
|
226
|
+
["255", "102"],
|
227
|
+
[">=256", "6201"],
|
228
|
+
]
|
229
|
+
|
230
|
+
{
|
231
|
+
"allocation_stats" => astats.map {|(a,b)| "#{a}=#{b}"}.join(','),
|
232
|
+
"aof_enabled" => "0",
|
233
|
+
"arch_bits" => "64",
|
234
|
+
"bgrewriteaof_in_progress" => "0",
|
235
|
+
"bgsave_in_progress" => "0",
|
236
|
+
"blocked_clients" => "0",
|
237
|
+
"changes_since_last_save" => "0",
|
238
|
+
"client_biggest_input_buf" => "0",
|
239
|
+
"client_longest_output_list" => "0",
|
240
|
+
"connected_clients" => "1",
|
241
|
+
"connected_slaves" => "0",
|
242
|
+
"db0" => "keys=8,expires=0",
|
243
|
+
"evicted_keys" => "0",
|
244
|
+
"expired_keys" => "0",
|
245
|
+
"hash_max_zipmap_entries" => "512",
|
246
|
+
"hash_max_zipmap_value" => "64",
|
247
|
+
"keyspace_hits" => "62645",
|
248
|
+
"keyspace_misses" => "29757",
|
249
|
+
"last_save_time" => "1310596333",
|
250
|
+
"loading" => "0",
|
251
|
+
"lru_clock" => "1036434",
|
252
|
+
"mem_fragmentation_ratio" => "2.04",
|
253
|
+
"multiplexing_api" => "kqueue",
|
254
|
+
"process_id" => "14508",
|
255
|
+
"pubsub_channels" => "0",
|
256
|
+
"pubsub_patterns" => "0",
|
257
|
+
"redis_git_dirty" => "0",
|
258
|
+
"redis_git_sha1" => "00000000",
|
259
|
+
"redis_version" => "2.2.11",
|
260
|
+
"role" => "master",
|
261
|
+
"total_commands_processed" => "196800",
|
262
|
+
"total_connections_received" => "4359",
|
263
|
+
"uptime_in_days" => "0",
|
264
|
+
"uptime_in_seconds" => "84215",
|
265
|
+
"use_tcmalloc" => "0",
|
266
|
+
"used_cpu_sys" => "5.54",
|
267
|
+
"used_cpu_sys_childrens" => "0.00",
|
268
|
+
"used_cpu_user" => "7.65",
|
269
|
+
"used_cpu_user_childrens" => "0.02",
|
270
|
+
"used_memory" => "931456",
|
271
|
+
"used_memory_human" => "909.62K",
|
272
|
+
"used_memory_rss" => "1904640",
|
273
|
+
"vm_enabled" => "0",
|
274
|
+
}
|
275
|
+
end
|
276
|
+
|
277
|
+
def keys(format)
|
278
|
+
data.keys.grep(redis_pattern_to_ruby_regex(format))
|
279
|
+
end
|
280
|
+
|
281
|
+
def lastsave
|
282
|
+
Time.now.to_i
|
283
|
+
end
|
284
|
+
|
285
|
+
def persist(key)
|
286
|
+
if exists(key) && has_expiration?(key)
|
287
|
+
remove_expiration(key)
|
288
|
+
true
|
289
|
+
else
|
290
|
+
false
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def ping
|
295
|
+
'PONG'
|
296
|
+
end
|
297
|
+
|
298
|
+
def quit
|
299
|
+
'OK'
|
300
|
+
end
|
301
|
+
|
302
|
+
def randomkey
|
303
|
+
data.keys[rand(data.length)]
|
304
|
+
end
|
305
|
+
|
306
|
+
def rename(key, newkey)
|
307
|
+
if key == newkey
|
308
|
+
raise RuntimeError, "ERR source and destination objects are the same"
|
309
|
+
end
|
310
|
+
data[newkey] = data.delete(key)
|
311
|
+
'OK'
|
312
|
+
end
|
313
|
+
|
314
|
+
def renamenx(key, newkey)
|
315
|
+
if key == newkey
|
316
|
+
raise RuntimeError, "ERR source and destination objects are the same"
|
317
|
+
end
|
318
|
+
if exists(newkey)
|
319
|
+
false
|
320
|
+
else
|
321
|
+
rename(key, newkey)
|
322
|
+
true
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
def save
|
327
|
+
'OK'
|
328
|
+
end
|
329
|
+
|
330
|
+
def ttl(key)
|
331
|
+
if has_expiration?(key)
|
332
|
+
expiration(key).to_i - Time.now.to_i
|
333
|
+
else
|
334
|
+
-1
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def type(key)
|
339
|
+
if !exists(key)
|
340
|
+
'none'
|
341
|
+
elsif hashy?(key)
|
342
|
+
'hash'
|
343
|
+
elsif stringy?(key)
|
344
|
+
'string'
|
345
|
+
elsif listy?(key)
|
346
|
+
'list'
|
347
|
+
elsif sety?(key)
|
348
|
+
'set'
|
349
|
+
elsif zsety?(key)
|
350
|
+
'zset'
|
351
|
+
else
|
352
|
+
raise ArgumentError, "Not sure how #{data[key].inspect} got in here"
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
private
|
357
|
+
|
358
|
+
def assert_valid_timeout(timeout)
|
359
|
+
if !looks_like_integer?(timeout.to_s)
|
360
|
+
raise RuntimeError, "ERR timeout is not an integer or out of range"
|
361
|
+
elsif timeout < 0
|
362
|
+
raise RuntimeError, "ERR timeout is negative"
|
363
|
+
end
|
364
|
+
timeout
|
365
|
+
end
|
366
|
+
|
367
|
+
def can_incr?(value)
|
368
|
+
value.nil? || looks_like_integer?(value)
|
369
|
+
end
|
370
|
+
|
371
|
+
def extract_timeout(arglist)
|
372
|
+
timeout = assert_valid_timeout(arglist.last)
|
373
|
+
[arglist[0..-2], arglist.last]
|
374
|
+
end
|
375
|
+
|
376
|
+
def expiration(key)
|
377
|
+
expire_times.find {|(_,k)| k == key}.first
|
378
|
+
end
|
379
|
+
|
380
|
+
def has_expiration?(key)
|
381
|
+
expire_times.any? {|(_,k)| k == key}
|
382
|
+
end
|
383
|
+
|
384
|
+
def looks_like_integer?(str)
|
385
|
+
str =~ /^-?\d+$/
|
386
|
+
end
|
387
|
+
|
388
|
+
def redis_pattern_to_ruby_regex(pattern)
|
389
|
+
Regexp.new(
|
390
|
+
"^#{pattern}$".
|
391
|
+
gsub(/([^\\])\?/, "\\1.").
|
392
|
+
gsub(/([^\\])\*/, "\\1.+"))
|
393
|
+
end
|
394
|
+
|
395
|
+
def remove_expiration(key)
|
396
|
+
expire_times.delete_if do |(t, k)|
|
397
|
+
key == k
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def set_expiration(key, time)
|
402
|
+
remove_expiration(key)
|
403
|
+
|
404
|
+
expire_times << [time, key]
|
405
|
+
expire_times.sort! do |a, b|
|
406
|
+
a.first <=> b.first
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
def zero_pad(string, desired_length)
|
411
|
+
padding = "\000" * [(desired_length - string.length), 0].max
|
412
|
+
string + padding
|
413
|
+
end
|
414
|
+
|
415
|
+
public
|
416
|
+
# This method isn't private, but it also isn't a Redis command, so
|
417
|
+
# it doesn't belong up above with all the Redis commands.
|
418
|
+
def expire_keys
|
419
|
+
now = Time.now
|
420
|
+
|
421
|
+
to_delete = expire_times.take_while do |(time, key)|
|
422
|
+
time <= now
|
423
|
+
end
|
424
|
+
|
425
|
+
to_delete.each do |(time, key)|
|
426
|
+
del(key)
|
427
|
+
end
|
428
|
+
|
429
|
+
expire_times.slice!(0, to_delete.length)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'mock_redis/undef_redis_methods'
|
2
|
+
|
3
|
+
class MockRedis
|
4
|
+
class ExpireWrapper
|
5
|
+
include UndefRedisMethods
|
6
|
+
|
7
|
+
def respond_to?(method, include_private=false)
|
8
|
+
super || @db.respond_to?(method)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(db)
|
12
|
+
@db = db
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(method, *args)
|
16
|
+
@db.expire_keys
|
17
|
+
@db.send(method, *args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize_copy(source)
|
21
|
+
super
|
22
|
+
@db = @db.clone
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'mock_redis/assertions'
|
2
|
+
require 'mock_redis/utility_methods'
|
3
|
+
|
4
|
+
class MockRedis
|
5
|
+
module HashMethods
|
6
|
+
include Assertions
|
7
|
+
include UtilityMethods
|
8
|
+
|
9
|
+
def hdel(key, field)
|
10
|
+
with_hash_at(key) do |hash|
|
11
|
+
hash.delete(field.to_s) ? 1 : 0
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def hexists(key, field)
|
16
|
+
with_hash_at(key) {|h| h.has_key?(field.to_s)}
|
17
|
+
end
|
18
|
+
|
19
|
+
def hget(key, field)
|
20
|
+
with_hash_at(key) {|h| h[field.to_s]}
|
21
|
+
end
|
22
|
+
|
23
|
+
def hgetall(key)
|
24
|
+
with_hash_at(key) {|h| h}
|
25
|
+
end
|
26
|
+
|
27
|
+
def hincrby(key, field, increment)
|
28
|
+
with_hash_at(key) do |hash|
|
29
|
+
field = field.to_s
|
30
|
+
unless can_incr?(data[key][field])
|
31
|
+
raise RuntimeError, "ERR hash value is not an integer"
|
32
|
+
end
|
33
|
+
unless looks_like_integer?(increment.to_s)
|
34
|
+
raise RuntimeError, "ERR value is not an integer or out of range"
|
35
|
+
end
|
36
|
+
|
37
|
+
new_value = (hash[field] || "0").to_i + increment.to_i
|
38
|
+
hash[field] = new_value.to_s
|
39
|
+
new_value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def hkeys(key)
|
44
|
+
with_hash_at(key, &:keys)
|
45
|
+
end
|
46
|
+
|
47
|
+
def hlen(key)
|
48
|
+
hkeys(key).length
|
49
|
+
end
|
50
|
+
|
51
|
+
def hmget(key, *fields)
|
52
|
+
assert_has_args(fields, 'hmget')
|
53
|
+
fields.map{|f| hget(key, f)}
|
54
|
+
end
|
55
|
+
|
56
|
+
def mapped_hmget(key, *fields)
|
57
|
+
reply = hmget(key, *fields)
|
58
|
+
Hash[*fields.zip(reply).flatten]
|
59
|
+
end
|
60
|
+
|
61
|
+
def hmset(key, *kvpairs)
|
62
|
+
assert_has_args(kvpairs, 'hmset')
|
63
|
+
if kvpairs.length.odd?
|
64
|
+
raise RuntimeError, "ERR wrong number of arguments for HMSET"
|
65
|
+
end
|
66
|
+
|
67
|
+
kvpairs.each_slice(2) do |(k,v)|
|
68
|
+
hset(key, k, v)
|
69
|
+
end
|
70
|
+
'OK'
|
71
|
+
end
|
72
|
+
|
73
|
+
def mapped_hmset(key, hash)
|
74
|
+
kvpairs = hash.to_a.flatten
|
75
|
+
assert_has_args(kvpairs, 'hmset')
|
76
|
+
if kvpairs.length.odd?
|
77
|
+
raise RuntimeError, "ERR wrong number of arguments for 'hmset' command"
|
78
|
+
end
|
79
|
+
|
80
|
+
hmset(key, *kvpairs)
|
81
|
+
end
|
82
|
+
|
83
|
+
def hset(key, field, value)
|
84
|
+
with_hash_at(key) {|h| h[field.to_s] = value.to_s}
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
88
|
+
def hsetnx(key, field, value)
|
89
|
+
if hget(key, field)
|
90
|
+
false
|
91
|
+
else
|
92
|
+
hset(key, field, value)
|
93
|
+
true
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def hvals(key)
|
98
|
+
with_hash_at(key, &:values)
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def with_hash_at(key, &blk)
|
104
|
+
with_thing_at(key, :assert_hashy, proc {{}}, &blk)
|
105
|
+
end
|
106
|
+
|
107
|
+
def hashy?(key)
|
108
|
+
data[key].nil? || data[key].kind_of?(Hash)
|
109
|
+
end
|
110
|
+
|
111
|
+
def assert_hashy(key)
|
112
|
+
unless hashy?(key)
|
113
|
+
raise RuntimeError, "ERR Operation against a key holding the wrong kind of value"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|