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   4632549 use strict;
  4         13  
  4         132  
4 4     4   22 use warnings;
  4         9  
  4         1378  
5              
6             our $VERSION = '1.03';
7              
8 4     4   25 use Data::Dumper;
  4         1229  
  4         2012  
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 1567 my($class, %args) = @_;
51 26         36 my $self;
52              
53 26   100     85 $args{type} ||= 'HASH'; $args{type} = uc($args{type});
  26         61  
54              
55 26 100       141 if($args{type} eq 'HASH') {
    50          
56 12         19 tie %{$self}, __PACKAGE__.'::Hash';
  12         63  
57             } elsif($args{type} eq 'ARRAY') {
58 14         44 tie @{$self}, __PACKAGE__.'::Array';
  14         66  
59             } else {
60 0         0 die(__PACKAGE__."::new(): type '$args{type}' unknown\n");
61             }
62              
63 26         97 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 1327 my $self = shift;
74 9         14 (tied %{$self})->checkpoint();
  9         37  
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 292 my $self = shift;
88 5         6 (tied %{$self})->commit();
  5         13  
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 5 my $self = shift;
99 1         2 undef $@;
100 1         4 while(!$@) { eval { $self->commit(); }; }
  3         4  
  3         7  
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 1327 my $self = shift;
112 10         12 (tied %{$self})->rollback();
  10         25  
113             }
114              
115             =item rollback_all
116              
117             Roll back all changes.
118              
119             =cut
120              
121             sub rollback_all {
122 1     1 1 5 my $self = shift;
123 1         2 undef $@;
124 1         4 while(!$@) { eval { $self->rollback(); }; }
  4         5  
  4         9  
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 88 my $self = shift;
135             return $self->isa('HASH') ?
136 32         80 tied(%{$self})->current_state() :
137 75 100       246 tied(@{$self})->current_state();
  43         111  
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   4112 use Storable qw(dclone);
  4         14585  
  4         298  
204 4     4   51 use strict;use warnings;
  4     4   9  
  4         95  
  4         21  
  4         8  
  4         2693  
205              
206             sub TIEHASH {
207 12     12   22 my $class = shift;
208 12         43 my $self = {
209             STACK => [],
210             CURRENT_STATE => {},
211             };
212              
213 12         41 return bless $self, $class;
214             }
215              
216             sub CLEAR {
217 4     4   403 my $self=shift;
218 4         26 $self->{CURRENT_STATE}={};
219             }
220              
221             sub STORE {
222 27     27   95 my($self, $key, $value)=@_;
223 27         38 my $newobj = $value;
224 27 100       84 if(ref($value)) {
225 16 100       47 if(ref($value) eq 'ARRAY') {
    100          
226 12         25 $newobj = Data::Transactional->new(type => 'ARRAY');
227             # @{$newobj} = @{$value};
228 12         16 push @{$newobj}, $_ foreach(@{$value});
  12         29  
  40         86  
229             } elsif(ref($value) eq 'HASH') {
230 3         13 $newobj = Data::Transactional->new(type => 'HASH');
231             # %{$newobj} = %{$value};
232 3         6 $newobj->{$_} = $value->{$_} foreach(keys %{$value});
  3         24  
233             } else {
234 1         9 die(__PACKAGE__."::STORE(): don't know how to store a ".ref($value)."\n");
235             }
236             }
237 26         133 $self->{CURRENT_STATE}->{$key} = $newobj;
238             }
239              
240             sub FETCH {
241 28     28   501 my($self, $key) = @_;
242 28         107 $self->{CURRENT_STATE}->{$key};
243             }
244              
245             sub FIRSTKEY {
246 8     8   2081 my $self = shift;
247 8         10 scalar keys %{$self->{CURRENT_STATE}}; # reset iterator
  8         18  
248             # scalar each %{$self->{CURRENT_STATE}};
249 8         37 $self->NEXTKEY();
250             }
251              
252 36     36   463 sub NEXTKEY { my $self = shift; scalar each %{$self->{CURRENT_STATE}}; }
  36         37  
  36         147  
253 2     2   12 sub DELETE { my($self, $key) = @_; delete $self->{CURRENT_STATE}->{$key}; }
  2         8  
254 2     2   399 sub EXISTS { my($self, $key) = @_; exists($self->{CURRENT_STATE}->{$key}); }
  2         8  
255              
256             sub checkpoint {
257 9     9   11 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         11 push @{$self->{STACK}}, dclone($self->{CURRENT_STATE});
  9         780  
262             }
263              
264             sub commit {
265 5     5   5 my $self = shift;
266             # $self->{STACK}=[]; # clear all checkpoints
267 5 100       6 defined(pop(@{$self->{STACK}})) ||
  5         49  
268             die("Attempt to commit without a checkpoint");
269             }
270              
271             sub rollback {
272 10     10   14 my $self = shift;
273 10 100       9 die("Attempt to rollback too far") unless(@{$self->{STACK}});
  10         68  
274             # no copying required, just update a pointer
275 6         8 $self->{CURRENT_STATE}=pop @{$self->{STACK}};
  6         29  
276             }
277              
278             sub current_state {
279 32     32   134 shift->{CURRENT_STATE};
280             }
281              
282             package Data::Transactional::Array;
283 4     4   22 use Storable qw(dclone);
  4         8  
  4         165  
284 4     4   25 use strict;use warnings;
  4     4   7  
  4         87  
  4         19  
  4         6  
  4         3416  
285              
286             sub TIEARRAY {
287 14     14   22 my $class = shift;
288 14         43 my $self = {
289             STACK => [],
290             CURRENT_STATE => [],
291             };
292              
293 14         42 return bless $self, $class;
294             }
295              
296             sub CLEAR {
297 1     1   333 my $self=shift;
298 1         8 $self->{CURRENT_STATE}=[];
299             }
300              
301             sub STORE {
302 52     52   81 my($self, $index, $value)=@_;
303 52         79 my $newobj = $value;
304 52 100       113 if(ref($value)) {
305 3 50       17 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         5 $newobj = Data::Transactional->new(type => 'HASH');
311             # %{$newobj} = %{$value};
312 2         3 $newobj->{$_} = $value->{$_} foreach(keys %{$value});
  2         30  
313             } else {
314 1         10 die(__PACKAGE__."::STORE(): don't know how to store a ".ref($value)."\n");
315             }
316             }
317 51         212 $self->{CURRENT_STATE}->[$index] = $newobj;
318             }
319              
320             sub FETCH {
321 10     10   70 my($self, $index) = @_;
322 10         39 $self->{CURRENT_STATE}->[$index];
323             }
324              
325 3     3   693 sub DELETE { my($self, $index) = @_; delete $self->{CURRENT_STATE}->[$index]; }
  3         13  
326 2     2   428 sub EXISTS { my($self, $index) = @_; exists($self->{CURRENT_STATE}->[$index]); }
  2         9  
327 2     2   400 sub POP { my $self = shift; pop @{$self->{CURRENT_STATE}}; }
  2         4  
  2         10  
328 1     1   347 sub SHIFT { my $self = shift; shift @{$self->{CURRENT_STATE}}; }
  1         2  
  1         6  
329              
330             sub PUSH {
331 42     42   409 my($self, @list) = @_;
332 42         114 $self->STORE($self->FETCHSIZE(), $_) foreach (@list);
333             }
334              
335             sub UNSHIFT {
336 1     1   352 my($self, @list) = @_;
337 1         3 my @oldlist = @{$self->{CURRENT_STATE}};
  1         4  
338             # shuffle existing contents along
339 1         4 for(my $i = $self->FETCHSIZE() - 1; $i >= 0; $i--) {
340             $self->{CURRENT_STATE}->[$i + scalar(@list)] =
341 4         18 $self->{CURRENT_STATE}->[$i];
342             }
343 1         8 $self->STORE($_, $list[$_]) foreach(0..$#list);
344 1         5 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   90 sub FETCHSIZE { my $self = shift; scalar(@{$self->{CURRENT_STATE}}); }
  51         54  
  51         159  
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   7 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   173 shift->{CURRENT_STATE};
380             }