From 5ed44e52fa6dcdccc4a9424c84916ddc40c7f433 Mon Sep 17 00:00:00 2001 From: "mahlon@martini.nu" Date: Thu, 17 Dec 2020 01:09:43 +0000 Subject: [PATCH] Multiple changes. - Provide a method to reopen the database environment if closed. - Add #keys, which returns an array of keys for the current collection. - Finish collection/namespace implementation. - Add various aliases (collection->namespace, etc) - Add #clear, which destroys data for a the current collection. If called in the main db, destroys all. Don't close the environment after a #clear, just the dbi handle. - Various bugfixes. FossilOrigin-Name: 8b00d59e8c5269266f3c6c0d10056a1c0fe1bb583ffbe099a6ed0dac3baf3a66 --- ext/mdbx_ext/database.c | 151 +++++++++++++----- lib/mdbx/database.rb | 14 +- spec/data/testdb/mdbx.dat | Bin 65536 -> 0 bytes spec/lib/helper.rb | 2 +- spec/mdbx/database_spec.rb | 74 ++++++++- spec/data/testdb/mdbx.lck => tmp/.placeholder | 0 6 files changed, 194 insertions(+), 47 deletions(-) delete mode 100644 spec/data/testdb/mdbx.dat rename spec/data/testdb/mdbx.lck => tmp/.placeholder (100%) 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 94a178e822003021973c6a492b9523ffc2a950ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65536 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