File Coverage

blib/lib/FlatFile/DataStore/DBM.pm
Criterion Covered Total %
statement 170 205 82.9
branch 54 86 62.7
condition 8 12 66.6
subroutine 23 28 82.1
pod 2 10 20.0
total 257 341 75.3


line stmt bran cond sub pod time code
1             #---------------------------------------------------------------------
2             package FlatFile::DataStore::DBM;
3             #---------------------------------------------------------------------
4              
5             =head1 NAME
6              
7             FlatFile::DataStore::DBM - Perl module that implements a flatfile
8             datastore with a DBM file key access.
9              
10             =head1 SYNOPSYS
11              
12             use Fctnl;
13             use FlatFile::DataStore::DBM;
14              
15             $FlatFile::DataStore::DBM::dbm_package = "SDBM_File"; # the defaults
16             $FlatFile::DataStore::DBM::dbm_parms = [ O_CREAT|O_RDWR, 0666 ];
17             $FlatFile::DataStore::DBM::dbm_lock_ext = ".dir";
18              
19             # new object
20              
21             my $obj = tie my %dshash, 'FlatFile::DataStore::DBM', {
22             name => "dsname",
23             dir => "/my/datastore/directory",
24             };
25              
26             # create a record and retrieve it
27              
28             my $id = "testrec1";
29             my $record = $dshash{ $id } = { data => "Test record", user => "Test user data" };
30              
31             # update it
32              
33             $record->data( "Updating the test record." );
34             $dshash{ $id } = $record;
35              
36             # delete it
37              
38             delete $dshash{ $id };
39              
40             # get its history
41              
42             my @records = $obj->history( $id );
43              
44             =head1 DESCRIPTION
45              
46             FlatFile::DataStore::DBM implements a tied hash interface to a
47             flatfile datastore. The hash keys are strings that you provide.
48             These keys do not necessarily have to exist as data in the record.
49              
50             In the case of delete, you're limited in the tied interface -- you
51             can't supply a "delete record" (one that has information about the
52             delete operation). Instead, it will simply retrieve the existing
53             record and store that as the delete record.
54              
55             Record data may be created or updated (i.e., STORE'd) three ways:
56              
57             As a data string (or scalar reference), e.g.,
58              
59             $record = $dshash{ $id } = $record_data;
60              
61             As a hash reference, e.g.
62              
63             $record = $dshash{ $id } = { data => $record_data, user => $user_data };
64              
65             As a record object (record data and user data gotten from object),
66             e.g.,
67              
68             $record->data( $record_data );
69             $record->user( $user_data );
70             $record = $dshash{ $id } = $record;
71              
72             In the last line above, the object fetched is not the same as
73             the one given to be stored (it has a different preamble).
74              
75             FWIW, this module is not a subclass of FlatFile::DataStore. Instead,
76             it is a wrapper, so it's a "has a" relationship rather than an "is a"
77             one. But many of the public flatfile methods are available via the
78             tied object, as illustrated by the history() call in the synopsis.
79              
80             These methods include
81              
82             name
83             dir
84             retrieve
85             retrieve_preamble
86             locate_record_data
87             history
88             userdata
89             howmany
90             lastkeynum
91             nextkeynum
92              
93             Note that create(), update(), and delete() are not included in this
94             list. If a datastore is set up using this module, all updates to its
95             data should use this module. This will keep the keys in sync with
96             the data.
97              
98             =head1 VERSION
99              
100             FlatFile::DataStore::DBM version 1.03
101              
102             =cut
103              
104             our $VERSION = '1.03';
105              
106 2     2   109727 use 5.008003;
  2         10  
  2         114  
107 2     2   12 use strict;
  2         5  
  2         89  
108 2     2   12 use warnings;
  2         3  
  2         107  
109              
110 2     2   12 use Fcntl qw(:DEFAULT :flock);
  2         5  
  2         1186  
111 2     2   14 use Carp;
  2         5  
  2         127  
112              
113 2     2   2259 use FlatFile::DataStore;
  2         6  
  2         6952  
114              
115             #---------------------------------------------------------------------
116              
117             =head1 DESCRIPTION
118              
119             =head2 Tieing the hash
120              
121             Accepts hash ref giving values for C and C.
122              
123             tie my %dshash, 'FlatFile::DataStore::DBM', {
124             name => $name,
125             dir => $dir,
126             };
127              
128             To initialize a new datastore, pass the URI as the value of the
129             C parameter, e.g.,
130              
131             tie my %dshash, 'FlatFile::DataStore::DBM', {
132             dir => $dir,
133             name => $name,
134             uri => join( ";" =>
135             "http://example.com?name=$name",
136             "desc=My%20Data%20Store",
137             "defaults=medium",
138             "user=8-%20-%7E",
139             "recsep=%0A",
140             ),
141             };
142              
143             (See URI Configuration in FlatFile::DataStore.)
144             Also accepts a C parameter, which sets the default user
145             data for this instance.
146              
147             Returns a reference to the FlatFile::DataStore::DBM object.
148              
149             =head2 Object Methods
150              
151             #---------------------------------------------------------------------
152              
153             =head3 get_key( $keynum );
154              
155             Gets the key associated with a record sequence number (keynum).
156             This could be handy if you have a record, but don't have its key
157             in the DBM file, e.g.,
158              
159             # have a record to update, but forgot its key
160             # (the key isn't necessarily in the record)
161            
162             my $id = tied(%dshash)->get_key( $record->keynum );
163             $dshash{ $id } = $record;
164              
165             =cut
166              
167             sub get_key {
168 2     2 1 1274 my( $self, $keynum ) = @_;
169              
170 2 100 66     213 croak qq/Not a keynum: $keynum/
171             unless defined $keynum and $keynum =~ /^[0-9]+$/;
172              
173 1         5 my $ds = $self->datastore;
174 1         5 my $dir = $ds->dir;
175 1         6 my $name = $ds->name;
176              
177             # lock the dbm file and read the key
178 1         4 $self->readlock;
179 1 50       4 tie my %dbm_hash, $self->dbm_package, "$dir/$name", @{$self->dbm_parms}
  1         8  
180             or die "Can't tie dbm hash: $!";
181              
182 1         18 my $key = $dbm_hash{ "_$keynum" };
183              
184 1         21 untie %dbm_hash;
185 1         4 $self->unlock;
186              
187 1         4 $key; # returned
188             }
189              
190             #---------------------------------------------------------------------
191              
192             =head3 get_keynum( $key );
193              
194             Gets the record sequence number (keynum) associated with a key. Don't
195             have a good use case yet -- included this method as a complement to
196             get_key().
197              
198             =cut
199              
200             sub get_keynum {
201 2     2 1 18 my( $self, $key ) = @_;
202              
203 2 100       187 croak qq/Unsupported key format: $key/ if $key =~ /^_[0-9]+$/;
204              
205 1         3 my $ds = $self->datastore;
206 1         4 my $dir = $ds->dir;
207 1         3 my $name = $ds->name;
208              
209             # lock the dbm file and read the keynum
210 1         4 $self->readlock;
211 1 50       4 tie my %dbm_hash, $self->dbm_package, "$dir/$name", @{$self->dbm_parms}
  1         3  
212             or die "Can't tie dbm hash: $!";
213              
214 1         13 my $keynum = $dbm_hash{ $key };
215              
216 1         16 untie %dbm_hash;
217 1         3 $self->unlock;
218              
219 1         4 $keynum; # returned
220             }
221              
222             #---------------------------------------------------------------------
223             # accessors
224             # the following are required attributes, so simple accessors are okay
225             #
226             # Private methods.
227              
228 49 50   49 0 148 sub datastore {for($_[0]->{datastore }){$_=$_[1]if@_>1;return$_}}
  49         129  
  49         140  
229 74 100   74 0 185 sub locked {for($_[0]->{locked }){$_=$_[1]if@_>1;return$_}}
  74         195  
  74         178  
230 74 50   74 0 220 sub dbm_lock_file {for($_[0]->{dbm_lock_file}){$_=$_[1]if@_>1;return$_}}
  74         160  
  74         154  
231 37 50   37 0 88 sub dbm_package {for($_[0]->{dbm_package }){$_=$_[1]if@_>1;return$_}}
  37         98  
  37         128  
232 37 50   37 0 94 sub dbm_parms {for($_[0]->{dbm_parms }){$_=$_[1]if@_>1;return$_}}
  37         85  
  37         1954  
233              
234             #---------------------------------------------------------------------
235             # globals
236             #
237             # These are read in TIEHASH(). They may be changed prior to calling
238             # tie(), e.g.,
239             #
240             # my $ds_parms = { name => $ds_name, dir => $ds_dir };
241             # $FlatFile::DataStore::DBM::dbm_parms = [ O_RDONLY, 0666 ];
242             #
243             # tie my %hash, "FlatFile::DataStore::DBM", $ds_parms;
244             #
245             # ... or different values may be passed to tie() using a hash
246             # reference as the second parameter, e.g.,
247             #
248             # my $ds_parms = { name => $ds_name, dir => $ds_dir };
249             # my $dbm_specs = { dbm_parms => [ O_RDONLY, 0666 ] }
250             #
251             # tie my %hash, "FlatFile::DataStore::DBM", $ds_parms, $dbm_specs;
252             #
253              
254             our $dbm_package = "SDBM_File";
255             our $dbm_parms = [ O_CREAT|O_RDWR, 0666 ];
256             our $dbm_lock_ext = ".dir";
257              
258             #---------------------------------------------------------------------
259             # TIEHASH() supports tied hash access
260             #
261             # Coding note: in TIEHASH(), the object attributes are set directly in
262             # the hash. In all the other subs the above accessors are used.
263             #
264              
265             sub TIEHASH {
266 9     9   10868 my( $class, $ds_parms, $dbm_specs ) = @_;
267              
268 9         75 my $ds = FlatFile::DataStore->new( $ds_parms );
269 8         26 my $dir = $ds->dir;
270 8         28 my $name = $ds->name;
271              
272 8         61 my $self = {
273             datastore => $ds,
274             dbm_package => $dbm_package, # may be changed by dbm_specs
275             dbm_parms => $dbm_parms, # "
276             dbm_lock_ext => $dbm_lock_ext, # "
277             };
278 8 50       29 if( $dbm_specs ) {
279 0         0 $self->{ $_ } = $dbm_specs->{ $_ } for keys %$dbm_specs;
280             }
281 8         40 $self->{'dbm_lock_file'} = "$dir/$name$self->{'dbm_lock_ext'}";
282              
283 8 50       832 eval qq{require $self->{'dbm_package'}; 1}
284             or croak qq/Can't use $self->{'dbm_package'}: $@/;
285              
286 8         81 bless $self, $class;
287             }
288              
289             #---------------------------------------------------------------------
290             # FETCH() supports tied hash access
291             # Returns a FlatFile::DataStore::Record object.
292              
293             sub FETCH {
294 17     17   108 my( $self, $key ) = @_;
295              
296             # block efforts to fetch a "_keynum" entry
297 17 100       262 croak qq/Unsupported key format: $key/ if $key =~ /^_[0-9]+$/;
298              
299 16         51 my $ds = $self->datastore;
300 16         57 my $dir = $ds->dir;
301 16         53 my $name = $ds->name;
302              
303             # lock the dbm file and read the keynum
304 16         55 $self->readlock;
305 16 50       54 tie my %dbm_hash, $self->dbm_package, "$dir/$name", @{$self->dbm_parms}
  16         48  
306             or die "Can't tie dbm hash: $!";
307              
308 16         235 my $keynum = $dbm_hash{ $key };
309              
310 16         288 untie %dbm_hash;
311 16         45 $self->unlock;
312              
313 16 50       40 return unless defined $keynum;
314 16         74 $ds->retrieve( $keynum ); # retrieve and return record
315             }
316              
317             #---------------------------------------------------------------------
318             # STORE() supports tied hash access
319             # Returns a FlatFile::DataStore::Record object.
320             #
321             # to help with FIRSTKEY/NEXTKEY, we're keeping two entries
322             # in the dbm file for every record:
323             # 1. record id => key sequence number
324             # 2. key sequence number => record id
325             #
326             # to avoid collisions with numeric keys, the key of the second
327             # entry has an underscore pasted on to the front, e.g., a record
328             # whose id is "able_baker_charlie" and whose keynum is 257 would
329             # have these entries:
330             # 1. able_baker_charlie => 257
331             # 2. _257 => able_baker_charlie
332             #
333             # Note: the $error variable is intended to avoid having a croak
334             # between writelock() and unlock(). On linux systems that don't
335             # allow a process to have multiple locks on the same file, if you
336             # trap those croaks in an eval{} (like for testing), the program
337             # will hang waiting for a lock.
338             #
339              
340             sub STORE {
341 18     18   3167 my( $self, $key, $parms ) = @_;
342              
343             # block efforts to store to "_keynum" entries
344 18 100       230 croak qq/Unsupported key format: $key/ if $key =~ /^_[0-9]+$/;
345              
346 17         61 my $ds = $self->datastore;
347 17         60 my $dir = $ds->dir;
348 17         59 my $name = $ds->name;
349              
350 17         29 my $error;
351              
352             # lock the dbm file and read the keynum
353 17         59 $self->writelock;
354 17 50       55 tie my %dbm_hash, $self->dbm_package, "$dir/$name", @{$self->dbm_parms}
  17         50  
355             or die "Can't tie dbm hash: $!";
356              
357 17         294 my $keynum = $dbm_hash{ $key };
358              
359             # $parms may be record, href, sref, or string
360 17         53 my $reftype = ref $parms;
361              
362 17         33 my $record; # to be returned
363              
364 17 100       49 if( defined $keynum ) { # update
365              
366             # record data string
367 4 50 33     46 if( !$reftype or $reftype eq "SCALAR" ) {
    100          
    100          
368 0         0 $record = $ds->retrieve( $keynum ); # read it
369 0         0 $record->data( $parms ); # update it
370 0         0 $record = $ds->update( $record ); # write it
371             }
372              
373             # record object
374             elsif( $reftype =~ /Record/ ) {
375              
376             # trying to update a record using the wrong key?
377 2 100       11 if( $keynum != $parms->keynum ) {
378 1         3 $error = qq/Record key number doesn't match key/;
379             }
380             else {
381 1         6 $record = $ds->update( $parms );
382             }
383             }
384              
385             # hash, e.g., {data=>'record data',user=>'user data'}
386             elsif( $reftype eq 'HASH' ) {
387 1 50       9 $parms->{'record'} = $ds->retrieve( $keynum ) unless $parms->{'record'};
388 1         7 $record = $ds->update( $parms );
389             }
390              
391             else {
392 1         4 $error = qq/Unsupported ref type: $reftype/;
393             }
394              
395             }
396              
397             else { # create
398              
399             # record data string
400 13 100 66     137 if( !$reftype or $reftype eq "SCALAR" ) {
    100 100        
401 1         7 $record = $ds->create({ data => $parms });
402             }
403              
404             # record object or hash, e.g.,
405             # { data => 'record data', user => 'user data' }
406             elsif( $reftype =~ /Record/ or
407             $reftype eq 'HASH' ) {
408 11         61 $record = $ds->create( $parms );
409             }
410              
411             else {
412 1         4 $error = qq/Unsupported ref type: $reftype/;
413             }
414              
415             # create succeeded, let's store the key
416 13 100       40 unless( $error ) {
417 12         54 for( $record->keynum ) {
418 12         383 $dbm_hash{ $key } = $_;
419 12         208 $dbm_hash{ "_$_" } = $key;
420             }
421             }
422             }
423              
424 17         354 untie %dbm_hash;
425 17         67 $self->unlock;
426              
427 17 100       451 croak $error if $error;
428              
429 14         149 $record; # returned
430              
431             }
432              
433             #---------------------------------------------------------------------
434             # DELETE() supports tied hash access
435             # Returns a FlatFile::DataStore::Record object.
436             #
437             # Otherwise, we must have a record to delete one, so we retrieve
438             # it first.
439             #
440              
441             sub DELETE {
442 1     1   3 my( $self, $key ) = @_;
443              
444 1         6 my $ds = $self->datastore;
445 1         6 my $dir = $ds->dir;
446 1         4 my $name = $ds->name;
447              
448 1         5 $self->writelock;
449 1 50       5 tie my %dbm_hash, $self->dbm_package, "$dir/$name", @{$self->dbm_parms}
  1         5  
450             or die "Can't tie dbm hash: $!";
451              
452 1         3 my $exists;
453             my $record;
454              
455 1 50       20 if( $exists = exists $dbm_hash{ $key } ) {
456              
457 1         7 my $keynum = $dbm_hash{ $key };
458              
459             # must have a record to delete it
460 1         8 $record = $ds->retrieve( $keynum );
461 1         8 $record = $ds->delete( $record );
462              
463 1         31 delete $dbm_hash{ $key };
464 1         16 delete $dbm_hash{ "_$keynum" };
465             }
466              
467 1         23 untie %dbm_hash;
468 1         6 $self->unlock;
469              
470 1 50       5 return unless $exists;
471 1         8 $record; # return the "delete record"
472             }
473              
474             #---------------------------------------------------------------------
475             # CLEAR() supports tied hash access
476             # except we don't support CLEAR, because it would be very
477             # destructive and it would be a pain to recover from an
478             # accidental %h = ();
479              
480             sub CLEAR {
481 1     1   199 croak qq/Clearing the entire datastore is not supported/;
482             }
483              
484             #---------------------------------------------------------------------
485             # FIRSTKEY() supports tied hash access
486              
487             sub FIRSTKEY {
488 0     0   0 my( $self ) = @_;
489              
490 0         0 my $ds = $self->datastore;
491 0         0 my $dir = $ds->dir;
492 0         0 my $name = $ds->name;
493              
494             # lock the dbm file and read the first key (stored as '_0')
495 0         0 $self->readlock;
496 0 0       0 tie my %dbm_hash, $self->dbm_package, "$dir/$name", @{$self->dbm_parms}
  0         0  
497             or die "Can't tie dbm hash: $!";
498              
499 0         0 my $firstkey = $dbm_hash{ '_0' };
500              
501 0         0 untie %dbm_hash;
502 0         0 $self->unlock;
503              
504 0         0 $firstkey; # returned, might be undef
505             }
506              
507             #---------------------------------------------------------------------
508             # NEXTKEY() supports tied hash access
509              
510             sub NEXTKEY {
511 0     0   0 my( $self, $prevkey ) = @_;
512              
513 0         0 my $ds = $self->datastore;
514 0         0 my $dir = $ds->dir;
515 0         0 my $name = $ds->name;
516              
517 0         0 my $nextkey;
518              
519             # lock the dbm file and get the prev key's keynum
520 0         0 $self->readlock;
521 0 0       0 tie my %dbm_hash, $self->dbm_package, "$dir/$name", @{$self->dbm_parms}
  0         0  
522             or die "Can't tie dbm hash: $!";
523              
524 0         0 my $keynum = $dbm_hash{ $prevkey };
525              
526 0 0       0 if( $keynum++ < $ds->lastkeynum ) {
527 0         0 $nextkey = $dbm_hash{ "_$keynum" };
528             }
529              
530 0         0 untie %dbm_hash;
531 0         0 $self->unlock;
532              
533 0         0 $nextkey; # returned, might be undef
534             }
535              
536             #---------------------------------------------------------------------
537             # SCALAR() supports tied hash access
538             # Here we're bypassing the dbm file altogether and simply getting
539             # the number of non-deleted records in the datastore. This
540             # should be the same as the number of (logical) entries in the
541             # dbm hash.
542              
543             sub SCALAR {
544 0     0   0 my $self = shift;
545 0         0 $self->datastore->howmany; # create|update (not deletes)
546             }
547              
548             #---------------------------------------------------------------------
549             # EXISTS() supports tied hash access
550              
551             sub EXISTS {
552 2     2   24 my( $self, $key ) = @_;
553              
554             # block efforts to look at a "_keynum" entry
555 2 100       191 croak qq/Unsupported key format: $key/ if $key =~ /^_[0-9]+$/;
556              
557 1         4 my $ds = $self->datastore;
558 1 50       5 return unless $ds->exists;
559              
560 1         5 my $dir = $ds->dir;
561 1         5 my $name = $ds->name;
562              
563             # lock the dbm file and call exists on dbm hash
564 1         5 $self->readlock;
565 1 50       4 tie my %dbm_hash, $self->dbm_package, "$dir/$name", @{$self->dbm_parms}
  1         4  
566             or die "Can't tie dbm hash: $!";
567              
568 1         13 my $exists = exists $dbm_hash{ $key };
569              
570 1         19 untie %dbm_hash;
571 1         4 $self->unlock;
572              
573 1 50       11 return unless $exists;
574 0         0 $exists;
575             }
576              
577             #---------------------------------------------------------------------
578             # UNTIE() supports tied hash access
579             # (see perldoc perltie, The "untie" Gotcha)
580              
581             sub UNTIE {
582 0     0   0 my( $self, $count ) = @_;
583 0 0       0 carp "untie attempted while $count inner references still exist" if $count;
584             }
585              
586 0     0   0 sub DESTROY {} # to keep from calling AUTOLOAD
587              
588             #---------------------------------------------------------------------
589             # readlock()
590             # Takes a file name, opens it for input, locks it, and stores the
591             # open file handle in the object. This file handle isn't really
592             # used except for locking, so it's bit of a "lock token"
593             #
594             # Private method.
595              
596             sub readlock {
597 19     19 0 33 my( $self ) = @_;
598              
599 19         47 my $file = $self->dbm_lock_file;
600 19         30 my $fh;
601              
602             # open $fh, '<', $file or croak qq/Can't open for read $file: $!/;
603 19 50       707 sysopen( $fh, $file, O_RDONLY|O_CREAT ) or croak qq/Can't open for read $file: $!/;
604 19 50       139 flock $fh, LOCK_SH or croak qq/Can't lock shared $file: $!/;
605 19         40 binmode $fh;
606              
607 19         49 $self->locked( $fh );
608             }
609              
610             #---------------------------------------------------------------------
611             # writelock()
612             # Takes a file name, opens it for read/write, locks it, and
613             # stores the open file handle in the object.
614             #
615             # Private method.
616              
617             sub writelock {
618 18     18 0 30 my( $self ) = @_;
619              
620 18         72 my $file = $self->dbm_lock_file;
621 18         28 my $fh;
622              
623 18 50       1092 sysopen( $fh, $file, O_RDWR|O_CREAT ) or croak qq/Can't open for read-write $file: $!/;
624 18         82 my $ofh = select( $fh ); $| = 1; select ( $ofh ); # flush buffers
  18         45  
  18         48  
625 18 50       173 flock $fh, LOCK_EX or croak qq/Can't lock exclusive $file: $!/;
626 18         38 binmode $fh;
627              
628 18         67 $self->locked( $fh );
629             }
630              
631             #---------------------------------------------------------------------
632             # unlock()
633             # closes the file handle -- the "lock token" in the object
634             #
635             # Private method.
636              
637             sub unlock {
638 37     37 0 59 my( $self ) = @_;
639              
640 37         98 my $file = $self->dbm_lock_file;
641 37         108 my $fh = $self->locked;
642              
643 37 50       456 close $fh or croak qq/Problem closing $file: $!/;
644             }
645              
646             #---------------------------------------------------------------------
647             our $AUTOLOAD;
648             sub AUTOLOAD {
649              
650 13     13   5663 my $method = $AUTOLOAD;
651 13         70 $method =~ s/.*:://;
652 13         26 for( $method ) {
653 13 100       255 croak qq/Unsupported method: $_/ unless /^
654             name
655             |dir
656             |retrieve
657             |retrieve_preamble
658             |locate_record_data
659             |history
660             |userdata
661             |howmany
662             |lastkeynum
663             |nextkeynum
664             $/x;
665             }
666              
667 12         21 my $self = shift;
668 12         33 $self->datastore->$method( @_ );
669             }
670              
671             1; # returned
672              
673             __END__