2020-11-30 05:57:26 +00:00
|
|
|
#!/usr/bin/env rspec -cfd
|
2021-01-25 00:36:40 +00:00
|
|
|
# vim: set nosta noet ts=4 sw=4 ft=ruby:
|
2020-11-30 05:57:26 +00:00
|
|
|
|
|
|
|
|
require_relative '../lib/helper'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RSpec.describe( MDBX::Database ) do
|
|
|
|
|
|
2021-02-14 09:47:04 +00:00
|
|
|
after( :all ) do
|
|
|
|
|
db = described_class.open( TEST_DATABASE.to_s )
|
|
|
|
|
db.clear
|
|
|
|
|
db.close
|
|
|
|
|
end
|
|
|
|
|
|
2020-11-30 05:57:26 +00:00
|
|
|
it "disallows direct calls to #new" do
|
|
|
|
|
expect{ described_class.new }.
|
|
|
|
|
to raise_exception( NoMethodError, /private/ )
|
|
|
|
|
end
|
2020-12-01 03:32:26 +00:00
|
|
|
|
2020-12-17 01:09:43 +00:00
|
|
|
it "knows the env handle open/close state" do
|
2020-12-01 03:32:26 +00:00
|
|
|
db = described_class.open( TEST_DATABASE.to_s )
|
|
|
|
|
expect( db.closed? ).to be_falsey
|
|
|
|
|
db.close
|
|
|
|
|
expect( db.closed? ).to be_truthy
|
|
|
|
|
end
|
|
|
|
|
|
2020-12-16 08:29:50 +00:00
|
|
|
it "closes itself automatically when used in block form" do
|
|
|
|
|
db = described_class.open( TEST_DATABASE.to_s ) do |db|
|
|
|
|
|
expect( db.closed? ).to be_falsey
|
|
|
|
|
end
|
|
|
|
|
expect( db.closed? ).to be_truthy
|
|
|
|
|
end
|
|
|
|
|
|
2021-03-19 01:15:04 +00:00
|
|
|
it "can create a closed clone" do
|
|
|
|
|
db = described_class.open( TEST_DATABASE.to_s )
|
|
|
|
|
db[1] = "doopydoo"
|
|
|
|
|
|
|
|
|
|
clone = db.clone
|
|
|
|
|
|
|
|
|
|
expect( db.closed? ).to be_falsey
|
|
|
|
|
expect( clone.closed? ).to be_truthy
|
|
|
|
|
expect( db.path ).to eq( clone.path )
|
|
|
|
|
db.close
|
|
|
|
|
|
|
|
|
|
clone.reopen
|
|
|
|
|
expect( clone[1] ).to eq( "doopydoo" )
|
|
|
|
|
clone.close
|
|
|
|
|
end
|
|
|
|
|
|
2020-12-16 08:29:50 +00:00
|
|
|
|
2020-12-01 07:58:49 +00:00
|
|
|
context 'an opened database' do
|
|
|
|
|
|
2020-12-16 08:29:50 +00:00
|
|
|
let!( :db ) { described_class.open( TEST_DATABASE.to_s ) }
|
2020-12-01 07:58:49 +00:00
|
|
|
|
|
|
|
|
after( :each ) do
|
2020-12-16 08:29:50 +00:00
|
|
|
db.close
|
|
|
|
|
end
|
|
|
|
|
|
2020-12-17 01:09:43 +00:00
|
|
|
it "can be reopened" do
|
|
|
|
|
db.close
|
|
|
|
|
expect( db ).to be_closed
|
|
|
|
|
db.reopen
|
|
|
|
|
expect( db ).to_not be_closed
|
|
|
|
|
end
|
|
|
|
|
|
2020-12-16 08:29:50 +00:00
|
|
|
it "knows its own path" do
|
2020-12-17 01:09:43 +00:00
|
|
|
expect( db.path ).to match( %r|tmp/testdb$| )
|
2020-12-01 07:58:49 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "fails if opened again within the same process" do
|
|
|
|
|
# This is a function of libmdbx internals, just testing
|
2020-12-17 01:09:43 +00:00
|
|
|
# here for behavior.
|
2020-12-01 07:58:49 +00:00
|
|
|
expect {
|
|
|
|
|
described_class.open( TEST_DATABASE.to_s )
|
|
|
|
|
}.
|
|
|
|
|
to raise_exception( MDBX::DatabaseError, /environment is already used/ )
|
|
|
|
|
end
|
2021-03-14 23:19:41 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
context 'hash-alike methods' do
|
2020-12-01 07:58:49 +00:00
|
|
|
|
2021-03-14 23:19:41 +00:00
|
|
|
let!( :db ) { described_class.open( TEST_DATABASE.to_s ) }
|
|
|
|
|
|
|
|
|
|
before( :each ) do
|
|
|
|
|
db.clear
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
after( :each ) do
|
|
|
|
|
db.close
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
it "can remove an entry by setting a key's value to nil" do
|
2020-12-17 01:09:43 +00:00
|
|
|
db[ 'test' ] = "hi"
|
|
|
|
|
expect( db['test'] ).to eq( 'hi' )
|
|
|
|
|
|
|
|
|
|
db[ 'test' ] = nil
|
|
|
|
|
expect( db['test'] ).to be_nil
|
|
|
|
|
end
|
|
|
|
|
|
2021-03-14 23:19:41 +00:00
|
|
|
it 'can remove an entry via delete()' do
|
|
|
|
|
val = 'hi'
|
|
|
|
|
db[ 'test' ] = val
|
|
|
|
|
expect( db['test'] ).to eq( val )
|
|
|
|
|
|
|
|
|
|
oldval = db.delete( 'test' )
|
|
|
|
|
expect( oldval ).to eq( val )
|
|
|
|
|
expect( db['test'] ).to be_nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'returns a the delete() block if a key is not found' do
|
|
|
|
|
db.clear
|
|
|
|
|
expect( db.delete( 'test' ) ).to be_nil
|
|
|
|
|
rv = db.delete( 'test' ) {|key| "Couldn't find %p key!" % [ key ] }
|
|
|
|
|
expect( rv ).to eq( "Couldn't find \"test\" key!" )
|
|
|
|
|
end
|
|
|
|
|
|
2020-12-17 01:09:43 +00:00
|
|
|
it "can return an array of its keys" do
|
|
|
|
|
db[ 'key1' ] = true
|
|
|
|
|
db[ 'key2' ] = true
|
|
|
|
|
db[ 'key3' ] = true
|
|
|
|
|
expect( db.keys ).to include( 'key1', 'key2', 'key3' )
|
|
|
|
|
end
|
2021-03-14 23:19:41 +00:00
|
|
|
|
|
|
|
|
it 'knows when there is data present' do
|
|
|
|
|
expect( db.empty? ).to be_truthy
|
|
|
|
|
db[ 'bloop' ] = 1
|
|
|
|
|
expect( db.empty? ).to be_falsey
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "can convert to an array" do
|
|
|
|
|
3.times{|i| db[i] = i }
|
|
|
|
|
expect( db.to_a ).to eq([ ["0",0], ["1",1], ["2",2] ])
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "can convert to a hash" do
|
|
|
|
|
3.times{|i| db[i] = i }
|
|
|
|
|
expect( db.to_h ).to eq({ "0"=>0, "1"=>1, "2"=>2 })
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "retrieves a value via fetch()" do
|
|
|
|
|
db[ 'test' ] = true
|
|
|
|
|
expect( db.fetch('test') ).to be_truthy
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "executes a fetch() block if the key was not found" do
|
|
|
|
|
rv = false
|
|
|
|
|
db.fetch( 'nopenopenope' ) { rv = true }
|
|
|
|
|
expect( rv ).to be_truthy
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "raises KeyError if fetch()ing without a block to a nonexistent key" do
|
|
|
|
|
expect{ db.fetch(:nopenopenope) }.to raise_exception( KeyError, /key not found/ )
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "can return a sliced hash" do
|
|
|
|
|
( 'a'..'z' ).each{|c| db[c] = c }
|
|
|
|
|
expect( db.slice( 'a', 'f' ) ).to eq( 'a' => 'a', 'f' => 'f' )
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "can return an array of specific values" do
|
|
|
|
|
( 'a'..'z' ).each{|c| db[c] = c * 3 }
|
|
|
|
|
expect( db.values_at('e', 'nopenopenope', 'g') ).to eq( ['eee', nil, 'ggg'] )
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "can return an array of all values" do
|
|
|
|
|
( 'a'..'z' ).each{|c| db[c] = c * 2 }
|
|
|
|
|
expect( db.values ).to include( 'aa', 'hh', 'tt' )
|
|
|
|
|
end
|
2020-12-17 01:09:43 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
context 'collections' do
|
|
|
|
|
|
|
|
|
|
let!( :db ) { described_class.open( TEST_DATABASE.to_s, max_collections: 5 ) }
|
|
|
|
|
|
|
|
|
|
after( :each ) do
|
|
|
|
|
db.close
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "fail if the max_collections option is not specified when opening" do
|
|
|
|
|
db.close
|
|
|
|
|
db = described_class.open( TEST_DATABASE.to_s )
|
2021-02-14 09:47:04 +00:00
|
|
|
expect{
|
|
|
|
|
db.collection( 'bucket' )
|
|
|
|
|
} .to raise_exception( /not enabled/ )
|
2020-12-17 01:09:43 +00:00
|
|
|
end
|
|
|
|
|
|
2021-03-14 23:19:41 +00:00
|
|
|
it "knows it's length" do
|
|
|
|
|
db.collection( 'size1' )
|
|
|
|
|
10.times {|i| db[i] = true }
|
|
|
|
|
db.collection( 'size2' )
|
|
|
|
|
25.times {|i| db[i] = true }
|
|
|
|
|
|
|
|
|
|
db.collection( 'size1' )
|
|
|
|
|
expect( db.length ).to be( 10 )
|
|
|
|
|
db.collection( 'size2' )
|
|
|
|
|
expect( db.length ).to be( 25 )
|
|
|
|
|
end
|
|
|
|
|
|
2020-12-17 01:09:43 +00:00
|
|
|
it "disallows regular key/val storage for namespace keys" do
|
|
|
|
|
db.collection( 'bucket' )
|
|
|
|
|
db[ 'okay' ] = 1
|
|
|
|
|
db.collection( nil )
|
|
|
|
|
|
|
|
|
|
expect{ db['bucket'] = 1 }.to raise_exception( /MDBX_INCOMPATIBLE/ )
|
|
|
|
|
end
|
|
|
|
|
|
2020-12-16 08:29:50 +00:00
|
|
|
it "defaults to the top-level namespace" do
|
|
|
|
|
expect( db.collection ).to be_nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "can set a collection namespace" do
|
2020-12-17 01:09:43 +00:00
|
|
|
expect( db.collection ).to be_nil
|
|
|
|
|
db[ 'key' ] = true
|
|
|
|
|
|
2020-12-16 08:29:50 +00:00
|
|
|
db.collection( 'bucket' )
|
|
|
|
|
expect( db.collection ).to eq( 'bucket' )
|
2020-12-17 01:09:43 +00:00
|
|
|
db[ 'key' ] = false
|
|
|
|
|
|
|
|
|
|
db.main
|
|
|
|
|
expect( db.collection ).to be_nil
|
|
|
|
|
expect( db['key'] ).to be_truthy
|
2020-12-16 08:29:50 +00:00
|
|
|
end
|
|
|
|
|
|
2021-04-05 23:43:45 +00:00
|
|
|
it "automatically stringifies the collection argument" do
|
|
|
|
|
db.collection( :bucket )
|
|
|
|
|
expect( db.collection ).to eq( 'bucket' )
|
|
|
|
|
end
|
|
|
|
|
|
2021-01-25 00:36:40 +00:00
|
|
|
it "revert back to the previous collection when used in a block" do
|
|
|
|
|
expect( db.collection ).to be_nil
|
|
|
|
|
db.collection( 'bucket' ) { 'no-op' }
|
|
|
|
|
expect( db.collection ).to be_nil
|
|
|
|
|
end
|
|
|
|
|
|
2020-12-17 01:09:43 +00:00
|
|
|
it "can be cleared of contents" do
|
|
|
|
|
db.collection( 'bucket' )
|
|
|
|
|
10.times {|i| db[i] = true }
|
|
|
|
|
expect( db[2] ).to be_truthy
|
|
|
|
|
|
|
|
|
|
db.clear
|
|
|
|
|
db.main
|
|
|
|
|
expect( db['bucket'] ).to be_nil
|
|
|
|
|
end
|
2020-12-01 07:58:49 +00:00
|
|
|
end
|
2021-02-14 09:47:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
context 'transactions' do
|
|
|
|
|
|
|
|
|
|
let!( :db ) { described_class.open( TEST_DATABASE.to_s, max_collections: 5 ) }
|
|
|
|
|
|
|
|
|
|
after( :each ) do
|
|
|
|
|
db.close
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "knows when a transaction is currently open" do
|
|
|
|
|
expect( db.in_transaction? ).to be_falsey
|
|
|
|
|
db.snapshot
|
|
|
|
|
expect( db.in_transaction? ).to be_truthy
|
|
|
|
|
db.abort
|
|
|
|
|
expect( db.in_transaction? ).to be_falsey
|
|
|
|
|
|
|
|
|
|
db.snapshot do
|
|
|
|
|
expect( db.in_transaction? ).to be_truthy
|
|
|
|
|
end
|
|
|
|
|
expect( db.in_transaction? ).to be_falsey
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "throws an error if changing collection mid-transaction" do
|
|
|
|
|
db.snapshot do
|
|
|
|
|
expect{ db.collection('nope') }.
|
|
|
|
|
to raise_exception( MDBX::DatabaseError, /transaction open/ )
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "are re-entrant" do
|
|
|
|
|
3.times { db.snapshot }
|
|
|
|
|
expect( db.in_transaction? ).to be_truthy
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "refuse to allow writes for read-only snapshots" do
|
|
|
|
|
db.snapshot
|
|
|
|
|
expect{ db[1] = true }.
|
|
|
|
|
to raise_exception( MDBX::DatabaseError, /permission denied/i )
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "revert changes via explicit rollbacks" do
|
|
|
|
|
db[ 1 ] = true
|
|
|
|
|
db.transaction
|
|
|
|
|
db[ 1 ] = false
|
|
|
|
|
expect( db[ 1 ] ).to be_falsey
|
|
|
|
|
db.rollback
|
|
|
|
|
expect( db[ 1 ] ).to be_truthy
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "revert changes via uncaught exceptions" do
|
|
|
|
|
db[ 1 ] = true
|
|
|
|
|
expect {
|
|
|
|
|
db.transaction do
|
|
|
|
|
db[ 1 ] = false
|
|
|
|
|
raise "boom"
|
|
|
|
|
end
|
|
|
|
|
}.to raise_exception( RuntimeError, "boom" )
|
|
|
|
|
expect( db[ 1 ] ).to be_truthy
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "revert changes via explicit exceptions" do
|
|
|
|
|
db[ 1 ] = true
|
|
|
|
|
expect {
|
|
|
|
|
db.transaction do
|
|
|
|
|
db[ 1 ] = false
|
|
|
|
|
raise MDBX::Rollback, "boom!"
|
|
|
|
|
end
|
|
|
|
|
}.to_not raise_exception
|
|
|
|
|
expect( db[ 1 ] ).to be_truthy
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "write changes after commit" do
|
|
|
|
|
db[ 1 ] = true
|
|
|
|
|
db.transaction
|
|
|
|
|
db[ 1 ] = false
|
|
|
|
|
expect( db[ 1 ] ).to be_falsey
|
|
|
|
|
db.commit
|
|
|
|
|
expect( db[ 1 ] ).to be_falsey
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "automatically write changes after block" do
|
|
|
|
|
db[ 1 ] = true
|
|
|
|
|
db.transaction do
|
|
|
|
|
db[ 1 ] = false
|
|
|
|
|
end
|
|
|
|
|
expect( db[ 1 ] ).to be_falsey
|
|
|
|
|
end
|
2021-03-15 06:52:44 +00:00
|
|
|
|
|
|
|
|
it "doesn't inadvertantly close transactions when using hash-alike methods" do
|
|
|
|
|
expect( db.in_transaction? ).to be_falsey
|
|
|
|
|
db.transaction
|
|
|
|
|
expect( db.in_transaction? ).to be_truthy
|
|
|
|
|
db.to_a
|
|
|
|
|
db.to_h
|
|
|
|
|
db.keys
|
|
|
|
|
db.slice( :woop )
|
|
|
|
|
db.values
|
|
|
|
|
db.values_at( :woop )
|
|
|
|
|
expect( db.in_transaction? ).to be_truthy
|
2021-03-15 07:39:01 +00:00
|
|
|
db.abort
|
2021-03-15 06:52:44 +00:00
|
|
|
end
|
2021-02-14 09:47:04 +00:00
|
|
|
end
|
2021-03-14 23:19:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
context "iterators" do
|
|
|
|
|
|
|
|
|
|
let( :db ) {
|
|
|
|
|
described_class.open( TEST_DATABASE.to_s, max_collections: 5 ).collection( 'iter' )
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
before( :each ) do
|
|
|
|
|
3.times {|i| db[i] = "#{i}-val" }
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
after( :each ) do
|
|
|
|
|
db.close
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "raises an exception if the caller didn't open a transaction first" do
|
|
|
|
|
expect{ db.each_key }.to raise_exception( MDBX::DatabaseError, /no .*currently open/i )
|
|
|
|
|
expect{ db.each_value }.to raise_exception( MDBX::DatabaseError, /no .*currently open/i )
|
|
|
|
|
expect{ db.each_pair }.to raise_exception( MDBX::DatabaseError, /no .*currently open/i )
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context "(with a transaction)" do
|
|
|
|
|
|
|
|
|
|
before( :each ) { db.snapshot }
|
|
|
|
|
after( :each ) { db.abort }
|
|
|
|
|
|
|
|
|
|
it "returns an iterator without a block" do
|
|
|
|
|
iter = db.each_key
|
|
|
|
|
expect( iter ).to be_a( Enumerator )
|
|
|
|
|
expect( iter.to_a.size ).to be( 3 )
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "can iterate through keys" do
|
|
|
|
|
rv = db.each_key.with_object([]){|k, acc| acc << k }
|
|
|
|
|
expect( db.each_key.to_a ).to eq( rv )
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "can iterate through values" do
|
|
|
|
|
rv = db.each_value.with_object([]){|v, acc| acc << v }
|
|
|
|
|
expect( rv ).to eq( %w[ 0-val 1-val 2-val ] )
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "can iterate through key/value pairs" do
|
|
|
|
|
expect( db.each_pair.to_a.first ).to eq([ "0", "0-val" ])
|
|
|
|
|
expect( db.each_pair.to_a.last ).to eq([ "2", "2-val" ])
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
context "serialization" do
|
|
|
|
|
|
|
|
|
|
let( :db ) {
|
|
|
|
|
described_class.open( TEST_DATABASE.to_s )
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
after( :each ) do
|
|
|
|
|
db.close
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "uses Marshalling as default" do
|
|
|
|
|
db.deserializer = nil
|
|
|
|
|
hash = { a_hash: true }
|
|
|
|
|
db[ 'test' ] = hash
|
|
|
|
|
expect( db['test'] ).to eq( Marshal.dump( hash ) )
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "can be disabled completely" do
|
|
|
|
|
db.serializer = nil
|
|
|
|
|
db.deserializer = nil
|
|
|
|
|
|
|
|
|
|
db[ 'test' ] = "doot"
|
|
|
|
|
db[ 'test2' ] = [1,2,3].to_s
|
|
|
|
|
expect( db['test'] ).to eq( "doot" )
|
|
|
|
|
expect( db['test2'] ).to eq( "[1, 2, 3]" )
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it "can be arbitrarily changed" do
|
|
|
|
|
db.serializer = ->( v ) { JSON.generate(v) }
|
|
|
|
|
db.deserializer = ->( v ) { JSON.parse(v) }
|
|
|
|
|
|
|
|
|
|
hash = { "a_hash" => true }
|
|
|
|
|
db[ 'test' ] = hash
|
|
|
|
|
expect( db['test'] ).to eq( hash )
|
|
|
|
|
end
|
|
|
|
|
end
|
2020-11-30 05:57:26 +00:00
|
|
|
end
|
|
|
|
|
|