lmdb 0.1.0

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