File Coverage

blib/lib/Pixie/Store/DBI/Default.pm
Criterion Covered Total %
statement 15 215 6.9
branch 0 56 0.0
condition 0 9 0.0
subroutine 5 45 11.1
pod 7 30 23.3
total 27 355 7.6


line stmt bran cond sub pod time code
1             ##
2             # NAME
3             # Pixie::Store::DBI::Default - base class for DBI stores
4             #
5             # SYNOPSIS
6             # # see Pixie::Store::DBI
7             #
8             # DESCRIPTION
9             # This contains a lot of common code for all DBI stores. It's based on
10             # DBIx::AnyDBD, so you should read that to understand how subclassing
11             # works.
12             ##
13              
14             package Pixie::Store::DBI::Default;
15              
16 5     5   30 use strict;
  5         12  
  5         207  
17              
18 5     5   30 use Carp qw( croak confess );
  5         12  
  5         390  
19 5     5   5269 use Storable qw( nfreeze thaw );
  5         17458  
  5         411  
20 5     5   45 use DBIx::AnyDBD;
  5         22  
  5         131  
21              
22 5     5   28 use base qw( Pixie::Store );
  5         11  
  5         7078  
23              
24             our $VERSION = "2.08_02";
25              
26             ## TODO: timeouts should really be part of a locking strategy
27             our $LOCK_TIMEOUT = 30;
28              
29             sub _raw_connect {
30 0     0     my $class = shift;
31 0           my($dsn, %args) = @_;
32              
33 0           $dsn =~ s/^(dbi:)?/dbi:/;
34              
35 0           my $dbi_args = {AutoCommit => 1, PrintError => 0, RaiseError => 1,};
36              
37 0           my $self = DBIx::AnyDBD->connect( $dsn,
38             $args{user},
39             $args{pass},
40             $dbi_args,
41             'Pixie::Store::DBI' );
42 0 0         return unless $self;
43              
44             $self->{reconnector} = sub {
45 0     0     DBI->connect( $dsn,
46             $args{user},
47             $args{pass},
48             $dbi_args )
49 0           };
50              
51 0   0       $self->set_object_table( $args{object_table} || 'px_object');
52 0   0       $self->set_lock_table( $args{lock_table} || 'px_lock_info');
53 0   0       $self->set_rootset_table( $args{rootset_table} || 'px_rootset');
54 0           return $self;
55             }
56              
57             sub connect {
58 0     0     my $proto = shift;
59 0           my $self = $proto->_raw_connect(@_);
60 0           $self->verify_connection;
61 0           return $self;
62             }
63              
64             sub deploy {
65 0     0     my $proto = shift;
66 0           my $self = $proto->_raw_connect(@_);
67 0           eval { $self->verify_connection };
  0            
68 0           $self->_do_deployment->verify_connection;
69 0           return $self;
70             }
71              
72             sub _do_deployment {
73 0     0     my $self = shift;
74 0           $self->create_object_table
75             ->create_lock_table
76             ->create_table_index
77             ->create_rootset_table
78             }
79              
80             sub create_object_table {
81 0     0 1   my $self = shift;
82 0           $self->{dbh}->do(qq{CREATE TABLE $self->{object_table}
83             (px_oid varchar(255) NOT NULL,
84             px_flat_obj blob NOT NULL,
85             PRIMARY KEY (px_oid))});
86 0           return $self;
87             }
88              
89             sub create_lock_table {
90 0     0 0   my $self = shift;
91 0           $self->{dbh}->do(qq{CREATE TABLE $self->{lock_table}
92             (px_oid varchar(255) NOT NULL,
93             px_locker varchar(255) NOT NULL,
94             PRIMARY KEY (px_oid))});
95 0           return $self;
96             }
97              
98             sub create_rootset_table {
99 0     0 0   my $self = shift;
100 0           $self->{dbh}->do(qq{CREATE TABLE $self->{rootset_table}
101             (px_oid varchar(255) NOT NULL,
102             PRIMARY KEY (px_oid))});
103 0           return $self;
104             }
105              
106             sub create_table_index {
107 0     0 0   return $_[0];
108             }
109              
110             ## TODO: replace with single accessor
111             sub set_object_table {
112 0     0 0   my $self = shift;
113 0           $self->{object_table} = shift;
114 0           return $self;
115             }
116              
117             ## TODO: replace with single accessor
118             sub object_table {
119 0     0 0   my $self = shift;
120 0           $self->{object_table};
121             }
122              
123             ## TODO: replace with single accessor
124             sub set_lock_table {
125 0     0 0   my $self = shift;
126 0           $self->{lock_table} = shift;
127 0           return $self;
128             }
129              
130             ## TODO: replace with single accessor
131             sub lock_table {
132 0     0 0   my $self = shift;
133 0           $self->{lock_table};
134             }
135              
136             ## TODO: replace with single accessor
137             sub set_rootset_table {
138 0     0 0   my $self = shift;
139 0           $self->{rootset_table} = shift;
140 0           return $self;
141             }
142              
143             ## TODO: replace with single accessor
144             sub rootset_table {
145 0     0 0   my $self = shift;
146 0           $self->{rootset_table};
147             }
148              
149             sub verify_connection {
150 0     0 0   my $self = shift;
151 0           my $sth = $self->prepare_execute(qq{SELECT px_oid} .
152             qq{ FROM $self->{object_table} LIMIT 1});
153 0 0         $sth->finish if $sth;
154              
155             # TODO:
156             # The following might be better - it wouldn't require you to eval this method
157             # and so may be a bit faster. Unfortunately it breaks the dbi store tests
158             # (11dbistore.t)
159             # -spurkis
160              
161             #my $dbh = $self->get_dbh;
162             #return $self if ($dbh && $dbh->ping);
163             #$self->reconnect;
164              
165 0           return $self;
166             }
167              
168 0     0     sub _init { $_[0] }
169              
170             sub reconnect {
171 0     0 0   my $self = shift;
172 0 0         my $reconnector = $self->{reconnector}
173             or confess( "Can't reconnect; reconnector is missing." );
174              
175 0 0         $self->{'dbh'}->disconnect if ($self->{'dbh'});
176              
177 0 0         $self->{'dbh'} = &$reconnector()
178             or confess( "Can't reconnect; reconnector returned nothing." );
179 0           $self->rebless;
180 0 0         $self->_init if $self->can('_init');
181              
182 0           return $self;
183             }
184              
185             ## TODO: replace with a single accessor, kill all DVA
186             sub get_dbh {
187 0     0 0   my $self = shift;
188 0 0         (ref $self) or confess( "Not a class method" );
189 0           return $self->{'dbh'};
190             }
191              
192              
193             sub clear {
194 0     0 1   my $self = shift;
195 0           $self->prepare_execute(qq{DELETE FROM $_}) for map $self->$_(),
196             qw/object_table lock_table rootset_table/;
197 0           return $self;
198             }
199              
200              
201             sub store_at {
202 0     0 1   my $self = shift;
203 0           my($oid, $obj, $strategy) = @_;
204              
205 0           $self->begin_transaction;
206 0           my $did_lock = $strategy->pre_store($oid, Pixie->get_the_current_pixie);
207 0           $self->prepare_execute(qq{ DELETE FROM @{[ $self->object_table ]}
  0            
208             WHERE px_oid = ? },
209             $oid);
210 0           $self->prepare_execute(qq{ INSERT INTO @{[ $self->object_table ]}
  0            
211             (px_oid, px_flat_obj)
212             VALUES ( ?, ? )},
213             $oid, nfreeze $obj);
214 0           $strategy->post_store($did_lock, Pixie->get_the_current_pixie);
215 0           $self->commit;
216 0           return($oid, $obj);
217             }
218              
219             sub get_object_at {
220 0     0 1   my $self = shift;
221 0           my($oid) = @_;
222              
223 0           my $sth = $self->prepare_execute( q{SELECT px_flat_obj FROM } .
224             $self->{object_table} .
225             q{ WHERE px_oid = ? },
226             $oid );
227 0           my $rows = $sth->fetchall_arrayref();
228 0           $sth->finish;
229              
230 0 0         if (@$rows == 0) {
    0          
231 0           return;
232             }
233             elsif (@$rows == 1) {
234 0           return thaw $rows->[0][0];
235             }
236             else {
237 0           confess( "Too many objects matched OID: $oid" );
238             }
239             }
240              
241             sub _delete {
242 0     0     my $self = shift;
243 0           my($oid) = shift;
244              
245 0           $self->prepare_execute(q{DELETE FROM } .
246             $self->object_table .
247             q{ WHERE px_oid = ?}, $oid)->rows;
248             }
249              
250             sub prepare_execute {
251 0     0 0   my($self, $sql, @params) = @_;
252              
253 0           my $sth = $self->prepare_cached($sql);
254 0           for my $param_no ( 0 .. $#params ) {
255 0           my $param_v = $params[$param_no];
256 0 0         my @param_v = ( ref($param_v) eq 'ARRAY' ) ? @$param_v : $param_v;
257 0           $sth->bind_param( $param_no+1, @param_v);
258             }
259              
260 0           eval { $sth->execute };
  0            
261 0 0         confess( $@ ) if $@;
262              
263 0           return $sth;
264             }
265              
266             sub begin_transaction {
267 0     0 0   my $self = shift;
268              
269 0 0         $self->{tran_count} = 0 unless defined $self->{tran_count};
270 0           $self->{tran_count}++;
271              
272             # reconnect as needed
273             # -spurkis
274 0           eval { $self->verify_connection };
  0            
275 0 0         $self->reconnect if $@;
276              
277 0           $self->get_dbh->{AutoCommit} = 0;
278              
279 0           return $self;
280             }
281              
282             ## TODO: tran_count is never checked - what are we rolling back to?
283             sub rollback_db {
284 0     0 0   my $self = shift;
285 0           my $dbh = $self->get_dbh;
286              
287 0 0         $dbh->rollback unless ( $dbh->{AutoCommit} );
288              
289 0           $dbh->{AutoCommit} = 1;
290 0           $self->{tran_count} = undef;
291             }
292              
293             sub commit {
294 0     0 0   my $self = shift;
295 0           my $dbh = $self->get_dbh;
296              
297 0 0         $dbh->commit unless ( $dbh->{AutoCommit} );
298              
299 0           $dbh->{AutoCommit} = 1;
300 0           $self->{tran_count} = undef;
301             }
302              
303             sub lock {
304 0     0 1   my $self = shift;
305 0           $self->begin_transaction;
306 0           return $self;
307             }
308              
309             sub unlock {
310 0     0 1   my $self = shift;
311 0           $self->commit;
312 0           return $self;
313             }
314              
315             sub rollback {
316 0     0 1   my $self = shift;
317 0           $self->rollback_db;
318 0           return $self;
319             }
320              
321             sub remove_from_rootset {
322 0     0 0   my $self = shift;
323 0           my($oid) = @_;
324              
325 0           $self->prepare_execute(qq{DELETE FROM @{[ $self->rootset_table ]}
  0            
326             WHERE px_oid = ?},
327             $oid);
328 0           return $self;
329             }
330              
331             sub _add_to_rootset {
332 0     0     my $self = shift;
333 0           my($thing) = @_;
334 0           $self->begin_transaction;
335 0           $self->prepare_execute(qq{DELETE FROM @{[ $self->rootset_table ]}
  0            
336             WHERE px_oid = ?},
337             $thing->PIXIE::oid);
338 0           $self->prepare_execute(qq{INSERT INTO @{[ $self->rootset_table ]} (px_oid)
  0            
339             VALUES ( ? )},
340             $thing->PIXIE::oid);
341 0           $self->commit;
342 0           return $self;
343             }
344              
345             sub rootset {
346 0     0 0   my $self = shift;
347             # TODO: use Pixie::Name instead of hard-coded text
348 0           my $rows = $self->selectall_arrayref(qq{SELECT px_oid FROM @{[$self->rootset_table]}
  0            
349             WHERE px_oid NOT LIKE '
350 0           my @ary = map $_->[0], @$rows;
351 0 0         return wantarray ? @ary : \@ary;
352             }
353              
354             sub working_set_for {
355 0     0 0   my $self = shift;
356 0           my $p = shift;
357             # TODO: use Pixie::Name instead of hard-coded text
358 0           my $rows = $self->selectall_arrayref(
359 0           qq{SELECT px_oid FROM @{[$self->object_table]}
  0            
360             WHERE px_oid NOT LIKE '
361             NOT(px_oid = '@{[$self->object_graph_for($p)->PIXIE::oid]}')});
362 0           my @ary = map $_->[0], @$rows;
363 0 0         return wantarray ? @ary : \@ary;
364             }
365              
366             ## TODO: there must be a better way to do this!
367             ## table or row-level locking, perhaps?
368             sub lock_object_for {
369 0     0 0   my $self = shift;
370 0           my($oid, $pixie, $timeout) = @_;
371              
372 0 0         $timeout = $LOCK_TIMEOUT unless defined $timeout;
373 0           my $lock_holder = $self->locker_for($oid);
374 0 0         return 0 if $lock_holder eq $pixie->_oid;
375              
376 0           my $keep_trying = 1;
377              
378 0     0     local $SIG{ALRM} = sub { $keep_trying = 0 };
  0            
379 0           alarm $timeout;
380              
381 0           while ($keep_trying) {
382 0           eval {$self->prepare_execute(q{INSERT INTO } . $self->lock_table .
  0            
383             q{ ( px_oid, px_locker ) } .
384             q{ VALUES ( ?, ? ) },
385             $oid, $pixie->_oid)};
386 0 0 0       last unless $keep_trying && $@;
387             ## TODO: replace this with Time::HiRes::usleep ?
388 0           select undef, undef, undef, rand(2 * 1000);
389             }
390              
391             ## TODO: restore previous value of alarm?
392 0           alarm 0;
393              
394 0           $lock_holder = $self->locker_for($oid);
395 0 0         unless ($lock_holder eq $pixie->_oid) {
396 0           confess "Cannot lock $oid for $pixie. Lock is held by $lock_holder";
397             }
398              
399 0           $self->SUPER::lock_object_for($oid, $pixie);
400              
401 0           return 1;
402             }
403              
404             sub unlock_object_for {
405 0     0 0   my $self = shift;
406 0           my($oid, $pixie) = @_;
407 0 0         my $pixie_oid = ref($pixie) ? $pixie->_oid : $pixie;
408              
409 0           eval { $self->prepare_execute(q{DELETE FROM } . $self->lock_table .
  0            
410             q{ WHERE px_oid = ? AND px_locker = ? },
411             $oid, $pixie_oid) };
412 0 0         confess( "Couldn't unlock $oid for $pixie_oid: $@" ) if $@;
413              
414 0 0         if ( my $other_locker = $self->locker_for($oid) ) {
415 0           confess "Couldn't unlock $oid for $pixie_oid, it's locked by another pixie: $other_locker";
416             }
417              
418 0           $self->SUPER::unlock_object_for($oid, $pixie);
419              
420 0           return 1;
421             }
422              
423             sub locker_for {
424 0     0 0   my $self = shift;
425 0           my($oid) = @_;
426 0 0         $oid = $oid->px_oid if ref $oid;
427 0           my $sth = $self->prepare_execute(q{SELECT px_locker FROM } . $self->lock_table .
428             q{ WHERE px_oid = ? },
429             $oid);
430 0           my $rows = $sth->fetchall_arrayref();
431 0           $sth->finish;
432 0 0         if ( @$rows == 0 ) {
    0          
433 0           return '';
434             }
435             elsif ( @$rows == 1 ) {
436 0           return $rows->[0][0];
437             }
438             else {
439 0           confess( "Too many objects matched OID: $oid" );
440             }
441             }
442              
443             sub release_all_locks {
444 0     0 0   my $self = shift;
445             # Ensure a connection
446 0 0         $self->{dbh} = &{$self->{reconnector}} unless $self->{dbh};
  0            
447 0           $self->SUPER::release_all_locks;
448             }
449              
450             sub DESTROY {
451 0     0     my $self = shift;
452 0           $self->release_all_locks;
453 0           $self->SUPER::DESTROY;
454             }
455              
456             1;