From: rkinyon@cpan.org Date: Sat, 18 Oct 2008 18:58:26 +0000 (+0000) Subject: First checkin of the reversion back from the failed optimization effort. I will be... X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=dbsrgits%2FDBM-Deep.git;a=commitdiff_plain;h=9c87a079060af9b367681cd1f96d0abd36745836 First checkin of the reversion back from the failed optimization effort. I will be keeping a lot of the code that worked, but not the optimization code. git-svn-id: http://svn.ali.as/cpan/trunk/DBM-Deep@4447 88f4d9cd-8a04-0410-9d60-8f63309c3137 --- diff --git a/Changes b/Changes index e627f61..794c0e6 100644 --- a/Changes +++ b/Changes @@ -7,7 +7,6 @@ Revision history for DBM::Deep. a lock_shared() method. The :flock constants are no longer imported into the DBM::Deep namespace. **** THIS IS AN API CHANGE **** - - Start the process of optimization. 1.0013 Jun 13 23:15:00 2008 EST - (This version is compatible with 1.0012) diff --git a/lib/DBM/Deep.pm b/lib/DBM/Deep.pm index 1defc2b..4d6a980 100644 --- a/lib/DBM/Deep.pm +++ b/lib/DBM/Deep.pm @@ -142,17 +142,17 @@ sub TIEARRAY { sub lock_exclusive { my $self = shift->_get_self; - return $self->_engine->lock_exclusive( $self ); + return $self->_engine->lock_exclusive( $self, @_ ); } *lock = \&lock_exclusive; sub lock_shared { my $self = shift->_get_self; - return $self->_engine->lock_shared( $self ); + return $self->_engine->lock_shared( $self, @_ ); } sub unlock { my $self = shift->_get_self; - return $self->_engine->unlock( $self ); + return $self->_engine->unlock( $self, @_ ); } sub _copy_value { diff --git a/lib/DBM/Deep.pod b/lib/DBM/Deep.pod index 7252f7c..33b7eb9 100644 --- a/lib/DBM/Deep.pod +++ b/lib/DBM/Deep.pod @@ -385,7 +385,7 @@ value. $db->clear(); # hashes or arrays -=item * lock_exclusive() / lock_shared() / lock() / unlock() +=item * lock() / unlock() / lock_exclusive() / lock_shared() q.v. L for more info. @@ -555,12 +555,12 @@ NFS> below for more. =head2 Explicit Locking You can explicitly lock a database, so it remains locked for multiple -actions. This is done by calling the C method, and passing an -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. +actions. This is done by calling the C method (for when you +want to write) or the C method (for when you want to read). +This is particularly useful for things like counters, where the current value +needs to be fetched, then incremented, then stored again. - $db->lock(); + $db->lock_exclusive(); my $counter = $db->get("counter"); $counter++; $db->put("counter", $counter); @@ -568,13 +568,10 @@ then incremented, then stored again. # or... - $db->lock(); + $db->lock_exclusive(); $db->{counter}++; $db->unlock(); -If you want a shared lock, you will need to call C. C is -an alias to C. - =head2 Win32/Cygwin Due to Win32 actually enforcing the read-only status of a shared lock, all diff --git a/lib/DBM/Deep/Array.pm b/lib/DBM/Deep/Array.pm index 5f8494e..5521477 100644 --- a/lib/DBM/Deep/Array.pm +++ b/lib/DBM/Deep/Array.pm @@ -3,7 +3,9 @@ package DBM::Deep::Array; use 5.006_000; use strict; -use warnings FATAL => 'all'; +use warnings; + +our $VERSION = q(1.0013); # This is to allow DBM::Deep::Array to handle negative indices on # its own. Otherwise, Perl would intercept the call to negative @@ -26,17 +28,12 @@ sub TIEARRAY { $args->{type} = $class->TYPE_ARRAY; - my $self = $class->_init($args); - -# $self->STORESIZE; - - return $self; + return $class->_init($args); } sub FETCH { my $self = shift->_get_self; my ($key) = @_; - warn "ARRAY:FETCH( $key )\n" if DBM::Deep::DEBUG; $self->lock_shared; @@ -68,7 +65,6 @@ sub FETCH { sub STORE { my $self = shift->_get_self; my ($key, $value) = @_; - warn "ARRAY::STORE($self, $key)\n" if DBM::Deep::DEBUG; $self->lock_exclusive; @@ -110,7 +106,6 @@ sub STORE { sub EXISTS { my $self = shift->_get_self; my ($key) = @_; - warn "ARRAY::EXISTS($self, $key)\n" if DBM::Deep::DEBUG; $self->lock_shared; @@ -181,14 +176,12 @@ sub DELETE { # going to work. sub FETCHSIZE { my $self = shift->_get_self; - warn "ARRAY::FETCHSIZE($self)\n" if DBM::Deep::DEBUG; $self->lock_shared; my $SAVE_FILTER = $self->_engine->storage->{filter_fetch_value}; $self->_engine->storage->{filter_fetch_value} = undef; - # If there is no flushing, then things get out of sync. my $size = $self->FETCH('length') || 0; $self->_engine->storage->{filter_fetch_value} = $SAVE_FILTER; @@ -201,7 +194,6 @@ sub FETCHSIZE { sub STORESIZE { my $self = shift->_get_self; my ($new_length) = @_; - warn "ARRAY::STORESIZE($self, $new_length)\n" if DBM::Deep::DEBUG; $self->lock_exclusive; @@ -219,7 +211,6 @@ sub STORESIZE { sub POP { my $self = shift->_get_self; - warn "ARRAY::POP($self)\n" if DBM::Deep::DEBUG; $self->lock_exclusive; @@ -241,7 +232,6 @@ sub POP { sub PUSH { my $self = shift->_get_self; - warn "ARRAY::PUSH($self)\n" if DBM::Deep::DEBUG; $self->lock_exclusive; @@ -268,7 +258,7 @@ sub _move_value { sub SHIFT { my $self = shift->_get_self; - warn "ARRAY::SHIFT($self)\n" if DBM::Deep::DEBUG; + warn "SHIFT($self)\n" if DBM::Deep::DEBUG; $self->lock_exclusive; @@ -297,7 +287,6 @@ sub SHIFT { sub UNSHIFT { my $self = shift->_get_self; - warn "ARRAY::UNSHIFT($self)\n" if DBM::Deep::DEBUG; my @new_elements = @_; $self->lock_exclusive; @@ -324,7 +313,6 @@ sub UNSHIFT { sub SPLICE { my $self = shift->_get_self; - warn "ARRAY::SPLICE($self)\n" if DBM::Deep::DEBUG; $self->lock_exclusive; @@ -391,7 +379,6 @@ sub SPLICE { # We don't need to populate it, yet. # It will be useful, though, when we split out HASH and ARRAY sub EXTEND { - warn "ARRAY::EXTEND()\n" if DBM::Deep::DEBUG; ## # Perl will call EXTEND() when the array is likely to grow. # We don't care, but include it because it gets called at times. diff --git a/lib/DBM/Deep/Engine.pm b/lib/DBM/Deep/Engine.pm index 0faa0d3..6fb2039 100644 --- a/lib/DBM/Deep/Engine.pm +++ b/lib/DBM/Deep/Engine.pm @@ -29,7 +29,7 @@ sub SIG_BLIST () { 'B' } sub SIG_FREE () { 'F' } sub SIG_SIZE () { 1 } -our $STALE_SIZE = 2; +my $STALE_SIZE = 2; # Please refer to the pack() documentation for further information my %StP = ( @@ -38,17 +38,6 @@ my %StP = ( 4 => 'N', # Unsigned long in "network" (big-endian) order 8 => 'Q', # Usigned quad (no order specified, presumably machine-dependent) ); -sub StP { $StP{$_[1]} } - -# Import these after the SIG_* definitions because those definitions are used -# in the headers of these classes. -RobK, 2008-06-20 -use DBM::Deep::Engine::Sector::BucketList; -use DBM::Deep::Engine::Sector::FileHeader; -use DBM::Deep::Engine::Sector::Index; -use DBM::Deep::Engine::Sector::Null; -use DBM::Deep::Engine::Sector::Reference; -use DBM::Deep::Engine::Sector::Scalar; -use DBM::Deep::Iterator; ################################################################################ @@ -186,7 +175,7 @@ sub make_reference { # This will be a Reference sector my $sector = $self->_load_sector( $obj->_base_offset ) - or DBM::Deep->_throw_error( "How did make_reference fail (no sector for '$obj')?!" ); + or DBM::Deep->_throw_error( "How did get_classname fail (no sector for '$obj')?!" ); if ( $sector->staleness != $obj->_staleness ) { return; @@ -284,10 +273,10 @@ sub write_value { # This will be a Reference sector my $sector = $self->_load_sector( $obj->_base_offset ) - or DBM::Deep->_throw_error( "1: Cannot write to a deleted spot in DBM::Deep." ); + or DBM::Deep->_throw_error( "Cannot write to a deleted spot in DBM::Deep." ); if ( $sector->staleness != $obj->_staleness ) { - DBM::Deep->_throw_error( "2: Cannot write to a deleted spot in DBM::Deep." ); + DBM::Deep->_throw_error( "Cannot write to a deleted spot in DBM::Deep." ); } my ($class, $type); @@ -414,42 +403,43 @@ sub setup_fh { my $self = shift; my ($obj) = @_; - return 1 if $obj->_base_offset; + # We're opening the file. + unless ( $obj->_base_offset ) { + my $bytes_read = $self->_read_file_header; - my $header = $self->_load_header; + # Creating a new file + unless ( $bytes_read ) { + $self->_write_file_header; - # Creating a new file - if ( $header->is_new ) { - # 1) Create Array/Hash entry - my $sector = DBM::Deep::Engine::Sector::Reference->new({ - engine => $self, - type => $obj->_type, - }); - $obj->{base_offset} = $sector->offset; - $obj->{staleness} = $sector->staleness; + # 1) Create Array/Hash entry + my $initial_reference = DBM::Deep::Engine::Sector::Reference->new({ + engine => $self, + type => $obj->_type, + }); + $obj->{base_offset} = $initial_reference->offset; + $obj->{staleness} = $initial_reference->staleness; - $self->flush; - } - # Reading from an existing file - else { - $obj->{base_offset} = $header->size; - my $sector = DBM::Deep::Engine::Sector::Reference->new({ - engine => $self, - offset => $obj->_base_offset, - }); - unless ( $sector ) { - DBM::Deep->_throw_error("Corrupted file, no master index record"); + $self->storage->flush; } + # Reading from an existing file + else { + $obj->{base_offset} = $bytes_read; + my $initial_reference = DBM::Deep::Engine::Sector::Reference->new({ + engine => $self, + offset => $obj->_base_offset, + }); + unless ( $initial_reference ) { + DBM::Deep->_throw_error("Corrupted file, no master index record"); + } - unless ($obj->_type eq $sector->type) { - DBM::Deep->_throw_error("File type mismatch"); - } + unless ($obj->_type eq $initial_reference->type) { + DBM::Deep->_throw_error("File type mismatch"); + } - $obj->{staleness} = $sector->staleness; + $obj->{staleness} = $initial_reference->staleness; + } } - $self->storage->set_inode; - return 1; } @@ -491,9 +481,23 @@ sub rollback { DBM::Deep->_throw_error( "Cannot rollback without an active transaction" ); } - foreach my $entry ( @{ $self->get_entries } ) { - my ($sector, $idx) = split ':', $entry; - $self->_load_sector( $sector )->rollback( $idx ); + # Each entry is the file location for a bucket that has a modification for + # this transaction. The entries need to be expunged. + foreach my $entry (@{ $self->get_entries } ) { + # Remove the entry here + my $read_loc = $entry + + $self->hash_size + + $self->byte_size + + $self->byte_size + + ($self->trans_id - 1) * ( $self->byte_size + $STALE_SIZE ); + + my $data_loc = $self->storage->read_at( $read_loc, $self->byte_size ); + $data_loc = unpack( $StP{$self->byte_size}, $data_loc ); + $self->storage->print_at( $read_loc, pack( $StP{$self->byte_size}, 0 ) ); + + if ( $data_loc > 1 ) { + $self->_load_sector( $data_loc )->free; + } } $self->clear_entries; @@ -515,9 +519,29 @@ sub commit { DBM::Deep->_throw_error( "Cannot commit without an active transaction" ); } - foreach my $entry ( @{ $self->get_entries } ) { - my ($sector, $idx) = split ':', $entry; - $self->_load_sector( $sector )->commit( $idx ); + foreach my $entry (@{ $self->get_entries } ) { + # Overwrite the entry in head with the entry in trans_id + my $base = $entry + + $self->hash_size + + $self->byte_size; + + my $head_loc = $self->storage->read_at( $base, $self->byte_size ); + $head_loc = unpack( $StP{$self->byte_size}, $head_loc ); + + my $spot = $base + $self->byte_size + ($self->trans_id - 1) * ( $self->byte_size + $STALE_SIZE ); + my $trans_loc = $self->storage->read_at( + $spot, $self->byte_size, + ); + + $self->storage->print_at( $base, $trans_loc ); + $self->storage->print_at( + $spot, + pack( $StP{$self->byte_size} . ' ' . $StP{$STALE_SIZE}, (0) x 2 ), + ); + + if ( $head_loc > 1 ) { + $self->_load_sector( $head_loc )->free; + } } $self->clear_entries; @@ -533,12 +557,21 @@ sub commit { sub read_txn_slots { my $self = shift; - return $self->_load_header->read_txn_slots(@_); + my $bl = $self->txn_bitfield_len; + my $num_bits = $bl * 8; + return split '', unpack( 'b'.$num_bits, + $self->storage->read_at( + $self->trans_loc, $bl, + ) + ); } sub write_txn_slots { my $self = shift; - return $self->_load_header->write_txn_slots(@_); + my $num_bits = $self->txn_bitfield_len * 8; + $self->storage->print_at( $self->trans_loc, + pack( 'b'.$num_bits, join('', @_) ), + ); } sub get_running_txn_ids { @@ -549,12 +582,30 @@ sub get_running_txn_ids { sub get_txn_staleness_counter { my $self = shift; - return $self->_load_header->get_txn_staleness_counter(@_); + my ($trans_id) = @_; + + # Hardcode staleness of 0 for the HEAD + return 0 unless $trans_id; + + return unpack( $StP{$STALE_SIZE}, + $self->storage->read_at( + $self->trans_loc + $self->txn_bitfield_len + $STALE_SIZE * ($trans_id - 1), + $STALE_SIZE, + ) + ); } sub inc_txn_staleness_counter { my $self = shift; - return $self->_load_header->inc_txn_staleness_counter(@_); + my ($trans_id) = @_; + + # Hardcode staleness of 0 for the HEAD + return 0 unless $trans_id; + + $self->storage->print_at( + $self->trans_loc + $self->txn_bitfield_len + $STALE_SIZE * ($trans_id - 1), + pack( $StP{$STALE_SIZE}, $self->get_txn_staleness_counter( $trans_id ) + 1 ), + ); } sub get_entries { @@ -564,25 +615,23 @@ sub get_entries { sub add_entry { my $self = shift; - my ($trans_id, $loc, $idx) = @_; - - return unless $trans_id; + my ($trans_id, $loc) = @_; $self->{entries}{$trans_id} ||= {}; - $self->{entries}{$trans_id}{"$loc:$idx"} = undef; + $self->{entries}{$trans_id}{$loc} = undef; } # If the buckets are being relocated because of a reindexing, the entries # mechanism needs to be made aware of it. sub reindex_entry { my $self = shift; - my ($old_loc, $old_idx, $new_loc, $new_idx) = @_; + my ($old_loc, $new_loc) = @_; TRANS: while ( my ($trans_id, $locs) = each %{ $self->{entries} } ) { - if ( exists $locs->{"$old_loc:$old_idx"} ) { - delete $locs->{"$old_loc:$old_idx"}; - $locs->{"$new_loc:$new_idx"} = undef; + if ( exists $locs->{$old_loc} ) { + delete $locs->{$old_loc}; + $locs->{$new_loc} = undef; next TRANS; } } @@ -595,144 +644,246 @@ sub clear_entries { ################################################################################ -sub _apply_digest { - my $self = shift; - return $self->{digest}->(@_); -} +{ + my $header_fixed = length( SIG_FILE ) + 1 + 4 + 4; + my $this_file_version = 3; -sub _add_free_blist_sector { shift->_add_free_sector( 0, @_ ) } -sub _add_free_data_sector { shift->_add_free_sector( 1, @_ ) } -sub _add_free_index_sector { shift->_add_free_sector( 2, @_ ) } -sub _add_free_sector { shift->_load_header->add_free_sector( @_ ) } + sub _write_file_header { + my $self = shift; -sub _request_blist_sector { shift->_request_sector( 0, @_ ) } -sub _request_data_sector { shift->_request_sector( 1, @_ ) } -sub _request_index_sector { shift->_request_sector( 2, @_ ) } -sub _request_sector { shift->_load_header->request_sector( @_ ) } + my $nt = $self->num_txns; + my $bl = $self->txn_bitfield_len; -################################################################################ + my $header_var = 1 + 1 + 1 + 1 + $bl + $STALE_SIZE * ($nt - 1) + 3 * $self->byte_size; -{ - my %t = ( - SIG_ARRAY => 'Reference', - SIG_HASH => 'Reference', - SIG_BLIST => 'BucketList', - SIG_INDEX => 'Index', - SIG_NULL => 'Null', - SIG_DATA => 'Scalar', - ); + my $loc = $self->storage->request_space( $header_fixed + $header_var ); - my %class_for; - while ( my ($k,$v) = each %t ) { - $class_for{ DBM::Deep::Engine->$k } = "DBM::Deep::Engine::Sector::$v"; - } + $self->storage->print_at( $loc, + SIG_FILE, + SIG_HEADER, + pack('N', $this_file_version), # At this point, we're at 9 bytes + pack('N', $header_var), # header size + # --- Above is $header_fixed. Below is $header_var + pack('C', $self->byte_size), - sub load_sector { - my $self = shift; - my ($offset) = @_; + # These shenanigans are to allow a 256 within a C + pack('C', $self->max_buckets - 1), + pack('C', $self->data_sector_size - 1), - my $data = $self->get_data( $offset ) - or return;#die "Cannot read from '$offset'\n"; - my $type = substr( $$data, 0, 1 ); - my $class = $class_for{ $type }; - return $class->new({ - engine => $self, - type => $type, - offset => $offset, - }); - } - *_load_sector = \&load_sector; + pack('C', $nt), + pack('C' . $bl, 0 ), # Transaction activeness bitfield + pack($StP{$STALE_SIZE}.($nt-1), 0 x ($nt-1) ), # Transaction staleness counters + pack($StP{$self->byte_size}, 0), # Start of free chain (blist size) + pack($StP{$self->byte_size}, 0), # Start of free chain (data size) + pack($StP{$self->byte_size}, 0), # Start of free chain (index size) + ); - sub load_header { - my $self = shift; + #XXX Set these less fragilely + $self->set_trans_loc( $header_fixed + 4 ); + $self->set_chains_loc( $header_fixed + 4 + $bl + $STALE_SIZE * ($nt-1) ); - #XXX Does this mean we make too many objects? -RobK, 2008-06-23 - return DBM::Deep::Engine::Sector::FileHeader->new({ - engine => $self, - offset => 0, - }); + return; } - *_load_header = \&load_header; - sub get_data { + sub _read_file_header { my $self = shift; - my ($offset, $size) = @_; - return unless defined $offset; - unless ( exists $self->sector_cache->{$offset} ) { - # Don't worry about the header sector. It will manage itself. - return unless $offset; + my $buffer = $self->storage->read_at( 0, $header_fixed ); + return unless length($buffer); - if ( !defined $size ) { - my $type = $self->storage->read_at( $offset, 1 ) - or die "($offset): Cannot read from '$offset' to find the type\n"; + my ($file_signature, $sig_header, $file_version, $size) = unpack( + 'A4 A N N', $buffer + ); - if ( $type eq $self->SIG_FREE ) { - return; - } + unless ( $file_signature eq SIG_FILE ) { + $self->storage->close; + DBM::Deep->_throw_error( "Signature not found -- file is not a Deep DB" ); + } - my $class = $class_for{$type} - or die "($offset): Cannot find class for '$type'\n"; - $size = $class->size( $self ) - or die "($offset): '$class' doesn't return a size\n"; - $self->sector_cache->{$offset} = $type . $self->storage->read_at( undef, $size - 1 ); - } - else { - $self->sector_cache->{$offset} = $self->storage->read_at( $offset, $size ) - or return; - } + unless ( $sig_header eq SIG_HEADER ) { + $self->storage->close; + DBM::Deep->_throw_error( "Pre-1.00 file version found" ); + } + + unless ( $file_version == $this_file_version ) { + $self->storage->close; + DBM::Deep->_throw_error( + "Wrong file version found - " . $file_version . + " - expected " . $this_file_version + ); + } + + my $buffer2 = $self->storage->read_at( undef, $size ); + my @values = unpack( 'C C C C', $buffer2 ); + + if ( @values != 4 || grep { !defined } @values ) { + $self->storage->close; + DBM::Deep->_throw_error("Corrupted file - bad header"); + } + + #XXX Add warnings if values weren't set right + @{$self}{qw(byte_size max_buckets data_sector_size num_txns)} = @values; + + # These shenangians are to allow a 256 within a C + $self->{max_buckets} += 1; + $self->{data_sector_size} += 1; + + my $bl = $self->txn_bitfield_len; + + my $header_var = scalar(@values) + $bl + $STALE_SIZE * ($self->num_txns - 1) + 3 * $self->byte_size; + unless ( $size == $header_var ) { + $self->storage->close; + DBM::Deep->_throw_error( "Unexpected size found ($size <-> $header_var)." ); } - return \$self->sector_cache->{$offset}; + $self->set_trans_loc( $header_fixed + scalar(@values) ); + $self->set_chains_loc( $header_fixed + scalar(@values) + $bl + $STALE_SIZE * ($self->num_txns - 1) ); + + return length($buffer) + length($buffer2); } } -sub sector_cache { +sub _load_sector { my $self = shift; - return $self->{sector_cache} ||= {}; -} + my ($offset) = @_; -sub clear_sector_cache { - my $self = shift; - $self->{sector_cache} = {}; + # Add a catch for offset of 0 or 1 + return if !$offset || $offset <= 1; + + my $type = $self->storage->read_at( $offset, 1 ); + return if $type eq chr(0); + + if ( $type eq $self->SIG_ARRAY || $type eq $self->SIG_HASH ) { + return DBM::Deep::Engine::Sector::Reference->new({ + engine => $self, + type => $type, + offset => $offset, + }); + } + # XXX Don't we need key_md5 here? + elsif ( $type eq $self->SIG_BLIST ) { + return DBM::Deep::Engine::Sector::BucketList->new({ + engine => $self, + type => $type, + offset => $offset, + }); + } + elsif ( $type eq $self->SIG_INDEX ) { + return DBM::Deep::Engine::Sector::Index->new({ + engine => $self, + type => $type, + offset => $offset, + }); + } + elsif ( $type eq $self->SIG_NULL ) { + return DBM::Deep::Engine::Sector::Null->new({ + engine => $self, + type => $type, + offset => $offset, + }); + } + elsif ( $type eq $self->SIG_DATA ) { + return DBM::Deep::Engine::Sector::Scalar->new({ + engine => $self, + type => $type, + offset => $offset, + }); + } + # This was deleted from under us, so just return and let the caller figure it out. + elsif ( $type eq $self->SIG_FREE ) { + return; + } + + DBM::Deep->_throw_error( "'$offset': Don't know what to do with type '$type'" ); } -sub dirty_sectors { +sub _apply_digest { my $self = shift; - return $self->{dirty_sectors} ||= {}; + return $self->{digest}->(@_); } -sub clear_dirty_sectors { +sub _add_free_blist_sector { shift->_add_free_sector( 0, @_ ) } +sub _add_free_data_sector { shift->_add_free_sector( 1, @_ ) } +sub _add_free_index_sector { shift->_add_free_sector( 2, @_ ) } + +sub _add_free_sector { my $self = shift; - $self->{dirty_sectors} = {}; + my ($multiple, $offset, $size) = @_; + + my $chains_offset = $multiple * $self->byte_size; + + my $storage = $self->storage; + + # Increment staleness. + # XXX Can this increment+modulo be done by "&= 0x1" ? + my $staleness = unpack( $StP{$STALE_SIZE}, $storage->read_at( $offset + SIG_SIZE, $STALE_SIZE ) ); + $staleness = ($staleness + 1 ) % ( 2 ** ( 8 * $STALE_SIZE ) ); + $storage->print_at( $offset + SIG_SIZE, pack( $StP{$STALE_SIZE}, $staleness ) ); + + my $old_head = $storage->read_at( $self->chains_loc + $chains_offset, $self->byte_size ); + + $storage->print_at( $self->chains_loc + $chains_offset, + pack( $StP{$self->byte_size}, $offset ), + ); + + # Record the old head in the new sector after the signature and staleness counter + $storage->print_at( $offset + SIG_SIZE + $STALE_SIZE, $old_head ); } -sub add_dirty_sector { +sub _request_blist_sector { shift->_request_sector( 0, @_ ) } +sub _request_data_sector { shift->_request_sector( 1, @_ ) } +sub _request_index_sector { shift->_request_sector( 2, @_ ) } + +sub _request_sector { my $self = shift; - my ($offset) = @_; + my ($multiple, $size) = @_; + + my $chains_offset = $multiple * $self->byte_size; + + my $old_head = $self->storage->read_at( $self->chains_loc + $chains_offset, $self->byte_size ); + my $loc = unpack( $StP{$self->byte_size}, $old_head ); + + # We don't have any free sectors of the right size, so allocate a new one. + unless ( $loc ) { + my $offset = $self->storage->request_space( $size ); + + # Zero out the new sector. This also guarantees correct increases + # in the filesize. + $self->storage->print_at( $offset, chr(0) x $size ); - $self->dirty_sectors->{ $offset } = undef; + return $offset; + } + + # Read the new head after the signature and the staleness counter + my $new_head = $self->storage->read_at( $loc + SIG_SIZE + $STALE_SIZE, $self->byte_size ); + $self->storage->print_at( $self->chains_loc + $chains_offset, $new_head ); + $self->storage->print_at( + $loc + SIG_SIZE + $STALE_SIZE, + pack( $StP{$self->byte_size}, 0 ), + ); + + return $loc; } +################################################################################ + sub flush { my $self = shift; - my $sectors = $self->dirty_sectors; - for my $offset (sort { $a <=> $b } keys %{ $sectors }) { - $self->storage->print_at( $offset, $self->sector_cache->{$offset} ); - } +# my $sectors = $self->dirty_sectors; +# for my $offset (sort { $a <=> $b } keys %{ $sectors }) { +# $self->storage->print_at( $offset, $self->sector_cache->{$offset} ); +# } # Why do we need to have the storage flush? Shouldn't autoflush take care of things? # -RobK, 2008-06-26 $self->storage->flush; - $self->clear_dirty_sectors; +# $self->clear_dirty_sectors; - $self->clear_sector_cache; +# $self->clear_sector_cache; } -################################################################################ - sub lock_exclusive { my $self = shift; my ($obj) = @_; @@ -794,12 +945,9 @@ sub clear_cache { %{$_[0]->cache} = () } sub _dump_file { my $self = shift; - $self->flush; # Read the header - my $header_sector = DBM::Deep::Engine::Sector::FileHeader->new({ - engine => $self, - }); + my $spot = $self->_read_file_header(); my %types = ( 0 => 'B', @@ -815,9 +963,6 @@ sub _dump_file { my $return = ""; - # Filesize - $return .= "Size: " . (-s $self->storage->{fh}) . $/; - # Header values $return .= "NumTxns: " . $self->num_txns . $/; @@ -844,7 +989,6 @@ sub _dump_file { $return .= $/; } - my $spot = $header_sector->size; SECTOR: while ( $spot < $self->storage->{end} ) { # Read each sector in order. @@ -903,5 +1047,1322 @@ sub _dump_file { return $return; } +################################################################################ + +package DBM::Deep::Iterator; + +sub new { + my $class = shift; + my ($args) = @_; + + my $self = bless { + breadcrumbs => [], + engine => $args->{engine}, + base_offset => $args->{base_offset}, + }, $class; + + Scalar::Util::weaken( $self->{engine} ); + + return $self; +} + +sub reset { $_[0]{breadcrumbs} = [] } + +sub get_sector_iterator { + my $self = shift; + my ($loc) = @_; + + my $sector = $self->{engine}->_load_sector( $loc ) + or return; + + if ( $sector->isa( 'DBM::Deep::Engine::Sector::Index' ) ) { + return DBM::Deep::Iterator::Index->new({ + iterator => $self, + sector => $sector, + }); + } + elsif ( $sector->isa( 'DBM::Deep::Engine::Sector::BucketList' ) ) { + return DBM::Deep::Iterator::BucketList->new({ + iterator => $self, + sector => $sector, + }); + } + + DBM::Deep->_throw_error( "get_sector_iterator(): Why did $loc make a $sector?" ); +} + +sub get_next_key { + my $self = shift; + my ($obj) = @_; + + my $crumbs = $self->{breadcrumbs}; + my $e = $self->{engine}; + + unless ( @$crumbs ) { + # This will be a Reference sector + my $sector = $e->_load_sector( $self->{base_offset} ) + # If no sector is found, thist must have been deleted from under us. + or return; + + if ( $sector->staleness != $obj->_staleness ) { + return; + } + + my $loc = $sector->get_blist_loc + or return; + + push @$crumbs, $self->get_sector_iterator( $loc ); + } + + FIND_NEXT_KEY: { + # We're at the end. + unless ( @$crumbs ) { + $self->reset; + return; + } + + my $iterator = $crumbs->[-1]; + + # This level is done. + if ( $iterator->at_end ) { + pop @$crumbs; + redo FIND_NEXT_KEY; + } + + if ( $iterator->isa( 'DBM::Deep::Iterator::Index' ) ) { + # If we don't have any more, it will be caught at the + # prior check. + if ( my $next = $iterator->get_next_iterator ) { + push @$crumbs, $next; + } + redo FIND_NEXT_KEY; + } + + unless ( $iterator->isa( 'DBM::Deep::Iterator::BucketList' ) ) { + DBM::Deep->_throw_error( + "Should have a bucketlist iterator here - instead have $iterator" + ); + } + + # At this point, we have a BucketList iterator + my $key = $iterator->get_next_key; + if ( defined $key ) { + return $key; + } + #XXX else { $iterator->set_to_end() } ? + + # We hit the end of the bucketlist iterator, so redo + redo FIND_NEXT_KEY; + } + + DBM::Deep->_throw_error( "get_next_key(): How did we get here?" ); +} + +package DBM::Deep::Iterator::Index; + +sub new { + my $self = bless $_[1] => $_[0]; + $self->{curr_index} = 0; + return $self; +} + +sub at_end { + my $self = shift; + return $self->{curr_index} >= $self->{iterator}{engine}->hash_chars; +} + +sub get_next_iterator { + my $self = shift; + + my $loc; + while ( !$loc ) { + return if $self->at_end; + $loc = $self->{sector}->get_entry( $self->{curr_index}++ ); + } + + return $self->{iterator}->get_sector_iterator( $loc ); +} + +package DBM::Deep::Iterator::BucketList; + +sub new { + my $self = bless $_[1] => $_[0]; + $self->{curr_index} = 0; + return $self; +} + +sub at_end { + my $self = shift; + return $self->{curr_index} >= $self->{iterator}{engine}->max_buckets; +} + +sub get_next_key { + my $self = shift; + + return if $self->at_end; + + my $idx = $self->{curr_index}++; + + my $data_loc = $self->{sector}->get_data_location_for({ + allow_head => 1, + idx => $idx, + }) or return; + + #XXX Do we want to add corruption checks here? + return $self->{sector}->get_key_for( $idx )->data; +} + +package DBM::Deep::Engine::Sector; + +sub new { + my $self = bless $_[1], $_[0]; + Scalar::Util::weaken( $self->{engine} ); + $self->_init; + return $self; +} + +#sub _init {} +#sub clone { DBM::Deep->_throw_error( "Must be implemented in the child class" ); } + +sub engine { $_[0]{engine} } +sub offset { $_[0]{offset} } +sub type { $_[0]{type} } + +sub base_size { + my $self = shift; + return $self->engine->SIG_SIZE + $STALE_SIZE; +} + +sub free { + my $self = shift; + + my $e = $self->engine; + + $e->storage->print_at( $self->offset, $e->SIG_FREE ); + # Skip staleness counter + $e->storage->print_at( $self->offset + $self->base_size, + chr(0) x ($self->size - $self->base_size), + ); + + my $free_meth = $self->free_meth; + $e->$free_meth( $self->offset, $self->size ); + + return; +} + +package DBM::Deep::Engine::Sector::Data; + +our @ISA = qw( DBM::Deep::Engine::Sector ); + +# This is in bytes +sub size { $_[0]{engine}->data_sector_size } +sub free_meth { return '_add_free_data_sector' } + +sub clone { + my $self = shift; + return ref($self)->new({ + engine => $self->engine, + type => $self->type, + data => $self->data, + }); +} + +package DBM::Deep::Engine::Sector::Scalar; + +our @ISA = qw( DBM::Deep::Engine::Sector::Data ); + +sub free { + my $self = shift; + + my $chain_loc = $self->chain_loc; + + $self->SUPER::free(); + + if ( $chain_loc ) { + $self->engine->_load_sector( $chain_loc )->free; + } + + return; +} + +sub type { $_[0]{engine}->SIG_DATA } +sub _init { + my $self = shift; + + my $engine = $self->engine; + + unless ( $self->offset ) { + my $data_section = $self->size - $self->base_size - $engine->byte_size - 1; + + $self->{offset} = $engine->_request_data_sector( $self->size ); + + my $data = delete $self->{data}; + my $dlen = length $data; + my $continue = 1; + my $curr_offset = $self->offset; + while ( $continue ) { + + my $next_offset = 0; + + my ($leftover, $this_len, $chunk); + if ( $dlen > $data_section ) { + $leftover = 0; + $this_len = $data_section; + $chunk = substr( $data, 0, $this_len ); + + $dlen -= $data_section; + $next_offset = $engine->_request_data_sector( $self->size ); + $data = substr( $data, $this_len ); + } + else { + $leftover = $data_section - $dlen; + $this_len = $dlen; + $chunk = $data; + + $continue = 0; + } + + $engine->storage->print_at( $curr_offset, $self->type ); # Sector type + # Skip staleness + $engine->storage->print_at( $curr_offset + $self->base_size, + pack( $StP{$engine->byte_size}, $next_offset ), # Chain loc + pack( $StP{1}, $this_len ), # Data length + $chunk, # Data to be stored in this sector + chr(0) x $leftover, # Zero-fill the rest + ); + + $curr_offset = $next_offset; + } + + return; + } +} + +sub data_length { + my $self = shift; + + my $buffer = $self->engine->storage->read_at( + $self->offset + $self->base_size + $self->engine->byte_size, 1 + ); + + return unpack( $StP{1}, $buffer ); +} + +sub chain_loc { + my $self = shift; + return unpack( + $StP{$self->engine->byte_size}, + $self->engine->storage->read_at( + $self->offset + $self->base_size, + $self->engine->byte_size, + ), + ); +} + +sub data { + my $self = shift; +# my ($args) = @_; +# $args ||= {}; + + my $data; + while ( 1 ) { + my $chain_loc = $self->chain_loc; + + $data .= $self->engine->storage->read_at( + $self->offset + $self->base_size + $self->engine->byte_size + 1, $self->data_length, + ); + + last unless $chain_loc; + + $self = $self->engine->_load_sector( $chain_loc ); + } + + return $data; +} + +package DBM::Deep::Engine::Sector::Null; + +our @ISA = qw( DBM::Deep::Engine::Sector::Data ); + +sub type { $_[0]{engine}->SIG_NULL } +sub data_length { 0 } +sub data { return } + +sub _init { + my $self = shift; + + my $engine = $self->engine; + + unless ( $self->offset ) { + my $leftover = $self->size - $self->base_size - 1 * $engine->byte_size - 1; + + $self->{offset} = $engine->_request_data_sector( $self->size ); + $engine->storage->print_at( $self->offset, $self->type ); # Sector type + # Skip staleness counter + $engine->storage->print_at( $self->offset + $self->base_size, + pack( $StP{$engine->byte_size}, 0 ), # Chain loc + pack( $StP{1}, $self->data_length ), # Data length + chr(0) x $leftover, # Zero-fill the rest + ); + + return; + } +} + +package DBM::Deep::Engine::Sector::Reference; + +our @ISA = qw( DBM::Deep::Engine::Sector::Data ); + +sub _init { + my $self = shift; + + my $e = $self->engine; + + unless ( $self->offset ) { + my $classname = Scalar::Util::blessed( delete $self->{data} ); + my $leftover = $self->size - $self->base_size - 3 * $e->byte_size; + + my $class_offset = 0; + if ( defined $classname ) { + my $class_sector = DBM::Deep::Engine::Sector::Scalar->new({ + engine => $e, + data => $classname, + }); + $class_offset = $class_sector->offset; + } + + $self->{offset} = $e->_request_data_sector( $self->size ); + $e->storage->print_at( $self->offset, $self->type ); # Sector type + # Skip staleness counter + $e->storage->print_at( $self->offset + $self->base_size, + pack( $StP{$e->byte_size}, 0 ), # Index/BList loc + pack( $StP{$e->byte_size}, $class_offset ), # Classname loc + pack( $StP{$e->byte_size}, 1 ), # Initial refcount + chr(0) x $leftover, # Zero-fill the rest + ); + } + else { + $self->{type} = $e->storage->read_at( $self->offset, 1 ); + } + + $self->{staleness} = unpack( + $StP{$STALE_SIZE}, + $e->storage->read_at( $self->offset + $e->SIG_SIZE, $STALE_SIZE ), + ); + + return; +} + +sub staleness { $_[0]{staleness} } + +sub get_data_location_for { + my $self = shift; + my ($args) = @_; + + # Assume that the head is not allowed unless otherwise specified. + $args->{allow_head} = 0 unless exists $args->{allow_head}; + + # Assume we don't create a new blist location unless otherwise specified. + $args->{create} = 0 unless exists $args->{create}; + + my $blist = $self->get_bucket_list({ + key_md5 => $args->{key_md5}, + key => $args->{key}, + create => $args->{create}, + }); + return unless $blist && $blist->{found}; + + # At this point, $blist knows where the md5 is. What it -doesn't- know yet + # is whether or not this transaction has this key. That's part of the next + # function call. + my $location = $blist->get_data_location_for({ + allow_head => $args->{allow_head}, + }) or return; + + return $location; +} + +sub get_data_for { + my $self = shift; + my ($args) = @_; + + my $location = $self->get_data_location_for( $args ) + or return; + + return $self->engine->_load_sector( $location ); +} + +sub write_data { + my $self = shift; + my ($args) = @_; + + my $blist = $self->get_bucket_list({ + key_md5 => $args->{key_md5}, + key => $args->{key}, + create => 1, + }) or DBM::Deep->_throw_error( "How did write_data fail (no blist)?!" ); + + # Handle any transactional bookkeeping. + if ( $self->engine->trans_id ) { + if ( ! $blist->has_md5 ) { + $blist->mark_deleted({ + trans_id => 0, + }); + } + } + else { + my @trans_ids = $self->engine->get_running_txn_ids; + if ( $blist->has_md5 ) { + if ( @trans_ids ) { + my $old_value = $blist->get_data_for; + foreach my $other_trans_id ( @trans_ids ) { + next if $blist->get_data_location_for({ + trans_id => $other_trans_id, + allow_head => 0, + }); + $blist->write_md5({ + trans_id => $other_trans_id, + key => $args->{key}, + key_md5 => $args->{key_md5}, + value => $old_value->clone, + }); + } + } + } + else { + if ( @trans_ids ) { + foreach my $other_trans_id ( @trans_ids ) { + #XXX This doesn't seem to possible to ever happen . . . + next if $blist->get_data_location_for({ trans_id => $other_trans_id, allow_head => 0 }); + $blist->mark_deleted({ + trans_id => $other_trans_id, + }); + } + } + } + } + + #XXX Is this safe to do transactionally? + # Free the place we're about to write to. + if ( $blist->get_data_location_for({ allow_head => 0 }) ) { + $blist->get_data_for({ allow_head => 0 })->free; + } + + $blist->write_md5({ + key => $args->{key}, + key_md5 => $args->{key_md5}, + value => $args->{value}, + }); +} + +sub delete_key { + my $self = shift; + my ($args) = @_; + + # XXX What should happen if this fails? + my $blist = $self->get_bucket_list({ + key_md5 => $args->{key_md5}, + }) or DBM::Deep->_throw_error( "How did delete_key fail (no blist)?!" ); + + # Save the location so that we can free the data + my $location = $blist->get_data_location_for({ + allow_head => 0, + }); + my $old_value = $location && $self->engine->_load_sector( $location ); + + my @trans_ids = $self->engine->get_running_txn_ids; + + # If we're the HEAD and there are running txns, then we need to clone this value to the other + # transactions to preserve Isolation. + if ( $self->engine->trans_id == 0 ) { + if ( @trans_ids ) { + foreach my $other_trans_id ( @trans_ids ) { + next if $blist->get_data_location_for({ trans_id => $other_trans_id, allow_head => 0 }); + $blist->write_md5({ + trans_id => $other_trans_id, + key => $args->{key}, + key_md5 => $args->{key_md5}, + value => $old_value->clone, + }); + } + } + } + + my $data; + if ( @trans_ids ) { + $blist->mark_deleted( $args ); + + if ( $old_value ) { + $data = $old_value->data({ export => 1 }); + $old_value->free; + } + } + else { + $data = $blist->delete_md5( $args ); + } + + return $data; +} + +sub get_blist_loc { + my $self = shift; + + my $e = $self->engine; + my $blist_loc = $e->storage->read_at( $self->offset + $self->base_size, $e->byte_size ); + return unpack( $StP{$e->byte_size}, $blist_loc ); +} + +sub get_bucket_list { + my $self = shift; + my ($args) = @_; + $args ||= {}; + + # XXX Add in check here for recycling? + + my $engine = $self->engine; + + my $blist_loc = $self->get_blist_loc; + + # There's no index or blist yet + unless ( $blist_loc ) { + return unless $args->{create}; + + my $blist = DBM::Deep::Engine::Sector::BucketList->new({ + engine => $engine, + key_md5 => $args->{key_md5}, + }); + + $engine->storage->print_at( $self->offset + $self->base_size, + pack( $StP{$engine->byte_size}, $blist->offset ), + ); + + return $blist; + } + + my $sector = $engine->_load_sector( $blist_loc ) + or DBM::Deep->_throw_error( "Cannot read sector at $blist_loc in get_bucket_list()" ); + my $i = 0; + my $last_sector = undef; + while ( $sector->isa( 'DBM::Deep::Engine::Sector::Index' ) ) { + $blist_loc = $sector->get_entry( ord( substr( $args->{key_md5}, $i++, 1 ) ) ); + $last_sector = $sector; + if ( $blist_loc ) { + $sector = $engine->_load_sector( $blist_loc ) + or DBM::Deep->_throw_error( "Cannot read sector at $blist_loc in get_bucket_list()" ); + } + else { + $sector = undef; + last; + } + } + + # This means we went through the Index sector(s) and found an empty slot + unless ( $sector ) { + return unless $args->{create}; + + DBM::Deep->_throw_error( "No last_sector when attempting to build a new entry" ) + unless $last_sector; + + my $blist = DBM::Deep::Engine::Sector::BucketList->new({ + engine => $engine, + key_md5 => $args->{key_md5}, + }); + + $last_sector->set_entry( ord( substr( $args->{key_md5}, $i - 1, 1 ) ) => $blist->offset ); + + return $blist; + } + + $sector->find_md5( $args->{key_md5} ); + + # See whether or not we need to reindex the bucketlist + # Yes, the double-braces are there for a reason. if() doesn't create a redo-able block, + # so we have to create a bare block within the if() for redo-purposes. Patch and idea + # submitted by sprout@cpan.org. -RobK, 2008-01-09 + if ( !$sector->has_md5 && $args->{create} && $sector->{idx} == -1 ) {{ + my $redo; + + my $new_index = DBM::Deep::Engine::Sector::Index->new({ + engine => $engine, + }); + + my %blist_cache; + #XXX q.v. the comments for this function. + foreach my $entry ( $sector->chopped_up ) { + my ($spot, $md5) = @{$entry}; + my $idx = ord( substr( $md5, $i, 1 ) ); + + # XXX This is inefficient + my $blist = $blist_cache{$idx} + ||= DBM::Deep::Engine::Sector::BucketList->new({ + engine => $engine, + }); + + $new_index->set_entry( $idx => $blist->offset ); + + my $new_spot = $blist->write_at_next_open( $md5 ); + $engine->reindex_entry( $spot => $new_spot ); + } + + # Handle the new item separately. + { + my $idx = ord( substr( $args->{key_md5}, $i, 1 ) ); + + # If all the previous blist's items have been thrown into one + # blist and the new item belongs in there too, we need + # another index. + if ( keys %blist_cache == 1 and each %blist_cache == $idx ) { + ++$i, ++$redo; + } else { + my $blist = $blist_cache{$idx} + ||= DBM::Deep::Engine::Sector::BucketList->new({ + engine => $engine, + }); + + $new_index->set_entry( $idx => $blist->offset ); + + #XXX THIS IS HACKY! + $blist->find_md5( $args->{key_md5} ); + $blist->write_md5({ + key => $args->{key}, + key_md5 => $args->{key_md5}, + value => DBM::Deep::Engine::Sector::Null->new({ + engine => $engine, + data => undef, + }), + }); + } +# my $blist = $blist_cache{$idx} +# ||= DBM::Deep::Engine::Sector::BucketList->new({ +# engine => $engine, +# }); +# +# $new_index->set_entry( $idx => $blist->offset ); +# +# #XXX THIS IS HACKY! +# $blist->find_md5( $args->{key_md5} ); +# $blist->write_md5({ +# key => $args->{key}, +# key_md5 => $args->{key_md5}, +# value => DBM::Deep::Engine::Sector::Null->new({ +# engine => $engine, +# data => undef, +# }), +# }); + } + + if ( $last_sector ) { + $last_sector->set_entry( + ord( substr( $args->{key_md5}, $i - 1, 1 ) ), + $new_index->offset, + ); + } else { + $engine->storage->print_at( $self->offset + $self->base_size, + pack( $StP{$engine->byte_size}, $new_index->offset ), + ); + } + + $sector->clear; + $sector->free; + + if ( $redo ) { + (undef, $sector) = %blist_cache; + $last_sector = $new_index; + redo; + } + + $sector = $blist_cache{ ord( substr( $args->{key_md5}, $i, 1 ) ) }; + $sector->find_md5( $args->{key_md5} ); + }} + + return $sector; +} + +sub get_class_offset { + my $self = shift; + + my $e = $self->engine; + return unpack( + $StP{$e->byte_size}, + $e->storage->read_at( + $self->offset + $self->base_size + 1 * $e->byte_size, $e->byte_size, + ), + ); +} + +sub get_classname { + my $self = shift; + + my $class_offset = $self->get_class_offset; + + return unless $class_offset; + + return $self->engine->_load_sector( $class_offset )->data; +} + +sub data { + my $self = shift; + my ($args) = @_; + $args ||= {}; + + my $obj; + unless ( $obj = $self->engine->cache->{ $self->offset } ) { + $obj = DBM::Deep->new({ + type => $self->type, + base_offset => $self->offset, + staleness => $self->staleness, + storage => $self->engine->storage, + engine => $self->engine, + }); + + if ( $self->engine->storage->{autobless} ) { + my $classname = $self->get_classname; + if ( defined $classname ) { + bless $obj, $classname; + } + } + + $self->engine->cache->{$self->offset} = $obj; + } + + # We're not exporting, so just return. + unless ( $args->{export} ) { + return $obj; + } + + # We shouldn't export if this is still referred to. + if ( $self->get_refcount > 1 ) { + return $obj; + } + + return $obj->export; +} + +sub free { + my $self = shift; + + # We're not ready to be removed yet. + if ( $self->decrement_refcount > 0 ) { + return; + } + + # Rebless the object into DBM::Deep::Null. + eval { %{ $self->engine->cache->{ $self->offset } } = (); }; + eval { @{ $self->engine->cache->{ $self->offset } } = (); }; + bless $self->engine->cache->{ $self->offset }, 'DBM::Deep::Null'; + delete $self->engine->cache->{ $self->offset }; + + my $blist_loc = $self->get_blist_loc; + $self->engine->_load_sector( $blist_loc )->free if $blist_loc; + + my $class_loc = $self->get_class_offset; + $self->engine->_load_sector( $class_loc )->free if $class_loc; + + $self->SUPER::free(); +} + +sub increment_refcount { + my $self = shift; + + my $refcount = $self->get_refcount; + + $refcount++; + + $self->write_refcount( $refcount ); + + return $refcount; +} + +sub decrement_refcount { + my $self = shift; + + my $refcount = $self->get_refcount; + + $refcount--; + + $self->write_refcount( $refcount ); + + return $refcount; +} + +sub get_refcount { + my $self = shift; + + my $e = $self->engine; + return unpack( + $StP{$e->byte_size}, + $e->storage->read_at( + $self->offset + $self->base_size + 2 * $e->byte_size, $e->byte_size, + ), + ); +} + +sub write_refcount { + my $self = shift; + my ($num) = @_; + + my $e = $self->engine; + $e->storage->print_at( + $self->offset + $self->base_size + 2 * $e->byte_size, + pack( $StP{$e->byte_size}, $num ), + ); +} + +package DBM::Deep::Engine::Sector::BucketList; + +our @ISA = qw( DBM::Deep::Engine::Sector ); + +sub _init { + my $self = shift; + + my $engine = $self->engine; + + unless ( $self->offset ) { + my $leftover = $self->size - $self->base_size; + + $self->{offset} = $engine->_request_blist_sector( $self->size ); + $engine->storage->print_at( $self->offset, $engine->SIG_BLIST ); # Sector type + # Skip staleness counter + $engine->storage->print_at( $self->offset + $self->base_size, + chr(0) x $leftover, # Zero-fill the data + ); + } + + if ( $self->{key_md5} ) { + $self->find_md5; + } + + return $self; +} + +sub clear { + my $self = shift; + $self->engine->storage->print_at( $self->offset + $self->base_size, + chr(0) x ($self->size - $self->base_size), # Zero-fill the data + ); +} + +sub size { + my $self = shift; + unless ( $self->{size} ) { + my $e = $self->engine; + # Base + numbuckets * bucketsize + $self->{size} = $self->base_size + $e->max_buckets * $self->bucket_size; + } + return $self->{size}; +} + +sub free_meth { return '_add_free_blist_sector' } + +sub free { + my $self = shift; + + my $e = $self->engine; + foreach my $bucket ( $self->chopped_up ) { + my $rest = $bucket->[-1]; + + # Delete the keysector + my $l = unpack( $StP{$e->byte_size}, substr( $rest, $e->hash_size, $e->byte_size ) ); + my $s = $e->_load_sector( $l ); $s->free if $s; + + # Delete the HEAD sector + $l = unpack( $StP{$e->byte_size}, + substr( $rest, + $e->hash_size + $e->byte_size, + $e->byte_size, + ), + ); + $s = $e->_load_sector( $l ); $s->free if $s; + + foreach my $txn ( 0 .. $e->num_txns - 2 ) { + my $l = unpack( $StP{$e->byte_size}, + substr( $rest, + $e->hash_size + 2 * $e->byte_size + $txn * ($e->byte_size + $STALE_SIZE), + $e->byte_size, + ), + ); + my $s = $e->_load_sector( $l ); $s->free if $s; + } + } + + $self->SUPER::free(); +} + +sub bucket_size { + my $self = shift; + unless ( $self->{bucket_size} ) { + my $e = $self->engine; + # Key + head (location) + transactions (location + staleness-counter) + my $location_size = $e->byte_size + $e->byte_size + ($e->num_txns - 1) * ($e->byte_size + $STALE_SIZE); + $self->{bucket_size} = $e->hash_size + $location_size; + } + return $self->{bucket_size}; +} + +# XXX This is such a poor hack. I need to rethink this code. +sub chopped_up { + my $self = shift; + + my $e = $self->engine; + + my @buckets; + foreach my $idx ( 0 .. $e->max_buckets - 1 ) { + my $spot = $self->offset + $self->base_size + $idx * $self->bucket_size; + my $md5 = $e->storage->read_at( $spot, $e->hash_size ); + + #XXX If we're chopping, why would we ever have the blank_md5? + last if $md5 eq $e->blank_md5; + + my $rest = $e->storage->read_at( undef, $self->bucket_size - $e->hash_size ); + push @buckets, [ $spot, $md5 . $rest ]; + } + + return @buckets; +} + +sub write_at_next_open { + my $self = shift; + my ($entry) = @_; + + #XXX This is such a hack! + $self->{_next_open} = 0 unless exists $self->{_next_open}; + + my $spot = $self->offset + $self->base_size + $self->{_next_open}++ * $self->bucket_size; + $self->engine->storage->print_at( $spot, $entry ); + + return $spot; +} + +sub has_md5 { + my $self = shift; + unless ( exists $self->{found} ) { + $self->find_md5; + } + return $self->{found}; +} + +sub find_md5 { + my $self = shift; + + $self->{found} = undef; + $self->{idx} = -1; + + if ( @_ ) { + $self->{key_md5} = shift; + } + + # If we don't have an MD5, then what are we supposed to do? + unless ( exists $self->{key_md5} ) { + DBM::Deep->_throw_error( "Cannot find_md5 without a key_md5 set" ); + } + + my $e = $self->engine; + foreach my $idx ( 0 .. $e->max_buckets - 1 ) { + my $potential = $e->storage->read_at( + $self->offset + $self->base_size + $idx * $self->bucket_size, $e->hash_size, + ); + + if ( $potential eq $e->blank_md5 ) { + $self->{idx} = $idx; + return; + } + + if ( $potential eq $self->{key_md5} ) { + $self->{found} = 1; + $self->{idx} = $idx; + return; + } + } + + return; +} + +sub write_md5 { + my $self = shift; + my ($args) = @_; + + DBM::Deep->_throw_error( "write_md5: no key" ) unless exists $args->{key}; + DBM::Deep->_throw_error( "write_md5: no key_md5" ) unless exists $args->{key_md5}; + DBM::Deep->_throw_error( "write_md5: no value" ) unless exists $args->{value}; + + my $engine = $self->engine; + + $args->{trans_id} = $engine->trans_id unless exists $args->{trans_id}; + + my $spot = $self->offset + $self->base_size + $self->{idx} * $self->bucket_size; + $engine->add_entry( $args->{trans_id}, $spot ); + + unless ($self->{found}) { + my $key_sector = DBM::Deep::Engine::Sector::Scalar->new({ + engine => $engine, + data => $args->{key}, + }); + + $engine->storage->print_at( $spot, + $args->{key_md5}, + pack( $StP{$engine->byte_size}, $key_sector->offset ), + ); + } + + my $loc = $spot + + $engine->hash_size + + $engine->byte_size; + + if ( $args->{trans_id} ) { + $loc += $engine->byte_size + ($args->{trans_id} - 1) * ( $engine->byte_size + $STALE_SIZE ); + + $engine->storage->print_at( $loc, + pack( $StP{$engine->byte_size}, $args->{value}->offset ), + pack( $StP{$STALE_SIZE}, $engine->get_txn_staleness_counter( $args->{trans_id} ) ), + ); + } + else { + $engine->storage->print_at( $loc, + pack( $StP{$engine->byte_size}, $args->{value}->offset ), + ); + } +} + +sub mark_deleted { + my $self = shift; + my ($args) = @_; + $args ||= {}; + + my $engine = $self->engine; + + $args->{trans_id} = $engine->trans_id unless exists $args->{trans_id}; + + my $spot = $self->offset + $self->base_size + $self->{idx} * $self->bucket_size; + $engine->add_entry( $args->{trans_id}, $spot ); + + my $loc = $spot + + $engine->hash_size + + $engine->byte_size; + + if ( $args->{trans_id} ) { + $loc += $engine->byte_size + ($args->{trans_id} - 1) * ( $engine->byte_size + $STALE_SIZE ); + + $engine->storage->print_at( $loc, + pack( $StP{$engine->byte_size}, 1 ), # 1 is the marker for deleted + pack( $StP{$STALE_SIZE}, $engine->get_txn_staleness_counter( $args->{trans_id} ) ), + ); + } + else { + $engine->storage->print_at( $loc, + pack( $StP{$engine->byte_size}, 1 ), # 1 is the marker for deleted + ); + } + +} + +sub delete_md5 { + my $self = shift; + my ($args) = @_; + + my $engine = $self->engine; + return undef unless $self->{found}; + + # Save the location so that we can free the data + my $location = $self->get_data_location_for({ + allow_head => 0, + }); + my $key_sector = $self->get_key_for; + + my $spot = $self->offset + $self->base_size + $self->{idx} * $self->bucket_size; + $engine->storage->print_at( $spot, + $engine->storage->read_at( + $spot + $self->bucket_size, + $self->bucket_size * ( $engine->max_buckets - $self->{idx} - 1 ), + ), + chr(0) x $self->bucket_size, + ); + + $key_sector->free; + + my $data_sector = $self->engine->_load_sector( $location ); + my $data = $data_sector->data({ export => 1 }); + $data_sector->free; + + return $data; +} + +sub get_data_location_for { + my $self = shift; + my ($args) = @_; + $args ||= {}; + + $args->{allow_head} = 0 unless exists $args->{allow_head}; + $args->{trans_id} = $self->engine->trans_id unless exists $args->{trans_id}; + $args->{idx} = $self->{idx} unless exists $args->{idx}; + + my $e = $self->engine; + + my $spot = $self->offset + $self->base_size + + $args->{idx} * $self->bucket_size + + $e->hash_size + + $e->byte_size; + + if ( $args->{trans_id} ) { + $spot += $e->byte_size + ($args->{trans_id} - 1) * ( $e->byte_size + $STALE_SIZE ); + } + + my $buffer = $e->storage->read_at( + $spot, + $e->byte_size + $STALE_SIZE, + ); + my ($loc, $staleness) = unpack( $StP{$e->byte_size} . ' ' . $StP{$STALE_SIZE}, $buffer ); + + # XXX Merge the two if-clauses below + if ( $args->{trans_id} ) { + # We have found an entry that is old, so get rid of it + if ( $staleness != (my $s = $e->get_txn_staleness_counter( $args->{trans_id} ) ) ) { + $e->storage->print_at( + $spot, + pack( $StP{$e->byte_size} . ' ' . $StP{$STALE_SIZE}, (0) x 2 ), + ); + $loc = 0; + } + } + + # If we're in a transaction and we never wrote to this location, try the + # HEAD instead. + if ( $args->{trans_id} && !$loc && $args->{allow_head} ) { + return $self->get_data_location_for({ + trans_id => 0, + allow_head => 1, + idx => $args->{idx}, + }); + } + + return $loc <= 1 ? 0 : $loc; +} + +sub get_data_for { + my $self = shift; + my ($args) = @_; + $args ||= {}; + + return unless $self->{found}; + my $location = $self->get_data_location_for({ + allow_head => $args->{allow_head}, + }); + return $self->engine->_load_sector( $location ); +} + +sub get_key_for { + my $self = shift; + my ($idx) = @_; + $idx = $self->{idx} unless defined $idx; + + if ( $idx >= $self->engine->max_buckets ) { + DBM::Deep->_throw_error( "get_key_for(): Attempting to retrieve $idx" ); + } + + my $location = $self->engine->storage->read_at( + $self->offset + $self->base_size + $idx * $self->bucket_size + $self->engine->hash_size, + $self->engine->byte_size, + ); + $location = unpack( $StP{$self->engine->byte_size}, $location ); + DBM::Deep->_throw_error( "get_key_for: No location?" ) unless $location; + + return $self->engine->_load_sector( $location ); +} + +package DBM::Deep::Engine::Sector::Index; + +our @ISA = qw( DBM::Deep::Engine::Sector ); + +sub _init { + my $self = shift; + + my $engine = $self->engine; + + unless ( $self->offset ) { + my $leftover = $self->size - $self->base_size; + + $self->{offset} = $engine->_request_index_sector( $self->size ); + $engine->storage->print_at( $self->offset, $engine->SIG_INDEX ); # Sector type + # Skip staleness counter + $engine->storage->print_at( $self->offset + $self->base_size, + chr(0) x $leftover, # Zero-fill the rest + ); + } + + return $self; +} + +#XXX Change here +sub size { + my $self = shift; + unless ( $self->{size} ) { + my $e = $self->engine; + $self->{size} = $self->base_size + $e->byte_size * $e->hash_chars; + } + return $self->{size}; +} + +sub free_meth { return '_add_free_index_sector' } + +sub free { + my $self = shift; + my $e = $self->engine; + + for my $i ( 0 .. $e->hash_chars - 1 ) { + my $l = $self->get_entry( $i ) or next; + $e->_load_sector( $l )->free; + } + + $self->SUPER::free(); +} + +sub _loc_for { + my $self = shift; + my ($idx) = @_; + return $self->offset + $self->base_size + $idx * $self->engine->byte_size; +} + +sub get_entry { + my $self = shift; + my ($idx) = @_; + + my $e = $self->engine; + + DBM::Deep->_throw_error( "get_entry: Out of range ($idx)" ) + if $idx < 0 || $idx >= $e->hash_chars; + + return unpack( + $StP{$e->byte_size}, + $e->storage->read_at( $self->_loc_for( $idx ), $e->byte_size ), + ); +} + +sub set_entry { + my $self = shift; + my ($idx, $loc) = @_; + + my $e = $self->engine; + + DBM::Deep->_throw_error( "set_entry: Out of range ($idx)" ) + if $idx < 0 || $idx >= $e->hash_chars; + + $self->engine->storage->print_at( + $self->_loc_for( $idx ), + pack( $StP{$e->byte_size}, $loc ), + ); +} + +# This was copied from MARCEL's Class::Null. However, I couldn't use it because +# I need an undef value, not an implementation of the Null Class pattern. +package DBM::Deep::Null; + +use overload + 'bool' => sub { undef }, + '""' => sub { undef }, + '0+' => sub { undef }, + fallback => 1, + nomethod => 'AUTOLOAD'; + +sub AUTOLOAD { return; } + 1; __END__ diff --git a/lib/DBM/Deep/Engine/Sector/BucketList.pm b/lib/DBM/Deep/Engine/Sector/BucketList.pm index 95215b0..73d1e0b 100644 --- a/lib/DBM/Deep/Engine/Sector/BucketList.pm +++ b/lib/DBM/Deep/Engine/Sector/BucketList.pm @@ -68,7 +68,7 @@ sub free { $e->byte_size, ), ); - $s = $e->_load_sector( $l ); $s->free if $s; + $s = $e->_load_sector( $l ); $s->free if $s; foreach my $txn ( 0 .. $e->num_txns - 2 ) { my $l = unpack( $e->StP($e->byte_size), @@ -352,7 +352,7 @@ sub get_key_for { $idx = $self->{idx} unless defined $idx; if ( $idx >= $self->engine->max_buckets ) { - DBM::Deep->_throw_error( "get_key_for(): Attempting to retrieve $idx" ); + DBM::Deep->_throw_error( "get_key_for(): Attempting to retrieve $idx beyond max_buckets" ); } my $location = $self->read( @@ -371,11 +371,14 @@ sub rollback { my $e = $self->engine; my $trans_id = $e->trans_id; +# warn "Rolling back $idx ($trans_id)\n"; + my $base = $self->base_size + ($idx * $self->bucket_size) + $e->hash_size + $e->byte_size; my $spot = $base + $e->byte_size + ($trans_id - 1) * ( $e->byte_size + $DBM::Deep::Engine::STALE_SIZE ); my $trans_loc = $self->read( $spot, $e->byte_size ); $trans_loc = unpack( $e->StP($e->byte_size), $trans_loc ); +# warn "$trans_loc\n"; $self->write( $spot, pack( $e->StP($e->byte_size), 0 ) ); diff --git a/lib/DBM/Deep/Engine/Sector/Reference.pm b/lib/DBM/Deep/Engine/Sector/Reference.pm index 0c5e215..de102c5 100644 --- a/lib/DBM/Deep/Engine/Sector/Reference.pm +++ b/lib/DBM/Deep/Engine/Sector/Reference.pm @@ -454,11 +454,11 @@ sub free { bless $self->engine->cache->{ $self->offset }, 'DBM::Deep::Null'; delete $self->engine->cache->{ $self->offset }; - my $blist_loc = $self->get_blist_loc; - $self->engine->_load_sector( $blist_loc )->free if $blist_loc; - - my $class_loc = $self->get_class_offset; - $self->engine->_load_sector( $class_loc )->free if $class_loc; + foreach my $meth ( qw( get_blist_loc get_class_offset ) ) { + my $l = $self->$meth; + my $s = $self->engine->_load_sector( $l ); + $s->free if $s; + } $self->SUPER::free(); } diff --git a/lib/DBM/Deep/Hash.pm b/lib/DBM/Deep/Hash.pm index e1d09d3..50dd19d 100644 --- a/lib/DBM/Deep/Hash.pm +++ b/lib/DBM/Deep/Hash.pm @@ -72,13 +72,11 @@ sub FIRSTKEY { ## my $self = shift->_get_self; - warn "HASH:FIRSTKEY($self)\n" if DBM::Deep::DEBUG; - $self->lock_shared; my $result = $self->_engine->get_next_key( $self ); - $self->unlock(); + $self->unlock; return ($result && $self->_engine->storage->{filter_fetch_key}) ? $self->_engine->storage->{filter_fetch_key}->($result) @@ -95,14 +93,12 @@ sub NEXTKEY { ? $self->_engine->storage->{filter_store_key}->($_[0]) : $_[0]; - warn "HASH:NEXTKEY($self,$prev_key)\n" if DBM::Deep::DEBUG; - $self->lock_shared; my $result = $self->_engine->get_next_key( $self, $prev_key ); - $self->unlock(); - + $self->unlock; + return ($result && $self->_engine->storage->{filter_fetch_key}) ? $self->_engine->storage->{filter_fetch_key}->($result) : $result; diff --git a/lib/DBM/Deep/Iterator.pm b/lib/DBM/Deep/Iterator.pm index 6de0e05..bd5905f 100644 --- a/lib/DBM/Deep/Iterator.pm +++ b/lib/DBM/Deep/Iterator.pm @@ -57,6 +57,7 @@ sub get_next_key { my $crumbs = $self->{breadcrumbs}; my $e = $self->{engine}; + warn "1\n"; unless ( @$crumbs ) { # This will be a Reference sector my $sector = $e->_load_sector( $self->{base_offset} ) @@ -73,6 +74,7 @@ sub get_next_key { push @$crumbs, $self->get_sector_iterator( $loc ); } + warn "2: " . $obj->_dump_file; FIND_NEXT_KEY: { # We're at the end. unless ( @$crumbs ) { diff --git a/t/01_basic.t b/t/01_basic.t index 4b9208e..5798da4 100644 --- a/t/01_basic.t +++ b/t/01_basic.t @@ -2,7 +2,7 @@ # DBM::Deep Test ## use strict; -use Test::More tests => 4; +use Test::More tests => 3; use t::common qw( new_fh ); @@ -27,4 +27,3 @@ isa_ok( $db, 'DBM::Deep' ); ok(1, "We can successfully open a file!" ); $db->{foo} = 'bar'; -is( $db->{foo}, 'bar' ); diff --git a/t/02_hash.t b/t/02_hash.t index ab428e5..6e9972a 100644 --- a/t/02_hash.t +++ b/t/02_hash.t @@ -45,7 +45,6 @@ is( $db->{key4}, undef, "Autovivified key4" ); ok( exists $db->{key4}, "Autovivified key4 now exists" ); delete $db->{key4}; - ok( !exists $db->{key4}, "And key4 doesn't exists anymore" ); # Keys will be done via an iterator that keeps a breadcrumb trail of the last @@ -181,3 +180,4 @@ throws_ok { throws_ok { $db->exists(undef); } qr/Cannot use an undefined hash key/, "EXISTS fails on an undefined key"; + diff --git a/t/03_bighash.t b/t/03_bighash.t index 6e6ccee..b362c0f 100644 --- a/t/03_bighash.t +++ b/t/03_bighash.t @@ -12,8 +12,6 @@ use t::common qw( new_fh ); plan tests => 9; -my $locked = 0; - use_ok( 'DBM::Deep' ); diag "This test can take up to a minute to run. Please be patient."; @@ -24,8 +22,6 @@ my $db = DBM::Deep->new( type => DBM::Deep->TYPE_HASH, ); -$db->lock_exclusive if $locked; - $db->{foo} = {}; my $foo = $db->{foo}; @@ -34,11 +30,9 @@ my $foo = $db->{foo}; ## my $max_keys = 4000; -warn localtime(time) . ": before put\n"; for ( 0 .. $max_keys ) { $foo->put( "hello $_" => "there " . $_ * 2 ); } -warn localtime(time) . ": after put\n"; my $count = -1; for ( 0 .. $max_keys ) { @@ -48,23 +42,16 @@ for ( 0 .. $max_keys ) { }; } is( $count, $max_keys, "We read $count keys" ); -warn localtime(time) . ": after read\n"; my @keys = sort keys %$foo; -warn localtime(time) . ": after keys\n"; cmp_ok( scalar(@keys), '==', $max_keys + 1, "Number of keys is correct" ); my @control = sort map { "hello $_" } 0 .. $max_keys; cmp_deeply( \@keys, \@control, "Correct keys are there" ); -warn localtime(time) . ": before exists\n"; ok( !exists $foo->{does_not_exist}, "EXISTS works on large hashes for non-existent keys" ); is( $foo->{does_not_exist}, undef, "autovivification works on large hashes" ); ok( exists $foo->{does_not_exist}, "EXISTS works on large hashes for newly-existent keys" ); cmp_ok( scalar(keys %$foo), '==', $max_keys + 2, "Number of keys after autovivify is correct" ); -warn localtime(time) . ": before clear\n"; $db->clear; -warn localtime(time) . ": after clear\n"; cmp_ok( scalar(keys %$db), '==', 0, "Number of keys after clear() is correct" ); - -$db->unlock if $locked; diff --git a/t/04_array.t b/t/04_array.t index 3eea452..24b52ec 100644 --- a/t/04_array.t +++ b/t/04_array.t @@ -63,8 +63,7 @@ is( $db->fetch(4), 'elem4.1' ); throws_ok { $db->[-6] = 'whoops!'; -} qr/Modification of non-creatable array value attempted, subscript -6/, - "Correct error thrown when attempting to modify a non-creatable array value"; +} qr/Modification of non-creatable array value attempted, subscript -6/, "Correct error thrown"; my $popped = $db->pop; is( $db->length, 4, "... and we have four after popping" ); diff --git a/t/27_filehandle.t b/t/27_filehandle.t index 006f26a..aff3007 100644 --- a/t/27_filehandle.t +++ b/t/27_filehandle.t @@ -98,7 +98,7 @@ locking => 0, } qr/Cannot write to a readonly filehandle/, "Can't write to a read-only filehandle"; ok( !$db->exists( 'foo' ), "foo still doesn't exist" ); - is( $db->{x}, 'b', "x is still 'b'" ); + is( $db->{x}, 'b' ); } exec( "$^X -Iblib/lib $filename" ); diff --git a/t/41_transaction_multilevel.t b/t/41_transaction_multilevel.t index d125582..2c3c44a 100644 --- a/t/41_transaction_multilevel.t +++ b/t/41_transaction_multilevel.t @@ -27,7 +27,6 @@ $db1->{x} = { xy => { foo => 'y' } }; is( $db1->{x}{xy}{foo}, 'y', "Before transaction, DB1's X is Y" ); is( $db2->{x}{xy}{foo}, 'y', "Before transaction, DB2's X is Y" ); -#warn $db1->_dump_file; $db1->begin_work; cmp_bag( [ keys %$db1 ], [qw( x )], "DB1 keys correct" ); @@ -49,9 +48,8 @@ $db1->begin_work; cmp_bag( [ keys %{$db1->{x}} ], [qw( yz )], "DB1->X keys correct" ); cmp_bag( [ keys %{$db2->{x}} ], [qw( xy )], "DB2->X keys correct" ); -#warn $db1->_dump_file; $db1->rollback; -__END__ + cmp_bag( [ keys %$db1 ], [qw( x )], "DB1 keys correct" ); cmp_bag( [ keys %$db2 ], [qw( x )], "DB2 keys correct" ); diff --git a/t/43_transaction_maximum.t b/t/43_transaction_maximum.t index d861010..e8462b3 100644 --- a/t/43_transaction_maximum.t +++ b/t/43_transaction_maximum.t @@ -6,20 +6,17 @@ use t::common qw( new_fh ); use DBM::Deep; -my $max_txns = 250; +my $max_txns = 255; my ($fh, $filename) = new_fh(); my @dbs = grep { $_ } map { - my $x = eval { DBM::Deep->new( - file => $filename, - num_txns => $max_txns, + file => $filename, + num_txns => $max_txns, ); }; - die $@ if $@; - $x; } 1 .. $max_txns; my $num = $#dbs; diff --git a/t/44_upgrade_db.t b/t/44_upgrade_db.t index 335a62e..4d943d5 100644 --- a/t/44_upgrade_db.t +++ b/t/44_upgrade_db.t @@ -11,9 +11,9 @@ BEGIN { if ( $^O =~ /bsd/i ); my @failures; - eval " use Pod::Usage 1.3;"; push @failures, 'Pod::Usage' if $@; - eval " use IO::Scalar;"; push @failures, 'IO::Scalar' if $@; - eval " use FileHandle::Fmode;"; push @failures, 'FileHandle::Fmode' if $@; + eval "use Pod::Usage 1.3;"; push @failures, 'Pod::Usage' if $@; + eval "use IO::Scalar;"; push @failures, 'IO::Scalar' if $@; + eval "use FileHandle::Fmode;"; push @failures, 'FileHandle::Fmode' if $@; if ( @failures ) { my $missing = join ',', @failures; plan skip_all => "'$missing' must be installed to run these tests"; diff --git a/t/97_dump_file.t b/t/97_dump_file.t index fec9980..1445517 100644 --- a/t/97_dump_file.t +++ b/t/97_dump_file.t @@ -11,7 +11,6 @@ my $db = DBM::Deep->new( ); is( $db->_dump_file, <<"__END_DUMP__", "Dump of initial file correct" ); -Size: 94 NumTxns: 1 Chains(B): Chains(D): @@ -22,7 +21,6 @@ __END_DUMP__ $db->{foo} = 'bar'; is( $db->_dump_file, <<"__END_DUMP__", "Dump of initial file correct" ); -Size: 609 NumTxns: 1 Chains(B): Chains(D): diff --git a/utils/upgrade_db.pl b/utils/upgrade_db.pl index b1162cc..91003c3 100755 --- a/utils/upgrade_db.pl +++ b/utils/upgrade_db.pl @@ -28,7 +28,7 @@ my %is_dev = ( my %opts = ( man => 0, help => 0, - version => '1.0014', + version => '1.0013', autobless => 1, ); GetOptions( \%opts, @@ -71,7 +71,10 @@ my %db; { my $ver = $opts{version}; - if ( $ver =~ /^1\.001[0-4]/ || $ver =~ /^1\.000[3-9]/) { + if ( $ver =~ /^1\.001[0-3]/) { + $ver = 3; + } + elsif ( $ver =~ /^1\.000[3-9]/) { $ver = 3; } elsif ( $ver =~ /^1\.000?[0-2]?/) {