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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/CHANGES +3 -0
- data/CONTRIBUTORS +3 -0
- data/Gemfile +2 -0
- data/README.md +37 -0
- data/Rakefile +14 -0
- data/ext/lmdb_ext/errors.h +15 -0
- data/ext/lmdb_ext/extconf.rb +24 -0
- data/ext/lmdb_ext/liblmdb/.gitignore +16 -0
- data/ext/lmdb_ext/liblmdb/CHANGES +41 -0
- data/ext/lmdb_ext/liblmdb/COPYRIGHT +20 -0
- data/ext/lmdb_ext/liblmdb/LICENSE +47 -0
- data/ext/lmdb_ext/liblmdb/lmdb.h +1367 -0
- data/ext/lmdb_ext/liblmdb/mdb.c +8354 -0
- data/ext/lmdb_ext/liblmdb/midl.c +348 -0
- data/ext/lmdb_ext/liblmdb/midl.h +177 -0
- data/ext/lmdb_ext/lmdb_ext.c +725 -0
- data/lib/lmdb.rb +14 -0
- data/lib/lmdb/database.rb +66 -0
- data/lib/lmdb/environment.rb +135 -0
- data/lmdb.gemspec +22 -0
- data/spec/lmdb/database_spec.rb +23 -0
- data/spec/lmdb/environment_spec.rb +22 -0
- data/spec/lmdb/lmdb_ext_spec.rb +253 -0
- data/spec/lmdb_spec.rb +41 -0
- data/spec/spec_helper.rb +33 -0
- metadata +118 -0
data/lib/lmdb.rb
ADDED
@@ -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
|