Moved _bucket_exists to Engine
[dbsrgits/DBM-Deep.git] / lib / DBM / Deep.pm
index 0a5a09a..e08ba84 100644 (file)
@@ -24,19 +24,21 @@ package DBM::Deep;
 #      print "This module " . $db->{my_complex}->[1]->{perl} . "!\n";
 #
 # Copyright:
-#      (c) 2002-2005 Joseph Huckaby.  All Rights Reserved.
+#      (c) 2002-2006 Joseph Huckaby.  All Rights Reserved.
 #      This program is free software; you can redistribute it and/or 
 #      modify it under the same terms as Perl itself.
 ##
 
 use strict;
 
-use Fcntl qw/:flock/;
+use Fcntl qw( :DEFAULT :flock :seek );
 use Digest::MD5 ();
 use Scalar::Util ();
-use vars qw/$VERSION/;
 
-$VERSION = "0.96";
+use DBM::Deep::Engine;
+
+use vars qw( $VERSION );
+$VERSION = q(0.99_01);
 
 ##
 # Set to 4 and 'N' for 32-bit offset tags (default).  Theoretical limit of 4 GB per file.
@@ -62,7 +64,7 @@ our ($LONG_SIZE, $LONG_PACK, $DATA_LENGTH_SIZE, $DATA_LENGTH_PACK);
 # Increase this value for slightly greater speed, but larger database files.
 # DO NOT decrease this value below 16, due to risk of recursive reindex overrun.
 ##
-my $MAX_BUCKETS = 16;
+our $MAX_BUCKETS = 16;
 
 ##
 # Better not adjust anything below here, unless you're me :-)
@@ -78,29 +80,54 @@ our ($DIGEST_FUNC, $HASH_SIZE);
 # Precalculate index and bucket sizes based on values above.
 ##
 #my $HASH_SIZE = 16;
-my ($INDEX_SIZE, $BUCKET_SIZE, $BUCKET_LIST_SIZE);
+our ($INDEX_SIZE, $BUCKET_SIZE, $BUCKET_LIST_SIZE);
 
 set_digest();
 #set_pack();
-#precalc_sizes();
+#_precalc_sizes();
 
 ##
 # Setup file and tag signatures.  These should never change.
 ##
-sub SIG_FILE  () { 'DPDB' }
-sub SIG_HASH  () { 'H' }
-sub SIG_ARRAY () { 'A' }
-sub SIG_NULL  () { 'N' }
-sub SIG_DATA  () { 'D' }
-sub SIG_INDEX () { 'I' }
-sub SIG_BLIST () { 'B' }
-sub SIG_SIZE  () {  1  }
+sub SIG_FILE   () { 'DPDB' }
+sub SIG_HASH   () { 'H' }
+sub SIG_ARRAY  () { 'A' }
+sub SIG_SCALAR () { 'S' }
+sub SIG_NULL   () { 'N' }
+sub SIG_DATA   () { 'D' }
+sub SIG_INDEX  () { 'I' }
+sub SIG_BLIST  () { 'B' }
+sub SIG_SIZE   () {  1  }
 
 ##
 # Setup constants for users to pass to new()
 ##
-sub TYPE_HASH  () { return SIG_HASH; }
-sub TYPE_ARRAY () { return SIG_ARRAY; }
+sub TYPE_HASH   () { SIG_HASH   }
+sub TYPE_ARRAY  () { SIG_ARRAY  }
+sub TYPE_SCALAR () { SIG_SCALAR }
+
+sub _get_args {
+    my $proto = shift;
+
+    my $args;
+    if (scalar(@_) > 1) {
+        if ( @_ % 2 ) {
+            $proto->_throw_error( "Odd number of parameters to " . (caller(1))[2] );
+        }
+        $args = {@_};
+    }
+       elsif ( ref $_[0] ) {
+        unless ( eval { local $SIG{'__DIE__'}; %{$_[0]} || 1 } ) {
+            $proto->_throw_error( "Not a hashref in args to " . (caller(1))[2] );
+        }
+        $args = $_[0];
+    }
+       else {
+        $args = { file => shift };
+    }
+
+    return $args;
+}
 
 sub new {
        ##
@@ -109,9 +136,7 @@ sub new {
        # providing a hybrid OO/tie interface.
        ##
        my $class = shift;
-       my $args;
-       if (scalar(@_) > 1) { $args = {@_}; }
-       else { $args = { file => shift }; }
+       my $args = $class->_get_args( @_ );
        
        ##
        # Check if we want a tied hash or array.
@@ -131,39 +156,35 @@ sub new {
        return bless $self, $class;
 }
 
-{
-    my @outer_params = qw( type base_offset );
-    sub _init {
-        ##
-        # Setup $self and bless into this class.
-        ##
-        my $class = shift;
-        my $args = shift;
-
-        my $self = {
-            type => TYPE_HASH,
-            base_offset => length(SIG_FILE),
-        };
-
-        bless $self, $class;
-
-        foreach my $outer_parm ( @outer_params ) {
-            next unless exists $args->{$outer_parm};
-            $self->{$outer_parm} = delete $args->{$outer_parm}
-        }
-        
-        $self->{root} = exists $args->{root}
-            ? $args->{root}
-            : DBM::Deep::_::Root->new( $args );
+sub _init {
+    ##
+    # Setup $self and bless into this class.
+    ##
+    my $class = shift;
+    my $args = shift;
 
-        if (!defined($self->fh)) { $self->_open(); }
+    # These are the defaults to be optionally overridden below
+    my $self = bless {
+        type        => TYPE_HASH,
+        base_offset => length(SIG_FILE),
+        engine      => 'DBM::Deep::Engine',
+    }, $class;
 
-        return $self;
+    foreach my $param ( keys %$self ) {
+        next unless exists $args->{$param};
+        $self->{$param} = delete $args->{$param}
     }
-}
+    
+    # locking implicitly enables autoflush
+    if ($args->{locking}) { $args->{autoflush} = 1; }
+    
+    $self->{root} = exists $args->{root}
+        ? $args->{root}
+        : DBM::Deep::_::Root->new( $args );
 
-sub _get_self {
-    tied( %{$_[0]} ) || $_[0]
+    if (!defined($self->_fh)) { $self->{engine}->open( $self ); }
+
+    return $self;
 }
 
 sub TIEHASH {
@@ -182,585 +203,6 @@ sub TIEARRAY {
 #sub DESTROY {
 #}
 
-my %translate_mode = (
-    'r' => '<',
-    'r+' => '+<',
-    'w' => '>',
-    'w+' => '+>',
-    'a' => '>>',
-    'a+' => '+>>',
-);
-sub _open {
-       ##
-       # Open a FileHandle to the database, create if nonexistent.
-       # Make sure file signature matches DeepDB spec.
-       ##
-    my $self = _get_self($_[0]);
-
-       if (defined($self->fh)) { $self->_close(); }
-       
-    eval {
-        my $filename = $self->root->{file};
-        my $mode = $translate_mode{ $self->root->{mode} };
-
-        if (!(-e $filename) && $mode eq '+<') {
-            open( FH, '>', $filename );
-            close FH;
-        }
-       
-        my $fh;
-        open( $fh, $mode, $filename )
-            or $fh = undef;
-        $self->root->{fh} = $fh;
-    }; if ($@ ) { $self->_throw_error( "Received error: $@\n" ); }
-       if (! defined($self->fh)) {
-               return $self->_throw_error("Cannot open file: " . $self->root->{file} . ": $!");
-       }
-
-    my $fh = $self->fh;
-
-    #XXX Can we remove this by using the right sysopen() flags?
-    binmode $fh; # for win32
-
-    if ($self->root->{autoflush}) {
-        my $old = select $fh;
-        $|=1;
-        select $old;
-    }
-    
-    my $signature;
-    seek($fh, 0, 0);
-    my $bytes_read = read( $fh, $signature, length(SIG_FILE));
-    
-    ##
-    # File is empty -- write signature and master index
-    ##
-    if (!$bytes_read) {
-        seek($fh, 0, 0);
-        print($fh SIG_FILE);
-        $self->root->{end} = length(SIG_FILE);
-        $self->_create_tag($self->base_offset, $self->type, chr(0) x $INDEX_SIZE);
-
-        my $plain_key = "[base]";
-        print($fh pack($DATA_LENGTH_PACK, length($plain_key)) . $plain_key );
-        $self->root->{end} += $DATA_LENGTH_SIZE + length($plain_key);
-
-        # Flush the filehandle
-        my $old_fh = select $fh;
-        my $old_af = $|;
-        $| = 1;
-        $| = $old_af;
-        select $old_fh;
-
-        return 1;
-    }
-    
-    ##
-    # Check signature was valid
-    ##
-    unless ($signature eq SIG_FILE) {
-        $self->_close();
-        return $self->_throw_error("Signature not found -- file is not a Deep DB");
-    }
-
-    $self->root->{end} = (stat($fh))[7];
-        
-    ##
-    # Get our type from master index signature
-    ##
-    my $tag = $self->_load_tag($self->base_offset);
-
-#XXX We probably also want to store the hash algorithm name and not assume anything
-
-    if (!$tag) {
-       return $self->_throw_error("Corrupted file, no master index record");
-    }
-    if ($self->{type} ne $tag->{signature}) {
-       return $self->_throw_error("File type mismatch");
-    }
-    
-    return 1;
-}
-
-sub _close {
-       ##
-       # Close database FileHandle
-       ##
-    my $self = _get_self($_[0]);
-    close $self->root->{fh};
-}
-
-sub _create_tag {
-       ##
-       # Given offset, signature and content, create tag and write to disk
-       ##
-       my ($self, $offset, $sig, $content) = @_;
-       my $size = length($content);
-       
-    my $fh = $self->fh;
-
-       seek($fh, $offset, 0);
-       print($fh $sig . pack($DATA_LENGTH_PACK, $size) . $content );
-       
-       if ($offset == $self->root->{end}) {
-               $self->root->{end} += SIG_SIZE + $DATA_LENGTH_SIZE + $size;
-       }
-       
-       return {
-               signature => $sig,
-               size => $size,
-               offset => $offset + SIG_SIZE + $DATA_LENGTH_SIZE,
-               content => $content
-       };
-}
-
-sub _load_tag {
-       ##
-       # Given offset, load single tag and return signature, size and data
-       ##
-       my $self = shift;
-       my $offset = shift;
-       
-    my $fh = $self->fh;
-
-       seek($fh, $offset, 0);
-       if (eof $fh) { return undef; }
-       
-       my $sig;
-       read( $fh, $sig, SIG_SIZE);
-       
-       my $size;
-       read( $fh, $size, $DATA_LENGTH_SIZE);
-       $size = unpack($DATA_LENGTH_PACK, $size);
-       
-       my $buffer;
-       read( $fh, $buffer, $size);
-       
-       return {
-               signature => $sig,
-               size => $size,
-               offset => $offset + SIG_SIZE + $DATA_LENGTH_SIZE,
-               content => $buffer
-       };
-}
-
-sub _index_lookup {
-       ##
-       # Given index tag, lookup single entry in index and return .
-       ##
-       my $self = shift;
-       my ($tag, $index) = @_;
-
-       my $location = unpack($LONG_PACK, substr($tag->{content}, $index * $LONG_SIZE, $LONG_SIZE) );
-       if (!$location) { return; }
-       
-       return $self->_load_tag( $location );
-}
-
-sub _add_bucket {
-       ##
-       # Adds one key/value pair to bucket list, given offset, MD5 digest of key,
-       # plain (undigested) key and value.
-       ##
-       my $self = shift;
-       my ($tag, $md5, $plain_key, $value) = @_;
-       my $keys = $tag->{content};
-       my $location = 0;
-       my $result = 2;
-
-    my $is_dbm_deep = eval { $value->isa( 'DBM::Deep' ) };
-       my $internal_ref = $is_dbm_deep && ($value->root eq $self->root);
-
-    my $fh = $self->fh;
-
-       ##
-       # Iterate through buckets, seeing if this is a new entry or a replace.
-       ##
-       for (my $i=0; $i<$MAX_BUCKETS; $i++) {
-               my $key = substr($keys, $i * $BUCKET_SIZE, $HASH_SIZE);
-               my $subloc = unpack($LONG_PACK, substr($keys, ($i * $BUCKET_SIZE) + $HASH_SIZE, $LONG_SIZE));
-               if (!$subloc) {
-                       ##
-                       # Found empty bucket (end of list).  Populate and exit loop.
-                       ##
-                       $result = 2;
-                       
-            $location = $internal_ref
-                ? $value->base_offset
-                : $self->root->{end};
-                       
-                       seek($fh, $tag->{offset} + ($i * $BUCKET_SIZE), 0);
-                       print($fh $md5 . pack($LONG_PACK, $location) );
-                       last;
-               }
-               elsif ($md5 eq $key) {
-                       ##
-                       # Found existing bucket with same key.  Replace with new value.
-                       ##
-                       $result = 1;
-                       
-                       if ($internal_ref) {
-                               $location = $value->base_offset;
-                               seek($fh, $tag->{offset} + ($i * $BUCKET_SIZE), 0);
-                               print($fh $md5 . pack($LONG_PACK, $location) );
-                       }
-                       else {
-                               seek($fh, $subloc + SIG_SIZE, 0);
-                               my $size;
-                               read( $fh, $size, $DATA_LENGTH_SIZE); $size = unpack($DATA_LENGTH_PACK, $size);
-                               
-                               ##
-                               # If value is a hash, array, or raw value with equal or less size, we can
-                               # reuse the same content area of the database.  Otherwise, we have to create
-                               # a new content area at the EOF.
-                               ##
-                               my $actual_length;
-                my $r = Scalar::Util::reftype( $value ) || '';
-                if ( $r eq 'HASH' || $r eq 'ARRAY' ) { $actual_length = $INDEX_SIZE; }
-                               else { $actual_length = length($value); }
-                               
-                               if ($actual_length <= $size) {
-                                       $location = $subloc;
-                               }
-                               else {
-                                       $location = $self->root->{end};
-                                       seek($fh, $tag->{offset} + ($i * $BUCKET_SIZE) + $HASH_SIZE, 0);
-                                       print($fh pack($LONG_PACK, $location) );
-                               }
-                       }
-                       last;
-               }
-       } # i loop
-       
-       ##
-       # If this is an internal reference, return now.
-       # No need to write value or plain key
-       ##
-       if ($internal_ref) {
-        return $result;
-    }
-       
-       ##
-       # If bucket didn't fit into list, split into a new index level
-       ##
-       if (!$location) {
-               seek($fh, $tag->{ref_loc}, 0);
-               print($fh pack($LONG_PACK, $self->root->{end}) );
-               
-               my $index_tag = $self->_create_tag($self->root->{end}, SIG_INDEX, chr(0) x $INDEX_SIZE);
-               my @offsets = ();
-               
-               $keys .= $md5 . pack($LONG_PACK, 0);
-               
-               for (my $i=0; $i<=$MAX_BUCKETS; $i++) {
-                       my $key = substr($keys, $i * $BUCKET_SIZE, $HASH_SIZE);
-                       if ($key) {
-                               my $old_subloc = unpack($LONG_PACK, substr($keys, ($i * $BUCKET_SIZE) + $HASH_SIZE, $LONG_SIZE));
-                               my $num = ord(substr($key, $tag->{ch} + 1, 1));
-                               
-                               if ($offsets[$num]) {
-                                       my $offset = $offsets[$num] + SIG_SIZE + $DATA_LENGTH_SIZE;
-                                       seek($fh, $offset, 0);
-                                       my $subkeys;
-                                       read( $fh, $subkeys, $BUCKET_LIST_SIZE);
-                                       
-                                       for (my $k=0; $k<$MAX_BUCKETS; $k++) {
-                                               my $subloc = unpack($LONG_PACK, substr($subkeys, ($k * $BUCKET_SIZE) + $HASH_SIZE, $LONG_SIZE));
-                                               if (!$subloc) {
-                                                       seek($fh, $offset + ($k * $BUCKET_SIZE), 0);
-                                                       print($fh $key . pack($LONG_PACK, $old_subloc || $self->root->{end}) );
-                                                       last;
-                                               }
-                                       } # k loop
-                               }
-                               else {
-                                       $offsets[$num] = $self->root->{end};
-                                       seek($fh, $index_tag->{offset} + ($num * $LONG_SIZE), 0);
-                                       print($fh pack($LONG_PACK, $self->root->{end}) );
-                                       
-                                       my $blist_tag = $self->_create_tag($self->root->{end}, SIG_BLIST, chr(0) x $BUCKET_LIST_SIZE);
-                                       
-                                       seek($fh, $blist_tag->{offset}, 0);
-                                       print($fh $key . pack($LONG_PACK, $old_subloc || $self->root->{end}) );
-                               }
-                       } # key is real
-               } # i loop
-               
-               $location ||= $self->root->{end};
-       } # re-index bucket list
-       
-       ##
-       # Seek to content area and store signature, value and plaintext key
-       ##
-       if ($location) {
-               my $content_length;
-               seek($fh, $location, 0);
-               
-               ##
-               # Write signature based on content type, set content length and write actual value.
-               ##
-        my $r = Scalar::Util::reftype($value) || '';
-               if ($r eq 'HASH') {
-                       print($fh TYPE_HASH );
-                       print($fh pack($DATA_LENGTH_PACK, $INDEX_SIZE) . chr(0) x $INDEX_SIZE );
-                       $content_length = $INDEX_SIZE;
-               }
-               elsif ($r eq 'ARRAY') {
-                       print($fh TYPE_ARRAY );
-                       print($fh pack($DATA_LENGTH_PACK, $INDEX_SIZE) . chr(0) x $INDEX_SIZE );
-                       $content_length = $INDEX_SIZE;
-               }
-               elsif (!defined($value)) {
-                       print($fh SIG_NULL );
-                       print($fh pack($DATA_LENGTH_PACK, 0) );
-                       $content_length = 0;
-               }
-               else {
-                       print($fh SIG_DATA );
-                       print($fh pack($DATA_LENGTH_PACK, length($value)) . $value );
-                       $content_length = length($value);
-               }
-               
-               ##
-               # Plain key is stored AFTER value, as keys are typically fetched less often.
-               ##
-               print($fh pack($DATA_LENGTH_PACK, length($plain_key)) . $plain_key );
-               
-               ##
-               # If value is blessed, preserve class name
-               ##
-               if ( $self->root->{autobless} ) {
-            my $value_class = Scalar::Util::blessed($value);
-            if ( defined $value_class && $value_class ne 'DBM::Deep' ) {
-                ##
-                # Blessed ref -- will restore later
-                ##
-                print($fh chr(1) );
-                print($fh pack($DATA_LENGTH_PACK, length($value_class)) . $value_class );
-                $content_length += 1;
-                $content_length += $DATA_LENGTH_SIZE + length($value_class);
-            }
-            else {
-                print($fh chr(0) );
-                $content_length += 1;
-            }
-        }
-            
-               ##
-               # If this is a new content area, advance EOF counter
-               ##
-               if ($location == $self->root->{end}) {
-                       $self->root->{end} += SIG_SIZE;
-                       $self->root->{end} += $DATA_LENGTH_SIZE + $content_length;
-                       $self->root->{end} += $DATA_LENGTH_SIZE + length($plain_key);
-               }
-               
-               ##
-               # If content is a hash or array, create new child DeepDB object and
-               # pass each key or element to it.
-               ##
-               if ($r eq 'HASH') {
-                       my $branch = DBM::Deep->new(
-                               type => TYPE_HASH,
-                               base_offset => $location,
-                               root => $self->root,
-                       );
-                       foreach my $key (keys %{$value}) {
-                #$branch->{$key} = $value->{$key};
-                $branch->STORE( $key, $value->{$key} );
-                       }
-               }
-               elsif ($r eq 'ARRAY') {
-                       my $branch = DBM::Deep->new(
-                               type => TYPE_ARRAY,
-                               base_offset => $location,
-                               root => $self->root,
-                       );
-                       my $index = 0;
-                       foreach my $element (@{$value}) {
-                #$branch->[$index] = $element;
-                $branch->STORE( $index, $element );
-                               $index++;
-                       }
-               }
-               
-               return $result;
-       }
-       
-       return $self->_throw_error("Fatal error: indexing failed -- possibly due to corruption in file");
-}
-
-sub _get_bucket_value {
-       ##
-       # Fetch single value given tag and MD5 digested key.
-       ##
-       my $self = shift;
-       my ($tag, $md5) = @_;
-       my $keys = $tag->{content};
-
-    my $fh = $self->fh;
-       
-       ##
-       # Iterate through buckets, looking for a key match
-       ##
-    BUCKET:
-       for (my $i=0; $i<$MAX_BUCKETS; $i++) {
-               my $key = substr($keys, $i * $BUCKET_SIZE, $HASH_SIZE);
-               my $subloc = unpack($LONG_PACK, substr($keys, ($i * $BUCKET_SIZE) + $HASH_SIZE, $LONG_SIZE));
-
-               if (!$subloc) {
-                       ##
-                       # Hit end of list, no match
-                       ##
-                       return;
-               }
-
-        if ( $md5 ne $key ) {
-            next BUCKET;
-        }
-
-        ##
-        # Found match -- seek to offset and read signature
-        ##
-        my $signature;
-        seek($fh, $subloc, 0);
-        read( $fh, $signature, SIG_SIZE);
-        
-        ##
-        # If value is a hash or array, return new DeepDB object with correct offset
-        ##
-        if (($signature eq TYPE_HASH) || ($signature eq TYPE_ARRAY)) {
-            my $obj = DBM::Deep->new(
-                type => $signature,
-                base_offset => $subloc,
-                root => $self->root
-            );
-            
-            if ($self->root->{autobless}) {
-                ##
-                # Skip over value and plain key to see if object needs
-                # to be re-blessed
-                ##
-                seek($fh, $DATA_LENGTH_SIZE + $INDEX_SIZE, 1);
-                
-                my $size;
-                read( $fh, $size, $DATA_LENGTH_SIZE); $size = unpack($DATA_LENGTH_PACK, $size);
-                if ($size) { seek($fh, $size, 1); }
-                
-                my $bless_bit;
-                read( $fh, $bless_bit, 1);
-                if (ord($bless_bit)) {
-                    ##
-                    # Yes, object needs to be re-blessed
-                    ##
-                    my $class_name;
-                    read( $fh, $size, $DATA_LENGTH_SIZE); $size = unpack($DATA_LENGTH_PACK, $size);
-                    if ($size) { read( $fh, $class_name, $size); }
-                    if ($class_name) { $obj = bless( $obj, $class_name ); }
-                }
-            }
-            
-            return $obj;
-        }
-        
-        ##
-        # Otherwise return actual value
-        ##
-        elsif ($signature eq SIG_DATA) {
-            my $size;
-            my $value = '';
-            read( $fh, $size, $DATA_LENGTH_SIZE); $size = unpack($DATA_LENGTH_PACK, $size);
-            if ($size) { read( $fh, $value, $size); }
-            return $value;
-        }
-        
-        ##
-        # Key exists, but content is null
-        ##
-        else { return; }
-       } # i loop
-
-       return;
-}
-
-sub _delete_bucket {
-       ##
-       # Delete single key/value pair given tag and MD5 digested key.
-       ##
-       my $self = shift;
-       my ($tag, $md5) = @_;
-       my $keys = $tag->{content};
-
-    my $fh = $self->fh;
-       
-       ##
-       # Iterate through buckets, looking for a key match
-       ##
-    BUCKET:
-       for (my $i=0; $i<$MAX_BUCKETS; $i++) {
-               my $key = substr($keys, $i * $BUCKET_SIZE, $HASH_SIZE);
-               my $subloc = unpack($LONG_PACK, substr($keys, ($i * $BUCKET_SIZE) + $HASH_SIZE, $LONG_SIZE));
-
-               if (!$subloc) {
-                       ##
-                       # Hit end of list, no match
-                       ##
-                       return;
-               }
-
-        if ( $md5 ne $key ) {
-            next BUCKET;
-        }
-
-        ##
-        # Matched key -- delete bucket and return
-        ##
-        seek($fh, $tag->{offset} + ($i * $BUCKET_SIZE), 0);
-        print($fh substr($keys, ($i+1) * $BUCKET_SIZE ) );
-        print($fh chr(0) x $BUCKET_SIZE );
-        
-        return 1;
-       } # i loop
-
-       return;
-}
-
-sub _bucket_exists {
-       ##
-       # Check existence of single key given tag and MD5 digested key.
-       ##
-       my $self = shift;
-       my ($tag, $md5) = @_;
-       my $keys = $tag->{content};
-       
-       ##
-       # Iterate through buckets, looking for a key match
-       ##
-    BUCKET:
-       for (my $i=0; $i<$MAX_BUCKETS; $i++) {
-               my $key = substr($keys, $i * $BUCKET_SIZE, $HASH_SIZE);
-               my $subloc = unpack($LONG_PACK, substr($keys, ($i * $BUCKET_SIZE) + $HASH_SIZE, $LONG_SIZE));
-
-               if (!$subloc) {
-                       ##
-                       # Hit end of list, no match
-                       ##
-                       return;
-               }
-
-        if ( $md5 ne $key ) {
-            next BUCKET;
-        }
-
-        ##
-        # Matched key -- return true
-        ##
-        return 1;
-       } # i loop
-
-       return;
-}
-
 sub _find_bucket_list {
        ##
        # Locate offset for bucket list, given digested key
@@ -772,11 +214,11 @@ sub _find_bucket_list {
        # Locate offset for bucket list using digest index system
        ##
        my $ch = 0;
-       my $tag = $self->_load_tag($self->base_offset);
+       my $tag = $self->{engine}->load_tag($self, $self->_base_offset);
        if (!$tag) { return; }
        
        while ($tag->{signature} ne SIG_BLIST) {
-               $tag = $self->_index_lookup($tag, ord(substr($md5, $ch, 1)));
+               $tag = $self->{engine}->index_lookup($self, $tag, ord(substr($md5, $ch, 1)));
                if (!$tag) { return; }
                $ch++;
        }
@@ -791,9 +233,9 @@ sub _traverse_index {
     my ($self, $offset, $ch, $force_return_next) = @_;
     $force_return_next = undef unless $force_return_next;
        
-       my $tag = $self->_load_tag( $offset );
+       my $tag = $self->{engine}->load_tag($self,  $offset );
 
-    my $fh = $self->fh;
+    my $fh = $self->_fh;
        
        if ($tag->{signature} ne SIG_BLIST) {
                my $content = $tag->{content};
@@ -841,14 +283,14 @@ sub _traverse_index {
                                ##
                                # Seek to bucket location and skip over signature
                                ##
-                               seek($fh, $subloc + SIG_SIZE, 0);
+                               seek($fh, $subloc + SIG_SIZE + $self->_root->{file_offset}, SEEK_SET);
                                
                                ##
                                # Skip over value to get to plain key
                                ##
                                my $size;
                                read( $fh, $size, $DATA_LENGTH_SIZE); $size = unpack($DATA_LENGTH_PACK, $size);
-                               if ($size) { seek($fh, $size, 1); }
+                               if ($size) { seek($fh, $size, SEEK_CUR); }
                                
                                ##
                                # Read in plain key and return as scalar
@@ -871,7 +313,7 @@ sub _get_next_key {
        ##
        # Locate next key, given digested previous one
        ##
-    my $self = _get_self($_[0]);
+    my $self = $_[0]->_get_self;
        
        $self->{prev_md5} = $_[1] ? $_[1] : undef;
        $self->{return_next} = 0;
@@ -885,7 +327,7 @@ sub _get_next_key {
                $self->{return_next} = 1;
        }
        
-       return $self->_traverse_index( $self->base_offset, 0 );
+       return $self->_traverse_index( $self->_base_offset, 0 );
 }
 
 sub lock {
@@ -894,13 +336,29 @@ sub lock {
        # times before unlock(), then the same number of unlocks() must
        # be called before the lock is released.
        ##
-    my $self = _get_self($_[0]);
+    my $self = $_[0]->_get_self;
        my $type = $_[1];
     $type = LOCK_EX unless defined $type;
        
-       if ($self->root->{locking}) {
-               if (!$self->root->{locked}) { flock($self->fh, $type); }
-               $self->root->{locked}++;
+       if (!defined($self->_fh)) { return; }
+
+       if ($self->_root->{locking}) {
+               if (!$self->_root->{locked}) {
+                       flock($self->_fh, $type);
+                       
+                       # refresh end counter in case file has changed size
+                       my @stats = stat($self->_root->{file});
+                       $self->_root->{end} = $stats[7];
+                       
+                       # double-check file inode, in case another process
+                       # has optimize()d our file while we were waiting.
+                       if ($stats[1] != $self->_root->{inode}) {
+                               $self->{engine}->open( $self ); # re-open
+                               flock($self->_fh, $type); # re-lock
+                               $self->_root->{end} = (stat($self->_fh))[7]; # re-end
+                       }
+               }
+               $self->_root->{locked}++;
 
         return 1;
        }
@@ -913,11 +371,13 @@ sub unlock {
        # If db locking is set, unlock the db file.  See note in lock()
        # regarding calling lock() multiple times.
        ##
-    my $self = _get_self($_[0]);
+    my $self = $_[0]->_get_self;
+
+       if (!defined($self->_fh)) { return; }
        
-       if ($self->root->{locking} && $self->root->{locked} > 0) {
-               $self->root->{locked}--;
-               if (!$self->root->{locked}) { flock($self->fh, LOCK_UN); }
+       if ($self->_root->{locking} && $self->_root->{locked} > 0) {
+               $self->_root->{locked}--;
+               if (!$self->_root->{locked}) { flock($self->_fh, LOCK_UN); }
 
         return 1;
        }
@@ -925,27 +385,47 @@ sub unlock {
     return;
 }
 
-#XXX These uses of ref() need verified
+sub _copy_value {
+    my $self = shift->_get_self;
+    my ($spot, $value) = @_;
+
+    if ( !ref $value ) {
+        ${$spot} = $value;
+    }
+    elsif ( eval { local $SIG{__DIE__}; $value->isa( 'DBM::Deep' ) } ) {
+        my $type = $value->_type;
+        ${$spot} = $type eq TYPE_HASH ? {} : [];
+        $value->_copy_node( ${$spot} );
+    }
+    else {
+        my $r = Scalar::Util::reftype( $value );
+        my $c = Scalar::Util::blessed( $value );
+        if ( $r eq 'ARRAY' ) {
+            ${$spot} = [ @{$value} ];
+        }
+        else {
+            ${$spot} = { %{$value} };
+        }
+        ${$spot} = bless ${$spot}, $c
+            if defined $c;
+    }
+
+    return 1;
+}
+
 sub _copy_node {
        ##
        # Copy single level of keys or elements to new DB handle.
        # Recurse for nested structures
        ##
-    my $self = _get_self($_[0]);
-       my $db_temp = $_[1];
+    my $self = shift->_get_self;
+       my ($db_temp) = @_;
 
-       if ($self->type eq TYPE_HASH) {
+       if ($self->_type eq TYPE_HASH) {
                my $key = $self->first_key();
                while ($key) {
                        my $value = $self->get($key);
-#XXX This doesn't work with autobless
-                       if (!ref($value)) { $db_temp->{$key} = $value; }
-                       else {
-                               my $type = $value->type;
-                               if ($type eq TYPE_HASH) { $db_temp->{$key} = {}; }
-                               else { $db_temp->{$key} = []; }
-                               $value->_copy_node( $db_temp->{$key} );
-                       }
+            $self->_copy_value( \$db_temp->{$key}, $value );
                        $key = $self->next_key($key);
                }
        }
@@ -953,27 +433,22 @@ sub _copy_node {
                my $length = $self->length();
                for (my $index = 0; $index < $length; $index++) {
                        my $value = $self->get($index);
-                       if (!ref($value)) { $db_temp->[$index] = $value; }
-            #XXX NO tests for this code
-                       else {
-                               my $type = $value->type;
-                               if ($type eq TYPE_HASH) { $db_temp->[$index] = {}; }
-                               else { $db_temp->[$index] = []; }
-                               $value->_copy_node( $db_temp->[$index] );
-                       }
+            $self->_copy_value( \$db_temp->[$index], $value );
                }
        }
+
+    return 1;
 }
 
 sub export {
        ##
        # Recursively export into standard Perl hashes and arrays.
        ##
-    my $self = _get_self($_[0]);
+    my $self = $_[0]->_get_self;
        
        my $temp;
-       if ($self->type eq TYPE_HASH) { $temp = {}; }
-       elsif ($self->type eq TYPE_ARRAY) { $temp = []; }
+       if ($self->_type eq TYPE_HASH) { $temp = {}; }
+       elsif ($self->_type eq TYPE_ARRAY) { $temp = []; }
        
        $self->lock();
        $self->_copy_node( $temp );
@@ -989,7 +464,7 @@ sub import {
     #XXX This use of ref() seems to be ok
        if (!ref($_[0])) { return; } # Perl calls import() on use -- ignore
        
-    my $self = _get_self($_[0]);
+    my $self = $_[0]->_get_self;
        my $struct = $_[1];
        
     #XXX This use of ref() seems to be ok
@@ -999,15 +474,15 @@ sub import {
                ##
                shift @_;
                
-               if ($self->type eq TYPE_HASH) { $struct = {@_}; }
-               elsif ($self->type eq TYPE_ARRAY) { $struct = [@_]; }
+               if ($self->_type eq TYPE_HASH) { $struct = {@_}; }
+               elsif ($self->_type eq TYPE_ARRAY) { $struct = [@_]; }
        }
        
     my $r = Scalar::Util::reftype($struct) || '';
-       if ($r eq "HASH" && $self->type eq TYPE_HASH) {
+       if ($r eq "HASH" && $self->_type eq TYPE_HASH) {
                foreach my $key (keys %$struct) { $self->put($key, $struct->{$key}); }
        }
-       elsif ($r eq "ARRAY" && $self->type eq TYPE_ARRAY) {
+       elsif ($r eq "ARRAY" && $self->_type eq TYPE_ARRAY) {
                $self->push( @$struct );
        }
        else {
@@ -1022,16 +497,16 @@ sub optimize {
        # Rebuild entire database into new file, then move
        # it back on top of original.
        ##
-    my $self = _get_self($_[0]);
+    my $self = $_[0]->_get_self;
 
 #XXX Need to create a new test for this
-#      if ($self->root->{links} > 1) {
+#      if ($self->_root->{links} > 1) {
 #              return $self->_throw_error("Cannot optimize: reference count is greater than 1");
 #      }
        
        my $db_temp = DBM::Deep->new(
-               file => $self->root->{file} . '.tmp',
-               type => $self->type
+               file => $self->_root->{file} . '.tmp',
+               type => $self->_type
        );
        if (!$db_temp) {
                return $self->_throw_error("Cannot optimize: failed to open temp file: $!");
@@ -1044,15 +519,15 @@ sub optimize {
        ##
        # Attempt to copy user, group and permissions over to new file
        ##
-       my @stats = stat($self->fh);
+       my @stats = stat($self->_fh);
        my $perms = $stats[2] & 07777;
        my $uid = $stats[4];
        my $gid = $stats[5];
-       chown( $uid, $gid, $self->root->{file} . '.tmp' );
-       chmod( $perms, $self->root->{file} . '.tmp' );
+       chown( $uid, $gid, $self->_root->{file} . '.tmp' );
+       chmod( $perms, $self->_root->{file} . '.tmp' );
        
     # q.v. perlport for more information on this variable
-    if ( $^O eq 'MSWin32' ) {
+    if ( $^O eq 'MSWin32' || $^O eq 'cygwin' ) {
                ##
                # Potential race condition when optmizing on Win32 with locking.
                # The Windows filesystem requires that the filehandle be closed 
@@ -1060,18 +535,18 @@ sub optimize {
                # with a soft copy.
                ##
                $self->unlock();
-               $self->_close();
+               $self->{engine}->close( $self );
        }
        
-       if (!rename $self->root->{file} . '.tmp', $self->root->{file}) {
-               unlink $self->root->{file} . '.tmp';
+       if (!rename $self->_root->{file} . '.tmp', $self->_root->{file}) {
+               unlink $self->_root->{file} . '.tmp';
                $self->unlock();
                return $self->_throw_error("Optimize failed: Cannot copy temp file over original: $!");
        }
        
        $self->unlock();
-       $self->_close();
-       $self->_open();
+       $self->{engine}->close( $self );
+       $self->{engine}->open( $self );
        
        return 1;
 }
@@ -1080,12 +555,12 @@ sub clone {
        ##
        # Make copy of object and return
        ##
-    my $self = _get_self($_[0]);
+    my $self = $_[0]->_get_self;
        
        return DBM::Deep->new(
-               type => $self->type,
-               base_offset => $self->base_offset,
-               root => $self->root
+               type => $self->_type,
+               base_offset => $self->_base_offset,
+               root => $self->_root
        );
 }
 
@@ -1101,12 +576,12 @@ sub clone {
         ##
         # Setup filter function for storing or fetching the key or value
         ##
-        my $self = _get_self($_[0]);
+        my $self = $_[0]->_get_self;
         my $type = lc $_[1];
         my $func = $_[2] ? $_[2] : undef;
        
         if ( $is_legal_filter{$type} ) {
-            $self->root->{"filter_$type"} = $func;
+            $self->_root->{"filter_$type"} = $func;
             return 1;
         }
 
@@ -1118,79 +593,48 @@ sub clone {
 # Accessor methods
 ##
 
-sub root {
+sub _root {
        ##
        # Get access to the root structure
        ##
-    my $self = _get_self($_[0]);
+    my $self = $_[0]->_get_self;
        return $self->{root};
 }
 
-sub fh {
+sub _fh {
        ##
-       # Get access to the raw FileHandle
+       # Get access to the raw fh
        ##
     #XXX It will be useful, though, when we split out HASH and ARRAY
-    my $self = _get_self($_[0]);
-       return $self->root->{fh};
+    my $self = $_[0]->_get_self;
+       return $self->_root->{fh};
 }
 
-sub type {
+sub _type {
        ##
        # Get type of current node (TYPE_HASH or TYPE_ARRAY)
        ##
-    my $self = _get_self($_[0]);
+    my $self = $_[0]->_get_self;
        return $self->{type};
 }
 
-sub base_offset {
+sub _base_offset {
        ##
        # Get base_offset of current node (TYPE_HASH or TYPE_ARRAY)
        ##
-    my $self = _get_self($_[0]);
+    my $self = $_[0]->_get_self;
        return $self->{base_offset};
 }
 
-sub error {
-       ##
-       # Get last error string, or undef if no error
-       ##
-       return $_[0]
-        ? ( _get_self($_[0])->{root}->{error} or undef )
-        : $@;
-}
-
 ##
 # Utility methods
 ##
 
 sub _throw_error {
-       ##
-       # Store error string in self
-       ##
-    my $self = _get_self($_[0]);
-       my $error_text = $_[1];
-       
-       $self->root->{error} = $error_text;
-       
-       unless ($self->root->{debug}) {
-        die "DBM::Deep: $error_text\n";
-    }
-
-    warn "DBM::Deep: $error_text\n";
-       return;
-}
-
-sub clear_error {
-       ##
-       # Clear error state
-       ##
-    my $self = _get_self($_[0]);
-       
-       undef $self->root->{error};
+    die "DBM::Deep: $_[1]\n";
 }
 
-sub precalc_sizes {
+sub _precalc_sizes {
        ##
        # Precalculate index, bucket and bucket list sizes
        ##
@@ -1215,7 +659,7 @@ sub set_pack {
     $DATA_LENGTH_SIZE = $data_s ? $data_s : 4;
     $DATA_LENGTH_PACK = $data_p ? $data_p : 'N';
 
-       precalc_sizes();
+       _precalc_sizes();
 }
 
 sub set_digest {
@@ -1227,9 +671,19 @@ sub set_digest {
     $DIGEST_FUNC = $digest_func ? $digest_func : \&Digest::MD5::md5;
     $HASH_SIZE = $hash_size ? $hash_size : 16;
 
-       precalc_sizes();
+       _precalc_sizes();
 }
 
+sub _is_writable {
+    my $fh = shift;
+    (O_WRONLY | O_RDWR) & fcntl( $fh, F_GETFL, my $slush = 0);
+}
+
+#sub _is_readable {
+#    my $fh = shift;
+#    (O_RDONLY | O_RDWR) & fcntl( $fh, F_GETFL, my $slush = 0);
+#}
+
 ##
 # tie() methods (hashes and arrays)
 ##
@@ -1238,65 +692,57 @@ sub STORE {
        ##
        # Store single hash key/value or array element in database.
        ##
-    my $self = _get_self($_[0]);
-       my $key = ($self->root->{filter_store_key} && $self->type eq TYPE_HASH) ? $self->root->{filter_store_key}->($_[1]) : $_[1];
-    #XXX What is ref() checking here?
-    #YYY User may be storing a hash, in which case we do not want it run 
-    #YYY through the filtering system
-       my $value = ($self->root->{filter_store_value} && !ref($_[2])) ? $self->root->{filter_store_value}->($_[2]) : $_[2];
+    my $self = $_[0]->_get_self;
+       my $key = $_[1];
+
+    # User may be storing a hash, in which case we do not want it run 
+    # through the filtering system
+       my $value = ($self->_root->{filter_store_value} && !ref($_[2]))
+        ? $self->_root->{filter_store_value}->($_[2])
+        : $_[2];
        
-       my $unpacked_key = $key;
-       if (($self->type eq TYPE_ARRAY) && ($key =~ /^\d+$/)) { $key = pack($LONG_PACK, $key); }
        my $md5 = $DIGEST_FUNC->($key);
        
-       ##
-       # Make sure file is open
-       ##
-       if (!defined($self->fh) && !$self->_open()) {
-               return;
-       }
-       ##
-
-    my $fh = $self->fh;
+    unless ( _is_writable( $self->_fh ) ) {
+        $self->_throw_error( 'Cannot write to a readonly filehandle' );
+    }
        
        ##
        # Request exclusive lock for writing
        ##
        $self->lock( LOCK_EX );
-
-       ##
-       # If locking is enabled, set 'end' parameter again, in case another
-       # DB instance appended to our file while we were unlocked.
-       ##
-       if ($self->root->{locking} || $self->root->{volatile}) {
-               $self->root->{end} = (stat($fh))[7];
-       }
+       
+       my $fh = $self->_fh;
        
        ##
        # Locate offset for bucket list using digest index system
        ##
-       my $tag = $self->_load_tag($self->base_offset);
+       my $tag = $self->{engine}->load_tag($self, $self->_base_offset);
        if (!$tag) {
-               $tag = $self->_create_tag($self->base_offset, SIG_INDEX, chr(0) x $INDEX_SIZE);
+               $tag = $self->{engine}->create_tag($self, $self->_base_offset, SIG_INDEX, chr(0) x $INDEX_SIZE);
        }
        
        my $ch = 0;
        while ($tag->{signature} ne SIG_BLIST) {
                my $num = ord(substr($md5, $ch, 1));
-               my $new_tag = $self->_index_lookup($tag, $num);
+
+        my $ref_loc = $tag->{offset} + ($num * $LONG_SIZE);
+               my $new_tag = $self->{engine}->index_lookup($self, $tag, $num);
+
                if (!$new_tag) {
-                       my $ref_loc = $tag->{offset} + ($num * $LONG_SIZE);
-                       seek($fh, $ref_loc, 0);
-                       print($fh pack($LONG_PACK, $self->root->{end}) );
+                       seek($fh, $ref_loc + $self->_root->{file_offset}, SEEK_SET);
+                       print( $fh pack($LONG_PACK, $self->_root->{end}) );
                        
-                       $tag = $self->_create_tag($self->root->{end}, SIG_BLIST, chr(0) x $BUCKET_LIST_SIZE);
+                       $tag = $self->{engine}->create_tag($self, $self->_root->{end}, SIG_BLIST, chr(0) x $BUCKET_LIST_SIZE);
+
                        $tag->{ref_loc} = $ref_loc;
                        $tag->{ch} = $ch;
+
                        last;
                }
                else {
-                       my $ref_loc = $tag->{offset} + ($num * $LONG_SIZE);
                        $tag = $new_tag;
+
                        $tag->{ref_loc} = $ref_loc;
                        $tag->{ch} = $ch;
                }
@@ -1306,15 +752,7 @@ sub STORE {
        ##
        # Add key/value to bucket list
        ##
-       my $result = $self->_add_bucket( $tag, $md5, $key, $value );
-       
-       ##
-       # If this object is an array, and bucket was not a replace, and key is numerical,
-       # and index is equal or greater than current length, advance length variable.
-       ##
-       if (($result == 2) && ($self->type eq TYPE_ARRAY) && ($unpacked_key =~ /^\d+$/) && ($unpacked_key >= $self->FETCHSIZE())) {
-               $self->STORESIZE( $unpacked_key + 1 );
-       }
+       my $result = $self->{engine}->add_bucket( $self, $tag, $md5, $key, $value );
        
        $self->unlock();
 
@@ -1325,28 +763,12 @@ sub FETCH {
        ##
        # Fetch single value or element given plain key or array index
        ##
-    my $self = _get_self($_[0]);
-
-    my $key = $_[1];
-    if ( $self->type eq TYPE_HASH ) {
-        if ( my $filter = $self->root->{filter_store_key} ) {
-            $key = $filter->( $key );
-        }
-    }
-    elsif ( $self->type eq TYPE_ARRAY ) { 
-        if ( $key =~ /^\d+$/ ) {
-            $key = pack($LONG_PACK, $key);
-        }
-    }
+    my $self = shift->_get_self;
+    my $key = shift;
 
        my $md5 = $DIGEST_FUNC->($key);
 
        ##
-       # Make sure file is open
-       ##
-       if (!defined($self->fh)) { $self->_open(); }
-       
-       ##
        # Request shared lock for reading
        ##
        $self->lock( LOCK_SH );
@@ -1360,31 +782,28 @@ sub FETCH {
        ##
        # Get value from bucket list
        ##
-       my $result = $self->_get_bucket_value( $tag, $md5 );
+       my $result = $self->{engine}->get_bucket_value( $self, $tag, $md5 );
        
        $self->unlock();
        
     #XXX What is ref() checking here?
-       return ($result && !ref($result) && $self->root->{filter_fetch_value}) ? $self->root->{filter_fetch_value}->($result) : $result;
+    #YYY Filters only apply on scalar values, so the ref check is making
+    #YYY sure the fetched bucket is a scalar, not a child hash or array.
+       return ($result && !ref($result) && $self->_root->{filter_fetch_value})
+        ? $self->_root->{filter_fetch_value}->($result)
+        : $result;
 }
 
 sub DELETE {
        ##
        # Delete single key/value pair or element given plain key or array index
        ##
-    my $self = _get_self($_[0]);
-       my $key = ($self->root->{filter_store_key} && $self->type eq TYPE_HASH) ? $self->root->{filter_store_key}->($_[1]) : $_[1];
+    my $self = $_[0]->_get_self;
+       my $key = $_[1];
        
-       my $unpacked_key = $key;
-       if (($self->type eq TYPE_ARRAY) && ($key =~ /^\d+$/)) { $key = pack($LONG_PACK, $key); }
        my $md5 = $DIGEST_FUNC->($key);
 
        ##
-       # Make sure file is open
-       ##
-       if (!defined($self->fh)) { $self->_open(); }
-       
-       ##
        # Request exclusive lock for writing
        ##
        $self->lock( LOCK_EX );
@@ -1398,37 +817,33 @@ sub DELETE {
        ##
        # Delete bucket
        ##
-       my $result = $self->_delete_bucket( $tag, $md5 );
+    my $value = $self->{engine}->get_bucket_value($self,  $tag, $md5 );
+       if ($value && !ref($value) && $self->_root->{filter_fetch_value}) {
+        $value = $self->_root->{filter_fetch_value}->($value);
+    }
+
+       my $result = $self->{engine}->delete_bucket( $self, $tag, $md5 );
        
        ##
        # If this object is an array and the key deleted was on the end of the stack,
        # decrement the length variable.
        ##
-       if ($result && ($self->type eq TYPE_ARRAY) && ($unpacked_key == $self->FETCHSIZE() - 1)) {
-               $self->STORESIZE( $unpacked_key );
-       }
        
        $self->unlock();
        
-       return $result;
+       return $value;
 }
 
 sub EXISTS {
        ##
        # Check if a single key or element exists given plain key or array index
        ##
-    my $self = _get_self($_[0]);
-       my $key = ($self->root->{filter_store_key} && $self->type eq TYPE_HASH) ? $self->root->{filter_store_key}->($_[1]) : $_[1];
+    my $self = $_[0]->_get_self;
+       my $key = $_[1];
        
-       if (($self->type eq TYPE_ARRAY) && ($key =~ /^\d+$/)) { $key = pack($LONG_PACK, $key); }
        my $md5 = $DIGEST_FUNC->($key);
 
        ##
-       # Make sure file is open
-       ##
-       if (!defined($self->fh)) { $self->_open(); }
-       
-       ##
        # Request shared lock for reading
        ##
        $self->lock( LOCK_SH );
@@ -1446,7 +861,7 @@ sub EXISTS {
        ##
        # Check if bucket exists and return 1 or ''
        ##
-       my $result = $self->_bucket_exists( $tag, $md5 ) || '';
+       my $result = $self->{engine}->bucket_exists( $self, $tag, $md5 ) || '';
        
        $self->unlock();
        
@@ -1457,27 +872,22 @@ sub CLEAR {
        ##
        # Clear all keys from hash, or all elements from array.
        ##
-    my $self = _get_self($_[0]);
+    my $self = $_[0]->_get_self;
 
        ##
-       # Make sure file is open
-       ##
-       if (!defined($self->fh)) { $self->_open(); }
-       
-       ##
        # Request exclusive lock for writing
        ##
        $self->lock( LOCK_EX );
        
-    my $fh = $self->fh;
+    my $fh = $self->_fh;
 
-       seek($fh, $self->base_offset, 0);
+       seek($fh, $self->_base_offset + $self->_root->{file_offset}, SEEK_SET);
        if (eof $fh) {
                $self->unlock();
                return;
        }
        
-       $self->_create_tag($self->base_offset, $self->type, chr(0) x $INDEX_SIZE);
+       $self->{engine}->create_tag($self, $self->_base_offset, $self->_type, chr(0) x $INDEX_SIZE);
        
        $self->unlock();
        
@@ -1487,11 +897,13 @@ sub CLEAR {
 ##
 # Public method aliases
 ##
-*put = *store = *STORE;
-*get = *fetch = *FETCH;
-*delete = *DELETE;
-*exists = *EXISTS;
-*clear = *CLEAR;
+sub put { (shift)->STORE( @_ ) }
+sub store { (shift)->STORE( @_ ) }
+sub get { (shift)->FETCH( @_ ) }
+sub fetch { (shift)->FETCH( @_ ) }
+sub delete { (shift)->DELETE( @_ ) }
+sub exists { (shift)->EXISTS( @_ ) }
+sub clear { (shift)->CLEAR( @_ ) }
 
 package DBM::Deep::_::Root;
 
@@ -1502,12 +914,11 @@ sub new {
     my $self = bless {
         file => undef,
         fh => undef,
+        file_offset => 0,
         end => 0,
         autoflush => undef,
         locking => undef,
-        volatile => undef,
         debug => undef,
-        mode => 'r+',
         filter_store_key => undef,
         filter_store_value => undef,
         filter_fetch_key => undef,
@@ -1517,6 +928,10 @@ sub new {
         %$args,
     }, $class;
 
+    if ( $self->{fh} && !$self->{file_offset} ) {
+        $self->{file_offset} = tell( $self->{fh} );
+    }
+
     return $self;
 }
 
@@ -1545,13 +960,14 @@ DBM::Deep - A pure perl multi-level hash/array DBM
   $db->{key} = 'value'; # tie() style
   print $db->{key};
   
-  $db->put('key', 'value'); # OO style
+  $db->put('key' => 'value'); # OO style
   print $db->get('key');
   
   # true multi-level support
   $db->{my_complex} = [
        'hello', { perl => 'rules' }, 
-       42, 99 ];
+       42, 99,
+  ];
 
 =head1 DESCRIPTION
 
@@ -1565,7 +981,7 @@ Mac OS X and Windows.
 
 =head1 INSTALLATION
 
-Hopefully you are using CPAN's excellent Perl module, which will download
+Hopefully you are using Perl's excellent CPAN module, which will download
 and install the module for you.  If not, get the tarball, and run these 
 commands:
 
@@ -1593,8 +1009,6 @@ file does not exist, it will automatically be created.  DB files are
 opened in "r+" (read/write) mode, and the type of object returned is a
 hash, unless otherwise specified (see L<OPTIONS> below).
 
-
-
 You can pass a number of options to the constructor to specify things like
 locking, autoflush, etc.  This is done by passing an inline hash:
 
@@ -1621,20 +1035,21 @@ specify the C<type> parameter:
 
 B<Note:> Specifing the C<type> parameter only takes effect when beginning
 a new DB file.  If you create a DBM::Deep object with an existing file, the
-C<type> will be loaded from the file header, and ignored if it is passed
-to the constructor.
+C<type> will be loaded from the file header, and an error will be thrown if
+the wrong type is passed in.
 
 =head2 TIE CONSTRUCTION
 
-Alternatively, you can create a DBM::Deep handle by using Perl's built-in
-tie() function.  This is not ideal, because you get only a basic, tied hash 
-(or array) which is not blessed, so you can't call any functions on it.
+Alternately, you can create a DBM::Deep handle by using Perl's built-in
+tie() function.  The object returned from tie() can be used to call methods,
+such as lock() and unlock(), but cannot be used to assign to the DBM::Deep
+file (as expected with most tie'd objects).
 
        my %hash;
-       tie %hash, "DBM::Deep", "foo.db";
+       my $db = tie %hash, "DBM::Deep", "foo.db";
        
        my @array;
-       tie @array, "DBM::Deep", "bar.db";
+       my $db = tie @array, "DBM::Deep", "bar.db";
 
 As with the OO constructor, you can replace the DB filename parameter with
 a hash containing one or more options (see L<OPTIONS> just below for the
@@ -1657,14 +1072,26 @@ DBM::Deep objects.  These apply to both the OO- and tie- based approaches.
 
 Filename of the DB file to link the handle to.  You can pass a full absolute
 filesystem path, partial path, or a plain filename if the file is in the 
-current working directory.  This is a required parameter.
+current working directory.  This is a required parameter (though q.v. fh).
+
+=item * fh
+
+If you want, you can pass in the fh instead of the file. This is most useful for doing
+something like:
+
+  my $db = DBM::Deep->new( { fh => \*DATA } );
+
+You are responsible for making sure that the fh has been opened appropriately for your
+needs. If you open it read-only and attempt to write, an exception will be thrown. If you
+open it write-only or append-only, an exception will be thrown immediately as DBM::Deep
+needs to read from the fh.
 
-=item * mode
+=item * file_offset
 
-File open mode (read-only, read-write, etc.) string passed to Perl's FileHandle
-module.  This is an optional parameter, and defaults to "r+" (read/write).
-B<Note:> If the default (r+) mode is selected, the file will also be auto-
-created if it doesn't exist.
+This is the offset within the file that the DBM::Deep db starts. Most of the time, you will
+not need to set this. However, it's there if you want it.
+
+If you pass in fh and do not set this, it will be set appropriately.
 
 =item * type
 
@@ -1683,19 +1110,10 @@ parameter, and defaults to 0 (disabled).  See L<LOCKING> below for more.
 
 =item * autoflush
 
-Specifies whether autoflush is to be enabled on the underlying FileHandle.  
+Specifies whether autoflush is to be enabled on the underlying filehandle.  
 This obviously slows down write operations, but is required if you may have 
-multiple processes accessing the same DB file (also consider enable I<locking> 
-or at least I<volatile>).  Pass any true value to enable.  This is an optional 
-parameter, and defaults to 0 (disabled).
-
-=item * volatile
-
-If I<volatile> mode is enabled, DBM::Deep will stat() the DB file before each
-STORE() operation.  This is required if an outside force may change the size of
-the file between transactions.  Locking also implicitly enables volatile.  This
-is useful if you want to use a different locking system or write your own.  Pass
-any true value to enable.  This is an optional parameter, and defaults to 0 
+multiple processes accessing the same DB file (also consider enable I<locking>).  
+Pass any true value to enable.  This is an optional parameter, and defaults to 0 
 (disabled).
 
 =item * autobless
@@ -1717,26 +1135,20 @@ Setting I<debug> mode will make all errors non-fatal, dump them out to
 STDERR, and continue on.  This is for debugging purposes only, and probably
 not what you want.  This is an optional parameter, and defaults to 0 (disabled).
 
-=item * fh
-
-Instead of passing a file path, you can instead pass a handle to an pre-opened
-filehandle.  Note: Beware of using the magick *DATA handle, as this actually 
-contains your entire Perl script, as well as the data following the __DATA__
-marker.  This will not work, because DBM::Deep uses absolute seek()s into the
-file.  Instead, consider reading *DATA into an IO::Scalar handle, then passing
-in that.
+B<NOTE>: This parameter is considered deprecated and should not be used anymore.
 
 =back
 
 =head1 TIE INTERFACE
 
 With DBM::Deep you can access your databases using Perl's standard hash/array
-syntax.  Because all DBM::Deep objects are I<tied> to hashes or arrays, you can treat
-them as such.  DBM::Deep will intercept all reads/writes and direct them to the right
-place -- the DB file.  This has nothing to do with the L<TIE CONSTRUCTION> 
-section above.  This simply tells you how to use DBM::Deep using regular hashes 
-and arrays, rather than calling functions like C<get()> and C<put()> (although those 
-work too).  It is entirely up to you how to want to access your databases.
+syntax.  Because all DBM::Deep objects are I<tied> to hashes or arrays, you can
+treat them as such.  DBM::Deep will intercept all reads/writes and direct them
+to the right place -- the DB file.  This has nothing to do with the
+L<TIE CONSTRUCTION> section above.  This simply tells you how to use DBM::Deep
+using regular hashes and arrays, rather than calling functions like C<get()>
+and C<put()> (although those work too).  It is entirely up to you how to want
+to access your databases.
 
 =head2 HASHES
 
@@ -1810,7 +1222,11 @@ C<put()>, C<get()>, C<exists()>, C<delete()> and C<clear()>.
 
 =over
 
-=item * put()
+=item * new() / clone()
+
+These are the constructor and copy-functions.
+
+=item * put() / store()
 
 Stores a new hash key/value pair, or sets an array element value.  Takes two
 arguments, the hash key or array index, and the new value.  The value can be
@@ -1819,7 +1235,7 @@ a scalar, hash ref or array ref.  Returns true on success, false on failure.
        $db->put("foo", "bar"); # for hashes
        $db->put(1, "bar"); # for arrays
 
-=item * get()
+=item * get() / fetch()
 
 Fetches the value of a hash key or array element.  Takes one argument: the hash
 key or array index.  Returns a scalar, hash ref or array ref, depending on the 
@@ -1858,6 +1274,22 @@ details and workarounds.
 
        $db->clear(); # hashes or arrays
 
+=item * lock() / unlock()
+
+q.v. Locking.
+
+=item * optimize()
+
+Recover lost disk space.
+
+=item * import() / export()
+
+Data going in and out.
+
+=item * set_digest() / set_pack() / set_filter()
+
+q.v. adjusting the interal parameters.
+
 =back
 
 =head2 HASHES
@@ -1991,7 +1423,7 @@ parameter when constructing your DBM::Deep object (see L<SETUP> above).
                locking => 1
        );
 
-This causes DBM::Deep to C<flock()> the underlying FileHandle object with exclusive 
+This causes DBM::Deep to C<flock()> the underlying filehandle with exclusive 
 mode for writes, and shared mode for reads.  This is required if you have 
 multiple processes accessing the same database file, to avoid file corruption.  
 Please note that C<flock()> does NOT work for files over NFS.  See L<DB OVER 
@@ -2001,7 +1433,7 @@ NFS> below for more.
 
 You can explicitly lock a database, so it remains locked for multiple 
 transactions.  This is done by calling the C<lock()> method, and passing an 
-optional lock mode argument (defaults to exclusive mode).  This is particularly 
+optional lock mode argument (defaults to exclusive mode).  This is particularly
 useful for things like counters, where the current value needs to be fetched, 
 then incremented, then stored again.
 
@@ -2026,11 +1458,6 @@ same as the constants defined in Perl's C<Fcntl> module.
        # something here
        $db->unlock();
 
-If you want to implement your own file locking scheme, be sure to create your
-DBM::Deep objects setting the C<volatile> option to true.  This hints to DBM::Deep
-that the DB file may change between transactions.  See L<LOW-LEVEL ACCESS> 
-below for more.
-
 =head1 IMPORTING/EXPORTING
 
 You can import existing complex structures by calling the C<import()> method,
@@ -2065,8 +1492,6 @@ keys are merged with the existing ones, replacing if they already exist.
 The C<import()> method can be called on any database level (not just the base 
 level), and works with both hash and array DB types.
 
-
-
 B<Note:> Make sure your existing structure has no circular references in it.
 These will cause an infinite loop when importing.
 
@@ -2095,8 +1520,6 @@ the base level), and works with both hash and array DB types.  Be careful of
 large databases -- you can store a lot more data in a DBM::Deep object than an 
 in-memory Perl structure.
 
-
-
 B<Note:> Make sure your database has no circular references in it.
 These will cause an infinite loop when exporting.
 
@@ -2238,22 +1661,12 @@ actually numerical index numbers, and are not filtered.
 =head1 ERROR HANDLING
 
 Most DBM::Deep methods return a true value for success, and call die() on
-failure.  You can wrap calls in an eval block to catch the die.  Also, the 
-actual error message is stored in an internal scalar, which can be fetched by 
-calling the C<error()> method.
+failure.  You can wrap calls in an eval block to catch the die.
 
        my $db = DBM::Deep->new( "foo.db" ); # create hash
        eval { $db->push("foo"); }; # ILLEGAL -- push is array-only call
        
-       print $db->error(); # prints error message
-
-You can then call C<clear_error()> to clear the current error state.
-
-       $db->clear_error();
-
-If you set the C<debug> option to true when creating your DBM::Deep object,
-all errors are considered NON-FATAL, and dumped to STDERR.  This is only
-for debugging purposes.
+    print $@;           # prints error message
 
 =head1 LARGEFILE SUPPORT
 
@@ -2268,37 +1681,33 @@ This tells DBM::Deep to pack all file offsets with 8-byte (64-bit) quad words
 instead of 32-bit longs.  After setting these values your DB files have a 
 theoretical maximum size of 16 XB (exabytes).
 
-
-
 B<Note:> Changing these values will B<NOT> work for existing database files.
 Only change this for new files, and make sure it stays set consistently 
 throughout the file's life.  If you do set these values, you can no longer 
 access 32-bit DB files.  You can, however, call C<set_pack(4, 'N')> to change 
 back to 32-bit mode.
 
-
-
 B<Note:> I have not personally tested files > 2 GB -- all my systems have 
 only a 32-bit Perl.  However, I have received user reports that this does 
 indeed work!
 
 =head1 LOW-LEVEL ACCESS
 
-If you require low-level access to the underlying FileHandle that DBM::Deep uses,
-you can call the C<fh()> method, which returns the handle:
+If you require low-level access to the underlying filehandle that DBM::Deep uses,
+you can call the C<_fh()> method, which returns the handle:
 
-       my $fh = $db->fh();
+       my $fh = $db->_fh();
 
 This method can be called on the root level of the datbase, or any child
 hashes or arrays.  All levels share a I<root> structure, which contains things
-like the FileHandle, a reference counter, and all your options you specified
+like the filehandle, a reference counter, and all the options specified
 when you created the object.  You can get access to this root structure by 
 calling the C<root()> method.
 
-       my $root = $db->root();
+       my $root = $db->_root();
 
 This is useful for changing options after the object has already been created,
-such as enabling/disabling locking, volatile or debug modes.  You can also
+such as enabling/disabling locking, or debug modes.  You can also
 store your own temporary user data in this structure (be wary of name 
 collision), which is then accessible from any child hash or array.
 
@@ -2392,8 +1801,6 @@ as the original, named with a ".tmp" extension, and is deleted when the
 operation completes.  Oh, and if locking is enabled, the DB is automatically 
 locked for the entire duration of the copy.
 
-
-
 B<WARNING:> Only call optimize() on the top-level node of the database, and 
 make sure there are no child references lying around.  DBM::Deep keeps a reference 
 counter, and if it is greater than 1, optimize() will abort and return undef.
@@ -2433,9 +1840,9 @@ be addressed in a later version of DBM::Deep.
 Beware of using DB files over NFS.  DBM::Deep uses flock(), which works well on local
 filesystems, but will NOT protect you from file corruption over NFS.  I've heard 
 about setting up your NFS server with a locking daemon, then using lockf() to 
-lock your files, but your milage may vary there as well.  From what I 
+lock your files, but your mileage may vary there as well.  From what I 
 understand, there is no real way to do it.  However, if you need access to the 
-underlying FileHandle in DBM::Deep for using some other kind of locking scheme like 
+underlying filehandle in DBM::Deep for using some other kind of locking scheme like 
 lockf(), see the L<LOW-LEVEL ACCESS> section above.
 
 =head2 COPYING OBJECTS
@@ -2446,12 +1853,22 @@ returns a new, blessed, tied hash or array to the same level in the DB.
 
        my $copy = $db->clone();
 
+B<Note>: Since clone() here is cloning the object, not the database location, any
+modifications to either $db or $copy will be visible in both.
+
 =head2 LARGE ARRAYS
 
 Beware of using C<shift()>, C<unshift()> or C<splice()> with large arrays.
 These functions cause every element in the array to move, which can be murder
 on DBM::Deep, as every element has to be fetched from disk, then stored again in
-a different location.  This may be addressed in a later version.
+a different location.  This will be addressed in the forthcoming version 1.00.
+
+=head2 WRITEONLY FILES
+
+If you pass in a filehandle to new(), you may have opened it in either a readonly or
+writeonly mode. STORE will verify that the filehandle is writable. However, there
+doesn't seem to be a good way to determine if a filehandle is readable. And, if the
+filehandle isn't readable, it's not clear what will happen. So, don't do that.
 
 =head1 PERFORMANCE
 
@@ -2540,7 +1957,7 @@ included for reference.
 
 DBM::Deep files always start with a 32-bit signature to identify the file type.
 This is at offset 0.  The signature is "DPDB" in network byte order.  This is
-checked when the file is opened.
+checked for when the file is opened and an error will be thrown if it's not found.
 
 =head2 TAG
 
@@ -2557,16 +1974,12 @@ This is a standard tag header followed by 1024 bytes (in 32-bit mode) or 2048
 bytes (in 64-bit mode) of data.  The type is I<H> for hash or I<A> for array, 
 depending on how the DBM::Deep object was constructed.
 
-
-
 The index works by looking at a I<MD5 Hash> of the hash key (or array index 
 number).  The first 8-bit char of the MD5 signature is the offset into the 
 index, multipled by 4 in 32-bit mode, or 8 in 64-bit mode.  The value of the 
 index element is a file offset of the next tag for the key/element in question,
 which is usually a I<Bucket List> tag (see below).
 
-
-
 The next tag I<could> be another index, depending on how many keys/elements
 exist.  See L<RE-INDEXING> below for details.
 
@@ -2591,8 +2004,6 @@ just after the value is another size (32-bit unsigned long) and then the plain
 key itself.  Since the value is likely to be fetched more often than the plain 
 key, I figured it would be I<slightly> faster to store the value first.
 
-
-
 If the type is I<H> (hash) or I<A> (array), the value is another I<Master Index>
 record for the nested structure, where the process begins all over again.
 
@@ -2608,8 +2019,6 @@ inserted into the new index.  Several new Bucket Lists are created in the
 process, as a new MD5 char from the key is being examined (it is unlikely that 
 the keys will all share the same next char of their MD5s).
 
-
-
 Because of the way the I<MD5> algorithm works, it is impossible to tell exactly
 when the Bucket Lists will turn into indexes, but the first round tends to 
 happen right around 4,000 keys.  You will see a I<slight> decrease in 
@@ -2625,10 +2034,10 @@ this is 340 unodecillion, but don't quote me).
 
 =head2 STORING
 
-When a new key/element is stored, the key (or index number) is first ran through 
+When a new key/element is stored, the key (or index number) is first run through 
 I<Digest::MD5> to get a 128-bit signature (example, in hex: 
 b05783b0773d894396d475ced9d2f4f6).  Then, the I<Master Index> record is checked
-for the first char of the signature (in this case I<b>).  If it does not exist,
+for the first char of the signature (in this case I<b0>).  If it does not exist,
 a new I<Bucket List> is created for our key (and the next 15 future keys that 
 happen to also have I<b> as their first MD5 char).  The entire MD5 is written 
 to the I<Bucket List> along with the offset of the new I<Bucket> record (EOF at
@@ -2645,12 +2054,10 @@ contains up to 16 full MD5 hashes.  Each is checked for equality to the key in
 question.  If we found a match, the I<Bucket> tag is loaded, where the value and 
 plain key are stored.
 
-
-
 Fetching the plain key occurs when calling the I<first_key()> and I<next_key()>
 methods.  In this process the indexes are walked systematically, and each key
 fetched in increasing MD5 order (which is why it appears random).   Once the
-I<Bucket> is found, the value is skipped the plain key returned instead.  
+I<Bucket> is found, the value is skipped and the plain key returned instead.  
 B<Note:> Do not count on keys being fetched as if the MD5 hashes were 
 alphabetically sorted.  This only happens on an index-level -- as soon as the 
 I<Bucket Lists> are hit, the keys will come out in the order they went in -- 
@@ -2659,22 +2066,29 @@ built-in hashes.
 
 =head1 CODE COVERAGE
 
-I use B<Devel::Cover> to test the code coverage of my tests, below is the B<Devel::Cover> report on this 
-module's test suite.
+We use B<Devel::Cover> to test the code coverage of our tests, below is the
+B<Devel::Cover> report on this module's test suite.
 
   ---------------------------- ------ ------ ------ ------ ------ ------ ------
   File                           stmt   bran   cond    sub    pod   time  total
   ---------------------------- ------ ------ ------ ------ ------ ------ ------
-  blib/lib/DBM/Deep.pm           94.1   82.9   74.5   98.0   10.5   98.1   88.2
-  blib/lib/DBM/Deep/Array.pm     97.8   83.3   50.0  100.0    n/a    1.6   94.4
-  blib/lib/DBM/Deep/Hash.pm      93.3   85.7  100.0  100.0    n/a    0.3   92.7
-  Total                          94.5   83.1   75.5   98.4   10.5  100.0   89.0
+  blib/lib/DBM/Deep.pm           95.2   83.8   70.0   98.2  100.0   58.0   91.0
+  blib/lib/DBM/Deep/Array.pm    100.0   91.1  100.0  100.0    n/a   26.7   98.0
+  blib/lib/DBM/Deep/Hash.pm      95.3   80.0  100.0  100.0    n/a   15.3   92.4
+  Total                          96.2   84.8   74.4   98.8  100.0  100.0   92.4
   ---------------------------- ------ ------ ------ ------ ------ ------ ------
 
-=head1 AUTHOR
+=head1 MORE INFORMATION
+
+Check out the DBM::Deep Google Group at L<http://groups.google.com/group/DBM-Deep>
+or send email to L<DBM-Deep@googlegroups.com>.
+
+=head1 AUTHORS
 
 Joseph Huckaby, L<jhuckaby@cpan.org>
 
+Rob Kinyon, L<rkinyon@cpan.org>
+
 Special thanks to Adam Sah and Rich Gaushell!  You know why :-)
 
 =head1 SEE ALSO
@@ -2684,7 +2098,7 @@ Digest::SHA256(3), Crypt::Blowfish(3), Compress::Zlib(3)
 
 =head1 LICENSE
 
-Copyright (c) 2002-2005 Joseph Huckaby.  All Rights Reserved.
+Copyright (c) 2002-2006 Joseph Huckaby.  All Rights Reserved.
 This is free software, you may use it and distribute it under the
 same terms as Perl itself.