diff --git a/ext/mdbx_ext/database.c b/ext/mdbx_ext/database.c index 23d9486..fd9c367 100644 --- a/ext/mdbx_ext/database.c +++ b/ext/mdbx_ext/database.c @@ -25,7 +25,10 @@ struct rmdbx_db { MDBX_txn *txn; MDBX_cursor *cursor; int env_flags; + int mode; int open; + int max_collections; + char *path; char *subdb; }; typedef struct rmdbx_db rmdbx_db_t; @@ -93,11 +96,41 @@ rmdbx_close( VALUE self ) } +/* + * Open the DB environment handle. + */ +VALUE +rmdbx_open_env( VALUE self ) +{ + int rc; + UNWRAP_DB( self, db ); + rmdbx_close_all( db ); + + /* Allocate an mdbx environment. + */ + rc = mdbx_env_create( &db->env ); + if ( rc != MDBX_SUCCESS ) + rb_raise( rmdbx_eDatabaseError, "mdbx_env_create: (%d) %s", rc, mdbx_strerror(rc) ); + + /* Set the maximum number of named databases for the environment. */ + mdbx_env_set_maxdbs( db->env, db->max_collections ); + + rc = mdbx_env_open( db->env, db->path, db->env_flags, db->mode ); + if ( rc != MDBX_SUCCESS ) { + rmdbx_close( self ); + rb_raise( rmdbx_eDatabaseError, "mdbx_env_open: (%d) %s", rc, mdbx_strerror(rc) ); + } + db->open = 1; + + return Qtrue; +} + + /* * call-seq: * db.closed? #=> false * - * Predicate: return true if the database handle is closed. + * Predicate: return true if the database environment is closed. */ VALUE rmdbx_closed_p( VALUE self ) @@ -139,23 +172,31 @@ rmdbx_open_txn( VALUE self, int rwflag ) /* * call-seq: - * db.destroy + * db.clear * - * Empty the database (or subdatabase) on disk. Unrecoverable. + * Empty the database (or collection) on disk. Unrecoverable! */ VALUE -rmdbx_destroy( VALUE self ) +rmdbx_clear( VALUE self ) { UNWRAP_DB( self, db ); + rmdbx_open_txn( self, MDBX_TXN_READWRITE ); int rc = mdbx_drop( db->txn, db->dbi, true ); - // FIXME: something fishy here - // if ( rc != 0 ) rb_raise( rmdbx_eDatabaseError, "mdbx_drop: (%d) %s", rc, mdbx_strerror(rc) ); + mdbx_txn_commit( db->txn ); - db->open = 0; + + // Close the current handle, will be re-opened + // on the next txn. + // + if ( db->dbi ) { + mdbx_dbi_close( db->env, db->dbi ); + db->dbi = 0; + } + return Qnil; } @@ -199,6 +240,42 @@ rmdbx_val_for( VALUE self, VALUE arg ) } +/* call-seq: + * db.keys #=> [ 'key1', 'key2', ... ] + * + * Return an array of all keys in the current collection. + */ +VALUE +rmdbx_keys( VALUE self ) +{ + UNWRAP_DB( self, db ); + VALUE rv = rb_ary_new(); + MDBX_val key, data; + int rc; + + if ( ! db->open ) rb_raise( rmdbx_eDatabaseError, "Closed database." ); + + rmdbx_open_txn( self, MDBX_TXN_RDONLY ); + rc = mdbx_cursor_open( db->txn, db->dbi, &db->cursor); + + if ( rc != MDBX_SUCCESS ) { + rmdbx_close( self ); + rb_raise( rmdbx_eDatabaseError, "Unable to open cursor: (%d) %s", rc, mdbx_strerror(rc) ); + } + + mdbx_cursor_get( db->cursor, &key, &data, MDBX_FIRST ); + rb_ary_push( rv, rb_str_new( key.iov_base, key.iov_len ) ); + while ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_NEXT ) == 0 ) { + rb_ary_push( rv, rb_str_new( key.iov_base, key.iov_len ) ); + } + + mdbx_cursor_close( db->cursor ); + db->cursor = NULL; + mdbx_txn_abort( db->txn ); + return rv; +} + + /* call-seq: * db[ 'key' ] #=> value * @@ -211,7 +288,7 @@ rmdbx_get_val( VALUE self, VALUE key ) VALUE deserialize_proc; UNWRAP_DB( self, db ); - if ( RTEST(rmdbx_closed_p(self)) ) rb_raise( rmdbx_eDatabaseError, "Closed database." ); + if ( ! db->open ) rb_raise( rmdbx_eDatabaseError, "Closed database." ); rmdbx_open_txn( self, MDBX_TXN_RDONLY ); @@ -224,10 +301,10 @@ rmdbx_get_val( VALUE self, VALUE key ) case MDBX_SUCCESS: deserialize_proc = rb_iv_get( self, "@deserializer" ); if ( ! NIL_P( deserialize_proc ) ) { - return rb_funcall( deserialize_proc, rb_intern("call"), 1, rb_str_new2(data.iov_base) ); + return rb_funcall( deserialize_proc, rb_intern("call"), 1, rb_str_new_cstr(data.iov_base) ); } else { - return rb_str_new2( data.iov_base ); + return rb_str_new_cstr( data.iov_base ); } case MDBX_NOTFOUND: @@ -251,7 +328,7 @@ rmdbx_put_val( VALUE self, VALUE key, VALUE val ) int rc; UNWRAP_DB( self, db ); - if ( RTEST(rmdbx_closed_p(self)) ) rb_raise( rmdbx_eDatabaseError, "Closed database." ); + if ( ! db->open ) rb_raise( rmdbx_eDatabaseError, "Closed database." ); rmdbx_open_txn( self, MDBX_TXN_READWRITE ); @@ -273,6 +350,8 @@ rmdbx_put_val( VALUE self, VALUE key, VALUE val ) switch ( rc ) { case MDBX_SUCCESS: return val; + case MDBX_NOTFOUND: + return Qnil; default: rb_raise( rmdbx_eDatabaseError, "Unable to store value: (%d) %s", rc, mdbx_strerror(rc) ); } @@ -290,7 +369,6 @@ rmdbx_put_val( VALUE self, VALUE key, VALUE val ) */ VALUE rmdbx_set_subdb( int argc, VALUE *argv, VALUE self ) -/* rmdbx_set_subdb( VALUE self, VALUE subdb ) */ { UNWRAP_DB( self, db ); VALUE subdb; @@ -298,17 +376,19 @@ rmdbx_set_subdb( int argc, VALUE *argv, VALUE self ) rb_scan_args( argc, argv, "01", &subdb ); if ( argc == 0 ) { if ( db->subdb == NULL ) return Qnil; - return rb_str_new2( db->subdb ); + return rb_str_new_cstr( db->subdb ); } rb_iv_set( self, "@collection", subdb ); db->subdb = NIL_P( subdb ) ? NULL : StringValueCStr( subdb ); - // Close any current dbi handle, to be re-opened with - // the new collection on next access. - // - // FIXME: Immediate transaction write to auto-create new env - // + /* Close any currently open dbi handle, to be re-opened with + * the new collection on next access. + * + FIXME: Immediate transaction write to auto-create new env? + Fetching from here at the moment causes an error if you + haven't written anything yet. + */ if ( db->dbi ) { mdbx_dbi_close( db->env, db->dbi ); db->dbi = 0; @@ -333,9 +413,9 @@ rmdbx_set_subdb( int argc, VALUE *argv, VALUE self ) VALUE rmdbx_database_initialize( int argc, VALUE *argv, VALUE self ) { - int rc = 0; - int mode = 0644; - int env_flags = MDBX_ENV_DEFAULTS; + int mode = 0644; + int max_collections = 0; + int env_flags = MDBX_ENV_DEFAULTS; VALUE path, opts, opt; rb_scan_args( argc, argv, "11", &path, &opts ); @@ -354,6 +434,8 @@ rmdbx_database_initialize( int argc, VALUE *argv, VALUE self ) */ opt = rb_hash_aref( opts, ID2SYM( rb_intern("mode") ) ); if ( ! NIL_P(opt) ) mode = FIX2INT( opt ); + opt = rb_hash_aref( opts, ID2SYM( rb_intern("max_collections") ) ); + if ( ! NIL_P(opt) ) max_collections = FIX2INT( opt ); opt = rb_hash_aref( opts, ID2SYM( rb_intern("nosubdir") ) ); if ( RTEST(opt) ) env_flags = env_flags | MDBX_NOSUBDIR; opt = rb_hash_aref( opts, ID2SYM( rb_intern("readonly") ) ); @@ -380,7 +462,6 @@ rmdbx_database_initialize( int argc, VALUE *argv, VALUE self ) /* Duplicate keys, on mdbx_dbi_open, maybe set here? */ /* MDBX_DUPSORT = UINT32_C(0x04), */ - /* Initialize the DB vals. */ UNWRAP_DB( self, db ); @@ -389,32 +470,18 @@ rmdbx_database_initialize( int argc, VALUE *argv, VALUE self ) db->txn = NULL; db->cursor = NULL; db->env_flags = env_flags; + db->mode = mode; + db->max_collections = max_collections; + db->path = StringValueCStr( path ); db->open = 0; db->subdb = NULL; - /* Allocate an mdbx environment. - */ - rc = mdbx_env_create( &db->env ); - if ( rc != MDBX_SUCCESS ) - rb_raise( rmdbx_eDatabaseError, "mdbx_env_create: (%d) %s", rc, mdbx_strerror(rc) ); - -//FIXME: configurable mdbx_env_set_maxdbs( db->env, 20 ); - mdbx_env_set_maxdbs( db->env, 20 ); - - /* Open the DB handle on disk. - */ - rc = mdbx_env_open( db->env, StringValueCStr(path), env_flags, mode ); - if ( rc != MDBX_SUCCESS ) { - rmdbx_close( self ); - rb_raise( rmdbx_eDatabaseError, "mdbx_env_open: (%d) %s", rc, mdbx_strerror(rc) ); - } - /* Set instance variables. */ - db->open = 1; rb_iv_set( self, "@path", path ); rb_iv_set( self, "@options", opts ); + rmdbx_open_env( self ); return self; } @@ -432,8 +499,10 @@ rmdbx_init_database() rb_define_protected_method( rmdbx_cDatabase, "initialize", rmdbx_database_initialize, -1 ); rb_define_method( rmdbx_cDatabase, "collection", rmdbx_set_subdb, -1 ); rb_define_method( rmdbx_cDatabase, "close", rmdbx_close, 0 ); + rb_define_method( rmdbx_cDatabase, "open", rmdbx_open_env, 0 ); rb_define_method( rmdbx_cDatabase, "closed?", rmdbx_closed_p, 0 ); - rb_define_method( rmdbx_cDatabase, "destroy", rmdbx_destroy, 0 ); + rb_define_method( rmdbx_cDatabase, "clear", rmdbx_clear, 0 ); + rb_define_method( rmdbx_cDatabase, "keys", rmdbx_keys, 0 ); rb_define_method( rmdbx_cDatabase, "[]", rmdbx_get_val, 1 ); rb_define_method( rmdbx_cDatabase, "[]=", rmdbx_put_val, 2 ); diff --git a/lib/mdbx/database.rb b/lib/mdbx/database.rb index b0787d4..6c4f927 100644 --- a/lib/mdbx/database.rb +++ b/lib/mdbx/database.rb @@ -9,7 +9,6 @@ require 'mdbx' unless defined?( MDBX ) # class MDBX::Database - ### Open an existing (or create a new) mdbx database at filesystem ### +path+. In block form, the database is automatically closed. ### @@ -40,6 +39,7 @@ class MDBX::Database # Only instantiate Database objects via #open. private_class_method :new + # The options used to instantiate this database. attr_reader :options @@ -52,5 +52,17 @@ class MDBX::Database # A Proc for automatically deserializing values. attr_accessor :deserializer + + # Allow for some common nomenclature. + alias_method :namespace, :collection + alias_method :reopen, :open + + + ### Switch to the top-level collection. + ### + def main + return self.collection( nil ) + end + end # class MDBX::Database diff --git a/spec/data/testdb/mdbx.dat b/spec/data/testdb/mdbx.dat deleted file mode 100644 index 94a178e..0000000 Binary files a/spec/data/testdb/mdbx.dat and /dev/null differ diff --git a/spec/lib/helper.rb b/spec/lib/helper.rb index dbca1f7..f5bfff2 100644 --- a/spec/lib/helper.rb +++ b/spec/lib/helper.rb @@ -14,7 +14,7 @@ require 'mdbx' module MDBX::Testing - TEST_DATABASE = Pathname( __FILE__ ).parent.parent + 'data' + 'testdb' + TEST_DATABASE = Pathname( __FILE__ ).parent.parent.parent + 'tmp' + 'testdb' end diff --git a/spec/mdbx/database_spec.rb b/spec/mdbx/database_spec.rb index 735b5d1..b6feab8 100644 --- a/spec/mdbx/database_spec.rb +++ b/spec/mdbx/database_spec.rb @@ -10,7 +10,7 @@ RSpec.describe( MDBX::Database ) do to raise_exception( NoMethodError, /private/ ) end - it "knows the db handle open/close state" do + it "knows the env handle open/close state" do db = described_class.open( TEST_DATABASE.to_s ) expect( db.closed? ).to be_falsey db.close @@ -33,29 +33,95 @@ RSpec.describe( MDBX::Database ) do db.close end + it "can be reopened" do + db.close + expect( db ).to be_closed + db.reopen + expect( db ).to_not be_closed + end + it "knows its own path" do - expect( db.path ).to match( %r|data/testdb$| ) + expect( db.path ).to match( %r|tmp/testdb$| ) end it "fails if opened again within the same process" do # This is a function of libmdbx internals, just testing - # here for behaviorals. + # here for behavior. expect { described_class.open( TEST_DATABASE.to_s ) }. to raise_exception( MDBX::DatabaseError, /environment is already used/ ) end + it "can remove a key by setting its value to nil" do + db[ 'test' ] = "hi" + expect( db['test'] ).to eq( 'hi' ) + + db[ 'test' ] = nil + expect( db['test'] ).to be_nil + + expect { db[ 'test' ] = nil }.to_not raise_exception + end + + 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 + 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 ) + db.collection( 'bucket' ) + + expect{ db['key'] = true }.to raise_exception( /MDBX_DBS_FULL/ ) + end + + 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 + it "defaults to the top-level namespace" do expect( db.collection ).to be_nil end it "can set a collection namespace" do + expect( db.collection ).to be_nil + db[ 'key' ] = true + db.collection( 'bucket' ) expect( db.collection ).to eq( 'bucket' ) - # TODO: set/retrieve data + db[ 'key' ] = false + + db.main + expect( db.collection ).to be_nil + expect( db['key'] ).to be_truthy end + 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 end end diff --git a/spec/data/testdb/mdbx.lck b/tmp/.placeholder similarity index 100% rename from spec/data/testdb/mdbx.lck rename to tmp/.placeholder