dbmlite3 1.0.0 → 2.0.0.pre.alpha.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +70 -19
- data/Rakefile +5 -4
- data/dbmlite3.gemspec +32 -10
- data/extras/benchmark.rb +172 -0
- data/lib/dbmlite3.rb +9 -949
- data/lib/internal_lite3/dbm.rb +542 -0
- data/lib/internal_lite3/error.rb +27 -0
- data/lib/internal_lite3/handle.rb +284 -0
- data/lib/internal_lite3/sql.rb +87 -0
- data/spec/dbmlite3_spec.rb +113 -72
- metadata +30 -29
- data/doc/Lite3/DBM.html +0 -2653
- data/doc/Lite3/Error.html +0 -135
- data/doc/Lite3/SQL.html +0 -390
- data/doc/Lite3.html +0 -117
- data/doc/_index.html +0 -152
- data/doc/class_list.html +0 -51
- data/doc/css/common.css +0 -1
- data/doc/css/full_list.css +0 -58
- data/doc/css/style.css +0 -496
- data/doc/file.README.html +0 -212
- data/doc/file_list.html +0 -56
- data/doc/frames.html +0 -17
- data/doc/index.html +0 -212
- data/doc/js/app.js +0 -314
- data/doc/js/full_list.js +0 -216
- data/doc/js/jquery.js +0 -4
- data/doc/method_list.html +0 -307
- data/doc/top-level-namespace.html +0 -110
@@ -0,0 +1,284 @@
|
|
1
|
+
|
2
|
+
require 'sequel'
|
3
|
+
|
4
|
+
require 'weakref'
|
5
|
+
|
6
|
+
module Lite3
|
7
|
+
|
8
|
+
# Wrapper around a Sequel::Database object.
|
9
|
+
#
|
10
|
+
# We do this instead of using them directly because transactions
|
11
|
+
# happen at the handle level rather than the file level and this
|
12
|
+
# lets us share the transaction across multiple tables in the same
|
13
|
+
# file.
|
14
|
+
#
|
15
|
+
# In addition, we can use this to transparently close and reopen the
|
16
|
+
# underlying database file when (e.g.) forking the process.
|
17
|
+
#
|
18
|
+
# Instances contain references to DBM objects using them. When the
|
19
|
+
# set becomes empty, the handle is closed; adding a reference will
|
20
|
+
# ensure the handle is open.
|
21
|
+
class Handle
|
22
|
+
attr_reader :path
|
23
|
+
def initialize(path)
|
24
|
+
@path = path
|
25
|
+
@db = open_db(path)
|
26
|
+
@refs = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def open_db(path)
|
32
|
+
return IS_JRUBY ?
|
33
|
+
Sequel.connect("jdbc:sqlite:#{path}") :
|
34
|
+
Sequel.sqlite(@path)
|
35
|
+
end
|
36
|
+
|
37
|
+
public
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
"<#{self.class}:0x#{object_id.to_s(16)} path=#{@path}>"
|
41
|
+
end
|
42
|
+
alias inspect to_s
|
43
|
+
|
44
|
+
|
45
|
+
#
|
46
|
+
# References to the DBM object(s) using this handle.
|
47
|
+
#
|
48
|
+
# References are weak. scrub_refs! will remove all reclaimed refs
|
49
|
+
# and close the handle if there are none left. (Note that this
|
50
|
+
# doesn't preclude us from reopening the handle later, though. We
|
51
|
+
# could keep Handles around longer if we want and reuse them, but we
|
52
|
+
# don't.)
|
53
|
+
#
|
54
|
+
|
55
|
+
def addref(parent)
|
56
|
+
@refs[parent.object_id] = WeakRef.new(parent)
|
57
|
+
end
|
58
|
+
|
59
|
+
def delref(parent)
|
60
|
+
@refs.delete(parent.object_id)
|
61
|
+
scrub_refs!
|
62
|
+
end
|
63
|
+
|
64
|
+
def scrub_refs!
|
65
|
+
@refs.delete_if{|k,v| ! v.weakref_alive? }
|
66
|
+
disconnect! if @refs.empty?
|
67
|
+
end
|
68
|
+
|
69
|
+
def live_refs
|
70
|
+
scrub_refs!
|
71
|
+
return @refs.size
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
#
|
76
|
+
# Opening and closing
|
77
|
+
#
|
78
|
+
|
79
|
+
# Disconnect the underlying database handle.
|
80
|
+
def disconnect!
|
81
|
+
@db.disconnect
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
#
|
86
|
+
# Transactions
|
87
|
+
#
|
88
|
+
|
89
|
+
# Perform &block in a transaction. See DBM.transaction.
|
90
|
+
def transaction(&block)
|
91
|
+
@db.transaction({}, &block)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Test if there is currently a transaction in progress
|
95
|
+
def transaction_active?
|
96
|
+
return @db.in_transaction?
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
#
|
102
|
+
# Table access; the common SQL idioms we care about. These all
|
103
|
+
# deal with tables of key/value pairs.
|
104
|
+
#
|
105
|
+
|
106
|
+
# Create a table of key-value pairs if it does not already exist.
|
107
|
+
def create_key_value_table(name)
|
108
|
+
@db.create_table?(name) do
|
109
|
+
String :key, primary_key: true
|
110
|
+
String :value
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Perform an upsert for the row with field 'key'
|
115
|
+
def upsert(table, key, value)
|
116
|
+
transaction {
|
117
|
+
recs = @db[table].where(key: key)
|
118
|
+
if recs.count == 0
|
119
|
+
@db[table].insert(key: key, value: value)
|
120
|
+
elsif recs.count == 1
|
121
|
+
recs.update(value: value)
|
122
|
+
else
|
123
|
+
raise InternalError.new("Duplicate entry for key '#{key}'")
|
124
|
+
end
|
125
|
+
}
|
126
|
+
|
127
|
+
return value
|
128
|
+
end
|
129
|
+
|
130
|
+
# Retrieve the 'value' field of the row with value 'key' in the given table.
|
131
|
+
def lookup(table, key)
|
132
|
+
row = @db[table].where(key:key).first
|
133
|
+
return nil unless row
|
134
|
+
|
135
|
+
return row[:value]
|
136
|
+
end
|
137
|
+
|
138
|
+
def clear_table(table)
|
139
|
+
@db[table].delete
|
140
|
+
end
|
141
|
+
|
142
|
+
def delete(table, key)
|
143
|
+
@db[table].where(key: key).delete
|
144
|
+
end
|
145
|
+
|
146
|
+
def get_size(table)
|
147
|
+
return @db[table].count
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
# Backend for `each`; evaluates `block` on each row in `table`
|
152
|
+
# with the undecoded key and value as arguments. It is *not* a
|
153
|
+
# single transaction.
|
154
|
+
#
|
155
|
+
# We do this instead of using `Dataset.each` because the latter is
|
156
|
+
# not guaranteed to be re-entrant.
|
157
|
+
#
|
158
|
+
# Each key/value pair is retrieved via a separate query so that it
|
159
|
+
# is safe to access the database from inside the block. Items are
|
160
|
+
# retrieved by rowid in increasing order. Since we preserve those,
|
161
|
+
# modifications done in the block (probably) won't break things.
|
162
|
+
#
|
163
|
+
# This is (probably) not very fast but it's (probably) good enough
|
164
|
+
# for most things.
|
165
|
+
def tbl_each(table, &block)
|
166
|
+
return if @db[table].count == 0
|
167
|
+
|
168
|
+
curr = -1
|
169
|
+
while true
|
170
|
+
row = @db[table].where{rowid > curr}
|
171
|
+
.limit(1)
|
172
|
+
.select(:rowid, :key, :value)
|
173
|
+
.first
|
174
|
+
|
175
|
+
return unless row
|
176
|
+
curr, key, value = *row.values
|
177
|
+
|
178
|
+
block.call(key, value)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Wrapper around Dataset.each, with all the ensuing limitations.
|
183
|
+
def tbl_each_fast(table, &block)
|
184
|
+
@db[table].each(&block)
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
|
192
|
+
#
|
193
|
+
# Private classes
|
194
|
+
#
|
195
|
+
|
196
|
+
# Dummy `Handle` that throws an `Error` exception whenever something
|
197
|
+
# tries to treat it as an open handle. This replaces a `DBM`'s
|
198
|
+
# `Handle` object when `DBM.close` is called so that the error
|
199
|
+
# message will be useful if something tries to access a closed
|
200
|
+
# handle.
|
201
|
+
class ClosedHandle
|
202
|
+
def initialize(filename, table)
|
203
|
+
@filename, @table = [filename, table]
|
204
|
+
end
|
205
|
+
|
206
|
+
# We clone the rest of Handle's interface with methods that throw
|
207
|
+
# an Error.
|
208
|
+
Handle.instance_methods(false).each { |name|
|
209
|
+
next if method_defined? name
|
210
|
+
define_method(name) { |*args|
|
211
|
+
raise Error.new("Use of closed database at #{@filename}/#{@table}")
|
212
|
+
}
|
213
|
+
}
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
# Module to manage the collection of active Handle objects. See the
|
218
|
+
# docs for `Lite3::SQL` for an overview; this module hold the actual
|
219
|
+
# code and data.
|
220
|
+
module HandlePool
|
221
|
+
@@handles = {} # The hash of `Handle` objects keyed by filename
|
222
|
+
|
223
|
+
# Retrieve the `Handle` associated with `filename`, creating it
|
224
|
+
# first if necessary. `filename` is normalized with
|
225
|
+
# `File.realpath` before using as a key and so is as good or bad
|
226
|
+
# as that for detecting an existing file.
|
227
|
+
def self.get(filename)
|
228
|
+
|
229
|
+
# Scrub @@handles of all inactive Handles
|
230
|
+
self.gc
|
231
|
+
|
232
|
+
# We need to convert the filename to a canonical
|
233
|
+
# form. `File.realpath` does this for us but only if the file
|
234
|
+
# exists. If not, we use it on the parent directory instead and
|
235
|
+
# use `File.join` to create the full path.
|
236
|
+
if File.exist?(filename)
|
237
|
+
File.file?(filename) or
|
238
|
+
raise Error.new("Filename '#{filename}' exists but is not a file.")
|
239
|
+
|
240
|
+
filename = File.realpath(filename)
|
241
|
+
else
|
242
|
+
dn = File.dirname(filename)
|
243
|
+
File.directory?(dn) or
|
244
|
+
raise Error.new("Parent directory '#{dn}' nonexistant or " +
|
245
|
+
"not a directory.")
|
246
|
+
|
247
|
+
filename = File.join(File.realpath(dn), File.basename(filename))
|
248
|
+
end
|
249
|
+
|
250
|
+
@@handles[filename] = Handle.new(filename) unless
|
251
|
+
@@handles.has_key?(filename)
|
252
|
+
|
253
|
+
return @@handles[filename]
|
254
|
+
end
|
255
|
+
|
256
|
+
# Close all underlying database connections.
|
257
|
+
def self.close_all
|
258
|
+
Sequel::DATABASES.each(&:disconnect)
|
259
|
+
end
|
260
|
+
|
261
|
+
# Close and remove all Handle objects with no refs and return a
|
262
|
+
# hash mapping the filename for each live Handle to the number of
|
263
|
+
# DBM objects that currently reference it. Does **NOT** perform a
|
264
|
+
# Ruby GC.
|
265
|
+
def self.gc
|
266
|
+
results = {}
|
267
|
+
@@handles.select!{|path, handle|
|
268
|
+
handle.scrub_refs!
|
269
|
+
|
270
|
+
if handle.live_refs == 0
|
271
|
+
@@handles.delete(path)
|
272
|
+
next false
|
273
|
+
end
|
274
|
+
|
275
|
+
results[path] = handle.live_refs
|
276
|
+
true
|
277
|
+
}
|
278
|
+
|
279
|
+
return results
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
private_constant :Handle, :ClosedHandle, :HandlePool
|
284
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
|
2
|
+
module Lite3
|
3
|
+
|
4
|
+
# This module provides some basic access to the underlying
|
5
|
+
# `Sequel::Database` objects used by `Lite3::DBM` to actually store
|
6
|
+
# and retrieve data.
|
7
|
+
#
|
8
|
+
# The only thing you need to care about is that if your process
|
9
|
+
# forks, you *must* invoke `Lite3::SQL.close_all` before forking the
|
10
|
+
# process. Otherwise, it will clone the connection and could lead
|
11
|
+
# to database corruption.
|
12
|
+
#
|
13
|
+
# More details:
|
14
|
+
#
|
15
|
+
# `Lite3` maintains a pool of private handle objects (private class
|
16
|
+
# `Lite3::Handle`) which in turn manage the `Sequel::Database`
|
17
|
+
# objects that actually do the work. There is one handle per
|
18
|
+
# SQLite3 database file; since each `DBM` represents one table in a
|
19
|
+
# SQLite3 file, multiple `DBM` objects will use the same handle.
|
20
|
+
#
|
21
|
+
# Handle objects can themselves close and replace their
|
22
|
+
# `Sequel::Database` objects transparently.
|
23
|
+
#
|
24
|
+
# The underlying system keeps track of which `DBM` objects reference
|
25
|
+
# which files and will close a file's `Sequel::Database` when all
|
26
|
+
# of the `DBM`s using it have been closed. (It does **not** handle
|
27
|
+
# the case where a `DBM` object remains open and goes out of scope;
|
28
|
+
# that object will be kept around for the life of the process.)
|
29
|
+
#
|
30
|
+
# Mostly, you don't need to care about this. However, it affects
|
31
|
+
# you in two ways:
|
32
|
+
#
|
33
|
+
# 1. Transactions are done at the file level and not the table level.
|
34
|
+
# This means that you can access separate tables in the same
|
35
|
+
# transaction, which is a Very Good Thing.
|
36
|
+
#
|
37
|
+
# 2. You can safely fork the current process and keep using existing
|
38
|
+
# `DBM` objects in both processes, provided you've invoked
|
39
|
+
# `close_all` before the fork. This will have closed the actual
|
40
|
+
# database handles (which can't tolerate being carried across a
|
41
|
+
# fork) and opens new ones the next time they're needed.
|
42
|
+
#
|
43
|
+
# If you find yourself needing to be sure that you don't have any
|
44
|
+
# unexpected open file handles (e.g. before forking or if you need
|
45
|
+
# Windows to unlock it), you should call `close_all`.
|
46
|
+
#
|
47
|
+
# Otherwise, it's safe to ignore this stuff.
|
48
|
+
|
49
|
+
|
50
|
+
# This module provides some basic, consistent access to the
|
51
|
+
# underlying database library(es) (currently `sequel`).
|
52
|
+
module SQL
|
53
|
+
|
54
|
+
# Tests if the underlying database libraries are threadsafe.
|
55
|
+
#
|
56
|
+
# (Currently, it always returns true, since Sequel does that for
|
57
|
+
# us.)
|
58
|
+
def self.threadsafe?
|
59
|
+
return true
|
60
|
+
end
|
61
|
+
|
62
|
+
# Disconnect and delete all database handles and associated
|
63
|
+
# metadata that are no longer needed (i.e. because their
|
64
|
+
# corresponding `DBM`s have been closed or reclaimed).
|
65
|
+
#
|
66
|
+
# Returns a hash mapping the path to each open database file to
|
67
|
+
# the number of live DBM objects referencing it.
|
68
|
+
#
|
69
|
+
# You normally won't need to explicitly call this, but it's
|
70
|
+
# useful for testing and debugging.
|
71
|
+
def self.gc() return HandlePool.gc; end
|
72
|
+
|
73
|
+
# Close and remove the underlying database connections. This does
|
74
|
+
# not invalidate existing `Lite3::DBM` objects; they will recreate
|
75
|
+
# the connections when needed.
|
76
|
+
#
|
77
|
+
# The main use for this is for safely forking the current process.
|
78
|
+
# You should call this just before each `fork` to avoid potential
|
79
|
+
# corruption from duplicated database handles.
|
80
|
+
#
|
81
|
+
# This **should not** be called while a database operation is in
|
82
|
+
# progress. (E.g. do **not** call this from the block of
|
83
|
+
# `DBM.each`.)
|
84
|
+
def self.close_all() return HandlePool.close_all end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
data/spec/dbmlite3_spec.rb
CHANGED
@@ -212,9 +212,11 @@ Serializations = Set.new
|
|
212
212
|
it "implements each_*" do
|
213
213
|
db = newdb.call(Tmp.file, "floop")
|
214
214
|
|
215
|
+
# Empty DBMs don't evaluate their bloc
|
215
216
|
count = 0
|
216
217
|
db.each {|k,v| count += 1}
|
217
218
|
db.each_pair {|k,v| count += 1}
|
219
|
+
expect( count ) .to eq 0
|
218
220
|
|
219
221
|
|
220
222
|
db["foo"] = 42
|
@@ -236,6 +238,64 @@ Serializations = Set.new
|
|
236
238
|
db.close
|
237
239
|
end
|
238
240
|
|
241
|
+
it "allows database modification in `each`" do
|
242
|
+
db = newdb.call(Tmp.file, "floop")
|
243
|
+
|
244
|
+
db["foo"] = 42
|
245
|
+
db["bar"] = 99
|
246
|
+
db["quux"] = 123
|
247
|
+
db["baz"] = 999
|
248
|
+
|
249
|
+
count = 0
|
250
|
+
db.each do|key, value|
|
251
|
+
count += 1
|
252
|
+
|
253
|
+
case count
|
254
|
+
when 1
|
255
|
+
expect(key) .to eq "foo"
|
256
|
+
expect(value) .to eq 42
|
257
|
+
db['foo'] = 'new_foo'
|
258
|
+
|
259
|
+
when 2
|
260
|
+
expect(key) .to eq "bar"
|
261
|
+
expect(value) .to eq 99
|
262
|
+
|
263
|
+
db['baz'] = "new_baz"
|
264
|
+
|
265
|
+
expect(db['foo']) .to eq "new_foo"
|
266
|
+
|
267
|
+
db.delete("quux")
|
268
|
+
|
269
|
+
when 3
|
270
|
+
expect(key) .to eq "baz"
|
271
|
+
expect(value) .to eq "new_baz"
|
272
|
+
|
273
|
+
when 4
|
274
|
+
fail "there should not be 4 items!"
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
expect(count) .to eq 3
|
279
|
+
|
280
|
+
db.close
|
281
|
+
end
|
282
|
+
|
283
|
+
it "implements fast_each" do
|
284
|
+
db = newdb.call(Tmp.file, "floop")
|
285
|
+
|
286
|
+
db["foo"] = 42
|
287
|
+
db["bar"] = 99
|
288
|
+
db["quux"] = 123
|
289
|
+
|
290
|
+
expected = []
|
291
|
+
db.fast_each{|key, value| expected.push [key, value]}
|
292
|
+
|
293
|
+
expect(expected) .to eq [ ["foo", 42], ["bar", 99], ["quux", 123]]
|
294
|
+
|
295
|
+
db.close
|
296
|
+
end
|
297
|
+
|
298
|
+
|
239
299
|
it "deletes items from the table" do
|
240
300
|
db = newdb.call(Tmp.file, "floop")
|
241
301
|
|
@@ -792,7 +852,7 @@ describe Lite3::DBM do
|
|
792
852
|
end
|
793
853
|
|
794
854
|
it "keeps most of its names private" do
|
795
|
-
expect( Lite3.constants.to_set ) .to eq %i{SQL DBM Error}.to_set
|
855
|
+
expect( Lite3.constants.to_set ) .to eq %i{SQL DBM Error InternalError}.to_set
|
796
856
|
end
|
797
857
|
end
|
798
858
|
|
@@ -816,15 +876,6 @@ describe Lite3::SQL do
|
|
816
876
|
db
|
817
877
|
}
|
818
878
|
|
819
|
-
# it "manages a pool of DB handles that should now all be closed." do
|
820
|
-
# # If this fails, it (probably) means the previous tests didn't
|
821
|
-
# # clean up after themselves.
|
822
|
-
# GC.start
|
823
|
-
# expect( Lite3::SQL.gc.empty? ) .to be true
|
824
|
-
|
825
|
-
# Lite3::SQL.close_all # smoketest
|
826
|
-
# end
|
827
|
-
|
828
879
|
it "lets you close the actual handle without impeding database use" do
|
829
880
|
expect( Lite3::SQL.gc.size ) .to eq 0
|
830
881
|
|
@@ -840,72 +891,19 @@ describe Lite3::SQL do
|
|
840
891
|
|
841
892
|
# Referencing DBM objects should be db1 and db2
|
842
893
|
path, refs = stats.to_a[0]
|
894
|
+
expect( path ) .to eq file
|
895
|
+
expect( refs ) .to eq 2
|
843
896
|
|
844
|
-
|
845
|
-
|
846
|
-
expect( refs.include?(db2) ) .to be true
|
847
|
-
|
848
|
-
# Underlying handles should be open
|
849
|
-
expect( db1.handle_closed? ) .to be false
|
850
|
-
expect( db2.handle_closed? ) .to be false
|
897
|
+
# We can no longer test if the underlying file handles are still
|
898
|
+
# open, so we don't.
|
851
899
|
|
852
|
-
# Test closing it
|
900
|
+
# Test closing it and forcing a re-open
|
853
901
|
Lite3::SQL.close_all
|
854
|
-
expect( db1.handle_closed? ) .to be true
|
855
|
-
expect( db2.handle_closed? ) .to be true
|
856
|
-
|
857
|
-
# Test auto-opening them.
|
858
902
|
expect( db1["foo"] ) .to eq vv["foo"]
|
859
|
-
expect( db1.handle_closed? ) .to be false
|
860
|
-
expect( db2.handle_closed? ) .to be false
|
861
903
|
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
expect( Lite3::SQL.gc.keys.size ) .to eq 0
|
866
|
-
end
|
867
|
-
|
868
|
-
# it "(eventually) closes handles that have gone out of scope" do
|
869
|
-
# expect( Lite3::SQL.gc.keys.size ) .to eq 0
|
870
|
-
|
871
|
-
# file = Tmp.file
|
872
|
-
# db1 = newbasic.call(file, "first")
|
873
|
-
|
874
|
-
# expect( db1.handle_closed? ) .to be false
|
875
|
-
# expect( Lite3::SQL.gc.keys.size ) .to eq 1
|
876
|
-
|
877
|
-
# db1 = nil
|
878
|
-
# GC.start
|
879
|
-
# expect( Lite3::SQL.gc.keys.size ) .to eq 0
|
880
|
-
# end
|
881
|
-
|
882
|
-
it "does close_all with multiple files" do
|
883
|
-
db1 = newbasic.call(Tmp.file, "first")
|
884
|
-
db2 = newbasic.call(Tmp.file, "second")
|
885
|
-
|
886
|
-
# The above should be using the same handle, which is currently
|
887
|
-
# open.
|
888
|
-
|
889
|
-
stats = Lite3::SQL.gc
|
890
|
-
expect( stats.keys.size ) .to eq 2
|
891
|
-
|
892
|
-
all_refs = stats.values.flatten
|
893
|
-
expect( all_refs.include?(db1) ) .to be true
|
894
|
-
expect( all_refs.include?(db2) ) .to be true
|
895
|
-
|
896
|
-
# Underlying handles should be open
|
897
|
-
expect( db1.handle_closed? ) .to be false
|
898
|
-
expect( db2.handle_closed? ) .to be false
|
899
|
-
|
900
|
-
# Test closing it
|
901
|
-
Lite3::SQL.close_all
|
902
|
-
expect( db1.handle_closed? ) .to be true
|
903
|
-
expect( db2.handle_closed? ) .to be true
|
904
|
-
|
905
|
-
# Test auto-opening them.
|
904
|
+
# Repeat, but this time use the underlying lib
|
905
|
+
Sequel::DATABASES.each(&:disconnect)
|
906
906
|
expect( db1["foo"] ) .to eq vv["foo"]
|
907
|
-
expect( db1.handle_closed? ) .to be false
|
908
|
-
expect( db2.handle_closed? ) .to be true
|
909
907
|
|
910
908
|
db1.close
|
911
909
|
db2.close
|
@@ -913,8 +911,7 @@ describe Lite3::SQL do
|
|
913
911
|
expect( Lite3::SQL.gc.keys.size ) .to eq 0
|
914
912
|
end
|
915
913
|
|
916
|
-
|
917
|
-
it "allows multipe table accesses in the same transaction" do
|
914
|
+
it "allows multiple table accesses in the same transaction" do
|
918
915
|
file = Tmp.file
|
919
916
|
db1 = newbasic.call(file, "first")
|
920
917
|
db2 = Lite3::DBM.new(file, "second")
|
@@ -964,6 +961,50 @@ describe Lite3::SQL do
|
|
964
961
|
expect{ db1.size } .to raise_error Lite3::Error
|
965
962
|
expect{ db1.to_a } .to raise_error Lite3::Error
|
966
963
|
end
|
964
|
+
|
965
|
+
it "finalizes DBM objects that have gone out of scope." do
|
966
|
+
|
967
|
+
# This is really difficult to test because there's no reliable way
|
968
|
+
# to get the garbage collector to clean up when we want it to. As
|
969
|
+
# such, we make the attempt and skip with a warning if db2 hasn't
|
970
|
+
# been reclaimed.
|
971
|
+
#
|
972
|
+
# (Dropping into the debugger after GC.start seems to help.)
|
973
|
+
|
974
|
+
|
975
|
+
file = Tmp.file
|
976
|
+
db1 = newbasic.call(file, "first")
|
977
|
+
db2 = Lite3::DBM.new(file, "second")
|
978
|
+
|
979
|
+
# Two DBMs are currently open.
|
980
|
+
stats = Lite3::SQL.gc
|
981
|
+
expect( stats.size ) .to be 1
|
982
|
+
expect( stats.values[0] ) .to be 2
|
983
|
+
|
984
|
+
# Make db2 a weak reference so it goes out of scope after a GC.
|
985
|
+
# It's possible that a GC redesign will this.
|
986
|
+
db2 = WeakRef.new(db2)
|
987
|
+
GC.start
|
988
|
+
|
989
|
+
# Uncommenting the next line and then resuming seems to work:
|
990
|
+
#byebug
|
991
|
+
|
992
|
+
if db2.weakref_alive?
|
993
|
+
db2.close
|
994
|
+
db1.close
|
995
|
+
skip "GC has't reclaimed the handle; bailing."
|
996
|
+
end
|
997
|
+
|
998
|
+
# There should now be exactly 1 open DBM
|
999
|
+
stats = Lite3::SQL.gc
|
1000
|
+
expect( stats.size ) .to be 1
|
1001
|
+
expect( stats.values[0] ) .to be 1
|
1002
|
+
|
1003
|
+
db1.close
|
1004
|
+
end
|
1005
|
+
|
1006
|
+
|
1007
|
+
|
967
1008
|
end
|
968
1009
|
|
969
1010
|
|