diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..ad1f57a --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,298 @@ +# vim: set nosta et ts=4 sw=4: +# +# Rubocop is known to have breaking changes. +# Disable everything by default, enable only the rulesets +# we're interested in. +# +--- +AllCops: + StyleGuideBaseURL: https://app.laika.com/intranet/it/RubyStyleGuide + DisabledByDefault: true + UseCache: true + MaxFilesInCache: 250 + AllowSymlinksInCacheRootDirectory: true + NewCops: enable + Exclude: + - gem.deps.rb + - "*.gemspec" + +# https://docs.rubocop.org/rubocop/0.93/cops_layout.html +# Matching the LAIKA style guide as much as possible. +# +Layout/AccessModifierIndentation: + Enabled: true + EnforcedStyle: indent +Layout/ArgumentAlignment: + Enabled: true + EnforcedStyle: with_fixed_indentation +Layout/ArrayAlignment: + Enabled: true + EnforcedStyle: with_first_element +Layout/AssignmentIndentation: + Enabled: true +Layout/BeginEndAlignment: + Enabled: true + EnforcedStyleAlignWith: start_of_line +Layout/BlockAlignment: + Enabled: true + EnforcedStyleAlignWith: either +Layout/BlockEndNewline: + Enabled: true +Layout/ClassStructure: + ExpectedOrder: + - module_inclusion + - constants + - association + - macros + - public_class_methods + - initializer + - public_attribute_macros + - public_delegate + - public_methods + - protected_attribute_macros + - protected_methods + - private_attribute_macros + - private_delegate + - private_methods +Layout/ClosingParenthesisIndentation: + Enabled: true +Layout/CommentIndentation: + Enabled: true +Layout/ConditionPosition: + Enabled: true +Layout/DotPosition: + Enabled: true + EnforcedStyle: trailing +Layout/ElseAlignment: + Enabled: false +Layout/EmptyComment: + Enabled: true + AllowBorderComment: true + AllowMarginComment: true +Layout/EmptyLines: + Enabled: false +Layout/EmptyLineAfterMagicComment: + Enabled: true +Layout/EmptyLineBetweenDefs: + Enabled: true + AllowAdjacentOneLineDefs: true + NumberOfEmptyLines: 2 +Layout/EmptyLinesAroundArguments: + Enabled: true +Layout/EmptyLinesAroundAttributeAccessor: + Enabled: true + AllowAliasSyntax: true +Layout/EndOfLine: + Enabled: true + EnforcedStyle: lf +Layout/ExtraSpacing: + Enabled: true + AllowForAlignment: true + AllowBeforeTrailingComments: true +Layout/HashAlignment: + Enabled: false # not configurable enough + EnforcedHashRocketStyle: table + EnforcedColonStyle: table +Layout/HeredocIndentation: + Enabled: true +Layout/IndentationConsistency: + Enabled: true +Layout/IndentationStyle: + Enabled: true + EnforcedStyle: tabs +Layout/IndentationWidth: + Enabled: true + Width: 1 +Layout/InitialIndentation: + Enabled: true +Layout/LeadingCommentSpace: + Enabled: true +Layout/LineLength: + Max: 100 +Layout/MultilineArrayBraceLayout: + Enabled: true + EnforcedStyle: symmetrical +Layout/MultilineArrayLineBreaks: + Enabled: true +Layout/MultilineAssignmentLayout: + Enabled: true + EnforcedStyle: same_line +Layout/MultilineHashBraceLayout: + Enabled: true + EnforcedStyle: symmetrical +Layout/MultilineHashKeyLineBreaks: + Enabled: true +Layout/MultilineMethodArgumentLineBreaks: + Enabled: false +Layout/MultilineMethodCallIndentation: + Enabled: true + EnforcedStyle: indented +Layout/RescueEnsureAlignment: + Enabled: true +Layout/SpaceAfterColon: + Enabled: true +Layout/SpaceAfterComma: + Enabled: true +Layout/SpaceAfterMethodName: + Enabled: true +Layout/SpaceAfterNot: + Enabled: false +Layout/SpaceAroundBlockParameters: + Enabled: true + EnforcedStyleInsidePipes: no_space +Layout/SpaceAroundEqualsInParameterDefault: + Enabled: true + EnforcedStyle: no_space +Layout/SpaceAroundKeyword: + Enabled: true +Layout/SpaceAroundMethodCallOperator: + Enabled: true +Layout/SpaceBeforeBlockBraces: + Enabled: true + EnforcedStyle: no_space +Layout/SpaceBeforeComma: + Enabled: true +Layout/SpaceBeforeComment: + Enabled: true +Layout/SpaceBeforeFirstArg: + Enabled: true +Layout/SpaceBeforeSemicolon: + Enabled: true +Layout/SpaceInLambdaLiteral: + Enabled: true + EnforcedStyle: require_no_space +Layout/SpaceInsideArrayLiteralBrackets: + Enabled: false # not configurable enough + EnforcedStyle: space +Layout/SpaceInsideBlockBraces: + Enabled: true + SpaceBeforeBlockParameters: false +Layout/SpaceInsideHashLiteralBraces: + Enabled: false # not configurable enough + EnforcedStyle: space +Layout/SpaceInsideParens: + Enabled: true + EnforcedStyle: space +Layout/SpaceInsideReferenceBrackets: + Enabled: true + EnforcedStyle: space +Layout/TrailingEmptyLines: + Enabled: true + EnforcedStyle: final_blank_line +Layout/TrailingWhitespace: + Enabled: true + +# https://docs.rubocop.org/rubocop/0.93/cops_lint.html +# Enabling everything, these are warnings, and seem generally reasonable. +# +Lint: + Enabled: true + +# https://docs.rubocop.org/rubocop/0.93/cops_naming.html +# +Naming/AccessorMethodName: + Enabled: true +Naming/BinaryOperatorParameterName: + Enabled: true +Naming/ClassAndModuleCamelCase: + Enabled: true +Naming/ConstantName: + Enabled: true +Naming/FileName: + Enabled: true +Naming/HeredocDelimiterCase: + Enabled: true +Naming/MemoizedInstanceVariableName: + Enabled: true + EnforcedStyleForLeadingUnderscores: optional +Naming/MethodName: + Enabled: true +Naming/PredicateName: + Enabled: true +Naming/VariableName: + Enabled: true + EnforcedStyle: snake_case + + +# https://docs.rubocop.org/rubocop/0.93/cops_style.html +# +Style/Documentation: + Enabled: true +Style/AccessorGrouping: + Enabled: true + EnforcedStyle: separated +Style/AndOr: + Enabled: true +Style/Attr: + Enabled: true +Style/AutoResourceCleanup: + Enabled: true +Style/BisectedAttrAccessor: + Enabled: true +Style/BlockDelimiters: + Enabled: true +Style/ClassMethods: + Enabled: true +Style/ClassMethodsDefinitions: + Enabled: true + EnforcedStyle: def_self +Style/DocumentationMethod: + Enabled: true +Style/EachForSimpleLoop: + Enabled: true +Style/EmptyBlockParameter: + Enabled: true +Style/For: + Enabled: true + EnforcedStyle: each +Style/GlobalVars: + Enabled: true +Style/HashSyntax: + Enabled: true + EnforcedStyle: no_mixed_keys +Style/IfWithSemicolon: + Enabled: true +Style/ImplicitRuntimeError: + Enabled: true +Style/MethodCallWithoutArgsParentheses: + Enabled: true +Style/MethodDefParentheses: + Enabled: true +Style/NegatedIf: + Enabled: true +Style/NegatedUnless: + Enabled: true +Style/NestedModifier: + Enabled: true +Style/Next: + Enabled: true +Style/NilComparison: + Enabled: true +Style/NonNilCheck: + Enabled: true +Style/Not: + Enabled: true +Style/OrAssignment: + Enabled: true +Style/RedundantConditional: + Enabled: true +Style/RedundantFileExtensionInRequire: + Enabled: true +Style/ReturnNil: + Enabled: true +Style/TrailingCommaInArguments: + Enabled: true + EnforcedStyleForMultiline: no_comma +Style/TrailingCommaInArrayLiteral: + Enabled: true + EnforcedStyleForMultiline: no_comma +Style/TrailingCommaInBlockArgs: + Enabled: true +Style/TrailingCommaInHashLiteral: + Enabled: true + EnforcedStyleForMultiline: no_comma +Style/TrivialAccessors: + Enabled: true +Style/UnlessElse: + Enabled: true + diff --git a/.ruby-version b/.ruby-version index 860487c..37c2961 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.1 +2.7.2 diff --git a/ext/mdbx_ext/database.c b/ext/mdbx_ext/database.c new file mode 100644 index 0000000..76f83fa --- /dev/null +++ b/ext/mdbx_ext/database.c @@ -0,0 +1,206 @@ +/* vim: set noet sta sw=4 ts=4 : */ + +#include "mdbx_ext.h" + +/* VALUE str = rb_sprintf( "path: %+"PRIsVALUE", opts: %+"PRIsVALUE, path, opts ); */ +/* printf( "%s\n", StringValueCStr(str) ); */ + + +/* 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 ); + + +/* + * A struct encapsulating an instance's DB state. + */ +struct rmdbx_db { + MDBX_env *env; + MDBX_dbi dbi; + MDBX_txn *txn; + MDBX_cursor *cursor; + int open; +}; +typedef struct rmdbx_db rmdbx_db_t; + + +/* + * Ruby allocation hook. + */ +void rmdbx_free( void *db ); /* forward declaration */ +static const rb_data_type_t rmdbx_db_data = { + .wrap_struct_name = "MDBX::Database::Data", + .function = { .dfree = rmdbx_free }, + .flags = RUBY_TYPED_FREE_IMMEDIATELY +}; + + +/* + * Allocate a DB environment onto the stack. + */ +VALUE +rmdbx_alloc( VALUE klass ) +{ + int *data = malloc( sizeof(rmdbx_db_t) ); + return TypedData_Wrap_Struct( klass, &rmdbx_db_data, data ); +} + + +/* + * Cleanup a previously allocated DB environment. + * FIXME: ... this should also close if not already closed? + */ +void +rmdbx_free( void *db ) +{ + if ( db ) free( db ); +} + + +/* + * Cleanly close an opened database. + */ +VALUE +rmdbx_close( VALUE self ) +{ + UNWRAP_DB( self, db ); + if ( db->cursor ) mdbx_cursor_close( db->cursor ); + if ( db->txn ) mdbx_txn_abort( db->txn ); + if ( db->dbi ) mdbx_dbi_close( db->env, db->dbi ); + if ( db->env ) mdbx_env_close( db->env ); + db->open = 0; + + // FIXME: or rather, maybe free() from here? + + return Qtrue; +} + + +/* + * call-seq: + * db.closed? #=> false + * + * Predicate: return true if the database has been closed, + * (or never was actually opened for some reason?) + */ +VALUE +rmdbx_closed_p( VALUE self ) +{ + UNWRAP_DB( self, db ); + return db->open == 1 ? Qfalse : Qtrue; +} + + +/* + * call-seq: + * MDBX::Database.open( path ) -> db + * MDBX::Database.open( path, options ) -> db + * MDBX::Database.open( path, options ) do |db| + * db... + * end + * + * Open an existing (or create a new) mdbx database at filesystem + * +path+. In block form, the database is automatically closed. + * + */ +VALUE +rmdbx_database_initialize( int argc, VALUE *argv, VALUE self ) +{ + int rc = 0; + int mode = 0644; + int db_flags = MDBX_ENV_DEFAULTS; + VALUE path, opts, opt; + + rb_scan_args( argc, argv, "11", &path, &opts ); + + /* Ensure options is a hash if it was passed in. + */ + if ( NIL_P(opts) ) { + opts = rb_hash_new(); + } + else { + Check_Type( opts, T_HASH ); + } + rb_hash_freeze( opts ); + + /* Options setup, overrides. + */ + 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; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("readonly") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_RDONLY; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("exclusive") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_EXCLUSIVE; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("compat") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_ACCEDE; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("writemap") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_WRITEMAP; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_threadlocal") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_NOTLS; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_readahead") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_NORDAHEAD; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_memory_init") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_NOMEMINIT; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("coalesce") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_COALESCE; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("lifo_reclaim") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_LIFORECLAIM; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_metasync") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_NOMETASYNC; + + + /* Initialize the DB vals. + */ + UNWRAP_DB( self, db ); + db->env = NULL; + db->dbi = 0; + db->txn = NULL; + db->cursor = NULL; + db->open = 0; + + /* 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) ); + + /* Open the DB handle on disk. + */ + rc = mdbx_env_open( db->env, StringValueCStr(path), db_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 ); + + return self; +} + + +/* + * Initialization for the MDBX::Database class. + */ +void +rmdbx_init_database() +{ + rmdbx_cDatabase = rb_define_class_under( rmdbx_mMDBX, "Database", rb_cData ); + + rb_define_alloc_func( rmdbx_cDatabase, rmdbx_alloc ); + + 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_require( "mdbx/database" ); +} + diff --git a/ext/mdbx_ext/mdbx_ext.c b/ext/mdbx_ext/mdbx_ext.c index 8a4ead8..574ad90 100644 --- a/ext/mdbx_ext/mdbx_ext.c +++ b/ext/mdbx_ext/mdbx_ext.c @@ -9,7 +9,14 @@ void Init_mdbx_ext() { - mdbx_mMDBX = rb_define_module( "MDBX" ); - rb_define_const( mdbx_mMDBX, "LIBRARY_VERSION", rb_str_new_cstr(mdbx_version.git.describe) ); + rmdbx_mMDBX = rb_define_module( "MDBX" ); + + /* The backend library version. */ + VALUE version = rb_str_new_cstr( mdbx_version.git.describe ); + rb_define_const( rmdbx_mMDBX, "LIBRARY_VERSION", version ); + + rmdbx_eDatabaseError = rb_define_class_under( rmdbx_mMDBX, "DatabaseError", rb_eRuntimeError ); + + rmdbx_init_database(); } diff --git a/ext/mdbx_ext/mdbx_ext.h b/ext/mdbx_ext/mdbx_ext.h index 07ac287..3b5e0e4 100644 --- a/ext/mdbx_ext/mdbx_ext.h +++ b/ext/mdbx_ext/mdbx_ext.h @@ -4,10 +4,25 @@ #include "mdbx.h" +#ifndef MDBX_EXT_0_9_2 +#define MDBX_EXT_0_9_2 + /* ------------------------------------------------------------ - Globals - ------------------------------------------------------------ */ + * Globals + * ------------------------------------------------------------ */ -VALUE mdbx_mMDBX; +VALUE rmdbx_mMDBX; +VALUE rmdbx_cDatabase; +VALUE rmdbx_eDatabaseError; + + +/* ------------------------------------------------------------ + * Functions + * ------------------------------------------------------------ */ +extern void Init_rmdbx ( void ); +extern void rmdbx_init_database ( void ); + + +#endif /* define MDBX_EXT_0_9_2 */ diff --git a/lib/mdbx.rb b/lib/mdbx.rb index 705c83e..9cd51b6 100644 --- a/lib/mdbx.rb +++ b/lib/mdbx.rb @@ -10,9 +10,11 @@ require 'mdbx_ext' module MDBX # The version of this gem. + # # Note: the MDBX library version can be found in # the 'LIBRARY_VERSION' constant. + # VERSION = '0.0.1' -end +end # module MDBX diff --git a/lib/mdbx/database.rb b/lib/mdbx/database.rb new file mode 100644 index 0000000..806bfc4 --- /dev/null +++ b/lib/mdbx/database.rb @@ -0,0 +1,43 @@ +# -*- ruby -*- +# vim: set nosta noet ts=4 sw=4 ft=ruby: +# encoding: utf-8 + +require 'mdbx' unless defined?( MDBX ) + + +# TODO: rdoc +# +class MDBX::Database + + + ### Open an existing (or create a new) mdbx database at filesystem + ### +path+. In block form, the database is automatically closed. + ### + ### MDBX::Database.open( path ) -> db + ### MDBX::Database.open( path, options ) -> db + ### MDBX::Database.open( path, options ) do |db| + ### db[ 'key' ] #=> value + ### end + ### + def self::open( *args, &block ) + db = new( *args ) + return db unless block_given? + + begin + yield db + ensure + db.close + end + end + + # Only instantiate Database objects via #open. + private_class_method :new + + # The options used to instantiate this database. + attr_reader :options + + # The path on disk of the database. + attr_reader :path + +end # class MDBX::Database + diff --git a/spec/data/testdb/mdbx.dat b/spec/data/testdb/mdbx.dat new file mode 100644 index 0000000..94a178e Binary files /dev/null and b/spec/data/testdb/mdbx.dat differ diff --git a/spec/data/testdb/mdbx.lck b/spec/data/testdb/mdbx.lck new file mode 100644 index 0000000..e69de29 diff --git a/spec/lib/helper.rb b/spec/lib/helper.rb index e80ba3f..dbca1f7 100644 --- a/spec/lib/helper.rb +++ b/spec/lib/helper.rb @@ -8,11 +8,19 @@ if ENV[ 'COVERAGE' ] SimpleCov.start end +require 'pathname' require 'rspec' require 'mdbx' +module MDBX::Testing + TEST_DATABASE = Pathname( __FILE__ ).parent.parent + 'data' + 'testdb' +end + + RSpec.configure do |config| + include MDBX::Testing + config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true expectations.syntax = :expect @@ -32,7 +40,7 @@ RSpec.configure do |config| config.run_all_when_everything_filtered = true # config.warnings = true - # config.include( Zyre::Testing ) + config.include( MDBX::Testing ) # config.include( Loggability::SpecHelpers ) end diff --git a/spec/mdbx/database_spec.rb b/spec/mdbx/database_spec.rb new file mode 100644 index 0000000..48ea47d --- /dev/null +++ b/spec/mdbx/database_spec.rb @@ -0,0 +1,13 @@ +#!/usr/bin/env rspec -cfd + +require_relative '../lib/helper' + + +RSpec.describe( MDBX::Database ) do + + it "disallows direct calls to #new" do + expect{ described_class.new }. + to raise_exception( NoMethodError, /private/ ) + end +end +