8b7328a16037de22fdcd849036e63ea946ec6628
[dbsrgits/DBM-Deep.git] / lib / DBM / Deep.pm
1 package DBM::Deep;
2
3 use 5.006_000;
4
5 use strict;
6 use warnings FATAL => 'all';
7
8 our $VERSION = q(1.0015);
9
10 use Scalar::Util ();
11
12 use DBM::Deep::Engine::File ();
13
14 use DBM::Deep::SQL::Util;
15 use DBM::Deep::SQL::Array;
16 use DBM::Deep::SQL::Hash;
17
18 use overload
19     '""' => sub { overload::StrVal( $_[0] ) },
20     fallback => 1;
21
22 use constant DEBUG => 0;
23
24 sub TYPE_HASH   () { DBM::Deep::Engine->SIG_HASH  }
25 sub TYPE_ARRAY  () { DBM::Deep::Engine->SIG_ARRAY }
26
27 # This is used in all the children of this class in their TIE<type> methods.
28 sub _get_args {
29     my $proto = shift;
30
31     my $args;
32     if (scalar(@_) > 1) {
33         if ( @_ % 2 ) {
34             $proto->_throw_error( "Odd number of parameters to " . (caller(1))[2] );
35         }
36         $args = {@_};
37     }
38     elsif ( ref $_[0] ) {
39         unless ( eval { local $SIG{'__DIE__'}; %{$_[0]} || 1 } ) {
40             $proto->_throw_error( "Not a hashref in args to " . (caller(1))[2] );
41         }
42         $args = $_[0];
43     }
44     else {
45         $args = { file => shift };
46     }
47
48     return $args;
49 }
50
51 # Class constructor method for Perl OO interface.
52 # Calls tie() and returns blessed reference to tied hash or array,
53 # providing a hybrid OO/tie interface.
54 sub new {
55     my $class = shift;
56     my $args = $class->_get_args( @_ );
57     my $self;
58     
59     ##
60     # Check for SQL storage
61     ##
62     if (exists $args->{dbi}) {
63         eval {
64             require DBIx::Abstract;
65         }; if ( $@ ) {
66             DBM::Deep->_throw_error('DBIx::Abstract not installed. You cannot use the SQL mode.');
67         }
68         unless (UNIVERSAL::isa($args->{dbi}, 'DBIx::Abstract')) {
69             $args->{dbi} = DBIx::Abstract->connect($args->{dbi});
70         }
71
72         if (defined $args->{id}) {
73             unless ($args->{id} =~ /^\d+$/ && $args->{id} > 0) {
74                 DBM::Deep->_throw_error('Invalid SQL record id');
75             }
76             my $util = {dbi => $args->{dbi}};
77             bless $util, 'DBM::Deep::SQL::Util';
78             my $q = $util->_select(
79                 table  => 'rec_item',
80                 fields => 'item_type',
81                 where  => {id => $args->{id}},
82             );
83             if ($q->[0]->[0] eq 'array') {
84                 $args->{type} = TYPE_ARRAY;
85             }
86             elsif ($q->[0]->[0] eq 'hash') {
87                 $args->{type} = TYPE_HASH;
88             }
89             else {
90                 DBM::Deep->_throw_error('Unknown SQL record id');
91             }
92         }
93         else {
94             my $util = {dbi => $args->{dbi}};
95             bless $util, 'DBM::Deep::SQL::Util';
96             if (defined($args->{type}) && $args->{type} eq TYPE_ARRAY) {
97                 $args->{id} = $util->_create('array');
98             }
99             else {
100                 $args->{id} = $util->_create('hash');
101             }
102         }
103
104         if (defined($args->{type}) && $args->{type} eq TYPE_ARRAY) {
105             $class = 'DBM::Deep::SQL::Array';
106             require DBM::Deep::SQL::Array;
107             tie @$self, $class, %$args;
108             if ($args->{prefetch}) {
109                 (tied(@$self))->_prefetch();
110             }
111             return bless $self, $class;
112         }
113         else {
114             $class = 'DBM::Deep::SQL::Hash';
115             require DBM::Deep::SQL::Hash;
116             tie %$self, $class, %$args;
117             if ($args->{prefetch}) {
118                 (tied(%$self))->_prefetch();
119             }
120             return bless $self, $class;
121         }
122     }
123
124     ##
125     # Check if we want a tied hash or array.
126     ##
127     if (defined($args->{type}) && $args->{type} eq TYPE_ARRAY) {
128         $class = 'DBM::Deep::Array';
129         require DBM::Deep::Array;
130         tie @$self, $class, %$args;
131     }
132     else {
133         $class = 'DBM::Deep::Hash';
134         require DBM::Deep::Hash;
135         tie %$self, $class, %$args;
136     }
137
138     return bless $self, $class;
139 }
140
141 # This initializer is called from the various TIE* methods. new() calls tie(),
142 # which allows for a single point of entry.
143 sub _init {
144     my $class = shift;
145     my ($args) = @_;
146
147     # locking implicitly enables autoflush
148     if ($args->{locking}) { $args->{autoflush} = 1; }
149
150     # These are the defaults to be optionally overridden below
151     my $self = bless {
152         type        => TYPE_HASH,
153         base_offset => undef,
154         staleness   => undef,
155         engine      => undef,
156     }, $class;
157
158     $args->{engine} = DBM::Deep::Engine::File->new( { %{$args}, obj => $self } )
159         unless exists $args->{engine};
160
161     # Grab the parameters we want to use
162     foreach my $param ( keys %$self ) {
163         next unless exists $args->{$param};
164         $self->{$param} = $args->{$param};
165     }
166
167     eval {
168       local $SIG{'__DIE__'};
169
170       $self->lock_exclusive;
171       $self->_engine->setup_fh( $self );
172       $self->unlock;
173     }; if ( $@ ) {
174       my $e = $@;
175       eval { local $SIG{'__DIE__'}; $self->unlock; };
176       die $e;
177     }
178
179     return $self;
180 }
181
182 sub TIEHASH {
183     shift;
184     require DBM::Deep::Hash;
185     return DBM::Deep::Hash->TIEHASH( @_ );
186 }
187
188 sub TIEARRAY {
189     shift;
190     require DBM::Deep::Array;
191     return DBM::Deep::Array->TIEARRAY( @_ );
192 }
193
194 sub lock_exclusive {
195     my $self = shift->_get_self;
196     return $self->_engine->lock_exclusive( $self, @_ );
197 }
198 *lock = \&lock_exclusive;
199 sub lock_shared {
200     my $self = shift->_get_self;
201     return $self->_engine->lock_shared( $self, @_ );
202 }
203
204 sub unlock {
205     my $self = shift->_get_self;
206     return $self->_engine->unlock( $self, @_ );
207 }
208
209 sub _copy_value {
210     my $self = shift->_get_self;
211     my ($spot, $value) = @_;
212
213     if ( !ref $value ) {
214         ${$spot} = $value;
215     }
216     else {
217         my $r = Scalar::Util::reftype( $value );
218         my $tied;
219         if ( $r eq 'ARRAY' ) {
220             $tied = tied(@$value);
221         }
222         # This assumes hash or array only. This is a bad assumption moving
223         # forward. -RobK, 2008-05-27
224         else {
225             $tied = tied(%$value);
226         }
227
228         if ( eval { local $SIG{__DIE__}; $tied->isa( 'DBM::Deep' ) } ) {
229             ${$spot} = $tied->_repr;
230             $tied->_copy_node( ${$spot} );
231         }
232         else {
233             if ( $r eq 'ARRAY' ) {
234                 ${$spot} = [ @{$value} ];
235             }
236             else {
237                 ${$spot} = { %{$value} };
238             }
239         }
240
241         my $c = Scalar::Util::blessed( $value );
242         if ( defined $c && !$c->isa( 'DBM::Deep') ) {
243             ${$spot} = bless ${$spot}, $c
244         }
245     }
246
247     return 1;
248 }
249
250 #sub _copy_node {
251 #    die "Must be implemented in a child class\n";
252 #}
253 #
254 #sub _repr {
255 #    die "Must be implemented in a child class\n";
256 #}
257
258 sub export {
259     my $self = shift->_get_self;
260
261     my $temp = $self->_repr;
262
263     $self->lock_exclusive;
264     $self->_copy_node( $temp );
265     $self->unlock;
266
267     my $classname = $self->_engine->get_classname( $self );
268     if ( defined $classname ) {
269       bless $temp, $classname;
270     }
271
272     return $temp;
273 }
274
275 sub _check_legality {
276     my $self = shift;
277     my ($val) = @_;
278
279     my $r = Scalar::Util::reftype( $val );
280
281     return $r if !defined $r || '' eq $r;
282     return $r if 'HASH' eq $r;
283     return $r if 'ARRAY' eq $r;
284
285     DBM::Deep->_throw_error(
286         "Storage of references of type '$r' is not supported."
287     );
288 }
289
290 sub import {
291     return if !ref $_[0]; # Perl calls import() on use -- ignore
292
293     my $self = shift->_get_self;
294     my ($struct) = @_;
295
296     my $type = $self->_check_legality( $struct );
297     if ( !$type ) {
298         DBM::Deep->_throw_error( "Cannot import a scalar" );
299     }
300
301     if ( substr( $type, 0, 1 ) ne $self->_type ) {
302         DBM::Deep->_throw_error(
303             "Cannot import " . ('HASH' eq $type ? 'a hash' : 'an array')
304             . " into " . ('HASH' eq $type ? 'an array' : 'a hash')
305         );
306     }
307
308     my %seen;
309     my $recurse;
310     $recurse = sub {
311         my ($db, $val) = @_;
312
313         my $obj = 'HASH' eq Scalar::Util::reftype( $db ) ? tied(%$db) : tied(@$db);
314         $obj ||= $db;
315
316         my $r = $self->_check_legality( $val );
317         if ( 'HASH' eq $r ) {
318             while ( my ($k, $v) = each %$val ) {
319                 my $r = $self->_check_legality( $v );
320                 if ( $r ) {
321                     my $temp = 'HASH' eq $r ? {} : [];
322                     if ( my $c = Scalar::Util::blessed( $v ) ) {
323                         bless $temp, $c;
324                     }
325                     $obj->put( $k, $temp );
326                     $recurse->( $temp, $v );
327                 }
328                 else {
329                     $obj->put( $k, $v );
330                 }
331             }
332         }
333         elsif ( 'ARRAY' eq $r ) {
334             foreach my $k ( 0 .. $#$val ) {
335                 my $v = $val->[$k];
336                 my $r = $self->_check_legality( $v );
337                 if ( $r ) {
338                     my $temp = 'HASH' eq $r ? {} : [];
339                     if ( my $c = Scalar::Util::blessed( $v ) ) {
340                         bless $temp, $c;
341                     }
342                     $obj->put( $k, $temp );
343                     $recurse->( $temp, $v );
344                 }
345                 else {
346                     $obj->put( $k, $v );
347                 }
348             }
349         }
350     };
351     $recurse->( $self, $struct );
352
353     return 1;
354 }
355
356 #XXX Need to keep track of who has a fh to this file in order to
357 #XXX close them all prior to optimize on Win32/cygwin
358 # Rebuild entire database into new file, then move
359 # it back on top of original.
360 sub optimize {
361     my $self = shift->_get_self;
362
363     # Optimizing is only something we need to do when we're working with our
364     # own file format. Otherwise, let the other guy do the optimizations.
365     return unless $self->_engine->isa( 'DBM::Deep::Engine::File' );
366
367 #XXX Need to create a new test for this
368 #    if ($self->_engine->storage->{links} > 1) {
369 #        $self->_throw_error("Cannot optimize: reference count is greater than 1");
370 #    }
371
372     #XXX Do we have to lock the tempfile?
373
374     #XXX Should we use tempfile() here instead of a hard-coded name?
375     my $temp_filename = $self->_engine->storage->{file} . '.tmp';
376     my $db_temp = DBM::Deep->new(
377         file => $temp_filename,
378         type => $self->_type,
379
380         # Bring over all the parameters that we need to bring over
381         ( map { $_ => $self->_engine->$_ } qw(
382             byte_size max_buckets data_sector_size num_txns
383         )),
384     );
385
386     $self->lock_exclusive;
387     $self->_engine->clear_cache;
388     $self->_copy_node( $db_temp );
389     $db_temp->_engine->storage->close;
390     undef $db_temp;
391
392     ##
393     # Attempt to copy user, group and permissions over to new file
394     ##
395     $self->_engine->storage->copy_stats( $temp_filename );
396
397     # q.v. perlport for more information on this variable
398     if ( $^O eq 'MSWin32' || $^O eq 'cygwin' ) {
399         ##
400         # Potential race condition when optmizing on Win32 with locking.
401         # The Windows filesystem requires that the filehandle be closed
402         # before it is overwritten with rename().  This could be redone
403         # with a soft copy.
404         ##
405         $self->unlock;
406         $self->_engine->storage->close;
407     }
408
409     if (!rename $temp_filename, $self->_engine->storage->{file}) {
410         unlink $temp_filename;
411         $self->unlock;
412         $self->_throw_error("Optimize failed: Cannot copy temp file over original: $!");
413     }
414
415     $self->unlock;
416     $self->_engine->storage->close;
417
418     $self->_engine->storage->open;
419     $self->lock_exclusive;
420     $self->_engine->setup_fh( $self );
421     $self->unlock;
422
423     return 1;
424 }
425
426 sub clone {
427     ##
428     # Make copy of object and return
429     ##
430     my $self = shift->_get_self;
431
432     return DBM::Deep->new(
433         type        => $self->_type,
434         base_offset => $self->_base_offset,
435         staleness   => $self->_staleness,
436         engine      => $self->_engine,
437     );
438 }
439
440 #XXX Migrate this to the engine, where it really belongs and go through some
441 # API - stop poking in the innards of someone else..
442 {
443     my %is_legal_filter = map {
444         $_ => ~~1,
445     } qw(
446         store_key store_value
447         fetch_key fetch_value
448     );
449
450     sub set_filter {
451         my $self = shift->_get_self;
452         my $type = lc shift;
453         my $func = shift;
454
455         if ( $is_legal_filter{$type} ) {
456             $self->_engine->storage->{"filter_$type"} = $func;
457             return 1;
458         }
459
460         return;
461     }
462
463     sub filter_store_key   { $_[0]->set_filter( store_key   => $_[1] ); }
464     sub filter_store_value { $_[0]->set_filter( store_value => $_[1] ); }
465     sub filter_fetch_key   { $_[0]->set_filter( fetch_key   => $_[1] ); }
466     sub filter_fetch_value { $_[0]->set_filter( fetch_value => $_[1] ); }
467 }
468
469 sub begin_work {
470     my $self = shift->_get_self;
471     $self->lock_exclusive;
472     my $rv = eval { $self->_engine->begin_work( $self, @_ ) };
473     my $e = $@;
474     $self->unlock;
475     die $e if $e;
476     return $rv;
477 }
478
479 sub rollback {
480     my $self = shift->_get_self;
481     $self->lock_exclusive;
482     my $rv = eval { $self->_engine->rollback( $self, @_ ) };
483     my $e = $@;
484     $self->unlock;
485     die $e if $e;
486     return $rv;
487 }
488
489 sub commit {
490     my $self = shift->_get_self;
491     $self->lock_exclusive;
492     my $rv = eval { $self->_engine->commit( $self, @_ ) };
493     my $e = $@;
494     $self->unlock;
495     die $e if $e;
496     return $rv;
497 }
498
499 # Accessor methods
500 sub _engine {
501     my $self = $_[0]->_get_self;
502     return $self->{engine};
503 }
504
505 sub _type {
506     my $self = $_[0]->_get_self;
507     return $self->{type};
508 }
509
510 sub _base_offset {
511     my $self = $_[0]->_get_self;
512     return $self->{base_offset};
513 }
514
515 sub _staleness {
516     my $self = $_[0]->_get_self;
517     return $self->{staleness};
518 }
519
520 # Utility methods
521 sub _throw_error {
522     my $n = 0;
523     while( 1 ) {
524         my @caller = caller( ++$n );
525         next if $caller[0] =~ m/^DBM::Deep/;
526
527         die "DBM::Deep: $_[1] at $0 line $caller[2]\n";
528     }
529 }
530
531 # Store single hash key/value or array element in database.
532 sub STORE {
533     my $self = shift->_get_self;
534     my ($key, $value) = @_;
535     warn "STORE($self, '$key', '@{[defined$value?$value:'undef']}')\n" if DEBUG;
536
537     unless ( $self->_engine->storage->is_writable ) {
538         $self->_throw_error( 'Cannot write to a readonly filehandle' );
539     }
540
541     $self->lock_exclusive;
542
543     # User may be storing a complex value, in which case we do not want it run
544     # through the filtering system.
545     if ( !ref($value) && $self->_engine->storage->{filter_store_value} ) {
546         $value = $self->_engine->storage->{filter_store_value}->( $value );
547     }
548
549     $self->_engine->write_value( $self, $key, $value);
550
551     $self->unlock;
552
553     return 1;
554 }
555
556 # Fetch single value or element given plain key or array index
557 sub FETCH {
558     my $self = shift->_get_self;
559     my ($key) = @_;
560     warn "FETCH($self, '$key')\n" if DEBUG;
561
562     $self->lock_shared;
563
564     my $result = $self->_engine->read_value( $self, $key);
565
566     $self->unlock;
567
568     # Filters only apply to scalar values, so the ref check is making
569     # sure the fetched bucket is a scalar, not a child hash or array.
570     return ($result && !ref($result) && $self->_engine->storage->{filter_fetch_value})
571         ? $self->_engine->storage->{filter_fetch_value}->($result)
572         : $result;
573 }
574
575 # Delete single key/value pair or element given plain key or array index
576 sub DELETE {
577     my $self = shift->_get_self;
578     my ($key) = @_;
579     warn "DELETE($self, '$key')\n" if DEBUG;
580
581     unless ( $self->_engine->storage->is_writable ) {
582         $self->_throw_error( 'Cannot write to a readonly filehandle' );
583     }
584
585     $self->lock_exclusive;
586
587     ##
588     # Delete bucket
589     ##
590     my $value = $self->_engine->delete_key( $self, $key);
591
592     if (defined $value && !ref($value) && $self->_engine->storage->{filter_fetch_value}) {
593         $value = $self->_engine->storage->{filter_fetch_value}->($value);
594     }
595
596     $self->unlock;
597
598     return $value;
599 }
600
601 # Check if a single key or element exists given plain key or array index
602 sub EXISTS {
603     my $self = shift->_get_self;
604     my ($key) = @_;
605     warn "EXISTS($self, '$key')\n" if DEBUG;
606
607     $self->lock_shared;
608
609     my $result = $self->_engine->key_exists( $self, $key );
610
611     $self->unlock;
612
613     return $result;
614 }
615
616 # Clear all keys from hash, or all elements from array.
617 sub CLEAR {
618     my $self = shift->_get_self;
619     warn "CLEAR($self)\n" if DEBUG;
620
621     unless ( $self->_engine->storage->is_writable ) {
622         $self->_throw_error( 'Cannot write to a readonly filehandle' );
623     }
624
625     $self->lock_exclusive;
626
627     #XXX Rewrite this dreck to do it in the engine as a tight loop vs.
628     # iterating over keys - such a WASTE - is this required for transactional
629     # clearning?! Surely that can be detected in the engine ...
630     if ( $self->_type eq TYPE_HASH ) {
631         my $key = $self->first_key;
632         while ( $key ) {
633             # Retrieve the key before deleting because we depend on next_key
634             my $next_key = $self->next_key( $key );
635             $self->_engine->delete_key( $self, $key, $key );
636             $key = $next_key;
637         }
638     }
639     else {
640         my $size = $self->FETCHSIZE;
641         for my $key ( 0 .. $size - 1 ) {
642             $self->_engine->delete_key( $self, $key, $key );
643         }
644         $self->STORESIZE( 0 );
645     }
646
647     $self->unlock;
648
649     return 1;
650 }
651
652 # Public method aliases
653 sub put    { (shift)->STORE( @_ )  }
654 sub get    { (shift)->FETCH( @_ )  }
655 sub store  { (shift)->STORE( @_ )  }
656 sub fetch  { (shift)->FETCH( @_ )  }
657 sub delete { (shift)->DELETE( @_ ) }
658 sub exists { (shift)->EXISTS( @_ ) }
659 sub clear  { (shift)->CLEAR( @_ )  }
660
661 sub _dump_file {shift->_get_self->_engine->_dump_file;}
662
663 1;
664 __END__