File Coverage

blib/lib/Boulder/Store.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             # $Id: Store.pm,v 1.6 2002/06/28 20:31:59 lstein Exp $
2              
3             # Prototype support library for storing Boulder streams.
4             # Basic design is as follows:
5             # The "data" file, named .records contains
6             # a recno style data file. Records are delimited by
7             # newlines. Each record has this form:
8              
9             # tag=long string value&tag=long string value...
10             # Subrecords are delimited by {} pairs as per the
11             # usual boulderio format.
12              
13             # The "index" file, named .index, is a DB_Hash
14             # that contains several things:
15             # 1. Indexes. The key is used to translate
16             # from index to the list of record entries.
17             # 2. Other information:
18             # .INDICES -- list of tags that are indexed.
19              
20             package Boulder::Store;
21              
22             =head1 NAME
23              
24             Boulder::Store - Simple persistent storage for Stone tag/value objects
25              
26             =head1 SYNOPSIS
27              
28             Boulder:Store;
29              
30             my $store=new Boulder::Store('test.db',1);
31             my $s = new Stone (Name=>'george',
32             Age=>23,
33             Sex=>M,
34             Address=>{
35             Street=>'29 Rockland drive',
36             Town=>'Fort Washington',
37             ZIP=>'77777'
38             }
39             );
40             $store->put($s);
41             $store->put(new Stone(Name=>'fred',
42             Age=>30,
43             Sex=>M,
44             Address=>{
45             Street=>'19 Gravel Path',
46             Town=>'Bedrock',
47             ZIP=>'12345'},
48             Phone=>{
49             Day=>'111-1111',
50             Eve=>'222-2222'
51             }
52             ));
53             $store->put(new Stone(Name=>'andrew',
54             Age=>18,
55             Sex=>M));
56              
57             $store->add_index('Name');
58              
59             my $stone = $store->get(0);
60             print "name = ",$stone->Name;
61              
62             =head1 DESCRIPTION
63              
64             Boulder::Store provides persistent storage for Boulder objects using a
65             simple DB_File implementation. To use it, you need to have Berkeley
66             db installed (also known as libdb), and the Perl DB_File module. See
67             the DB_File package for more details on obtaining Berkeley db if you
68             do not already have it.
69              
70             Boulder::Store provides an unsophisticated query mechanism which takes
71             advantage of indexes that you specify. Despite its lack of
72             sophistication, the query system is often very helpful.
73              
74             =head1 CLASS METHODS
75              
76             =over 4
77              
78             =item $store = Boulder::Store->new("database/path",$writable)
79              
80             The B method creates a new Boulder::Store object and associates
81             it with the database file provided in the first parameter (undef is a
82             valid pathname, in which case all methods work but the data isn't
83             stored). The second parameter should be a B value if you want
84             to open the database for writing. Otherwise it's opened read only.
85              
86             Because the underlying storage implementation is not multi-user, only
87             one process can have the database for writing at a time. A
88             B-based locking mechanism is used to give a process that has
89             the database opened for writing exclusive access to the database.
90             This also prevents the database from being opened for reading while
91             another process is writing to it (this is a B thing). Multiple
92             simultaneous processes can open the database read only.
93              
94             Physically the data is stored in a human-readable file with the
95             extension ".data".
96              
97             =back
98              
99             =head1 OBJECT METHODS
100              
101             =over 4
102              
103             =item $stone = $store->read_record(@taglist)
104              
105             The semantics of this call are exactly the same as in
106             B. Stones are returned in sequential order, starting
107             with the first record. In addition to their built-in tags, each stone
108             returned from this call has an additional tag called "record_no".
109             This is the zero-based record number of the stone in the database.
110             Use the B method to begin iterating from the beginning of the
111             database.
112              
113             If called in an array context, B returns a list of all
114             stones in the database that contains one or more of the provided tags.
115              
116             =item $stone = $store->write_record($stone [,$index])
117              
118             This has the same semantics as B. A stone is
119             appended to the end of the database. If successful, this call returns
120             the record number of the new entry. By providing an optional second
121             parameter, you can control where the stone is entered. A positive
122             numeric index will write the stone into the database at that position.
123             A value of -1 will use the Stone's internal record number (if present)
124             to determine where to place it.
125              
126             =item $stone = $store->get($record_no)
127              
128             This is random access to the database. Provide a record number and
129             this call will return the stone stored at that position.
130              
131             =item $record_number = $store->put($stone,$record_no)
132              
133             This is a random write to the database. Provide a record number and
134             this call stores the stone at the indicated position, replacing whatever
135             was there before.
136              
137             If no record number is provided, this call will look for the presence
138             of a 'record_no' tag in the stone itself and put it back in that
139             position. This allows you to pull a stone out of the database, modify
140             it, and then put it back in without worrying about its record number.
141             If no record is found in the stone, then the effect is identical to
142             write_record().
143              
144             The record number of the inserted stone is returned from this call, or
145             -1 if an error occurred.
146              
147             =item $store->delete($stone),Boulder::Store::delete($record_no)
148              
149             These method calls delete a stone from the database. You can provide
150             either the record number or a stone containing the 'record_no' tag.
151             B: if the database is heavily indexed deletes can be
152             time-consuming as it requires the index to be brought back into synch.
153              
154             =item $record_count = $store->length()
155              
156             This returns the length of the database, in records.
157              
158             =item $store->reset()
159              
160             This resets the database, nullifying any queries in effect, and
161             causing read_record() to begin fetching stones from the first record.
162              
163             =item $store->query(%query_array)
164              
165             This creates a query on the database used for selecting stones in
166             B. The query is an associative array. Three types of
167             keys/value pairs are allowed:
168              
169             =over 4
170              
171             =item (1) $index=>$value
172              
173             This instructs Boulder::Store to look for stones containing the
174             specified tags in which the tag's value (determined by the Stone
175             B method) exactly matches the provided
176             value. Example:
177              
178             $db->query('STS.left_primer.length'=>30);
179              
180             Only the non-bracketed forms of the index string are allowed (this
181             is probably a bug...)
182              
183             If the tag path was declared to be an index, then this search
184             will be fast. Otherwise Boulder::Store must iterate over every
185             record in the database.
186              
187             =item (2) EVAL=>'expression'
188              
189             This instructs Boulder::Store to look for stones in which the
190             provided expression evaluates to B. When the expression
191             is evaluated, the variable B<$s> will be set to the current
192             record's stone. As a shortcut, you can use ""
193             as shorthand for "$s->index('index.string')".
194              
195             =item (3) EVAL=>['expression1','expression2','expression3'...]
196              
197             This lets you provide a whole bunch of expressions, and is exactly
198             equivalent to EVAL=>'(expression1) && (expression2) && (expression3)'.
199              
200             =back
201              
202             You can mix query types in the parameter provided to B.
203             For example, here's how to look up all stones in which the sex is
204             male and the age is greater than 30:
205              
206             $db->query('sex'=>'M',EVAL=>' > 30');
207              
208             When a query is in effect, B returns only Stones
209             that satisfy the query. In an array context, B
210             returns a list of all Stones that satisfy the query. When no
211             more satisfactory Stones are found, B returns
212             B until a new query is entered or B is called.
213              
214             =item $store->add_index(@indices)
215              
216             Declare one or more tag paths to be a part of a fast index.
217             B will take advantage of this record when processing
218             queries. For example:
219              
220             $db->add_index('age','sex','person.pets');
221              
222             You can add indexes any time you like, when the database is first
223             created or later. There is a trade off: B,
224             B, and other data-modifying calls will become slower as
225             more indexes are added.
226              
227             The index is stored in an external file with the extension ".index".
228             An index file is created even if you haven't indexed any tags.
229              
230             =item $store->reindex_all()
231              
232             Call this if the index gets screwed up (or lost). It rebuilds it
233             from scratch.
234              
235             =back
236              
237             =head1 CAVEATS
238              
239             Boulder::Store makes heavy use of the flock() call in order to avoid
240             corruption of DB_File databases when multiple processes try to write
241             simultaneously. flock() may not work correctly across NFS mounts,
242             particularly on Linux machines that are not running the rpc.lockd
243             daemon. Please confirm that your flock() works across NFS before
244             attempting to use Boulder::Store. If the store.t test hangs during
245             testing, this is the likely culprit.
246              
247             =head1 AUTHOR
248              
249             Lincoln D. Stein , Cold Spring Harbor Laboratory,
250             Cold Spring Harbor, NY. This module can be used and distributed on
251             the same terms as Perl itself.
252              
253             =head1 SEE ALSO
254              
255             L, L, L
256              
257             =cut
258              
259 1     1   4182 use Boulder::Stream;
  1         5  
  1         39  
260 1     1   5 use Carp;
  1         2  
  1         78  
261 1     1   6 use Fcntl;
  1         1  
  1         303  
262 1     1   1793 use DB_File;
  0            
  0            
263              
264             $VERSION = '1.20';
265              
266             @ISA = 'Boulder::Stream';
267             $lockfh='lock00000';
268             $LOCK_SH = 1;
269             $LOCK_EX = 2;
270             $LOCK_UN = 8;
271              
272             # Override the old new() method.
273             # There is no passthrough behavior in the database version,
274             # because this is usually undesirable.
275             # In this case,$in is the pathname to the database to open.
276             sub new {
277             my($package,$in,$writable) = @_;
278             my $self = bless {
279             'records'=>undef, # filled in by _open_databases
280             'dbrecno'=>undef, # filled in by _open_databases
281             'index'=>undef, # filled in by _open_databases
282             'writable'=>$writable,
283             'basename'=>$in,
284             'passthru'=>undef,
285             'binary'=>'true',
286             'nextrecord'=>0, # next record to retrieve during iterations
287             'query_records'=>undef, # list of records during optimized queries
288             'query_test'=>undef, # an expression to apply to each record during a query
289             'IN'=>undef,
290             'OUT'=>undef,
291             'delim'=>'=',
292             'record_stop'=>"\n",
293             'line_end'=>'&',
294             'index_delim'=>' ',
295             'subrec_start'=>"\{",
296             'subrec_end'=>"\}"
297             },$package;
298             return undef unless _lock($self,'lock');
299             return _open_databases($self,$in) ? $self : undef;
300             }
301              
302             sub DESTROY {
303             my $self = shift;
304             undef $self->{'dbrecno'};
305             untie %{$self->{'index'}};
306             untie @{$self->{'records'}};
307             _lock($self,'unlock');
308             }
309              
310             #####################
311             # private routines
312             ####################
313             # Obtain exclusive privileges if database is
314             # writable. Otherwise obtain shared privileges.
315             # Note that this call does not work across file systems,
316             # at least on non-linux systems. Should use fcntl()
317             # instead (but don't have Stevens at hand).
318             sub _lock {
319             my($self,$lockit) = @_;
320             my $in = $self->{'basename'};
321             my $lockfilename = "$in.lock";
322             if ($lockit eq 'lock') {
323             $lockfh++;
324             open($lockfh,"+>$lockfilename") || return undef;
325             $self->{'lockfh'}=$lockfh;
326             return flock($lockfh,$self->{'writable'} ? $LOCK_EX : $LOCK_SH);
327             } else {
328             my $lockfh = $self->{'lockfh'};
329             unlink $lockfilename;
330             flock($lockfh,$LOCK_UN);
331             close($lockfh);
332             1;
333             }
334             }
335              
336             sub _open_databases {
337             my $self = shift;
338              
339             # Try to open up and/or create the recno and index files
340             my($in)=$self->{'basename'};
341             my (@records,%index);
342             my ($permissions) = $self->{'writable'} ? (O_RDWR|O_CREAT) : O_RDONLY;
343             $self->{'dbrecno'} = tie(@records,DB_File,"$in.data",
344             $permissions,0640,$DB_RECNO) || return undef;
345             tie(%index,DB_File,"$in.index",$permissions,0640,$DB_HASH) || return undef;
346              
347             $self->{'records'}=\@records;
348             $self->{'index'}=\%index;
349             1;
350             }
351              
352             #########################################################################
353             # DELETE EVERYTHING FROM THE DATABASE
354             #########################################################################
355             sub empty {
356             my $self = shift;
357             my($base) = $self->{'basename'};
358             &DESTROY($self); # this closes the database and releases locks
359              
360             # delete the files
361             foreach ('.data','.index') {
362             unlink "$base$_";
363             }
364            
365             # Now reopen things
366             return _open_databases($self);
367             }
368              
369             ########################################################################
370             # DATA STORAGE
371             ########################################################################
372             # This overrides the base object write_record.
373             # It writes the stone into the given position in the file.
374             # You can provide an index to put the record at a particular
375             # position, leave it undef to append the record to the end
376             # of the table, or provide a -1 to use the current record
377             # number of the stone to get the position. Just for fun,
378             # we return the record number of the added object.
379             sub write_record {
380             my($self,$stone,$index) = @_;
381             unless ($self->{'writable'}) {
382             warn "Attempt to write to read-only database $self->{'basename'}";
383             return undef;
384             }
385              
386             my ($nextrecord);
387              
388             if (defined($index) && $index == -1) {
389             my $stonepos = $stone->get('record_no');
390             $nextrecord = defined($stonepos) ? $stonepos : $self->length;
391             } else {
392             $nextrecord = (defined($index) && ($index >= 0) && ($index < $self->length))
393             ? $index : $self->length;
394             }
395              
396             # We figure out here what indices need to be updated
397             my %need_updating; # indexes that need fixing
398             if ($nextrecord != $self->length) {
399             my $old = $self->get($nextrecord);
400             if ($old) {
401             foreach ($self->indexed_keys) {
402             my $oldvalue = join('',$old->index($_));
403             my $newvalue = join('',$stone->index($_));
404             $need_updating{$_}++ if $oldvalue ne $newvalue;
405             }
406             }
407             $self->unindex_record($nextrecord,keys %need_updating) if %need_updating;
408             } else {
409             grep($need_updating{$_}++,$self->indexed_keys);
410             }
411              
412             # Write out the Stone record.
413             $stone->replace('record_no',$nextrecord); # keep track of this please
414             my ($key,$value,@value,@lines);
415              
416             foreach $key ($stone->tags) {
417             @value = $stone->get($key);
418             $key = $self->escapekey($key);
419             foreach $value (@value) {
420             if (ref $value && defined $value->{'.name'}) {
421             $value = $self->escapeval($value);
422             push(@lines,"$key$self->{delim}$value");
423             } else {
424             push(@lines,"$key$self->{delim}$self->{subrec_start}");
425             push(@lines,_write_nested($self,1,$value));
426             }
427             }
428             }
429             $self->{'records'}->[$nextrecord]=join("$self->{line_end}",@lines);
430             $self->index_record($nextrecord,keys %need_updating) if %need_updating;
431              
432             $nextrecord;
433             }
434              
435             # put() is an alias for write_record, except that it
436             # requires a record number.
437             sub put {
438             my($self,$stone,$record_no) = @_;
439             croak 'Usage: put($stone [,$record_no])' unless defined $stone;
440             $record_no = $stone->get('record_no') unless defined($record_no);
441             $self->write_record($stone,$record_no);
442             }
443              
444             # Delete the record number from the database. You may
445             # provide either a numeric recno, or the stone itself.
446             # The deleted stone is returned (sans its record no).
447             sub delete {
448             my($self,$s) = @_;
449             my $recno;
450             if ( $s->isa('Stone') ) {
451             $recno = $s->get('record_no');
452             } else {
453             $recno = $s;
454             }
455             $self->unindex_record($recno); # remove from the index
456             $s = $self->get($recno) unless $s->isa('Stone');
457             delete $s->{recno}; # record number is gonzo
458             $self->{'dbrecno'}->del($recno); # this does the actual delete
459             $self->renumber_indices($recno);
460             $self->renumber_records($recno);
461             return $s;
462             }
463              
464             ########################################################################
465             # DATA RETRIEVAL
466             ########################################################################
467             sub read_one_record {
468             my($self,@keywords) = @_;
469              
470             return undef if $self->done;
471              
472             my(%interested,$key,$value);
473             grep($interested{$_}++,@keywords);
474             $interested{'record_no'}++; # always interested in this one
475              
476             my $delim=$self->{'delim'};
477             my $subrec_start=$self->{'subrec_start'};
478             my $subrec_end=$self->{'subrec_end'};
479             my ($stone,$pebble,$found);
480              
481             while (1) {
482            
483             undef $self->{LEVEL},last unless $_ = $self->next_pair;
484              
485             if (/$subrec_end$/o) {
486             $self->{LEVEL}--,last if $self->{LEVEL};
487             next;
488             }
489              
490             next unless ($key,$value) = split($self->{delim},$_);
491             $key = $self->unescapekey($key);
492             $stone = new Stone() unless $stone;
493              
494             if ((!@keywords) || $interested{$key}) {
495              
496             $found++;
497             if ($value =~ /$subrec_start/o) {
498             $self->{LEVEL}++;
499             $pebble = read_one_record($self); # call ourselves recursively
500             $stone->insert($key=>$pebble);
501             next;
502             }
503              
504             $stone->insert($key=>$self->unescapeval($value));
505             }
506             }
507              
508             return undef unless $found;
509             return $stone;
510             }
511              
512             # Read_record has the semantics that if a query is active,
513             # it will only return stones that satisfy the query.
514             sub read_record {
515             my($self,@tags) = @_;
516             my $query = $self->{'query_test'};
517             my $s;
518              
519             if (wantarray) {
520             my(@result);
521             while (!$self->done) {
522             $s = $self->read_one_record(@tags);
523             next unless $s;
524             next if $query && !($query->($s));
525             push(@result,$s);
526             }
527             return @result;
528             } else {
529             while (!$self->done) {
530             $s = $self->read_one_record(@tags);
531             next unless $s;
532             return $s unless $query;
533             return $s if $query->($s);
534             }
535             return undef;
536             }
537             }
538              
539             # Random access. This will have the interesting side effect
540             # of causing read_record() to begin iterating from this record
541             # number.
542             sub get {
543             my($self,$record,@tags) = @_;
544             $self->{'nextrecord'} = $record if defined($record);
545             undef $self->{'EOF'};
546             return $self->read_record(@tags);
547             }
548              
549             # Reset database so we start iterating over the entire
550             # database at record no 0 again.
551             sub reset {
552             my $self = shift;
553             $self->{'EOF'} = undef;
554             $self->{'nextrecord'} = 0;
555             $self->{'query_test'} = undef;
556             $self->{'query_records'} = undef;
557             }
558              
559             # Return the number of records in this file
560             sub length {
561             my $self = shift;
562             return $self->{'dbrecno'}->length;
563             }
564              
565             # Return the number of unread query records
566             sub length_qrecs {
567             my $self = shift;
568             return $#{$self->{'query_records'}} + 1;
569             }
570              
571             # Create a query. read_record() will then
572             # iterate over the query results. A query consists of
573             # an associative array of this form:
574             # index1=>value1,
575             # index2=>value2,
576             # ...
577             # indexN=>valueN,
578             # 'EVAL'=>[expression1,expression2,expression3...]
579             # 'EVAL'=>expression
580             #
581             # The index forms test for equality, and take advantage
582             # of any fast indexed keywords you've declared. For
583             # example, this will identify all white males:
584             # $db->query('Demographics.Sex'=>'M',
585             # 'Demographics.Race'=>'white');
586             #
587             # The code form allows you to retrieve Stones satisfying
588             # any arbitrary snippets of Perl code. Internally, the
589             # variable "$s" will be set to the current Stone.
590             # For example, find all whites > 30 years of age:
591             #
592             # $db->query('Demographics.Race'=>'white',
593             # 'EVAL'=>'$s->index(Age) > 30');
594             #
595             # EVAL (and "eval" too) expressions are ANDed together
596             # in the order you declare them. Internally indexed
597             # keywords are evaluated first in order to speed things up.
598              
599             # A cute feature that may go away:
600             # You can use the expression as shorthand
601             # for $s->index('path.to.index')
602             sub query {
603             my($self,%query) = @_;
604             my($type,@expressions,%keylookups);
605              
606             foreach $type (keys %query) {
607             if ($type =~ /^EVAL$/i) {
608             push (@expressions,$query{$type}) unless ref $query{$type};
609             push (@expressions,@{$query{$type}}) if ref $query{$type};
610             } else {
611             $keylookups{$type} = $query{$type};
612             }
613             }
614              
615             # All the eval expressions are turned into a piece
616             # of perl code.
617             my $perlcode;
618             foreach (@expressions) {
619             s/<([\w.]+)>/\$s->index('$1')/g;
620             $_ = "($_)";
621             }
622              
623             my %fast;
624             grep($fast{$_}++,$self->indexed_keys);
625             my %fastrecs;
626             my $fastset; # this flag keeps track of the first access to %fastrecs
627              
628             foreach (keys %keylookups) {
629              
630             if ($fast{$_}) {
631             my (@records) = $self->lookup($_,$keylookups{$_});
632             if ($fastset) {
633             my %tmp;
634             grep($fastrecs{$_} && $tmp{$_}++,@records);
635             %fastrecs = %tmp;
636             } else {
637             grep($fastrecs{$_}++,@records);
638             $fastset++;
639             }
640              
641             } else { # slow record-by-record search
642             unshift(@expressions,"(\$s->index('$_') eq '$keylookups{$_}')");
643             }
644              
645             }
646             $perlcode = 'sub { my $s = shift;' . join(' && ',@expressions) . ';}' if @expressions;
647             $perlcode = 'sub {1;}' unless @expressions;
648            
649             # The next step either looks up a compiled query or
650             # creates one. We use a package global for this
651             # purpose, since the same query may be used for
652             # different databases.
653             my $coderef;
654             unless ($coderef = $QUERIES{$perlcode}) {
655             $coderef = $QUERIES{$perlcode} = eval $perlcode;
656             return undef if $@;
657             }
658              
659             $self->reset; # clear out old information
660             $self->{'query_test'} = $coderef; # set us to check each record against the code
661             $self->{'query_records'} = [keys %fastrecs] if $fastset;
662             return 1;
663             }
664              
665             # fetch() allows you to pass a query to the
666             # database, and get out all the stones that hit.
667             # Internally it is just a call to query() followed
668             # by an array-context call to read_record
669             sub fetch {
670             my($self,%query) = @_;
671             $self->query(%query);
672             my(@result) = $self->read_record(); # call in array context
673             return @result;
674             }
675              
676             #--------------------------------------
677             # Internal (private) procedures.
678             #--------------------------------------
679             sub _write_nested {
680             my($self,$level,$stone) = @_;
681              
682             my($key,$value,@value,@lines);
683              
684             foreach $key ($stone->tags) {
685             @value = $stone->get($key);
686             $key = $self->escapekey($key);
687             foreach $value (@value) {
688             if (ref $value && defined $value->{'.name'}) {
689             $value = $self->escapeval($value);
690             push(@lines,"$key$self->{delim}$value");
691             } else {
692             push(@lines,"$key$self->{delim}$self->{subrec_start}");
693             push(@lines,_write_nested($self,$level+1,$value));
694             }
695             }
696             }
697              
698             push(@lines,$self->{'subrec_end'});
699             return @lines;
700             }
701              
702             # This finds an array of key/value pairs and
703             # stashes it where we can find it.
704             # This is overriden from the basic Boulder::Stream class,
705             # and relies on the state variable 'nextrecord' to tell
706             # it where to start reading from.
707             sub read_next_rec {
708             my($self) = @_;
709             my $data;
710              
711             # two modes of retrieval:
712             # 1. regular iterate through the entire database
713             # 2. iterate through subset of records in 'query_records'
714             unless ($self->{'query_records'}) {
715             return !($self->{EOF}++) if $self->length <= $self->{'nextrecord'};
716             $data = $self->{'records'}->[$self->{'nextrecord'}];
717             $self->{'nextrecord'}++;
718             } else {
719             my $nextrecord = shift @{$self->{'query_records'}};
720             return !($self->{EOF}++) unless $nextrecord ne '';
721             $data = $self->{'records'}->[$nextrecord];
722             }
723              
724             # unpack the guy into pairs
725             $self->{PAIRS}=[split($self->{'line_end'},$data)];
726             }
727              
728             # This fiddles 'nextrecord' or 'query_records', as appropriate, so that
729             # the next call to read_next_rec will skip over $skip records.
730             sub skip_recs {
731             my($self,$skip) = @_;
732             unless ($self->{'query_records'}) {
733             $self->{'nextrecord'} += $skip;
734             } else {
735             splice(@{$self->{'query_records'}}, 0, $skip);
736             }
737             }
738              
739             # Index a stone record
740             sub index_record {
741             my ($self,$recno,@indices) = @_;
742              
743             my $s = $self->get($recno);
744             return undef unless defined($s);
745              
746             my($index,@values,$value);
747             @indices = $self->indexed_keys unless @indices;
748             foreach $index (@indices) {
749             @values = $s->index($index);
750             foreach $value (@values) {
751             my %current;
752             grep($current{$_}++,split(" ",$self->{'index'}->{"$index:$value"}));
753             $current{$recno}++; # add us to the list
754             $self->{'index'}->{"$index:$value"} = join(" ",keys %current);
755             }
756             }
757             1;
758             }
759              
760             # This is a NOP for now.
761             sub unindex_record {
762             my ($self,$recno,@indices) = @_;
763              
764             my $s = $self->get($recno);
765             return undef unless defined($s);
766              
767             my($index,@values,$value);
768             @indices = $self->indexed_keys unless @indices;
769              
770             foreach $index (@indices) {
771             @values = $s->index($index);
772             foreach $value (@values) {
773             my %current;
774             grep($current{$_}++,split(" ",$self->{'index'}->{"$index:$value"}));
775             delete $current{$recno}; # remove us from the list
776             $self->{'index'}->{"$index:$value"} = join(" ",keys %current); # put index back
777             }
778             }
779             1;
780             };
781              
782             # This gets called after a record delete, when all the indexes need to be
783             # shifted downwards -- this is probably WAY slow.
784             sub renumber_indices {
785             my ($self,$deleted_recno) = @_;
786             while (($key,$value) = each %{$self->{'index'}}) {
787             next if $key =~/^\./;
788             @values = split(" ",$value);
789             foreach (@values) {
790             $_-- if $_ > $deleted_recno;
791             }
792             # This will probably put us into an infinite loop!
793             $self->{'index'}->{$key} = join(" ",@values);
794             }
795             }
796              
797             # This also gets called after a record delete, when all the indexes need to be
798             # shifted downwards -- this is probably WAY slow.
799             sub renumber_records {
800             my ($self,$deleted_recno) = @_;
801             $self->reset;
802             $recno = -1;
803             while ($s=$self->read_record) {
804             $recno++;
805             next unless $s->get('record_no') > $deleted_recno;
806             $s->replace('record_no',$recno);
807             $self->put($s);
808             }
809             }
810              
811             # Look up a stone record using its index. Will return a list
812             # of the matching records
813             sub lookup {
814             my ($self,$index,$value) = @_;
815             my %records;
816             grep($records{$_}++,split(" ",$self->{'index'}->{"$index:$value"}));
817             return keys %records;
818             }
819              
820             # Add an index (or list of indices) to the database.
821             # If new, then we do a reindexing.
822             sub add_index {
823             my ($self,@indices) = @_;
824             my (%oldindices);
825             grep($oldindices{$_}++,$self->indexed_keys);
826             my (@newindices) = grep(!$oldindices{$_},@indices);
827             $self->reindex_some_keys(@newindices);
828             $self->{'index'}->{'.INDICES'}=join($self->{'index_delim'},keys %oldindices,@newindices);
829             }
830              
831             # Return the indexed keys as an associative array (convenient)
832             sub indexed_keys {
833             my $self = shift;
834             return split($self->{'index_delim'},$self->{'index'}->{'.INDICES'});
835             }
836              
837             # Reindex all records that contain records involving the provided indices.
838             sub reindex_some_keys {
839             my($self,@new) = @_;
840             my ($s,$index,$value);
841             $self->reset; # reset to beginning of database
842              
843             while ($s=$self->read_record) { # return all the stones
844             foreach $index (@new) {
845             foreach $value ($s->index($index)){ # pull out all the values at this index (if any)
846             my %current;
847             grep($current{$_}++,split(" ",$self->{'index'}->{"$index:$value"}));
848             $current{$s->get('record_no')}++;
849             $self->{'index'}->{"$index:$value"}=join(" ",keys %current);
850             }
851             }
852             }
853              
854             }
855              
856             # Completely rebuild the index.
857             sub reindex_all {
858             my $self = shift;
859             my ($index,$s,@values,$value);
860             $self->reset;
861             foreach $index ($self->indexed_keys) {
862             undef %records;
863             while ($s=$self->read_record) { # return all the stones
864             foreach $value ($s->index($index)){ # pull out all the values at this index (if any)
865             $records{"$index:$value"}->{$s->get('record_no')}++;
866             }
867             }
868             foreach (keys %records) {
869             $self->{'index'}->{$_}=join(" ",keys %{$records{$_}});
870             }
871             }
872             }
873              
874              
875             1;