dbmlite3 1.0.0 → 2.0.0.pre.alpha.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fdf8e20ae5e85f35d83d1d5e6926864e130614b292f8003a2b895e0c15f10e2
4
- data.tar.gz: beb87e9421bcfdb2fb14caaa7a56b27e88b5c8f61239b28ed083477a15431e11
3
+ metadata.gz: dc6777e1faeaa79a0ec9b34c5bd5d12cc7568949eea71ae18845f8ff24045796
4
+ data.tar.gz: 909bc62a04f0585355b867af912660b65926665ebbb9b69404d7d461aea6b5d4
5
5
  SHA512:
6
- metadata.gz: 715dcf9851a5a95f301560798aeba2216fdebf9fc20f4fa993d586b3d45b47f6b2a9ad3610e648e7afec685a232d202e13e35cb5eb316bf6e8499b2869379a36
7
- data.tar.gz: b77d404a72410cdbd29bca42fdd7ca7d4490858e2ea80b0ce681438b347d8ab6e768134f4793743d14137d6025cc882aade4872591aa0e85b0a8466e0fea08ae
6
+ metadata.gz: d388662969b8f9cf2d44fe697ee54bd0f58e1feba77bfafeb0061341c03b048d05fa6e877cedf56cc545e98678f02016142a0b1ccfae477abce2ccd53ebbe787
7
+ data.tar.gz: 98ecd13107c15f3db41b0093c4acdfb81f23caa2424d5163ddd2d7e626e08b5cedf7cefb01589ec19717ba14275b94d6825fb87525b7bf270104eca14f5e147c
data/README.md CHANGED
@@ -23,7 +23,7 @@ settings = Lite3::DBM.new("config.sqlite3", "settings")
23
23
 
24
24
  # You use it like a hash
25
25
  settings["speed"] = 88
26
- settings["date"] = Date.new(1955, 11, 5) # Normal Ruby values are allowed
26
+ settings["date"] = Date.new(1955, 11, 5) # Most Ruby types are allowed
27
27
  settings["power_threshold"] = 2.2
28
28
 
29
29
  puts settings['power_threshold']
@@ -65,7 +65,7 @@ Alternately, you can fetch the source code from GitLab and build it yourself:
65
65
  $ cd dbmlite3
66
66
  $ rake
67
67
 
68
- Obviously, it depends on the gem `sqlite3`.
68
+ It depends on the gem `sequel`; previously, it used `sqlite3`.
69
69
 
70
70
  ## Quirks and Hints
71
71
 
@@ -95,6 +95,30 @@ read-modify-write cycle in a transaction:
95
95
  Or, of course, you could just design your script or program so that
96
96
  only one program accesses the table at a time.
97
97
 
98
+ ### Keys must be strings
99
+
100
+ While values may be any serializable type, keys *must* be strings. As
101
+ a special exception, Symbols are also allowed but are transparently
102
+ converted to Strings first. This means that while something like this
103
+ will work:
104
+
105
+ db[:foo] = 42
106
+
107
+ a subseqent
108
+
109
+ db.keys.include?(:foo) or raise AbjectFailure.new
110
+
111
+ will raise an exception because the key `:foo` was turned into a
112
+ string before being used. You will need to do this instead:
113
+
114
+ db.keys.include?('foo') or raise AbjectFailure.new
115
+
116
+ However, this
117
+
118
+ db.has_key?(:foo)
119
+
120
+ will work because `has_key?` does the conversion for us.
121
+
98
122
 
99
123
  ### Transactions and performance
100
124
 
@@ -107,9 +131,9 @@ do these in batches in one or more transactions.
107
131
  `Lite3::DBM` stores Ruby data by first serializing values using the
108
132
  `Marshal` or `Psych` modules. This can pose a security risk if an
109
133
  untrusted third party has direct access to the underlying SQLite3
110
- database. This tends to be pretty rare for most use-cases but if it
111
- is a concern, you can always configure `Lite3::DBM` to store its
112
- values as plain strings.
134
+ database. This tends to be pretty rare most of the time but if it is
135
+ a concern, you can always configure `Lite3::DBM` to store its values
136
+ as plain strings.
113
137
 
114
138
  ### Forking safely
115
139
 
@@ -125,20 +149,47 @@ also lets the child and parent use the same `Lite3::DBM` objects.
125
149
 
126
150
  ### `Lite3::DBM` objects act like file handles but are not
127
151
 
128
- While it is generally safe to treat `Lite3::DBM` as a wrapper
129
- around file handle (i.e. `open` and `close` work as expected), you
130
- should be aware that this is not precisely the way things
131
- actually work. Instead, the gem maintains a pool of database
132
- handles, one per file, and associates them with `Lite3::DBM`
133
- instances as needed. This is necessary for transactions to work
134
- correctly.
135
-
136
- See the reference doc for `Lite3::SQL` for more details.
137
-
138
- Mostly, you don't need to worry about this but certain types of
139
- bugs could behave in unexpected ways and knowing this may help you
140
- make sense of them.
141
-
152
+ While it is generally safe to treat `Lite3::DBM` as a wrapper around a
153
+ file handle (i.e. `open` and `close` work as expected), you should be
154
+ aware that this is not precisely the way things actually work.
155
+ Instead, the gem maintains a pool of database handles, one per file,
156
+ and associates them with `Lite3::DBM` instances as needed. This is
157
+ necessary for transactions to work correctly.
158
+
159
+ Mostly, you don't need to care about this. However, it affects
160
+ you in the following ways:
161
+
162
+ 1. Transactions are done at the file level and not the table level.
163
+ This means that you can access separate tables in the same
164
+ transaction, which is a Very Good Thing.
165
+
166
+ 2. You can safely fork the current process and keep using existing
167
+ `DBM` objects in both processes, provided you've invoked
168
+ `Lite3::SQL.close_all` before the fork. This will have closed the
169
+ actual database handles (which can't tolerate being carried across
170
+ a fork) and opens new ones the next time they're needed.
171
+
172
+ `DBM` objects that go out of scope without first being closed **will**
173
+ eventually have their underlying resources cleaned up. However, given
174
+ that *when* when that happens depends on the vagaries of the garbage
175
+ collector and various library internals, it's almost always a bad idea
176
+ to not explicitly call `close` first.
177
+
178
+ ### Under the hood
179
+
180
+ Currently, `Lite3::DBM` uses [Sequel](https://sequel.jeremyevans.net)
181
+ to access the `sqlite3` library. On JRuby, it goes through the `jdbc`
182
+ interface. The previous version (1.0.0) used
183
+ [sqlite3](https://github.com/sparklemotion/sqlite3-ruby) and only
184
+ worked on MRI. However, you should make no assumptions about the
185
+ underlying database libraries this gem uses. It may change in a
186
+ future release.
187
+
188
+ All tables created by `Lite3::DBM` will have names beginning with
189
+ `dbmlite3_` and you should not modify them directly. It **might** be
190
+ safe to put other tables in the same database file (e.g. via `Sequel`)
191
+ provided that you don't make global changes or mix transactions across
192
+ interfaces. However, I make no guarantees.
142
193
 
143
194
 
144
195
 
data/Rakefile CHANGED
@@ -7,15 +7,16 @@ RSpec::Core::RakeTask.new(:test) do |t|
7
7
  end
8
8
 
9
9
  YARD::Rake::YardocTask.new(:docs_via_yard) do |t|
10
- t.files = ['lib/*.rb']
10
+ t.files = ['lib/*.rb', 'lib/internal_lite3/*.rb']
11
11
  end
12
12
 
13
- task :gem do
14
- `gem build dbmlite3`
13
+ task :gem => [:doc] do
14
+ sh "gem build dbmlite3"
15
+ sh "JRUBY_GEM=yes gem build dbmlite3"
15
16
  end
16
17
 
17
18
  task :clean do
18
- gems = Dir.glob("dbmlite3-*.gem")
19
+ gems = Dir.glob("dbmlite3*.gem")
19
20
  rm gems if gems.size > 0
20
21
  rm_rf "doc"
21
22
  end
data/dbmlite3.gemspec CHANGED
@@ -1,10 +1,25 @@
1
1
 
2
+ JRUBY_GEM = (ENV['JRUBY_GEM'] == "yes")
3
+
4
+ if JRUBY_GEM
5
+ SUFFIX = '_jruby'
6
+ DESC_EXTRA = <<EOF
7
+
8
+ This is the JRuby version of dbmlite3; it is identical to the MRI
9
+ version except for its dependencies.
10
+ EOF
11
+ else
12
+ SUFFIX = ''
13
+ DESC_EXTRA = ''
14
+ end
15
+
16
+
2
17
  Gem::Specification.new do |s|
3
- s.name = 'dbmlite3'
4
- s.version = '1.0.0'
18
+ s.name = "dbmlite3#{SUFFIX}"
19
+ s.version = '2.0.0-alpha.3'
5
20
  s.date = '2022-02-21'
6
21
  s.summary = "A DBM-style key-value store using SQLite3"
7
- s.description = <<-EOF
22
+ s.description = <<-EOF + DESC_EXTRA
8
23
  Lite3::DBM is an object that behaves like a Ruby Hash but stores
9
24
  its data in a SQLite3 database table. It is a drop-in replacement
10
25
  for DBM.
@@ -19,15 +34,22 @@ EOF
19
34
 
20
35
  # I'm just going to add everything so that if you've got the gem,
21
36
  # you've also got the source distribution. Yay! Open source!
22
- s.files = ["README.md", "LICENSE.txt", "dbmlite3.gemspec",
23
- "Rakefile", ".yardopts"] +
24
- Dir.glob('doc/**/*') +
25
- Dir.glob('{spec,lib}/*.rb')
37
+ s.files = `git ls-files`
38
+ .split
39
+ .reject {|f| f =~ /\.org$/} # Skip local developer notes
26
40
 
27
- s.required_ruby_version = '>= 2.2.0'
28
- s.requirements << "sqlite3 gem, Ruby MRI (required by sqlite3)"
41
+ s.required_ruby_version = '>= 2.7.0'
42
+ s.requirements << (JRUBY_GEM ?
43
+ "Sequel, jdbc-sqlite3, JRuby" :
44
+ "Sequel, sqlite3, Ruby MRI")
45
+
46
+ s.add_runtime_dependency "sequel", '~> 5.65.0'
29
47
 
30
- s.add_runtime_dependency "sqlite3", '~> 1.4'
48
+ if JRUBY_GEM
49
+ s.add_runtime_dependency "jdbc-sqlite3", "~> 3.32.3.3"
50
+ else
51
+ s.add_runtime_dependency "sqlite3", "~> 1.6.1"
52
+ end
31
53
 
32
54
  s.add_development_dependency "rspec", '~> 3.10', '>= 3.10.0'
33
55
  s.add_development_dependency "yard", '~> 0.9.25', '>= 0.9.25'
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Low-effort benchmark comparing Lite3::DBM in various modes against
4
+ # other equivalent Hash-like Ruby storage mechanisms.
5
+
6
+ require 'dbmlite3'
7
+ require 'yaml/dbm' unless RUBY_PLATFORM == "java"
8
+ require 'fileutils'
9
+ require 'optparse'
10
+
11
+ COUNT = 10000
12
+
13
+ Opts = proc do
14
+ opt_defaults = {
15
+ count: COUNT,
16
+ transaction_only: true,
17
+ }
18
+
19
+ opts = Struct.new(*opt_defaults.keys).new(*opt_defaults.values)
20
+
21
+ OptionParser.new do |opo|
22
+ opo.banner = "Usage: bench_1.rb [options]"
23
+
24
+ opo.on("-t", "--multi-transaction",
25
+ "Don't batch Lite3 accesses in one big transaction.") {
26
+ opts.transaction_only = false
27
+ }
28
+
29
+ opo.on("-c", "--count N", Integer, "Set test count.") { |c|
30
+ opts.count = c
31
+ }
32
+ end.parse!
33
+
34
+ next opts
35
+ end.call
36
+
37
+
38
+ module Tmp
39
+ @root = File.join( File.dirname(__FILE__), "tmpdata")
40
+ @count = 0
41
+
42
+ def self.file
43
+ FileUtils.mkdir(@root) unless File.directory?(@root)
44
+
45
+ file = "testfile_#{@count}_#{$$}.sqlite3"
46
+ @count += 1
47
+
48
+ return File.join(@root, file)
49
+ end
50
+
51
+ def self.cleanup
52
+ return unless File.directory?(@root)
53
+ FileUtils.rm_rf(@root)
54
+ end
55
+ end
56
+
57
+
58
+
59
+ def insert(count, db, offset)
60
+ for n in 0 .. count
61
+ db["k_#{n}"] = "#{n + offset}"
62
+ end
63
+ end
64
+
65
+
66
+ def lookup(count, db)
67
+ rnd = Random.new(69_420)
68
+ sz = db.size
69
+
70
+ for _ in 0 .. count * 3
71
+ idx = rnd.rand(sz)
72
+ v = db["k_#{idx}"]
73
+ raise "Invalid value: #{v}" unless v.to_s == "#{idx + 1}"
74
+ end
75
+ end
76
+
77
+
78
+ Times = {}
79
+
80
+ def time_it(type, task, db, use_tr, &block)
81
+ print "#{type} #{task} - "
82
+ STDOUT.flush
83
+
84
+ start = Time.now
85
+
86
+ if use_tr && db.respond_to?(:transaction)
87
+ db.transaction { block.call }
88
+ else
89
+ block.call
90
+ end
91
+
92
+ finish = Time.now
93
+
94
+ elapsed = (finish - start).to_f
95
+ Times[type] = Times.fetch(type, 0) + elapsed
96
+
97
+ puts "#{elapsed.round(4)}"
98
+ end
99
+
100
+ def bench(count, desc, db, use_tr)
101
+ time_it(desc, "insert", db, use_tr) {
102
+ insert(count, db, 0)
103
+ }
104
+ time_it(desc, "upsert", db, use_tr) {
105
+ insert(count, db, 1)
106
+ }
107
+ time_it(desc, "lookup", db, use_tr) {
108
+ lookup(count, db)
109
+ }
110
+ time_it(desc, "delete_if", db, use_tr) {
111
+ rnd = Random.new(69_420)
112
+ db.delete_if{|k,v| rnd.rand(2) == 0}
113
+ }
114
+ puts
115
+ end
116
+
117
+
118
+
119
+ def main
120
+ puts "Count = #{Opts.count}\n"
121
+
122
+ Tmp.cleanup
123
+
124
+ bench(Opts.count, "hash", {}, false)
125
+
126
+ if RUBY_PLATFORM != "java"
127
+ DBM.open(Tmp.file) {|dbm|
128
+ bench(Opts.count, "DBM", dbm, false)
129
+ }
130
+
131
+ YAML::DBM.open(Tmp.file) {|dbm|
132
+ bench(Opts.count, "YAML::DBM", dbm, false)
133
+ }
134
+ end
135
+
136
+ Lite3::DBM.open(Tmp.file, "benchmark", :yaml) { |dbm|
137
+ bench(Opts.count, "Lite3::DBM(yaml)", dbm, true)
138
+ }
139
+
140
+ Lite3::DBM.open(Tmp.file, "benchmark", :marshal) { |dbm|
141
+ bench(Opts.count, "Lite3::DBM(marshal)", dbm, true)
142
+ }
143
+
144
+ Lite3::DBM.open(Tmp.file, "benchmark", :string) { |dbm|
145
+ bench(Opts.count, "Lite3::DBM(string)", dbm, true)
146
+ }
147
+
148
+ if !Opts.transaction_only
149
+ Lite3::DBM.open(Tmp.file, "benchmark", :yaml) { |dbm|
150
+ bench(Opts.count, "Lite3::DBM(yaml, single-trans)", dbm, false)
151
+ }
152
+
153
+ Lite3::DBM.open(Tmp.file, "benchmark", :marshal) { |dbm|
154
+ bench(Opts.count, "Lite3::DBM(marshal, single-trans)", dbm, false)
155
+ }
156
+
157
+ Lite3::DBM.open(Tmp.file, "benchmark", :string) { |dbm|
158
+ bench(Opts.count, "Lite3::DBM(string, single-trans)", dbm, false)
159
+ }
160
+ end
161
+
162
+
163
+ puts
164
+ puts "Totals:"
165
+ Times.each{|k,v|
166
+ puts " #{k} - #{v.round(4)}"
167
+ }
168
+
169
+ Tmp.cleanup
170
+ end
171
+
172
+ main