File Coverage

blib/lib/DiaColloDB/MultiMapFile.pm
Criterion Covered Total %
statement 21 166 12.6
branch 0 78 0.0
condition 0 56 0.0
subroutine 7 26 26.9
pod 17 18 94.4
total 45 344 13.0


line stmt bran cond sub pod time code
1             ## -*- Mode: CPerl -*-
2             ## File: DiaColloDB::MultiMapFile.pm
3             ## Author: Bryan Jurish <moocow@cpan.org>
4             ## Description: collocation db, integer->integer* multimap file, e.g. for expansion indices
5              
6             package DiaColloDB::MultiMapFile;
7 1     1   9 use DiaColloDB::Logger;
  1         3  
  1         33  
8 1     1   6 use DiaColloDB::Persistent;
  1         2  
  1         25  
9 1     1   6 use DiaColloDB::Utils qw(:fcntl :file :json :pack);
  1         3  
  1         60  
10 1     1   446 use Fcntl qw(:DEFAULT :seek);
  1         3  
  1         37  
11 1     1   472 use File::Basename qw(basename dirname);
  1         3  
  1         30  
12 1     1   697 use version;
  1         2227  
  1         7  
13 1     1   86 use strict;
  1         3  
  1         2647  
14              
15             ##==============================================================================
16             ## Globals & Constants
17              
18             our @ISA = qw(DiaColloDB::Persistent);
19              
20             ##==============================================================================
21             ## Constructors etc.
22              
23             ## $mmf = CLASS_OR_OBJECT->new(%args)
24             ## + %args, object structure:
25             ## (
26             ## ##-- basic options
27             ## base => $base, ##-- database basename; use files "${base}.ma", "${base}.mb", "${base}.hdr"
28             ## perms => $perms, ##-- default: 0666 & ~umask
29             ## flags => $flags, ##-- default: 'r'
30             ## pack_i => $pack_i, ##-- integer pack template (default='N')
31             ## size => $size, ##-- number of mapped , like scalar(@data)
32             ## logCompat => $level, ##-- log-level for compatibility warnings (default='warn')
33             ## ##
34             ## ##-- in-memory construction
35             ## a2b => \@a2b, ##-- maps source integers to (packed) target integer-sets: [$a] => pack("${pack_i}*", @bs)
36             ## ##
37             ## ##-- computed pack templates and lengths (after open())
38             ## pack_a => $pack_a, ##-- "($pack_i)[2]"
39             ## pack_b => $pack_a, ##-- "($pack_i)*"
40             ## len_i => $len_i, ##-- bytes::length(pack($pack_i,0))
41             ## len_a => $len_a, ##-- bytes::length(pack($pack_a,0))
42             ## ##
43             ## ##-- filehandles (after open())
44             ## afh => $afh, ##-- $base.ma : [$a] => pack(${pack_a}, $bidx_a, $blen_a) : $byte_offset_in_bfh = $len_i*$bidx_a
45             ## bfh => $bfh, ##-- $base.mb : [$bidx_a] => pack(${pack_b}, @targets_for_a) : $byte_length_in_bfh = $len_i*$blen_a
46             ## )
47             sub new {
48 0     0 1   my $that = shift;
49 0   0       my $mmf = bless({
50             base => undef,
51             perms => (0666 & ~umask),
52             flags => 'r',
53             size => 0,
54             pack_i => 'N',
55             version => $DiaColloDB::VERSION,
56             logCompat => 'warn',
57              
58             a2b=>[],
59              
60             #len_i => undef,
61              
62             #afh =>undef,
63             #bfh =>undef,
64              
65             @_, ##-- user arguments
66             },
67             ref($that)||$that);
68 0           $mmf->{class} = ref($mmf);
69 0   0       $mmf->{a2b} //= [];
70 0 0         return defined($mmf->{base}) ? $mmf->open($mmf->{base}) : $mmf;
71             }
72              
73             sub DESTROY {
74 0 0   0     $_[0]->close() if ($_[0]->opened);
75             }
76              
77             ##==============================================================================
78             ## I/O
79              
80             ##--------------------------------------------------------------
81             ## I/O: open/close (file)
82              
83             ## $mmf_or_undef = $mmf->open($base,$flags)
84             ## $mmf_or_undef = $mmf->open($base)
85             ## $mmf_or_undef = $mmf->open()
86             sub open {
87 0     0 1   my ($mmf,$base,$flags) = @_;
88 0   0       $base //= $mmf->{base};
89 0   0       $flags //= $mmf->{flags};
90 0 0         $mmf->close() if ($mmf->opened);
91 0           $mmf->{base} = $base;
92 0           $mmf->{flags} = $flags = fcflags($flags);
93 0           my ($hdr); ##-- save header, for version-checking
94 0 0 0       if (fcread($flags) && !fctrunc($flags)) {
95 0 0         $hdr = $mmf->readHeader()
96             or $mmf->logconess("failed to read header data from '$mmf->{base}.hdr': $!");
97 0 0         $mmf->loadHeaderData($hdr)
98             or $mmf->logconess("failed to instantiate header from '$mmf->{base}.hdr': $!");
99             }
100              
101             ##-- check compatibility
102 0           my $min_version = qv(0.09.000);
103 0 0 0       if ($hdr && (!defined($hdr->{version}) || version->parse($hdr->{version}) < $min_version)) {
      0        
104 0           $mmf->vlog($mmf->{logCompat}, "using compatibility mode for $mmf->{base}.*; consider running \`dcdb-upgrade.perl ", dirname($mmf->{base}), "\'");
105 0           DiaColloDB::Compat->usecompat('v0_08');
106 0           bless($mmf, 'DiaColloDB::Compat::v0_08::MultiMapFile');
107 0           $mmf->{version} = $hdr->{version};
108 0           return $mmf->open($base,$flags);
109             }
110              
111             ##-- open underlying files
112             $mmf->{afh} = fcopen("$base.ma", $flags, $mmf->{perms})
113 0 0         or $mmf->logconfess("open failed for $base.ma: $!");
114             $mmf->{bfh} = fcopen("$base.mb", $flags, $mmf->{perms})
115 0 0         or $mmf->logconfess("open failed for $base.mb: $!");
116 0           binmode($_,':raw') foreach (@$mmf{qw(afh bfh)});
117              
118             ##-- computed pack-templates & lengths
119 0           $mmf->{pack_a} = $mmf->{pack_i}."[2]";
120 0           $mmf->{pack_b} = $mmf->{pack_i}."*";
121 0           $mmf->{len_i} = packsize($mmf->{pack_i});
122 0           $mmf->{len_a} = packsize($mmf->{pack_a});
123              
124 0           return $mmf;
125             }
126              
127             ## $mmf_or_undef = $mmf->close()
128             sub close {
129 0     0 1   my $mmf = shift;
130 0 0 0       if ($mmf->opened && fcwrite($mmf->{flags})) {
131 0 0         $mmf->flush() or return undef;
132             }
133 0 0 0       !defined($mmf->{afh}) or $mmf->{afh}->close() or return undef;
134 0 0 0       !defined($mmf->{bfh}) or $mmf->{bfh}->close() or return undef;
135             $mmf->{a2b} //= [],
136 0   0       undef $mmf->{base};
137 0           return $mmf;
138             }
139              
140             ## $bool = $mmf->opened()
141             sub opened {
142 0     0 1   my $mmf = shift;
143             return
144             (
145             #defined($mmf->{base}) &&
146             defined($mmf->{afh})
147             && defined($mmf->{bfh})
148 0   0       );
149             }
150              
151             ## $bool = $enum->reopen()
152             ## + re-opens datafiles
153             sub reopen {
154 0     0 0   my $mmf = shift;
155 0   0       my $base = $mmf->{base} || "$mmf";
156             return (
157             $mmf->opened
158             && fh_reopen($mmf->{afh}, "$base.ma")
159 0   0       && fh_reopen($mmf->{bfh}, "$base.mb")
160             );
161             }
162              
163              
164             ## $bool = $mmf->dirty()
165             ## + returns true iff some in-memory structures haven't been flushed to disk
166             sub dirty {
167 0     0 1   return @{$_[0]{a2b}};
  0            
168             }
169              
170             ## $bool = $mmf->flush()
171             ## + flush in-memory structures to disk
172             ## + clobbers any old disk-file contents with in-memory maps
173             ## + file must be opened in write-mode
174             ## + invalidates any old references to {a2b} (but doesn't empty them if you need to keep a reference)
175             sub flush {
176 0     0 1   my $mmf = shift;
177 0 0 0       return undef if (!$mmf->opened || !fcwrite($mmf->{flags}));
178 0 0         return $mmf if (!$mmf->dirty);
179              
180             ##-- save header
181 0 0         $mmf->saveHeader()
182             or $mmf->logconfess("flush(): failed to store header $mmf->{base}.hdr: $!");
183              
184             #use bytes; ##-- deprecated in perl v5.18.2
185 0           my ($afh,$bfh) = @$mmf{qw(afh bfh)};
186 0           $afh->seek(0,SEEK_SET);
187 0           $bfh->seek(0,SEEK_SET);
188              
189             ##-- dump datafiles $base.ma, $base.mb
190 0           my ($a2b,$pack_a,$len_i) = @$mmf{qw(a2b pack_a len_i)};
191 0           my $bidx = 0;
192 0           my $ai = 0;
193 0           my ($blen);
194 0           foreach (@$a2b) {
195 0   0       $_ //= '';
196 0           $blen = length($_)/$len_i;
197 0 0         $afh->print(pack($pack_a, $bidx, $blen))
198             or $mmf->logconfess("flush(): failed to write source record for a=$ai to $mmf->{base}.ma");
199 0 0         $bfh->print($_)
200             or $mmf->logconfess("flush(): failed to write targets for a=$ai to $mmf->{base}.mb");
201 0           ++$ai;
202 0           $bidx += $blen;
203             }
204              
205             ##-- truncate datafiles at current position
206 0           CORE::truncate($afh, $afh->tell());
207 0           CORE::truncate($bfh, $bfh->tell());
208              
209             ##-- clear in-memory structures (but don't clobber existing references)
210 0           $mmf->{a2b} = [];
211              
212 0 0 0       $mmf->reopen() or return undef if ((caller(1))[3] !~ /::close$/);
213 0           return $mmf;
214             }
215              
216              
217             ##--------------------------------------------------------------
218             ## I/O: memory <-> file
219              
220             ## \@a2b = $mmf->toArray()
221             sub toArray {
222 0     0 1   my $mmf = shift;
223 0 0         return $mmf->{a2b} if (!$mmf->opened);
224              
225             #use bytes; ##-- deprecated in perl v5.18.2
226 0           my ($afh,$bfh,$pack_a,$len_a,$pack_i,$len_i) = @$mmf{qw(afh bfh pack_a len_a pack_i len_i)};
227 0           my @a2b = qw();
228              
229             ##-- ye olde loope
230 0           my ($bidx,$blen,$buf);
231 0           for (CORE::seek($afh,0,SEEK_SET); !eof($afh); ) {
232             ##-- get position, length
233 0 0         CORE::read($afh,$buf,$len_a)==$len_a
234             or $mmf->logconfess("toArray(): read() failed for $mmf->{base}.ma item ", scalar(@a2b));
235 0           ($bidx,$blen) = unpack($pack_a,$buf);
236              
237             ##-- read targets
238 0           $blen *= $len_i;
239 0           CORE::seek($bfh,$bidx*$len_i,SEEK_SET);
240 0 0         CORE::read($bfh,$buf,$blen)==$blen
241             or $mmf->logconfess("toArray(): read() failed for $blen byte(s) on $mmf->{base}.mb at logical record $bidx, item ", scalar(@a2b));
242 0           push(@a2b,$buf);
243             }
244 0 0         push(@a2b, @{$mmf->{a2b}}[scalar(@a2b)..$#{$mmf->{a2b}}]) if ($mmf->dirty);
  0            
  0            
245 0           return \@a2b;
246             }
247              
248             ## $mmf = $mmf->fromArray(\@a2b)
249             ## + clobbers $mmf contents, steals \@a2b
250             sub fromArray {
251 0     0 1   my ($mmf,$a2b) = @_;
252 0           $mmf->{a2b} = $a2b;
253 0           $mmf->{size} = scalar(@{$mmf->{a2b}});
  0            
254 0           return $mmf;
255             }
256              
257             ## $bool = $mmf->load()
258             ## + loads files to memory; must be opened
259             sub load {
260 0     0 1   my $mmf = shift;
261 0           return $mmf->fromArray($mmf->toArray);
262             }
263              
264             ## $mmf = $mmf->save()
265             ## $mmf = $mmf->save($base)
266             ## + saves multimap to $base; really just a wrapper for open() and flush()
267             sub save {
268 0     0 1   my ($mmf,$base) = @_;
269 0 0         $mmf->open($base,'rw') if (defined($base));
270 0 0         $mmf->logconfess("save(): cannot save un-opened multimap") if (!$mmf->opened);
271 0 0         $mmf->flush() or $mmf->logconfess("save(): failed to flush to $mmf->{base}: $!");
272 0           return $mmf;
273             }
274              
275              
276             ##--------------------------------------------------------------
277             ## I/O: header
278             ## + see also DiaColloDB::Persistent
279              
280             ## @keys = $coldb->headerKeys()
281             ## + keys to save as header
282             sub headerKeys {
283 0   0 0 1   return (qw(version), grep {!ref($_[0]{$_}) && $_ !~ m{^(?:flags|perms|base|version)$}} keys %{$_[0]});
  0            
  0            
284             }
285              
286             ## $bool = $CLASS_OR_OBJECT->loadHeader()
287             ## + wraps $CLASS_OR_OBJECT->loadHeaderFile($CLASS_OR_OBJ->headerFile())
288             ## + INHERITED from DiaColloDB::Persistent
289              
290             ## $bool = $mmf->loadHeaderData($hdr)
291             sub loadHeaderData {
292 0     0 1   my ($mmf,$hdr) = @_;
293 0 0 0       if (!defined($hdr) && (fcflags($mmf->{flags})&O_CREAT) != O_CREAT) {
    0          
294 0           $mmf->logconfess("loadHeader() failed to load '$mmf->{base}.hdr': $!");
295             }
296             elsif (defined($hdr)) {
297 0           return $mmf->SUPER::loadHeaderData($hdr);
298             }
299 0           return $mmf;
300             }
301              
302             ## $bool = $enum->saveHeader()
303             ## + inherited from DiaColloDB::Persistent
304              
305             ##--------------------------------------------------------------
306             ## I/O: text
307              
308             ## $bool = $obj->loadTextFile($filename_or_handle, %opts)
309             ## + wraps loadTextFh()
310             ## + INHERITED from DiaColloDB::Persistent
311              
312             ## $mmf = $CLASS_OR_OBJECT->loadTextFh($fh)
313             ## + loads from text file with lines of the form "A B1 B2..."
314             ## + clobbers multimap contents
315             sub loadTextFh {
316 0     0 1   my ($mmf,$fh,%opts) = @_;
317 0 0         $mmf = $mmf->new(%opts) if (!ref($mmf));
318              
319 0           my $pack_b = $mmf->{pack_b};
320 0           my @a2b = qw();
321 0           my ($a,@b);
322 0           while (defined($_=<$fh>)) {
323 0           chomp;
324 0 0 0       next if (/^%%/ || /^$/);
325 0           ($a,@b) = split(' ',$_);
326 0           $a2b[$a] = pack($pack_b, @b);
327             }
328              
329             ##-- clobber multimap
330 0           return $mmf->fromArray(\@a2b);
331             }
332              
333             ## $bool = $obj->saveTextFile($filename_or_handle, %opts)
334             ## + wraps saveTextFh()
335             ## + INHERITED from DiaColloDB::Persistent
336              
337             ## $bool = $mmf->saveTextFh($filename_or_fh,%opts)
338             ## + save from text file with lines of the form "A B1 B2..."
339             ## + %opts:
340             ## a2s=>\&a2s ##-- stringification code for A items, called as $s=$a2s->($bi)
341             ## b2s=>\&b2s ##-- stringification code for B items, called as $s=$b2s->($bi)
342             sub saveTextFh {
343 0     0 1   my ($mmf,$fh,%opts) = @_;
344              
345 0           my $a2s = $opts{a2s};
346 0           my $b2s = $opts{b2s};
347 0           my $pack_b = $mmf->{pack_b};
348 0           my $a2b = $mmf->toArray;
349 0           my $a = 0;
350 0           foreach (@$a2b) {
351 0 0         if (defined($_)) {
352             $fh->print(($a2s ? $a2s->($a) : $a),
353             "\t",
354             join(' ',
355             ($b2s
356 0 0         ? (map {$b2s->($_)} unpack($pack_b,$_))
  0 0          
357             : unpack($pack_b, $_))),
358             "\n");
359             }
360 0           ++$a;
361             }
362              
363 0           return $mmf;
364             }
365              
366              
367             ##==============================================================================
368             ## Methods: population (in-memory only)
369              
370             ## $newsize = $mmf->addPairs($a,@bs)
371             ## $newsize = $mmf->addPairs($a,\@bs)
372             ## + adds mappings $a=>$b foreach $b in @bs
373             ## + multimap must be loaded to memory
374             sub addPairs {
375 0     0 1   my $mmf = shift;
376 0           my $a = shift;
377 0 0         my $bs = UNIVERSAL::isa($_[0],'ARRAY') ? $_[0] : \@_;
378 0           $mmf->{a2b}[$a] .= pack($mmf->{pack_b}, @$bs);
379 0           return $mmf->{size} = scalar(@{$mmf->{a2b}});
  0            
380             }
381              
382             ##==============================================================================
383             ## Methods: lookup
384              
385             ## $bs_packed = $mmf->fetchraw($a)
386             ## + returns packed array \@bs of targets for $a, or undef if not found
387             sub fetchraw {
388 0     0 1   my ($mmf,$a) = @_;
389 0 0         return '' if (!defined($a));
390              
391 0           my ($boff,$blen,$buf);
392 0 0         CORE::seek($mmf->{afh}, $a*$mmf->{len_a}, SEEK_SET)
393             or $mmf->logconfess("fetch(): seek() failed on $mmf->{base}.ma for a=$a");
394             CORE::read($mmf->{afh},$buf,$mmf->{len_a})==$mmf->{len_a}
395 0 0         or $mmf->logconfess("fetch(): read() failed on $mmf->{base}.ma for a=$a");
396 0           ($boff,$blen) = unpack($mmf->{pack_a}, $buf);
397              
398 0           $boff *= $mmf->{len_i};
399 0           $blen *= $mmf->{len_i};
400 0 0         CORE::seek($mmf->{bfh}, $boff, SEEK_SET)
401             or $mmf->logconfess("fetch(): seek() failed on $mmf->{base}.mb to offset $boff for a=$a");
402 0 0         CORE::read($mmf->{bfh}, $buf, $blen)==$blen
403             or $mmf->logconfess("fetch(): read() failed on $mmf->{base}.mb for target-set of $blen byte(s) at offset $boff for a=$a");
404              
405 0           return $buf;
406             }
407              
408             ## \@bs_or_undef = $mmf->fetch($a)
409             ## + returns array \@bs of targets for $a, or undef if not found
410             ## + multimap must be opened
411             sub fetch {
412 0     0 1   return [unpack($_[0]{pack_b}, $_[0]->fetchraw(@_[1..$#_]))];
413             }
414              
415             ##==============================================================================
416             ## Footer
417             1;
418              
419             __END__
420              
421              
422              
423