mdbx 0.1.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e88f3e5ea2e66b7e924500a33f25f635c712e359bd0f8e19c52f60605bcafe46
4
- data.tar.gz: c069a2b838b86f1cc9873613ad1b99c1f888f24eab395c9f264d830aa7ba810d
3
+ metadata.gz: 678369c278e65d4faf56eb814e2cb7eba22da57fb5c07c6e4f9dc4e6edd7f09f
4
+ data.tar.gz: 0fc4b455b4556bb932f867fa40e7c240f555a5a6bbb7d09cb55c68ed7745bfd0
5
5
  SHA512:
6
- metadata.gz: 6d575ad77ff4c72382d13852d0d075c8b8caa664f8e55ace63e0e638695cd2cff43d2580484abff76fce0cf4efa60b8423d4330d65d1a1f39da07c8cd93ea7e9
7
- data.tar.gz: 895d6aee64d76e6e2b62071ae7d1cc98aa4d692d2b377c0d04e6f9f5ebb7495f23db7dafa66c8a2cdaf6a9a1925f003fca6d02e4646e4a23bf415167050dc0b4
6
+ metadata.gz: b9844be65fc6e46df8dbaee242ce9bf185c4330559d2a5d8c803eca43f4d09a783276c3d2a20258ef84be099a133031acf0f0b36ff0c2af1f69d0882a0dd4335
7
+ data.tar.gz: 22fd3c418db16d2a58100ad6bd8f3ae7b7ddce2172075713a798065440e59f6a2fd4cea47bd37d3585781dbeaebc0421f483e20375a5e652fe3a81ee76e0da2f
checksums.yaml.gz.sig CHANGED
Binary file
data.tar.gz.sig CHANGED
Binary file
data/History.md CHANGED
@@ -1,6 +1,68 @@
1
1
  # Release History for MDBX
2
2
 
3
3
  ---
4
+ ## v0.3.2 [2021-07-13] Mahlon E. Smith <mahlon@martini.nu>
5
+
6
+ Bugfixes:
7
+
8
+ - Fix double memory allocation during initialization.
9
+ - Make various ruby->c string allocations safe with the garbage collector.
10
+
11
+
12
+ Minutiae:
13
+
14
+ - Raise exception if instantiating with invalid options.
15
+
16
+
17
+ ---
18
+ ## v0.3.1 [2021-05-16] Mahlon E. Smith <mahlon@martini.nu>
19
+
20
+ Bugfix:
21
+
22
+ - #drop could potentially remove unintended data. Yanked version
23
+ v0.3.0.
24
+
25
+
26
+ ---
27
+ ## v0.3.0 [2021-04-09] Mahlon E. Smith <mahlon@martini.nu>
28
+
29
+ Enhancements:
30
+
31
+ - Alter the behavior of #clear, so it doesn't destroy collection
32
+ environments, but just empties them.
33
+
34
+ - Add #drop, which explictly -does- destroy a collection environment.
35
+
36
+ - Switching to a collection now automatically creates its environment.
37
+
38
+ - Add include? and has_key?, for presence checks without allocating
39
+ value memory or requiring deserialization.
40
+
41
+
42
+ Bugfixes:
43
+
44
+ - Run all cursor methods through rb_protect, to ensure proper
45
+ cursor cleanup in the event of an exception mid iteration.
46
+
47
+ - Fix the block form of collections to support multiple scopes.
48
+
49
+
50
+ ## v0.2.1 [2021-04-06] Mahlon E. Smith <mahlon@martini.nu>
51
+
52
+ Enhancement:
53
+
54
+ - Automatically stringify any argument to the collection() method.
55
+
56
+
57
+ ## v0.2.0 [2021-03-19] Mahlon E. Smith <mahlon@martini.nu>
58
+
59
+ Enhancement:
60
+
61
+ - Support dup/clone. This has limited use, as there can only
62
+ be one open handle per process, but implemented in the interests
63
+ of avoiding unexpected behavior.
64
+
65
+
4
66
  ## v0.1.1 [2021-03-14] Mahlon E. Smith <mahlon@martini.nu>
5
67
 
6
68
  Bugfix:
data/README.md CHANGED
@@ -340,13 +340,6 @@ information about the build environment, the database environment, and
340
340
  the currently connected clients.
341
341
 
342
342
 
343
- ## TODO
344
-
345
- - Expose more database/collection information to statistics
346
- - Support libmdbx multiple values per key DUPSORT via `put`, `get`
347
- Enumerators, and a 'value' argument for `delete`.
348
-
349
-
350
343
  ## Contributing
351
344
 
352
345
  You can check out the current development source with Mercurial via its
@@ -1,18 +1,16 @@
1
- /* vim: set noet sta sw=4 ts=4 : */
2
-
3
- #include "mdbx_ext.h"
4
-
5
- /* Shortcut for fetching current DB variables.
1
+ /* vim: set noet sta sw=4 ts=4 fdm=marker: */
2
+ /*
3
+ * Primary database handle functions.
4
+ *
6
5
  */
7
- #define UNWRAP_DB( val, db ) \
8
- rmdbx_db_t *db; \
9
- TypedData_Get_Struct( val, rmdbx_db_t, &rmdbx_db_data, db );
10
6
 
7
+ #include "mdbx_ext.h"
11
8
 
12
9
  VALUE rmdbx_cDatabase;
13
10
 
11
+
14
12
  /*
15
- * Ruby allocation hook.
13
+ * Ruby data allocation wrapper.
16
14
  */
17
15
  static const rb_data_type_t rmdbx_db_data = {
18
16
  .wrap_struct_name = "MDBX::Database::Data",
@@ -27,11 +25,24 @@ static const rb_data_type_t rmdbx_db_data = {
27
25
  VALUE
28
26
  rmdbx_alloc( VALUE klass )
29
27
  {
30
- rmdbx_db_t *new = RB_ALLOC( rmdbx_db_t );
28
+ rmdbx_db_t *new;
31
29
  return TypedData_Make_Struct( klass, rmdbx_db_t, &rmdbx_db_data, new );
32
30
  }
33
31
 
34
32
 
33
+ /*
34
+ * Cleanup a previously allocated DB environment.
35
+ */
36
+ void
37
+ rmdbx_free( void *db )
38
+ {
39
+ if ( db ) {
40
+ rmdbx_close_all( db );
41
+ xfree( db );
42
+ }
43
+ }
44
+
45
+
35
46
  /*
36
47
  * Ensure all database file descriptors are collected and
37
48
  * removed.
@@ -62,19 +73,9 @@ rmdbx_close_dbi( rmdbx_db_t *db )
62
73
 
63
74
 
64
75
  /*
65
- * Cleanup a previously allocated DB environment.
66
- */
67
- void
68
- rmdbx_free( void *db )
69
- {
70
- if ( db ) {
71
- rmdbx_close_all( db );
72
- xfree( db );
73
- }
74
- }
75
-
76
-
77
- /*
76
+ * call-seq:
77
+ * db.close => true
78
+ *
78
79
  * Cleanly close an opened database.
79
80
  */
80
81
  VALUE
@@ -101,17 +102,63 @@ rmdbx_closed_p( VALUE self )
101
102
 
102
103
 
103
104
  /*
104
- * call-seq:
105
- * db.in_transaction? => false
105
+ * Check if a given +flag+ is enabled for flag +val+.
106
+ */
107
+ int
108
+ rmdbx_flag_enabled( val, flag )
109
+ {
110
+ return ( val & flag ) == flag;
111
+ }
112
+
113
+
114
+ /*
115
+ * Given a ruby string +key+ and a pointer to an MDBX_val, prepare the
116
+ * key for usage within mdbx. All keys are explicitly converted to
117
+ * strings.
106
118
  *
107
- * Predicate: return true if a transaction (or snapshot)
108
- * is currently open.
119
+ */
120
+ void
121
+ rmdbx_key_for( VALUE key, MDBX_val *ckey )
122
+ {
123
+ VALUE key_str = rb_funcall( key, rb_intern("to_s"), 0 );
124
+ ckey->iov_len = RSTRING_LEN( key_str );
125
+ ckey->iov_base = malloc( ckey->iov_len );
126
+ strlcpy( ckey->iov_base, StringValuePtr(key_str), ckey->iov_len + 1 );
127
+ }
128
+
129
+
130
+ /*
131
+ * Given a ruby +value+ and a pointer to an MDBX_val, prepare
132
+ * the value for usage within mdbx. Values are potentially serialized.
133
+ *
134
+ */
135
+ void
136
+ rmdbx_val_for( VALUE self, VALUE val, MDBX_val *data )
137
+ {
138
+ VALUE serialize_proc = rb_iv_get( self, "@serializer" );
139
+
140
+ if ( ! NIL_P( serialize_proc ) )
141
+ val = rb_funcall( serialize_proc, rb_intern("call"), 1, val );
142
+
143
+ Check_Type( val, T_STRING );
144
+
145
+ data->iov_len = RSTRING_LEN( val );
146
+ data->iov_base = malloc( data->iov_len );
147
+ strlcpy( data->iov_base, StringValuePtr(val), data->iov_len + 1 );
148
+ }
149
+
150
+
151
+ /*
152
+ * Deserialize and return a value.
109
153
  */
110
154
  VALUE
111
- rmdbx_in_transaction_p( VALUE self )
155
+ rmdbx_deserialize( VALUE self, VALUE val )
112
156
  {
113
- UNWRAP_DB( self, db );
114
- return db->txn ? Qtrue : Qfalse;
157
+ VALUE deserialize_proc = rb_iv_get( self, "@deserializer" );
158
+ if ( ! NIL_P( deserialize_proc ) )
159
+ val = rb_funcall( deserialize_proc, rb_intern("call"), 1, val );
160
+
161
+ return val;
115
162
  }
116
163
 
117
164
 
@@ -148,138 +195,65 @@ rmdbx_open_env( VALUE self )
148
195
  rmdbx_close_all( db );
149
196
  rb_raise( rmdbx_eDatabaseError, "mdbx_env_open: (%d) %s", rc, mdbx_strerror(rc) );
150
197
  }
151
- db->state.open = 1;
152
198
 
199
+ db->state.open = 1;
153
200
  return Qtrue;
154
201
  }
155
202
 
156
203
 
157
- /*
158
- * Open a cursor for iteration.
159
- */
160
- void
161
- rmdbx_open_cursor( rmdbx_db_t *db )
162
- {
163
- if ( ! db->state.open ) rb_raise( rmdbx_eDatabaseError, "Closed database." );
164
- if ( ! db->txn ) rb_raise( rmdbx_eDatabaseError, "No snapshot or transaction currently open." );
165
-
166
- int rc = mdbx_cursor_open( db->txn, db->dbi, &db->cursor );
167
- if ( rc != MDBX_SUCCESS ) {
168
- rmdbx_close_all( db );
169
- rb_raise( rmdbx_eDatabaseError, "Unable to open cursor: (%d) %s", rc, mdbx_strerror(rc) );
170
- }
171
-
172
- return;
173
- }
174
-
175
-
176
- /*
177
- * Open a new database transaction. If a transaction is already
178
- * open, this is a no-op.
179
- *
180
- * +rwflag+ must be either MDBX_TXN_RDONLY or MDBX_TXN_READWRITE.
181
- */
182
- void
183
- rmdbx_open_txn( rmdbx_db_t *db, int rwflag )
184
- {
185
- if ( db->txn ) return;
186
-
187
- int rc = mdbx_txn_begin( db->env, NULL, rwflag, &db->txn );
188
- if ( rc != MDBX_SUCCESS ) {
189
- rmdbx_close_all( db );
190
- rb_raise( rmdbx_eDatabaseError, "mdbx_txn_begin: (%d) %s", rc, mdbx_strerror(rc) );
191
- }
192
-
193
- if ( db->dbi == 0 ) {
194
- // FIXME: dbi_flags
195
- rc = mdbx_dbi_open( db->txn, db->subdb, MDBX_CREATE, &db->dbi );
196
- if ( rc != MDBX_SUCCESS ) {
197
- rmdbx_close_all( db );
198
- rb_raise( rmdbx_eDatabaseError, "mdbx_dbi_open: (%d) %s", rc, mdbx_strerror(rc) );
199
- }
200
- }
201
-
202
- return;
203
- }
204
-
205
-
206
- /*
207
- * Close any existing database transaction. If there is no
208
- * active transaction, this is a no-op. If there is a long
209
- * running transaction open, this is a no-op.
210
- *
211
- * +txnflag must either be RMDBX_TXN_ROLLBACK or RMDBX_TXN_COMMIT.
212
- */
213
- void
214
- rmdbx_close_txn( rmdbx_db_t *db, int txnflag )
215
- {
216
- if ( ! db->txn || db->state.retain_txn > -1 ) return;
217
-
218
- switch ( txnflag ) {
219
- case RMDBX_TXN_COMMIT:
220
- mdbx_txn_commit( db->txn );
221
- default:
222
- mdbx_txn_abort( db->txn );
223
- }
224
-
225
- db->txn = 0;
226
- return;
227
- }
228
-
229
-
230
204
  /*
231
205
  * call-seq:
232
- * db.open_transaction( mode )
233
- *
234
- * Open a new long-running transaction. If +mode+ is true,
235
- * it is opened read/write.
206
+ * db.clear
236
207
  *
208
+ * Empty the current collection on disk. If collections are not enabled
209
+ * or the database handle is set to the top-level (main) db - this
210
+ * deletes *all records* from the database.
237
211
  */
238
212
  VALUE
239
- rmdbx_rb_opentxn( VALUE self, VALUE mode )
213
+ rmdbx_clear( VALUE self )
240
214
  {
241
215
  UNWRAP_DB( self, db );
242
216
 
243
- rmdbx_open_txn( db, RTEST(mode) ? MDBX_TXN_READWRITE : MDBX_TXN_RDONLY );
244
- db->state.retain_txn = RTEST(mode) ? 1 : 0;
217
+ rmdbx_open_txn( db, MDBX_TXN_READWRITE );
218
+ int rc = mdbx_drop( db->txn, db->dbi, false );
245
219
 
246
- return Qtrue;
220
+ if ( rc != MDBX_SUCCESS )
221
+ rb_raise( rmdbx_eDatabaseError, "mdbx_drop: (%d) %s", rc, mdbx_strerror(rc) );
222
+
223
+ rmdbx_close_txn( db, RMDBX_TXN_COMMIT );
224
+
225
+ return Qnil;
247
226
  }
248
227
 
249
228
 
250
229
  /*
251
230
  * call-seq:
252
- * db.close_transaction( mode )
253
- *
254
- * Close a long-running transaction. If +write+ is true,
255
- * the transaction is committed. Otherwise, rolled back.
231
+ * db.drop( collection ) -> db
256
232
  *
233
+ * Destroy a collection. You must be in the top level database to call
234
+ * this method.
257
235
  */
258
236
  VALUE
259
- rmdbx_rb_closetxn( VALUE self, VALUE write )
237
+ rmdbx_drop( VALUE self, VALUE name )
260
238
  {
261
239
  UNWRAP_DB( self, db );
262
240
 
263
- db->state.retain_txn = -1;
264
- rmdbx_close_txn( db, RTEST(write) ? RMDBX_TXN_COMMIT : RMDBX_TXN_ROLLBACK );
241
+ /* Provide a friendlier error message if max_collections is 0. */
242
+ if ( db->settings.max_collections == 0 )
243
+ rb_raise( rmdbx_eDatabaseError, "Unable to drop collection: collections are not enabled." );
265
244
 
266
- return Qtrue;
267
- }
245
+ /* All transactions must be closed when dropping a database. */
246
+ if ( db->txn )
247
+ rb_raise( rmdbx_eDatabaseError, "Unable to drop collection: transaction open" );
268
248
 
249
+ /* A drop can only be performed from the top-level database. */
250
+ if ( db->subdb != NULL )
251
+ rb_raise( rmdbx_eDatabaseError, "Unable to drop collection: switch to top-level db first" );
269
252
 
270
- /*
271
- * call-seq:
272
- * db.clear
273
- *
274
- * Empty the current collection on disk. If collections are not enabled
275
- * or the database handle is set to the top-level (main) db - this
276
- * deletes *all records* from the database. This is not recoverable!
277
- */
278
- VALUE
279
- rmdbx_clear( VALUE self )
280
- {
281
- UNWRAP_DB( self, db );
253
+ name = rb_funcall( name, rb_intern("to_s"), 0 );
254
+ db->subdb = StringValueCStr( name );
282
255
 
256
+ rmdbx_close_dbi( db ); /* ensure we're reopening within the new subdb */
283
257
  rmdbx_open_txn( db, MDBX_TXN_READWRITE );
284
258
  int rc = mdbx_drop( db->txn, db->dbi, true );
285
259
 
@@ -288,345 +262,478 @@ rmdbx_clear( VALUE self )
288
262
 
289
263
  rmdbx_close_txn( db, RMDBX_TXN_COMMIT );
290
264
 
291
- /* Refresh the environment handles. */
292
- rmdbx_open_env( self );
265
+ /* Reset the current collection to the top level. */
266
+ db->subdb = NULL;
267
+ rmdbx_close_dbi( db ); /* ensure next access is not in the defunct subdb */
293
268
 
294
- return Qnil;
269
+ return self;
295
270
  }
296
271
 
297
272
 
298
- /*
299
- * Given a ruby +arg+, convert and return a structure
300
- * suitable for usage as a key for mdbx. All keys are explicitly
301
- * converted to strings.
273
+ /* call-seq:
274
+ * db.length -> Integer
275
+ *
276
+ * Returns the count of keys in the currently selected collection.
302
277
  */
303
- MDBX_val
304
- rmdbx_key_for( VALUE arg )
278
+ VALUE
279
+ rmdbx_length( VALUE self )
305
280
  {
306
- MDBX_val rv;
281
+ UNWRAP_DB( self, db );
282
+ MDBX_stat mstat;
283
+
284
+ CHECK_HANDLE();
285
+ rmdbx_open_txn( db, MDBX_TXN_RDONLY );
286
+
287
+ int rc = mdbx_dbi_stat( db->txn, db->dbi, &mstat, sizeof(mstat) );
288
+ if ( rc != MDBX_SUCCESS )
289
+ rb_raise( rmdbx_eDatabaseError, "mdbx_dbi_stat: (%d) %s", rc, mdbx_strerror(rc) );
307
290
 
308
- arg = rb_funcall( arg, rb_intern("to_s"), 0 );
309
- rv.iov_len = RSTRING_LEN( arg );
310
- rv.iov_base = StringValuePtr( arg );
291
+ VALUE rv = LONG2FIX( mstat.ms_entries );
292
+ rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK );
311
293
 
312
294
  return rv;
313
295
  }
314
296
 
315
297
 
316
- /*
317
- * Given a ruby +arg+, convert and return a structure
318
- * suitable for usage as a value for mdbx.
298
+ /* call-seq:
299
+ * db.include?( 'key' ) => bool
300
+ *
301
+ * Returns true if the current collection contains +key+.
319
302
  */
320
- MDBX_val
321
- rmdbx_val_for( VALUE self, VALUE arg )
303
+ VALUE
304
+ rmdbx_include( VALUE self, VALUE key )
322
305
  {
323
- MDBX_val rv;
324
- VALUE serialize_proc;
306
+ UNWRAP_DB( self, db );
325
307
 
326
- serialize_proc = rb_iv_get( self, "@serializer" );
327
- if ( ! NIL_P( serialize_proc ) )
328
- arg = rb_funcall( serialize_proc, rb_intern("call"), 1, arg );
308
+ CHECK_HANDLE();
309
+ rmdbx_open_txn( db, MDBX_TXN_RDONLY );
329
310
 
330
- rv.iov_len = RSTRING_LEN( arg );
331
- rv.iov_base = StringValuePtr( arg );
311
+ MDBX_val ckey;
312
+ MDBX_val data;
313
+ rmdbx_key_for( key, &ckey );
332
314
 
333
- return rv;
334
- }
315
+ int rc = mdbx_get( db->txn, db->dbi, &ckey, &data );
316
+ rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK );
317
+ xfree( ckey.iov_base );
335
318
 
319
+ switch ( rc ) {
320
+ case MDBX_SUCCESS:
321
+ return Qtrue;
336
322
 
337
- /*
338
- * Deserialize and return a value.
339
- */
340
- VALUE
341
- rmdbx_deserialize( VALUE self, VALUE val )
342
- {
343
- VALUE deserialize_proc = rb_iv_get( self, "@deserializer" );
344
- if ( ! NIL_P( deserialize_proc ) )
345
- val = rb_funcall( deserialize_proc, rb_intern("call"), 1, val );
323
+ case MDBX_NOTFOUND:
324
+ return Qfalse;
346
325
 
347
- return val;
326
+ default:
327
+ rmdbx_close( self );
328
+ rb_raise( rmdbx_eDatabaseError, "Unable to fetch key: (%d) %s", rc, mdbx_strerror(rc) );
329
+ }
348
330
  }
349
331
 
350
332
 
351
333
  /* call-seq:
352
- * db.each_key {|key| block } => self
334
+ * db[ 'key' ] => value
353
335
  *
354
- * Calls the block once for each key, returning self.
355
- * A transaction must be opened prior to use.
336
+ * Return a single value for +key+ immediately.
356
337
  */
357
338
  VALUE
358
- rmdbx_each_key( VALUE self )
339
+ rmdbx_get_val( VALUE self, VALUE key )
359
340
  {
360
341
  UNWRAP_DB( self, db );
361
- MDBX_val key, data;
362
-
363
- rmdbx_open_cursor( db );
364
- RETURN_ENUMERATOR( self, 0, 0 );
365
342
 
366
- if ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_FIRST ) == MDBX_SUCCESS ) {
367
- rb_yield( rb_str_new( key.iov_base, key.iov_len ) );
368
- while ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_NEXT ) == MDBX_SUCCESS ) {
369
- rb_yield( rb_str_new( key.iov_base, key.iov_len ) );
370
- }
371
- }
343
+ CHECK_HANDLE();
344
+ rmdbx_open_txn( db, MDBX_TXN_RDONLY );
372
345
 
373
- mdbx_cursor_close( db->cursor );
374
- db->cursor = NULL;
375
- return self;
376
- }
346
+ MDBX_val ckey;
347
+ MDBX_val data;
377
348
 
349
+ rmdbx_key_for( key, &ckey );
350
+ int rc = mdbx_get( db->txn, db->dbi, &ckey, &data );
351
+ rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK );
352
+ xfree( ckey.iov_base );
378
353
 
379
- /* call-seq:
380
- * db.each_value {|value| block } => self
354
+ VALUE rv;
355
+ switch ( rc ) {
356
+ case MDBX_SUCCESS:
357
+ rv = rb_str_new( data.iov_base, data.iov_len );
358
+ return rmdbx_deserialize( self, rv );
359
+
360
+ case MDBX_NOTFOUND:
361
+ return Qnil;
362
+
363
+ default:
364
+ rmdbx_close( self );
365
+ rb_raise( rmdbx_eDatabaseError, "Unable to fetch value: (%d) %s", rc, mdbx_strerror(rc) );
366
+ }
367
+ }
368
+
369
+
370
+ /* call-seq:
371
+ * db[ 'key' ] = value
381
372
  *
382
- * Calls the block once for each value, returning self.
383
- * A transaction must be opened prior to use.
373
+ * Set a single value for +key+. If the value is +nil+, the
374
+ * key is removed.
384
375
  */
385
376
  VALUE
386
- rmdbx_each_value( VALUE self )
377
+ rmdbx_put_val( VALUE self, VALUE key, VALUE val )
387
378
  {
379
+ int rc;
388
380
  UNWRAP_DB( self, db );
389
- MDBX_val key, data;
390
381
 
391
- rmdbx_open_cursor( db );
392
- RETURN_ENUMERATOR( self, 0, 0 );
382
+ CHECK_HANDLE();
383
+ rmdbx_open_txn( db, MDBX_TXN_READWRITE );
393
384
 
394
- if ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_FIRST ) == MDBX_SUCCESS ) {
395
- VALUE rv = rb_str_new( data.iov_base, data.iov_len );
396
- rb_yield( rmdbx_deserialize( self, rv ) );
385
+ MDBX_val ckey;
386
+ rmdbx_key_for( key, &ckey );
397
387
 
398
- while ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_NEXT ) == MDBX_SUCCESS ) {
399
- rv = rb_str_new( data.iov_base, data.iov_len );
400
- rb_yield( rmdbx_deserialize( self, rv ) );
401
- }
388
+ if ( NIL_P(val) ) { /* remove if set to nil */
389
+ rc = mdbx_del( db->txn, db->dbi, &ckey, NULL );
390
+ }
391
+ else {
392
+ MDBX_val old;
393
+ MDBX_val data;
394
+ rmdbx_val_for( self, val, &data );
395
+ rc = mdbx_replace( db->txn, db->dbi, &ckey, &data, &old, 0 );
396
+ xfree( data.iov_base );
402
397
  }
403
398
 
404
- mdbx_cursor_close( db->cursor );
405
- db->cursor = NULL;
406
- return self;
399
+ rmdbx_close_txn( db, RMDBX_TXN_COMMIT );
400
+ xfree( ckey.iov_base );
401
+
402
+ switch ( rc ) {
403
+ case MDBX_SUCCESS:
404
+ return val;
405
+ case MDBX_NOTFOUND:
406
+ return Qnil;
407
+ default:
408
+ rb_raise( rmdbx_eDatabaseError, "Unable to update value: (%d) %s", rc, mdbx_strerror(rc) );
409
+ }
407
410
  }
408
411
 
409
412
 
410
- /* call-seq:
411
- * db.each_pair {|key, value| block } => self
413
+ /*
414
+ * Return the currently selected collection, or +nil+ if at the
415
+ * top-level.
416
+ */
417
+ VALUE
418
+ rmdbx_get_subdb( VALUE self )
419
+ {
420
+ UNWRAP_DB( self, db );
421
+ return ( db->subdb == NULL ) ? Qnil : rb_str_new_cstr( db->subdb );
422
+ }
423
+
424
+
425
+ /*
426
+ * Sets the current collection name for read/write operations.
412
427
  *
413
- * Calls the block once for each key and value, returning self.
414
- * A transaction must be opened prior to use.
415
428
  */
416
429
  VALUE
417
- rmdbx_each_pair( VALUE self )
430
+ rmdbx_set_subdb( VALUE self, VALUE name )
418
431
  {
419
432
  UNWRAP_DB( self, db );
420
- MDBX_val key, data;
421
433
 
422
- rmdbx_open_cursor( db );
423
- RETURN_ENUMERATOR( self, 0, 0 );
434
+ /* Provide a friendlier error message if max_collections is 0. */
435
+ if ( db->settings.max_collections == 0 )
436
+ rb_raise( rmdbx_eDatabaseError, "Unable to change collection: collections are not enabled." );
424
437
 
425
- if ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_FIRST ) == MDBX_SUCCESS ) {
426
- VALUE rkey = rb_str_new( key.iov_base, key.iov_len );
427
- VALUE rval = rb_str_new( data.iov_base, data.iov_len );
428
- rb_yield( rb_assoc_new( rkey, rmdbx_deserialize( self, rval ) ) );
438
+ /* All transactions must be closed when switching database handles. */
439
+ if ( db->txn )
440
+ rb_raise( rmdbx_eDatabaseError, "Unable to change collection: transaction open" );
429
441
 
430
- while ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_NEXT ) == MDBX_SUCCESS ) {
431
- rkey = rb_str_new( key.iov_base, key.iov_len );
432
- rval = rb_str_new( data.iov_base, data.iov_len );
433
- rb_yield( rb_assoc_new( rkey, rmdbx_deserialize( self, rval ) ) );
434
- }
442
+ xfree( db->subdb );
443
+ db->subdb = NULL;
444
+
445
+ if ( ! NIL_P(name) ) {
446
+ size_t len = RSTRING_LEN( name ) + 1;
447
+ db->subdb = malloc( len );
448
+ strlcpy( db->subdb, StringValuePtr(name), len );
435
449
  }
436
450
 
437
- mdbx_cursor_close( db->cursor );
438
- db->cursor = NULL;
451
+ /* Reset the db handle and issue a single transaction to reify
452
+ the collection.
453
+ */
454
+ rmdbx_close_dbi( db );
455
+ rmdbx_open_txn( db, MDBX_TXN_READWRITE );
456
+ rmdbx_close_txn( db, RMDBX_TXN_COMMIT );
457
+
439
458
  return self;
440
459
  }
441
460
 
442
461
 
443
- /* call-seq:
444
- * db.length -> Integer
462
+ /*
463
+ * call-seq:
464
+ * db.in_transaction? => false
445
465
  *
446
- * Returns the count of keys in the currently selected collection.
466
+ * Predicate: return true if a transaction (or snapshot)
467
+ * is currently open.
447
468
  */
448
469
  VALUE
449
- rmdbx_length( VALUE self )
470
+ rmdbx_in_transaction_p( VALUE self )
450
471
  {
451
472
  UNWRAP_DB( self, db );
452
- MDBX_stat mstat;
473
+ return db->txn ? Qtrue : Qfalse;
474
+ }
453
475
 
454
- if ( ! db->state.open ) rb_raise( rmdbx_eDatabaseError, "Closed database." );
455
- rmdbx_open_txn( db, MDBX_TXN_RDONLY );
456
476
 
457
- int rc = mdbx_dbi_stat( db->txn, db->dbi, &mstat, sizeof(mstat) );
458
- if ( rc != MDBX_SUCCESS )
459
- rb_raise( rmdbx_eDatabaseError, "mdbx_dbi_stat: (%d) %s", rc, mdbx_strerror(rc) );
477
+ /*
478
+ * Open a new database transaction. If a transaction is already
479
+ * open, this is a no-op.
480
+ *
481
+ * +rwflag+ must be either MDBX_TXN_RDONLY or MDBX_TXN_READWRITE.
482
+ */
483
+ void
484
+ rmdbx_open_txn( rmdbx_db_t *db, int rwflag )
485
+ {
486
+ if ( db->txn ) return;
460
487
 
461
- VALUE rv = LONG2FIX( mstat.ms_entries );
462
- rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK );
488
+ int rc = mdbx_txn_begin( db->env, NULL, rwflag, &db->txn );
489
+ if ( rc != MDBX_SUCCESS ) {
490
+ rmdbx_close_all( db );
491
+ rb_raise( rmdbx_eDatabaseError, "mdbx_txn_begin: (%d) %s", rc, mdbx_strerror(rc) );
492
+ }
463
493
 
464
- return rv;
494
+ if ( db->dbi == 0 ) {
495
+ rc = mdbx_dbi_open( db->txn, db->subdb, db->settings.db_flags, &db->dbi );
496
+ if ( rc != MDBX_SUCCESS ) {
497
+ rmdbx_close_all( db );
498
+ rb_raise( rmdbx_eDatabaseError, "mdbx_dbi_open: (%d) %s", rc, mdbx_strerror(rc) );
499
+ }
500
+ }
501
+
502
+ return;
465
503
  }
466
504
 
467
505
 
468
- /* call-seq:
469
- * db[ 'key' ] => value
506
+ /*
507
+ * Close any existing database transaction. If there is no
508
+ * active transaction, this is a no-op. If there is a long
509
+ * running transaction open, this is a no-op.
510
+ *
511
+ * +txnflag must either be RMDBX_TXN_ROLLBACK or RMDBX_TXN_COMMIT.
512
+ */
513
+ void
514
+ rmdbx_close_txn( rmdbx_db_t *db, int txnflag )
515
+ {
516
+ if ( ! db->txn || db->state.retain_txn > -1 ) return;
517
+
518
+ if ( txnflag == RMDBX_TXN_COMMIT ) {
519
+ mdbx_txn_commit( db->txn );
520
+ }
521
+ else {
522
+ mdbx_txn_abort( db->txn );
523
+ }
524
+
525
+ db->txn = 0;
526
+ return;
527
+ }
528
+
529
+
530
+ /*
531
+ * call-seq:
532
+ * db.open_transaction( mode )
533
+ *
534
+ * Open a new long-running transaction. If +mode+ is true,
535
+ * it is opened read/write.
470
536
  *
471
- * Return a single value for +key+ immediately.
472
537
  */
473
538
  VALUE
474
- rmdbx_get_val( VALUE self, VALUE key )
539
+ rmdbx_rb_opentxn( VALUE self, VALUE mode )
475
540
  {
476
- int rc;
477
541
  UNWRAP_DB( self, db );
542
+ CHECK_HANDLE();
478
543
 
479
- if ( ! db->state.open ) rb_raise( rmdbx_eDatabaseError, "Closed database." );
480
- rmdbx_open_txn( db, MDBX_TXN_RDONLY );
544
+ rmdbx_open_txn( db, RTEST(mode) ? MDBX_TXN_READWRITE : MDBX_TXN_RDONLY );
545
+ db->state.retain_txn = RTEST(mode) ? 1 : 0;
481
546
 
482
- MDBX_val ckey = rmdbx_key_for( key );
483
- MDBX_val data;
484
- VALUE rv;
485
- rc = mdbx_get( db->txn, db->dbi, &ckey, &data );
486
- rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK );
547
+ return Qtrue;
548
+ }
487
549
 
488
- switch ( rc ) {
489
- case MDBX_SUCCESS:
490
- rv = rb_str_new( data.iov_base, data.iov_len );
491
- return rmdbx_deserialize( self, rv );
492
550
 
493
- case MDBX_NOTFOUND:
494
- return Qnil;
551
+ /*
552
+ * call-seq:
553
+ * db.close_transaction( mode )
554
+ *
555
+ * Close a long-running transaction. If +write+ is true,
556
+ * the transaction is committed. Otherwise, rolled back.
557
+ *
558
+ */
559
+ VALUE
560
+ rmdbx_rb_closetxn( VALUE self, VALUE write )
561
+ {
562
+ UNWRAP_DB( self, db );
495
563
 
496
- default:
497
- rmdbx_close( self );
498
- rb_raise( rmdbx_eDatabaseError, "Unable to fetch value: (%d) %s", rc, mdbx_strerror(rc) );
564
+ db->state.retain_txn = -1;
565
+ rmdbx_close_txn( db, RTEST(write) ? RMDBX_TXN_COMMIT : RMDBX_TXN_ROLLBACK );
566
+
567
+ return Qtrue;
568
+ }
569
+
570
+
571
+ /*
572
+ * Open a cursor for iteration.
573
+ */
574
+ void
575
+ rmdbx_open_cursor( rmdbx_db_t *db )
576
+ {
577
+ CHECK_HANDLE();
578
+ if ( ! db->txn ) rb_raise( rmdbx_eDatabaseError, "No snapshot or transaction currently open." );
579
+
580
+ int rc = mdbx_cursor_open( db->txn, db->dbi, &db->cursor );
581
+ if ( rc != MDBX_SUCCESS ) {
582
+ rmdbx_close_all( db );
583
+ rb_raise( rmdbx_eDatabaseError, "Unable to open cursor: (%d) %s", rc, mdbx_strerror(rc) );
499
584
  }
585
+
586
+ return;
587
+ }
588
+
589
+
590
+ /*
591
+ * Enumerate over keys for the current collection.
592
+ */
593
+ VALUE
594
+ rmdbx_each_key_i( VALUE self )
595
+ {
596
+ UNWRAP_DB( self, db );
597
+ MDBX_val key, data;
598
+
599
+ if ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_FIRST ) == MDBX_SUCCESS ) {
600
+ rb_yield( rb_str_new( key.iov_base, key.iov_len ) );
601
+ while ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_NEXT ) == MDBX_SUCCESS ) {
602
+ rb_yield( rb_str_new( key.iov_base, key.iov_len ) );
603
+ }
604
+ }
605
+
606
+ return self;
500
607
  }
501
608
 
502
609
 
503
610
  /* call-seq:
504
- * db[ 'key' ] = value
611
+ * db.each_key {|key| block } => self
505
612
  *
506
- * Set a single value for +key+.
613
+ * Calls the block once for each key, returning self.
614
+ * A transaction must be opened prior to use.
507
615
  */
508
616
  VALUE
509
- rmdbx_put_val( VALUE self, VALUE key, VALUE val )
617
+ rmdbx_each_key( VALUE self )
510
618
  {
511
- int rc;
512
619
  UNWRAP_DB( self, db );
620
+ int state;
513
621
 
514
- if ( ! db->state.open ) rb_raise( rmdbx_eDatabaseError, "Closed database." );
515
- rmdbx_open_txn( db, MDBX_TXN_READWRITE );
622
+ CHECK_HANDLE();
623
+ rmdbx_open_cursor( db );
624
+ RETURN_ENUMERATOR( self, 0, 0 );
516
625
 
517
- MDBX_val ckey = rmdbx_key_for( key );
626
+ rb_protect( rmdbx_each_key_i, self, &state );
518
627
 
519
- // FIXME: DUPSORT is enabled -- different api?
520
- // See: MDBX_NODUPDATA / MDBX_NOOVERWRITE
521
- if ( NIL_P(val) ) { /* remove if set to nil */
522
- rc = mdbx_del( db->txn, db->dbi, &ckey, NULL );
523
- }
524
- else {
525
- MDBX_val old;
526
- MDBX_val data = rmdbx_val_for( self, val );
527
- rc = mdbx_replace( db->txn, db->dbi, &ckey, &data, &old, 0 );
528
- }
628
+ mdbx_cursor_close( db->cursor );
629
+ db->cursor = NULL;
529
630
 
530
- rmdbx_close_txn( db, RMDBX_TXN_COMMIT );
631
+ if ( state ) rb_jump_tag( state );
531
632
 
532
- switch ( rc ) {
533
- case MDBX_SUCCESS:
534
- return val;
535
- case MDBX_NOTFOUND:
536
- return Qnil;
537
- default:
538
- rb_raise( rmdbx_eDatabaseError, "Unable to store value: (%d) %s", rc, mdbx_strerror(rc) );
539
- }
633
+ return self;
540
634
  }
541
635
 
542
636
 
543
- /*
544
- * call-seq:
545
- * db.statistics => (hash of stats)
546
- *
547
- * Returns a hash populated with various metadata for the opened
548
- * database.
549
- *
637
+ /* Enumerate over values for the current collection.
550
638
  */
551
639
  VALUE
552
- rmdbx_stats( VALUE self )
640
+ rmdbx_each_value_i( VALUE self )
553
641
  {
554
642
  UNWRAP_DB( self, db );
555
- if ( ! db->state.open ) rb_raise( rmdbx_eDatabaseError, "Closed database." );
643
+ MDBX_val key, data;
556
644
 
557
- return rmdbx_gather_stats( db );
645
+ if ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_FIRST ) == MDBX_SUCCESS ) {
646
+ VALUE rv = rb_str_new( data.iov_base, data.iov_len );
647
+ rb_yield( rmdbx_deserialize( self, rv ) );
648
+
649
+ while ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_NEXT ) == MDBX_SUCCESS ) {
650
+ rv = rb_str_new( data.iov_base, data.iov_len );
651
+ rb_yield( rmdbx_deserialize( self, rv ) );
652
+ }
653
+ }
654
+
655
+ return self;
558
656
  }
559
657
 
560
658
 
561
- /*
562
- * call-seq:
563
- * db.collection -> (collection name, or nil if in main)
564
- * db.collection( 'collection_name' ) -> db
565
- * db.collection( nil ) -> db (main)
566
- *
567
- * Gets or sets the sub-database "collection" that read/write
568
- * operations apply to.
569
- * Passing +nil+ sets the database to the main, top-level namespace.
570
- * If a block is passed, the collection automatically reverts to the
571
- * prior collection when it exits.
572
- *
573
- * db.collection( 'collection_name' ) do
574
- * [ ... ]
575
- * end # reverts to the previous collection name
659
+ /* call-seq:
660
+ * db.each_value {|value| block } => self
576
661
  *
662
+ * Calls the block once for each value, returning self.
663
+ * A transaction must be opened prior to use.
577
664
  */
578
665
  VALUE
579
- rmdbx_set_subdb( int argc, VALUE *argv, VALUE self )
666
+ rmdbx_each_value( VALUE self )
580
667
  {
581
668
  UNWRAP_DB( self, db );
582
- VALUE subdb, block;
583
- char *prev_db = NULL;
669
+ int state;
584
670
 
585
- rb_scan_args( argc, argv, "01&", &subdb, &block );
586
- if ( argc == 0 ) {
587
- if ( db->subdb == NULL ) return Qnil;
588
- return rb_str_new_cstr( db->subdb );
589
- }
671
+ CHECK_HANDLE();
672
+ rmdbx_open_cursor( db );
673
+ RETURN_ENUMERATOR( self, 0, 0 );
590
674
 
591
- /* Provide a friendlier error message if max_collections is 0. */
592
- if ( db->settings.max_collections == 0 )
593
- rb_raise( rmdbx_eDatabaseError, "Unable to change collection: collections are not enabled." );
675
+ rb_protect( rmdbx_each_value_i, self, &state );
594
676
 
595
- /* All transactions must be closed when switching database handles. */
596
- if ( db->txn )
597
- rb_raise( rmdbx_eDatabaseError, "Unable to change collection: transaction open" );
677
+ mdbx_cursor_close( db->cursor );
678
+ db->cursor = NULL;
598
679
 
599
- /* Retain the prior database collection if a block was passed.
600
- */
601
- if ( rb_block_given_p() && db->subdb != NULL ) {
602
- prev_db = (char *) malloc( strlen(db->subdb) + 1 );
603
- strcpy( prev_db, db->subdb );
604
- }
680
+ if ( state ) rb_jump_tag( state );
605
681
 
606
- db->subdb = NIL_P( subdb ) ? NULL : StringValueCStr( subdb );
607
- rmdbx_close_dbi( db );
682
+ return self;
683
+ }
608
684
 
609
- /*
610
- FIXME: Immediate transaction write to auto-create new env?
611
- Fetching from here at the moment causes an error if you
612
- haven't written anything to the new collection yet.
613
- */
614
685
 
615
- /* Revert to the previous collection after the block is done.
616
- */
617
- if ( rb_block_given_p() ) {
618
- rb_yield( self );
619
- if ( db->subdb != prev_db ) {
620
- db->subdb = prev_db;
621
- rmdbx_close_dbi( db );
686
+ /* Enumerate over key and value pairs for the current collection.
687
+ */
688
+ VALUE
689
+ rmdbx_each_pair_i( VALUE self )
690
+ {
691
+ UNWRAP_DB( self, db );
692
+ MDBX_val key, data;
693
+
694
+ if ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_FIRST ) == MDBX_SUCCESS ) {
695
+ VALUE rkey = rb_str_new( key.iov_base, key.iov_len );
696
+ VALUE rval = rb_str_new( data.iov_base, data.iov_len );
697
+ rb_yield( rb_assoc_new( rkey, rmdbx_deserialize( self, rval ) ) );
698
+
699
+ while ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_NEXT ) == MDBX_SUCCESS ) {
700
+ rkey = rb_str_new( key.iov_base, key.iov_len );
701
+ rval = rb_str_new( data.iov_base, data.iov_len );
702
+ rb_yield( rb_assoc_new( rkey, rmdbx_deserialize( self, rval ) ) );
622
703
  }
623
- xfree( prev_db );
624
704
  }
625
705
 
626
706
  return self;
627
707
  }
628
708
 
629
709
 
710
+ /* call-seq:
711
+ * db.each_pair {|key, value| block } => self
712
+ *
713
+ * Calls the block once for each key and value, returning self.
714
+ * A transaction must be opened prior to use.
715
+ */
716
+ VALUE
717
+ rmdbx_each_pair( VALUE self )
718
+ {
719
+ UNWRAP_DB( self, db );
720
+ int state;
721
+
722
+ CHECK_HANDLE();
723
+ rmdbx_open_cursor( db );
724
+ RETURN_ENUMERATOR( self, 0, 0 );
725
+
726
+ rb_protect( rmdbx_each_pair_i, self, &state );
727
+
728
+ mdbx_cursor_close( db->cursor );
729
+ db->cursor = NULL;
730
+
731
+ if ( state ) rb_jump_tag( state );
732
+
733
+ return self;
734
+ }
735
+
736
+
630
737
  /*
631
738
  * Open an existing (or create a new) mdbx database at filesystem
632
739
  * +path+. In block form, the database is automatically closed.
@@ -666,57 +773,108 @@ rmdbx_database_initialize( int argc, VALUE *argv, VALUE self )
666
773
  db->state.open = 0;
667
774
  db->state.retain_txn = -1;
668
775
  db->settings.env_flags = MDBX_ENV_DEFAULTS;
776
+ db->settings.db_flags = MDBX_DB_DEFAULTS | MDBX_CREATE;
669
777
  db->settings.mode = 0644;
670
778
  db->settings.max_collections = 0;
671
779
  db->settings.max_readers = 0;
672
780
  db->settings.max_size = 0;
673
781
 
674
- /* Options setup, overrides.
782
+ /* Set instance variables.
675
783
  */
676
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("mode") ) );
677
- if ( ! NIL_P(opt) ) db->settings.mode = FIX2INT( opt );
678
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("max_collections") ) );
784
+ rb_iv_set( self, "@path", path );
785
+ rb_iv_set( self, "@options", rb_hash_dup( opts ) );
786
+
787
+ /* Environment and database options setup, overrides.
788
+ */
789
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("coalesce") ) );
790
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_COALESCE;
791
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("compatible") ) );
792
+ if ( RTEST(opt) ) {
793
+ db->settings.db_flags = db->settings.db_flags | MDBX_DB_ACCEDE;
794
+ db->settings.env_flags = db->settings.env_flags | MDBX_ACCEDE;
795
+ }
796
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("exclusive") ) );
797
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_EXCLUSIVE;
798
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("lifo_reclaim") ) );
799
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_LIFORECLAIM;
800
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("max_collections") ) );
679
801
  if ( ! NIL_P(opt) ) db->settings.max_collections = FIX2INT( opt );
680
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("max_readers") ) );
802
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("max_readers") ) );
681
803
  if ( ! NIL_P(opt) ) db->settings.max_readers = FIX2INT( opt );
682
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("max_size") ) );
804
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("max_size") ) );
683
805
  if ( ! NIL_P(opt) ) db->settings.max_size = NUM2LONG( opt );
684
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("nosubdir") ) );
806
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("mode") ) );
807
+ if ( ! NIL_P(opt) ) db->settings.mode = FIX2INT( opt );
808
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("no_memory_init") ) );
809
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOMEMINIT;
810
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("no_metasync") ) );
811
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOMETASYNC;
812
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("no_subdir") ) );
685
813
  if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOSUBDIR;
686
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("readonly") ) );
814
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("no_readahead") ) );
815
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NORDAHEAD;
816
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("no_threadlocal") ) );
817
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOTLS;
818
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("readonly") ) );
687
819
  if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_RDONLY;
688
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("exclusive") ) );
689
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_EXCLUSIVE;
690
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("compat") ) );
691
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_ACCEDE;
692
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("writemap") ) );
820
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("writemap") ) );
693
821
  if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_WRITEMAP;
694
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_threadlocal") ) );
695
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOTLS;
696
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_readahead") ) );
697
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NORDAHEAD;
698
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_memory_init") ) );
699
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOMEMINIT;
700
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("coalesce") ) );
701
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_COALESCE;
702
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("lifo_reclaim") ) );
703
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_LIFORECLAIM;
704
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_metasync") ) );
705
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOMETASYNC;
706
822
 
707
- /* Duplicate keys, on mdbx_dbi_open, maybe set here? */
708
- /* MDBX_DUPSORT = UINT32_C(0x04), */
709
-
710
- /* Set instance variables.
711
- */
712
- rb_iv_set( self, "@path", path );
713
- rb_iv_set( self, "@options", opts );
823
+ if ( rb_hash_size_num(opts) > 0 ) {
824
+ rb_raise( rb_eArgError, "Unknown option(s): %"PRIsVALUE, opts );
825
+ }
714
826
 
715
827
  rmdbx_open_env( self );
716
828
  return self;
717
829
  }
718
830
 
719
831
 
832
+ /*
833
+ * call-seq:
834
+ * db.statistics => (hash of stats)
835
+ *
836
+ * Returns a hash populated with various metadata for the opened
837
+ * database.
838
+ *
839
+ */
840
+ VALUE
841
+ rmdbx_stats( VALUE self )
842
+ {
843
+ UNWRAP_DB( self, db );
844
+ CHECK_HANDLE();
845
+
846
+ return rmdbx_gather_stats( db );
847
+ }
848
+
849
+
850
+ /*
851
+ * call-seq:
852
+ * db.clone => [copy of db]
853
+ *
854
+ * Copy the object (clone/dup). The returned copy is closed and needs
855
+ * to be reopened before use. This function likely has limited use,
856
+ * considering you can't open two handles within the same process.
857
+ */
858
+ static VALUE rmdbx_init_copy( VALUE copy, VALUE orig )
859
+ {
860
+ rmdbx_db_t *orig_db;
861
+ rmdbx_db_t *copy_db;
862
+
863
+ if ( copy == orig ) return copy;
864
+
865
+ TypedData_Get_Struct( orig, rmdbx_db_t, &rmdbx_db_data, orig_db );
866
+ TypedData_Get_Struct( copy, rmdbx_db_t, &rmdbx_db_data, copy_db );
867
+
868
+ /* Copy all fields from the original to the copy, and force-close
869
+ the copy.
870
+ */
871
+ MEMCPY( copy_db, orig_db, rmdbx_db_t, 1 );
872
+ rmdbx_close_all( copy_db );
873
+
874
+ return copy;
875
+ }
876
+
877
+
720
878
  /*
721
879
  * Initialization for the MDBX::Database class.
722
880
  */
@@ -732,25 +890,35 @@ rmdbx_init_database()
732
890
  rb_define_alloc_func( rmdbx_cDatabase, rmdbx_alloc );
733
891
 
734
892
  rb_define_protected_method( rmdbx_cDatabase, "initialize", rmdbx_database_initialize, -1 );
735
- rb_define_method( rmdbx_cDatabase, "collection", rmdbx_set_subdb, -1 );
893
+ rb_define_protected_method( rmdbx_cDatabase, "initialize_copy", rmdbx_init_copy, 1 );
894
+
895
+ rb_define_method( rmdbx_cDatabase, "clear", rmdbx_clear, 0 );
736
896
  rb_define_method( rmdbx_cDatabase, "close", rmdbx_close, 0 );
737
- rb_define_method( rmdbx_cDatabase, "reopen", rmdbx_open_env, 0 );
738
897
  rb_define_method( rmdbx_cDatabase, "closed?", rmdbx_closed_p, 0 );
739
- rb_define_method( rmdbx_cDatabase, "in_transaction?", rmdbx_in_transaction_p, 0 );
740
- rb_define_method( rmdbx_cDatabase, "clear", rmdbx_clear, 0 );
741
- rb_define_method( rmdbx_cDatabase, "each_key", rmdbx_each_key, 0 );
742
- rb_define_method( rmdbx_cDatabase, "each_value", rmdbx_each_value, 0 );
743
- rb_define_method( rmdbx_cDatabase, "each_pair", rmdbx_each_pair, 0 );
898
+ rb_define_method( rmdbx_cDatabase, "drop", rmdbx_drop, 1 );
899
+ rb_define_method( rmdbx_cDatabase, "include?", rmdbx_include, 1 );
744
900
  rb_define_method( rmdbx_cDatabase, "length", rmdbx_length, 0 );
901
+ rb_define_method( rmdbx_cDatabase, "reopen", rmdbx_open_env, 0 );
745
902
  rb_define_method( rmdbx_cDatabase, "[]", rmdbx_get_val, 1 );
746
903
  rb_define_method( rmdbx_cDatabase, "[]=", rmdbx_put_val, 2 );
747
904
 
905
+ /* Enumerables */
906
+ rb_define_method( rmdbx_cDatabase, "each_key", rmdbx_each_key, 0 );
907
+ rb_define_method( rmdbx_cDatabase, "each_pair", rmdbx_each_pair, 0 );
908
+ rb_define_method( rmdbx_cDatabase, "each_value", rmdbx_each_value, 0 );
909
+
748
910
  /* Manually open/close transactions from ruby. */
911
+ rb_define_method( rmdbx_cDatabase, "in_transaction?", rmdbx_in_transaction_p, 0 );
749
912
  rb_define_protected_method( rmdbx_cDatabase, "open_transaction", rmdbx_rb_opentxn, 1 );
750
913
  rb_define_protected_method( rmdbx_cDatabase, "close_transaction", rmdbx_rb_closetxn, 1 );
751
914
 
915
+ /* Collection functions */
916
+ rb_define_protected_method( rmdbx_cDatabase, "get_subdb", rmdbx_get_subdb, 0 );
917
+ rb_define_protected_method( rmdbx_cDatabase, "set_subdb", rmdbx_set_subdb, 1 );
918
+
752
919
  rb_define_protected_method( rmdbx_cDatabase, "raw_stats", rmdbx_stats, 0 );
753
920
 
754
921
  rb_require( "mdbx/database" );
755
922
  }
756
923
 
924
+