mdbx 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97a83bdd83e99ab3b58cf823a914c8a7b75b5b63e33e13cd00f135aa2c8988b4
4
- data.tar.gz: aa6687110e91dfe083332fc93299c492ce5628ad41fe26cb36d9ee33531e1039
3
+ metadata.gz: 678369c278e65d4faf56eb814e2cb7eba22da57fb5c07c6e4f9dc4e6edd7f09f
4
+ data.tar.gz: 0fc4b455b4556bb932f867fa40e7c240f555a5a6bbb7d09cb55c68ed7745bfd0
5
5
  SHA512:
6
- metadata.gz: 732ecf85ef600ae8edb433b18d67ecf26fbc1332fc673e7e01b20637dbcffff94735a8c1d79cc43c9d293cdda7b7532670f9a0c9f101234367b19aaac03149a9
7
- data.tar.gz: 1623ff8c9d4b6e7752ef9faaf4075961119d812c7dfd78bb619fa74eaacb3e7fd1042b66b630e5638010478f61a0f3f232fa6cf2e1a37e40ef3d010a5c3257f6
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,5 +1,28 @@
1
1
  # Release History for MDBX
2
2
 
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
+
3
26
  ---
4
27
  ## v0.3.0 [2021-04-09] Mahlon E. Smith <mahlon@martini.nu>
5
28
 
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,21 +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
-
11
- #define CHECK_HANDLE \
12
- if ( ! db->state.open ) rb_raise( rmdbx_eDatabaseError, "Closed database." )
13
6
 
7
+ #include "mdbx_ext.h"
14
8
 
15
9
  VALUE rmdbx_cDatabase;
16
10
 
11
+
17
12
  /*
18
- * Ruby allocation hook.
13
+ * Ruby data allocation wrapper.
19
14
  */
20
15
  static const rb_data_type_t rmdbx_db_data = {
21
16
  .wrap_struct_name = "MDBX::Database::Data",
@@ -30,11 +25,24 @@ static const rb_data_type_t rmdbx_db_data = {
30
25
  VALUE
31
26
  rmdbx_alloc( VALUE klass )
32
27
  {
33
- rmdbx_db_t *new = RB_ALLOC( rmdbx_db_t );
28
+ rmdbx_db_t *new;
34
29
  return TypedData_Make_Struct( klass, rmdbx_db_t, &rmdbx_db_data, new );
35
30
  }
36
31
 
37
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
+
38
46
  /*
39
47
  * Ensure all database file descriptors are collected and
40
48
  * removed.
@@ -65,19 +73,9 @@ rmdbx_close_dbi( rmdbx_db_t *db )
65
73
 
66
74
 
67
75
  /*
68
- * Cleanup a previously allocated DB environment.
69
- */
70
- void
71
- rmdbx_free( void *db )
72
- {
73
- if ( db ) {
74
- rmdbx_close_all( db );
75
- xfree( db );
76
- }
77
- }
78
-
79
-
80
- /*
76
+ * call-seq:
77
+ * db.close => true
78
+ *
81
79
  * Cleanly close an opened database.
82
80
  */
83
81
  VALUE
@@ -104,17 +102,63 @@ rmdbx_closed_p( VALUE self )
104
102
 
105
103
 
106
104
  /*
107
- * call-seq:
108
- * 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.
109
118
  *
110
- * Predicate: return true if a transaction (or snapshot)
111
- * 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.
112
153
  */
113
154
  VALUE
114
- rmdbx_in_transaction_p( VALUE self )
155
+ rmdbx_deserialize( VALUE self, VALUE val )
115
156
  {
116
- UNWRAP_DB( self, db );
117
- 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;
118
162
  }
119
163
 
120
164
 
@@ -151,245 +195,395 @@ rmdbx_open_env( VALUE self )
151
195
  rmdbx_close_all( db );
152
196
  rb_raise( rmdbx_eDatabaseError, "mdbx_env_open: (%d) %s", rc, mdbx_strerror(rc) );
153
197
  }
154
- db->state.open = 1;
155
198
 
199
+ db->state.open = 1;
156
200
  return Qtrue;
157
201
  }
158
202
 
159
203
 
160
204
  /*
161
- * Open a cursor for iteration.
205
+ * call-seq:
206
+ * db.clear
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.
162
211
  */
163
- void
164
- rmdbx_open_cursor( rmdbx_db_t *db )
212
+ VALUE
213
+ rmdbx_clear( VALUE self )
165
214
  {
166
- CHECK_HANDLE;
167
- if ( ! db->txn ) rb_raise( rmdbx_eDatabaseError, "No snapshot or transaction currently open." );
215
+ UNWRAP_DB( self, db );
168
216
 
169
- int rc = mdbx_cursor_open( db->txn, db->dbi, &db->cursor );
170
- if ( rc != MDBX_SUCCESS ) {
171
- rmdbx_close_all( db );
172
- rb_raise( rmdbx_eDatabaseError, "Unable to open cursor: (%d) %s", rc, mdbx_strerror(rc) );
173
- }
217
+ rmdbx_open_txn( db, MDBX_TXN_READWRITE );
218
+ int rc = mdbx_drop( db->txn, db->dbi, false );
174
219
 
175
- return;
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;
176
226
  }
177
227
 
178
228
 
179
229
  /*
180
- * Open a new database transaction. If a transaction is already
181
- * open, this is a no-op.
230
+ * call-seq:
231
+ * db.drop( collection ) -> db
182
232
  *
183
- * +rwflag+ must be either MDBX_TXN_RDONLY or MDBX_TXN_READWRITE.
233
+ * Destroy a collection. You must be in the top level database to call
234
+ * this method.
184
235
  */
185
- void
186
- rmdbx_open_txn( rmdbx_db_t *db, int rwflag )
236
+ VALUE
237
+ rmdbx_drop( VALUE self, VALUE name )
187
238
  {
188
- if ( db->txn ) return;
239
+ UNWRAP_DB( self, db );
189
240
 
190
- int rc = mdbx_txn_begin( db->env, NULL, rwflag, &db->txn );
191
- if ( rc != MDBX_SUCCESS ) {
192
- rmdbx_close_all( db );
193
- rb_raise( rmdbx_eDatabaseError, "mdbx_txn_begin: (%d) %s", rc, mdbx_strerror(rc) );
194
- }
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." );
195
244
 
196
- if ( db->dbi == 0 ) {
197
- // FIXME: dbi_flags
198
- rc = mdbx_dbi_open( db->txn, db->subdb, MDBX_CREATE, &db->dbi );
199
- if ( rc != MDBX_SUCCESS ) {
200
- rmdbx_close_all( db );
201
- rb_raise( rmdbx_eDatabaseError, "mdbx_dbi_open: (%d) %s", rc, mdbx_strerror(rc) );
202
- }
203
- }
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" );
204
248
 
205
- return;
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" );
252
+
253
+ name = rb_funcall( name, rb_intern("to_s"), 0 );
254
+ db->subdb = StringValueCStr( name );
255
+
256
+ rmdbx_close_dbi( db ); /* ensure we're reopening within the new subdb */
257
+ rmdbx_open_txn( db, MDBX_TXN_READWRITE );
258
+ int rc = mdbx_drop( db->txn, db->dbi, true );
259
+
260
+ if ( rc != MDBX_SUCCESS )
261
+ rb_raise( rmdbx_eDatabaseError, "mdbx_drop: (%d) %s", rc, mdbx_strerror(rc) );
262
+
263
+ rmdbx_close_txn( db, RMDBX_TXN_COMMIT );
264
+
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 */
268
+
269
+ return self;
206
270
  }
207
271
 
208
272
 
209
- /*
210
- * Close any existing database transaction. If there is no
211
- * active transaction, this is a no-op. If there is a long
212
- * running transaction open, this is a no-op.
273
+ /* call-seq:
274
+ * db.length -> Integer
213
275
  *
214
- * +txnflag must either be RMDBX_TXN_ROLLBACK or RMDBX_TXN_COMMIT.
276
+ * Returns the count of keys in the currently selected collection.
215
277
  */
216
- void
217
- rmdbx_close_txn( rmdbx_db_t *db, int txnflag )
278
+ VALUE
279
+ rmdbx_length( VALUE self )
218
280
  {
219
- if ( ! db->txn || db->state.retain_txn > -1 ) return;
281
+ UNWRAP_DB( self, db );
282
+ MDBX_stat mstat;
220
283
 
221
- if ( txnflag == RMDBX_TXN_COMMIT ) {
222
- mdbx_txn_commit( db->txn );
223
- }
224
- else {
225
- mdbx_txn_abort( db->txn );
226
- }
284
+ CHECK_HANDLE();
285
+ rmdbx_open_txn( db, MDBX_TXN_RDONLY );
227
286
 
228
- db->txn = 0;
229
- return;
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) );
290
+
291
+ VALUE rv = LONG2FIX( mstat.ms_entries );
292
+ rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK );
293
+
294
+ return rv;
230
295
  }
231
296
 
232
297
 
233
- /*
234
- * call-seq:
235
- * db.open_transaction( mode )
236
- *
237
- * Open a new long-running transaction. If +mode+ is true,
238
- * it is opened read/write.
298
+ /* call-seq:
299
+ * db.include?( 'key' ) => bool
239
300
  *
301
+ * Returns true if the current collection contains +key+.
240
302
  */
241
303
  VALUE
242
- rmdbx_rb_opentxn( VALUE self, VALUE mode )
304
+ rmdbx_include( VALUE self, VALUE key )
243
305
  {
244
306
  UNWRAP_DB( self, db );
245
- CHECK_HANDLE;
246
307
 
247
- rmdbx_open_txn( db, RTEST(mode) ? MDBX_TXN_READWRITE : MDBX_TXN_RDONLY );
248
- db->state.retain_txn = RTEST(mode) ? 1 : 0;
308
+ CHECK_HANDLE();
309
+ rmdbx_open_txn( db, MDBX_TXN_RDONLY );
249
310
 
250
- return Qtrue;
311
+ MDBX_val ckey;
312
+ MDBX_val data;
313
+ rmdbx_key_for( key, &ckey );
314
+
315
+ int rc = mdbx_get( db->txn, db->dbi, &ckey, &data );
316
+ rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK );
317
+ xfree( ckey.iov_base );
318
+
319
+ switch ( rc ) {
320
+ case MDBX_SUCCESS:
321
+ return Qtrue;
322
+
323
+ case MDBX_NOTFOUND:
324
+ return Qfalse;
325
+
326
+ default:
327
+ rmdbx_close( self );
328
+ rb_raise( rmdbx_eDatabaseError, "Unable to fetch key: (%d) %s", rc, mdbx_strerror(rc) );
329
+ }
251
330
  }
252
331
 
253
332
 
254
- /*
255
- * call-seq:
256
- * db.close_transaction( mode )
257
- *
258
- * Close a long-running transaction. If +write+ is true,
259
- * the transaction is committed. Otherwise, rolled back.
333
+ /* call-seq:
334
+ * db[ 'key' ] => value
260
335
  *
336
+ * Return a single value for +key+ immediately.
261
337
  */
262
338
  VALUE
263
- rmdbx_rb_closetxn( VALUE self, VALUE write )
339
+ rmdbx_get_val( VALUE self, VALUE key )
264
340
  {
265
341
  UNWRAP_DB( self, db );
266
342
 
267
- db->state.retain_txn = -1;
268
- rmdbx_close_txn( db, RTEST(write) ? RMDBX_TXN_COMMIT : RMDBX_TXN_ROLLBACK );
343
+ CHECK_HANDLE();
344
+ rmdbx_open_txn( db, MDBX_TXN_RDONLY );
269
345
 
270
- return Qtrue;
346
+ MDBX_val ckey;
347
+ MDBX_val data;
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 );
353
+
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
+ }
271
367
  }
272
368
 
273
369
 
274
- /*
275
- * call-seq:
276
- * db.clear
370
+ /* call-seq:
371
+ * db[ 'key' ] = value
277
372
  *
278
- * Empty the current collection on disk. If collections are not enabled
279
- * or the database handle is set to the top-level (main) db - this
280
- * deletes *all records* from the database.
373
+ * Set a single value for +key+. If the value is +nil+, the
374
+ * key is removed.
281
375
  */
282
376
  VALUE
283
- rmdbx_clear( VALUE self )
377
+ rmdbx_put_val( VALUE self, VALUE key, VALUE val )
284
378
  {
379
+ int rc;
285
380
  UNWRAP_DB( self, db );
286
381
 
382
+ CHECK_HANDLE();
287
383
  rmdbx_open_txn( db, MDBX_TXN_READWRITE );
288
- int rc = mdbx_drop( db->txn, db->dbi, false );
289
384
 
290
- if ( rc != MDBX_SUCCESS )
291
- rb_raise( rmdbx_eDatabaseError, "mdbx_drop: (%d) %s", rc, mdbx_strerror(rc) );
385
+ MDBX_val ckey;
386
+ rmdbx_key_for( key, &ckey );
387
+
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 );
397
+ }
292
398
 
293
399
  rmdbx_close_txn( db, RMDBX_TXN_COMMIT );
400
+ xfree( ckey.iov_base );
294
401
 
295
- return Qnil;
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
+ }
296
410
  }
297
411
 
298
412
 
299
413
  /*
300
- * call-seq:
301
- * db.drop( collection ) -> db
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.
302
427
  *
303
- * Destroy a collection. You must be in the top level database to call
304
- * this method.
305
428
  */
306
429
  VALUE
307
- rmdbx_drop( VALUE self, VALUE name )
430
+ rmdbx_set_subdb( VALUE self, VALUE name )
308
431
  {
309
432
  UNWRAP_DB( self, db );
310
433
 
311
434
  /* Provide a friendlier error message if max_collections is 0. */
312
435
  if ( db->settings.max_collections == 0 )
313
- rb_raise( rmdbx_eDatabaseError, "Unable to drop collection: collections are not enabled." );
436
+ rb_raise( rmdbx_eDatabaseError, "Unable to change collection: collections are not enabled." );
314
437
 
315
- /* All transactions must be closed when dropping a database. */
438
+ /* All transactions must be closed when switching database handles. */
316
439
  if ( db->txn )
317
- rb_raise( rmdbx_eDatabaseError, "Unable to drop collection: transaction open" );
440
+ rb_raise( rmdbx_eDatabaseError, "Unable to change collection: transaction open" );
318
441
 
319
- /* A drop can only be performed from the top-level database. */
320
- if ( db->subdb != NULL )
321
- rb_raise( rmdbx_eDatabaseError, "Unable to drop collection: switch to top-level db first" );
442
+ xfree( db->subdb );
443
+ db->subdb = NULL;
322
444
 
323
- name = rb_funcall( name, rb_intern("to_s"), 0 );
324
- db->subdb = StringValueCStr( name );
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 );
449
+ }
325
450
 
326
- rmdbx_close_dbi( db ); /* ensure we're reopening within the new subdb */
451
+ /* Reset the db handle and issue a single transaction to reify
452
+ the collection.
453
+ */
454
+ rmdbx_close_dbi( db );
327
455
  rmdbx_open_txn( db, MDBX_TXN_READWRITE );
328
- int rc = mdbx_drop( db->txn, db->dbi, true );
456
+ rmdbx_close_txn( db, RMDBX_TXN_COMMIT );
329
457
 
330
- if ( rc != MDBX_SUCCESS )
331
- rb_raise( rmdbx_eDatabaseError, "mdbx_drop: (%d) %s", rc, mdbx_strerror(rc) );
458
+ return self;
459
+ }
332
460
 
333
- rmdbx_close_txn( db, RMDBX_TXN_COMMIT );
334
461
 
335
- /* Reset the current collection to the top level. */
336
- db->subdb = NULL;
337
- rmdbx_close_dbi( db ); /* ensure next access is not in the defunct subdb */
462
+ /*
463
+ * call-seq:
464
+ * db.in_transaction? => false
465
+ *
466
+ * Predicate: return true if a transaction (or snapshot)
467
+ * is currently open.
468
+ */
469
+ VALUE
470
+ rmdbx_in_transaction_p( VALUE self )
471
+ {
472
+ UNWRAP_DB( self, db );
473
+ return db->txn ? Qtrue : Qfalse;
474
+ }
338
475
 
339
- return self;
476
+
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;
487
+
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
+ }
493
+
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;
340
503
  }
341
504
 
342
505
 
343
506
  /*
344
- * Given a ruby +arg+, convert and return a structure
345
- * suitable for usage as a key for mdbx. All keys are explicitly
346
- * converted to strings.
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.
347
512
  */
348
- MDBX_val
349
- rmdbx_key_for( VALUE arg )
513
+ void
514
+ rmdbx_close_txn( rmdbx_db_t *db, int txnflag )
350
515
  {
351
- MDBX_val rv;
516
+ if ( ! db->txn || db->state.retain_txn > -1 ) return;
352
517
 
353
- arg = rb_funcall( arg, rb_intern("to_s"), 0 );
354
- rv.iov_len = RSTRING_LEN( arg );
355
- rv.iov_base = StringValuePtr( arg );
518
+ if ( txnflag == RMDBX_TXN_COMMIT ) {
519
+ mdbx_txn_commit( db->txn );
520
+ }
521
+ else {
522
+ mdbx_txn_abort( db->txn );
523
+ }
356
524
 
357
- return rv;
525
+ db->txn = 0;
526
+ return;
358
527
  }
359
528
 
360
529
 
361
530
  /*
362
- * Given a ruby +arg+, convert and return a structure
363
- * suitable for usage as a value for mdbx.
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.
536
+ *
364
537
  */
365
- MDBX_val
366
- rmdbx_val_for( VALUE self, VALUE arg )
538
+ VALUE
539
+ rmdbx_rb_opentxn( VALUE self, VALUE mode )
367
540
  {
368
- MDBX_val rv;
369
- VALUE serialize_proc;
541
+ UNWRAP_DB( self, db );
542
+ CHECK_HANDLE();
370
543
 
371
- serialize_proc = rb_iv_get( self, "@serializer" );
372
- if ( ! NIL_P( serialize_proc ) )
373
- arg = rb_funcall( serialize_proc, rb_intern("call"), 1, arg );
544
+ rmdbx_open_txn( db, RTEST(mode) ? MDBX_TXN_READWRITE : MDBX_TXN_RDONLY );
545
+ db->state.retain_txn = RTEST(mode) ? 1 : 0;
546
+
547
+ return Qtrue;
548
+ }
549
+
550
+
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 );
374
563
 
375
- rv.iov_len = RSTRING_LEN( arg );
376
- rv.iov_base = StringValuePtr( arg );
564
+ db->state.retain_txn = -1;
565
+ rmdbx_close_txn( db, RTEST(write) ? RMDBX_TXN_COMMIT : RMDBX_TXN_ROLLBACK );
377
566
 
378
- return rv;
567
+ return Qtrue;
379
568
  }
380
569
 
381
570
 
382
571
  /*
383
- * Deserialize and return a value.
572
+ * Open a cursor for iteration.
384
573
  */
385
- VALUE
386
- rmdbx_deserialize( VALUE self, VALUE val )
574
+ void
575
+ rmdbx_open_cursor( rmdbx_db_t *db )
387
576
  {
388
- VALUE deserialize_proc = rb_iv_get( self, "@deserializer" );
389
- if ( ! NIL_P( deserialize_proc ) )
390
- val = rb_funcall( deserialize_proc, rb_intern("call"), 1, val );
577
+ CHECK_HANDLE();
578
+ if ( ! db->txn ) rb_raise( rmdbx_eDatabaseError, "No snapshot or transaction currently open." );
391
579
 
392
- return val;
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) );
584
+ }
585
+
586
+ return;
393
587
  }
394
588
 
395
589
 
@@ -425,7 +619,7 @@ rmdbx_each_key( VALUE self )
425
619
  UNWRAP_DB( self, db );
426
620
  int state;
427
621
 
428
- CHECK_HANDLE;
622
+ CHECK_HANDLE();
429
623
  rmdbx_open_cursor( db );
430
624
  RETURN_ENUMERATOR( self, 0, 0 );
431
625
 
@@ -474,7 +668,7 @@ rmdbx_each_value( VALUE self )
474
668
  UNWRAP_DB( self, db );
475
669
  int state;
476
670
 
477
- CHECK_HANDLE;
671
+ CHECK_HANDLE();
478
672
  rmdbx_open_cursor( db );
479
673
  RETURN_ENUMERATOR( self, 0, 0 );
480
674
 
@@ -525,7 +719,7 @@ rmdbx_each_pair( VALUE self )
525
719
  UNWRAP_DB( self, db );
526
720
  int state;
527
721
 
528
- CHECK_HANDLE;
722
+ CHECK_HANDLE();
529
723
  rmdbx_open_cursor( db );
530
724
  RETURN_ENUMERATOR( self, 0, 0 );
531
725
 
@@ -540,201 +734,6 @@ rmdbx_each_pair( VALUE self )
540
734
  }
541
735
 
542
736
 
543
-
544
- /* call-seq:
545
- * db.length -> Integer
546
- *
547
- * Returns the count of keys in the currently selected collection.
548
- */
549
- VALUE
550
- rmdbx_length( VALUE self )
551
- {
552
- UNWRAP_DB( self, db );
553
- MDBX_stat mstat;
554
-
555
- CHECK_HANDLE;
556
- rmdbx_open_txn( db, MDBX_TXN_RDONLY );
557
-
558
- int rc = mdbx_dbi_stat( db->txn, db->dbi, &mstat, sizeof(mstat) );
559
- if ( rc != MDBX_SUCCESS )
560
- rb_raise( rmdbx_eDatabaseError, "mdbx_dbi_stat: (%d) %s", rc, mdbx_strerror(rc) );
561
-
562
- VALUE rv = LONG2FIX( mstat.ms_entries );
563
- rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK );
564
-
565
- return rv;
566
- }
567
-
568
-
569
- /* call-seq:
570
- * db.include?( 'key' ) => bool
571
- *
572
- * Returns true if the current collection contains +key+.
573
- */
574
- VALUE
575
- rmdbx_include( VALUE self, VALUE key )
576
- {
577
- int rc;
578
- UNWRAP_DB( self, db );
579
-
580
- CHECK_HANDLE;
581
- rmdbx_open_txn( db, MDBX_TXN_RDONLY );
582
-
583
- MDBX_val ckey = rmdbx_key_for( key );
584
- MDBX_val data;
585
- rc = mdbx_get( db->txn, db->dbi, &ckey, &data );
586
- rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK );
587
-
588
- switch ( rc ) {
589
- case MDBX_SUCCESS:
590
- return Qtrue;
591
-
592
- case MDBX_NOTFOUND:
593
- return Qfalse;
594
-
595
- default:
596
- rmdbx_close( self );
597
- rb_raise( rmdbx_eDatabaseError, "Unable to fetch key: (%d) %s", rc, mdbx_strerror(rc) );
598
- }
599
- }
600
-
601
-
602
- /* call-seq:
603
- * db[ 'key' ] => value
604
- *
605
- * Return a single value for +key+ immediately.
606
- */
607
- VALUE
608
- rmdbx_get_val( VALUE self, VALUE key )
609
- {
610
- int rc;
611
- UNWRAP_DB( self, db );
612
-
613
- CHECK_HANDLE;
614
- rmdbx_open_txn( db, MDBX_TXN_RDONLY );
615
-
616
- MDBX_val ckey = rmdbx_key_for( key );
617
- MDBX_val data;
618
- VALUE rv;
619
- rc = mdbx_get( db->txn, db->dbi, &ckey, &data );
620
- rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK );
621
-
622
- switch ( rc ) {
623
- case MDBX_SUCCESS:
624
- rv = rb_str_new( data.iov_base, data.iov_len );
625
- return rmdbx_deserialize( self, rv );
626
-
627
- case MDBX_NOTFOUND:
628
- return Qnil;
629
-
630
- default:
631
- rmdbx_close( self );
632
- rb_raise( rmdbx_eDatabaseError, "Unable to fetch value: (%d) %s", rc, mdbx_strerror(rc) );
633
- }
634
- }
635
-
636
-
637
- /* call-seq:
638
- * db[ 'key' ] = value
639
- *
640
- * Set a single value for +key+. If the value is +nil+, the
641
- * key is removed.
642
- */
643
- VALUE
644
- rmdbx_put_val( VALUE self, VALUE key, VALUE val )
645
- {
646
- int rc;
647
- UNWRAP_DB( self, db );
648
-
649
- CHECK_HANDLE;
650
- rmdbx_open_txn( db, MDBX_TXN_READWRITE );
651
-
652
- MDBX_val ckey = rmdbx_key_for( key );
653
-
654
- // FIXME: DUPSORT is enabled -- different api?
655
- // See: MDBX_NODUPDATA / MDBX_NOOVERWRITE
656
- if ( NIL_P(val) ) { /* remove if set to nil */
657
- rc = mdbx_del( db->txn, db->dbi, &ckey, NULL );
658
- }
659
- else {
660
- MDBX_val old;
661
- MDBX_val data = rmdbx_val_for( self, val );
662
- rc = mdbx_replace( db->txn, db->dbi, &ckey, &data, &old, 0 );
663
- }
664
-
665
- rmdbx_close_txn( db, RMDBX_TXN_COMMIT );
666
-
667
- switch ( rc ) {
668
- case MDBX_SUCCESS:
669
- return val;
670
- case MDBX_NOTFOUND:
671
- return Qnil;
672
- default:
673
- rb_raise( rmdbx_eDatabaseError, "Unable to store value: (%d) %s", rc, mdbx_strerror(rc) );
674
- }
675
- }
676
-
677
-
678
- /*
679
- * call-seq:
680
- * db.statistics => (hash of stats)
681
- *
682
- * Returns a hash populated with various metadata for the opened
683
- * database.
684
- *
685
- */
686
- VALUE
687
- rmdbx_stats( VALUE self )
688
- {
689
- UNWRAP_DB( self, db );
690
- CHECK_HANDLE;
691
-
692
- return rmdbx_gather_stats( db );
693
- }
694
-
695
-
696
- /*
697
- * Return the currently selected collection, or +nil+ if at the
698
- * top-level.
699
- */
700
- VALUE
701
- rmdbx_get_subdb( VALUE self )
702
- {
703
- UNWRAP_DB( self, db );
704
- return ( db->subdb == NULL ) ? Qnil : rb_str_new_cstr( db->subdb );
705
- }
706
-
707
-
708
- /*
709
- * Sets the current collection name for read/write operations.
710
- *
711
- */
712
- VALUE
713
- rmdbx_set_subdb( VALUE self, VALUE name )
714
- {
715
- UNWRAP_DB( self, db );
716
-
717
- /* Provide a friendlier error message if max_collections is 0. */
718
- if ( db->settings.max_collections == 0 )
719
- rb_raise( rmdbx_eDatabaseError, "Unable to change collection: collections are not enabled." );
720
-
721
- /* All transactions must be closed when switching database handles. */
722
- if ( db->txn )
723
- rb_raise( rmdbx_eDatabaseError, "Unable to change collection: transaction open" );
724
-
725
- db->subdb = NIL_P( name ) ? NULL : StringValueCStr( name );
726
-
727
- /* Reset the db handle and issue a single transaction to reify
728
- the collection.
729
- */
730
- rmdbx_close_dbi( db );
731
- rmdbx_open_txn( db, MDBX_TXN_READWRITE );
732
- rmdbx_close_txn( db, RMDBX_TXN_COMMIT );
733
-
734
- return self;
735
- }
736
-
737
-
738
737
  /*
739
738
  * Open an existing (or create a new) mdbx database at filesystem
740
739
  * +path+. In block form, the database is automatically closed.
@@ -774,57 +773,80 @@ rmdbx_database_initialize( int argc, VALUE *argv, VALUE self )
774
773
  db->state.open = 0;
775
774
  db->state.retain_txn = -1;
776
775
  db->settings.env_flags = MDBX_ENV_DEFAULTS;
776
+ db->settings.db_flags = MDBX_DB_DEFAULTS | MDBX_CREATE;
777
777
  db->settings.mode = 0644;
778
778
  db->settings.max_collections = 0;
779
779
  db->settings.max_readers = 0;
780
780
  db->settings.max_size = 0;
781
781
 
782
- /* Options setup, overrides.
782
+ /* Set instance variables.
783
+ */
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.
783
788
  */
784
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("mode") ) );
785
- if ( ! NIL_P(opt) ) db->settings.mode = FIX2INT( opt );
786
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("max_collections") ) );
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") ) );
787
801
  if ( ! NIL_P(opt) ) db->settings.max_collections = FIX2INT( opt );
788
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("max_readers") ) );
802
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("max_readers") ) );
789
803
  if ( ! NIL_P(opt) ) db->settings.max_readers = FIX2INT( opt );
790
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("max_size") ) );
804
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("max_size") ) );
791
805
  if ( ! NIL_P(opt) ) db->settings.max_size = NUM2LONG( opt );
792
- 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") ) );
793
813
  if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOSUBDIR;
794
- 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") ) );
795
819
  if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_RDONLY;
796
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("exclusive") ) );
797
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_EXCLUSIVE;
798
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("compat") ) );
799
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_ACCEDE;
800
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("writemap") ) );
820
+ opt = rb_hash_delete( opts, ID2SYM( rb_intern("writemap") ) );
801
821
  if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_WRITEMAP;
802
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_threadlocal") ) );
803
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOTLS;
804
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_readahead") ) );
805
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NORDAHEAD;
806
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_memory_init") ) );
807
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOMEMINIT;
808
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("coalesce") ) );
809
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_COALESCE;
810
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("lifo_reclaim") ) );
811
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_LIFORECLAIM;
812
- opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_metasync") ) );
813
- if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOMETASYNC;
814
822
 
815
- /* Duplicate keys, on mdbx_dbi_open, maybe set here? */
816
- /* MDBX_DUPSORT = UINT32_C(0x04), */
817
-
818
- /* Set instance variables.
819
- */
820
- rb_iv_set( self, "@path", path );
821
- 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
+ }
822
826
 
823
827
  rmdbx_open_env( self );
824
828
  return self;
825
829
  }
826
830
 
827
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
+
828
850
  /*
829
851
  * call-seq:
830
852
  * db.clone => [copy of db]
@@ -874,17 +896,19 @@ rmdbx_init_database()
874
896
  rb_define_method( rmdbx_cDatabase, "close", rmdbx_close, 0 );
875
897
  rb_define_method( rmdbx_cDatabase, "closed?", rmdbx_closed_p, 0 );
876
898
  rb_define_method( rmdbx_cDatabase, "drop", rmdbx_drop, 1 );
877
- rb_define_method( rmdbx_cDatabase, "each_key", rmdbx_each_key, 0 );
878
- rb_define_method( rmdbx_cDatabase, "each_pair", rmdbx_each_pair, 0 );
879
- rb_define_method( rmdbx_cDatabase, "each_value", rmdbx_each_value, 0 );
880
- rb_define_method( rmdbx_cDatabase, "in_transaction?", rmdbx_in_transaction_p, 0 );
881
899
  rb_define_method( rmdbx_cDatabase, "include?", rmdbx_include, 1 );
882
900
  rb_define_method( rmdbx_cDatabase, "length", rmdbx_length, 0 );
883
901
  rb_define_method( rmdbx_cDatabase, "reopen", rmdbx_open_env, 0 );
884
902
  rb_define_method( rmdbx_cDatabase, "[]", rmdbx_get_val, 1 );
885
903
  rb_define_method( rmdbx_cDatabase, "[]=", rmdbx_put_val, 2 );
886
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
+
887
910
  /* Manually open/close transactions from ruby. */
911
+ rb_define_method( rmdbx_cDatabase, "in_transaction?", rmdbx_in_transaction_p, 0 );
888
912
  rb_define_protected_method( rmdbx_cDatabase, "open_transaction", rmdbx_rb_opentxn, 1 );
889
913
  rb_define_protected_method( rmdbx_cDatabase, "close_transaction", rmdbx_rb_closetxn, 1 );
890
914
 
@@ -897,3 +921,4 @@ rmdbx_init_database()
897
921
  rb_require( "mdbx/database" );
898
922
  }
899
923
 
924
+