lmdb 0.1.0

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/lib/lmdb.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'lmdb_ext'
2
+
3
+ module LMDB
4
+
5
+ # @see LMDB::Environment#new
6
+ def self.new(*a, &b)
7
+ LMDB::Environment.new(*a, &b)
8
+ end
9
+
10
+ end
11
+
12
+ %w|environment database|.each do |name|
13
+ require "lmdb/#{name}"
14
+ end
@@ -0,0 +1,66 @@
1
+ class LMDB::Database
2
+
3
+ class << self
4
+ private :new
5
+ end
6
+
7
+ # @attr_reader [String] name
8
+ attr_reader :name
9
+
10
+ # @attr_reader [LMDB::Environment] env
11
+ attr_reader :env
12
+
13
+ # @param [LMDB::Environment] env
14
+ # @param [String] name the DB name
15
+ def initialize(env, name)
16
+ @env = env
17
+ @name = name.to_s
18
+ end
19
+
20
+ # Closes the DB, doesn't fail if already closed
21
+ def close
22
+ raw.close if loaded?
23
+ end
24
+
25
+ # Reads a key
26
+ # @param [String] key
27
+ def get(key)
28
+ ensure_db!
29
+ rtxn {|txn| raw.get(txn, key) }
30
+ rescue LMDB::Ext::Error::NOTFOUND
31
+ end
32
+ alias_method :[], :get
33
+
34
+ # Sets a key to a new value
35
+ # @param [String] key
36
+ # @param [String] value
37
+ def set(key, value)
38
+ ensure_db!
39
+ rtxn {|txn| raw.put(txn, key, value) }
40
+ end
41
+ alias_method :put, :set
42
+ alias_method :[]=, :set
43
+
44
+ private
45
+
46
+ def raw
47
+ @raw ||= rtxn {|txn| renv.open(txn, name, LMDB::CREATE) }
48
+ end
49
+
50
+ def renv
51
+ @renv ||= env.send(:raw)
52
+ end
53
+
54
+ def rtxn(&block)
55
+ env.send(:rtxn, &block)
56
+ end
57
+
58
+ def loaded?
59
+ !!@raw
60
+ end
61
+
62
+ def ensure_db!
63
+ raw unless loaded?
64
+ end
65
+
66
+ end
@@ -0,0 +1,135 @@
1
+ class LMDB::Environment
2
+
3
+ DEFAULT_OPTS = {
4
+ path: ".",
5
+ mode: 0755,
6
+ max_size: (2**28-1), # 4G
7
+ # max_size: 1_000_000,
8
+ max_dbs: 16,
9
+ max_readers: 126,
10
+ sub_dirs: true,
11
+ read_only: false,
12
+ mmap: false,
13
+ sync: true,
14
+ meta_sync: false
15
+ }.freeze
16
+
17
+ INFO_MAP = {
18
+ mapaddr: :address,
19
+ mapsize: :max_size,
20
+ last_pgno: :last_page,
21
+ last_txnid: :last_transaction_id,
22
+ maxreaders: :maxreaders,
23
+ numreaders: :readers
24
+ }.freeze
25
+
26
+ STAT_MAP = {
27
+ psize: :page_size,
28
+ depth: :depth,
29
+ branch_pages: :branch_pages,
30
+ leaf_pages: :leaf_pages,
31
+ overflow_pages: :overflow_pages,
32
+ entries: :entries
33
+ }.freeze
34
+
35
+ # @attr_reader [String] path environment path
36
+ attr_reader :path
37
+
38
+ # Constructor, opens an environment
39
+ #
40
+ # @param [Hash] opts options
41
+ # @option opts [String] :path
42
+ # the environment path, defaults to current path
43
+ # @option opts [String] :mode
44
+ # the file mode, defaults to 0755
45
+ # @option opts [Integer] :max_size
46
+ # the maximum environment size in bytes, defaults to 4G
47
+ # @option opts [Integer] :max_dbs
48
+ # the maximum number of databases, defaults to 16
49
+ # @option opts [Integer] :max_readers
50
+ # the maximum number of readers, defaults to 126
51
+ # @option opts [Boolean] :sub_dirs
52
+ # allow subdirectories, defaults to true
53
+ # @option opts [Boolean] :read_only
54
+ # open in read-only mode, defaults to false
55
+ # @option opts [Boolean] :mmap
56
+ # use memory map, faster, but less durable, incompatible with nested transactions, defaults to false
57
+ # @option opts [Boolean] :sync
58
+ # flushes system buffers at the end of a transaction; slower, but more durable, defaults to true
59
+ # @option opts [Boolean] :meta_sync
60
+ # flush system buffers on every write and not only at the end of a transaction; slower, but more durable, defaults to false
61
+ # @raises [LMDB::Error] on errors
62
+ def initialize(opts = {})
63
+ opts = DEFAULT_OPTS.merge(opts)
64
+ @path = opts[:path].to_s
65
+ @raw = LMDB::Ext::Environment.open path, parse(opts)
66
+ end
67
+
68
+ # Manually sync the environment
69
+ #
70
+ # @param [Hash] opts options
71
+ # @option opts [Boolean] :force force sync, default false
72
+ # @return [Boolean] true if successful
73
+ # @raises [LMDB::Error] on errors
74
+ def sync(opts = {})
75
+ @raw.sync !!opts[:force]
76
+ true
77
+ end
78
+
79
+ # @return [Hash] environment info & stats
80
+ def info
81
+ info, stat = @raw.info, @raw.stat
82
+ hash = {}
83
+ INFO_MAP.each {|m, f| hash[f] = info.send(m) }
84
+ STAT_MAP.each {|m, f| hash[f] = stat.send(m) }
85
+ hash
86
+ end
87
+
88
+ # TODO: Thread-safe global transactions
89
+ #
90
+ # Executes a transaction
91
+ # @yield a transaction
92
+ # @yieldparam [LMDB::Transaction] txn the transaction instance
93
+ # def transaction(&block)
94
+ # LMDB::Transaction.send(:new, self, &block)
95
+ # end
96
+
97
+ # Opens a database
98
+ # @param [String] name
99
+ # @return [LMDB::Database] the database
100
+ def database(name)
101
+ LMDB::Database.send(:new, self, name)
102
+ end
103
+ alias_method :db, :database
104
+
105
+ # @return [String] introspection
106
+ def inspect
107
+ "#<#{self.class.name} @path=#{path.inspect}>"
108
+ end
109
+
110
+ private
111
+
112
+ def raw
113
+ @raw
114
+ end
115
+
116
+ def rtxn(&block)
117
+ @raw.transaction(&block)
118
+ end
119
+
120
+ def parse(opts)
121
+ flags = 0
122
+ flags |= LMDB::NOSUBDIR unless opts[:sub_dirs]
123
+ flags |= LMDB::RDONLY if opts[:read_only]
124
+ flags |= LMDB::WRITEMAP if opts[:mmap]
125
+ flags |= LMDB::NOSYNC unless opts[:sync]
126
+ flags |= LMDB::NOMETASYNC unless opts[:meta_sync]
127
+
128
+ { flags: flags,
129
+ mode: opts[:mode],
130
+ maxreaders: opts[:max_readers].to_i,
131
+ mapsize: opts[:max_size].to_i,
132
+ maxdbs: opts[:max_dbs].to_i }
133
+ end
134
+
135
+ end
data/lmdb.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = File.basename(__FILE__, ".gemspec")
5
+ s.version = "0.1.0"
6
+ s.platform = Gem::Platform::RUBY
7
+ s.licenses = ["MIT"]
8
+ s.summary = "Ruby bindings to Lightning MDB"
9
+ s.email = "mail@daniel-mendler.de"
10
+ s.homepage = "https://github.com/minad/mdb"
11
+ s.description = "imdb is a Ruby binding to OpenLDAP Lightning MDB."
12
+ s.authors = ["Daniel Mendler", "Black Square Media"]
13
+ s.extensions = Dir["ext/**/extconf.rb"]
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- spec/*`.split("\n")
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_development_dependency "rake"
20
+ s.add_development_dependency "rake-compiler"
21
+ s.add_development_dependency "rspec"
22
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe LMDB::Database do
4
+
5
+ subject { env.db(:pets) }
6
+ after { subject.close }
7
+
8
+ its(:name) { should == "pets" }
9
+ its(:env) { should == env }
10
+
11
+ it 'should get/set data' do
12
+ subject.get("cat").should be_nil
13
+ subject.set("cat", "garfield").should be_nil
14
+ subject.get("cat").should == "garfield"
15
+ end
16
+
17
+ it 'should get/set data' do
18
+ subject.get("cat").should be_nil
19
+ subject.set("cat", "garfield").should be_nil
20
+ subject.get("cat").should == "garfield"
21
+ end
22
+
23
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe LMDB::Environment do
4
+
5
+ subject { env }
6
+
7
+ it { should be_instance_of(described_class) }
8
+ its(:path) { should be_instance_of(String) }
9
+ its(:info) { should be_instance_of(Hash) }
10
+ its("info.keys") { should =~ [:address, :branch_pages, :depth, :entries, :last_page, :last_transaction_id, :leaf_pages, :max_size, :maxreaders, :overflow_pages, :page_size, :readers] }
11
+
12
+ it 'should sync' do
13
+ subject.sync.should be(true)
14
+ subject.sync(force: true).should be(true)
15
+ end
16
+
17
+ it 'should open databases' do
18
+ subject.database(:pets).should be_instance_of(LMDB::Database)
19
+ subject.db(:pets).should be_instance_of(LMDB::Database)
20
+ end
21
+
22
+ end
@@ -0,0 +1,253 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples "an environment" do
4
+ describe 'open' do
5
+ it 'returns environment' do
6
+ env = subject.open(path)
7
+ env.should be_instance_of(described_class::Environment)
8
+ env.close
9
+ end
10
+
11
+ it 'accepts block' do
12
+ subject.open(path) do |env|
13
+ env.should be_instance_of(described_class::Environment)
14
+ 42
15
+ end.should == 42
16
+ end
17
+
18
+ it 'accepts options' do
19
+ env = subject.open(path, :flags => LMDB::NOSYNC, :mode => 0777, :maxreaders => 777, :mapsize => 111111, :maxdbs => 666)
20
+ env.should be_instance_of(described_class::Environment)
21
+ env.info.maxreaders.should == 777
22
+ env.info.mapsize.should == 111111
23
+ env.close
24
+ end
25
+ end
26
+ end
27
+
28
+ describe LMDB::Ext do
29
+
30
+ let(:env) { LMDB::Ext.open(path) }
31
+ after { env.close }
32
+
33
+ it_behaves_like "an environment" do
34
+ subject { LMDB::Ext }
35
+ end
36
+
37
+ describe "Stat" do
38
+ subject { env.stat }
39
+
40
+ it { should be_instance_of(described_class::Stat) }
41
+ its(:psize) { should be_instance_of(Fixnum) }
42
+ its(:depth) { should be_instance_of(Fixnum) }
43
+ its(:branch_pages) { should be_instance_of(Fixnum) }
44
+ its(:leaf_pages) { should be_instance_of(Fixnum) }
45
+ its(:overflow_pages) { should be_instance_of(Fixnum) }
46
+ its(:entries) { should be_instance_of(Fixnum) }
47
+ end
48
+
49
+ describe "Info" do
50
+ subject { env.info }
51
+
52
+ it { should be_instance_of(described_class::Info) }
53
+ its(:mapaddr) { should be_instance_of(Fixnum) }
54
+ its(:mapsize) { should be_instance_of(Fixnum) }
55
+ its(:last_pgno) { should be_instance_of(Fixnum) }
56
+ its(:last_txnid) { should be_instance_of(Fixnum) }
57
+ its(:maxreaders) { should be_instance_of(Fixnum) }
58
+ its(:numreaders) { should be_instance_of(Fixnum) }
59
+ end
60
+
61
+ describe "Environment" do
62
+ it_behaves_like "an environment" do
63
+ subject { described_class::Environment }
64
+ end
65
+
66
+ subject { env }
67
+
68
+ its(:path) { should == path }
69
+ its(:flags) { should == 0 }
70
+
71
+ it 'should copy' do
72
+ target = mkpath('copy')
73
+ subject.copy(target).should be_nil
74
+ end
75
+
76
+ it 'should sync' do
77
+ subject.sync.should be_nil
78
+ end
79
+
80
+ it 'should force-sync' do
81
+ subject.sync(true).should be_nil
82
+ end
83
+
84
+ it 'should accept custom flags' do
85
+ (subject.flags = LMDB::NOSYNC).should == LMDB::NOSYNC
86
+ subject.flags.should == LMDB::NOSYNC
87
+
88
+ (subject.flags = 0).should == 0
89
+ subject.flags.should == 0
90
+ end
91
+
92
+ it 'should create transactions' do
93
+ txn = subject.transaction
94
+ txn.should be_instance_of(described_class::Transaction)
95
+ txn.parent.should be_nil
96
+ txn.environment.should == subject
97
+ txn.abort
98
+ end
99
+
100
+ it 'should create read-only transactions' do
101
+ txn = subject.transaction(true)
102
+ txn.should be_instance_of(described_class::Transaction)
103
+ txn.abort
104
+ end
105
+
106
+ it 'should create transactions blocks' do
107
+ subject.transaction do |txn|
108
+ txn.should be_instance_of(described_class::Transaction)
109
+ txn.environment.should == subject
110
+ txn.parent.should be_nil
111
+ end
112
+ end
113
+ end
114
+
115
+ describe "Transaction" do
116
+ subject { env.transaction }
117
+ after { subject.abort rescue nil }
118
+
119
+ its(:environment) { should == env }
120
+ its(:parent) { should be_nil }
121
+
122
+ it 'can create child transactions' do
123
+ txn = subject.transaction
124
+ txn.should be_instance_of(described_class::Transaction)
125
+ txn.parent.should == subject
126
+ txn.environment.should == env
127
+ txn.abort
128
+ end
129
+
130
+ it 'can create block transactions' do
131
+ subject.transaction do |txn|
132
+ txn.should be_instance_of(described_class::Transaction)
133
+ txn.parent.should == subject
134
+ end
135
+ end
136
+ end
137
+
138
+ describe "Database" do
139
+
140
+ subject do
141
+ env.transaction do |txn|
142
+ env.open(txn, 'db', LMDB::CREATE)
143
+ end
144
+ end
145
+ let!(:db) { subject }
146
+ after { subject.close rescue nil }
147
+
148
+ it 'stores key/values in same transaction' do
149
+ env.transaction do |txn|
150
+ db.put(txn, 'key', 'value').should be_nil
151
+ db.get(txn, 'key').should == 'value'
152
+ end
153
+ end
154
+
155
+ it 'stores key/values in different transactions' do
156
+ env.transaction do |txn|
157
+ db.put(txn, 'key', 'value').should be_nil
158
+ end
159
+
160
+ env.transaction do |txn|
161
+ db.get(txn, 'key').should == 'value'
162
+ end
163
+ end
164
+
165
+ it 'should return cursor' do
166
+ env.transaction do |txn|
167
+ cursor = db.cursor(txn)
168
+ cursor.should be_instance_of(described_class::Cursor)
169
+ cursor.close
170
+ end
171
+ end
172
+
173
+ it 'should return stat' do
174
+ env.transaction do |txn|
175
+ db.stat(txn).should be_instance_of(described_class::Stat)
176
+ end
177
+ end
178
+
179
+ it 'should close' do
180
+ db.close.should be_nil
181
+ -> { db.close }.should raise_error(LMDB::Ext::Error, /closed/)
182
+ end
183
+
184
+ it "should be correctly GC'd", segfault: true do
185
+ db = env.transaction {|t| env.open(t, 'db', LMDB::CREATE) }
186
+ db = nil
187
+ GC.start
188
+ db = env.transaction {|t| env.open(t, 'db', LMDB::CREATE) }
189
+ end
190
+
191
+ end
192
+
193
+ describe "Cursor" do
194
+
195
+ let! :db do
196
+ env.transaction {|txn| env.open(txn, 'db', LMDB::CREATE) }
197
+ end
198
+
199
+ before do
200
+ env.transaction do |txn|
201
+ db.put(txn, 'key1', 'value1')
202
+ db.put(txn, 'key2', 'value2')
203
+ end
204
+ end
205
+
206
+ after do
207
+ db.close
208
+ end
209
+
210
+ def with_cursor
211
+ env.transaction do |txn|
212
+ cursor = db.cursor(txn)
213
+ begin
214
+ yield cursor
215
+ ensure
216
+ cursor.close
217
+ end
218
+ end
219
+ end
220
+
221
+ it 'should get next key/value' do
222
+ with_cursor do |c|
223
+ c.first.should == ['key1', 'value1']
224
+ end
225
+ end
226
+
227
+ it 'should get next key/value' do
228
+ with_cursor do |c|
229
+ c.first
230
+ c.next.should == ['key2', 'value2']
231
+ end
232
+ end
233
+
234
+ it 'should seek to key' do
235
+ with_cursor do |c|
236
+ c.set('key1').should == ['key1', 'value1']
237
+ end
238
+ end
239
+
240
+ it 'should seek to closest key' do
241
+ with_cursor do |c|
242
+ c.set_range('key0').should == ['key1', 'value1']
243
+ end
244
+ end
245
+
246
+ it 'should seek to key with nuls' do
247
+ with_cursor do |c|
248
+ c.set_range("\x00").should == ['key1', 'value1']
249
+ end
250
+ end
251
+
252
+ end
253
+ end