diff --git a/ext/mdbx_ext/database.c b/ext/mdbx_ext/database.c index 76f83fa..5cbd68f 100644 --- a/ext/mdbx_ext/database.c +++ b/ext/mdbx_ext/database.c @@ -22,6 +22,7 @@ struct rmdbx_db { MDBX_dbi dbi; MDBX_txn *txn; MDBX_cursor *cursor; + int env_flags; int open; }; typedef struct rmdbx_db rmdbx_db_t; @@ -94,6 +95,122 @@ rmdbx_closed_p( VALUE self ) } +/* + * Open a new database transaction. + * + * +rwflag+ must be either MDBX_TXN_RDONLY or MDBX_TXN_READWRITE. + */ +void +rmdbx_open_txn( VALUE self, int rwflag ) +{ + int rc; + UNWRAP_DB( self, db ); + + rc = mdbx_txn_begin( db->env, NULL, rwflag, &db->txn); + if ( rc != MDBX_SUCCESS ) { + rmdbx_close( self ); + 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, NULL, 0, &db->dbi ); + if ( rc != MDBX_SUCCESS ) { + rmdbx_close( self ); + rb_raise( rmdbx_eDatabaseError, "mdbx_dbi_open: (%d) %s", rc, mdbx_strerror(rc) ); + } + } + + return; +} + + +/* + * Given a ruby +arg+, convert and return a structure + * suitable for usage as a key for mdbx. + */ +MDBX_val +rmdbx_vec_for( VALUE arg ) +{ + MDBX_val rv; + + // FIXME: arbitrary data types! + rv.iov_len = RSTRING_LEN( arg ); + rv.iov_base = StringValuePtr( arg ); + + return rv; +} + + +/* call-seq: + * db[ 'key' ] #=> value + * + * Convenience method: return a single value for +key+ immediately. + */ +VALUE +rmdbx_get_val( VALUE self, VALUE key ) +{ + int rc; + UNWRAP_DB( self, db ); + + if ( RTEST(rmdbx_closed_p(self)) ) rb_raise( rmdbx_eDatabaseError, "Closed database." ); + + rmdbx_open_txn( self, MDBX_TXN_RDONLY ); + + MDBX_val ckey = rmdbx_vec_for( key ); + MDBX_val data; + rc = mdbx_get( db->txn, db->dbi, &ckey, &data ); + mdbx_txn_abort( db->txn ); + + switch ( rc ) { + case MDBX_SUCCESS: + // FIXME: arbitrary data types! + return rb_str_new2( data.iov_base ); + + 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 #=> value + * + * Convenience method: set a single value for +key+ + */ +VALUE +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." ); + + rmdbx_open_txn( self, MDBX_TXN_READWRITE ); + + MDBX_val ckey = rmdbx_vec_for( key ); + MDBX_val data = rmdbx_vec_for( val ); + rc = mdbx_get( db->txn, db->dbi, &ckey, &data ); + + // FIXME: DUPSORT is enabled -- different api? + // See: MDBX_NODUPDATA / MDBX_NOOVERWRITE + rc = mdbx_put( db->txn, db->dbi, &ckey, &data, 0 ); + + mdbx_txn_commit( db->txn ); + + switch ( rc ) { + case MDBX_SUCCESS: + return val; + default: + rb_raise( rmdbx_eDatabaseError, "Unable to fetch value: (%d) %s", rc, mdbx_strerror(rc) ); + } +} + + /* * call-seq: * MDBX::Database.open( path ) -> db @@ -111,7 +228,7 @@ rmdbx_database_initialize( int argc, VALUE *argv, VALUE self ) { int rc = 0; int mode = 0644; - int db_flags = MDBX_ENV_DEFAULTS; + int env_flags = MDBX_ENV_DEFAULTS; VALUE path, opts, opt; rb_scan_args( argc, argv, "11", &path, &opts ); @@ -131,37 +248,41 @@ 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("nosubdir") ) ); - if ( RTEST(opt) ) db_flags = db_flags | MDBX_NOSUBDIR; + if ( RTEST(opt) ) env_flags = env_flags | MDBX_NOSUBDIR; opt = rb_hash_aref( opts, ID2SYM( rb_intern("readonly") ) ); - if ( RTEST(opt) ) db_flags = db_flags | MDBX_RDONLY; + if ( RTEST(opt) ) env_flags = env_flags | MDBX_RDONLY; opt = rb_hash_aref( opts, ID2SYM( rb_intern("exclusive") ) ); - if ( RTEST(opt) ) db_flags = db_flags | MDBX_EXCLUSIVE; + if ( RTEST(opt) ) env_flags = env_flags | MDBX_EXCLUSIVE; opt = rb_hash_aref( opts, ID2SYM( rb_intern("compat") ) ); - if ( RTEST(opt) ) db_flags = db_flags | MDBX_ACCEDE; + if ( RTEST(opt) ) env_flags = env_flags | MDBX_ACCEDE; opt = rb_hash_aref( opts, ID2SYM( rb_intern("writemap") ) ); - if ( RTEST(opt) ) db_flags = db_flags | MDBX_WRITEMAP; + if ( RTEST(opt) ) env_flags = env_flags | MDBX_WRITEMAP; opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_threadlocal") ) ); - if ( RTEST(opt) ) db_flags = db_flags | MDBX_NOTLS; + if ( RTEST(opt) ) env_flags = env_flags | MDBX_NOTLS; opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_readahead") ) ); - if ( RTEST(opt) ) db_flags = db_flags | MDBX_NORDAHEAD; + if ( RTEST(opt) ) env_flags = env_flags | MDBX_NORDAHEAD; opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_memory_init") ) ); - if ( RTEST(opt) ) db_flags = db_flags | MDBX_NOMEMINIT; + if ( RTEST(opt) ) env_flags = env_flags | MDBX_NOMEMINIT; opt = rb_hash_aref( opts, ID2SYM( rb_intern("coalesce") ) ); - if ( RTEST(opt) ) db_flags = db_flags | MDBX_COALESCE; + if ( RTEST(opt) ) env_flags = env_flags | MDBX_COALESCE; opt = rb_hash_aref( opts, ID2SYM( rb_intern("lifo_reclaim") ) ); - if ( RTEST(opt) ) db_flags = db_flags | MDBX_LIFORECLAIM; + if ( RTEST(opt) ) env_flags = env_flags | MDBX_LIFORECLAIM; opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_metasync") ) ); - if ( RTEST(opt) ) db_flags = db_flags | MDBX_NOMETASYNC; + if ( RTEST(opt) ) env_flags = env_flags | MDBX_NOMETASYNC; + + /* Duplicate keys, on mdbx_dbi_open, maybe set here? */ + /* MDBX_DUPSORT = UINT32_C(0x04), */ /* Initialize the DB vals. */ UNWRAP_DB( self, db ); - db->env = NULL; - db->dbi = 0; - db->txn = NULL; - db->cursor = NULL; - db->open = 0; + db->env = NULL; + db->dbi = 0; + db->txn = NULL; + db->cursor = NULL; + db->env_flags = env_flags; + db->open = 0; /* Allocate an mdbx environment. */ @@ -171,7 +292,7 @@ rmdbx_database_initialize( int argc, VALUE *argv, VALUE self ) /* Open the DB handle on disk. */ - rc = mdbx_env_open( db->env, StringValueCStr(path), db_flags, mode ); + 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) ); @@ -197,9 +318,11 @@ rmdbx_init_database() rb_define_alloc_func( rmdbx_cDatabase, rmdbx_alloc ); + rb_define_protected_method( rmdbx_cDatabase, "initialize", rmdbx_database_initialize, -1 ); rb_define_method( rmdbx_cDatabase, "close", rmdbx_close, 0 ); rb_define_method( rmdbx_cDatabase, "closed?", rmdbx_closed_p, 0 ); - rb_define_protected_method( rmdbx_cDatabase, "initialize", rmdbx_database_initialize, -1 ); + rb_define_method( rmdbx_cDatabase, "[]", rmdbx_get_val, 1 ); + rb_define_method( rmdbx_cDatabase, "[]=", rmdbx_put_val, 2 ); rb_require( "mdbx/database" ); } diff --git a/spec/mdbx/database_spec.rb b/spec/mdbx/database_spec.rb index 48ea47d..d05d7ee 100644 --- a/spec/mdbx/database_spec.rb +++ b/spec/mdbx/database_spec.rb @@ -9,5 +9,13 @@ RSpec.describe( MDBX::Database ) do expect{ described_class.new }. to raise_exception( NoMethodError, /private/ ) end + + it "knows the db handle open/close state" do + db = described_class.open( TEST_DATABASE.to_s ) + expect( db.closed? ).to be_falsey + db.close + expect( db.closed? ).to be_truthy + end + end