oklahoma_mixer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS.rdoc +1 -0
- data/CHANGELOG.rdoc +7 -0
- data/INSTALL.rdoc +22 -0
- data/MIT-LICENSE +19 -0
- data/README.rdoc +48 -0
- data/Rakefile +61 -0
- data/TODO.rdoc +11 -0
- data/lib/oklahoma_mixer/array_list/c.rb +15 -0
- data/lib/oklahoma_mixer/array_list.rb +23 -0
- data/lib/oklahoma_mixer/error.rb +6 -0
- data/lib/oklahoma_mixer/extensible_string/c.rb +18 -0
- data/lib/oklahoma_mixer/extensible_string.rb +27 -0
- data/lib/oklahoma_mixer/hash_database/c.rb +128 -0
- data/lib/oklahoma_mixer/hash_database.rb +335 -0
- data/lib/oklahoma_mixer/utilities.rb +88 -0
- data/lib/oklahoma_mixer.rb +33 -0
- data/test/binary_data_test.rb +41 -0
- data/test/file_system_test.rb +74 -0
- data/test/getting_and_setting_keys_test.rb +218 -0
- data/test/iteration_test.rb +94 -0
- data/test/test_helper.rb +51 -0
- data/test/top_level_interface_test.rb +47 -0
- data/test/transactions_test.rb +66 -0
- data/test/tuning_test.rb +173 -0
- metadata +115 -0
@@ -0,0 +1,335 @@
|
|
1
|
+
module OklahomaMixer
|
2
|
+
class HashDatabase
|
3
|
+
#################
|
4
|
+
### Constants ###
|
5
|
+
#################
|
6
|
+
|
7
|
+
MODES = { "r" => :HDBOREADER,
|
8
|
+
"w" => :HDBOWRITER,
|
9
|
+
"c" => :HDBOCREAT,
|
10
|
+
"t" => :HDBOTRUNC,
|
11
|
+
"e" => :HDBONOLCK,
|
12
|
+
"f" => :HDBOLCKNB,
|
13
|
+
"s" => :HDBOTSYNC }
|
14
|
+
OPTS = { "l" => :HDBTLARGE,
|
15
|
+
"d" => :HDBTDEFLATE,
|
16
|
+
"b" => :HDBTBZIP,
|
17
|
+
"t" => :HDBTTCBS }
|
18
|
+
|
19
|
+
###################
|
20
|
+
### File System ###
|
21
|
+
###################
|
22
|
+
|
23
|
+
def initialize(path, *args)
|
24
|
+
options = args.last.is_a?(Hash) ? args.last : { }
|
25
|
+
mode = !args.first.is_a?(Hash) ? args.first : nil
|
26
|
+
@path = path
|
27
|
+
@db = C.new
|
28
|
+
self.default = options[:default]
|
29
|
+
@in_transaction = false
|
30
|
+
@abort = false
|
31
|
+
@nested_transactions = options[:nested_transactions]
|
32
|
+
|
33
|
+
try(:setmutex) if options[:mutex]
|
34
|
+
if options.values_at(:bnum, :apow, :fpow, :opts).any?
|
35
|
+
optimize(options.merge(:tune => true))
|
36
|
+
end
|
37
|
+
{:rcnum => :cache, :xmsiz => nil, :dfunit => nil}.each do |option, func|
|
38
|
+
if i = options[option]
|
39
|
+
try("set#{func || option}", i.to_i)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
warn "mode option supersedes mode argument" if mode and options[:mode]
|
44
|
+
try(:open, path, to_enum_int(options.fetch(:mode, mode || "wc"), :mode))
|
45
|
+
end
|
46
|
+
|
47
|
+
def optimize(options)
|
48
|
+
try( options[:tune] ? :tune : :optimize,
|
49
|
+
options.fetch(:bnum, 0).to_i,
|
50
|
+
options.fetch(:apow, -1).to_i,
|
51
|
+
options.fetch(:fpow, -1).to_i,
|
52
|
+
to_enum_int(options.fetch(:opts, 0xFF), :opt) )
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :path
|
56
|
+
|
57
|
+
def file_size
|
58
|
+
C.fsiz(@db)
|
59
|
+
end
|
60
|
+
|
61
|
+
def flush
|
62
|
+
try(:sync)
|
63
|
+
end
|
64
|
+
alias_method :sync, :flush
|
65
|
+
alias_method :fsync, :flush
|
66
|
+
|
67
|
+
def copy(path)
|
68
|
+
try(:copy, path)
|
69
|
+
end
|
70
|
+
alias_method :backup, :copy
|
71
|
+
|
72
|
+
def defrag(steps = 0)
|
73
|
+
try(:defrag, steps.to_i)
|
74
|
+
end
|
75
|
+
|
76
|
+
def close
|
77
|
+
try(:del) # closes before it deletes the object
|
78
|
+
end
|
79
|
+
|
80
|
+
################################
|
81
|
+
### Getting and Setting Keys ###
|
82
|
+
################################
|
83
|
+
|
84
|
+
def default(key = nil)
|
85
|
+
@default[key] if @default
|
86
|
+
end
|
87
|
+
|
88
|
+
def default=(value_or_proc)
|
89
|
+
@default = case value_or_proc
|
90
|
+
when Proc then value_or_proc
|
91
|
+
when nil then nil
|
92
|
+
else lambda { |key| value_or_proc }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def store(key, value, mode = nil)
|
97
|
+
k, v = key.to_s, value.to_s
|
98
|
+
result = value
|
99
|
+
if block_given?
|
100
|
+
warn "block supersedes mode argument" unless mode.nil?
|
101
|
+
callback = lambda { |old_value_pointer, old_size, returned_size, _|
|
102
|
+
old_value = old_value_pointer.get_bytes(0, old_size)
|
103
|
+
replacement = yield(key, old_value, value).to_s
|
104
|
+
returned_size.put_int(0, replacement.size)
|
105
|
+
FFI::MemoryPointer.from_string(replacement)
|
106
|
+
}
|
107
|
+
try(:putproc, k, k.size, v, v.size, callback, nil)
|
108
|
+
else
|
109
|
+
case mode
|
110
|
+
when :keep
|
111
|
+
result = try( :putkeep, k, k.size, v, v.size,
|
112
|
+
:no_error => {21 => false} )
|
113
|
+
when :cat
|
114
|
+
try(:putcat, k, k.size, v, v.size)
|
115
|
+
when :async
|
116
|
+
try(:putasync, k, k.size, v, v.size)
|
117
|
+
when :add
|
118
|
+
result = case value
|
119
|
+
when Float then try( :adddouble, k, k.size, value,
|
120
|
+
:failure => lambda { |n| n.nan? } )
|
121
|
+
else try( :addint, k, k.size, value.to_i,
|
122
|
+
:failure => Utilities::INT_MIN )
|
123
|
+
end
|
124
|
+
else
|
125
|
+
try(:put, k, k.size, v, v.size)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
result
|
129
|
+
end
|
130
|
+
alias_method :[]=, :store
|
131
|
+
|
132
|
+
def fetch(key, *default)
|
133
|
+
k = key.to_s
|
134
|
+
if value = try( :read_from_func, :get, k, k.size,
|
135
|
+
:failure => nil, :no_error => {22 => nil} )
|
136
|
+
value
|
137
|
+
else
|
138
|
+
if block_given?
|
139
|
+
warn "block supersedes default value argument" unless default.empty?
|
140
|
+
yield key
|
141
|
+
elsif not default.empty?
|
142
|
+
default.first
|
143
|
+
else
|
144
|
+
fail IndexError, "key not found"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def [](key)
|
150
|
+
fetch(key, &@default)
|
151
|
+
rescue IndexError
|
152
|
+
nil
|
153
|
+
end
|
154
|
+
|
155
|
+
def update(hash, &dup_handler)
|
156
|
+
hash.each do |key, value|
|
157
|
+
store(key, value, &dup_handler)
|
158
|
+
end
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
162
|
+
def values_at(*keys)
|
163
|
+
keys.map { |key| self[key] }
|
164
|
+
end
|
165
|
+
|
166
|
+
def keys(options = { })
|
167
|
+
prefix = options.fetch(:prefix, "").to_s
|
168
|
+
limit = options.fetch(:limit, -1)
|
169
|
+
list = ArrayList.new(C.fwmkeys(@db, prefix, prefix.size, limit))
|
170
|
+
list.to_a
|
171
|
+
ensure
|
172
|
+
list.free if list
|
173
|
+
end
|
174
|
+
|
175
|
+
def values
|
176
|
+
values = [ ]
|
177
|
+
each_value do |value|
|
178
|
+
values << value
|
179
|
+
end
|
180
|
+
values
|
181
|
+
end
|
182
|
+
|
183
|
+
def delete(key, &missing_handler)
|
184
|
+
value = fetch(key, &missing_handler)
|
185
|
+
k = key.to_s
|
186
|
+
try(:out, k, k.size, :no_error => {22 => nil})
|
187
|
+
value
|
188
|
+
rescue IndexError
|
189
|
+
nil
|
190
|
+
end
|
191
|
+
|
192
|
+
def clear
|
193
|
+
try(:vanish)
|
194
|
+
self
|
195
|
+
end
|
196
|
+
|
197
|
+
def include?(key)
|
198
|
+
fetch(key)
|
199
|
+
true
|
200
|
+
rescue IndexError
|
201
|
+
false
|
202
|
+
end
|
203
|
+
alias_method :has_key?, :include?
|
204
|
+
alias_method :key?, :include?
|
205
|
+
alias_method :member?, :include?
|
206
|
+
|
207
|
+
def size
|
208
|
+
C.rnum(@db)
|
209
|
+
end
|
210
|
+
alias_method :length, :size
|
211
|
+
|
212
|
+
#################
|
213
|
+
### Iteration ###
|
214
|
+
#################
|
215
|
+
|
216
|
+
include Enumerable
|
217
|
+
|
218
|
+
def each_key
|
219
|
+
try(:iterinit)
|
220
|
+
loop do
|
221
|
+
return self unless key = try( :read_from_func, :iternext,
|
222
|
+
:failure => nil,
|
223
|
+
:no_error => {22 => nil} )
|
224
|
+
yield key
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def each
|
229
|
+
try(:iterinit)
|
230
|
+
loop do
|
231
|
+
Utilities.temp_xstr do |key|
|
232
|
+
Utilities.temp_xstr do |value|
|
233
|
+
return self unless try( :iternext3, key.pointer, value.pointer,
|
234
|
+
:no_error => {22 => false} )
|
235
|
+
yield [key.to_s, value.to_s]
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
alias_method :each_pair, :each
|
241
|
+
|
242
|
+
def each_value
|
243
|
+
each do |key, value|
|
244
|
+
yield value
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def delete_if
|
249
|
+
each do |key, value|
|
250
|
+
delete(key) if yield key, value
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
####################
|
255
|
+
### Transactions ###
|
256
|
+
####################
|
257
|
+
|
258
|
+
def transaction
|
259
|
+
if @in_transaction
|
260
|
+
case @nested_transactions
|
261
|
+
when :ignore
|
262
|
+
return yield
|
263
|
+
when :fail, :raise
|
264
|
+
fail Error::TransactionError, "nested transaction"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
@in_transaction = true
|
269
|
+
@abort = false
|
270
|
+
|
271
|
+
begin
|
272
|
+
catch(:finish_transaction) do
|
273
|
+
try(:tranbegin)
|
274
|
+
yield
|
275
|
+
end
|
276
|
+
rescue Exception
|
277
|
+
@abort = true
|
278
|
+
raise
|
279
|
+
ensure
|
280
|
+
try("tran#{@abort ? :abort : :commit}")
|
281
|
+
@in_transaction = false
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def commit
|
286
|
+
fail Error::TransactionError, "not in transaction" unless @in_transaction
|
287
|
+
throw :finish_transaction
|
288
|
+
end
|
289
|
+
|
290
|
+
def abort
|
291
|
+
fail Error::TransactionError, "not in transaction" unless @in_transaction
|
292
|
+
@abort = true
|
293
|
+
throw :finish_transaction
|
294
|
+
end
|
295
|
+
|
296
|
+
#######
|
297
|
+
private
|
298
|
+
#######
|
299
|
+
|
300
|
+
def try(func, *args)
|
301
|
+
options = args.last.is_a?(Hash) ? args.pop : { }
|
302
|
+
failure = options.fetch(:failure, false)
|
303
|
+
no_error = options.fetch(:no_error, { })
|
304
|
+
result = func == :read_from_func ?
|
305
|
+
C.read_from_func(args[0], @db, *args[1..-1]) :
|
306
|
+
C.send(func, @db, *args)
|
307
|
+
if (failure.is_a?(Proc) and failure[result]) or result == failure
|
308
|
+
error_code = C.ecode(@db)
|
309
|
+
if no_error.include? error_code
|
310
|
+
no_error[error_code]
|
311
|
+
else
|
312
|
+
error_message = C.errmsg(error_code)
|
313
|
+
fail Error::CabinetError, "#{error_message} (#{error_code})"
|
314
|
+
end
|
315
|
+
else
|
316
|
+
result
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def to_enum_int(str_or_int, name)
|
321
|
+
return str_or_int if str_or_int.is_a? Integer
|
322
|
+
const = "#{name.to_s.upcase}S"
|
323
|
+
names = self.class.const_get(const)
|
324
|
+
enum = C.const_get(const)
|
325
|
+
str_or_int.to_s.downcase.scan(/./m).inject(0) do |int, c|
|
326
|
+
if n = names[c]
|
327
|
+
int | enum[n]
|
328
|
+
else
|
329
|
+
warn "skipping unrecognized #{name}"
|
330
|
+
int
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module OklahomaMixer
|
2
|
+
module Utilities # :nodoc:
|
3
|
+
module FFIDSL # :nodoc:
|
4
|
+
def self.extended(ffi_interface)
|
5
|
+
ffi_interface.extend(FFI::Library)
|
6
|
+
ffi_interface.ffi_lib(
|
7
|
+
*Array(
|
8
|
+
ENV.fetch(
|
9
|
+
"TOKYO_CABINET_LIB",
|
10
|
+
Dir["/{opt,usr}/{,local/}lib{,64}/libtokyocabinet.{dylib,so*}"]
|
11
|
+
)
|
12
|
+
)
|
13
|
+
)
|
14
|
+
rescue LoadError
|
15
|
+
fail "Tokyo Cabinet could not be loaded " +
|
16
|
+
"(you can install it from http://1978th.net/ " +
|
17
|
+
"and set the TOKYO_CABINET_LIB environment variable to its path)"
|
18
|
+
end
|
19
|
+
|
20
|
+
def prefix(new_prefix = nil)
|
21
|
+
@prefix = new_prefix unless new_prefix.nil?
|
22
|
+
defined?(@prefix) and @prefix
|
23
|
+
end
|
24
|
+
|
25
|
+
def func(details)
|
26
|
+
args = [ ]
|
27
|
+
args << details.fetch(:alias, details[:name])
|
28
|
+
args << "#{prefix}#{details[:name]}".to_sym
|
29
|
+
args << Array(details[:args])
|
30
|
+
args << details.fetch(:returns, :void)
|
31
|
+
attach_function(*args)
|
32
|
+
end
|
33
|
+
|
34
|
+
def read_from_func(func, *args)
|
35
|
+
Utilities.temp_int do |size|
|
36
|
+
begin
|
37
|
+
args << size
|
38
|
+
pointer = send(func, *args)
|
39
|
+
pointer.address.zero? ? nil : pointer.get_bytes(0, size.get_int(0))
|
40
|
+
ensure
|
41
|
+
Utilities.free(pointer) if pointer
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def call(details)
|
47
|
+
args = [ ]
|
48
|
+
args << details[:name]
|
49
|
+
args << Array(details[:args])
|
50
|
+
args << details.fetch(:returns, :void)
|
51
|
+
callback(*args)
|
52
|
+
end
|
53
|
+
|
54
|
+
def def_new_and_del_funcs
|
55
|
+
func :name => :new,
|
56
|
+
:returns => :pointer
|
57
|
+
func :name => :del,
|
58
|
+
:args => :pointer
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
int_min = `getconf INT_MIN 2>&1`[/-\d+/]
|
63
|
+
unless INT_MIN = int_min && int_min.to_i
|
64
|
+
warn "set OKMixer::Utilities::INT_MIN before using counters"
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.temp_int
|
68
|
+
int = FFI::MemoryPointer.new(:int)
|
69
|
+
yield int
|
70
|
+
ensure
|
71
|
+
int.free if int
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.temp_xstr
|
75
|
+
xstr = ExtensibleString.new
|
76
|
+
yield xstr
|
77
|
+
ensure
|
78
|
+
xstr.free if xstr
|
79
|
+
end
|
80
|
+
|
81
|
+
extend FFIDSL
|
82
|
+
|
83
|
+
prefix :tc
|
84
|
+
|
85
|
+
func :name => :free,
|
86
|
+
:args => :pointer
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "ffi"
|
2
|
+
|
3
|
+
require "oklahoma_mixer/error"
|
4
|
+
require "oklahoma_mixer/utilities"
|
5
|
+
|
6
|
+
require "oklahoma_mixer/extensible_string/c"
|
7
|
+
require "oklahoma_mixer/extensible_string"
|
8
|
+
require "oklahoma_mixer/array_list/c"
|
9
|
+
require "oklahoma_mixer/array_list"
|
10
|
+
|
11
|
+
require "oklahoma_mixer/hash_database/c"
|
12
|
+
require "oklahoma_mixer/hash_database"
|
13
|
+
|
14
|
+
module OklahomaMixer
|
15
|
+
VERSION = "0.1.0"
|
16
|
+
|
17
|
+
def self.open(path, *args)
|
18
|
+
db = case File.extname(path).downcase
|
19
|
+
when ".tch" then HashDatabase.new(path, *args)
|
20
|
+
else fail ArgumentError, "unsupported database type"
|
21
|
+
end
|
22
|
+
if block_given?
|
23
|
+
begin
|
24
|
+
yield db
|
25
|
+
ensure
|
26
|
+
db.close
|
27
|
+
end
|
28
|
+
else
|
29
|
+
db
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
OKMixer = OklahomaMixer
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class TestBinaryData < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@db = hdb
|
6
|
+
@key = "Binary\0Name"
|
7
|
+
@value = "James\0Edward\0Gray\0II"
|
8
|
+
@db[@key] = @value
|
9
|
+
end
|
10
|
+
|
11
|
+
def teardown
|
12
|
+
@db.close
|
13
|
+
remove_db_files
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_keys_and_values_can_be_read_with_null_bytes
|
17
|
+
assert_equal(@value, @db[@key])
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_null_bytes_are_preservered_by_update_callback
|
21
|
+
@db.update(@key => "conflict") { |key, old_value, new_value| "new\0value" }
|
22
|
+
assert_equal("new\0value", @db[@key])
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_null_bytes_are_preserved_during_key_iteration
|
26
|
+
@db.each_key do |key|
|
27
|
+
assert_equal(@key, key)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_null_bytes_are_preserved_during_iteration
|
32
|
+
@db.each do |key, value|
|
33
|
+
assert_equal(@key, key)
|
34
|
+
assert_equal(@value, value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_null_bytes_are_preserved_by_key_listing
|
39
|
+
assert_equal([@key], @db.keys)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class TestFileSystem < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@db = hdb
|
6
|
+
end
|
7
|
+
|
8
|
+
def teardown
|
9
|
+
@db.close
|
10
|
+
remove_db_files
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_creating_a_hash_database_creates_the_corresponding_file
|
14
|
+
assert(File.exist?(db_path("tch")), "The HashDatabase file was not created")
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_path_returns_the_path_the_database_was_created_with
|
18
|
+
assert_equal(db_path("tch"), @db.path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_file_size_returns_the_size_of_the_contents_on_disk
|
22
|
+
@db[:data] = "X" * 1024
|
23
|
+
assert_operator(@db.file_size, :>, 1024)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_flush_and_aliases_force_contents_to_be_written_to_disk
|
27
|
+
@db[:data] = "X" * 1024
|
28
|
+
@db.flush
|
29
|
+
assert_operator(File.size(@db.path), :>, 1024)
|
30
|
+
@db[:more_data] = "X" * 1024
|
31
|
+
@db.sync
|
32
|
+
assert_operator(File.size(@db.path), :>, 2048)
|
33
|
+
@db[:even_more_data] = "X" * 1024
|
34
|
+
@db.fsync
|
35
|
+
assert_operator(File.size(@db.path), :>, 3072)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_copy_and_backup_create_a_backup_copy_of_the_database_file
|
39
|
+
@db[:data] = "X" * 1024
|
40
|
+
old_path = @db.path
|
41
|
+
old_ext = File.extname(old_path)
|
42
|
+
new_path = "#{File.basename(old_path, old_ext)}_2#{old_ext}"
|
43
|
+
new_path_2 = "#{File.basename(old_path, old_ext)}_3#{old_ext}"
|
44
|
+
assert(@db.copy(new_path), "Database could not be copied")
|
45
|
+
new_db = OKMixer::HashDatabase.new(new_path)
|
46
|
+
assert_equal(@db.to_a.sort, new_db.to_a.sort)
|
47
|
+
assert(@db.backup(new_path_2), "Database could not be copied")
|
48
|
+
new_db_2 = OKMixer::HashDatabase.new(new_path_2)
|
49
|
+
assert_equal(@db.to_a.sort, new_db_2.to_a.sort)
|
50
|
+
ensure
|
51
|
+
File.unlink(new_path) if new_path and File.exist? new_path
|
52
|
+
File.unlink(new_path_2) if new_path_2 and File.exist? new_path_2
|
53
|
+
new_db.close if new_db
|
54
|
+
new_db_2.close if new_db_2
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_defrag_removes_wholes_in_the_database_file
|
58
|
+
# load some data
|
59
|
+
data = "X" * 1024
|
60
|
+
100.times do |i|
|
61
|
+
@db[i] = data
|
62
|
+
end
|
63
|
+
# delete some data
|
64
|
+
(0...100).sort_by { rand }.first(10).each do |i|
|
65
|
+
@db.delete(i)
|
66
|
+
end
|
67
|
+
# push changes to disk
|
68
|
+
@db.flush
|
69
|
+
# defragment the database file
|
70
|
+
old_size = File.size(@db.path)
|
71
|
+
@db.defrag
|
72
|
+
assert_operator(old_size, :>, File.size(@db.path))
|
73
|
+
end
|
74
|
+
end
|