mdbx 0.3.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: 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
+