File Coverage

blib/lib/MMapDB.pm
Criterion Covered Total %
statement 420 643 65.3
branch 99 282 35.1
condition 11 65 16.9
subroutine 53 86 61.6
pod 20 20 100.0
total 603 1096 55.0


line stmt bran cond sub pod time code
1             package MMapDB;
2              
3 2     2   47547 use 5.008008;
  2         8  
  2         78  
4 2     2   10 use strict;
  2         4  
  2         61  
5 2     2   7 use warnings;
  2         7  
  2         74  
6 2     2   7 no warnings qw/uninitialized/;
  2         3  
  2         65  
7              
8             # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
9             # keep this in mind
10 2     2   1708 use integer;
  2         17  
  2         9  
11             # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12              
13 2     2   55 use Fcntl qw/:seek :flock/;
  2         3  
  2         278  
14 2     2   10 use File::Spec;
  2         4  
  2         43  
15 2     2   1601 use File::Map qw/map_handle protect/;
  2         15845  
  2         11  
16 2     2   265 use Exporter qw/import/;
  2         4  
  2         53  
17 2     2   433121 use Encode ();
  2         57985  
  2         388  
18              
19             { # limit visibility of "our"/"my" variables
20             our $VERSION = '0.15';
21             our %EXPORT_TAGS=
22             (
23             mode =>[qw/DATAMODE_NORMAL DATAMODE_SIMPLE/],
24             error=>[qw/E_READONLY E_TWICE E_TRANSACTION E_FULL E_DUPLICATE
25             E_OPEN E_READ E_WRITE E_CLOSE E_RENAME E_TRUNCATE E_LOCK
26             E_RANGE E_NOT_IMPLEMENTED/],
27             );
28             my %seen;
29             undef @seen{map {@$_} values %EXPORT_TAGS};
30             our @EXPORT_OK=keys %seen;
31             $EXPORT_TAGS{all}=\@EXPORT_OK;
32              
33             require XSLoader;
34             XSLoader::load('MMapDB', $VERSION);
35              
36             our @attributes;
37             BEGIN {
38             # define attributes and implement accessor methods
39             # !! keep in sync with MMapDB.xs !!
40 2     2   12 @attributes=(qw/filename readonly intfmt _data _intsize _stringfmt
41             _stringtbl mainidx _ididx main_index id_index
42             _nextid _idmap _tmpfh _tmpname _stringfh _stringmap
43             _strpos lockfile flags dbformat_in dbformat_out
44             _stringfmt_out stringmap_prealloc _stringmap_end
45             index_prealloc _index_end _tmpmap
46             /);
47 2         12 for( my $i=0; $i<@attributes; $i++ ) {
48 56         62 my $method_num=$i;
49             ## no critic
50 2     2   19 no strict 'refs';
  2         4  
  2         145  
51 56     50736   706 *{__PACKAGE__.'::'.$attributes[$method_num]}=
  50736         112467  
52 56         276 sub : lvalue {$_[0]->[$method_num]};
53             ## use critic
54             }
55             }
56             }
57              
58             my @dbformats=qw/MMDB MMDC/;
59             my %dbformats=do { my $i=0; map {($_=>$i++)} @dbformats };
60              
61 2     2   19603 BEGIN {
62             use constant {
63 2         1087 FORMATVERSION => 0, # magic number position (in bytes)
64             INTFMT => 4, # INTFMT byte position (in bytes)
65             BASEOFFSET => 8,
66             MAINIDX => 0, # (in words (units of _intsize bytes))
67             IDIDX => 1, # (in words)
68             NEXTID => 2, # (in words)
69             STRINGTBL => 3, # (in words)
70             DATASTART => 4, # (in words)
71              
72             DBFMT0 => 0, # MMDB format
73             DBFMT1 => 1, # MMDC format with utf8 support
74              
75             # iterator questions
76             IT_NTH =>0, # reposition iterator
77             IT_CUR =>1, # what is the current index
78             IT_NELEM =>2, # how many elements does it iterate over
79              
80             DATAMODE_NORMAL=>0,
81             DATAMODE_SIMPLE=>1,
82              
83             E_READONLY => \'database is read-only',
84             E_TWICE => \'can\'t insert the same ID twice',
85             E_TRANSACTION => \'there is already an active transaction',
86             E_FULL => \'can\'t allocate ID',
87             E_DUPLICATE => \'data records cannot be mixed up with subindices',
88              
89             E_OPEN => \'can\'t open file',
90             E_READ => \'can\'t read from file',
91             E_WRITE => \'can\'t write to file',
92             E_CLOSE => \'file could not be closed',
93             E_RENAME => \'can\'t rename file',
94             E_SEEK => \'can\'t move file pointer',
95             E_TRUNCATE => \'can\'t truncate file',
96             E_LOCK => \'can\'t (un)lock lockfile',
97             E_RANGE => \'attempt move iterator out of its range',
98             E_NOT_IMPLEMENTED => \'function not implemented',
99 2     2   12 };
  2         14  
100             }
101              
102             #sub D {
103             # use Data::Dumper;
104             # local $Data::Dumper::Useqq=1;
105             # warn Dumper @_;
106             #}
107              
108             sub _putdata {
109 3343     3343   8993 my ($I, $pos, $fmt, @param)=@_;
110              
111 3343         6628 my $pstr=pack $fmt, @param;
112 3343         6490 my $map=$I->_tmpmap;
113 3343 100       7400 if( $pos+length($pstr)>length $$map ) {
114 16         48 my $prea=$I->index_prealloc;
115 16         43 my $need=$prea*(($pos+length($pstr)+$prea-1)/$prea);
116 16         32 eval {
117 16         38 my $fh=$I->_tmpfh;
118 16 50 33     1161 sysseek $fh, $need, SEEK_SET and
119             truncate $fh, $need and
120             map_handle $$map, $fh, '+>', 0, $need;
121             };
122 16 50       4028 $I->_e(E_OPEN) if $@;
123             }
124 3343         7464 substr $$map, $pos, length($pstr), $pstr;
125 3343         9511 return length($pstr);
126             }
127              
128             sub set_intfmt {
129 7     7 1 18 my ($I, $fmt)=@_;
130              
131 7 50       19 $fmt='N' unless $fmt;
132              
133 7         11 my %allowed; undef @allowed{qw/L N J Q/};
  7         28  
134 7 50       24 return unless exists $allowed{$fmt};
135              
136 7         22 $I->intfmt=$fmt;
137 7         33 $I->_intsize=length pack($fmt, 0);
138              
139 7 100       18 if( $I->dbformat_in>DBFMT0 ) {
140             # new format with utf8 support
141 6         13 $I->_stringfmt=$I->intfmt.'/a*C x!'.$I->_intsize;
142             } else {
143 1         3 $I->_stringfmt=$I->intfmt.'/a* x!'.$I->_intsize;
144             }
145              
146 7 100       18 if( $I->dbformat_out>DBFMT0 ) {
147             # new format with utf8 support
148 6         15 $I->_stringfmt_out=$I->intfmt.'/a*C x!'.$I->_intsize;
149             } else {
150 1         3 $I->_stringfmt_out=$I->intfmt.'/a* x!'.$I->_intsize;
151             }
152              
153 7         22 return 1;
154             }
155              
156             sub new {
157 1     1 1 233 my ($parent, @param)=@_;
158 1         2 my $I;
159              
160 1 50       4 if (ref $parent) {
161 0         0 $I=bless [@$parent]=>ref($parent);
162 0         0 for my $k (qw/_nextid _idmap _tmpfh _tmpname _stringfh _stringmap
163             _strpos main_index id_index/) {
164 0         0 undef $I->$k;
165             }
166 0 0       0 if( defined $I->_data ) {
167             # parameters: PARENT POS DATAMODE
168 0         0 tie %{$I->main_index=+{}}, 'MMapDB::Index', $I, $I->mainidx, 0;
  0         0  
169 0         0 tie %{$I->id_index=+{}}, 'MMapDB::IDIndex', $I, undef, 0;
  0         0  
170             }
171             } else {
172 1         2 $I=bless []=>$parent;
173 1         4 $I->set_intfmt('N');
174 1         3 $I->flags=0;
175 1         5 $I->dbformat_in=$#dbformats; # use the newest by default
176 1         3 $I->dbformat_out=$#dbformats; # use the newest by default
177             }
178 1         3 $I->stringmap_prealloc=1024*1024*10; # 10MB
179 1         4 $I->index_prealloc=1024*1024*10; # 10MB
180              
181 1 50       4 if( @param==1 ) {
182 0         0 $I->filename=$param[0];
183             } else {
184 1         6 while( my ($k, $v)=splice @param, 0, 2 ) {
185 1 50       17 $I->$k=$v if $k=$I->can($k);
186             }
187 1 50       3 $I->set_intfmt($I->intfmt) unless $I->intfmt eq 'N';
188             }
189              
190 1         3 return $I;
191             }
192              
193             sub is_valid {
194 0     0 1 0 my ($I)=@_;
195              
196 0 0       0 return unless $I->_data;
197             # the INTFMT field serves 2 ways:
198             # 1) it specifies the used integer format
199             # 2) it works as VALID flag. commit() write a NULL byte here
200             # to mark the old file as invalid.
201             # we must reconnect if our cached fmt does not match.
202 0         0 return substr( ${$I->_data}, INTFMT, 1 ) eq $I->intfmt;
  0         0  
203             }
204              
205             sub start {
206 4     4 1 9 my ($I)=@_;
207              
208 4 50       13 $I->_e(E_TRANSACTION) if defined $I->_tmpfh;
209              
210 4         6 my $retry=5;
211 4 50       14 RETRY: {
212 4         5 return unless $retry--;
213 2         5 $I->stop if (defined $I->_data and
214 4 100 66     9 substr( ${$I->_data}, INTFMT, 1 ) ne $I->intfmt);
215              
216 4 50       9 unless( $I->_data ) {
217 4         8 my ($dummy, $fmt);
218 0         0 my $fh;
219 4 50       11 if( $I->readonly ) {
220 0 0       0 open $fh, '<', $I->filename or return;
221             } else {
222 4 100       21 open $fh, '+<', $I->filename or return;
223             }
224              
225             # Map the main data always read-only. If we are in writable mode
226             # map only the header page again writable.
227 3         9 eval {
228 3         18 map_handle $dummy, $fh, '<';
229             };
230 3         9821 close $fh;
231 3 50       12 return if $@; # perhaps throw something here
232 3 50       11 return unless length $dummy;
233              
234             # check magic number
235 3 50       32 return unless exists $dbformats{substr($dummy, FORMATVERSION, 4)};
236 3         27 $I->dbformat_out=$I->dbformat_in=
237             $dbformats{substr($dummy, FORMATVERSION, 4)};
238              
239             # read integer format
240 3         20 $fmt=unpack 'x4a', $dummy;
241 3 50       12 if( $fmt eq "\0" ) {
242 0         0 select undef, undef, undef, 0.1;
243 0         0 redo RETRY;
244             }
245 3 50       14 return unless $I->set_intfmt($fmt);
246              
247             # read the byte just after the format character
248 3         15 $I->flags=unpack 'x5C', $dummy;
249              
250 3         9 $I->_data=\$dummy; # now mapped
251              
252             # read main index position
253 3         5 $I->mainidx=unpack('x'.(BASEOFFSET+MAINIDX*$I->_intsize).$I->intfmt,
254 3         9 ${$I->_data});
255 3         8 $I->_ididx=unpack('x'.(BASEOFFSET+IDIDX*$I->_intsize).$I->intfmt,
256 3         9 ${$I->_data});
257 3         6 $I->_stringtbl=unpack('x'.(BASEOFFSET+STRINGTBL*$I->_intsize).
258 3         9 $I->intfmt, ${$I->_data});
259              
260             # parameters: PARENT POS DATAMODE
261 3         7 tie %{$I->main_index=+{}}, 'MMapDB::Index', $I, $I->mainidx, 0;
  3         8  
262 3         4 tie %{$I->id_index=+{}}, 'MMapDB::IDIndex', $I, undef, 0;
  3         8  
263             }
264             }
265              
266 3         13937 return $I;
267             }
268              
269             sub stop {
270 4     4 1 6 my ($I)=@_;
271              
272 4 50       13 $I->_e(E_TRANSACTION) if defined $I->_tmpfh;
273              
274 4 100       13 return $I unless defined $I->_data;
275              
276 3         9 for my $k (qw/_data _stringtbl mainidx _ididx/) {
277 12         34 undef $I->$k;
278             }
279              
280 3         92 untie %{$I->main_index}; undef $I->main_index;
  3         8  
  3         11  
281 3         8 untie %{$I->id_index}; undef $I->id_index;
  3         22  
  3         8  
282              
283 3         7 return $I;
284             }
285              
286             sub index_iterator {
287 0     0 1 0 my ($I, $pos, $nth)=@_;
288              
289 0         0 my $data=$I->_data;
290 0 0 0 0   0 return sub {} unless $data and defined $pos;
  0         0  
291 0         0 my $fmt=$I->intfmt;
292 0         0 my $isz=$I->_intsize;
293 0         0 my ($nrecords, $recordlen)=unpack 'x'.$pos.$fmt.'2', $$data;
294 0 0       0 die E_RANGE if $nth>$nrecords;
295 0         0 $recordlen*=$isz;
296 0         0 my ($cur, $end)=($pos+2*$isz+$nth*$recordlen,
297             $pos+2*$isz+$nrecords*$recordlen);
298 0         0 my $stroff=$I->_stringtbl;
299 0         0 my $sfmt=$I->_stringfmt;
300 0         0 my $dbfmt=$I->dbformat_in;
301              
302             my $it=MMapDB::Iterator->new
303             ( sub {
304 0 0   0   0 if( @_ ) {
305 0         0 for( my $i=0; $i<@_; $i++ ) {
306 0 0       0 if( $_[$i]==IT_NTH ) {
    0          
    0          
307 0         0 my $nth=$_[++$i];
308 0         0 $nth=$pos+2*$isz+$nth*$recordlen;
309 0 0 0     0 die E_RANGE unless( $pos+2*$isz<=$nth and $nth<=$end );
310 0         0 $cur=$nth;
311             # return in VOID context
312 0 0       0 return unless defined wantarray;
313             } elsif( $_[$i]==IT_CUR ) {
314 0         0 return ($cur-2*$isz-$pos)/$recordlen;
315             } elsif( $_[$i]==IT_NELEM ) {
316 0         0 return ($end-2*$isz-$pos)/$recordlen;
317             }
318             }
319             }
320 0 0       0 return if $cur>=$end;
321 0         0 my ($key, $npos)=unpack 'x'.$cur.$fmt.'2', $$data;
322 0         0 my @list=unpack 'x'.($cur+2*$isz).$fmt.$npos, $$data;
323 0         0 $cur+=$recordlen;
324 0 0       0 if( $dbfmt>DBFMT0 ) {
325 0         0 my ($str, $utf8)=unpack('x'.($stroff+$key).$sfmt, $$data);
326 0 0       0 Encode::_utf8_on($str) if( $utf8 );
327 0         0 return ($str, @list);
328             } else {
329 0         0 return (unpack('x'.($stroff+$key).$sfmt, $$data), @list);
330             }
331             }
332 0         0 );
333              
334 0 0       0 return wantarray ? ($it, $nrecords) : $it;
335             }
336              
337             sub id_index_iterator {
338 0     0 1 0 my ($I)=@_;
339              
340 0         0 my $data=$I->_data;
341 0 0   0   0 return sub {} unless $data;
  0         0  
342 0         0 my $pos=$I->_ididx;
343 0         0 my ($nrecords)=unpack 'x'.$pos.$I->intfmt, $$data;
344 0         0 my $isz=$I->_intsize;
345 0         0 my $recordlen=2*$isz;
346 0         0 my ($cur, $end)=($pos+$isz,
347             $pos+$isz+$nrecords*$recordlen);
348 0         0 my $fmt=$I->intfmt.'2';
349              
350             my $it=MMapDB::Iterator->new
351             ( sub {
352 0 0   0   0 if( @_ ) {
353 0         0 for( my $i=0; $i<@_; $i++ ) {
354 0 0       0 if( $_[$i]==IT_NTH ) {
    0          
    0          
355 0         0 my $nth=$_[++$i];
356 0         0 $nth=$pos+$isz+$nth*$recordlen;
357 0 0 0     0 die E_RANGE unless( $pos+$isz<=$nth and $nth<=$end );
358 0         0 $cur=$nth;
359             # return in VOID context
360 0 0       0 return unless defined wantarray;
361             } elsif( $_[$i]==IT_CUR ) {
362 0         0 return ($cur-$isz-$pos)/$recordlen;
363             } elsif( $_[$i]==IT_NELEM ) {
364 0         0 return ($end-$isz-$pos)/$recordlen;
365             }
366             }
367             }
368 0 0       0 return if $cur>=$end;
369 0         0 my @l=unpack 'x'.$cur.$fmt, $$data;
370 0         0 $cur+=$recordlen;
371 0         0 return @l;
372             }
373 0         0 );
374              
375 0 0       0 return wantarray ? ($it, $nrecords) : $it;
376             }
377              
378             sub is_datapos {
379 0     0 1 0 my ($I, $pos)=@_;
380 0         0 return $pos<$I->mainidx;
381             }
382              
383             sub datamode : lvalue {
384 0     0 1 0 tied(%{$_[0]->main_index})->datamode;
  0         0  
385             }
386              
387             sub id_datamode : lvalue {
388 0     0 1 0 tied(%{$_[0]->id_index})->datamode;
  0         0  
389             }
390              
391 0     0   0 sub _e {$_[0]->_rollback; die $_[1]}
  0         0  
392 1520 50   1520   3104 sub _ct {$_[0]->_tmpfh or die E_TRANSACTION}
393              
394             sub begin {
395 3     3 1 15 my ($I, $dbfmt)=@_;
396              
397 3 50       9 $I->_e(E_TRANSACTION) if defined $I->_tmpfh;
398              
399 3 50       7 die E_READONLY if $I->readonly;
400              
401 3 50       8 if( defined $I->lockfile ) {
402             # open lockfile
403 0 0       0 unless( ref $I->lockfile ) {
404 0 0       0 open my $fh, '>', $I->lockfile or die E_OPEN;
405 0         0 $I->lockfile=$fh;
406             }
407 0 0       0 flock $I->lockfile, LOCK_EX or die E_LOCK;
408             }
409              
410 3 50       14 if (defined $dbfmt) {
411 0 0       0 $I->dbformat_out=($dbfmt==-1 ? $#dbformats : $dbfmt);
412             }
413 3         12 $I->set_intfmt($I->intfmt); # adjust string format
414              
415 3         9 $I->_tmpname=$I->filename.'.'.$$;
416              
417             {
418             # open stringtbl tmpfile
419 3 50       6 open my $fh, '+>', $I->_tmpname.'.strings' or die E_OPEN;
  3         15  
420 3         10 $I->_stringfh=$fh;
421 3         8 $I->_stringmap=\my $strings;
422 3         5 eval {
423 3 50 33     9 sysseek $fh, $I->stringmap_prealloc, SEEK_SET and
424             truncate $fh, $I->stringmap_prealloc and
425             map_handle $strings, $fh, '+>', 0, $I->stringmap_prealloc;
426             };
427 3 50       623 die E_OPEN if $@;
428 3         10 $I->_stringmap_end=0;
429             }
430              
431             {
432             # open tmpfile
433 3 50       5 open my $fh, '+>', $I->_tmpname or die E_OPEN;
  3         14  
434 3         8 $I->_tmpfh=$fh; # this starts the transaction
435 3         3 $I->_tmpmap=\do{my $map=''};
  3         10  
436              
437 3         15 $I->_putdata(0, 'a4aC', $dbformats[$I->dbformat_out], $I->intfmt,
438             $I->flags & 0xff);
439 3         10 $I->_index_end=BASEOFFSET+DATASTART*$I->_intsize;
440             }
441              
442             # and copy every *valid* entry from the old file
443             # create _idmap on the way
444 3         8 $I->_idmap={};
445 3         9 $I->_strpos=[];
446 3         11 for( my $it=$I->iterator; my ($pos)=$it->(); ) {
447 258         1508 $I->insert($I->data_record($pos));
448             }
449 3 100       6 if( $I->_data ) {
450 2         5 $I->_nextid=unpack('x'.(BASEOFFSET+NEXTID*$I->_intsize).$I->intfmt,
451 2         5 ${$I->_data});
452             } else {
453 1         2 $I->_nextid=1;
454             }
455              
456 3         31 return $I;
457             }
458              
459             # The interator() below hops over the mmapped area. This one works on the file.
460             # It can be used only within a begin/commit cycle.
461             sub _fiterator {
462 3     3   25 my ($I, $end)=@_;
463              
464 3         11 my $map=$I->_tmpmap;
465 3         11 my $pos=BASEOFFSET+$I->_intsize*DATASTART;
466              
467             return sub {
468 1261 100       2386 LOOP: {
469 1261     1261   1436 return if $pos>=$end;
470 1258         1396 my $elpos=$pos;
471              
472             # valid id nkeys key1...keyn sort data
473             # read (valid, id, nkeys)
474 1258         2899 my ($valid, $id, $nkeys)=unpack 'x'.$pos.$I->intfmt.'3', $$map;
475              
476             # move iterator position
477             # 5: valid, id, nkeys ... sort, data
478 1258         2380 $pos+=$I->_intsize*(5+$nkeys);
479 1258 50       2511 redo LOOP unless ($valid);
480              
481 1258         2249 my @l=unpack 'x'.($elpos+3*$I->_intsize).$I->intfmt.($nkeys+2), $$map;
482 1258         1956 my $data=pop @l;
483 1258         1512 my $sort=pop @l;
484              
485 1258         5088 return ([\@l, $sort, $data, $id], $elpos);
486             }
487 3         31 };
488             }
489              
490             sub _really_write_index {
491 28     28   49 my ($I, $map, $level)=@_;
492              
493 28         36 my $recordlen=1; # in ints: (1): for subindexes there is one
494             # position to store
495              
496             # find the max. number of positions we have to store
497 28         157 foreach my $v (values %$map) {
498 523 100       1000 if( ref($v) eq 'ARRAY' ) {
499             # list of data records
500 498 100       1089 $recordlen=@$v if @$v>$recordlen;
501             }
502             # else: recordlen is initialized with 1. So for subindexes there is
503             # nothing to do
504             }
505             # each record comes with a header of 2 integers, the key position in the
506             # string table and the actual position count of the record. So we have to
507             # add 2 to $recordlen.
508 28         53 $recordlen+=2;
509              
510             # the index itself has a 2 integer header, the recordlen and the number
511             # of index records that belong to the index.
512 28         91 my $indexsize=(2+$recordlen*keys(%$map))*$I->_intsize; # in bytes
513              
514 28         61 my $pos=$I->_index_end;
515              
516             # make room
517 28         58 $I->_index_end=$pos+$indexsize;
518              
519             # and write subindices after this index
520 28         51 my $strings=$I->_stringmap;
521 28         60 my $sfmt=$I->_stringfmt_out;
522 28         50 my $dbfmt=$I->dbformat_out;
523 28         88 foreach my $v (values %$map) {
524 523 100       1153 if( ref($v) eq 'HASH' ) {
525             # convert the subindex into a position list
526 25         85 $v=[$I->_really_write_index($v, $level+1)];
527             } else {
528             # here we already have a position list but it still contains
529             # sorting ids.
530 1258         12234 @$v=map {
531 965         1525 $_->[1];
532             } sort {
533             $a->[0] cmp $b->[0];
534             } map {
535             # fetch sort string from string table
536 498 50       764 if( $dbfmt>DBFMT0 ) {
  1258         2032  
537 1258         4878 my ($str, $utf8)=unpack('x'.$_->[0].$sfmt, $$strings);
538 1258 50       2659 Encode::_utf8_on($str) if $utf8;
539 1258         3983 [$str, $_->[1]];
540             } else {
541 0         0 [unpack('x'.$_->[0].$sfmt, $$strings), $_->[1]];
542             }
543             } @$v;
544             }
545             }
546              
547 28         86 my $fmt=$I->intfmt;
548 28         38 my $written=$pos;
549 28         126 $written+=$I->_putdata($written, $fmt.'2', 0+keys(%$map), $recordlen);
550              
551 28         57 $fmt.=$recordlen;
552             # write the records
553 28         146 foreach my $key (map {
  523         767  
554 1683         2108 $_->[0]
555             } sort {
556             $a->[1] cmp $b->[1];
557             } map {
558 523 50       873 if( $dbfmt>DBFMT0 ) {
559 523         1538 my ($str, $utf8)=unpack('x'.$_.$sfmt, $$strings);
560 523 50       1062 Encode::_utf8_on($str) if $utf8;
561 523         1262 [$_, $str];
562             } else {
563 0         0 [$_, unpack('x'.$_.$sfmt, $$strings)];
564             }
565             } keys %$map) {
566 523         851 my $v=$map->{$key};
567              
568             #D($key, $v);
569             #warn "$prefix> idx rec: ".unpack('H*', pack($fmt, $key, 0+@$v, @$v))."\n";
570              
571 523         1254 $written+=$I->_putdata($written, $fmt, $key, 0+@$v, @$v);
572             }
573              
574 28         521 return $pos;
575             }
576              
577             sub _write_index {
578 3     3   8 my ($I)=@_;
579              
580 3         5 my %map;
581 3         10 for( my $it=$I->_fiterator($I->_index_end); my ($el, $pos)=$it->(); ) {
582 1258         9831 my $m=\%map;
583 1258         1560 my @k=@{$el->[0]};
  1258         4405  
584 1258   66     5144 while(@k>1 and ref($m) eq 'HASH') {
585 1000         1363 my $k=shift @k;
586 1000 100       2231 $m->{$k}={} unless exists $m->{$k};
587 1000         2805 $m=$m->{$k};
588             }
589 1258 50       3057 $I->_e(E_DUPLICATE) unless ref($m) eq 'HASH';
590 1258 100       3993 $m->{$k[0]}=[] unless defined $m->{$k[0]};
591 1258 50       12073 $I->_e(E_DUPLICATE) unless ref($m->{$k[0]}) eq 'ARRAY';
592             # Actually we want to save only positions but they must be ordered.
593             # So either keep the order field together with the position here to
594             # sort it later or do sort of ordered insert here.
595             # The former is simpler. So it's it.
596 1258         1395 push @{$m->{$k[0]}}, [$el->[1], $pos];
  1258         6684  
597             }
598              
599 3         19 return $I->_really_write_index(\%map, 0);
600             }
601              
602             sub _write_id_index {
603 3     3   6 my ($I)=@_;
604              
605 3         11 my $map=$I->_idmap;
606 3         8 my $fmt=$I->intfmt;
607              
608 3         8 my $pos=$I->_index_end;
609 3         4 my $written=$pos;
610 3         11 $written+=$I->_putdata($written, $fmt, 0+keys(%$map));
611              
612 3         8 $fmt.='2';
613             # write the records
614 3         452 foreach my $key (sort {$a <=> $b} keys %$map) {
  10463         9376  
615 1258         2132 my $v=$map->{$key};
616              
617             #warn "id> idx rec: ".unpack('H*', pack($fmt, $key, $v))."\n";
618              
619 1258         2266 $written+=$I->_putdata($written, $fmt, $key, $v);
620             }
621 3         147 $I->_index_end=$written;
622              
623             #warn sprintf "id> index written @ %#x\n", $pos;
624              
625 3         8 return $pos;
626             }
627              
628             sub invalidate {
629 3     3 1 5 my ($I)=@_;
630 3 50       12 $I->_e(E_READONLY) if $I->readonly;
631 3 100       8 return unless defined $I->_data;
632 2         4 protect ${$I->_data}, '+<';
  2         6  
633 2         4 substr( ${$I->_data}, INTFMT, 1, "\0" );
  2         7  
634 2         5 protect ${$I->_data}, '<';
  2         5  
635             }
636              
637             sub commit {
638 3     3 1 10 my ($I, $dont_invalidate)=@_;
639              
640 3         8 $I->_ct;
641              
642             # write NEXTID
643 3         12 $I->_putdata(BASEOFFSET+NEXTID*$I->_intsize, $I->intfmt, $I->_nextid);
644              
645             # write MAINIDX and IDIDX
646 3         13 my $mainidx=$I->_write_index;
647 3         29 my $ididx=$I->_write_id_index;
648              
649 3         38 $I->_putdata(BASEOFFSET+MAINIDX*$I->_intsize, $I->intfmt, $mainidx);
650 3         8 $I->_putdata(BASEOFFSET+IDIDX*$I->_intsize, $I->intfmt, $ididx);
651 3         10 $I->_putdata(BASEOFFSET+STRINGTBL*$I->_intsize, $I->intfmt, $I->_index_end);
652              
653             # now copy the string table
654 3         8 my $fh=$I->_tmpfh;
655 3         10 my $strings=$I->_stringmap;
656 3         9 my $map=$I->_tmpmap;
657 3         10 my $need=$I->_index_end+$I->_stringmap_end;
658 3 100       12 if( $need>length $$map ) {
659 1         4 eval {
660 1 50 33     62 sysseek $fh, $need, SEEK_SET and
661             truncate $fh, $need and
662             map_handle $$map, $fh, '+>', 0, $need;
663             };
664 1 50       198 $I->_e(E_OPEN) if $@;
665             }
666 3         9 substr($$map, $I->_index_end, $I->_stringmap_end,
667             substr($$strings, 0, $I->_stringmap_end));
668 3         218 truncate $fh, $need;
669              
670             #warn "mainidx=$mainidx, ididx=$ididx, strtbl=$strtbl\n";
671              
672 3         11 undef $I->_idmap;
673 3         384 undef $I->_strpos;
674 3         211 undef $I->_stringmap;
675              
676 3 50       11 close $I->_stringfh or $I->_e(E_CLOSE);
677 3         8 undef $I->_stringfh;
678 3         19 unlink $I->_tmpname.'.strings';
679              
680 3         11 undef $I->_tmpmap;
681 3 50       20 close $fh or $I->_e(E_CLOSE);
682 3         9 undef $I->_tmpfh;
683              
684             # rename is (at least on Linux) an atomic operation
685 3 50       10 rename $I->_tmpname, $I->filename or $I->_e(E_RENAME);
686              
687 3 50       18 $I->invalidate unless $dont_invalidate;
688              
689 3 50       9 if( $I->lockfile ) {
690 0 0       0 flock $I->lockfile, LOCK_UN or die E_LOCK;
691             }
692              
693 3         14 $I->start;
694             }
695              
696             sub _rollback {
697 0     0   0 my ($I)=@_;
698              
699 0         0 close $I->_tmpfh;
700 0         0 undef $I->_tmpfh;
701 0         0 unlink $I->_tmpname;
702              
703 0         0 close $I->_stringfh;
704 0         0 undef $I->_stringfh;
705 0         0 unlink $I->_tmpname.'.strings';
706              
707 0         0 undef $I->_stringmap;
708 0         0 undef $I->_strpos;
709 0         0 undef $I->_idmap;
710              
711 0         0 $I->_stringfmt_out=$I->_stringfmt;
712 0         0 $I->dbformat_out=$I->dbformat_in;
713              
714 0 0       0 if( $I->lockfile ) {
715 0 0       0 flock $I->lockfile, LOCK_UN or die E_LOCK;
716             }
717             }
718              
719             sub rollback {
720 0     0 1 0 my ($I)=@_;
721              
722 0 0       0 $I->_e(E_TRANSACTION) unless defined $I->_tmpfh;
723 0         0 $I->_rollback;
724             }
725              
726             sub DESTROY {
727 1     1   573 my ($I)=@_;
728              
729 1 50       3 $I->_rollback if defined $I->_tmpfh;
730 1         5 $I->stop;
731             }
732              
733             sub backup {
734 0     0 1 0 my ($I, $fn)=@_;
735              
736 0         0 $I->start;
737              
738 0 0       0 my $backup=$I->new(filename=>(defined $fn ? $fn : $I->filename.'.BACKUP'));
739              
740 0         0 $backup->begin;
741 0         0 $backup->commit(1);
742             }
743              
744             sub restore {
745 0     0 1 0 my ($I, $fn)=@_;
746              
747 0         0 $I->start;
748 0 0       0 $fn=$I->filename.'.BACKUP' unless defined $fn;
749              
750             # rename is (at least on Linux) an atomic operation
751 0 0       0 rename $fn, $I->filename or die E_RENAME;
752 0         0 $I->invalidate;
753              
754 0         0 return $I->start;
755             }
756              
757             # Returns the position of $key in the stringtable
758             # If $key is not found it is inserted. @{$I->_strpos} is kept ordered.
759             # So, we can do a binary search.
760             # This implements something very similar to a HASH. So, why not use a HASH?
761             # A HASH is kept completely in core and the memory is not returned to the
762             # operating system when finished. The number of strings in the database
763             # can become quite large. So if a long running process updates the database
764             # only once it will consume much memory for nothing. To avoid this we map
765             # the string table currently under construction in a separate file that
766             # is mmapped into the address space of this process and keep here only
767             # a list of pointer into this area. When the transaction is committed the
768             # memory is returned to the OS. But on the other hand we need fast access.
769             # This is achieved by the binary search.
770             sub _string2pos {
771 5548     5548   8186 my ($I, $key)=@_;
772              
773 5548         10144 my $fmt=$I->_stringfmt_out;
774 5548         9681 my $dbfmt=$I->dbformat_out;
775              
776 5548 50       11360 Encode::_utf8_off($key) if $dbfmt==DBFMT0;
777              
778 5548         9687 my $strings=$I->_stringmap;
779 5548         9670 my $poslist=$I->_strpos;
780              
781 5548         10641 my ($low, $high)=(0, 0+@$poslist);
782             #warn "_string2pos($key): low=$low, high=$high\n";
783              
784 5548         5975 my ($cur, $rel, $curstr);
785 5548         10906 while( $low<$high ) {
786 48535         54001 $cur=($high+$low)/2; # "use integer" is active, see above
787 48535 50       74672 if( $dbfmt>DBFMT0 ) {
788 48535         175999 ($curstr, my $utf8)=unpack 'x'.$poslist->[$cur].$fmt, $$strings;
789 48535 50       113766 Encode::_utf8_on($curstr) if $utf8;
790             } else {
791 0         0 $curstr=unpack 'x'.$poslist->[$cur].$fmt, $$strings;
792             }
793             #warn " --> looking at $curstr: low=$low, high=$high, cur=$cur\n";
794 48535         66884 $rel=($curstr cmp $key);
795 48535 100       81145 if( $rel<0 ) {
    100          
796             #warn " --> moving low: $low ==> ".($cur+1)."\n";
797 29982         59137 $low=$cur+1;
798             } elsif( $rel>0 ) {
799             #warn " --> moving high: $high ==> ".($cur)."\n";
800             # don't try to optimize here: $high=$cur-1 will not work in border cases
801 16064         31562 $high=$cur;
802             } else {
803             #warn " --> BINGO\n";
804 2489         8198 return $poslist->[$cur];
805             }
806             }
807             #warn " --> NOT FOUND\n";
808 3059         6818 my $pos=$I->_stringmap_end;
809 3059         6529 splice @$poslist, $low, 0, $pos;
810             #warn " --> inserting $pos into poslist at $low ==> @$poslist\n";
811              
812 3059         3251 my $newstr;
813 3059 50       6194 if( $dbfmt>DBFMT0 ) {
814 3059 50       6924 if( Encode::is_utf8($key) ) {
815 0         0 $newstr=pack($fmt, Encode::encode_utf8($key), 1);
816             } else {
817 3059         9760 $newstr=pack($fmt, $key, 0);
818             }
819             } else {
820 0         0 $newstr=pack($fmt, $key);
821             }
822              
823 3059 100       7144 if( $pos+length($newstr)>length $$strings ) {
824             # remap
825 10         31 my $prea=$I->stringmap_prealloc;
826 10         24 my $need=$prea*(($pos+length($newstr)+$prea-1)/$prea);
827 10         20 eval {
828 10         28 my $fh=$I->_stringfh;
829 10 50 33     1334 sysseek $fh, $need, SEEK_SET and
830             truncate $fh, $need and
831             map_handle $$strings, $fh, '+>', 0, $need;
832             };
833 10 50       2437 $I->_e(E_OPEN) if $@;
834             }
835              
836 3059         7132 substr $$strings, $pos, length($newstr), $newstr;
837 3059         6470 $I->_stringmap_end=$pos+length($newstr);
838              
839 3059         11126 return $pos;
840             }
841              
842             sub insert {
843 1516     1516 1 23301 my ($I, $rec)=@_;
844             #my ($I, $key, $sort, $data, $id)=@_;
845              
846             #use Data::Dumper; $Data::Dumper::Useqq=1; warn Dumper $rec;
847              
848 1516         2903 $I->_ct;
849              
850 1516 50       3976 $rec->[0]=[$rec->[0]] unless ref $rec->[0];
851 1516         1945 for my $v (@{$rec}[1,2]) {
  1516         3178  
852 3032 50       6867 $v='' unless defined $v;
853             }
854              
855             # create new ID if necessary
856 1516         2733 my $id=$rec->[3];
857 1516         2834 my $idmap=$I->_idmap;
858 1516 100       3048 if( defined $id ) {
859 258 50       664 $I->_e(E_TWICE) if exists $idmap->{$id};
860             } else {
861 1258         2089 $id=$I->_nextid;
862 1258         3286 undef $idmap->{$id}; # allocate it
863              
864 2     2   31 my $mask=do{no integer; unpack( $I->intfmt, pack $I->intfmt, -1 )>>1};
  2         5  
  2         20  
  1258         1362  
  1258         2338  
865 1258         1877 my $nid=($id+1)&$mask;
866 1258 50       2550 $nid=1 if $nid==0;
867 1258         3339 while(exists $idmap->{$nid}) {
868 0 0       0 $nid=($nid+1)&$mask; $nid=1 if $nid==0;
  0         0  
869 0 0       0 $I->_e(E_FULL) if $nid==$id;
870             }
871 1258         2443 $I->_nextid=$nid;
872             }
873              
874 1516         3012 my $pos=$I->_index_end;
875 1516         5986 $I->_index_end+=$I->_putdata($pos, $I->intfmt.'*', 1, $id, 0+@{$rec->[0]},
  5548         10931  
876 1516         2819 map {$I->_string2pos($_)}
877 1516         2669 @{$rec->[0]}, @{$rec}[1,2]);
  1516         2610  
878              
879 1516         3942 $idmap->{$id}=$pos;
880              
881 1516         6468 return ($id, $pos);
882             }
883              
884             sub delete_by_id {
885 0     0 1 0 my ($I, $id, $return_element)=@_;
886              
887             # warn "delete_by_id($id)\n";
888              
889             # no such id
890 0 0       0 return unless exists $I->_idmap->{$id};
891              
892 0         0 my $map=$I->_tmpmap;
893 0         0 my $idmap=$I->_idmap;
894 0         0 my $pos;
895              
896 0 0       0 return unless defined($pos=delete $idmap->{$id});
897              
898             # read VALID, ID, NKEYS
899 0         0 my ($valid, $elid, $nkeys)=unpack 'x'.$pos.$I->intfmt.'3', $$map;
900              
901 0 0       0 return unless $valid;
902 0 0       0 return unless $id==$elid; # XXX: should'nt that be an E_CORRUPT
903              
904 0         0 my $rc=1;
905 0 0       0 if( $return_element ) {
906 0         0 my $strings=$I->_stringmap;
907 0         0 my $sfmt=$I->_stringfmt_out;
908 0         0 my $dbfmt=$I->dbformat_out;
909             my @l=map {
910 0 0       0 if( $dbfmt>DBFMT0 ) {
  0         0  
911 0         0 my ($str, $utf8)=unpack('x'.$_.$sfmt, $$strings);
912 0 0       0 Encode::_utf8_on($str) if $utf8;
913 0         0 $str;
914             } else {
915 0         0 unpack('x'.$_.$sfmt, $$strings);
916             }
917             } unpack('x'.($pos+3*$I->_intsize).$I->intfmt.($nkeys+2), $$map);
918              
919 0         0 my $rdata=pop @l;
920 0         0 my $rsort=pop @l;
921              
922 0         0 $rc=[\@l, $rsort, $rdata, $id];
923             }
924              
925 0         0 $I->_putdata($pos, $I->intfmt, 0); # invalidate the record
926              
927 0         0 return $rc;
928             }
929              
930             sub clear {
931 1     1 1 3 my ($I)=@_;
932              
933 1         3 $I->_ct;
934              
935 1         3 $I->_index_end=BASEOFFSET+DATASTART*$I->_intsize;
936 1         4 $I->_stringmap_end=0;
937              
938 1         3 $I->_idmap={};
939 1         75 $I->_strpos=[];
940              
941 1         41 return;
942             }
943              
944             # sub xdata_record {
945             # my ($I, $pos)=@_;
946              
947             # return unless $pos>0 and $pos<$I->mainidx;
948              
949             # # valid id nkeys key1...keyn sort data
950             # my ($id, $nkeys)=unpack('x'.($pos+$I->_intsize).' '.$I->intfmt.'3',
951             # ${$I->_data});
952              
953             # my $off=$I->_stringtbl;
954             # my $data=$I->_data;
955             # my $sfmt=$I->_stringfmt;
956             # my @l=map {
957             # unpack('x'.($off+$_).$sfmt, $$data);
958             # } unpack('x'.($pos+3*$I->_intsize).' '.$I->intfmt.($nkeys+2), $$data);
959              
960             # my $rdata=pop @l;
961             # my $rsort=pop @l;
962              
963             # #warn "data_record: keys=[@l], sort=$rsort, data=$rdata, id=$id\n";
964             # return [\@l, $rsort, $rdata, $id];
965             # }
966              
967             sub iterator {
968 3     3 1 5 my ($I, $show_invalid)=@_;
969              
970 3 100   1   7 return sub {} unless $I->_data;
  1         4  
971              
972 2         6 my $pos=BASEOFFSET+DATASTART*$I->_intsize;
973 2         7 my $end=$I->mainidx;
974              
975             return MMapDB::Iterator->new
976             (sub {
977 260 50   260   521 die E_NOT_IMPLEMENTED if @_;
978 260 100       558 LOOP: {
979 260         306 return if $pos>=$end;
980              
981             # valid id nkeys key1...keyn sort data
982 258         462 my ($valid, undef, $nkeys)=
983 258         835 unpack 'x'.$pos.' '.$I->intfmt.'3', ${$I->_data};
984              
985 258 50 25     1107 if( $valid xor $show_invalid ) {
986 258         367 my $rc=$pos;
987 258         482 $pos+=$I->_intsize*($nkeys+5); # 5=(valid id nkeys sort data)
988 258         936 return $rc;
989             }
990 0         0 $pos+=$I->_intsize*($nkeys+5); # 5=(valid id nkeys sort data)
991 0         0 redo LOOP;
992             }
993 2         21 });
994             }
995              
996             package MMapDB::Iterator;
997              
998 2     2   2973 use strict;
  2         6  
  2         542  
999              
1000             sub new {
1001 2     2   5 my ($class, $func)=@_;
1002 2   33     10 $class=ref($class) || $class;
1003 2         16 return bless $func=>$class;
1004             }
1005              
1006             sub nth {
1007 0     0   0 return $_[0]->(MMapDB::IT_NTH, $_[1]);
1008             }
1009              
1010             sub cur {
1011 0     0   0 return $_[0]->(MMapDB::IT_CUR);
1012             }
1013              
1014             sub nelem {
1015 0     0   0 return $_[0]->(MMapDB::IT_NELEM);
1016             }
1017              
1018             #######################################################################
1019             # High Level Accessor Classes
1020             #######################################################################
1021              
1022             {
1023             package
1024             MMapDB::_base;
1025              
1026 2     2   12 use strict;
  2         52  
  2         105  
1027 2     2   12 use Carp qw/croak/;
  2         84  
  2         155  
1028 2     2   11 use Scalar::Util ();
  2         4  
  2         208  
1029 2     2   12 use Exporter qw/import/;
  2         5  
  2         100  
1030              
1031             use constant ({
1032 2         286 PARENT=>0,
1033             POS=>1,
1034             DATAMODE=>2,
1035             ITERATOR=>3,
1036             SHADOW=>4,
1037 2     2   13 });
  2         10  
1038 2     2   1424 BEGIN {our @EXPORT=(qw!PARENT POS DATAMODE ITERATOR SHADOW!)};
1039              
1040             sub new {
1041 6     6   15 my ($class, @param)=@_;
1042 6   33     26 $class=ref($class) || $class;
1043 6 50       15 $param[DATAMODE]=0 unless defined $param[DATAMODE];
1044 6         22 Scalar::Util::weaken $param[0];
1045 6         29 return bless \@param=>$class;
1046             }
1047              
1048 0     0     sub readonly {croak "Modification of a read-only value attempted";}
1049              
1050 0     0     sub datamode : lvalue {$_[0]->[DATAMODE]}
1051              
1052             BEGIN {
1053 2     2   8 *TIEHASH=\&new;
1054             # STORE must be allowed to support constructs like this (with aliases):
1055             # map {
1056             # local $_;
1057             # } values %{$db->main_index};
1058             # or
1059             # for (values %{$db->main_index}) {
1060             # local $_;
1061             # }
1062             *STORE=sub {
1063 0     0   0 my ($I, $key, $value)=@_;
1064 0         0 my $el;
1065 0         0 my $ll=MMapDB::_localizing();
1066             # Carp::cluck "PL_localizing=$ll";
1067              
1068 0   0     0 $el=($I->[SHADOW]||={});
1069 0         0 my $sh;
1070 0 0 0     0 if( $ll==0 and $sh=$el->{$key} ) { # is already localized
    0          
    0          
1071             # warn " ==> already shadowed";
1072 0         0 $sh->[1]=$value;
1073             } elsif( $ll==1 ) {
1074             # warn " ==> shadowing";
1075 0   0     0 $sh=($el->{$key}||=[]);
1076 0         0 $sh->[0]++;
1077 0         0 $sh->[1]=$value;
1078             } elsif( $ll==2 ) {
1079 0 0       0 if( --$sh->[0] ) {
1080             # warn " ==> decremented shadow counter";
1081 0         0 $sh->[1]=$value;
1082             } else {
1083             # warn " ==> deleting shadow";
1084 0         0 delete $el->{$key};
1085             }
1086             } else {
1087             # warn " ==> ro";
1088 0         0 goto &readonly;
1089             }
1090 2         21 };
1091 2         5 *DELETE=\&readonly;
1092 2         6 *CLEAR=\&readonly;
1093              
1094 2         4 *TIEARRAY=\&new;
1095             #*STORE=sub {};
1096 2         4 *STORESIZE=\&readonly;
1097 2         4 *EXTEND=\&readonly;
1098             #*DELETE=\&readonly;
1099             #*CLEAR=\&readonly;
1100 2         4 *PUSH=\&readonly;
1101 2         4 *UNSHIFT=\&readonly;
1102 2         4 *POP=\&readonly;
1103 2         4 *SHIFT=\&readonly;
1104 2         87 *SPLICE=\&readonly;
1105             }
1106             }
1107              
1108             #######################################################################
1109             # Normal Index Accessor
1110             #######################################################################
1111              
1112             {
1113             package MMapDB::Index;
1114              
1115 2     2   13 use strict;
  2         3  
  2         172  
1116 2     2   1723 BEGIN {MMapDB::_base->import}
1117             {our @ISA=qw/MMapDB::_base/}
1118              
1119             sub FETCH {
1120 0     0     my ($I, $key)=@_;
1121              
1122             {
1123 0           my $shel;
  0            
1124 0 0 0       $shel=$I->[SHADOW] and
      0        
1125             keys %$shel and
1126             $shel=$shel->{$key} and
1127             return $shel->[1];
1128             }
1129              
1130 0           my @el=$I->[PARENT]->index_lookup($I->[POS], $key);
1131              
1132 0 0         return unless @el;
1133              
1134 0           my $rc;
1135              
1136 0 0 0       if( @el==1 and $el[0]>=$I->[PARENT]->mainidx ) {
1137             # another index
1138 0           tie %{$rc={}}, ref($I), $I->[PARENT], $el[0], $I->[DATAMODE];
  0            
1139             } else {
1140 0           tie @{$rc=[]}, 'MMapDB::Data', $I->[PARENT], \@el, $I->[DATAMODE];
  0            
1141             }
1142              
1143 0           return $rc;
1144             }
1145              
1146             sub EXISTS {
1147 0     0     my ($I, $key)=@_;
1148 0 0         return $I->[PARENT]->index_lookup($I->[POS], $key) ? 1 : undef;
1149             }
1150              
1151             sub FIRSTKEY {
1152 0     0     my ($I)=@_;
1153 0           my @el=($I->[ITERATOR]=$I->[PARENT]->index_iterator($I->[POS]))->();
1154 0 0         return @el ? $el[0] : ();
1155             }
1156              
1157             sub NEXTKEY {
1158 0     0     my ($I)=@_;
1159 0           my @el=$I->[ITERATOR]->();
1160 0 0         return @el ? $el[0] : ();
1161             }
1162              
1163             sub SCALAR {
1164 0     0     my ($I)=@_;
1165 0 0         my $pos=defined $I->[POS] ? $I->[POS] : $I->[PARENT]->_ididx;
1166 0           my $n=unpack 'x'.$pos.$I->[PARENT]->intfmt,${$I->[PARENT]->_data};
  0            
1167 0 0         return $n==0 ? $n : "$n/$n";
1168             }
1169             }
1170              
1171             #######################################################################
1172             # ID Index Accessor
1173             #######################################################################
1174              
1175             {
1176             package MMapDB::IDIndex;
1177              
1178 2     2   11 use strict;
  2         5  
  2         137  
1179 2     2   1423 BEGIN {MMapDB::_base->import}
1180             {our @ISA=qw/MMapDB::Index/}
1181              
1182             sub FETCH {
1183             {
1184 0     0     my $shel;
  0            
1185 0 0 0       $shel=$_[0]->[SHADOW] and
      0        
1186             keys %$shel and
1187             $shel=$shel->{$_[1]} and
1188             return $shel->[1];
1189             }
1190              
1191 0 0         if( $_[0]->[DATAMODE]==MMapDB::DATAMODE_SIMPLE ) {
1192 0           $_[0]->[PARENT]->data_value($_[0]->[PARENT]->id_index_lookup($_[1]));
1193             } else {
1194 0           $_[0]->[PARENT]->data_record($_[0]->[PARENT]->id_index_lookup($_[1]));
1195             }
1196             }
1197              
1198             sub EXISTS {
1199 0     0     my ($I, $key)=@_;
1200 0 0         return $I->[PARENT]->id_index_lookup($key) ? 1 : undef;
1201             }
1202              
1203             sub FIRSTKEY {
1204 0     0     my ($I)=@_;
1205 0           my @el=($I->[ITERATOR]=$I->[PARENT]->id_index_iterator)->();
1206 0 0         return @el ? $el[0] : ();
1207             }
1208             }
1209              
1210             #######################################################################
1211             # Data Accessor
1212             #######################################################################
1213              
1214             {
1215             package MMapDB::Data;
1216              
1217 2     2   12 use strict;
  2         3  
  2         136  
1218 2     2   1040 BEGIN {MMapDB::_base->import}
1219             {our @ISA=qw/MMapDB::_base/}
1220              
1221             sub FETCH {
1222 0     0     my ($I, $idx)=@_;
1223              
1224             {
1225 0           my $shel;
  0            
1226 0 0 0       $shel=$I->[SHADOW] and
      0        
1227             keys %$shel and
1228             $shel=$shel->{$idx} and
1229             return $shel->[1];
1230             }
1231              
1232 0 0         return unless @{$I->[POS]}>$idx;
  0            
1233 0 0         if( $I->[DATAMODE]==MMapDB::DATAMODE_SIMPLE ) {
1234 0           return $I->[PARENT]->data_value($I->[POS]->[$idx]);
1235             } else {
1236 0           return $I->[PARENT]->data_record($I->[POS]->[$idx]);
1237             }
1238             }
1239              
1240 0     0     sub FETCHSIZE {scalar @{$_[0]->[POS]}}
  0            
1241              
1242 0     0     sub EXISTS {@{$_[0]->[POS]}>$_[1]}
  0            
1243             }
1244              
1245             1;
1246             __END__