lmdb 0.4.7 → 0.6

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.
@@ -26,6 +26,7 @@ static void check(int code) {
26
26
 
27
27
  const char* err = mdb_strerror(code);
28
28
  const char* sep = strchr(err, ':');
29
+ // increment the offset by two in case there is a colon (plus space)
29
30
  if (sep)
30
31
  err = sep + 2;
31
32
 
@@ -37,15 +38,19 @@ static void check(int code) {
37
38
  }
38
39
 
39
40
  static void transaction_free(Transaction* transaction) {
40
- if (transaction->txn) {
41
- rb_warn("Memory leak - Garbage collecting active transaction");
42
- // mdb_txn_abort(transaction->txn);
41
+ if (transaction->txn && !NIL_P(transaction->txn)) {
42
+ //int id = (int)mdb_txn_id(transaction->txn);
43
+ //rb_warn(sprintf("Memory leak: Garbage collecting active transaction %d", id));
44
+ rb_warn("Memory leak: Garbage collecting active transaction");
45
+ // transaction_abort(transaction);
46
+ // mdb_txn_abort(transaction->txn);
43
47
  }
44
48
  free(transaction);
45
49
  }
46
50
 
47
51
  static void transaction_mark(Transaction* transaction) {
48
52
  rb_gc_mark(transaction->parent);
53
+ rb_gc_mark(transaction->child);
49
54
  rb_gc_mark(transaction->env);
50
55
  rb_gc_mark(transaction->cursors);
51
56
  }
@@ -108,7 +113,7 @@ static VALUE transaction_abort(VALUE self) {
108
113
  }
109
114
 
110
115
  /**
111
- * @overload transaction_env
116
+ * @overload env
112
117
  * @return [Environment] the environment in which this transaction is running.
113
118
  * @example
114
119
  * env.transaction do |t|
@@ -121,49 +126,85 @@ static VALUE transaction_env(VALUE self) {
121
126
  return transaction->env;
122
127
  }
123
128
 
124
- static void transaction_finish(VALUE self, int commit) {
125
- TRANSACTION(self, transaction);
126
-
127
- if (!transaction->txn)
128
- rb_raise(cError, "Transaction is terminated");
129
-
130
- if (transaction->thread != rb_thread_current())
131
- rb_raise(cError, "Wrong thread");
132
-
133
- // Check nesting
134
- VALUE p = environment_active_txn(transaction->env);
135
- while (!NIL_P(p) && p != self) {
136
- TRANSACTION(p, txn);
137
- p = txn->parent;
138
- }
139
- if (p != self)
140
- rb_raise(cError, "Transaction is not active");
141
-
142
- int ret = 0;
143
- if (commit)
144
- ret = mdb_txn_commit(transaction->txn);
145
- else
146
- mdb_txn_abort(transaction->txn);
129
+ /**
130
+ * @overload readonly?
131
+ * @note This predicate is considered *unstable*; do not get used to it.
132
+ * @return [false,true] whether the transaction is read-only.
133
+ */
134
+ static VALUE transaction_is_readonly(VALUE self) {
135
+ TRANSACTION(self, transaction);
136
+ //MDB_txn* txn = transaction->txn;
137
+ return (transaction->flags & MDB_RDONLY) ? Qtrue : Qfalse;
138
+ }
147
139
 
148
- long i;
149
- for (i=0; i<RARRAY_LEN(transaction->cursors); i++) {
150
- VALUE cursor = RARRAY_AREF(transaction->cursors, i);
151
- cursor_close(cursor);
152
- }
153
- rb_ary_clear(transaction->cursors);
154
140
 
155
- // Mark child transactions as closed
156
- p = environment_active_txn(transaction->env);
141
+ static void transaction_finish(VALUE self, int commit) {
142
+ TRANSACTION(self, transaction);
143
+
144
+ if (!transaction->txn)
145
+ rb_raise(cError, "Transaction is already terminated");
146
+
147
+ if (transaction->thread != rb_thread_current())
148
+ rb_raise(cError, "The thread closing the transaction "
149
+ "is not the one that opened it");
150
+
151
+ // ensure the transaction being closed is the active one
152
+ VALUE p = environment_active_txn(transaction->env);
153
+ while (!NIL_P(p) && p != self) {
154
+ TRANSACTION(p, txn);
155
+ p = txn->parent;
156
+ }
157
+ // bail out if the transaction `self` is not the active one
158
+ if (p != self)
159
+ rb_raise(cError, "Transaction is not active");
160
+
161
+ // now eliminate the cursors
162
+ long i;
163
+ for (i=0; i<RARRAY_LEN(transaction->cursors); i++) {
164
+ VALUE cursor = RARRAY_AREF(transaction->cursors, i);
165
+ cursor_close(cursor);
166
+ }
167
+ rb_ary_clear(transaction->cursors);
168
+
169
+ // now actually finish the internal transaction
170
+ int ret = 0;
171
+ if (commit)
172
+ ret = mdb_txn_commit(transaction->txn);
173
+ else
174
+ mdb_txn_abort(transaction->txn);
175
+
176
+ // eliminate child transactions
177
+ if (transaction->child) {
178
+ p = self; // again this is a VALUE
179
+ Transaction* txn = transaction; // and this is the struct
180
+
181
+ // descend into deepest child transaction
182
+ do {
183
+ p = txn->child;
184
+ // this is TRANSACTION minus the declaration
185
+ Data_Get_Struct(txn->child, Transaction, txn);
186
+ } while (txn->child);
187
+
188
+ // now we ascend back up
157
189
  while (p != self) {
158
- TRANSACTION(p, txn);
159
- txn->txn = 0;
160
- p = txn->parent;
190
+ TRANSACTION(p, txn);
191
+ txn->txn = 0;
192
+ p = txn->parent;
161
193
  }
162
- transaction->txn = 0;
194
+ }
195
+ transaction->txn = 0;
163
196
 
164
- environment_set_active_txn(transaction->env, transaction->thread, transaction->parent);
197
+ // no more active read-write transaction; unset the registry
198
+ if (!(transaction->flags & MDB_RDONLY) && !transaction->parent) {
199
+ ENVIRONMENT(transaction->env, env);
200
+ env->rw_txn_thread = NULL;
201
+ }
165
202
 
166
- check(ret);
203
+ // now set the active transaction to the parent, if there is one
204
+ environment_set_active_txn(transaction->env, transaction->thread,
205
+ transaction->parent);
206
+
207
+ check(ret);
167
208
  }
168
209
 
169
210
  // Ruby 1.8.7 compatibility
@@ -204,63 +245,143 @@ static void stop_txn_begin(void *arg)
204
245
  txn_args->stop = 1;
205
246
  }
206
247
 
248
+ /**
249
+ * This is the code that opens transactions. Read-write transactions
250
+ * have to be called outside the GVL because they will block without
251
+ *
252
+ *
253
+ * Here is the basic problem with LMDB transactions:
254
+ *
255
+ * - There can only be one read-write transaction per LMDB
256
+ * ENVIRONMENT, not process, not thread.
257
+ *
258
+ * - Read-write transactions can nest.
259
+ *
260
+ * - There can only be one active transaction per thread.
261
+ *
262
+ * - Every thread can have an active read-ONLY transaction, but only one.
263
+ *
264
+ * - This is because read-only transactions canNOT nest (it is
265
+ * meaningless to have a nested read-only transaction)
266
+ *
267
+ * - Furthermore it is an error to open a read-only transaction under
268
+ * a read-write transaction.
269
+ *
270
+ * - Same goes for opening a read-write transaction in the same thread
271
+ * as an active read-only transaction.
272
+ *
273
+ * Nevertheless, the downstream Ruby user may not be able to
274
+ * completely control calls to env.transaction (e.g. if they are
275
+ * wrapping a transaction around a proc or lambda that happens to
276
+ * contain its own transaction), plus every call in LMDB needs to be
277
+ * implicitly wrapped in a transaction anyway.
278
+ *
279
+ * What this means is that we will, first, have to keep explicit track
280
+ * of the read-write transaction, if one exists. Second, we will have
281
+ * to simulate nesting for read-only transactions, so the behaviour in
282
+ * Ruby is the same as a read-write transaction.
283
+ *
284
+ */
285
+
207
286
  static VALUE with_transaction(VALUE venv, VALUE(*fn)(VALUE), VALUE arg, int flags) {
208
- ENVIRONMENT(venv, environment);
287
+ ENVIRONMENT(venv, environment);
288
+
289
+ MDB_txn* txn;
290
+ TxnArgs txn_args;
291
+
292
+ VALUE thread = rb_thread_current();
293
+ VALUE vparent = environment_active_txn(venv);
294
+
295
+ Transaction* tparent = NULL;
296
+ if (vparent && !NIL_P(vparent))
297
+ Data_Get_Struct(vparent, Transaction, tparent);
209
298
 
210
- MDB_txn* txn;
211
- TxnArgs txn_args;
299
+ // rb_warn("fart lol");
212
300
 
213
- retry:
214
- txn = NULL;
301
+ // XXX note this is a cursed goto loop that could almost certainly
302
+ // be rewritten as a do-while
303
+ retry:
304
+ txn = NULL;
215
305
 
216
- txn_args.env = environment->env;
217
- txn_args.parent = active_txn(venv);
218
- txn_args.flags = flags;
219
- txn_args.htxn = &txn;
220
- txn_args.result = 0;
221
- txn_args.stop = 0;
306
+ txn_args.env = environment->env;
307
+ txn_args.parent = active_txn(venv);
308
+ txn_args.flags = flags;
309
+ txn_args.htxn = &txn;
310
+ txn_args.result = 0;
311
+ txn_args.stop = 0;
222
312
 
223
- if (flags & MDB_RDONLY) {
224
- call_txn_begin(&txn_args);
313
+ if (flags & MDB_RDONLY) {
314
+ if (tparent && tparent->flags & MDB_RDONLY)
315
+ // this is a no-op: put the same actual transaction in a
316
+ // different wrapper struct
317
+ txn = txn_args.parent;
318
+ else
319
+ // this will return an error if the parent transaction is
320
+ // read-write, so we don't need to handle the case explicitly
321
+ call_txn_begin(&txn_args);
322
+ }
323
+ else {
324
+ if (tparent) {
325
+ // first we have to determine if we're on the same thread
326
+ // as the parent, which in turn must be the same as the
327
+ // environment's registry for which thread has the
328
+ // read-write transaction
329
+ if (thread != tparent->thread ||
330
+ thread != environment->rw_txn_thread)
331
+ rb_raise(cError,
332
+ "Attempt to nest transaction on a different thread");
225
333
  }
226
- else {
227
- CALL_WITHOUT_GVL(
228
- call_txn_begin, &txn_args,
229
- stop_txn_begin, &txn_args);
230
-
231
- if (txn_args.stop || !txn) {
232
- // !txn is when rb_thread_call_without_gvl2
233
- // returns before calling txn_begin
234
- if (txn) {
235
- mdb_txn_abort(txn);
236
- }
237
- rb_thread_check_ints();
238
- goto retry; // in what cases do we get here?
239
- }
334
+
335
+ CALL_WITHOUT_GVL(call_txn_begin, &txn_args, stop_txn_begin, &txn_args);
336
+
337
+ if (txn_args.stop || !txn) {
338
+ // !txn is when rb_thread_call_without_gvl2
339
+ // returns before calling txn_begin
340
+ if (txn) {
341
+ mdb_txn_abort(txn);
342
+ txn_args.result = 0;
343
+ }
344
+
345
+ //rb_warn("got here lol");
346
+ rb_thread_check_ints();
347
+ goto retry; // in what cases do we get here?
240
348
  }
241
349
 
242
- check(txn_args.result);
350
+ // set the thread
351
+ environment->rw_txn_thread = thread;
352
+ }
243
353
 
244
- Transaction* transaction;
245
- VALUE vtxn = Data_Make_Struct(cTransaction, Transaction, transaction_mark, transaction_free, transaction);
246
- transaction->parent = environment_active_txn(venv);
247
- transaction->env = venv;
248
- transaction->txn = txn;
249
- transaction->thread = rb_thread_current();
250
- transaction->cursors = rb_ary_new();
251
- environment_set_active_txn(venv, transaction->thread, vtxn);
354
+ // this will raise unless result is zero
355
+ check(txn_args.result);
252
356
 
253
- int exception;
254
- VALUE ret = rb_protect(fn, NIL_P(arg) ? vtxn : arg, &exception);
357
+ Transaction* transaction;
358
+ VALUE vtxn = Data_Make_Struct(cTransaction, Transaction, transaction_mark,
359
+ transaction_free, transaction);
360
+ transaction->parent = vparent;
361
+ transaction->env = venv;
362
+ transaction->txn = txn;
363
+ transaction->flags = flags;
364
+ transaction->thread = rb_thread_current();
365
+ transaction->cursors = rb_ary_new();
255
366
 
256
- if (exception) {
257
- if (vtxn == environment_active_txn(venv))
258
- transaction_abort(vtxn);
259
- rb_jump_tag(exception);
260
- }
367
+ // set the parent's child to self
368
+ if (tparent) tparent->child = vtxn;
369
+
370
+ environment_set_active_txn(venv, transaction->thread, vtxn);
371
+
372
+ // now we run the function in the transaction
373
+ int exception;
374
+ VALUE ret = rb_protect(fn, NIL_P(arg) ? vtxn : arg, &exception);
375
+
376
+ if (exception) {
377
+ // rb_warn("lol got exception");
261
378
  if (vtxn == environment_active_txn(venv))
262
- transaction_commit(vtxn);
263
- return ret;
379
+ transaction_abort(vtxn);
380
+ rb_jump_tag(exception);
381
+ }
382
+ if (vtxn == environment_active_txn(venv))
383
+ transaction_commit(vtxn);
384
+ return ret;
264
385
  }
265
386
 
266
387
  static void environment_check(Environment* environment) {
@@ -319,6 +440,16 @@ static VALUE stat2hash(const MDB_stat* stat) {
319
440
  return ret;
320
441
  }
321
442
 
443
+ static VALUE flags2hash(int flags) {
444
+ VALUE ret = rb_hash_new();
445
+
446
+ #define FLAG(const, name) rb_hash_aset(ret, ID2SYM(rb_intern(#name)), (flags & MDB_##const) == 0 ? Qfalse : Qtrue);
447
+ #include "dbi_flags.h"
448
+ #undef FLAG
449
+
450
+ return ret;
451
+ }
452
+
322
453
  /**
323
454
  * @overload stat
324
455
  * Return useful statistics about an environment.
@@ -356,7 +487,7 @@ static VALUE environment_info(VALUE self) {
356
487
 
357
488
  VALUE ret = rb_hash_new();
358
489
 
359
- #define INFO_SET(name) rb_hash_aset(ret, ID2SYM(rb_intern(#name)), INT2NUM((size_t)info.me_##name));
490
+ #define INFO_SET(name) rb_hash_aset(ret, ID2SYM(rb_intern(#name)), SIZET2NUM((size_t)info.me_##name));
360
491
  INFO_SET(mapaddr);
361
492
  INFO_SET(mapsize);
362
493
  INFO_SET(last_pgno);
@@ -470,7 +601,13 @@ static int environment_options(VALUE key, VALUE value, EnvironmentOptions* optio
470
601
  */
471
602
  static VALUE environment_new(int argc, VALUE *argv, VALUE klass) {
472
603
  VALUE path, option_hash;
604
+
605
+ #ifdef RB_SCAN_ARGS_KEYWORDS
606
+ rb_scan_args_kw(RB_SCAN_ARGS_LAST_HASH_KEYWORDS,
607
+ argc, argv, "1:", &path, &option_hash);
608
+ #else
473
609
  rb_scan_args(argc, argv, "1:", &path, &option_hash);
610
+ #endif
474
611
 
475
612
  EnvironmentOptions options = {
476
613
  .flags = MDB_NOTLS,
@@ -641,7 +778,7 @@ static MDB_txn* active_txn(VALUE self) {
641
778
  return 0;
642
779
  TRANSACTION(vtxn, transaction);
643
780
  if (!transaction->txn)
644
- rb_raise(cError, "Transaction is terminated");
781
+ rb_raise(cError, "Transaction is already terminated");
645
782
  if (transaction->thread != rb_thread_current())
646
783
  rb_raise(cError, "Wrong thread");
647
784
  return transaction->txn;
@@ -745,7 +882,13 @@ static VALUE environment_database(int argc, VALUE *argv, VALUE self) {
745
882
  return call_with_transaction(self, self, "database", argc, argv, 0);
746
883
 
747
884
  VALUE name, option_hash;
885
+ #ifdef RB_SCAN_ARGS_KEYWORDS
886
+ rb_scan_args_kw(RB_SCAN_ARGS_KEYWORDS,
887
+ argc, argv, "01:", &name, &option_hash);
888
+ #else
748
889
  rb_scan_args(argc, argv, "01:", &name, &option_hash);
890
+ #endif
891
+
749
892
 
750
893
  int flags = 0;
751
894
  if (!NIL_P(option_hash))
@@ -776,13 +919,63 @@ static VALUE environment_database(int argc, VALUE *argv, VALUE self) {
776
919
  static VALUE database_stat(VALUE self) {
777
920
  DATABASE(self, database);
778
921
  if (!active_txn(database->env))
779
- return call_with_transaction(database->env, self, "stat", 0, 0, MDB_RDONLY);
922
+ return call_with_transaction(database->env,
923
+ self, "stat", 0, 0, MDB_RDONLY);
780
924
 
781
925
  MDB_stat stat;
782
926
  check(mdb_stat(need_txn(database->env), database->dbi, &stat));
783
927
  return stat2hash(&stat);
784
928
  }
785
929
 
930
+ /**
931
+ * @overload flags
932
+ * Return the flags used to open the database.
933
+ * @return [Hash] The flags.
934
+ */
935
+ static VALUE database_get_flags(VALUE self) {
936
+ DATABASE(self, database);
937
+ if (!active_txn(database->env))
938
+ return call_with_transaction(database->env,
939
+ self, "flags", 0, 0, MDB_RDONLY);
940
+ unsigned int flags;
941
+ check(mdb_dbi_flags(need_txn(database->env), database->dbi, &flags));
942
+ return flags2hash(flags);
943
+ }
944
+
945
+ /* XXX these two could probably also be macro'd, or maybe not i dunno */
946
+
947
+ /**
948
+ * @overload dupsort?
949
+ * Returns whether the database is in +:dupsort+ mode.
950
+ * @return [true, false]
951
+ */
952
+ static VALUE database_is_dupsort(VALUE self) {
953
+ DATABASE(self, database);
954
+ if (!active_txn(database->env))
955
+ return call_with_transaction(database->env, self,
956
+ "dupsort?", 0, 0, MDB_RDONLY);
957
+ unsigned int flags;
958
+ check(mdb_dbi_flags(need_txn(database->env), database->dbi, &flags));
959
+
960
+ return (flags & MDB_DUPSORT) == 0 ? Qfalse : Qtrue;
961
+ }
962
+
963
+ /**
964
+ * @overload dupfixed?
965
+ * Returns whether the database is in +:dupfixed+ mode.
966
+ * @return [true, false]
967
+ */
968
+ static VALUE database_is_dupfixed(VALUE self) {
969
+ DATABASE(self, database);
970
+ if (!active_txn(database->env))
971
+ return call_with_transaction(database->env, self,
972
+ "dupfixed?", 0, 0, MDB_RDONLY);
973
+ unsigned int flags;
974
+ check(mdb_dbi_flags(need_txn(database->env), database->dbi, &flags));
975
+
976
+ return (flags & MDB_DUPFIXED) == 0 ? Qfalse : Qtrue;
977
+ }
978
+
786
979
  /**
787
980
  * @overload drop
788
981
  * Remove a database from the environment.
@@ -875,8 +1068,13 @@ static VALUE database_put(int argc, VALUE *argv, VALUE self) {
875
1068
  if (!active_txn(database->env))
876
1069
  return call_with_transaction(database->env, self, "put", argc, argv, 0);
877
1070
 
878
- VALUE vkey, vval, option_hash;
879
- rb_scan_args(argc, argv, "2:", &vkey, &vval, &option_hash);
1071
+ VALUE vkey, vval, option_hash = Qnil;
1072
+ #ifdef RB_SCAN_ARGS_KEYWORDS
1073
+ rb_scan_args_kw(RB_SCAN_ARGS_LAST_HASH_KEYWORDS,
1074
+ argc, argv, "20:", &vkey, &vval, &option_hash);
1075
+ #else
1076
+ rb_scan_args(argc, argv, "20:", &vkey, &vval, &option_hash);
1077
+ #endif
880
1078
 
881
1079
  int flags = 0;
882
1080
  if (!NIL_P(option_hash))
@@ -927,7 +1125,7 @@ static VALUE database_delete(int argc, VALUE *argv, VALUE self) {
927
1125
  if (NIL_P(vval)) {
928
1126
  check(mdb_del(need_txn(database->env), database->dbi, &key, 0));
929
1127
  } else {
930
- VALUE vval = StringValue(vval);
1128
+ vval = StringValue(vval);
931
1129
  MDB_val value;
932
1130
  value.mv_size = RSTRING_LEN(vval);
933
1131
  value.mv_data = RSTRING_PTR(vval);
@@ -1025,7 +1223,7 @@ static VALUE database_cursor(VALUE self) {
1025
1223
  }
1026
1224
 
1027
1225
  /**
1028
- * @overload database_env
1226
+ * @overload env
1029
1227
  * @return [Environment] the environment to which this database belongs.
1030
1228
  */
1031
1229
  static VALUE database_env(VALUE self) {
@@ -1082,21 +1280,30 @@ static VALUE cursor_prev(VALUE self) {
1082
1280
  }
1083
1281
 
1084
1282
  /**
1085
- * @overload next
1283
+ * @overload next nodup = nil
1086
1284
  * Position the cursor to the next record in the database, and
1087
1285
  * return its value.
1286
+ * @param nodup [true, false] If true, skip over duplicate records.
1088
1287
  * @return [Array,nil] The [key, value] pair for the next record, or
1089
1288
  * nil if no next record.
1090
1289
  */
1091
- static VALUE cursor_next(VALUE self) {
1290
+ static VALUE cursor_next(int argc, VALUE* argv, VALUE self) {
1092
1291
  CURSOR(self, cursor);
1292
+ VALUE nodup;
1093
1293
  MDB_val key, value;
1294
+ MDB_cursor_op op = MDB_NEXT;
1094
1295
 
1095
- int ret = mdb_cursor_get(cursor->cur, &key, &value, MDB_NEXT);
1296
+ rb_scan_args(argc, argv, "01", &nodup);
1297
+
1298
+ if (RTEST(nodup))
1299
+ op = MDB_NEXT_NODUP;
1300
+
1301
+ int ret = mdb_cursor_get(cursor->cur, &key, &value, op);
1096
1302
  if (ret == MDB_NOTFOUND)
1097
1303
  return Qnil;
1098
1304
  check(ret);
1099
- return rb_assoc_new(rb_str_new(key.mv_data, key.mv_size), rb_str_new(value.mv_data, value.mv_size));
1305
+ return rb_assoc_new(rb_str_new(key.mv_data, key.mv_size),
1306
+ rb_str_new(value.mv_data, value.mv_size));
1100
1307
  }
1101
1308
 
1102
1309
  /**
@@ -1104,6 +1311,7 @@ static VALUE cursor_next(VALUE self) {
1104
1311
  * Position the cursor to the next record in the database, and
1105
1312
  * return its value if the record's key is less than or equal to
1106
1313
  * the specified key, or nil otherwise.
1314
+ * @param key [#to_s] The key to serve as the upper bound
1107
1315
  * @return [Array,nil] The [key, value] pair for the next record, or
1108
1316
  * nil if no next record or the next record is out of the range.
1109
1317
  */
@@ -1119,7 +1327,7 @@ static VALUE cursor_next_range(VALUE self, VALUE upper_bound_key) {
1119
1327
  ub_key.mv_size = RSTRING_LEN(upper_bound_key);
1120
1328
  ub_key.mv_data = StringValuePtr(upper_bound_key);
1121
1329
 
1122
- MDB_txn *txn = mdb_cursor_txn(cursor->cur);
1330
+ MDB_txn* txn = mdb_cursor_txn(cursor->cur);
1123
1331
  MDB_dbi dbi = mdb_cursor_dbi(cursor->cur);
1124
1332
 
1125
1333
  if (mdb_cmp(txn, dbi, &key, &ub_key) <= 0) {
@@ -1130,20 +1338,40 @@ static VALUE cursor_next_range(VALUE self, VALUE upper_bound_key) {
1130
1338
  }
1131
1339
 
1132
1340
  /**
1133
- * @overload set(key)
1134
- * Set the cursor to a specified key
1135
- * @param key The key to which the cursor should be positioned
1136
- * @return [Array] The [key, value] pair to which the cursor now points.
1341
+ * @overload set(key, value = nil)
1342
+ * Set the cursor to a specified key, optionally at the specified
1343
+ * value if the database was opened with +:dupsort+.
1344
+ * @param key [#to_s] The key to which the cursor should be positioned
1345
+ * @param value [nil, #to_s] The optional value (+:dupsort+ only)
1346
+ * @return [Array] The +[key, value]+ pair to which the cursor now points.
1137
1347
  */
1138
- static VALUE cursor_set(VALUE self, VALUE vkey) {
1139
- CURSOR(self, cursor);
1140
- MDB_val key, value;
1348
+ static VALUE cursor_set(int argc, VALUE* argv, VALUE self) {
1349
+ CURSOR(self, cursor);
1350
+ VALUE vkey, vval;
1351
+ MDB_val key, value;
1352
+ MDB_cursor_op op = MDB_SET_KEY;
1353
+ int ret;
1141
1354
 
1142
- key.mv_size = RSTRING_LEN(vkey);
1143
- key.mv_data = StringValuePtr(vkey);
1355
+ rb_scan_args(argc, argv, "11", &vkey, &vval);
1144
1356
 
1145
- check(mdb_cursor_get(cursor->cur, &key, &value, MDB_SET_KEY));
1146
- return rb_assoc_new(rb_str_new(key.mv_data, key.mv_size), rb_str_new(value.mv_data, value.mv_size));
1357
+ key.mv_size = RSTRING_LEN(vkey);
1358
+ key.mv_data = StringValuePtr(vkey);
1359
+
1360
+ if (!NIL_P(vval)) {
1361
+ op = MDB_GET_BOTH;
1362
+ value.mv_size = RSTRING_LEN(vval);
1363
+ value.mv_data = StringValuePtr(vval);
1364
+ }
1365
+
1366
+ ret = mdb_cursor_get(cursor->cur, &key, &value, op);
1367
+
1368
+ if (!NIL_P(vval) && ret == MDB_NOTFOUND)
1369
+ return Qnil;
1370
+
1371
+ check(ret);
1372
+
1373
+ return rb_assoc_new(rb_str_new(key.mv_data, key.mv_size),
1374
+ rb_str_new(value.mv_data, value.mv_size));
1147
1375
  }
1148
1376
 
1149
1377
  /**
@@ -1155,12 +1383,20 @@ static VALUE cursor_set(VALUE self, VALUE vkey) {
1155
1383
  static VALUE cursor_set_range(VALUE self, VALUE vkey) {
1156
1384
  CURSOR(self, cursor);
1157
1385
  MDB_val key, value;
1386
+ int ret;
1158
1387
 
1159
1388
  key.mv_size = RSTRING_LEN(vkey);
1160
1389
  key.mv_data = StringValuePtr(vkey);
1161
1390
 
1162
- check(mdb_cursor_get(cursor->cur, &key, &value, MDB_SET_RANGE));
1163
- return rb_assoc_new(rb_str_new(key.mv_data, key.mv_size), rb_str_new(value.mv_data, value.mv_size));
1391
+ ret = mdb_cursor_get(cursor->cur, &key, &value, MDB_SET_RANGE);
1392
+
1393
+ /* not sure why we were letting this throw an exception */
1394
+ if (ret == MDB_NOTFOUND) return Qnil;
1395
+
1396
+ check(ret);
1397
+
1398
+ return rb_assoc_new(rb_str_new(key.mv_data, key.mv_size),
1399
+ rb_str_new(value.mv_data, value.mv_size));
1164
1400
  }
1165
1401
 
1166
1402
  /**
@@ -1221,7 +1457,12 @@ static VALUE cursor_put(int argc, VALUE* argv, VALUE self) {
1221
1457
  CURSOR(self, cursor);
1222
1458
 
1223
1459
  VALUE vkey, vval, option_hash;
1460
+ #ifdef RB_SCAN_ARGS_KEYWORDS
1461
+ rb_scan_args_kw(RB_SCAN_ARGS_LAST_HASH_KEYWORDS,
1462
+ argc, argv, "2:", &vkey, &vval, &option_hash);
1463
+ #else
1224
1464
  rb_scan_args(argc, argv, "2:", &vkey, &vval, &option_hash);
1465
+ #endif
1225
1466
 
1226
1467
  int flags = 0;
1227
1468
  if (!NIL_P(option_hash))
@@ -1258,7 +1499,12 @@ static VALUE cursor_delete(int argc, VALUE *argv, VALUE self) {
1258
1499
  CURSOR(self, cursor);
1259
1500
 
1260
1501
  VALUE option_hash;
1502
+ #ifdef RB_SCAN_ARGS_KEYWORDS
1503
+ rb_scan_args_kw(RB_SCAN_ARGS_LAST_HASH_KEYWORDS,
1504
+ argc, argv, ":", &option_hash);
1505
+ #else
1261
1506
  rb_scan_args(argc, argv, ":", &option_hash);
1507
+ #endif
1262
1508
 
1263
1509
  int flags = 0;
1264
1510
  if (!NIL_P(option_hash))
@@ -1388,6 +1634,9 @@ void Init_lmdb_ext() {
1388
1634
  cDatabase = rb_define_class_under(mLMDB, "Database", rb_cObject);
1389
1635
  rb_undef_method(rb_singleton_class(cDatabase), "new");
1390
1636
  rb_define_method(cDatabase, "stat", database_stat, 0);
1637
+ rb_define_method(cDatabase, "flags", database_get_flags, 0);
1638
+ rb_define_method(cDatabase, "dupsort?", database_is_dupsort, 0);
1639
+ rb_define_method(cDatabase, "dupfixed?", database_is_dupfixed, 0);
1391
1640
  rb_define_method(cDatabase, "drop", database_drop, 0);
1392
1641
  rb_define_method(cDatabase, "clear", database_clear, 0);
1393
1642
  rb_define_method(cDatabase, "get", database_get, 1);
@@ -1458,6 +1707,7 @@ void Init_lmdb_ext() {
1458
1707
  rb_define_method(cTransaction, "commit", transaction_commit, 0);
1459
1708
  rb_define_method(cTransaction, "abort", transaction_abort, 0);
1460
1709
  rb_define_method(cTransaction, "env", transaction_env, 0);
1710
+ rb_define_method(cTransaction, "readonly?", transaction_is_readonly, 0);
1461
1711
 
1462
1712
  /**
1463
1713
  * Document-class: LMDB::Cursor
@@ -1491,10 +1741,10 @@ void Init_lmdb_ext() {
1491
1741
  rb_define_method(cCursor, "get", cursor_get, 0);
1492
1742
  rb_define_method(cCursor, "first", cursor_first, 0);
1493
1743
  rb_define_method(cCursor, "last", cursor_last, 0);
1494
- rb_define_method(cCursor, "next", cursor_next, 0);
1744
+ rb_define_method(cCursor, "next", cursor_next, -1);
1495
1745
  rb_define_method(cCursor, "next_range", cursor_next_range, 1);
1496
1746
  rb_define_method(cCursor, "prev", cursor_prev, 0);
1497
- rb_define_method(cCursor, "set", cursor_set, 1);
1747
+ rb_define_method(cCursor, "set", cursor_set, -1);
1498
1748
  rb_define_method(cCursor, "set_range", cursor_set_range, 1);
1499
1749
  rb_define_method(cCursor, "put", cursor_put, -1);
1500
1750
  rb_define_method(cCursor, "count", cursor_count, 0);