File Coverage

blib/lib/Tie/TransactHash.pm
Criterion Covered Total %
statement 110 167 65.8
branch 33 90 36.6
condition 9 12 75.0
subroutine 11 17 64.7
pod 5 7 71.4
total 168 293 57.3


line stmt bran cond sub pod time code
1             package Tie::TransactHash;
2              
3             $Tie::TransactHash::VERSION = '0.03'; #BETA; not heavily tested.
4 3     3   2484 use strict;
  3         5  
  3         105  
5 3     3   15 use Carp;
  3         6  
  3         6703  
6              
7             require Tie::IxHash;
8             require 5.002; #I think older versions don't have proper working tie.
9             # please note; perl 5.003 to something below 5.003_25 (that's a
10             # developers version before perl 5.004) has a bug which causes loss of
11             # data during the destructor calls at the end opf the program
12              
13             #TransactHash - a perl module to allow editing of hashes in transactions
14             #maintaining the sequence of the hash through the transaction.
15             #Copyright (c) 1997 Michael De La Rue
16             #This is free software and may be distributed under the same terms
17             #as perl. There is no warantee. See the file COPYING which should
18             #have been included with the distribution for one set of terms under
19             #which it may be distributed.
20              
21             =head1 NAME
22              
23             Tie::TransactHash - Edit hash in transactions not changing order during trans.
24              
25             =head1 SYNOPSIS
26              
27             use Tie::TransactHash;
28             $::edit_db = tie %::edit_me, TransactHash, \%::db_as_hash, $::db;
29             while (($key, $value)=each %edit_me)) {
30             $::edit_me{$key} ++ if $key =~ m/counters/ ;
31             }
32              
33             =head1 DESCRIPTION
34              
35             Tie::TransactHash is a package which provides facilities for editing
36             any other hash in transactions. A transaction is a group of changes
37             which go together and are either all applied or none. When working on
38             a standard perl hash or a hash indexed DBM file, one advantage is that
39             the original hash remains untouched during the transaction, so its
40             order (the order the each(), keys() or values functions give out) is
41             maintained - changes can be made to the transact hash whilst iterating
42             over it.
43              
44             =head1 OVERVIEW
45              
46             Editing a hash causes problems because it rearranges the hash. If the
47             editing is to be done in sequence then this makes life difficult. The
48             TransactHash class uses a fixed sequence hash class which overlays the
49             normal hash and allows editing in place. It stores all of the changes
50             to the original hash in memory until it is told to apply them.
51              
52             As a side effect of this design, the class also provides a
53             commit/rollback system. When a commit is called, the order of the
54             hidden hash will be changed.
55              
56             A commit will normally be done as the TransactHash object is being
57             destroyed. This could be undesirable if your program exits when it
58             discovers a failure. You can change the.
59              
60             If you can accept the re-ordering, then you can do partial
61             edits and commit half way through.
62              
63             When working on a DBM file, if a crash occurs during the editing and
64             no commit has been called then the original hash will be left intact.
65             If however the crash occurs during the commit, bad things could
66             happen.
67              
68             use DB_File;
69             use Tie::TransactHash;
70             use Fcntl;
71              
72             $::db = tie %::db_as_hash, DB_File, $::dbname, O_RDWR|O_CREAT, 0640, $db_type
73             or die $!;
74              
75             $::edit_db = tie %::edit_me, TransactHash, \%::db_as_hash, $::db;
76             #the $::db doesn't really do any good right now, but in future it might
77              
78             my $count = 0;
79             my ($key,$value)
80             while(($key,$_)=each %edit_me) {
81             s/bouncy/bouncy, very very bouncy./;
82             m/Fred/ && do {
83             $count++;
84             $edit_me{ Fred . $count } = $key;
85             }
86             }
87             print "Found Fred in the values $count times\n";
88              
89             Generally, this package should be used if you want to occasionally do
90             small numbers of changes across the values of a large hash. If you
91             are using it overly (often or for large numbers of changes on the
92             database), then you should probably switch to btree indexed hashes
93             (Berkley DBM) which give you the same ordering effect but don't use a
94             large chunk of memory. Alternately you could consider some kind of
95             multi-pass algorithm (scan through the database putting planned
96             changes to a file then apply them afterwards all in one go).
97              
98             =head1 METHODS
99              
100             =cut
101              
102             $TransactHash::autostore = 1; #we automatically commit at destructor time.
103             #$TransactHash::verbose= 0xfff; #
104             $TransactHash::verbose= 0; #turn this up for debugging messages
105              
106 0     0 0 0 sub version { return $Tie::TransactHash::VERSION };
107              
108             =head2 new( \%hidehash [,$hideobj] )
109              
110             This creates a new TransactHash, hiding the hash \%hidehash.
111              
112             =cut
113              
114              
115             sub new {
116 3     3 1 8 my $class=shift;
117 3         12 my $self=bless {}, $class;
118             #now for the underlying hash (& possibly it's object) that we are editing
119 3         19 $self->{"hidehash"} = shift;
120             #FIXME check that actually was a hash reference.
121             #now create a place to store our changes for the transaction.
122 3         9 $self->{"hideobj"} = shift;
123 3         26 my $tempstore = tie my(%temphash), "Tie::IxHash";
124 3         51 $self->{"tempstore"} = $tempstore;
125 3         9 $self->{"temphash"} = \%temphash;
126 3         11 $self->{"deleted"} = {};
127             #FIXME isn't this bad for inheritance? what is the alternative?
128 3         8 $self->{"autostore"} = $TransactHash::autostore;
129 3         14 return $self;
130             }
131              
132             =head2 TIEHASH (and other hash methods)
133              
134             This is simply a call to new. See above. The other hash methods are just as
135             for a standard hash (see perltie) and act just like one.
136              
137             =cut
138              
139             sub TIEHASH {
140 3     3   146 return new(@_);
141             }
142              
143             sub DESTROY {
144 1     1   31 my $self=shift;
145 1 50       5 if ($self->{"autostore"}) {
146 1         4 $self->commit();
147             }
148             }
149              
150             sub FETCH {
151 1     1   36 my $self=shift;
152 1         3 my $key=shift;
153 1         2 my $value;
154 1 50       5 if (defined $self->{"temphash"}->{$key}) {
155 1 50       10 print STDERR "Recovering changed value for key $key\n"
156             if $TransactHash::verbose;
157 1         4 return $self->{"temphash"}->{$key};
158             }
159 0 0       0 if (defined $self->{"deleted"}->{$key}) {
160 0 0       0 print STDERR "Value for $key has been deleted\n"
161             if $TransactHash::verbose;
162 0         0 return undef;
163             }
164 0 0       0 print STDERR "Recovering value for $key from hidden hash" .
165             $self->{"hidehash"} . "\n"
166             if $TransactHash::verbose;
167 0         0 $value=$self->{"hidehash"}->{$key};
168 0 0       0 print STDERR "returning" . $value . "\n"
169             if $TransactHash::verbose;
170 0         0 return $value;
171             }
172              
173             sub STORE {
174 8     8   150 my $self=shift;
175 8         15 my $key=shift;
176 8         14 my $value=shift;
177             #if we have it marked as deleted then
178 8 50       44 if (defined $self->{"deleted"}->{$key}) {
179 0 0       0 print STDERR "Value for $key no longer deleted\n"
180             if $TransactHash::verbose;
181 0         0 delete $self->{"deleted"}->{$key};
182             }
183 8 50       23 print STDERR "$key having value $value stored\n"
184             if $TransactHash::verbose;
185 8         73 $self->{"temphash"}->{$key} = $value;
186             }
187              
188             sub DELETE {
189 0     0   0 my $self=shift;
190 0         0 my $key=shift;
191 0 0       0 print STDERR "Doing delete of key $key\n"
192             if $TransactHash::verbose;
193             #if it exists in our temphash get rid of it
194 0         0 delete $self->{"temphash"}->{$key};
195             #if it exists in the database mark it into deletes
196 0 0       0 if ( defined $self->{"hidehash"}->{$key} ) {
197 0 0       0 print STDERR "Marking key deleted from database\n"
198             if $TransactHash::verbose;
199 0         0 $self->{"deleted"}->{$key} = 1;
200             }
201             }
202              
203             sub EXISTS {
204 0     0   0 my $self=shift;
205 0         0 my $key=shift;
206 0 0       0 if (defined $self->{"deleted"}->{$key}) {
207 0         0 return 0; #it has been deleted
208             }
209 0 0       0 if (defined $self->{"temphash"}->{$key}) {
210 0         0 return 1; #it has been changed, but exists
211             }
212 0 0       0 if (defined $self->{"hidehash"}->{$key}) {
213 0         0 return 1; #it exists as was
214             }
215 0         0 return 0; #never heard of it
216             }
217              
218             =head2 Iterator functions (FIRSTKEY & NEXTKEY)
219              
220             The iterators first iterate over the hidden hash as normal (giving out changed
221             values) then iterate over the storehash skipping values in the original hash.
222              
223             =cut
224              
225             sub FIRSTKEY {
226 2     2   48 my $self=shift;
227 2         5 $self->{"iteratehidden"} = 1;
228             #FIXME checking for an empty hash..
229             #don't use this cos then perl doesn't notice the start of the iteration
230 2 50       7 print STDERR "Using hash hack to get first hidden value\n"
231             if $TransactHash::verbose;
232 2         4 my $count = scalar keys %{$self->{"hidehash"}};
  2         6  
233 2 50       6 if ( $count ) { #there are elements in the hash we are editing.
234 2         3 my ($key,$value);
235 2         11 ($key,$value) = each %{$self->{"hidehash"}} ;
  2         7  
236 2   33     16 while (defined $key && defined $self->{"deleted"}->{$key}) {
237 0         0 ($key,$value) = each %{$self->{"hidehash"}}
  0         0  
238             }
239 2 50       15 return $key if defined $key;
240             }
241              
242             #none of the elements in the original hash remain, or there weren't
243             #any to start with.
244              
245 0         0 $self->{"iteratehidden"}=0;
246             #reset the iteration across the temphash
247 0         0 my $a = scalar keys %{$self->{"temphash"}};
  0         0  
248 0         0 return each %{$self->{"temphash"}};
  0         0  
249             #which will be undef if there is nothing at all..
250             }
251              
252             sub NEXTKEY {
253 13     13   19 my $self=shift;
254 13         18 my $lastkey=shift;
255 13 50       24 print STDERR "TransactHash nextkey called last key was $lastkey\n"
256             if $TransactHash::verbose;
257             #you could optimise by just using the NEXTKEY from the object when
258             #available
259 13 100       38 if ($self->{"iteratehidden"}) {
260 12 50       22 print STDERR "Getting values from underlying hash\n"
261             if $TransactHash::verbose;
262 12         14 my ($key, $value) = each %{$self->{"hidehash"}} ;
  12         70  
263             #skip over the ones we've deleted
264 12   66     64 while (defined $key && defined $self->{"deleted"}->{$key}) {
265 0 0       0 print STDERR "$key is deleted, skipping over it\n"
266             if $TransactHash::verbose;
267 0         0 my ($key, $value) = each %{$self->{"hidehash"}} ;
  0         0  
268             }
269 12 100 100     67 if (defined $key && defined $self->{"temphash"}->{$key}) {
270 4 50       38 print STDERR "$key is changed, returning new value\n"
271             if $TransactHash::verbose;
272 4         16 $value=$self->{"temphash"}->{$key};
273             }
274 12 100       95 if (defined $key) {
275 10 50       22 print STDERR "Returning key $key and value $value from main sequence\n"
276             if $TransactHash::verbose;
277 10         38 return $key; #, $value;
278             }
279 2 50       6 print STDERR "Reached last hidden value, changing to iterating new values\n"
280             if $TransactHash::verbose;
281 2         3 $self->{"iteratehidden"}=0;
282             #reset the iteration across the temphash
283 2         3 my $a = scalar keys %{$self->{"temphash"}};
  2         11  
284             }
285             #we have completed the sequence of original values and are now
286             #iterating to find added values..
287              
288 3         103 my ($key, $value) = each %{$self->{"temphash"}} ;
  3         12  
289             #skip over the ones from the main sequence
290 3   100     44 while (defined $key && defined $self->{"hidehash"}->{$key}) {
291 6 50       69 print STDERR "$key is only changed. Skipping\n"
292             if $TransactHash::verbose;
293 6         6 ($key, $value) = each %{$self->{"temphash"}} ;
  6         19  
294             }
295 3 100       27 $self->{"iteratehidden"}=1 unless defined $key;
296 3         23 return $key; #, $value;
297             }
298              
299             =head2 commit() and reset()
300              
301             These functions are not normally visible in the hash interface, but can be
302             used as object methods. commit() updates the original hidden hash (which
303             changes its order) and reset() loses all of the changes that we have made.
304              
305             In the hash interface commit is called as the variable is destroyed. This
306             should happen at exit time, but didn't seem to to me. Assigning undef to the
307             variable you stored the object in and untie()ing the hash will force it to
308             happen.
309              
310             =cut
311              
312             sub commit {
313 2     2 1 24 my $self=shift;
314 2 50       116 print STDERR "commit called on TransactHash ($self)\n"
315             if $TransactHash::verbose;
316             #FIXME should really validate that there is not a delete.. just to
317             #be sure
318 2         5 my ($key, $value);
319 2 50       9 print STDERR "using temp database (" . $self->{"temphash"} . ")\n"
320             if $TransactHash::verbose;
321              
322 2         4 my $junka = scalar keys %{$self->{"temphash"}};
  2         18  
323              
324 2 50       51 print STDERR "about to gen list\n"
325             if $TransactHash::verbose;
326 2 50       24 if ($TransactHash::verbose) {
327 0         0 print STDERR "list of values to commit\n";
328 0         0 while (($key,$value) = each %{$self->{"temphash"}}) {
  0         0  
329 0 0       0 print STDERR "$key has value $value\n"
330             if $TransactHash::verbose;
331             }
332             }
333 2 50       15 print STDERR "about to do changes\n"
334             if $TransactHash::verbose;
335 2         15 while (($key,$value) = each %{$self->{"temphash"}}) {
  6         25  
336 4 50       70 print STDERR "writing $key with $value to hidden hash\n"
337             if $TransactHash::verbose;
338 4         10 my $hashref = $self->{"hidehash"};
339 4         10 $hashref->{$key} = $value;
340 4 50       21 print STDERR "hidehash stores " . $hashref->{$key} ."\n"
341             if $::TransactHash::verbose;
342             }
343 2         15 my $junkb = scalar keys %{$self->{"deleted"}};
  2         7  
344 2 50       16 print STDERR "about to do deletes\n"
345             if $TransactHash::verbose;
346 2         5 while (($key,$value) = each %{$self->{"deleted"}}) {
  2         10  
347 0 0       0 print STDERR "deleting $key from hidden hash\n"
348             if $TransactHash::verbose;
349 0         0 delete $self->{"hidehash"}->{$key};
350             }
351              
352             #FIXME file syncronisation; warn if we can't and it's a file that
353             # we're writing to .. we have to eval this because it might be a
354             # normal simple perl hash that we are editing
355              
356 2         5 eval { $self->{"hideobj"}->sync() };
  2         37  
357              
358             #FIXME we store the old values for verification.. if we don't want
359             # this then it would be worth throwing them away to avoid waste of
360             # memory..
361              
362 2         12 $self->{"oldstore"}=$self->{"tempstore"};
363 2         8 $self->{"oldhash"}=$self->{"temphash"};
364 2         10 $self->{"olddeleted"}=$self->{"deleted"};
365              
366             #now create a place to store our changes for the next transaction.
367 2         9 my $tempstore = tie my(%temphash), "Tie::IxHash";
368 2         26 $self->{"tempstore"} = $tempstore;
369 2         5 $self->{"temphash"} = \%temphash;
370 2         19 $self->{"deleted"} = {};
371             }
372              
373             =head2 $transhash->autostore()
374              
375             This method stores a true or false value in the object telling it
376             whether it should automatically commit if it is destroyed. If this is
377             set to false, then the object method $transhash->commit() must be
378             called to store any changes, otherwise they will be lost.
379              
380             If this is set to true, then be aware that exiting your program from
381             some kind of error condition of your program (that is, not one perl
382             knows about) would commit the changes.
383              
384             =cut
385              
386             sub autostore {
387 0     0 1 0 my $self=shift;
388 0 0       0 return $self->{"autostore"} unless defined @_;
389 0         0 $self->{"autostore"} = shift;
390             }
391              
392              
393              
394             =head2 $transhash->verify_write()
395              
396             This function checks that a write has committed to the hash correctly.
397             It does this by checking that all of the values in the old temporary
398             stores match those in the new ones.
399              
400             This function is untested since I don't have a sensible test case for
401             it yet and don't need it myself. should work though.
402              
403             =cut
404              
405             sub verify_write {
406 1     1 1 25 my $self=shift;
407 1         2 my $hidehash=$self->{"hidehash"};
408 1         2 my $key;
409             my $value;
410 1         2 my $pass=1;
411 1 50       3 croak "Commit doesn't seem to have been called yet"
412             unless defined $self->{"oldhash"};
413 1         2 CHANGE: while(($key, $value)=each %{$self->{"oldhash"}}) {
  4         17  
414 3 50       50 unless(defined $hidehash->{$key} ) {
415 0         0 warn "Key $key gives undefined; should be $value";
416 0         0 next CHANGE;
417 0         0 my $pass=0;
418             }
419 3 50       15 unless($value=$hidehash->{$key}) {
420 0         0 warn "Key $key has value $value, should be " . $hidehash->{$key};
421 0         0 my $pass=0;
422             }
423              
424             }
425 1         8 DELETE: while(($key, $value)=each %{$self->{"olddeleted"}}) {
  1         5  
426 0 0       0 if(defined $hidehash->{$key}) {
427 0         0 warn "Key $key gives $value; should be undefined";
428 0         0 my $pass=0;
429             }
430             }
431 1         2 return $pass;
432             }
433              
434             sub reset {
435 0     0 1   my $self=shift;
436 0           $self->{"temphash"} = {};
437 0           $self->{"deleted"} = {};
438             #FIXME reset the sequence?
439             }
440              
441 0     0 0   sub rollback {reset @_}
442              
443             =head2 COPYING
444              
445             Copyright (c) 1997 Michael De La Rue
446              
447             This is free software and may be distributed under the same terms as perl.
448             There is no warantee. See the file COPYING which should have been included
449             with the distribution for one set of terms under which it may be distributed.
450             The artistic license, distributed with perl gives the other one.
451              
452             =cut
453              
454             1; #he said and rested.