File Coverage

blib/lib/LMDB_File.pm
Criterion Covered Total %
statement 244 292 83.5
branch 82 156 52.5
condition 51 92 55.4
subroutine 53 63 84.1
pod 15 17 88.2
total 445 620 71.7


line stmt bran cond sub pod time code
1             package LMDB_File;
2              
3 5     5   163720 use 5.010000;
  5         11  
4 5     5   17 use strict;
  5         4  
  5         80  
5 5     5   14 use warnings;
  5         8  
  5         113  
6 5     5   15 use Carp;
  5         4  
  5         264  
7              
8             require Exporter;
9 5     5   2319 use AutoLoader;
  5         5079  
  5         19  
10              
11             our $VERSION = '0.10';
12             our $DEBUG = 0;
13              
14             our @ISA = qw(Exporter);
15             our @CARP_NOT = qw(LMDB::Env LMDB::Txn LMDB::Cursor LMDB_File);
16              
17             our @EXPORT = qw();
18             our %EXPORT_TAGS = (
19             envflags => [qw(MDB_FIXEDMAP MDB_NOSUBDIR MDB_NOSYNC MDB_RDONLY MDB_NOMETASYNC
20             MDB_NOMEMINIT MDB_WRITEMAP MDB_MAPASYNC MDB_NOTLS MDB_NOLOCK MDB_NORDAHEAD)],
21             dbflags => [qw(MDB_REVERSEKEY MDB_DUPSORT MDB_INTEGERKEY MDB_DUPFIXED
22             MDB_INTEGERDUP MDB_REVERSEDUP MDB_CREATE)],
23             writeflags => [qw(MDB_NOOVERWRITE MDB_NODUPDATA MDB_CURRENT MDB_RESERVE
24             MDB_APPEND MDB_APPENDDUP MDB_MULTIPLE)],
25             cursor_op => [qw(MDB_FIRST MDB_FIRST_DUP MDB_GET_BOTH MDB_GET_BOTH_RANGE
26             MDB_GET_CURRENT MDB_GET_MULTIPLE MDB_NEXT MDB_NEXT_DUP MDB_NEXT_MULTIPLE
27             MDB_NEXT_NODUP MDB_PREV MDB_PREV_DUP MDB_PREV_NODUP MDB_LAST MDB_LAST_DUP
28             MDB_SET MDB_SET_KEY MDB_SET_RANGE)],
29             copyflags => [qw(MDB_CP_COMPACT)],
30             error => [qw(MDB_SUCCESS MDB_KEYEXIST MDB_NOTFOUND MDB_PAGE_NOTFOUND MDB_CORRUPTED
31             MDB_PANIC MDB_VERSION_MISMATCH MDB_INVALID MDB_MAP_FULL MDB_DBS_FULL
32             MDB_READERS_FULL MDB_TLS_FULL MDB_TXN_FULL MDB_CURSOR_FULL MDB_PAGE_FULL
33             MDB_MAP_RESIZED MDB_INCOMPATIBLE MDB_BAD_RSLOT MDB_BAD_TXN MDB_BAD_VALSIZE
34             MDB_BAD_DBI MDB_LAST_ERRCODE)],
35             version => [qw(MDB_VERSION_FULL MDB_VERSION_MAJOR MDB_VERSION_MINOR
36             MDB_VERSION_PATCH MDB_VERSION_STRING MDB_VERSION_DATE)],
37             );
38             $EXPORT_TAGS{flags} = [
39             @{$EXPORT_TAGS{envflags}}, @{$EXPORT_TAGS{dbflags}},
40             @{$EXPORT_TAGS{writeflags}}, @{$EXPORT_TAGS{copyflags}}
41             ];
42             {
43             my %seen;
44             push @{$EXPORT_TAGS{all}},
45             grep {!$seen{$_}++} @{$EXPORT_TAGS{$_}} foreach keys %EXPORT_TAGS;
46             }
47             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
48              
49             sub AUTOLOAD {
50 115     115   3832 my $constname;
51 115         74 our $AUTOLOAD;
52 115         362 ($constname = $AUTOLOAD) =~ s/.*:://;
53 115 50       199 croak "&LMDB_File::constant not defined" if $constname eq 'constant';
54 115         327 my ($error, $val) = constant($constname);
55 115 50       144 if ($error) { croak $error; }
  0         0  
56             {
57 5     5   1293 no strict 'refs';
  5         8  
  5         363  
  115         77  
58 115     2000343   448 *$AUTOLOAD = sub { $val };
  2000343         2830833  
59             }
60 115         196 goto &$AUTOLOAD;
61             }
62              
63             require XSLoader;
64             XSLoader::load('LMDB_File', $VERSION);
65              
66             my $dbflmask = do {
67 5     5   18 no strict 'refs';
  5         5  
  5         284  
68             my $f = 0;
69             $f |= &{"LMDB_File::$_"}() for @{$EXPORT_TAGS{dbflags}};
70             $f;
71             };
72              
73             package LMDB::Env;
74 5     5   17 use Scalar::Util ();
  5         8  
  5         68  
75 5     5   13 use Fcntl;
  5         7  
  5         14489  
76              
77             our %Envs;
78             sub new {
79 8     8   8166 my ($proto, $path, $eflags) = @_;
80 8         73 create(my $self);
81 8 50       42 return unless $self;
82 8 100 50     37 $eflags = { flags => ($eflags || 0) } unless ref $eflags;
83             $eflags->{mapsize} and $self->set_mapsize($eflags->{mapsize})
84 8 50 66     46 and return;
85             $eflags->{maxdbs} and $self->set_maxdbs($eflags->{maxdbs})
86 8 50 66     55 and return;
87             $eflags->{maxreaders} and $self->set_maxreaders($eflags->{maxreaders})
88 8 50 33     23 and return;
89 8 50       26 if($^O =~ /openbsd/) {
90             # OpenBSD lacks an unified buffer cache (UBC) so LMDB only works
91             # with MDB_WRITEMAP set when not in read-only mode
92             $eflags->{flags} |= LMDB_File::MDB_WRITEMAP()
93 0 0       0 unless $eflags->{flags} & LMDB_File::MDB_RDONLY();
94             }
95 8 50 50     1359 $self->open($path, $eflags->{flags}, $eflags->{mode} || 0600)
96             and return;
97 6 50       1172 warn "Created LMDB::Env $$self\n" if $DEBUG;
98 6         24 return $self;
99             }
100              
101             sub Clean {
102 0     0   0 my $self = shift;
103 0 0       0 my $txl = $Envs{ $$self }[0] or return;
104 0 0       0 if(@$txl) {
105 0         0 Carp::carp("LMDB: Aborting $#$txl transactions in $$self.");
106 0         0 $txl->[$#$txl]->abort;
107             }
108 0         0 $Envs{ $$self }[0] = [];
109             }
110              
111             sub DESTROY {
112 8     8   47 my $self = shift;
113 8   66     38 my $txl = $Envs{ $$self } && $Envs{ $$self }[0];
114 8 50 66     44 if($txl and @$txl and my $topTxn = $txl->[$#$txl]) {
      33        
115             # As every Txn references its Env this is only possible at global destruction
116 0         0 Carp::carp("LMDB: OOPS! Destroying an active environment!");
117 0 0       0 warn("LMDB: Aborting $#$txl transactions in $$self.") if $DEBUG;
118 0         0 $topTxn->abort;
119             }
120 8         6643 $self->close;
121 8 50       206 warn "Closed LMDB::Env $$self (remains @{[scalar keys %Envs]})\n"
  0         0  
122             if $DEBUG;
123             }
124              
125             sub BeginTxn {
126 20     20   9515 my $self = shift;
127 20         73 $self->get_flags(my $eflags);
128 20   66     71 my $tflags = shift || ($eflags & LMDB_File::MDB_RDONLY());
129 20         40 my $txl = $Envs{ $$self }[0];
130 20 50       40 warn "BeginTxn $$self($$), deep: ", scalar(@$txl), "\n" if $DEBUG;
131 20 100       37 return $txl->[0]->SubTxn($tflags) if @$txl;
132 19         57 LMDB::Txn->new($self, $tflags);
133             }
134              
135             sub CLONE {
136             # After a thread is created all Txns of parent thread are forgot
137 0     0   0 $_->[0] = [] for values %Envs;
138 0         0 _clone();
139 0         0 1;
140             }
141              
142             package LMDB::Txn;
143              
144             our %Txns;
145             my %Cursors;
146              
147             sub new {
148 20     20   33 my ($parent, $env, $tflags) = @_;
149 20         29 my $txl = $Envs{ $$env }[0];
150 20 50 66     85 Carp::croak("Transaction active, should be subtransaction")
151             if !ref($parent) && @$txl;
152 20   66     186 _begin($env, ref($parent) && $parent, $tflags, my $self);
153 20 50       53 return unless $self;
154 20         32 $Txns{$$self} = {
155             Active => 1,
156             Env => $env, # A transaction references the environment
157             RO => $tflags & LMDB_File::MDB_RDONLY(),
158             };
159 20         39 unshift @$txl, $self;
160 20         55 Scalar::Util::weaken($txl->[0]);
161 20 50       46 warn "Created LMDB::Txn $$self in $$env\n" if $DEBUG;
162 20         67 return $self;
163             }
164              
165             sub SubTxn {
166 1     1   1 my $self = shift;
167 1 50       4 if($^O =~ /openbsd/) {
168             # Needs MDB_WRITEMAP so
169 0         0 Carp::croak("Subtransactions are unsupported in this OS");
170             }
171 1   50     6 my $tflags = shift || 0;
172 1         3 return $self->new($self->env, $tflags);
173             }
174              
175             sub DESTROY {
176 20     20   1335 my $self = shift;
177 20 100       102 my $td = $Txns{ $$self } or return;
178 15 100 66     103 if($td->{Active} && !$td->{RO} && $td->{AC}) {
      66        
179 3 100       18 warn "LMDB: Destroying an active transaction, commiting $$self...\n"
180             if $DEBUG;
181 3         11 $self->commit;
182             } else {
183 12 50       21 warn "LMDB: Destroying an active transaction, aborting $$self...\n"
184             if $DEBUG;
185 12         31 $self->abort;
186             }
187             }
188              
189             sub _prune {
190 20     20   28 my $self = shift;
191 20         28 my $eid = shift;
192 20 50 33     129 if(my $txl = $Envs{ $eid } && $Envs{ $eid }[0]) {
193 20         53 while(my $rel = shift @$txl) {
194 20         57 my $td = delete $Txns{ $$rel };
195 20         26 delete $Cursors{$_} for keys %{ $td->{Cursors} };
  20         98  
196 20 50       63 last if $$rel == $$self;
197             }
198 20 50       47 warn "LMDB::Txn: Txns list deep: @{[scalar @$txl]}\n" if $DEBUG > 2;
  0         0  
199             }
200 20 50       40 warn "LMDB::Txn: $$self($$) finalized in $eid\n" if $DEBUG > 1;
201 20         115 $$self = 0;
202             }
203              
204             sub abort {
205 13     13   33 my $self = shift;
206 13 100       28 unless($Txns{ $$self }) {
207 1         10 Carp::carp("Terminated transaction");
208 1         256 return;
209             }
210 12         33 my $eid = $self->_env;
211 12         49 $self->_abort;
212 12 50       25 warn "LMDB::Txn $$self aborted\n" if $DEBUG;
213 12         24 $self->_prune($eid);
214             }
215              
216             sub commit {
217 9     9   1189 my $self = shift;
218 9 100       48 my $td = $Txns{ $$self } or Carp::croak("Terminated transaction");
219 8 50       20 Carp::croak("Not an active transaction") unless $td->{Active};
220 8         23 my $eid = $self->_env;
221 8         14220702 $self->_commit;
222 8 100       58 warn "LMDB::Txn $$self commited\n" if $DEBUG;
223 8         64 $self->_prune($eid);
224             }
225              
226             sub Flush {
227 0     0   0 my $self = shift;
228 0 0       0 my $td = $Txns{ $$self } or Carp::croak("Terminated transaction");
229 0 0       0 Carp::croak("Not an active transaction") unless $td->{Active};
230 0         0 $self->_commit;
231             # This depends on malloc order, beware!
232 0         0 _begin($td->{Env}, undef, $td->{RO}, my $ntxn);
233 0 0       0 Carp::croak("Can't recreate Txn") unless $$ntxn == $$self;
234 0         0 $$ntxn = 0;
235             }
236              
237             sub reset {
238 0     0   0 my $self = shift;
239 0 0       0 my $td = $Txns{ $$self } or Carp::croak("Terminated transaction");
240 0 0       0 Carp::croak("Not a read-only transaction") unless $td->{RO};
241 0 0       0 $self->_reset if $td->{Active};
242 0         0 $td->{Active} = 0;
243             }
244              
245             sub renew {
246 0     0   0 my $self = shift;
247 0 0       0 my $td = $Txns{ $$self } or Carp::croak("Terminated transaction");
248 0 0       0 $self->_reset if $td->{Active};
249 0         0 $self->_renew;
250 0         0 $td->{Active} = 1;
251             }
252              
253             sub OpenDB {
254 18     18   116 my ($self, $name, $flags) = @_;
255 18 100       62 my $options = ref($name) eq 'HASH' ? $name : { dbname => $name, flags => $flags };
256 18         77 LMDB_File->open($self, $options->{dbname}, $options->{flags});
257             }
258              
259             sub env {
260 3     3   258 my $self = shift;
261 3 50       25 $Txns{$$self} && $Txns{$$self}{Env};
262             }
263              
264             sub AutoCommit {
265 3     3   7 my $self = shift;
266 3 50       10 my $td = $Txns{ $$self } or Carp::croak("Terminated transaction");
267 3         6 my $prev = $td->{AC};
268 3 50       8 $td->{AC} = shift if(@_);
269 3         5 $prev;
270             }
271              
272             # Fast low-level dbi API
273             sub open {
274 22     22   28 my($txn, $name, $flags) = @_;
275 22   100     65 $flags ||= 0;
276 22 100       54 Carp::croak("Not an alive transaction") unless $Txns{ $$txn };
277             Carp::croak("Not the current child transaction")
278 21 50       16 unless(${$Envs{ $txn->_env }[0][0]} == $$txn);
  21         92  
279 21         177 _dbi_open($txn, $name, $flags & $dbflmask, my $dbi);
280 17 50 33     59 warn "Opened dbi $dbi\n" if $dbi && $DEBUG;
281 17         33 return $dbi;
282             }
283             *get = \&LMDB_File::_get;
284             *put = \&LMDB_File::_put;
285             *del = \&LMDB_File::_del;
286              
287             sub CLONE_SKIP {
288             # All LMDB Transactions are usable only in the thread that create it
289 0     0   0 1;
290             }
291              
292             package LMDB::Cursor;
293              
294             sub get {
295 96     96   56 LMDB_File::_chkalive($Cursors{${$_[0]}});
  96         114  
296 96         299 goto &_get;
297             }
298              
299             sub put {
300 0     0   0 LMDB_File::_chkalive($Cursors{${$_[0]}});
  0         0  
301 0         0 goto &_put;
302             }
303              
304             sub del {
305 0     0   0 LMDB_File::_chkalive($Cursors{${$_[0]}});
  0         0  
306 0         0 goto &_del;
307             }
308              
309             sub DESTROY {
310 6     6   912 my $self = shift;
311 6 50       17 return unless $Cursors{$$self};
312 6         18 my $txnId = $self->txn;
313 6         15 $self->close;
314 6         15 delete $Txns{$txnId}{Cursors}{$$self};
315 6         23 delete $Cursors{$$self};
316             }
317              
318             package LMDB_File;
319 0     0   0 sub CLONE_SKIP { 1; }
320              
321             our $die_on_err = 1;
322             our $last_err = 0;
323              
324             sub new {
325 4     4 1 10 my($proto, $txn, $dbi) = @_;
326 4 50       22 Carp::croak("Need a Txn") unless $txn->isa('LMDB::Txn');
327 4   33     36 bless [ $txn, $dbi ], ref($proto) || $proto;
328             }
329              
330             sub open {
331 21     21 1 54 my $proto = shift;
332 21         19 my $class = ref $proto;
333 21 100       33 my $txn = $class ? $proto->[0] : shift;
334 21 50       69 Carp::croak("Need a Txn") unless $txn->isa('LMDB::Txn');
335 21 50       42 my $dbi = $txn->open(@_) or return;
336 16   66     109 bless [ $txn, $dbi ], $class || $proto;
337             }
338              
339             sub DESTROY {
340 20     20   89026 my $self = shift;
341             }
342              
343             sub _chkalive {
344 100282     100282   76762 my $self = shift;
345 100282         69539 my $txn = $self->[0];
346             Carp::croak("Not an active transaction")
347 100282 50 50     361130 unless($txn && ($Txns{ $$txn } || undef $self->[0]) && $Txns{ $$txn }{Active} );
      66        
      33        
348             # A parent transaction and its cursors may not issue any other operations than
349             # mdb_txn_commit and mdb_txn_abort while it has active child transactions.
350             Carp::croak("Not the current child transaction")
351 100281 50       56797 unless(${$Envs{ $txn->_env }[0][0]} == $$txn);
  100281         256263  
352 100281         126764 $txn, $self->[1];
353             }
354              
355             sub Alive {
356 7     7 1 579 my $self = shift;
357 7         13 my $txn = $self->[0];
358 7 50 66     61 $txn && (($Txns{ $$txn } && $self->[1]) || undef $self->[0]);
      100        
359             }
360              
361             sub flags {
362 2     2 1 3 my $self = shift;
363 2         9 _dbi_flags(_chkalive($self), my $flags);
364 2         20 $flags;
365             }
366              
367             sub put {
368 91     91 1 4761 my $self = shift;
369 91 50       133 warn "put: '$_[0]' => '$_[1]'\n" if $DEBUG > 2;
370 91         380 _put(_chkalive($self), @_);
371 90         230 $_[1];
372             }
373              
374             sub get {
375 100034 50   100034 1 302308 warn "get: '$_[1]'\n" if $DEBUG > 2;
376 100034         104220 my($txn, $dbi) = _chkalive($_[0]);
377 100033 100       5852657 return _get($txn, $dbi, $_[1], $_[2]) if @_ > 2;
378 27         19 my($res, $data);
379             {
380 27         19 local $die_on_err = 0;
  27         23  
381 27         177 $res = _get($txn, $dbi, $_[1], $data);
382             }
383 27 50 66     150 croak($@) if $res && $die_on_err && $res != MDB_NOTFOUND() or undef $@;
      33        
      50        
384 27         76 $data;
385             }
386              
387             sub Rget {
388 2 50   2 0 3141 warn "get: '$_[1]'\n" if $DEBUG > 2;
389 2         4 local $die_on_err = 0;
390 2         5 _get(_chkalive($_[0]), $_[1], my $data);
391 2         7 return \$data;
392             }
393              
394              
395             sub del {
396 3     3 1 8 _del(_chkalive($_[0]), $_[1], $_[2]);
397             }
398              
399             sub stat {
400 26     26 1 2864 _stat(_chkalive($_[0]));
401             }
402              
403             sub set_dupsort {
404 0     0 0 0 my $self = shift;
405 0         0 my $txn = $self->[0];
406 0         0 $Envs{ $txn->_env }[1][ $self->[1] ] = shift;
407             }
408              
409             sub set_compare {
410 1     1 1 2 my $self = shift;
411 1         2 my $txn = $self->[0];
412 1         6 $Envs{ $txn->_env }[2][ $self->[1] ] = shift;
413             }
414              
415             sub Cursor {
416 6     6 1 9 my $DB = shift;
417 6         9 my ($txn, $dbi) = _chkalive($DB);
418 6         34 LMDB::Cursor::open($txn, $dbi, my $cursor);
419 6 50       26 return unless $cursor;
420 6         16 $Txns{$$txn}{Cursors}{$$cursor} = 1;
421 6         8 $Cursors{$$cursor} = $DB;
422 6 50       17 warn "Cursor opened for #$dbi\n" if $DEBUG;
423 6         14 $cursor;
424             }
425              
426 7     7 1 761 sub Txn : lvalue { $_[0][0]; }
427              
428 3     3 1 13 sub dbi : lvalue { $_[0][1]; }
429              
430             sub drop {
431 3   50 3 1 27 _drop(_chkalive($_[0]), $_[1] || 0);
432             }
433              
434             sub TIEHASH {
435 3     3   6 my $proto = shift;
436 3 100 66     11 return $proto if ref($proto) && _chkalive($proto); # Auto
437 1         2 my $mux = shift;
438 1         1 my $options = shift;
439 1 50       4 $options = { flags => $options } unless ref $options; # DBM Compat
440 1         1 my $txn;
441 1 50       4 if(ref $mux eq 'LMDB::Txn') {
    50          
442 0         0 $txn = $mux;
443             } elsif(ref $mux eq 'LMDB::Env') {
444 0         0 $txn = $mux->BeginTxn;
445 0         0 $txn->AutoCommit(1);
446             } else { # mux is dir
447 1 50       3 $options->{mode} = shift if @_; # DBM Compat
448 1         5 $txn = LMDB::Env->new($mux, $options)->BeginTxn;
449 1         5 $txn->AutoCommit(1);
450             }
451 1         3 $txn->OpenDB($options);
452             }
453              
454             sub FETCH {
455 7     7   39 my($self, $key) = @_;
456 7         5 my ($data, $res);
457             {
458 7         5 local $die_on_err = 0;
  7         7  
459 7         8 $res = _get(_chkalive($self), $key, $data);
460             }
461 7 50 66     35 croak($@) if $res && $die_on_err && $res != MDB_NOTFOUND() or undef $@;
      33        
      50        
462 7         15 $data;
463             }
464              
465             *STORE = \&put;
466             *CLEAR = \&drop;
467              
468             sub UNTIE {
469 3     3   9 my $self = shift;
470 3         5 my $txn = $self->[0];
471 3 50 50     15 return unless($txn && ($Txns{ $$txn } || undef($self->[0])));
      33        
472 3         9 delete $self->[2]; # Free cursor
473             }
474              
475             sub SCALAR {
476 5     5   242 return $_[0]->stat->{entries};
477             }
478              
479             sub EXISTS {
480 2     2   775 my($self, $key) = @_;
481 2         3 local $die_on_err = 0;
482 2         3 return !_get(_chkalive($self), $key, my $dummy);
483             }
484              
485             sub DELETE {
486 1     1   2 my($self, $key) = @_;
487 1         2 my @self = _chkalive($self);
488 1         1 my $data;
489 1         1 local $die_on_err = 0;
490 1 50       6 if(_get(@self, $key, $data) != MDB_NOTFOUND()) {
491 1         13 _del(@self, $key, undef);
492             }
493 1         4 return $data;
494             }
495              
496             sub FIRSTKEY {
497 4     4   11 my $self = shift;
498 4         8 $self->[2] = $self->Cursor;
499 4         9 $self->NEXTKEY;
500             }
501              
502             # I hop some day tie hashed are optimized
503             sub NEXTKEY {
504 85     85   765 my($self, $key) = @_;
505 85 100       108 my $op = defined($key) ? MDB_NEXT() : MDB_FIRST() ;
506 85         60 local $die_on_err = 0;
507 85         113 my $res = $self->[2]->get($key, my $data, $op);
508 85 100       93 if($res == MDB_NOTFOUND()) {
509 4         26 return;
510             }
511 81 50       199 return wantarray ? ($key, $data) : $key;
512             }
513              
514             sub _mydbflags {
515 7     7   10 my($envid, $dbi, $bit) = @_;
516 7         35 my $cm = \vec($Envs{ $envid }[3], $dbi, LMDB_OFLAGN());
517 7         12 my $om = $$cm;
518 7 100       17 if(@_ > 3) {
519 3 50       16 $$cm = $_[3] ? ($$cm | $bit) : ($$cm & ~$bit);
520 3         18 _resetcurdbi();
521             }
522 7         33 return $om & $bit;
523             }
524              
525             sub ReadMode {
526 6     6 1 438 my $self = shift;
527 6         13 my($txn, $dbi) = _chkalive($self);
528 6         19 _mydbflags($txn->_env, $dbi, 1, @_);
529             }
530              
531             sub UTF8 {
532 1     1 1 271 my $self = shift;
533 1         3 my($txn, $dbi) = _chkalive($self);
534 1         7 _mydbflags($txn->_env, $dbi, 2, @_);
535             }
536              
537             1;
538             __END__