mdbx 0.1.0.pre.20201217111933 → 0.1.0

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: be5af4b8718ccda99471808092ab76ed497758add8a8c429d6eae16b9279c007
4
- data.tar.gz: a183a3ba5019cc1b8cbda9ae45f803342c02011de762202728b6ab96c8f7d133
3
+ metadata.gz: c7cc7a297e0f41d6caa23aa581bd11b1013a64a63ec6b04e9f07daae06f153d4
4
+ data.tar.gz: b5a27168b461c29f88bbaf0c1fc45ec92f0fd121735ef483c76be66adef0750e
5
5
  SHA512:
6
- metadata.gz: e7e463a36ed38653930e9894404431de494cc39e6efa2ae6993681d4198b28c9ecf0c160b09f01353d6296699e9a2ea780973f983a7a32b91b6630d76ad10e2a
7
- data.tar.gz: 300534e3d3c342019d803840e956afdc7d4e3bb114d8fe4df95918cfb85808df146979ff56af539e23ae8bfbacf9f7acab1aaaef4742b0b16a162ad6364d7bba
6
+ metadata.gz: ea27eb32cb736c4cc5d0a154d445e9c7e53966a219ab61bdca7fd4bb886538320e47f40fb9d91569b14b0d6aacf69e0dcab045c5ba5c51416231e5abde8326cd
7
+ data.tar.gz: 0b5496433f6854926f90edc33aeccf9aba653fd76c6db6903ef1d5d95bbafca538957417eba1a90ee087f09faebc2df11eab0e6ceabb67846428ecc8635776c7
checksums.yaml.gz.sig CHANGED
Binary file
data.tar.gz.sig CHANGED
Binary file
data/History.md CHANGED
@@ -1,6 +1,10 @@
1
- # Release History for mdbx
1
+ # Release History for MDBX
2
2
 
3
3
  ---
4
+ ## v0.1.0 [2021-03-14] Mahlon E. Smith <mahlon@martini.nu>
5
+
6
+ Initial public release.
7
+
4
8
 
5
9
  ## v0.0.1 [2020-12-16] Mahlon E. Smith <mahlon@martini.nu>
6
10
 
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
- # Ruby MDBX
1
+
2
+ # Ruby::MDBX
2
3
 
3
4
  home
4
5
  : https://code.martini.nu/ruby-mdbx
@@ -24,64 +25,333 @@ sourcehut:
24
25
  This is a Ruby (MRI) binding for the libmdbx database library.
25
26
 
26
27
  libmdbx is an extremely fast, compact, powerful, embedded, transactional
27
- key-value database, with permissive license. libmdbx has a specific set
28
+ key-value database, with a permissive license. libmdbx has a specific set
28
29
  of properties and capabilities, focused on creating unique lightweight
29
30
  solutions.
30
31
 
31
- - Allows a swarm of multi-threaded processes to ACIDly read and update
32
- several key-value maps and multimaps in a locally-shared database.
32
+ For more information about libmdbx (features, limitations, etc), see the
33
+ [introduction](https://erthink.github.io/libmdbx/intro.html).
33
34
 
34
- - Provides extraordinary performance, minimal overhead through
35
- Memory-Mapping and Olog(N) operations costs by virtue of B+ tree.
36
35
 
37
- - Requires no maintenance and no crash recovery since it doesn't use
38
- WAL, but that might be a caveat for write-intensive workloads with
39
- durability requirements.
36
+ ## Prerequisites
40
37
 
41
- - Compact and friendly for fully embedding. Only ≈25KLOC of C11,
42
- ≈64K x86 binary code of core, no internal threads neither server
43
- process(es), but implements a simplified variant of the Berkeley DB
44
- and dbm API.
38
+ * Ruby 2.6+
39
+ * [libmdbx](https://github.com/erthink/libmdbx)
45
40
 
46
- - Enforces serializability for writers just by single mutex and
47
- affords wait-free for parallel readers without atomic/interlocked
48
- operations, while writing and reading transactions do not block each
49
- other.
50
41
 
51
- - Guarantee data integrity after crash unless this was explicitly
52
- neglected in favour of write performance.
42
+ ## Installation
53
43
 
54
- - Supports Linux, Windows, MacOS, Android, iOS, FreeBSD, DragonFly,
55
- Solaris, OpenSolaris, OpenIndiana, NetBSD, OpenBSD and other systems
56
- compliant with POSIX.1-2008.
44
+ $ gem install mdbx
57
45
 
58
- - Historically, libmdbx is a deeply revised and extended descendant
59
- of the amazing Lightning Memory-Mapped Database. libmdbx inherits
60
- all benefits from LMDB, but resolves some issues and adds a set of
61
- improvements.
46
+ You may need to be specific if the libmdbx headers are located in a
47
+ nonstandard location for your operating system:
62
48
 
49
+ $ gem install mdbx -- --with-opt-dir=/usr/local
63
50
 
64
- ### Examples
65
51
 
66
- [forthcoming]
52
+ ## Usage
67
53
 
54
+ Some quick concepts:
68
55
 
69
- ## Prerequisites
56
+ - A **database** is contained in a file, normally contained in directory
57
+ with it's associated lockfile.
58
+ - Each database can optionally contain multiple named **collections**,
59
+ which can be thought of as distinct namespaces.
60
+ - Each collection can contain any number of **keys**, and their associated
61
+ **values**.
62
+ - A **snapshot** is a self-consistent read-only view of the database.
63
+ It remains consistent even if another thread or process writes changes.
64
+ - A **transaction** is a writable snapshot. Changes made within a
65
+ transaction are not seen by other snapshots until committed.
70
66
 
71
- * Ruby 2.6+
72
- * libmdbx (https://github.com/erthink/libmdbx)
67
+ ### Open (and close) a database handle
73
68
 
69
+ Open a database handle, creating an empty one if not already present.
74
70
 
75
- ## Installation
71
+ ```ruby
72
+ db = MDBX::Database.open( "/path/to/file", options )
73
+ db.close
74
+ ```
76
75
 
77
- $ gem install mdbx
76
+ In block form, the handle is automatically closed.
77
+
78
+ ```ruby
79
+ MDBX::Database.open( 'database' ) do |db|
80
+ puts db[ 'key1' ]
81
+ end # closed database
82
+ ```
83
+
84
+
85
+ ### Read data
86
+
87
+ You can use the database handle as a hash. Reading a value automatically
88
+ creates a snapshot, retrieves the value, and closes the snapshot before
89
+ returning it.
90
+
91
+ ```ruby
92
+ db[ 'key1' ] #=> val
93
+ ```
94
+
95
+ All data reads require a snapshot (or transaction).
96
+
97
+ The `snapshot` method creates a long-running snapshot manually. In
98
+ block form, the snapshot is automatically closed when the block exits.
99
+ Sharing a snapshot between reads is significantly faster when fetching
100
+ many values or in tight loops.
101
+
102
+ ```ruby
103
+ # read-only block
104
+ db.snapshot do
105
+ db[ 'key1' ] #=> val
106
+ ...
107
+ end # snapshot closed
108
+ ```
109
+
110
+ You can also open and close a snapshot manually.
111
+
112
+ ```ruby
113
+ db.snapshot
114
+ db.values_at( 'key1', 'key2' ) #=> [ value, value ]
115
+ db.rollback
116
+ ```
117
+
118
+ Technically, `snapshot` just sets the internal state and returns the
119
+ database handle - the handle is also yielded when using blocks. The
120
+ following 3 examples are identical, use whatever form you prefer.
121
+
122
+ ```ruby
123
+ snap = db.snapshot
124
+ snap[ 'key1' ]
125
+ snap.abort
126
+
127
+ db.snapshot do |snap|
128
+ snap[ 'key1' ]
129
+ end
130
+
131
+ db.snapshot do
132
+ db[ 'key1' ]
133
+ end
134
+ ```
135
+
136
+ Attempting writes while within an open snapshot is an exception.
137
+
138
+
139
+ ### Write data
140
+
141
+ Writing data is also hash-like. Assigning a value to a key
142
+ automatically opens a writable transaction, stores the value, and
143
+ commits the transaction before returning.
144
+
145
+ All keys are strings, or converted to a string automatically.
146
+
147
+ ```ruby
148
+ db[ 'key1' ] = val
149
+ db[ :key1 ] == db[ 'key1' ] #=> true
150
+ ```
151
+
152
+ All data writes require a transaction.
153
+
154
+ The `transaction` method creates a long-running transaction manually. In
155
+ block form, the transaction is automatically closed when the block exits.
156
+ Sharing a transaction between writes is significantly faster when
157
+ storing many values or in tight loops.
158
+
159
+ ```ruby
160
+ # read/write block
161
+ db.transaction do
162
+ db[ 'key1' ] = val
163
+ end # transaction committed and closed
164
+ ```
165
+
166
+ You can also open and close a transaction manually.
167
+
168
+ ```ruby
169
+ db.transaction
170
+ db[ 'key1' ] = val
171
+ db.commit
172
+ ```
173
+
174
+ Like snapshots, `transaction` just sets the internal state and returns
175
+ the database handle - the handle is also yielded when using blocks. The
176
+ following 3 examples are identical, use whatever form you prefer.
177
+
178
+ ```ruby
179
+ txn = db.transaction
180
+ txn[ 'key1' ] = true
181
+ txn.save
182
+
183
+ db.transaction do |txn|
184
+ txn[ 'key1' ] = true
185
+ end
186
+
187
+ db.transaction do
188
+ db[ 'key1' ] = true
189
+ end
190
+ ```
191
+
192
+ ### Delete data
193
+
194
+ Just write a `nil` value to remove a key entirely, or like Hash, use the
195
+ `#delete` method:
196
+
197
+ ```ruby
198
+ db[ 'key1' ] = nil
199
+ ```
200
+
201
+ ```ruby
202
+ oldval = db.delete( 'key1' )
203
+ ```
204
+
205
+
206
+ ### Transactions
207
+
208
+ Transactions are largely modelled after the
209
+ [Sequel](https://sequel.jeremyevans.net/rdoc/files/doc/transactions_rdoc.html)
210
+ transaction basics.
211
+
212
+ While in a transaction block, if no exception is raised, the
213
+ transaction is automatically committed and closed when the block exits.
214
+
215
+ ```ruby
216
+ db[ 'key' ] = false
217
+
218
+ db.transaction do # BEGIN
219
+ db[ 'key' ] = true
220
+ end # COMMIT
221
+
222
+ db[ 'key' ] #=> true
223
+ ```
224
+
225
+ If the block raises a MDBX::Rollback exception, the transaction is
226
+ rolled back, but no exception is raised outside the block:
227
+
228
+ ```ruby
229
+ db[ 'key' ] = false
230
+
231
+ db.transaction do # BEGIN
232
+ db[ 'key' ] = true
233
+ raise MDBX::Rollback
234
+ end # ROLLBACK
235
+
236
+ db[ 'key' ] #=> false
237
+ ```
238
+
239
+ If any other exception is raised, the transaction is rolled back, and
240
+ the exception is raised outside the block:
241
+
242
+ ```ruby
243
+ db[ 'key' ] = false
244
+
245
+ db.transaction do # BEGIN
246
+ db[ 'key' ] = true
247
+ raise ArgumentError
248
+ end # ROLLBACK
249
+
250
+ # ArgumentError raised
251
+ ```
252
+
253
+
254
+ If you want to check whether you are currently in a transaction, use the
255
+ Database#in_transaction? method:
256
+
257
+ ```ruby
258
+ db.in_transaction? #=> false
259
+ db.transaction do
260
+ db.in_transaction? #=> true
261
+ end
262
+ ```
263
+
264
+ MDBX writes are strongly serialized, and an open transaction blocks
265
+ other writers until it has completed. Snapshots have no such
266
+ serialization, and readers from separate processes do not interfere with
267
+ each other. Be aware of libmdbx behaviors while in open transactions.
268
+
269
+
270
+ ### Collections
271
+
272
+ A MDBX collection is a sub-database, or a namespace. In order to use
273
+ this feature, the database must be opened with the `max_collections`
274
+ option:
275
+
276
+ ```ruby
277
+ db = MDBX::Database.open( "/path/to/file", max_collections: 10 )
278
+ ```
279
+
280
+ Afterwards, you can switch collections at will.
281
+
282
+ ```ruby
283
+ db.collection( 'sub' )
284
+ db.collection #=> 'sub'
285
+ db[ :key ] = true
286
+ db.main # switch to the top level
287
+ db[ :key ] #=> nil
288
+ ```
289
+
290
+ In block form, the collection is reverted to the current collection when
291
+ the block was started:
292
+
293
+ ```ruby
294
+ db.collection( 'sub1' )
295
+ db.collection( 'sub2' ) do
296
+ db[ :key ] = true
297
+ end # the collection is reverted to 'sub1'
298
+ ```
299
+
300
+ Collections cannot be switched while a snapshot or transaction is open.
301
+
302
+ Collection names are stored in the top-level database as keys. Attempts
303
+ to use these keys as regular values, or switching to a key that is not
304
+ a collection will result in an incompatibility error. While using
305
+ collections, It's probably wise to not store regular key/value data in a
306
+ top-level database to avoid this ambiguity.
307
+
308
+
309
+ ### Value Serialization
310
+
311
+ By default, all values are stored as Marshal data - this is the most
312
+ "Ruby" behavior, as you can store any Ruby object directly that supports
313
+ `Marshal.dump`.
314
+
315
+ ```ruby
316
+ db.serializer = ->( v ) { Marshal.dump( v ) }
317
+ db.deserializer = ->( v ) { Marshal.load( v ) }
318
+ ```
319
+
320
+ For compatibility with databases used by other languages, or if your
321
+ needs are more specific, you can disable or override the default
322
+ serialization behaviors after opening the database.
323
+
324
+ ```ruby
325
+ # All values are JSON strings
326
+ db.serializer = ->( v ) { JSON.generate( v ) }
327
+ db.deserializer = ->( v ) { JSON.parse( v ) }
328
+ ```
329
+
330
+ ```ruby
331
+ # Disable all automatic serialization
332
+ db.serializer = nil
333
+ db.deserializer = nil
334
+ ```
335
+
336
+ ### Introspection
337
+
338
+ Calling `statistics` on a database handle will provide a subset of
339
+ information about the build environment, the database environment, and
340
+ the currently connected clients.
341
+
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`.
78
348
 
79
349
 
80
350
  ## Contributing
81
351
 
82
352
  You can check out the current development source with Mercurial via its
83
353
  [home repo](https://code.martini.nu/ruby-mdbx), or with Git at its
84
- [project page](https://gitlab.com/mahlon/ruby-mdbx).
354
+ [project mirror](https://gitlab.com/mahlon/ruby-mdbx).
85
355
 
86
356
  After checking out the source, run:
87
357
 
@@ -99,7 +369,7 @@ development.
99
369
 
100
370
  ## License
101
371
 
102
- Copyright (c) 2020, Mahlon E. Smith
372
+ Copyright (c) 2020-2021 Mahlon E. Smith
103
373
  All rights reserved.
104
374
 
105
375
  Redistribution and use in source and binary forms, with or without
@@ -2,42 +2,18 @@
2
2
 
3
3
  #include "mdbx_ext.h"
4
4
 
5
- /* VALUE str = rb_sprintf( "path: %+"PRIsVALUE", opts: %+"PRIsVALUE, path, opts ); */
6
- /* printf( "%s\n", StringValueCStr(str) ); */
7
-
8
- VALUE rmdbx_cDatabase;
9
-
10
-
11
5
  /* Shortcut for fetching current DB variables.
12
- *
13
6
  */
14
7
  #define UNWRAP_DB( val, db ) \
15
8
  rmdbx_db_t *db; \
16
9
  TypedData_Get_Struct( val, rmdbx_db_t, &rmdbx_db_data, db );
17
10
 
18
11
 
19
- /*
20
- * A struct encapsulating an instance's DB state.
21
- */
22
- struct rmdbx_db {
23
- MDBX_env *env;
24
- MDBX_dbi dbi;
25
- MDBX_txn *txn;
26
- MDBX_cursor *cursor;
27
- int env_flags;
28
- int mode;
29
- int open;
30
- int max_collections;
31
- char *path;
32
- char *subdb;
33
- };
34
- typedef struct rmdbx_db rmdbx_db_t;
35
-
12
+ VALUE rmdbx_cDatabase;
36
13
 
37
14
  /*
38
15
  * Ruby allocation hook.
39
16
  */
40
- void rmdbx_free( void *db ); /* forward declaration */
41
17
  static const rb_data_type_t rmdbx_db_data = {
42
18
  .wrap_struct_name = "MDBX::Database::Data",
43
19
  .function = { .dfree = rmdbx_free },
@@ -61,13 +37,27 @@ rmdbx_alloc( VALUE klass )
61
37
  * removed.
62
38
  */
63
39
  void
64
- rmdbx_close_all( rmdbx_db_t* db )
40
+ rmdbx_close_all( rmdbx_db_t *db )
65
41
  {
66
42
  if ( db->cursor ) mdbx_cursor_close( db->cursor );
67
43
  if ( db->txn ) mdbx_txn_abort( db->txn );
68
44
  if ( db->dbi ) mdbx_dbi_close( db->env, db->dbi );
69
45
  if ( db->env ) mdbx_env_close( db->env );
70
- db->open = 0;
46
+ db->state.open = 0;
47
+ }
48
+
49
+
50
+ /*
51
+ * Close any open database handle. Will be automatically
52
+ * re-opened on next transaction. This is primarily useful for
53
+ * switching between subdatabases.
54
+ */
55
+ void
56
+ rmdbx_close_dbi( rmdbx_db_t *db )
57
+ {
58
+ if ( ! db->dbi ) return;
59
+ mdbx_dbi_close( db->env, db->dbi );
60
+ db->dbi = 0;
71
61
  }
72
62
 
73
63
 
@@ -79,13 +69,13 @@ rmdbx_free( void *db )
79
69
  {
80
70
  if ( db ) {
81
71
  rmdbx_close_all( db );
82
- free( db );
72
+ xfree( db );
83
73
  }
84
74
  }
85
75
 
86
76
 
87
77
  /*
88
- * Cleanly close an opened database from Ruby.
78
+ * Cleanly close an opened database.
89
79
  */
90
80
  VALUE
91
81
  rmdbx_close( VALUE self )
@@ -96,8 +86,38 @@ rmdbx_close( VALUE self )
96
86
  }
97
87
 
98
88
 
89
+ /*
90
+ * call-seq:
91
+ * db.closed? => false
92
+ *
93
+ * Predicate: return true if the database environment is closed.
94
+ */
95
+ VALUE
96
+ rmdbx_closed_p( VALUE self )
97
+ {
98
+ UNWRAP_DB( self, db );
99
+ return db->state.open == 1 ? Qfalse : Qtrue;
100
+ }
101
+
102
+
103
+ /*
104
+ * call-seq:
105
+ * db.in_transaction? => false
106
+ *
107
+ * Predicate: return true if a transaction (or snapshot)
108
+ * is currently open.
109
+ */
110
+ VALUE
111
+ rmdbx_in_transaction_p( VALUE self )
112
+ {
113
+ UNWRAP_DB( self, db );
114
+ return db->txn ? Qtrue : Qfalse;
115
+ }
116
+
117
+
99
118
  /*
100
119
  * Open the DB environment handle.
120
+ *
101
121
  */
102
122
  VALUE
103
123
  rmdbx_open_env( VALUE self )
@@ -113,48 +133,60 @@ rmdbx_open_env( VALUE self )
113
133
  rb_raise( rmdbx_eDatabaseError, "mdbx_env_create: (%d) %s", rc, mdbx_strerror(rc) );
114
134
 
115
135
  /* Set the maximum number of named databases for the environment. */
116
- // FIXME: potenially more env setups here? maxreaders, pagesize?
117
- mdbx_env_set_maxdbs( db->env, db->max_collections );
136
+ mdbx_env_set_maxdbs( db->env, db->settings.max_collections );
137
+
138
+ /* Customize the maximum number of simultaneous readers. */
139
+ if ( db->settings.max_readers )
140
+ mdbx_env_set_maxreaders( db->env, db->settings.max_readers );
118
141
 
119
- rc = mdbx_env_open( db->env, db->path, db->env_flags, db->mode );
142
+ /* Set an upper boundary (in bytes) for the database map size. */
143
+ if ( db->settings.max_size )
144
+ mdbx_env_set_geometry( db->env, -1, -1, db->settings.max_size, -1, -1, -1 );
145
+
146
+ rc = mdbx_env_open( db->env, db->path, db->settings.env_flags, db->settings.mode );
120
147
  if ( rc != MDBX_SUCCESS ) {
121
- rmdbx_close( self );
148
+ rmdbx_close_all( db );
122
149
  rb_raise( rmdbx_eDatabaseError, "mdbx_env_open: (%d) %s", rc, mdbx_strerror(rc) );
123
150
  }
124
- db->open = 1;
151
+ db->state.open = 1;
125
152
 
126
153
  return Qtrue;
127
154
  }
128
155
 
129
156
 
130
157
  /*
131
- * call-seq:
132
- * db.closed? #=> false
133
- *
134
- * Predicate: return true if the database environment is closed.
158
+ * Open a cursor for iteration.
135
159
  */
136
- VALUE
137
- rmdbx_closed_p( VALUE self )
160
+ void
161
+ rmdbx_open_cursor( rmdbx_db_t *db )
138
162
  {
139
- UNWRAP_DB( self, db );
140
- return db->open == 1 ? Qfalse : Qtrue;
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;
141
173
  }
142
174
 
143
175
 
144
176
  /*
145
- * Open a new database transaction.
177
+ * Open a new database transaction. If a transaction is already
178
+ * open, this is a no-op.
146
179
  *
147
180
  * +rwflag+ must be either MDBX_TXN_RDONLY or MDBX_TXN_READWRITE.
148
181
  */
149
182
  void
150
- rmdbx_open_txn( VALUE self, int rwflag )
183
+ rmdbx_open_txn( rmdbx_db_t *db, int rwflag )
151
184
  {
152
- int rc;
153
- UNWRAP_DB( self, db );
185
+ if ( db->txn ) return;
154
186
 
155
- rc = mdbx_txn_begin( db->env, NULL, rwflag, &db->txn);
187
+ int rc = mdbx_txn_begin( db->env, NULL, rwflag, &db->txn );
156
188
  if ( rc != MDBX_SUCCESS ) {
157
- rmdbx_close( self );
189
+ rmdbx_close_all( db );
158
190
  rb_raise( rmdbx_eDatabaseError, "mdbx_txn_begin: (%d) %s", rc, mdbx_strerror(rc) );
159
191
  }
160
192
 
@@ -162,7 +194,7 @@ rmdbx_open_txn( VALUE self, int rwflag )
162
194
  // FIXME: dbi_flags
163
195
  rc = mdbx_dbi_open( db->txn, db->subdb, MDBX_CREATE, &db->dbi );
164
196
  if ( rc != MDBX_SUCCESS ) {
165
- rmdbx_close( self );
197
+ rmdbx_close_all( db );
166
198
  rb_raise( rmdbx_eDatabaseError, "mdbx_dbi_open: (%d) %s", rc, mdbx_strerror(rc) );
167
199
  }
168
200
  }
@@ -171,24 +203,90 @@ rmdbx_open_txn( VALUE self, int rwflag )
171
203
  }
172
204
 
173
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
+ /*
231
+ * 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.
236
+ *
237
+ */
238
+ VALUE
239
+ rmdbx_rb_opentxn( VALUE self, VALUE mode )
240
+ {
241
+ UNWRAP_DB( self, db );
242
+
243
+ rmdbx_open_txn( db, RTEST(mode) ? MDBX_TXN_READWRITE : MDBX_TXN_RDONLY );
244
+ db->state.retain_txn = RTEST(mode) ? 1 : 0;
245
+
246
+ return Qtrue;
247
+ }
248
+
249
+
250
+ /*
251
+ * 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.
256
+ *
257
+ */
258
+ VALUE
259
+ rmdbx_rb_closetxn( VALUE self, VALUE write )
260
+ {
261
+ UNWRAP_DB( self, db );
262
+
263
+ db->state.retain_txn = -1;
264
+ rmdbx_close_txn( db, RTEST(write) ? RMDBX_TXN_COMMIT : RMDBX_TXN_ROLLBACK );
265
+
266
+ return Qtrue;
267
+ }
268
+
269
+
174
270
  /*
175
271
  * call-seq:
176
272
  * db.clear
177
273
  *
178
- * Empty the database (or collection) on disk. Unrecoverable!
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!
179
277
  */
180
278
  VALUE
181
279
  rmdbx_clear( VALUE self )
182
280
  {
183
281
  UNWRAP_DB( self, db );
184
282
 
185
- rmdbx_open_txn( self, MDBX_TXN_READWRITE );
283
+ rmdbx_open_txn( db, MDBX_TXN_READWRITE );
186
284
  int rc = mdbx_drop( db->txn, db->dbi, true );
187
285
 
188
286
  if ( rc != MDBX_SUCCESS )
189
287
  rb_raise( rmdbx_eDatabaseError, "mdbx_drop: (%d) %s", rc, mdbx_strerror(rc) );
190
288
 
191
- mdbx_txn_commit( db->txn );
289
+ rmdbx_close_txn( db, RMDBX_TXN_COMMIT );
192
290
 
193
291
  /* Refresh the environment handles. */
194
292
  rmdbx_open_env( self );
@@ -236,72 +334,161 @@ rmdbx_val_for( VALUE self, VALUE arg )
236
334
  }
237
335
 
238
336
 
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 );
346
+
347
+ return val;
348
+ }
349
+
350
+
239
351
  /* call-seq:
240
- * db.keys #=> [ 'key1', 'key2', ... ]
352
+ * db.each_key {|key| block } => self
241
353
  *
242
- * Return an array of all keys in the current collection.
354
+ * Calls the block once for each key, returning self.
355
+ * A transaction must be opened prior to use.
243
356
  */
244
357
  VALUE
245
- rmdbx_keys( VALUE self )
358
+ rmdbx_each_key( VALUE self )
246
359
  {
247
360
  UNWRAP_DB( self, db );
248
- VALUE rv = rb_ary_new();
249
361
  MDBX_val key, data;
250
- int rc;
251
362
 
252
- if ( ! db->open ) rb_raise( rmdbx_eDatabaseError, "Closed database." );
363
+ rmdbx_open_cursor( db );
364
+ RETURN_ENUMERATOR( self, 0, 0 );
253
365
 
254
- rmdbx_open_txn( self, MDBX_TXN_RDONLY );
255
- rc = mdbx_cursor_open( db->txn, db->dbi, &db->cursor);
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
+ }
256
372
 
257
- if ( rc != MDBX_SUCCESS ) {
258
- rmdbx_close( self );
259
- rb_raise( rmdbx_eDatabaseError, "Unable to open cursor: (%d) %s", rc, mdbx_strerror(rc) );
373
+ mdbx_cursor_close( db->cursor );
374
+ db->cursor = NULL;
375
+ return self;
376
+ }
377
+
378
+
379
+ /* call-seq:
380
+ * db.each_value {|value| block } => self
381
+ *
382
+ * Calls the block once for each value, returning self.
383
+ * A transaction must be opened prior to use.
384
+ */
385
+ VALUE
386
+ rmdbx_each_value( VALUE self )
387
+ {
388
+ UNWRAP_DB( self, db );
389
+ MDBX_val key, data;
390
+
391
+ rmdbx_open_cursor( db );
392
+ RETURN_ENUMERATOR( self, 0, 0 );
393
+
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 ) );
397
+
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
+ }
260
402
  }
261
403
 
262
- rc = mdbx_cursor_get( db->cursor, &key, &data, MDBX_FIRST );
263
- if ( rc == MDBX_SUCCESS ) {
264
- rb_ary_push( rv, rb_str_new( key.iov_base, key.iov_len ) );
265
- while ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_NEXT ) == 0 ) {
266
- rb_ary_push( rv, rb_str_new( key.iov_base, key.iov_len ) );
404
+ mdbx_cursor_close( db->cursor );
405
+ db->cursor = NULL;
406
+ return self;
407
+ }
408
+
409
+
410
+ /* call-seq:
411
+ * db.each_pair {|key, value| block } => self
412
+ *
413
+ * Calls the block once for each key and value, returning self.
414
+ * A transaction must be opened prior to use.
415
+ */
416
+ VALUE
417
+ rmdbx_each_pair( VALUE self )
418
+ {
419
+ UNWRAP_DB( self, db );
420
+ MDBX_val key, data;
421
+
422
+ rmdbx_open_cursor( db );
423
+ RETURN_ENUMERATOR( self, 0, 0 );
424
+
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 ) ) );
429
+
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 ) ) );
267
434
  }
268
435
  }
269
436
 
270
437
  mdbx_cursor_close( db->cursor );
271
438
  db->cursor = NULL;
272
- mdbx_txn_abort( db->txn );
439
+ return self;
440
+ }
441
+
442
+
443
+ /* call-seq:
444
+ * db.length -> Integer
445
+ *
446
+ * Returns the count of keys in the currently selected collection.
447
+ */
448
+ VALUE
449
+ rmdbx_length( VALUE self )
450
+ {
451
+ UNWRAP_DB( self, db );
452
+ MDBX_stat mstat;
453
+
454
+ if ( ! db->state.open ) rb_raise( rmdbx_eDatabaseError, "Closed database." );
455
+ rmdbx_open_txn( db, MDBX_TXN_RDONLY );
456
+
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) );
460
+
461
+ VALUE rv = LONG2FIX( mstat.ms_entries );
462
+ rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK );
463
+
273
464
  return rv;
274
465
  }
275
466
 
276
467
 
277
468
  /* call-seq:
278
- * db[ 'key' ] #=> value
469
+ * db[ 'key' ] => value
279
470
  *
280
- * Convenience method: return a single value for +key+ immediately.
471
+ * Return a single value for +key+ immediately.
281
472
  */
282
473
  VALUE
283
474
  rmdbx_get_val( VALUE self, VALUE key )
284
475
  {
285
476
  int rc;
286
- VALUE deserialize_proc;
287
477
  UNWRAP_DB( self, db );
288
478
 
289
- if ( ! db->open ) rb_raise( rmdbx_eDatabaseError, "Closed database." );
290
-
291
- rmdbx_open_txn( self, MDBX_TXN_RDONLY );
479
+ if ( ! db->state.open ) rb_raise( rmdbx_eDatabaseError, "Closed database." );
480
+ rmdbx_open_txn( db, MDBX_TXN_RDONLY );
292
481
 
293
482
  MDBX_val ckey = rmdbx_key_for( key );
294
483
  MDBX_val data;
484
+ VALUE rv;
295
485
  rc = mdbx_get( db->txn, db->dbi, &ckey, &data );
296
- mdbx_txn_abort( db->txn );
486
+ rmdbx_close_txn( db, RMDBX_TXN_ROLLBACK );
297
487
 
298
488
  switch ( rc ) {
299
489
  case MDBX_SUCCESS:
300
- deserialize_proc = rb_iv_get( self, "@deserializer" );
301
- VALUE rv = rb_str_new( data.iov_base, data.iov_len );
302
- if ( ! NIL_P( deserialize_proc ) )
303
- return rb_funcall( deserialize_proc, rb_intern("call"), 1, rv );
304
- return rv;
490
+ rv = rb_str_new( data.iov_base, data.iov_len );
491
+ return rmdbx_deserialize( self, rv );
305
492
 
306
493
  case MDBX_NOTFOUND:
307
494
  return Qnil;
@@ -314,9 +501,9 @@ rmdbx_get_val( VALUE self, VALUE key )
314
501
 
315
502
 
316
503
  /* call-seq:
317
- * db[ 'key' ] = value #=> value
504
+ * db[ 'key' ] = value
318
505
  *
319
- * Convenience method: set a single value for +key+
506
+ * Set a single value for +key+.
320
507
  */
321
508
  VALUE
322
509
  rmdbx_put_val( VALUE self, VALUE key, VALUE val )
@@ -324,9 +511,8 @@ rmdbx_put_val( VALUE self, VALUE key, VALUE val )
324
511
  int rc;
325
512
  UNWRAP_DB( self, db );
326
513
 
327
- if ( ! db->open ) rb_raise( rmdbx_eDatabaseError, "Closed database." );
328
-
329
- rmdbx_open_txn( self, MDBX_TXN_READWRITE );
514
+ if ( ! db->state.open ) rb_raise( rmdbx_eDatabaseError, "Closed database." );
515
+ rmdbx_open_txn( db, MDBX_TXN_READWRITE );
330
516
 
331
517
  MDBX_val ckey = rmdbx_key_for( key );
332
518
 
@@ -341,7 +527,7 @@ rmdbx_put_val( VALUE self, VALUE key, VALUE val )
341
527
  rc = mdbx_replace( db->txn, db->dbi, &ckey, &data, &old, 0 );
342
528
  }
343
529
 
344
- mdbx_txn_commit( db->txn );
530
+ rmdbx_close_txn( db, RMDBX_TXN_COMMIT );
345
531
 
346
532
  switch ( rc ) {
347
533
  case MDBX_SUCCESS:
@@ -356,38 +542,85 @@ rmdbx_put_val( VALUE self, VALUE key, VALUE val )
356
542
 
357
543
  /*
358
544
  * call-seq:
359
- * db.collection( 'collection_name' ) # => db
360
- * db.collection( nil ) # => db (main)
545
+ * db.statistics => (hash of stats)
546
+ *
547
+ * Returns a hash populated with various metadata for the opened
548
+ * database.
549
+ *
550
+ */
551
+ VALUE
552
+ rmdbx_stats( VALUE self )
553
+ {
554
+ UNWRAP_DB( self, db );
555
+ if ( ! db->state.open ) rb_raise( rmdbx_eDatabaseError, "Closed database." );
556
+
557
+ return rmdbx_gather_stats( db );
558
+ }
559
+
560
+
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)
361
566
  *
362
- * Operate on a sub-database "collection". Passing +nil+
363
- * sets the database to the main, top-level namespace.
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
364
576
  *
365
577
  */
366
578
  VALUE
367
579
  rmdbx_set_subdb( int argc, VALUE *argv, VALUE self )
368
580
  {
369
581
  UNWRAP_DB( self, db );
370
- VALUE subdb;
582
+ VALUE subdb, block;
583
+ char *prev_db = NULL;
371
584
 
372
- rb_scan_args( argc, argv, "01", &subdb );
585
+ rb_scan_args( argc, argv, "01&", &subdb, &block );
373
586
  if ( argc == 0 ) {
374
587
  if ( db->subdb == NULL ) return Qnil;
375
588
  return rb_str_new_cstr( db->subdb );
376
589
  }
377
590
 
378
- rb_iv_set( self, "@collection", subdb );
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." );
594
+
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" );
598
+
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
+ }
605
+
379
606
  db->subdb = NIL_P( subdb ) ? NULL : StringValueCStr( subdb );
607
+ rmdbx_close_dbi( db );
380
608
 
381
- /* Close any currently open dbi handle, to be re-opened with
382
- * the new collection on next access.
383
- *
609
+ /*
384
610
  FIXME: Immediate transaction write to auto-create new env?
385
611
  Fetching from here at the moment causes an error if you
386
- haven't written anything yet.
612
+ haven't written anything to the new collection yet.
387
613
  */
388
- if ( db->dbi ) {
389
- mdbx_dbi_close( db->env, db->dbi );
390
- db->dbi = 0;
614
+
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 );
622
+ }
623
+ xfree( prev_db );
391
624
  }
392
625
 
393
626
  return self;
@@ -395,25 +628,20 @@ rmdbx_set_subdb( int argc, VALUE *argv, VALUE self )
395
628
 
396
629
 
397
630
  /*
398
- * call-seq:
631
+ * Open an existing (or create a new) mdbx database at filesystem
632
+ * +path+. In block form, the database is automatically closed.
633
+ *
399
634
  * MDBX::Database.open( path ) -> db
400
635
  * MDBX::Database.open( path, options ) -> db
401
636
  * MDBX::Database.open( path, options ) do |db|
402
- * db...
637
+ * db...
403
638
  * end
404
639
  *
405
- * Open an existing (or create a new) mdbx database at filesystem
406
- * +path+. In block form, the database is automatically closed.
407
- *
408
640
  */
409
641
  VALUE
410
642
  rmdbx_database_initialize( int argc, VALUE *argv, VALUE self )
411
643
  {
412
- int mode = 0644;
413
- int max_collections = 0;
414
- int env_flags = MDBX_ENV_DEFAULTS;
415
644
  VALUE path, opts, opt;
416
-
417
645
  rb_scan_args( argc, argv, "11", &path, &opts );
418
646
 
419
647
  /* Ensure options is a hash if it was passed in.
@@ -426,52 +654,59 @@ rmdbx_database_initialize( int argc, VALUE *argv, VALUE self )
426
654
  }
427
655
  rb_hash_freeze( opts );
428
656
 
657
+ /* Initialize the DB vals.
658
+ */
659
+ UNWRAP_DB( self, db );
660
+ db->env = NULL;
661
+ db->dbi = 0;
662
+ db->txn = NULL;
663
+ db->cursor = NULL;
664
+ db->path = StringValueCStr( path );
665
+ db->subdb = NULL;
666
+ db->state.open = 0;
667
+ db->state.retain_txn = -1;
668
+ db->settings.env_flags = MDBX_ENV_DEFAULTS;
669
+ db->settings.mode = 0644;
670
+ db->settings.max_collections = 0;
671
+ db->settings.max_readers = 0;
672
+ db->settings.max_size = 0;
673
+
429
674
  /* Options setup, overrides.
430
675
  */
431
676
  opt = rb_hash_aref( opts, ID2SYM( rb_intern("mode") ) );
432
- if ( ! NIL_P(opt) ) mode = FIX2INT( opt );
677
+ if ( ! NIL_P(opt) ) db->settings.mode = FIX2INT( opt );
433
678
  opt = rb_hash_aref( opts, ID2SYM( rb_intern("max_collections") ) );
434
- if ( ! NIL_P(opt) ) max_collections = FIX2INT( opt );
679
+ if ( ! NIL_P(opt) ) db->settings.max_collections = FIX2INT( opt );
680
+ opt = rb_hash_aref( opts, ID2SYM( rb_intern("max_readers") ) );
681
+ if ( ! NIL_P(opt) ) db->settings.max_readers = FIX2INT( opt );
682
+ opt = rb_hash_aref( opts, ID2SYM( rb_intern("max_size") ) );
683
+ if ( ! NIL_P(opt) ) db->settings.max_size = NUM2LONG( opt );
435
684
  opt = rb_hash_aref( opts, ID2SYM( rb_intern("nosubdir") ) );
436
- if ( RTEST(opt) ) env_flags = env_flags | MDBX_NOSUBDIR;
685
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOSUBDIR;
437
686
  opt = rb_hash_aref( opts, ID2SYM( rb_intern("readonly") ) );
438
- if ( RTEST(opt) ) env_flags = env_flags | MDBX_RDONLY;
687
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_RDONLY;
439
688
  opt = rb_hash_aref( opts, ID2SYM( rb_intern("exclusive") ) );
440
- if ( RTEST(opt) ) env_flags = env_flags | MDBX_EXCLUSIVE;
689
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_EXCLUSIVE;
441
690
  opt = rb_hash_aref( opts, ID2SYM( rb_intern("compat") ) );
442
- if ( RTEST(opt) ) env_flags = env_flags | MDBX_ACCEDE;
691
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_ACCEDE;
443
692
  opt = rb_hash_aref( opts, ID2SYM( rb_intern("writemap") ) );
444
- if ( RTEST(opt) ) env_flags = env_flags | MDBX_WRITEMAP;
693
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_WRITEMAP;
445
694
  opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_threadlocal") ) );
446
- if ( RTEST(opt) ) env_flags = env_flags | MDBX_NOTLS;
695
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOTLS;
447
696
  opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_readahead") ) );
448
- if ( RTEST(opt) ) env_flags = env_flags | MDBX_NORDAHEAD;
697
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NORDAHEAD;
449
698
  opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_memory_init") ) );
450
- if ( RTEST(opt) ) env_flags = env_flags | MDBX_NOMEMINIT;
699
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOMEMINIT;
451
700
  opt = rb_hash_aref( opts, ID2SYM( rb_intern("coalesce") ) );
452
- if ( RTEST(opt) ) env_flags = env_flags | MDBX_COALESCE;
701
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_COALESCE;
453
702
  opt = rb_hash_aref( opts, ID2SYM( rb_intern("lifo_reclaim") ) );
454
- if ( RTEST(opt) ) env_flags = env_flags | MDBX_LIFORECLAIM;
703
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_LIFORECLAIM;
455
704
  opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_metasync") ) );
456
- if ( RTEST(opt) ) env_flags = env_flags | MDBX_NOMETASYNC;
705
+ if ( RTEST(opt) ) db->settings.env_flags = db->settings.env_flags | MDBX_NOMETASYNC;
457
706
 
458
707
  /* Duplicate keys, on mdbx_dbi_open, maybe set here? */
459
708
  /* MDBX_DUPSORT = UINT32_C(0x04), */
460
709
 
461
- /* Initialize the DB vals.
462
- */
463
- UNWRAP_DB( self, db );
464
- db->env = NULL;
465
- db->dbi = 0;
466
- db->txn = NULL;
467
- db->cursor = NULL;
468
- db->env_flags = env_flags;
469
- db->mode = mode;
470
- db->max_collections = max_collections;
471
- db->path = StringValueCStr( path );
472
- db->open = 0;
473
- db->subdb = NULL;
474
-
475
710
  /* Set instance variables.
476
711
  */
477
712
  rb_iv_set( self, "@path", path );
@@ -501,11 +736,21 @@ rmdbx_init_database()
501
736
  rb_define_method( rmdbx_cDatabase, "close", rmdbx_close, 0 );
502
737
  rb_define_method( rmdbx_cDatabase, "reopen", rmdbx_open_env, 0 );
503
738
  rb_define_method( rmdbx_cDatabase, "closed?", rmdbx_closed_p, 0 );
739
+ rb_define_method( rmdbx_cDatabase, "in_transaction?", rmdbx_in_transaction_p, 0 );
504
740
  rb_define_method( rmdbx_cDatabase, "clear", rmdbx_clear, 0 );
505
- rb_define_method( rmdbx_cDatabase, "keys", rmdbx_keys, 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 );
744
+ rb_define_method( rmdbx_cDatabase, "length", rmdbx_length, 0 );
506
745
  rb_define_method( rmdbx_cDatabase, "[]", rmdbx_get_val, 1 );
507
746
  rb_define_method( rmdbx_cDatabase, "[]=", rmdbx_put_val, 2 );
508
747
 
748
+ /* Manually open/close transactions from ruby. */
749
+ rb_define_protected_method( rmdbx_cDatabase, "open_transaction", rmdbx_rb_opentxn, 1 );
750
+ rb_define_protected_method( rmdbx_cDatabase, "close_transaction", rmdbx_rb_closetxn, 1 );
751
+
752
+ rb_define_protected_method( rmdbx_cDatabase, "raw_stats", rmdbx_stats, 0 );
753
+
509
754
  rb_require( "mdbx/database" );
510
755
  }
511
756