redis_migrator 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|