File Coverage

blib/lib/Data/Transactional.pm
Criterion Covered Total %
statement 143 162 88.2
branch 20 26 76.9
condition 2 2 100.0
subroutine 41 45 91.1
pod 7 7 100.0
total 213 242 88.0


line stmt bran cond sub pod time code
1             package Data::Transactional;
2              
3 4     4   99180 use strict;
  4         7  
  4         1690  
4 4     4   20 use warnings;
  4         7  
  4         158  
5              
6             our $VERSION = '1.04';
7              
8 4     4   17 use Data::Dumper;
  4         5  
  4         1456  
9              
10             =head1 NAME
11              
12             Data::Transactional - data structures with RDBMS-like transactions
13              
14             =head1 SYNOPSIS
15              
16             use Data::Transactional;
17              
18             my $data = Data::Transactional->new(type => 'hash');
19             $data->{food_and_drink} = [ [], [] ];
20             $data->checkpoint();
21             $data->{food_and_drink}->[0] = [qw(pie curry chips)];
22             $data->checkpoint();
23             $data->{food_and_drink}->[1] = [qw(beer gin whisky)];
24             $data->rollback(); # back to last checkpoint
25              
26             =head1 METHODS
27              
28             =over
29              
30             =item new
31              
32             The constructor. This takes named parameters. The only parameter
33             so far is:
34              
35             =over
36              
37             =item type
38              
39             Optional parameter, taking either 'ARRAY' or 'HASH' as its value (case-
40             insensitive), to determine what data type to base the structure on.
41             Regardless of what you choose for the first level of the structure, you
42             may use whatever you like further down the tree. If not supplied, this
43             defaults to 'HASH'.
44              
45             =back
46              
47             =cut
48              
49             sub new {
50 26     26 1 954 my($class, %args) = @_;
51 26         24 my $self;
52              
53 26   100     60 $args{type} ||= 'HASH'; $args{type} = uc($args{type});
  26         37  
54              
55 26 100       48 if($args{type} eq 'HASH') {
    50          
56 12         13 tie %{$self}, __PACKAGE__.'::Hash';
  12         45  
57             } elsif($args{type} eq 'ARRAY') {
58 14         13 tie @{$self}, __PACKAGE__.'::Array';
  14         44  
59             } else {
60 0         0 die(__PACKAGE__."::new(): type '$args{type}' unknown\n");
61             }
62              
63 26         52 return bless $self, $class;
64             }
65              
66             =item checkpoint
67              
68             Saves the current state of the structure so that we can roll back to it.
69              
70             =cut
71              
72             sub checkpoint {
73 9     9 1 1235 my $self = shift;
74 9         8 (tied %{$self})->checkpoint();
  9         18  
75             }
76              
77             =item commit
78              
79             Discards the most recent saved state, so it can no longer be rolled back.
80             Why do this? Well, throwing away the history saves a load of memory.
81             It is a fatal error to commit() when there's no saved states.
82              
83             =cut
84              
85             # should this also commit_all in sub-structures?
86             sub commit {
87 5     5 1 269 my $self = shift;
88 5         4 (tied %{$self})->commit();
  5         9  
89             }
90              
91             =item commit_all
92              
93             Throws away all saved states, effectively committing all current transactions.
94              
95             =cut
96              
97             sub commit_all {
98 1     1 1 3 my $self = shift;
99 1         3 undef $@;
100 1         3 while(!$@) { eval { $self->commit(); }; }
  3         4  
  3         5  
101             }
102              
103             =item rollback
104              
105             Revert the data structure to the last checkpoint. To roll back beyond the
106             first checkpoint is a fatal error.
107              
108             =cut
109              
110             sub rollback {
111 10     10 1 1058 my $self = shift;
112 10         9 (tied %{$self})->rollback();
  10         17  
113             }
114              
115             =item rollback_all
116              
117             Roll back all changes.
118              
119             =cut
120              
121             sub rollback_all {
122 1     1 1 4 my $self = shift;
123 1         2 undef $@;
124 1         3 while(!$@) { eval { $self->rollback(); }; }
  4         4  
  4         3  
125             }
126              
127             =item current_state
128              
129             Return a reference to the current state of the underlying object.
130              
131             =cut
132              
133             sub current_state {
134 75     75 1 51 my $self = shift;
135             return $self->isa('HASH') ?
136 32         44 tied(%{$self})->current_state() :
137 75 100       141 tied(@{$self})->current_state();
  43         59  
138             }
139              
140             =back
141              
142             =head1 IMPLEMENTATION NOTES
143              
144             This module relies on two other packages which are included in the same
145             file - Data::Transactional::Hash and Data::Transactional::Array. These
146             are where the magic really happens. These implement everything needed
147             for Cing those structures, plus their own C,
148             C and C methods. When you create a
149             Data::Transactional object, what you really get is one of these tied
150             structures, reblessed into the Data::Transactional class. The
151             transactional methods simply call through to the same method on the
152             underlying tied structure.
153              
154             This is loosely inspired by L.
155              
156             =head1 BUGS/WARNINGS
157              
158             I assume that C<$[> is zero.
159              
160             Storing blessed objects in a C structure is not
161             supported. I suppose it could be, but there's no sane way that they
162             could be transactionalised. This also applies to tie()d objects.
163             Please note that in the case of tie()d objects, we don't do a great deal
164             of checking, so things may break in subtle and hard-to-debug ways.
165              
166             The precise details of how the transactional methods affect sub-structures
167             in your data may change before a 1.0 release. If you have suggestions for
168             how it could be improved, do please let me know.
169              
170             The SPLICE() operation is *not defined* for transactionalised arrays,
171             because it makes my brane hurt. If you want to implement this please
172             do! Remember that you should use STORE() to put each new entry in the
173             array, as that will properly handle adding complex data structures.
174              
175             No doubt there are others. When submitting a bug report please please
176             please include a test case, in the form of a .t file, which will fail
177             with my version of the module and pass once the bug is fixed. If you
178             include a patch as well, that's even better!
179              
180             =head1 FEEDBACK
181              
182             I welcome all comments - both praise and constructive criticism - about
183             my code. If you have a question about how to use it please read *all*
184             of the documentation first, and let me know what you have tried and how
185             the results differ from what you wanted or expected.
186              
187             I do not consider blind, automatically generated and automatically sent
188             error reports to be constructive.
189             Don't send them, you'll only get flamed.
190              
191             =head1 AUTHOR
192              
193             David Cantrell Edavid@cantrell.org.ukE
194              
195             =head1 LICENCE
196              
197             This software is Copyright 2004 David Cantrell. You may use, modify and
198             distribute it under the same terms as perl itself.
199              
200             =cut
201              
202             package Data::Transactional::Hash;
203 4     4   2591 use Storable qw(dclone);
  4         10263  
  4         232  
204 4     4   44 use strict;use warnings;
  4     4   6  
  4         68  
  4         15  
  4         4  
  4         1881  
205              
206             sub TIEHASH {
207 12     12   10 my $class = shift;
208 12         26 my $self = {
209             STACK => [],
210             CURRENT_STATE => {},
211             };
212              
213 12         27 return bless $self, $class;
214             }
215              
216             sub CLEAR {
217 4     4   341 my $self=shift;
218 4         20 $self->{CURRENT_STATE}={};
219             }
220              
221             sub STORE {
222 27     27   44 my($self, $key, $value)=@_;
223 27         21 my $newobj = $value;
224 27 100       62 if(ref($value)) {
225 16 100       32 if(ref($value) eq 'ARRAY') {
    100          
226 12         15 $newobj = Data::Transactional->new(type => 'ARRAY');
227             # @{$newobj} = @{$value};
228 12         8 push @{$newobj}, $_ foreach(@{$value});
  12         18  
  40         44  
229             } elsif(ref($value) eq 'HASH') {
230 3         10 $newobj = Data::Transactional->new(type => 'HASH');
231             # %{$newobj} = %{$value};
232 3         4 $newobj->{$_} = $value->{$_} foreach(keys %{$value});
  3         12  
233             } else {
234 1         8 die(__PACKAGE__."::STORE(): don't know how to store a ".ref($value)."\n");
235             }
236             }
237 26         69 $self->{CURRENT_STATE}->{$key} = $newobj;
238             }
239              
240             sub FETCH {
241 28     28   417 my($self, $key) = @_;
242 28         62 $self->{CURRENT_STATE}->{$key};
243             }
244              
245             sub FIRSTKEY {
246 8     8   1309 my $self = shift;
247 8         7 scalar keys %{$self->{CURRENT_STATE}}; # reset iterator
  8         11  
248             # scalar each %{$self->{CURRENT_STATE}};
249 8         30 $self->NEXTKEY();
250             }
251              
252 36     36   365 sub NEXTKEY { my $self = shift; scalar each %{$self->{CURRENT_STATE}}; }
  36         26  
  36         80  
253 2     2   9 sub DELETE { my($self, $key) = @_; delete $self->{CURRENT_STATE}->{$key}; }
  2         7  
254 2     2   264 sub EXISTS { my($self, $key) = @_; exists($self->{CURRENT_STATE}->{$key}); }
  2         5  
255              
256             sub checkpoint {
257 9     9   9 my $self = shift;
258             # make a new copy of CURRENT_STATE before putting on stack,
259             # otherwise CURRENT_STATE and top-of-STACK will reference the
260             # same data structure, which would be a Bad Thing
261 9         5 push @{$self->{STACK}}, dclone($self->{CURRENT_STATE});
  9         457  
262             }
263              
264             sub commit {
265 5     5   4 my $self = shift;
266             # $self->{STACK}=[]; # clear all checkpoints
267 5 100       4 defined(pop(@{$self->{STACK}})) ||
  5         34  
268             die("Attempt to commit without a checkpoint");
269             }
270              
271             sub rollback {
272 10     10   8 my $self = shift;
273 10 100       7 die("Attempt to rollback too far") unless(@{$self->{STACK}});
  10         40  
274             # no copying required, just update a pointer
275 6         6 $self->{CURRENT_STATE}=pop @{$self->{STACK}};
  6         16  
276             }
277              
278             sub current_state {
279 32     32   82 shift->{CURRENT_STATE};
280             }
281              
282             package Data::Transactional::Array;
283 4     4   18 use Storable qw(dclone);
  4         5  
  4         150  
284 4     4   16 use strict;use warnings;
  4     4   4  
  4         68  
  4         13  
  4         5  
  4         3993  
285              
286             sub TIEARRAY {
287 14     14   14 my $class = shift;
288 14         33 my $self = {
289             STACK => [],
290             CURRENT_STATE => [],
291             };
292              
293 14         28 return bless $self, $class;
294             }
295              
296             sub CLEAR {
297 1     1   265 my $self=shift;
298 1         5 $self->{CURRENT_STATE}=[];
299             }
300              
301             sub STORE {
302 52     52   43 my($self, $index, $value)=@_;
303 52         32 my $newobj = $value;
304 52 100       70 if(ref($value)) {
305 3 50       11 if(ref($value) eq 'ARRAY') {
    100          
306 0         0 $newobj = Data::Transactional->new(type => 'ARRAY');
307             # @{$newobj} = @{$value};
308 0         0 push @{$newobj}, $_ foreach(@{$value});
  0         0  
  0         0  
309             } elsif(ref($value) eq 'HASH') {
310 2         4 $newobj = Data::Transactional->new(type => 'HASH');
311             # %{$newobj} = %{$value};
312 2         3 $newobj->{$_} = $value->{$_} foreach(keys %{$value});
  2         17  
313             } else {
314 1         7 die(__PACKAGE__."::STORE(): don't know how to store a ".ref($value)."\n");
315             }
316             }
317 51         112 $self->{CURRENT_STATE}->[$index] = $newobj;
318             }
319              
320             sub FETCH {
321 10     10   33 my($self, $index) = @_;
322 10         18 $self->{CURRENT_STATE}->[$index];
323             }
324              
325 3     3   535 sub DELETE { my($self, $index) = @_; delete $self->{CURRENT_STATE}->[$index]; }
  3         9  
326 2     2   277 sub EXISTS { my($self, $index) = @_; exists($self->{CURRENT_STATE}->[$index]); }
  2         4  
327 2     2   263 sub POP { my $self = shift; pop @{$self->{CURRENT_STATE}}; }
  2         4  
  2         11  
328 1     1   314 sub SHIFT { my $self = shift; shift @{$self->{CURRENT_STATE}}; }
  1         2  
  1         3  
329              
330             sub PUSH {
331 42     42   314 my($self, @list) = @_;
332 42         59 $self->STORE($self->FETCHSIZE(), $_) foreach (@list);
333             }
334              
335             sub UNSHIFT {
336 1     1   337 my($self, @list) = @_;
337 1         1 my @oldlist = @{$self->{CURRENT_STATE}};
  1         3  
338             # shuffle existing contents along
339 1         2 for(my $i = $self->FETCHSIZE() - 1; $i >= 0; $i--) {
340             $self->{CURRENT_STATE}->[$i + scalar(@list)] =
341 4         9 $self->{CURRENT_STATE}->[$i];
342             }
343 1         5 $self->STORE($_, $list[$_]) foreach(0..$#list);
344 1         2 return $self->FETCHSIZE();
345             }
346              
347             # # FIXME - this needs to shuffle stuff as UNSHIFT does, then use STORE
348             # # for anything we insert
349             # sub SPLICE {
350             # }
351              
352 51     51   47 sub FETCHSIZE { my $self = shift; scalar(@{$self->{CURRENT_STATE}}); }
  51         31  
  51         82  
353             sub STORESIZE {
354 0     0   0 my($self, $count) = @_;
355 0         0 $self->{CURRENT_STATE} = [(@{$self->{CURRENT_STATE}})[0..$count - 1]];
  0         0  
356             }
357 1     1   3 sub EXTEND { 'the voices told me to write this method' }
358              
359             sub checkpoint {
360 0     0   0 my $self = shift;
361 0         0 push @{$self->{STACK}}, dclone($self->{CURRENT_STATE});
  0         0  
362             }
363              
364             sub commit {
365 0     0   0 my $self = shift;
366             # $self->{STACK}=[]; # clear all checkpoints
367 0 0       0 defined(pop(@{$self->{STACK}})) ||
  0         0  
368             die("Attempt to commit without a checkpoint");
369             }
370              
371             sub rollback {
372 0     0   0 my $self = shift;
373 0 0       0 die("Attempt to rollback too far") unless(@{$self->{STACK}});
  0         0  
374             # no copying required, just update a pointer
375 0         0 $self->{CURRENT_STATE} = pop @{$self->{STACK}};
  0         0  
376             }
377              
378             sub current_state {
379 43     43   108 shift->{CURRENT_STATE};
380             }