diff --git a/.hgignore b/.hgignore index ee5f68e..0df4e77 100644 --- a/.hgignore +++ b/.hgignore @@ -8,6 +8,7 @@ spec/\.status coverage/ vendor/ pkg/ +^tags$ ^Session.vim$ ^Makefile$ diff --git a/.pryrc b/.pryrc index 3088e92..8765b2e 100644 --- a/.pryrc +++ b/.pryrc @@ -11,5 +11,6 @@ rescue Exception => e e.backtrace.join( "\n\t" ) end -db = MDBX::Database.open( 'tmp/testdb', max_collections: 100 ) +db = MDBX::Database.open( 'tmp/testdb', max_collections: 50 ) +#db = MDBX::Database.open( 'tmp/testdb', duplicate_keys: true ) diff --git a/History.md b/History.md index a04d58c..8133d01 100644 --- a/History.md +++ b/History.md @@ -1,5 +1,14 @@ # Release History for MDBX +--- +## v0.3.1 [2021-05-16] Mahlon E. Smith + +Bugfix: + + - #drop could potentially remove unintended data. Yanked version + v0.3.0. + + --- ## v0.3.0 [2021-04-09] Mahlon E. Smith diff --git a/README.md b/README.md index 90757a7..e517bda 100644 --- a/README.md +++ b/README.md @@ -340,13 +340,6 @@ information about the build environment, the database environment, and the currently connected clients. -## TODO - - - Expose more database/collection information to statistics - - Support libmdbx multiple values per key DUPSORT via `put`, `get` - Enumerators, and a 'value' argument for `delete`. - - ## Contributing You can check out the current development source with Mercurial via its diff --git a/experiments/thread_usage.rb b/experiments/thread_usage.rb index bac77ff..9a21746 100755 --- a/experiments/thread_usage.rb +++ b/experiments/thread_usage.rb @@ -37,7 +37,6 @@ def run_bench( db, msg ) threads.map( &:join ) end - # Long running transactions require a mutex across threads. # x.report( "txn per thread:" ) do @@ -78,4 +77,3 @@ run_bench( db, "Default database flags:" ) db = MDBX::Database.open( 'tmpdb', no_metasync: true ) run_bench( db, "Disabled metasync:" ) - diff --git a/ext/mdbx_ext/database.c b/ext/mdbx_ext/database.c index e8ef676..32f09f6 100644 --- a/ext/mdbx_ext/database.c +++ b/ext/mdbx_ext/database.c @@ -1,21 +1,16 @@ -/* vim: set noet sta sw=4 ts=4 : */ +/* vim: set noet sta sw=4 ts=4 fdm=marker: */ +/* + * Primary database handle functions. + * + */ #include "mdbx_ext.h" -/* Shortcut for fetching current DB variables. - */ -#define UNWRAP_DB( val, db ) \ - rmdbx_db_t *db; \ - TypedData_Get_Struct( val, rmdbx_db_t, &rmdbx_db_data, db ) - -#define CHECK_HANDLE \ - if ( ! db->state.open ) rb_raise( rmdbx_eDatabaseError, "Closed database." ) - - VALUE rmdbx_cDatabase; + /* - * Ruby allocation hook. + * Ruby data allocation wrapper. */ static const rb_data_type_t rmdbx_db_data = { .wrap_struct_name = "MDBX::Database::Data", @@ -30,11 +25,24 @@ static const rb_data_type_t rmdbx_db_data = { VALUE rmdbx_alloc( VALUE klass ) { - rmdbx_db_t *new = RB_ALLOC( rmdbx_db_t ); + rmdbx_db_t *new; return TypedData_Make_Struct( klass, rmdbx_db_t, &rmdbx_db_data, new ); } +/* + * Cleanup a previously allocated DB environment. + */ +void +rmdbx_free( void *db ) +{ + if ( db ) { + rmdbx_close_all( db ); + xfree( db ); + } +} + + /* * Ensure all database file descriptors are collected and * removed. @@ -65,19 +73,9 @@ rmdbx_close_dbi( rmdbx_db_t *db ) /* - * Cleanup a previously allocated DB environment. - */ -void -rmdbx_free( void *db ) -{ - if ( db ) { - rmdbx_close_all( db ); - xfree( db ); - } -} - - -/* + * call-seq: + * db.close => true + * * Cleanly close an opened database. */ VALUE @@ -104,17 +102,63 @@ rmdbx_closed_p( VALUE self ) /* - * call-seq: - * db.in_transaction? => false + * Check if a given +flag+ is enabled for flag +val+. + */ +int +rmdbx_flag_enabled( val, flag ) +{ + return ( val & flag ) == flag; +} + + +/* + * Given a ruby string +key+ and a pointer to an MDBX_val, prepare the + * key for usage within mdbx. All keys are explicitly converted to + * strings. * - * Predicate: return true if a transaction (or snapshot) - * is currently open. + */ +void +rmdbx_key_for( VALUE key, MDBX_val *ckey ) +{ + VALUE key_str = rb_funcall( key, rb_intern("to_s"), 0 ); + ckey->iov_len = RSTRING_LEN( key_str ); + ckey->iov_base = malloc( ckey->iov_len ); + strlcpy( ckey->iov_base, StringValuePtr(key_str), ckey->iov_len + 1 ); +} + + +/* + * Given a ruby +value+ and a pointer to an MDBX_val, prepare + * the value for usage within mdbx. Values are potentially serialized. + * + */ +void +rmdbx_val_for( VALUE self, VALUE val, MDBX_val *data ) +{ + VALUE serialize_proc = rb_iv_get( self, "@serializer" ); + + if ( ! NIL_P( serialize_proc ) ) + val = rb_funcall( serialize_proc, rb_intern("call"), 1, val ); + + Check_Type( val, T_STRING ); + + data->iov_len = RSTRING_LEN( val ); + data->iov_base = malloc( data->iov_len ); + strlcpy( data->iov_base, StringValuePtr(val), data->iov_len + 1 ); +} + + +/* + * Deserialize and return a value. */ VALUE -rmdbx_in_transaction_p( VALUE self ) +rmdbx_deserialize( VALUE self, VALUE val ) { - UNWRAP_DB( self, db ); - return db->txn ? Qtrue : Qfalse; + VALUE deserialize_proc = rb_iv_get( self, "@deserializer" ); + if ( ! NIL_P( deserialize_proc ) ) + val = rb_funcall( deserialize_proc, rb_intern("call"), 1, val ); + + return val; } @@ -151,122 +195,8 @@ rmdbx_open_env( VALUE self ) rmdbx_close_all( db ); rb_raise( rmdbx_eDatabaseError, "mdbx_env_open: (%d) %s", rc, mdbx_strerror(rc) ); } + db->state.open = 1; - - return Qtrue; -} - - -/* - * Open a cursor for iteration. - */ -void -rmdbx_open_cursor( rmdbx_db_t *db ) -{ - CHECK_HANDLE; - if ( ! db->txn ) rb_raise( rmdbx_eDatabaseError, "No snapshot or transaction currently open." ); - - int rc = mdbx_cursor_open( db->txn, db->dbi, &db->cursor ); - if ( rc != MDBX_SUCCESS ) { - rmdbx_close_all( db ); - rb_raise( rmdbx_eDatabaseError, "Unable to open cursor: (%d) %s", rc, mdbx_strerror(rc) ); - } - - return; -} - - -/* - * Open a new database transaction. If a transaction is already - * open, this is a no-op. - * - * +rwflag+ must be either MDBX_TXN_RDONLY or MDBX_TXN_READWRITE. - */ -void -rmdbx_open_txn( rmdbx_db_t *db, int rwflag ) -{ - if ( db->txn ) return; - - int rc = mdbx_txn_begin( db->env, NULL, rwflag, &db->txn ); - if ( rc != MDBX_SUCCESS ) { - rmdbx_close_all( db ); - rb_raise( rmdbx_eDatabaseError, "mdbx_txn_begin: (%d) %s", rc, mdbx_strerror(rc) ); - } - - if ( db->dbi == 0 ) { - // FIXME: dbi_flags - rc = mdbx_dbi_open( db->txn, db->subdb, MDBX_CREATE, &db->dbi ); - if ( rc != MDBX_SUCCESS ) { - rmdbx_close_all( db ); - rb_raise( rmdbx_eDatabaseError, "mdbx_dbi_open: (%d) %s", rc, mdbx_strerror(rc) ); - } - } - - return; -} - - -/* - * Close any existing database transaction. If there is no - * active transaction, this is a no-op. If there is a long - * running transaction open, this is a no-op. - * - * +txnflag must either be RMDBX_TXN_ROLLBACK or RMDBX_TXN_COMMIT. - */ -void -rmdbx_close_txn( rmdbx_db_t *db, int txnflag ) -{ - if ( ! db->txn || db->state.retain_txn > -1 ) return; - - if ( txnflag == RMDBX_TXN_COMMIT ) { - mdbx_txn_commit( db->txn ); - } - else { - mdbx_txn_abort( db->txn ); - } - - db->txn = 0; - return; -} - - -/* - * call-seq: - * db.open_transaction( mode ) - * - * Open a new long-running transaction. If +mode+ is true, - * it is opened read/write. - * - */ -VALUE -rmdbx_rb_opentxn( VALUE self, VALUE mode ) -{ - UNWRAP_DB( self, db ); - CHECK_HANDLE; - - rmdbx_open_txn( db, RTEST(mode) ? MDBX_TXN_READWRITE : MDBX_TXN_RDONLY ); - db->state.retain_txn = RTEST(mode) ? 1 : 0; - - return Qtrue; -} - - -/* - * call-seq: - * db.close_transaction( mode ) - * - * Close a long-running transaction. If +write+ is true, - * the transaction is committed. Otherwise, rolled back. - * - */ -VALUE -rmdbx_rb_closetxn( VALUE self, VALUE write ) -{ - UNWRAP_DB( self, db ); - - db->state.retain_txn = -1; - rmdbx_close_txn( db, RTEST(write) ? RMDBX_TXN_COMMIT : RMDBX_TXN_ROLLBACK ); - return Qtrue; } @@ -323,6 +253,7 @@ rmdbx_drop( VALUE self, VALUE name ) name = rb_funcall( name, rb_intern("to_s"), 0 ); db->subdb = StringValueCStr( name ); + rmdbx_close_dbi( db ); /* ensure we're reopening within the new subdb */ rmdbx_open_txn( db, MDBX_TXN_READWRITE ); int rc = mdbx_drop( db->txn, db->dbi, true ); @@ -333,61 +264,319 @@ rmdbx_drop( VALUE self, VALUE name ) /* Reset the current collection to the top level. */ db->subdb = NULL; + rmdbx_close_dbi( db ); /* ensure next access is not in the defunct subdb */ + + return self; +} + + +/* call-seq: + * db.length -> Integer + * + * Returns the count of keys in the currently selected collection. + */ +VALUE +rmdbx_length( VALUE self ) +{ + UNWRAP_DB( self, db ); + MDBX_stat mstat; + + CHECK_HANDLE(); + rmdbx_open_txn( db, MDBX_TXN_RDONLY ); + + int rc = mdbx_dbi_stat( db->txn, db->dbi, &mstat, sizeof(mstat) ); + if ( rc != MDBX_SUCCESS ) + rb_raise( rmdbx_eDatabaseError, "mdbx_dbi_stat: (%d) %s", rc, mdbx_strerror(rc) ); + + VALUE rv = LONG2FIX( mstat.ms_entries ); + rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK ); + + return rv; +} + + +/* call-seq: + * db.include?( 'key' ) => bool + * + * Returns true if the current collection contains +key+. + */ +VALUE +rmdbx_include( VALUE self, VALUE key ) +{ + UNWRAP_DB( self, db ); + + CHECK_HANDLE(); + rmdbx_open_txn( db, MDBX_TXN_RDONLY ); + + MDBX_val ckey; + MDBX_val data; + rmdbx_key_for( key, &ckey ); + + int rc = mdbx_get( db->txn, db->dbi, &ckey, &data ); + rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK ); + xfree( ckey.iov_base ); + + switch ( rc ) { + case MDBX_SUCCESS: + return Qtrue; + + case MDBX_NOTFOUND: + return Qfalse; + + default: + rmdbx_close( self ); + rb_raise( rmdbx_eDatabaseError, "Unable to fetch key: (%d) %s", rc, mdbx_strerror(rc) ); + } +} + + +/* call-seq: + * db[ 'key' ] => value + * + * Return a single value for +key+ immediately. + */ +VALUE +rmdbx_get_val( VALUE self, VALUE key ) +{ + UNWRAP_DB( self, db ); + + CHECK_HANDLE(); + rmdbx_open_txn( db, MDBX_TXN_RDONLY ); + + MDBX_val ckey; + MDBX_val data; + + rmdbx_key_for( key, &ckey ); + int rc = mdbx_get( db->txn, db->dbi, &ckey, &data ); + rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK ); + xfree( ckey.iov_base ); + + VALUE rv; + switch ( rc ) { + case MDBX_SUCCESS: + rv = rb_str_new( data.iov_base, data.iov_len ); + return rmdbx_deserialize( self, rv ); + + case MDBX_NOTFOUND: + return Qnil; + + default: + rmdbx_close( self ); + rb_raise( rmdbx_eDatabaseError, "Unable to fetch value: (%d) %s", rc, mdbx_strerror(rc) ); + } +} + + +/* call-seq: + * db[ 'key' ] = value + * + * Set a single value for +key+. If the value is +nil+, the + * key is removed. + */ +VALUE +rmdbx_put_val( VALUE self, VALUE key, VALUE val ) +{ + int rc; + UNWRAP_DB( self, db ); + + CHECK_HANDLE(); + rmdbx_open_txn( db, MDBX_TXN_READWRITE ); + + MDBX_val ckey; + rmdbx_key_for( key, &ckey ); + + if ( NIL_P(val) ) { /* remove if set to nil */ + rc = mdbx_del( db->txn, db->dbi, &ckey, NULL ); + } + else { + MDBX_val old; + MDBX_val data; + rmdbx_val_for( self, val, &data ); + rc = mdbx_replace( db->txn, db->dbi, &ckey, &data, &old, 0 ); + xfree( data.iov_base ); + } + + rmdbx_close_txn( db, RMDBX_TXN_COMMIT ); + xfree( ckey.iov_base ); + + switch ( rc ) { + case MDBX_SUCCESS: + return val; + case MDBX_NOTFOUND: + return Qnil; + default: + rb_raise( rmdbx_eDatabaseError, "Unable to update value: (%d) %s", rc, mdbx_strerror(rc) ); + } +} + + +/* + * Return the currently selected collection, or +nil+ if at the + * top-level. + */ +VALUE +rmdbx_get_subdb( VALUE self ) +{ + UNWRAP_DB( self, db ); + return ( db->subdb == NULL ) ? Qnil : rb_str_new_cstr( db->subdb ); +} + + +/* + * Sets the current collection name for read/write operations. + * + */ +VALUE +rmdbx_set_subdb( VALUE self, VALUE name ) +{ + UNWRAP_DB( self, db ); + + /* Provide a friendlier error message if max_collections is 0. */ + if ( db->settings.max_collections == 0 ) + rb_raise( rmdbx_eDatabaseError, "Unable to change collection: collections are not enabled." ); + + /* All transactions must be closed when switching database handles. */ + if ( db->txn ) + rb_raise( rmdbx_eDatabaseError, "Unable to change collection: transaction open" ); + + db->subdb = NIL_P( name ) ? NULL : StringValueCStr( name ); + + /* Reset the db handle and issue a single transaction to reify + the collection. + */ + rmdbx_close_dbi( db ); + rmdbx_open_txn( db, MDBX_TXN_READWRITE ); + rmdbx_close_txn( db, RMDBX_TXN_COMMIT ); return self; } /* - * Given a ruby +arg+, convert and return a structure - * suitable for usage as a key for mdbx. All keys are explicitly - * converted to strings. - */ -MDBX_val -rmdbx_key_for( VALUE arg ) -{ - MDBX_val rv; - - arg = rb_funcall( arg, rb_intern("to_s"), 0 ); - rv.iov_len = RSTRING_LEN( arg ); - rv.iov_base = StringValuePtr( arg ); - - return rv; -} - - -/* - * Given a ruby +arg+, convert and return a structure - * suitable for usage as a value for mdbx. - */ -MDBX_val -rmdbx_val_for( VALUE self, VALUE arg ) -{ - MDBX_val rv; - VALUE serialize_proc; - - serialize_proc = rb_iv_get( self, "@serializer" ); - if ( ! NIL_P( serialize_proc ) ) - arg = rb_funcall( serialize_proc, rb_intern("call"), 1, arg ); - - rv.iov_len = RSTRING_LEN( arg ); - rv.iov_base = StringValuePtr( arg ); - - return rv; -} - - -/* - * Deserialize and return a value. + * call-seq: + * db.in_transaction? => false + * + * Predicate: return true if a transaction (or snapshot) + * is currently open. */ VALUE -rmdbx_deserialize( VALUE self, VALUE val ) +rmdbx_in_transaction_p( VALUE self ) { - VALUE deserialize_proc = rb_iv_get( self, "@deserializer" ); - if ( ! NIL_P( deserialize_proc ) ) - val = rb_funcall( deserialize_proc, rb_intern("call"), 1, val ); + UNWRAP_DB( self, db ); + return db->txn ? Qtrue : Qfalse; +} - return val; + +/* + * Open a new database transaction. If a transaction is already + * open, this is a no-op. + * + * +rwflag+ must be either MDBX_TXN_RDONLY or MDBX_TXN_READWRITE. + */ +void +rmdbx_open_txn( rmdbx_db_t *db, int rwflag ) +{ + if ( db->txn ) return; + + int rc = mdbx_txn_begin( db->env, NULL, rwflag, &db->txn ); + if ( rc != MDBX_SUCCESS ) { + rmdbx_close_all( db ); + rb_raise( rmdbx_eDatabaseError, "mdbx_txn_begin: (%d) %s", rc, mdbx_strerror(rc) ); + } + + if ( db->dbi == 0 ) { + rc = mdbx_dbi_open( db->txn, db->subdb, db->settings.db_flags, &db->dbi ); + if ( rc != MDBX_SUCCESS ) { + rmdbx_close_all( db ); + rb_raise( rmdbx_eDatabaseError, "mdbx_dbi_open: (%d) %s", rc, mdbx_strerror(rc) ); + } + } + + return; +} + + +/* + * Close any existing database transaction. If there is no + * active transaction, this is a no-op. If there is a long + * running transaction open, this is a no-op. + * + * +txnflag must either be RMDBX_TXN_ROLLBACK or RMDBX_TXN_COMMIT. + */ +void +rmdbx_close_txn( rmdbx_db_t *db, int txnflag ) +{ + if ( ! db->txn || db->state.retain_txn > -1 ) return; + + if ( txnflag == RMDBX_TXN_COMMIT ) { + mdbx_txn_commit( db->txn ); + } + else { + mdbx_txn_abort( db->txn ); + } + + db->txn = 0; + return; +} + + +/* + * call-seq: + * db.open_transaction( mode ) + * + * Open a new long-running transaction. If +mode+ is true, + * it is opened read/write. + * + */ +VALUE +rmdbx_rb_opentxn( VALUE self, VALUE mode ) +{ + UNWRAP_DB( self, db ); + CHECK_HANDLE(); + + rmdbx_open_txn( db, RTEST(mode) ? MDBX_TXN_READWRITE : MDBX_TXN_RDONLY ); + db->state.retain_txn = RTEST(mode) ? 1 : 0; + + return Qtrue; +} + + +/* + * call-seq: + * db.close_transaction( mode ) + * + * Close a long-running transaction. If +write+ is true, + * the transaction is committed. Otherwise, rolled back. + * + */ +VALUE +rmdbx_rb_closetxn( VALUE self, VALUE write ) +{ + UNWRAP_DB( self, db ); + + db->state.retain_txn = -1; + rmdbx_close_txn( db, RTEST(write) ? RMDBX_TXN_COMMIT : RMDBX_TXN_ROLLBACK ); + + return Qtrue; +} + + +/* + * Open a cursor for iteration. + */ +void +rmdbx_open_cursor( rmdbx_db_t *db ) +{ + CHECK_HANDLE(); + if ( ! db->txn ) rb_raise( rmdbx_eDatabaseError, "No snapshot or transaction currently open." ); + + int rc = mdbx_cursor_open( db->txn, db->dbi, &db->cursor ); + if ( rc != MDBX_SUCCESS ) { + rmdbx_close_all( db ); + rb_raise( rmdbx_eDatabaseError, "Unable to open cursor: (%d) %s", rc, mdbx_strerror(rc) ); + } + + return; } @@ -423,7 +612,7 @@ rmdbx_each_key( VALUE self ) UNWRAP_DB( self, db ); int state; - CHECK_HANDLE; + CHECK_HANDLE(); rmdbx_open_cursor( db ); RETURN_ENUMERATOR( self, 0, 0 ); @@ -472,7 +661,7 @@ rmdbx_each_value( VALUE self ) UNWRAP_DB( self, db ); int state; - CHECK_HANDLE; + CHECK_HANDLE(); rmdbx_open_cursor( db ); RETURN_ENUMERATOR( self, 0, 0 ); @@ -523,7 +712,7 @@ rmdbx_each_pair( VALUE self ) UNWRAP_DB( self, db ); int state; - CHECK_HANDLE; + CHECK_HANDLE(); rmdbx_open_cursor( db ); RETURN_ENUMERATOR( self, 0, 0 ); @@ -538,201 +727,6 @@ rmdbx_each_pair( VALUE self ) } - -/* call-seq: - * db.length -> Integer - * - * Returns the count of keys in the currently selected collection. - */ -VALUE -rmdbx_length( VALUE self ) -{ - UNWRAP_DB( self, db ); - MDBX_stat mstat; - - CHECK_HANDLE; - rmdbx_open_txn( db, MDBX_TXN_RDONLY ); - - int rc = mdbx_dbi_stat( db->txn, db->dbi, &mstat, sizeof(mstat) ); - if ( rc != MDBX_SUCCESS ) - rb_raise( rmdbx_eDatabaseError, "mdbx_dbi_stat: (%d) %s", rc, mdbx_strerror(rc) ); - - VALUE rv = LONG2FIX( mstat.ms_entries ); - rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK ); - - return rv; -} - - -/* call-seq: - * db.include?( 'key' ) => bool - * - * Returns true if the current collection contains +key+. - */ -VALUE -rmdbx_include( VALUE self, VALUE key ) -{ - int rc; - UNWRAP_DB( self, db ); - - CHECK_HANDLE; - rmdbx_open_txn( db, MDBX_TXN_RDONLY ); - - MDBX_val ckey = rmdbx_key_for( key ); - MDBX_val data; - rc = mdbx_get( db->txn, db->dbi, &ckey, &data ); - rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK ); - - switch ( rc ) { - case MDBX_SUCCESS: - return Qtrue; - - case MDBX_NOTFOUND: - return Qfalse; - - default: - rmdbx_close( self ); - rb_raise( rmdbx_eDatabaseError, "Unable to fetch key: (%d) %s", rc, mdbx_strerror(rc) ); - } -} - - -/* call-seq: - * db[ 'key' ] => value - * - * Return a single value for +key+ immediately. - */ -VALUE -rmdbx_get_val( VALUE self, VALUE key ) -{ - int rc; - UNWRAP_DB( self, db ); - - CHECK_HANDLE; - rmdbx_open_txn( db, MDBX_TXN_RDONLY ); - - MDBX_val ckey = rmdbx_key_for( key ); - MDBX_val data; - VALUE rv; - rc = mdbx_get( db->txn, db->dbi, &ckey, &data ); - rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK ); - - switch ( rc ) { - case MDBX_SUCCESS: - rv = rb_str_new( data.iov_base, data.iov_len ); - return rmdbx_deserialize( self, rv ); - - case MDBX_NOTFOUND: - return Qnil; - - default: - rmdbx_close( self ); - rb_raise( rmdbx_eDatabaseError, "Unable to fetch value: (%d) %s", rc, mdbx_strerror(rc) ); - } -} - - -/* call-seq: - * db[ 'key' ] = value - * - * Set a single value for +key+. If the value is +nil+, the - * key is removed. - */ -VALUE -rmdbx_put_val( VALUE self, VALUE key, VALUE val ) -{ - int rc; - UNWRAP_DB( self, db ); - - CHECK_HANDLE; - rmdbx_open_txn( db, MDBX_TXN_READWRITE ); - - MDBX_val ckey = rmdbx_key_for( key ); - - // FIXME: DUPSORT is enabled -- different api? - // See: MDBX_NODUPDATA / MDBX_NOOVERWRITE - if ( NIL_P(val) ) { /* remove if set to nil */ - rc = mdbx_del( db->txn, db->dbi, &ckey, NULL ); - } - else { - MDBX_val old; - MDBX_val data = rmdbx_val_for( self, val ); - rc = mdbx_replace( db->txn, db->dbi, &ckey, &data, &old, 0 ); - } - - rmdbx_close_txn( db, RMDBX_TXN_COMMIT ); - - 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) ); - } -} - - -/* - * call-seq: - * db.statistics => (hash of stats) - * - * Returns a hash populated with various metadata for the opened - * database. - * - */ -VALUE -rmdbx_stats( VALUE self ) -{ - UNWRAP_DB( self, db ); - CHECK_HANDLE; - - return rmdbx_gather_stats( db ); -} - - -/* - * Return the currently selected collection, or +nil+ if at the - * top-level. - */ -VALUE -rmdbx_get_subdb( VALUE self ) -{ - UNWRAP_DB( self, db ); - return ( db->subdb == NULL ) ? Qnil : rb_str_new_cstr( db->subdb ); -} - - -/* - * Sets the current collection name for read/write operations. - * - */ -VALUE -rmdbx_set_subdb( VALUE self, VALUE name ) -{ - UNWRAP_DB( self, db ); - - /* Provide a friendlier error message if max_collections is 0. */ - if ( db->settings.max_collections == 0 ) - rb_raise( rmdbx_eDatabaseError, "Unable to change collection: collections are not enabled." ); - - /* All transactions must be closed when switching database handles. */ - if ( db->txn ) - rb_raise( rmdbx_eDatabaseError, "Unable to change collection: transaction open" ); - - db->subdb = NIL_P( name ) ? NULL : StringValueCStr( name ); - - /* Reset the db handle and issue a single transaction to reify - the collection. - */ - rmdbx_close_dbi( db ); - rmdbx_open_txn( db, MDBX_TXN_READWRITE ); - rmdbx_close_txn( db, RMDBX_TXN_COMMIT ); - - return self; -} - - /* * Open an existing (or create a new) mdbx database at filesystem * +path+. In block form, the database is automatically closed. @@ -772,57 +766,88 @@ rmdbx_database_initialize( int argc, VALUE *argv, VALUE self ) db->state.open = 0; db->state.retain_txn = -1; db->settings.env_flags = MDBX_ENV_DEFAULTS; + db->settings.db_flags = MDBX_DB_DEFAULTS | MDBX_CREATE; db->settings.mode = 0644; db->settings.max_collections = 0; db->settings.max_readers = 0; db->settings.max_size = 0; - /* Options setup, overrides. - */ - opt = rb_hash_aref( opts, ID2SYM( rb_intern("mode") ) ); - if ( ! NIL_P(opt) ) db->settings.mode = FIX2INT( opt ); - opt = rb_hash_aref( opts, ID2SYM( rb_intern("max_collections") ) ); - if ( ! NIL_P(opt) ) db->settings.max_collections = FIX2INT( opt ); - opt = rb_hash_aref( opts, ID2SYM( rb_intern("max_readers") ) ); - if ( ! NIL_P(opt) ) db->settings.max_readers = FIX2INT( opt ); - opt = rb_hash_aref( opts, ID2SYM( rb_intern("max_size") ) ); - if ( ! NIL_P(opt) ) db->settings.max_size = NUM2LONG( opt ); - opt = rb_hash_aref( opts, ID2SYM( rb_intern("nosubdir") ) ); - if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOSUBDIR; - opt = rb_hash_aref( opts, ID2SYM( rb_intern("readonly") ) ); - if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_RDONLY; - opt = rb_hash_aref( opts, ID2SYM( rb_intern("exclusive") ) ); - if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_EXCLUSIVE; - opt = rb_hash_aref( opts, ID2SYM( rb_intern("compat") ) ); - if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_ACCEDE; - opt = rb_hash_aref( opts, ID2SYM( rb_intern("writemap") ) ); - if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_WRITEMAP; - opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_threadlocal") ) ); - if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOTLS; - opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_readahead") ) ); - if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NORDAHEAD; - opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_memory_init") ) ); - if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOMEMINIT; - opt = rb_hash_aref( opts, ID2SYM( rb_intern("coalesce") ) ); - if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_COALESCE; - opt = rb_hash_aref( opts, ID2SYM( rb_intern("lifo_reclaim") ) ); - if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_LIFORECLAIM; - opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_metasync") ) ); - if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOMETASYNC; - - /* Duplicate keys, on mdbx_dbi_open, maybe set here? */ - /* MDBX_DUPSORT = UINT32_C(0x04), */ - /* Set instance variables. */ rb_iv_set( self, "@path", path ); - rb_iv_set( self, "@options", opts ); + rb_iv_set( self, "@options", rb_hash_dup( opts ) ); + + /* Environment and database options setup, overrides. + */ + opt = rb_hash_delete( opts, ID2SYM( rb_intern("coalesce") ) ); + if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_COALESCE; + opt = rb_hash_delete( opts, ID2SYM( rb_intern("compatible") ) ); + if ( RTEST(opt) ) { + db->settings.db_flags = db->settings.db_flags | MDBX_DB_ACCEDE; + db->settings.env_flags = db->settings.env_flags | MDBX_ACCEDE; + } + /* opt = rb_hash_delete( opts, ID2SYM( rb_intern("duplicate_keys") ) ); */ + /* if ( RTEST(opt) ) db->settings.db_flags = db->settings.db_flags | MDBX_DUPSORT; */ + opt = rb_hash_delete( opts, ID2SYM( rb_intern("exclusive") ) ); + if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_EXCLUSIVE; + opt = rb_hash_delete( opts, ID2SYM( rb_intern("lifo_reclaim") ) ); + if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_LIFORECLAIM; + opt = rb_hash_delete( opts, ID2SYM( rb_intern("max_collections") ) ); + if ( ! NIL_P(opt) ) db->settings.max_collections = FIX2INT( opt ); + opt = rb_hash_delete( opts, ID2SYM( rb_intern("max_readers") ) ); + if ( ! NIL_P(opt) ) db->settings.max_readers = FIX2INT( opt ); + opt = rb_hash_delete( opts, ID2SYM( rb_intern("max_size") ) ); + if ( ! NIL_P(opt) ) db->settings.max_size = NUM2LONG( opt ); + opt = rb_hash_delete( opts, ID2SYM( rb_intern("mode") ) ); + if ( ! NIL_P(opt) ) db->settings.mode = FIX2INT( opt ); + opt = rb_hash_delete( opts, ID2SYM( rb_intern("no_memory_init") ) ); + if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOMEMINIT; + opt = rb_hash_delete( opts, ID2SYM( rb_intern("no_metasync") ) ); + if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOMETASYNC; + opt = rb_hash_delete( opts, ID2SYM( rb_intern("no_subdir") ) ); + if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOSUBDIR; + opt = rb_hash_delete( opts, ID2SYM( rb_intern("no_readahead") ) ); + if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NORDAHEAD; + opt = rb_hash_delete( opts, ID2SYM( rb_intern("no_threadlocal") ) ); + if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOTLS; + opt = rb_hash_delete( opts, ID2SYM( rb_intern("readonly") ) ); + if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_RDONLY; + opt = rb_hash_delete( opts, ID2SYM( rb_intern("writemap") ) ); + if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_WRITEMAP; + + /* Bail early on incompatible options. FIXME: I don't think this is true. */ + /* if ( db->settings.max_collections > 0 && */ + /* rmdbx_flag_enabled( db->settings.db_flags, MDBX_DUPSORT ) ) { */ + /* rb_raise( rb_eArgError, "Collections and multiple values per key are mutually exclusive options." ); */ + /* } */ + + if ( rb_hash_size_num(opts) > 0 ) { + rb_raise( rb_eArgError, "Unknown option(s): %"PRIsVALUE, opts ); + } rmdbx_open_env( self ); return self; } +/* + * call-seq: + * db.statistics => (hash of stats) + * + * Returns a hash populated with various metadata for the opened + * database. + * + */ +VALUE +rmdbx_stats( VALUE self ) +{ + UNWRAP_DB( self, db ); + CHECK_HANDLE(); + + return rmdbx_gather_stats( db ); +} + + /* * call-seq: * db.clone => [copy of db] @@ -872,17 +897,19 @@ rmdbx_init_database() rb_define_method( rmdbx_cDatabase, "close", rmdbx_close, 0 ); rb_define_method( rmdbx_cDatabase, "closed?", rmdbx_closed_p, 0 ); rb_define_method( rmdbx_cDatabase, "drop", rmdbx_drop, 1 ); - rb_define_method( rmdbx_cDatabase, "each_key", rmdbx_each_key, 0 ); - rb_define_method( rmdbx_cDatabase, "each_pair", rmdbx_each_pair, 0 ); - rb_define_method( rmdbx_cDatabase, "each_value", rmdbx_each_value, 0 ); - rb_define_method( rmdbx_cDatabase, "in_transaction?", rmdbx_in_transaction_p, 0 ); rb_define_method( rmdbx_cDatabase, "include?", rmdbx_include, 1 ); rb_define_method( rmdbx_cDatabase, "length", rmdbx_length, 0 ); rb_define_method( rmdbx_cDatabase, "reopen", rmdbx_open_env, 0 ); rb_define_method( rmdbx_cDatabase, "[]", rmdbx_get_val, 1 ); rb_define_method( rmdbx_cDatabase, "[]=", rmdbx_put_val, 2 ); + /* Enumerables */ + rb_define_method( rmdbx_cDatabase, "each_key", rmdbx_each_key, 0 ); + rb_define_method( rmdbx_cDatabase, "each_pair", rmdbx_each_pair, 0 ); + rb_define_method( rmdbx_cDatabase, "each_value", rmdbx_each_value, 0 ); + /* Manually open/close transactions from ruby. */ + rb_define_method( rmdbx_cDatabase, "in_transaction?", rmdbx_in_transaction_p, 0 ); rb_define_protected_method( rmdbx_cDatabase, "open_transaction", rmdbx_rb_opentxn, 1 ); rb_define_protected_method( rmdbx_cDatabase, "close_transaction", rmdbx_rb_closetxn, 1 ); @@ -895,3 +922,4 @@ rmdbx_init_database() rb_require( "mdbx/database" ); } + diff --git a/ext/mdbx_ext/mdbx_ext.h b/ext/mdbx_ext/mdbx_ext.h index a3f45bb..64bb63d 100644 --- a/ext/mdbx_ext/mdbx_ext.h +++ b/ext/mdbx_ext/mdbx_ext.h @@ -4,12 +4,23 @@ #include "mdbx.h" -#ifndef MDBX_EXT_0_9_3 -#define MDBX_EXT_0_9_3 +#ifndef RBMDBX_EXT +#define RBMDBX_EXT #define RMDBX_TXN_ROLLBACK 0 #define RMDBX_TXN_COMMIT 1 +/* Shortcut for fetching wrapped data structure. + */ +#define UNWRAP_DB( self, db ) \ + rmdbx_db_t *db; \ + TypedData_Get_Struct( self, rmdbx_db_t, &rmdbx_db_data, db ) + +/* Raise if current DB is not open. */ +#define CHECK_HANDLE() \ + if ( ! db->state.open ) rb_raise( rmdbx_eDatabaseError, "Closed database." ) + + /* * A struct encapsulating an instance's DB * state and settings. @@ -21,7 +32,8 @@ struct rmdbx_db { MDBX_cursor *cursor; struct { - int env_flags; + unsigned int env_flags; + unsigned int db_flags; int mode; int open; int max_collections; @@ -40,12 +52,11 @@ struct rmdbx_db { typedef struct rmdbx_db rmdbx_db_t; static const rb_data_type_t rmdbx_db_data; -extern void rmdbx_free( void *db ); /* forward declaration for the allocator */ + /* ------------------------------------------------------------ * Globals * ------------------------------------------------------------ */ - extern VALUE rmdbx_mMDBX; extern VALUE rmdbx_cDatabase; extern VALUE rmdbx_eDatabaseError; @@ -55,13 +66,15 @@ extern VALUE rmdbx_eRollback; /* ------------------------------------------------------------ * Functions * ------------------------------------------------------------ */ +extern void rmdbx_free( void *db ); /* forward declaration for the allocator */ extern void Init_rmdbx ( void ); extern void rmdbx_init_database ( void ); +extern void rmdbx_close_all( rmdbx_db_t* ); extern void rmdbx_open_txn( rmdbx_db_t*, int ); extern void rmdbx_close_txn( rmdbx_db_t*, int ); - +extern void rmdbx_open_cursor( rmdbx_db_t* ); extern VALUE rmdbx_gather_stats( rmdbx_db_t* ); -#endif /* define MDBX_EXT_0_9_3 */ +#endif /* define RBMDBX_EXT */ diff --git a/ext/mdbx_ext/stats.c b/ext/mdbx_ext/stats.c index 7ac003d..c645a80 100644 --- a/ext/mdbx_ext/stats.c +++ b/ext/mdbx_ext/stats.c @@ -3,8 +3,6 @@ * Expose a bunch of mdbx internals to ruby. * This is all largely stolen from mdbx_stat.c. * - * Entry point is rmdbx_stats() in database.c. - * */ #include "mdbx_ext.h" @@ -28,6 +26,32 @@ rmdbx_gather_build_stats( VALUE stat ) } +/* + * Grab current memory usage. (Available since MDBX 0.10.0). + */ +void +rmdbx_gather_memory_stats( VALUE stat ) +{ + if (! ( MDBX_VERSION_MAJOR >= 0 && MDBX_VERSION_MINOR >= 10 ) ) + return; + + VALUE mem = rb_hash_new(); + rb_hash_aset( stat, ID2SYM(rb_intern("system_memory")), mem ); + + intptr_t page_size; + intptr_t total_pages; + intptr_t avail_pages; + + mdbx_get_sysraminfo( &page_size, &total_pages, &avail_pages ); + + rb_hash_aset( mem, ID2SYM(rb_intern("pagesize")), LONG2FIX( page_size ) ); + rb_hash_aset( mem, ID2SYM(rb_intern("total_pages")), LONG2FIX( total_pages ) ); + rb_hash_aset( mem, ID2SYM(rb_intern("avail_pages")), LONG2FIX( avail_pages ) ); + + return; +} + + /* * Metadata for the database file. */ @@ -80,6 +104,16 @@ rmdbx_gather_environment_stats( rb_hash_aset( environ, ID2SYM(rb_intern("pagesize")), INT2NUM(mstat.ms_psize) ); + rb_hash_aset( environ, ID2SYM(rb_intern("branch_pages")), + LONG2NUM(mstat.ms_branch_pages) ); + rb_hash_aset( environ, ID2SYM(rb_intern("leaf_pages")), + LONG2NUM(mstat.ms_leaf_pages) ); + rb_hash_aset( environ, ID2SYM(rb_intern("overflow_pages")), + LONG2NUM(mstat.ms_overflow_pages) ); + rb_hash_aset( environ, ID2SYM(rb_intern("btree_depth")), + INT2NUM(mstat.ms_depth) ); + rb_hash_aset( environ, ID2SYM(rb_intern("entries")), + LONG2NUM(mstat.ms_entries) ); rb_hash_aset( environ, ID2SYM(rb_intern("last_txnid")), INT2NUM(menvinfo.mi_recent_txnid) ); rb_hash_aset( environ, ID2SYM(rb_intern("last_reader_txnid")), @@ -101,7 +135,7 @@ rmdbx_gather_environment_stats( * */ int -reader_list_callback( +rmdbx_reader_list_cb( void *ctx, int num, int slot, @@ -148,7 +182,7 @@ rmdbx_gather_reader_stats( { VALUE readers = rb_ary_new(); - mdbx_reader_list( db->env, reader_list_callback, (void*)readers ); + mdbx_reader_list( db->env, rmdbx_reader_list_cb, (void*)readers ); rb_hash_aset( stat, ID2SYM(rb_intern("readers")), readers ); return; @@ -168,6 +202,7 @@ rmdbx_gather_stats( rmdbx_db_t *db ) MDBX_stat mstat; MDBX_envinfo menvinfo; + rmdbx_gather_memory_stats( stat ); rmdbx_gather_build_stats( stat ); rmdbx_open_txn( db, MDBX_TXN_RDONLY ); @@ -183,9 +218,6 @@ rmdbx_gather_stats( rmdbx_db_t *db ) rmdbx_gather_environment_stats( stat, mstat, menvinfo ); rmdbx_gather_reader_stats( db, stat, mstat, menvinfo ); - /* TODO: database and subdatabase stats */ - return stat; } - diff --git a/lib/mdbx/database.rb b/lib/mdbx/database.rb index 1cd1293..a25114f 100644 --- a/lib/mdbx/database.rb +++ b/lib/mdbx/database.rb @@ -29,10 +29,23 @@ class MDBX::Database ### Unless otherwise mentioned, option keys are symbols, and values ### are boolean. ### - ### [:mode] - ### Whe creating a new database, set permissions to this 4 digit - ### octal number. Defaults to `0644`. Set to `0` to never automatically - ### create a new file, only opening existing databases. + ### [:coalesce] + ### Attempt to coalesce items for the garbage collector, + ### potentialy increasing the chance of unallocating storage + ### earlier. + ### + ### [:compatible] + ### Skip compatibility checks when opening an in-use database with + ### unknown or mismatched flag values. + ### + ### [:exclusive] + ### Access is restricted to the first opening process. Other attempts + ### to use this database (even in readonly mode) are denied. + ### + ### [:lifo_reclaim] + ### Recycle garbage collected items via LIFO, instead of FIFO. + ### Depending on underlying hardware (disk write-back cache), this + ### could increase write performance. ### ### [:max_collections] ### Set the maximum number of "subdatabase" collections allowed. By @@ -45,51 +58,38 @@ class MDBX::Database ### Set an upper boundary (in bytes) for the database map size. ### The default is 10485760 bytes. ### - ### [:nosubdir] + ### [:mode] + ### Whe creating a new database, set permissions to this 4 digit + ### octal number. Defaults to `0644`. Set to `0` to never automatically + ### create a new file, only opening existing databases. + ### + ### [:no_memory_init] + ### Skip initializing malloc'ed memory to zeroes before writing. + ### + ### [:no_metasync] + ### A system crash may sacrifice the last commit for a potentially + ### large write performance increase. Database integrity is + ### maintained. + ### + ### [:no_subdir] ### When creating a new database, don't put the data and lock file ### under a dedicated subdirectory. ### - ### [:readonly] - ### Reject any write attempts while using this database handle. - ### - ### [:exclusive] - ### Access is restricted to the first opening process. Other attempts - ### to use this database (even in readonly mode) are denied. - ### - ### [:compat] - ### Skip compatibility checks when opening an in-use database with - ### unknown or mismatched flag values. - ### - ### [:writemap] - ### Trade safety for speed for databases that fit within available - ### memory. (See MDBX documentation for details.) - ### - ### [:no_threadlocal] - ### Parallelize read-only transactions across threads. Writes are - ### always thread local. (See MDBX documentatoin for details.) - ### ### [:no_readahead] ### Disable all use of OS readahead. Potentially useful for ### random reads wunder low memory conditions. Default behavior ### is to dynamically choose when to use or omit readahead. ### - ### [:no_memory_init] - ### Skip initializing malloc'ed memory to zeroes before writing. + ### [:no_threadlocal] + ### Parallelize read-only transactions across threads. Writes are + ### always thread local. (See MDBX documentatoin for details.) ### - ### [:coalesce] - ### Attempt to coalesce items for the garbage collector, - ### potentialy increasing the chance of unallocating storage - ### earlier. + ### [:readonly] + ### Reject any write attempts while using this database handle. ### - ### [:lifo_reclaim] - ### Recycle garbage collected items via LIFO, instead of FIFO. - ### Depending on underlying hardware (disk write-back cache), this - ### could increase write performance. - ### - ### [:no_metasync] - ### A system crash may sacrifice the last commit for a potentially - ### large write performance increase. Database integrity is - ### maintained. + ### [:writemap] + ### Trade safety for speed for databases that fit within available + ### memory. (See MDBX documentation for details.) ### def self::open( *args, &block ) db = new( *args ) diff --git a/spec/mdbx/database_spec.rb b/spec/mdbx/database_spec.rb index 7b44d33..d82ce20 100644 --- a/spec/mdbx/database_spec.rb +++ b/spec/mdbx/database_spec.rb @@ -3,20 +3,18 @@ require_relative '../lib/helper' - RSpec.describe( MDBX::Database ) do - before( :all ) do - db = described_class.open( TEST_DATABASE.to_s ) - db.clear - db.close - end - it "disallows direct calls to #new" do expect{ described_class.new }. to raise_exception( NoMethodError, /private/ ) end + it "raises an exception if passed unknown options" do + expect{ described_class.open( TEST_DATABASE.to_s, nope: true ) }. + to raise_exception( ArgumentError, /unknown option/i ) + end + it "knows the env handle open/close state" do db = described_class.open( TEST_DATABASE.to_s ) expect( db.closed? ).to be_falsey @@ -52,12 +50,9 @@ RSpec.describe( MDBX::Database ) do let!( :db ) { described_class.open( TEST_DATABASE.to_s ) } - before( :each ) do - db.clear - end - after( :each ) do db.close + TEST_DATABASE.rmtree end it "can be reopened" do @@ -86,12 +81,9 @@ RSpec.describe( MDBX::Database ) do let!( :db ) { described_class.open( TEST_DATABASE.to_s ) } - before( :each ) do - db.clear - end - after( :each ) do db.close + TEST_DATABASE.rmtree end @@ -181,16 +173,23 @@ RSpec.describe( MDBX::Database ) do end + # context 'duplicate keys' do + # + # let( :db ) { described_class.open( TEST_DATABASE.to_s, max_collections: 5, duplicate_keys: true ) } + # + # after( :each ) do + # db.close + # end + # end + + context 'collections' do let!( :db ) { described_class.open( TEST_DATABASE.to_s, max_collections: 5 ) } - before( :each ) do - db.clear - end - after( :each ) do db.close + TEST_DATABASE.rmtree end it "fail if the max_collections option is not specified when opening" do @@ -324,6 +323,17 @@ RSpec.describe( MDBX::Database ) do expect( db['doom'] ).to be_nil end + it "retains other collections, only dropping what is specified" do + db.collection( 'boots' ) + db.collection( 'pants' ) + db.main + db.drop( 'boots' ) + + expect( db.collection ).to be_nil + expect( db['doom'] ).to be_nil + expect( db ).to have_key( :pants ) + end + it "retains the collection environment when clearing data" do db.collection( 'doom' ) db[ 'key' ] = 1 @@ -340,7 +350,7 @@ RSpec.describe( MDBX::Database ) do context 'transactions' do - let!( :db ) { described_class.open( TEST_DATABASE.to_s, max_collections: 5 ) } + let( :db ) { described_class.open( TEST_DATABASE.to_s, max_collections: 5 ) } before( :each ) do db.clear @@ -348,6 +358,7 @@ RSpec.describe( MDBX::Database ) do after( :each ) do db.close + TEST_DATABASE.rmtree end it "knows when a transaction is currently open" do @@ -447,7 +458,7 @@ RSpec.describe( MDBX::Database ) do context "iterators" do - let( :db ) { + let!( :db ) { described_class.open( TEST_DATABASE.to_s, max_collections: 5 ).collection( 'iter' ) } @@ -457,6 +468,7 @@ RSpec.describe( MDBX::Database ) do after( :each ) do db.close + TEST_DATABASE.rmtree end it "raises an exception if the caller didn't open a transaction first" do @@ -496,12 +508,13 @@ RSpec.describe( MDBX::Database ) do context "serialization" do - let( :db ) { + let!( :db ) { described_class.open( TEST_DATABASE.to_s ) } after( :each ) do db.close + TEST_DATABASE.rmtree end it "uses Marshalling as default" do