Checkpoint commit.

Fleshing out collections, automatic serialization of values.  Stringify
all keys.

FossilOrigin-Name: 8bb5e27eacd18bc34b60309a03cdce31921f790c821dab89bf12cd9bfc19825d
This commit is contained in:
Mahlon E. Smith 2020-12-16 08:29:50 +00:00
parent 9f1388a8de
commit d92ba7c5eb
3 changed files with 154 additions and 24 deletions

View file

@ -26,6 +26,7 @@ struct rmdbx_db {
MDBX_cursor *cursor; MDBX_cursor *cursor;
int env_flags; int env_flags;
int open; int open;
char *subdb;
}; };
typedef struct rmdbx_db rmdbx_db_t; typedef struct rmdbx_db rmdbx_db_t;
@ -57,7 +58,7 @@ rmdbx_alloc( VALUE klass )
* removed. * removed.
*/ */
void void
rmdbx_destroy( rmdbx_db_t* db ) rmdbx_close_all( rmdbx_db_t* db )
{ {
if ( db->cursor ) mdbx_cursor_close( db->cursor ); if ( db->cursor ) mdbx_cursor_close( db->cursor );
if ( db->txn ) mdbx_txn_abort( db->txn ); if ( db->txn ) mdbx_txn_abort( db->txn );
@ -74,20 +75,20 @@ void
rmdbx_free( void *db ) rmdbx_free( void *db )
{ {
if ( db ) { if ( db ) {
rmdbx_destroy( db ); rmdbx_close_all( db );
free( db ); free( db );
} }
} }
/* /*
* Cleanly close an opened database. * Cleanly close an opened database from Ruby.
*/ */
VALUE VALUE
rmdbx_close( VALUE self ) rmdbx_close( VALUE self )
{ {
UNWRAP_DB( self, db ); UNWRAP_DB( self, db );
rmdbx_destroy( db ); rmdbx_close_all( db );
return Qtrue; return Qtrue;
} }
@ -96,8 +97,7 @@ rmdbx_close( VALUE self )
* call-seq: * call-seq:
* db.closed? #=> false * db.closed? #=> false
* *
* Predicate: return true if the database has been closed, * Predicate: return true if the database handle is closed.
* (or never was actually opened for some reason?)
*/ */
VALUE VALUE
rmdbx_closed_p( VALUE self ) rmdbx_closed_p( VALUE self )
@ -126,7 +126,7 @@ rmdbx_open_txn( VALUE self, int rwflag )
if ( db->dbi == 0 ) { if ( db->dbi == 0 ) {
// FIXME: dbi_flags // FIXME: dbi_flags
rc = mdbx_dbi_open( db->txn, NULL, 0, &db->dbi ); rc = mdbx_dbi_open( db->txn, db->subdb, MDBX_CREATE, &db->dbi );
if ( rc != MDBX_SUCCESS ) { if ( rc != MDBX_SUCCESS ) {
rmdbx_close( self ); rmdbx_close( self );
rb_raise( rmdbx_eDatabaseError, "mdbx_dbi_open: (%d) %s", rc, mdbx_strerror(rc) ); rb_raise( rmdbx_eDatabaseError, "mdbx_dbi_open: (%d) %s", rc, mdbx_strerror(rc) );
@ -137,16 +137,61 @@ rmdbx_open_txn( VALUE self, int rwflag )
} }
/*
* call-seq:
* db.destroy
*
* Empty the database (or subdatabase) on disk. Unrecoverable.
*/
VALUE
rmdbx_destroy( 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;
return Qnil;
}
/* /*
* Given a ruby +arg+, convert and return a structure * Given a ruby +arg+, convert and return a structure
* suitable for usage as a key for mdbx. * suitable for usage as a key for mdbx. All keys are explicitly
* converted to strings.
*/ */
MDBX_val MDBX_val
rmdbx_vec_for( VALUE arg ) rmdbx_key_for( VALUE arg )
{ {
MDBX_val rv; MDBX_val rv;
// FIXME: arbitrary data types! 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_len = RSTRING_LEN( arg );
rv.iov_base = StringValuePtr( arg ); rv.iov_base = StringValuePtr( arg );
@ -163,21 +208,27 @@ VALUE
rmdbx_get_val( VALUE self, VALUE key ) rmdbx_get_val( VALUE self, VALUE key )
{ {
int rc; int rc;
VALUE deserialize_proc;
UNWRAP_DB( self, db ); UNWRAP_DB( self, db );
if ( RTEST(rmdbx_closed_p(self)) ) rb_raise( rmdbx_eDatabaseError, "Closed database." ); if ( RTEST(rmdbx_closed_p(self)) ) rb_raise( rmdbx_eDatabaseError, "Closed database." );
rmdbx_open_txn( self, MDBX_TXN_RDONLY ); rmdbx_open_txn( self, MDBX_TXN_RDONLY );
MDBX_val ckey = rmdbx_vec_for( key ); MDBX_val ckey = rmdbx_key_for( key );
MDBX_val data; MDBX_val data;
rc = mdbx_get( db->txn, db->dbi, &ckey, &data ); rc = mdbx_get( db->txn, db->dbi, &ckey, &data );
mdbx_txn_abort( db->txn ); mdbx_txn_abort( db->txn );
switch ( rc ) { switch ( rc ) {
case MDBX_SUCCESS: case MDBX_SUCCESS:
// FIXME: arbitrary data types! 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) );
}
else {
return rb_str_new2( data.iov_base ); return rb_str_new2( data.iov_base );
}
case MDBX_NOTFOUND: case MDBX_NOTFOUND:
return Qnil; return Qnil;
@ -203,7 +254,8 @@ rmdbx_put_val( VALUE self, VALUE key, VALUE val )
if ( RTEST(rmdbx_closed_p(self)) ) rb_raise( rmdbx_eDatabaseError, "Closed database." ); if ( RTEST(rmdbx_closed_p(self)) ) rb_raise( rmdbx_eDatabaseError, "Closed database." );
rmdbx_open_txn( self, MDBX_TXN_READWRITE ); rmdbx_open_txn( self, MDBX_TXN_READWRITE );
MDBX_val ckey = rmdbx_vec_for( key );
MDBX_val ckey = rmdbx_key_for( key );
// FIXME: DUPSORT is enabled -- different api? // FIXME: DUPSORT is enabled -- different api?
// See: MDBX_NODUPDATA / MDBX_NOOVERWRITE // See: MDBX_NODUPDATA / MDBX_NOOVERWRITE
@ -212,7 +264,7 @@ rmdbx_put_val( VALUE self, VALUE key, VALUE val )
} }
else { else {
MDBX_val old; MDBX_val old;
MDBX_val data = rmdbx_vec_for( val ); MDBX_val data = rmdbx_val_for( self, val );
rc = mdbx_replace( db->txn, db->dbi, &ckey, &data, &old, 0 ); rc = mdbx_replace( db->txn, db->dbi, &ckey, &data, &old, 0 );
} }
@ -227,6 +279,45 @@ rmdbx_put_val( VALUE self, VALUE key, VALUE val )
} }
/*
* call-seq:
* db.collection( 'collection_name' ) # => db
* db.collection( nil ) # => db (main)
*
* Operate on a sub-database "collection". Passing +nil+
* sets the database to the main, top-level namespace.
*
*/
VALUE
rmdbx_set_subdb( int argc, VALUE *argv, VALUE self )
/* rmdbx_set_subdb( VALUE self, VALUE subdb ) */
{
UNWRAP_DB( self, db );
VALUE subdb;
rb_scan_args( argc, argv, "01", &subdb );
if ( argc == 0 ) {
if ( db->subdb == NULL ) return Qnil;
return rb_str_new2( 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
//
if ( db->dbi ) {
mdbx_dbi_close( db->env, db->dbi );
db->dbi = 0;
}
return self;
}
/* /*
* call-seq: * call-seq:
* MDBX::Database.open( path ) -> db * MDBX::Database.open( path ) -> db
@ -299,6 +390,7 @@ rmdbx_database_initialize( int argc, VALUE *argv, VALUE self )
db->cursor = NULL; db->cursor = NULL;
db->env_flags = env_flags; db->env_flags = env_flags;
db->open = 0; db->open = 0;
db->subdb = NULL;
/* Allocate an mdbx environment. /* Allocate an mdbx environment.
*/ */
@ -306,6 +398,9 @@ rmdbx_database_initialize( int argc, VALUE *argv, VALUE self )
if ( rc != MDBX_SUCCESS ) if ( rc != MDBX_SUCCESS )
rb_raise( rmdbx_eDatabaseError, "mdbx_env_create: (%d) %s", rc, mdbx_strerror(rc) ); 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. /* Open the DB handle on disk.
*/ */
rc = mdbx_env_open( db->env, StringValueCStr(path), env_flags, mode ); rc = mdbx_env_open( db->env, StringValueCStr(path), env_flags, mode );
@ -335,8 +430,10 @@ rmdbx_init_database()
rb_define_alloc_func( rmdbx_cDatabase, rmdbx_alloc ); rb_define_alloc_func( rmdbx_cDatabase, rmdbx_alloc );
rb_define_protected_method( rmdbx_cDatabase, "initialize", rmdbx_database_initialize, -1 ); 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, "close", rmdbx_close, 0 );
rb_define_method( rmdbx_cDatabase, "closed?", rmdbx_closed_p, 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, "[]", rmdbx_get_val, 1 ); rb_define_method( rmdbx_cDatabase, "[]", rmdbx_get_val, 1 );
rb_define_method( rmdbx_cDatabase, "[]=", rmdbx_put_val, 2 ); rb_define_method( rmdbx_cDatabase, "[]=", rmdbx_put_val, 2 );

View file

@ -21,8 +21,11 @@ class MDBX::Database
### ###
def self::open( *args, &block ) def self::open( *args, &block )
db = new( *args ) db = new( *args )
return db unless block_given?
db.serializer = ->( v ) { Marshal.dump( v ) }
db.deserializer = ->( v ) { Marshal.load( v ) }
if block_given?
begin begin
yield db yield db
ensure ensure
@ -30,6 +33,10 @@ class MDBX::Database
end end
end end
return db
end
# Only instantiate Database objects via #open. # Only instantiate Database objects via #open.
private_class_method :new private_class_method :new
@ -39,5 +46,11 @@ class MDBX::Database
# The path on disk of the database. # The path on disk of the database.
attr_reader :path attr_reader :path
# A Proc for automatically serializing values.
attr_accessor :serializer
# A Proc for automatically deserializing values.
attr_accessor :deserializer
end # class MDBX::Database end # class MDBX::Database

View file

@ -17,14 +17,24 @@ RSpec.describe( MDBX::Database ) do
expect( db.closed? ).to be_truthy expect( db.closed? ).to be_truthy
end end
context 'an opened database' do it "closes itself automatically when used in block form" do
db = described_class.open( TEST_DATABASE.to_s ) do |db|
before( :each ) do expect( db.closed? ).to be_falsey
@db = described_class.open( TEST_DATABASE.to_s ) end
expect( db.closed? ).to be_truthy
end end
context 'an opened database' do
let!( :db ) { described_class.open( TEST_DATABASE.to_s ) }
after( :each ) do after( :each ) do
@db.close db.close
end
it "knows its own path" do
expect( db.path ).to match( %r|data/testdb$| )
end end
it "fails if opened again within the same process" do it "fails if opened again within the same process" do
@ -36,6 +46,16 @@ RSpec.describe( MDBX::Database ) do
to raise_exception( MDBX::DatabaseError, /environment is already used/ ) to raise_exception( MDBX::DatabaseError, /environment is already used/ )
end end
it "defaults to the top-level namespace" do
expect( db.collection ).to be_nil
end
it "can set a collection namespace" do
db.collection( 'bucket' )
expect( db.collection ).to eq( 'bucket' )
# TODO: set/retrieve data
end
end end
end end