mdbx 0.1.0.pre.20201217111933 → 0.1.0

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: 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