From: rkinyon Date: Fri, 28 Apr 2006 00:34:29 +0000 (+0000) Subject: Tagged 0.99_01 X-Git-Tag: 0-99_01^0 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=15d754ddb85bbacab5748d2fc02f21b5debd48c3;p=dbsrgits%2FDBM-Deep.git Tagged 0.99_01 --- diff --git a/MANIFEST b/MANIFEST index 4d5457e..b049317 100644 --- a/MANIFEST +++ b/MANIFEST @@ -36,10 +36,11 @@ t/24_autobless.t t/25_tie_return_value.t t/26_scalar_ref.t t/27_filehandle.t -t/28_transactions.t +t/28_audit_trail.t t/29_freespace_manager.t t/30_already_tied.t t/31_references.t t/32_dash_ell.t -t/33_audit_trail.t +t/33_transactions.t t/34_transaction_arrays.t +t/35_transaction_multiple.t diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP index 96426fe..b008153 100644 --- a/MANIFEST.SKIP +++ b/MANIFEST.SKIP @@ -15,3 +15,5 @@ cover_db \.old$ ^#.*#$ ^\.# +^\.DS_Store +^__MACOSX diff --git a/lib/DBM/Deep.pm b/lib/DBM/Deep.pm index 75aee67..395aefd 100644 --- a/lib/DBM/Deep.pm +++ b/lib/DBM/Deep.pm @@ -129,7 +129,7 @@ sub _init { $self->{$param} = $args->{$param}; } - $self->{engine}->setup_fh( $self ); + $self->_engine->setup_fh( $self ); $self->{fileobj}->set_db( $self ); @@ -277,7 +277,7 @@ sub optimize { $self->unlock(); $self->_fileobj->close; $self->_fileobj->open; - $self->{engine}->setup_fh( $self ); + $self->_engine->setup_fh( $self ); return 1; } @@ -342,6 +342,11 @@ sub commit { # Accessor methods ## +sub _engine { + my $self = $_[0]->_get_self; + return $self->{engine}; +} + sub _fileobj { my $self = $_[0]->_get_self; return $self->{fileobj}; @@ -464,9 +469,9 @@ sub STORE { ## $self->lock( LOCK_EX ); - my $md5 = $self->{engine}{digest}->($key); + my $md5 = $self->_engine->{digest}->($key); - my $tag = $self->{engine}->find_bucket_list( $self->_base_offset, $md5, { create => 1 } ); + my $tag = $self->_engine->find_blist( $self->_base_offset, $md5, { create => 1 } ); # User may be storing a hash, in which case we do not want it run # through the filtering system @@ -477,7 +482,7 @@ sub STORE { ## # Add key/value to bucket list ## - $self->{engine}->add_bucket( $tag, $md5, $key, $value, undef, $orig_key ); + $self->_engine->add_bucket( $tag, $md5, $key, $value, undef, $orig_key ); $self->unlock(); @@ -491,14 +496,14 @@ sub FETCH { my $self = shift->_get_self; my ($key, $orig_key) = @_; - my $md5 = $self->{engine}{digest}->($key); + my $md5 = $self->_engine->{digest}->($key); ## # Request shared lock for reading ## $self->lock( LOCK_SH ); - my $tag = $self->{engine}->find_bucket_list( $self->_base_offset, $md5 );#, { create => 1 } ); + my $tag = $self->_engine->find_blist( $self->_base_offset, $md5 );#, { create => 1 } ); #XXX This needs to autovivify if (!$tag) { $self->unlock(); @@ -508,7 +513,7 @@ sub FETCH { ## # Get value from bucket list ## - my $result = $self->{engine}->get_bucket_value( $tag, $md5, $orig_key ); + my $result = $self->_engine->get_bucket_value( $tag, $md5, $orig_key ); $self->unlock(); @@ -545,9 +550,9 @@ sub DELETE { ## $self->lock( LOCK_EX ); - my $md5 = $self->{engine}{digest}->($key); + my $md5 = $self->_engine->{digest}->($key); - my $tag = $self->{engine}->find_bucket_list( $self->_base_offset, $md5 ); + my $tag = $self->_engine->find_blist( $self->_base_offset, $md5 ); if (!$tag) { $self->unlock(); return; @@ -556,13 +561,13 @@ sub DELETE { ## # Delete bucket ## - my $value = $self->{engine}->get_bucket_value( $tag, $md5 ); + my $value = $self->_engine->get_bucket_value( $tag, $md5 ); if (defined $value && !ref($value) && $self->_fileobj->{filter_fetch_value}) { $value = $self->_fileobj->{filter_fetch_value}->($value); } - my $result = $self->{engine}->delete_bucket( $tag, $md5, $orig_key ); + my $result = $self->_engine->delete_bucket( $tag, $md5, $orig_key ); ## # If this object is an array and the key deleted was on the end of the stack, @@ -581,14 +586,14 @@ sub EXISTS { my $self = shift->_get_self; my ($key) = @_; - my $md5 = $self->{engine}{digest}->($key); + my $md5 = $self->_engine->{digest}->($key); ## # Request shared lock for reading ## $self->lock( LOCK_SH ); - my $tag = $self->{engine}->find_bucket_list( $self->_base_offset, $md5 ); + my $tag = $self->_engine->find_blist( $self->_base_offset, $md5 ); if (!$tag) { $self->unlock(); @@ -601,7 +606,7 @@ sub EXISTS { ## # Check if bucket exists and return 1 or '' ## - my $result = $self->{engine}->bucket_exists( $tag, $md5 ) || ''; + my $result = $self->_engine->bucket_exists( $tag, $md5 ) || ''; $self->unlock(); @@ -636,11 +641,30 @@ sub CLEAR { ## $self->lock( LOCK_EX ); + if ( $self->_type eq TYPE_HASH ) { + my $key = $self->first_key; + while ( $key ) { + my $next_key = $self->next_key( $key ); + my $md5 = $self->_engine->{digest}->($key); + my $tag = $self->_engine->find_blist( $self->_base_offset, $md5 ); + $self->_engine->delete_bucket( $tag, $md5, $key ); + $key = $next_key; + } + } + else { + my $size = $self->FETCHSIZE; + for my $key ( map { pack ( $self->_engine->{long_pack}, $_ ) } 0 .. $size - 1 ) { + my $md5 = $self->_engine->{digest}->($key); + my $tag = $self->_engine->find_blist( $self->_base_offset, $md5 ); + $self->_engine->delete_bucket( $tag, $md5, $key ); + } + $self->STORESIZE( 0 ); + } #XXX This needs updating to use _release_space - $self->{engine}->write_tag( - $self->_base_offset, $self->_type, - chr(0)x$self->{engine}{index_size}, - ); +# $self->_engine->write_tag( +# $self->_base_offset, $self->_type, +# chr(0)x$self->_engine->{index_size}, +# ); $self->unlock(); @@ -670,10 +694,10 @@ DBM::Deep - A pure perl multi-level hash/array DBM use DBM::Deep; my $db = DBM::Deep->new( "foo.db" ); - $db->{key} = 'value'; # tie() style + $db->{key} = 'value'; print $db->{key}; - $db->put('key' => 'value'); # OO style + $db->put('key' => 'value'); print $db->get('key'); # true multi-level support @@ -682,33 +706,29 @@ DBM::Deep - A pure perl multi-level hash/array DBM 42, 99, ]; -=head1 DESCRIPTION + tie my %db, 'DBM::Deep', 'foo.db'; + $db{key} = 'value'; + print $db{key}; -A unique flat-file database module, written in pure perl. True -multi-level hash/array support (unlike MLDBM, which is faked), hybrid -OO / tie() interface, cross-platform FTPable files, and quite fast. Can -handle millions of keys and unlimited hash levels without significant -slow-down. Written from the ground-up in pure perl -- this is NOT a -wrapper around a C-based DBM. Out-of-the-box compatibility with Unix, -Mac OS X and Windows. + tied(%db)->put('key' => 'value'); + print tied(%db)->get('key'); -=head1 VERSION DIFFERENCES - -B: 0.99_01 and above have significant file format differences from 0.98 and -before. While attempts have been made to be backwards compatible, no guarantees. +=head1 DESCRIPTION -=head1 INSTALLATION +A unique flat-file database module, written in pure perl. True multi-level +hash/array support (unlike MLDBM, which is faked), hybrid OO / tie() +interface, cross-platform FTPable files, ACID transactions, and is quite fast. +Can handle millions of keys and unlimited levels without significant +slow-down. Written from the ground-up in pure perl -- this is NOT a wrapper +around a C-based DBM. Out-of-the-box compatibility with Unix, Mac OS X and +Windows. -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: +=head1 VERSION DIFFERENCES - tar zxf DBM-Deep-* - cd DBM-Deep-* - perl Makefile.PL - make - make test - make install +B: 0.99_01 and above have significant file format differences from 0.983 and +before. There will be a backwards-compatibility layer in 1.00, but that is +slated for a later 0.99_x release. This version is B backwards compatible +with 0.983 and before. =head1 SETUP @@ -718,7 +738,7 @@ Perl's tie() function. Both are examined here. =head2 OO CONSTRUCTION The recommended way to construct a DBM::Deep object is to use the new() -method, which gets you a blessed, tied hash or array reference. +method, which gets you a blessed I tied hash (or array) reference. my $db = DBM::Deep->new( "foo.db" ); @@ -728,11 +748,11 @@ opened in "r+" (read/write) mode, and the type of object returned is a hash, unless otherwise specified (see L 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: +locking, autoflush, etc. This is done by passing an inline hash (or hashref): my $db = DBM::Deep->new( - file => "foo.db", - locking => 1, + file => "foo.db", + locking => 1, autoflush => 1 ); @@ -741,8 +761,6 @@ the "file" parameter, as opposed to being the sole argument to the constructor. This is required if any options are specified. See L below for the complete list. - - You can also start with an array instead of a hash. For this, you must specify the C parameter: @@ -760,8 +778,8 @@ the wrong type is passed in. 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). +such as lock() and unlock(). (That object can be retrieved from the tied +variable at any time using tied() - please see L for more info. my %hash; my $db = tie %hash, "DBM::Deep", "foo.db"; @@ -804,6 +822,11 @@ needs. If you open it read-only and attempt to write, an exception will be throw open it write-only or append-only, an exception will be thrown immediately as DBM::Deep needs to read from the fh. +=item * audit_file / audit_fh + +These are just like file/fh, except for auditing. Please see L for +more information. + =item * file_offset This is the offset within the file that the DBM::Deep db starts. Most of the time, you will @@ -829,11 +852,12 @@ parameter, and defaults to CTYPE_HASH>. =item * locking -Specifies whether locking is to be enabled. DBM::Deep uses Perl's Fnctl flock() -function to lock the database in exclusive mode for writes, and shared mode for -reads. Pass any true value to enable. This affects the base DB handle I that use the same DB file. This is an optional -parameter, and defaults to 0 (disabled). See L below for more. +Specifies whether locking is to be enabled. DBM::Deep uses Perl's flock() +function to lock the database in exclusive mode for writes, and shared mode +for reads. Pass any true value to enable. This affects the base DB handle +I that use the same DB file. This is an +optional parameter, and defaults to 0 (disabled). See L below for +more. =item * autoflush @@ -889,7 +913,7 @@ You can even step through hash keys using the normal Perl C function: Remember that Perl's C function extracts I key from the hash and pushes them onto an array, all before the loop even begins. If you have an -extra large hash, this may exhaust Perl's memory. Instead, consider using +extremely large hash, this may exhaust Perl's memory. Instead, consider using Perl's C function, which pulls keys/values one at a time, using very little memory: @@ -936,7 +960,8 @@ or simply be a nested array reference inside a hash. Example: In addition to the I interface, you can also use a standard OO interface to manipulate all aspects of DBM::Deep databases. Each type of object (hash or array) has its own methods, but both types share the following common methods: -C, C, C, C and C. +C, C, C, C and C. C and +C are aliases to C and C, respectively. =over @@ -998,7 +1023,8 @@ q.v. Locking. =item * optimize() -Recover lost disk space. +Recover lost disk space. This is important to do, especially if you use +transactions. =item * import() / export() @@ -1207,7 +1233,8 @@ The C method can be called on any database level (not just the base level), and works with both hash and array DB types. B Make sure your existing structure has no circular references in it. -These will cause an infinite loop when importing. +These will cause an infinite loop when importing. There are plans to fix this +in a later release. =head2 EXPORTING @@ -1235,7 +1262,8 @@ large databases -- you can store a lot more data in a DBM::Deep object than an in-memory Perl structure. B Make sure your database has no circular references in it. -These will cause an infinite loop when exporting. +These will cause an infinite loop when exporting. There are plans to fix this +in a later release. =head1 FILTERS @@ -1495,6 +1523,41 @@ object tree (such as I or even the built-in C or C methods) will result in an infinite loop. This will be fixed in a future release. +=head1 AUDITING + +New in 0.99_01 is the ability to audit your databases actions. By passing in +audit_file (or audit_fh) to the constructor, all actions will be logged to +that file. The format is one that is suitable for eval'ing against the +database to replay the actions. Please see t/33_audit_trail.t for an example +of how to do this. + +=head1 TRANSACTIONS + +New in 0.99_01 is ACID transactions. Every DBM::Deep object is completely +transaction-ready - it is not an option you have to turn on. Three new methods +have been added to support them. They are: + +=over 4 + +=item * begin_work() + +This starts a transaction. + +=item * commit() + +This applies the changes done within the transaction to the mainline and ends +the transaction. + +=item * rollback() + +This discards the changes done within the transaction to the mainline and ends +the transaction. + +=back + +Transactions in DBM::Deep are done using the MVCC method, the same method used +by the InnoDB MySQL table type. + =head1 CAVEATS / ISSUES / BUGS This section describes all the known issues with DBM::Deep. It you have found @@ -1814,30 +1877,32 @@ built-in hashes. =head1 CODE COVERAGE -We use B to test the code coverage of our tests, below is the -B report on this module's test suite. +B is used to test the code coverage of the tests. Below is the +B report on this distribution's test suite. - ----------------------------------- ------ ------ ------ ------ ------ ------ - File stmt bran cond sub time total - ----------------------------------- ------ ------ ------ ------ ------ ------ - blib/lib/DBM/Deep.pm 94.9 80.6 73.0 100.0 37.9 90.4 - blib/lib/DBM/Deep/Array.pm 100.0 91.1 100.0 100.0 18.2 98.1 - blib/lib/DBM/Deep/Engine.pm 98.9 87.3 80.0 100.0 34.2 95.2 - blib/lib/DBM/Deep/Hash.pm 100.0 87.5 100.0 100.0 9.7 97.3 - Total 97.9 85.9 79.7 100.0 100.0 94.3 - ----------------------------------- ------ ------ ------ ------ ------ ------ + ---------------------------- ------ ------ ------ ------ ------ ------ ------ + File stmt bran cond sub pod time total + ---------------------------- ------ ------ ------ ------ ------ ------ ------ + blib/lib/DBM/Deep.pm 96.2 89.0 75.0 95.8 89.5 36.0 92.9 + blib/lib/DBM/Deep/Array.pm 96.1 88.3 100.0 96.4 100.0 15.9 94.7 + blib/lib/DBM/Deep/Engine.pm 96.6 86.6 89.5 100.0 0.0 20.0 91.0 + blib/lib/DBM/Deep/File.pm 99.4 88.3 55.6 100.0 0.0 19.6 89.5 + blib/lib/DBM/Deep/Hash.pm 98.5 83.3 100.0 100.0 100.0 8.5 96.3 + Total 96.9 87.4 81.2 98.0 38.5 100.0 92.1 + ---------------------------- ------ ------ ------ ------ ------ ------ ------ =head1 MORE INFORMATION Check out the DBM::Deep Google Group at L -or send email to L. +or send email to L. You can also visit #dbm-deep on +irc.perl.org -=head1 AUTHORS - -Joseph Huckaby, L +=head1 MAINTAINERS Rob Kinyon, L +Originally written by Joseph Huckaby, L + Special thanks to Adam Sah and Rich Gaushell! You know why :-) =head1 SEE ALSO diff --git a/lib/DBM/Deep/Array.pm b/lib/DBM/Deep/Array.pm index 65af4a7..ae8dba8 100644 --- a/lib/DBM/Deep/Array.pm +++ b/lib/DBM/Deep/Array.pm @@ -61,7 +61,7 @@ sub FETCH { } } - $key = pack($self->{engine}{long_pack}, $key); + $key = pack($self->_engine->{long_pack}, $key); } my $rv = $self->SUPER::FETCH( $key, $orig_key ); @@ -92,7 +92,7 @@ sub STORE { } } - $key = pack($self->{engine}{long_pack}, $key); + $key = pack($self->_engine->{long_pack}, $key); } my $rv = $self->SUPER::STORE( $key, $value, $orig_key ); @@ -124,7 +124,7 @@ sub EXISTS { } } - $key = pack($self->{engine}{long_pack}, $key); + $key = pack($self->_engine->{long_pack}, $key); } my $rv = $self->SUPER::EXISTS( $key ); @@ -153,7 +153,7 @@ sub DELETE { } } - $key = pack($self->{engine}{long_pack}, $key); + $key = pack($self->_engine->{long_pack}, $key); } my $rv = $self->SUPER::DELETE( $key, $orig ); @@ -182,7 +182,7 @@ sub FETCHSIZE { $self->unlock; if ($packed_size) { - return int(unpack($self->{engine}{long_pack}, $packed_size)); + return int(unpack($self->_engine->{long_pack}, $packed_size)); } return 0; @@ -197,7 +197,7 @@ sub STORESIZE { my $SAVE_FILTER = $self->_fileobj->{filter_store_value}; $self->_fileobj->{filter_store_value} = undef; - my $result = $self->STORE('length', pack($self->{engine}{long_pack}, $new_length), 'length'); + my $result = $self->STORE('length', pack($self->_engine->{long_pack}, $new_length), 'length'); $self->_fileobj->{filter_store_value} = $SAVE_FILTER; diff --git a/lib/DBM/Deep/Engine.pm b/lib/DBM/Deep/Engine.pm index 0ceb233..be720be 100644 --- a/lib/DBM/Deep/Engine.pm +++ b/lib/DBM/Deep/Engine.pm @@ -98,24 +98,29 @@ sub calculate_sizes { # The 2**8 here indicates the number of different characters in the # current hashing algorithm #XXX Does this need to be updated with different hashing algorithms? - $self->{index_size} = (2**8) * $self->{long_size}; - $self->{bucket_size} = $self->{hash_size} + $self->{long_size} * 3; + $self->{hash_chars_used} = (2**8); + $self->{index_size} = $self->{hash_chars_used} * $self->{long_size}; + + $self->{bucket_size} = $self->{hash_size} + $self->{long_size} * 2; $self->{bucket_list_size} = $self->{max_buckets} * $self->{bucket_size}; + $self->{key_size} = $self->{long_size} * 2; + $self->{keyloc_size} = $self->{max_buckets} * $self->{key_size}; + return; } sub write_file_header { my $self = shift; - my $loc = $self->_fileobj->request_space( length( SIG_FILE ) + 21 ); + my $loc = $self->_fileobj->request_space( length( SIG_FILE ) + 33 ); $self->_fileobj->print_at( $loc, SIG_FILE, SIG_HEADER, pack('N', 1), # header version - pack('N', 12), # header size - pack('N', 0), # currently running transaction IDs + pack('N', 24), # header size + pack('N4', 0, 0, 0, 0), # currently running transaction IDs pack('n', $self->{long_size}), pack('A', $self->{long_pack}), pack('n', $self->{data_size}), @@ -149,7 +154,7 @@ sub read_file_header { } my $buffer2 = $self->_fileobj->read_at( undef, $size ); - my ($running_transactions, @values) = unpack( 'N n A n A n', $buffer2 ); + my ($a1, $a2, $a3, $a4, @values) = unpack( 'N4 n A n A n', $buffer2 ); $self->_fileobj->set_transaction_offset( 13 ); @@ -253,6 +258,7 @@ sub write_tag { return { signature => $sig, + #XXX Is this even used? size => $size, offset => $offset + SIG_SIZE + $self->{data_size}, content => $content @@ -275,12 +281,37 @@ sub load_tag { return { signature => $sig, + #XXX Is this even used? size => $size, offset => $offset + SIG_SIZE + $self->{data_size}, content => $fileobj->read_at( undef, $size ), }; } +sub find_keyloc { + my $self = shift; + my ($tag, $transaction_id) = @_; + $transaction_id = $self->_fileobj->transaction_id + unless defined $transaction_id; + + for ( my $i = 0; $i < $self->{max_buckets}; $i++ ) { + my ($loc, $trans_id, $is_deleted) = unpack( + "$self->{long_pack} C C", + substr( $tag->{content}, $i * $self->{key_size}, $self->{key_size} ), + ); + + if ( $loc == 0 ) { + return ( $loc, $is_deleted, $i * $self->{key_size} ); + } + + next if $transaction_id != $trans_id; + + return ( $loc, $is_deleted, $i * $self->{key_size} ); + } + + return; +} + sub add_bucket { ## # Adds one key/value pair to bucket list, given offset, MD5 digest of key, @@ -288,7 +319,6 @@ sub add_bucket { ## my $self = shift; my ($tag, $md5, $plain_key, $value, $deleted, $orig_key) = @_; - $deleted ||= 0; # This verifies that only supported values will be stored. { @@ -305,10 +335,8 @@ sub add_bucket { my $fileobj = $self->_fileobj; - my $actual_length = $self->_length_needed( $value, $plain_key ); - #ACID - This is a mutation. Must only find the exact transaction - my ($subloc, $offset, $size,$is_deleted) = $self->_find_in_buckets( $tag, $md5, 1 ); + my ($keyloc, $offset) = $self->_find_in_buckets( $tag, $md5, 1 ); my @transactions; if ( $fileobj->transaction_id == 0 ) { @@ -316,56 +344,72 @@ sub add_bucket { } # $self->_release_space( $size, $subloc ); - # Updating a known md5 #XXX This needs updating to use _release_space + my $location; - if ( $subloc ) { - if ($actual_length <= $size) { - $location = $subloc; - } - else { - $location = $fileobj->request_space( $actual_length ); + my $size = $self->_length_needed( $value, $plain_key ); - $fileobj->print_at( $tag->{offset} + $offset + $self->{hash_size}, - pack($self->{long_pack}, $location ), - pack($self->{long_pack}, $actual_length ), - pack('n n', $fileobj->transaction_id, $deleted ), - ); + # Updating a known md5 + if ( $keyloc ) { + my $keytag = $self->load_tag( $keyloc ); + my ($subloc, $is_deleted, $offset) = $self->find_keyloc( $keytag ); + + if ( @transactions ) { + my $old_value = $self->read_from_loc( $subloc, $orig_key ); + my $old_size = $self->_length_needed( $old_value, $plain_key ); + + for my $trans_id ( @transactions ) { + my ($loc, $is_deleted, $offset2) = $self->find_keyloc( $keytag, $trans_id ); + unless ($loc) { + my $location2 = $fileobj->request_space( $old_size ); + $fileobj->print_at( $keytag->{offset} + $offset2, + pack($self->{long_pack}, $location2 ), + pack( 'C C', $trans_id, 0 ), + ); + $self->write_value( $location2, $plain_key, $old_value, $orig_key ); + } + } } - my $old_value = $self->read_from_loc( $subloc, $orig_key ); - for ( @transactions ) { - my $tag2 = $self->load_tag( $tag->{offset} - SIG_SIZE - $self->{data_size} ); - $fileobj->{transaction_id} = $_; - $self->add_bucket( $tag2, $md5, $orig_key, $old_value, undef, $orig_key ); - $fileobj->{transaction_id} = 0; - } - $tag = $self->load_tag( $tag->{offset} - SIG_SIZE - $self->{data_size} ); + $location = $self->_fileobj->request_space( $size ); + #XXX This needs to be transactionally-aware in terms of which keytag->{offset} to use + $fileobj->print_at( $keytag->{offset} + $offset, + pack($self->{long_pack}, $location ), + pack( 'C C', $fileobj->transaction_id, 0 ), + ); } # Adding a new md5 - elsif ( defined $offset ) { - $location = $fileobj->request_space( $actual_length ); + else { + my $keyloc = $fileobj->request_space( $self->tag_size( $self->{keyloc_size} ) ); - $fileobj->print_at( $tag->{offset} + $offset, - $md5, - pack($self->{long_pack}, $location ), - pack($self->{long_pack}, $actual_length ), - pack('n n', $fileobj->transaction_id, $deleted ), + # The bucket fit into list + if ( defined $offset ) { + $fileobj->print_at( $tag->{offset} + $offset, + $md5, pack( $self->{long_pack}, $keyloc ), + ); + } + # If bucket didn't fit into list, split into a new index level + else { + $self->split_index( $tag, $md5, $keyloc ); + } + + my $keytag = $self->write_tag( + $keyloc, SIG_KEYS, chr(0)x$self->{keyloc_size}, ); - for ( @transactions ) { - my $tag2 = $self->load_tag( $tag->{offset} - SIG_SIZE - $self->{data_size} ); - $fileobj->{transaction_id} = $_; - $self->add_bucket( $tag2, $md5, '', '', 1, $orig_key ); - $fileobj->{transaction_id} = 0; + $location = $self->_fileobj->request_space( $size ); + $fileobj->print_at( $keytag->{offset}, + pack( $self->{long_pack}, $location ), + pack( 'C C', $fileobj->transaction_id, 0 ), + ); + + my $offset = 1; + for my $trans_id ( @transactions ) { + $fileobj->print_at( $keytag->{offset} + $self->{key_size} * $offset++, + pack( $self->{long_pack}, -1 ), + pack( 'C C', $trans_id, 1 ), + ); } - $tag = $self->load_tag( $tag->{offset} - SIG_SIZE - $self->{data_size} ); - } - # If bucket didn't fit into list, split into a new index level - # split_index() will do the $self->_fileobj->request_space() call - #XXX It also needs to be transactionally aware - else { - $location = $self->split_index( $md5, $tag ); } $self->write_value( $location, $plain_key, $value, $orig_key ); @@ -465,7 +509,7 @@ sub write_value { sub split_index { my $self = shift; - my ($md5, $tag) = @_; + my ($tag, $md5, $keyloc) = @_; my $fileobj = $self->_fileobj; @@ -480,21 +524,15 @@ sub split_index { chr(0)x$self->{index_size}, ); - my $newtag_loc = $fileobj->request_space( - $self->tag_size( $self->{bucket_list_size} ), - ); - my $keys = $tag->{content} - . $md5 . pack($self->{long_pack}, $newtag_loc) - . pack($self->{long_pack}, 0) # size - . pack($self->{long_pack}, 0); # transaction ID + . $md5 . pack($self->{long_pack}, $keyloc); my @newloc = (); BUCKET: # The <= here is deliberate - we have max_buckets+1 keys to iterate # through, unlike every other loop that uses max_buckets as a stop. for (my $i = 0; $i <= $self->{max_buckets}; $i++) { - my ($key, $old_subloc, $size) = $self->_get_key_subloc( $keys, $i ); + my ($key, $old_subloc) = $self->_get_key_subloc( $keys, $i ); die "[INTERNAL ERROR]: No key in split_index()\n" unless $key; die "[INTERNAL ERROR]: No subloc in split_index()\n" unless $old_subloc; @@ -505,7 +543,7 @@ sub split_index { my $subkeys = $fileobj->read_at( $newloc[$num], $self->{bucket_list_size} ); # This is looking for the first empty spot - my ($subloc, $offset, $size) = $self->_find_in_buckets( + my ($subloc, $offset) = $self->_find_in_buckets( { content => $subkeys }, '', ); @@ -541,7 +579,7 @@ sub split_index { $tag->{offset} - SIG_SIZE - $self->{data_size}, ); - return $newtag_loc; + return 1; } sub read_from_loc { @@ -628,15 +666,23 @@ sub get_bucket_value { my ($tag, $md5, $orig_key) = @_; #ACID - This is a read. Can find exact or HEAD - my ($subloc, $offset, $size, $is_deleted) = $self->_find_in_buckets( $tag, $md5 ); + my ($keyloc, $offset) = $self->_find_in_buckets( $tag, $md5 ); - if ( !$subloc ) { + if ( !$keyloc ) { #XXX Need to use real key -# $self->add_bucket( $tag, $md5, $orig_key, undef, undef, $orig_key ); +# $self->add_bucket( $tag, $md5, $orig_key, undef, $orig_key ); # return; } - elsif ( !$is_deleted ) { - return $self->read_from_loc( $subloc, $orig_key ); +# elsif ( !$is_deleted ) { + else { + my $keytag = $self->load_tag( $keyloc ); + my ($subloc, $is_deleted) = $self->find_keyloc( $keytag ); + if (!$subloc) { + ($subloc, $is_deleted) = $self->find_keyloc( $keytag, 0 ); + } + if ( $subloc && !$is_deleted ) { + return $self->read_from_loc( $subloc, $orig_key ); + } } return; @@ -651,9 +697,9 @@ sub delete_bucket { #ACID - Although this is a mutation, we must find any transaction. # This is because we need to mark something as deleted that is in the HEAD. - my ($subloc, $offset, $size,$is_deleted) = $self->_find_in_buckets( $tag, $md5 ); + my ($keyloc, $offset) = $self->_find_in_buckets( $tag, $md5 ); - return if !$subloc; + return if !$keyloc; my $fileobj = $self->_fileobj; @@ -663,25 +709,38 @@ sub delete_bucket { } if ( $fileobj->transaction_id == 0 ) { + my $keytag = $self->load_tag( $keyloc ); + my ($subloc, $is_deleted, $offset) = $self->find_keyloc( $keytag ); my $value = $self->read_from_loc( $subloc, $orig_key ); - for (@transactions) { - $fileobj->{transaction_id} = $_; - #XXX Need to use real key - $self->add_bucket( $tag, $md5, $orig_key, $value, undef, $orig_key ); - $fileobj->{transaction_id} = 0; + my $size = $self->_length_needed( $value, $orig_key ); + + for my $trans_id ( @transactions ) { + my ($loc, $is_deleted, $offset2) = $self->find_keyloc( $keytag, $trans_id ); + unless ($loc) { + my $location2 = $fileobj->request_space( $size ); + $fileobj->print_at( $keytag->{offset} + $offset2, + pack($self->{long_pack}, $location2 ), + pack( 'C C', $trans_id, 0 ), + ); + $self->write_value( $location2, $orig_key, $value, $orig_key ); + } } - $tag = $self->load_tag( $tag->{offset} - SIG_SIZE - $self->{data_size} ); - #XXX This needs _release_space() for the value and anything below - $fileobj->print_at( - $tag->{offset} + $offset, - substr( $tag->{content}, $offset + $self->{bucket_size} ), - chr(0) x $self->{bucket_size}, + $keytag = $self->load_tag( $keyloc ); + ($subloc, $is_deleted, $offset) = $self->find_keyloc( $keytag ); + $fileobj->print_at( $keytag->{offset} + $offset, + substr( $keytag->{content}, $offset + $self->{key_size} ), + chr(0) x $self->{key_size}, ); } else { - $self->add_bucket( $tag, $md5, '', '', 1, $orig_key ); + my $keytag = $self->load_tag( $keyloc ); + my ($subloc, $is_deleted, $offset) = $self->find_keyloc( $keytag ); + $fileobj->print_at( $keytag->{offset} + $offset, + pack($self->{long_pack}, -1 ), + pack( 'C C', $fileobj->transaction_id, 1 ), + ); } return 1; @@ -695,11 +754,16 @@ sub bucket_exists { my ($tag, $md5) = @_; #ACID - This is a read. Can find exact or HEAD - my ($subloc, $offset, $size, $is_deleted) = $self->_find_in_buckets( $tag, $md5 ); + my ($keyloc) = $self->_find_in_buckets( $tag, $md5 ); + my $keytag = $self->load_tag( $keyloc ); + my ($subloc, $is_deleted, $offset) = $self->find_keyloc( $keytag ); + if ( !$subloc ) { + ($subloc, $is_deleted, $offset) = $self->find_keyloc( $keytag, 0 ); + } return ($subloc && !$is_deleted) && 1; } -sub find_bucket_list { +sub find_blist { ## # Locate offset for bucket list, given digested key ## @@ -773,19 +837,18 @@ sub traverse_index { # Scan index and recursively step into deeper levels, looking for next key. ## my $self = shift; - my ($obj, $offset, $ch, $force_return_next) = @_; + my ($xxxx, $offset, $ch, $force_return_next) = @_; my $tag = $self->load_tag( $offset ); if ($tag->{signature} ne SIG_BLIST) { - my $content = $tag->{content}; - my $start = $obj->{return_next} ? 0 : ord(substr($obj->{prev_md5}, $ch, 1)); + my $start = $xxxx->{return_next} ? 0 : ord(substr($xxxx->{prev_md5}, $ch, 1)); - for (my $idx = $start; $idx < (2**8); $idx++) { + for (my $idx = $start; $idx < $self->{hash_chars_used}; $idx++) { my $subloc = unpack( $self->{long_pack}, substr( - $content, + $tag->{content}, $idx * $self->{long_size}, $self->{long_size}, ), @@ -793,46 +856,48 @@ sub traverse_index { if ($subloc) { my $result = $self->traverse_index( - $obj, $subloc, $ch + 1, $force_return_next, + $xxxx, $subloc, $ch + 1, $force_return_next, ); - if (defined($result)) { return $result; } + if (defined $result) { return $result; } } } # index loop - $obj->{return_next} = 1; + $xxxx->{return_next} = 1; } # This is the bucket list else { my $keys = $tag->{content}; - if ($force_return_next) { $obj->{return_next} = 1; } + if ($force_return_next) { $xxxx->{return_next} = 1; } ## # Iterate through buckets, looking for a key match ## my $transaction_id = $self->_fileobj->transaction_id; for (my $i = 0; $i < $self->{max_buckets}; $i++) { - my ($key, $subloc, $size, $trans_id, $is_deleted) = $self->_get_key_subloc( $keys, $i ); - - next if $is_deleted; -#XXX Need to find all the copies of this key to find out if $transaction_id has it -#XXX marked as deleted, in use, or what. - next if $trans_id && $trans_id != $transaction_id; + my ($key, $keyloc) = $self->_get_key_subloc( $keys, $i ); # End of bucket list -- return to outer loop - if (!$subloc) { - $obj->{return_next} = 1; + if (!$keyloc) { + $xxxx->{return_next} = 1; last; } # Located previous key -- return next one found - elsif ($key eq $obj->{prev_md5}) { - $obj->{return_next} = 1; + elsif ($key eq $xxxx->{prev_md5}) { + $xxxx->{return_next} = 1; next; } # Seek to bucket location and skip over signature - elsif ($obj->{return_next}) { + elsif ($xxxx->{return_next}) { my $fileobj = $self->_fileobj; + my $keytag = $self->load_tag( $keyloc ); + my ($subloc, $is_deleted) = $self->find_keyloc( $keytag ); + if ( $subloc == 0 ) { + ($subloc, $is_deleted) = $self->find_keyloc( $keytag, 0 ); + } + next if $is_deleted; + # Skip over value to get to plain key my $sig = $fileobj->read_at( $subloc, SIG_SIZE ); @@ -850,7 +915,7 @@ sub traverse_index { } } - $obj->{return_next} = 1; + $xxxx->{return_next} = 1; } return; @@ -863,19 +928,25 @@ sub get_next_key { my $self = shift; my ($obj) = @_; - $obj->{prev_md5} = $_[1] ? $_[1] : undef; - $obj->{return_next} = 0; - ## # If the previous key was not specifed, start at the top and # return the first one found. ## - if (!$obj->{prev_md5}) { - $obj->{prev_md5} = chr(0) x $self->{hash_size}; - $obj->{return_next} = 1; + my $temp; + if ( @_ > 1 ) { + $temp = { + prev_md5 => $_[1], + return_next => 0, + }; + } + else { + $temp = { + prev_md5 => chr(0) x $self->{hash_size}, + return_next => 1, + }; } - return $self->traverse_index( $obj, $obj->_base_offset, 0 ); + return $self->traverse_index( $temp, $obj->_base_offset, 0 ); } # Utilities @@ -884,51 +955,36 @@ sub _get_key_subloc { my $self = shift; my ($keys, $idx) = @_; - my ($key, $subloc, $size, $transaction_id, $is_deleted) = unpack( + return unpack( # This is 'a', not 'A'. Please read the pack() documentation for the # difference between the two and why it's important. - "a$self->{hash_size} $self->{long_pack}2 n2", + "a$self->{hash_size} $self->{long_pack}", substr( $keys, ($idx * $self->{bucket_size}), $self->{bucket_size}, ), ); - - return ($key, $subloc, $size, $transaction_id, $is_deleted); } sub _find_in_buckets { my $self = shift; - my ($tag, $md5, $exact) = @_; - $exact ||= 0; - - my $trans_id = $self->_fileobj->transaction_id; - - my @zero; + my ($tag, $md5) = @_; BUCKET: for ( my $i = 0; $i < $self->{max_buckets}; $i++ ) { - my ($key, $subloc, $size, $transaction_id, $is_deleted) = $self->_get_key_subloc( + my ($key, $subloc) = $self->_get_key_subloc( $tag->{content}, $i, ); - my @rv = ($subloc, $i * $self->{bucket_size}, $size, $is_deleted); + my @rv = ($subloc, $i * $self->{bucket_size}); unless ( $subloc ) { - if ( !$exact && @zero && $trans_id ) { - @rv = ($zero[2], $zero[0] * $self->{bucket_size},$zero[3],$is_deleted); - } return @rv; } next BUCKET if $key ne $md5; - # Save off the HEAD in case we need it. - @zero = ($i,$key,$subloc,$size,$transaction_id,$is_deleted) if $transaction_id == 0; - - next BUCKET if $transaction_id != $trans_id; - return @rv; } diff --git a/lib/DBM/Deep/File.pm b/lib/DBM/Deep/File.pm index 72c2540..be03615 100644 --- a/lib/DBM/Deep/File.pm +++ b/lib/DBM/Deep/File.pm @@ -312,7 +312,7 @@ sub begin_transaction { $self->lock; my $buffer = $self->read_at( $self->{transaction_offset}, 4 ); - my ($next, @trans) = unpack( 'C C C C', $buffer ); + my ($next, @trans) = unpack( 'C C C C C C C C C C C C C C C C', $buffer ); $self->{transaction_id} = ++$next; @@ -326,7 +326,7 @@ sub begin_transaction { $self->print_at( $self->{transaction_offset}, - pack( 'C C C C', $next, @trans), + pack( 'C C C C C C C C C C C C C C C C', $next, @trans), ); $self->unlock; @@ -344,13 +344,13 @@ sub end_transaction { $self->lock; my $buffer = $self->read_at( $self->{transaction_offset}, 4 ); - my ($next, @trans) = unpack( 'C C C C', $buffer ); + my ($next, @trans) = unpack( 'C C C C C C C C C C C C C C C C', $buffer ); @trans = grep { $_ != $self->{transaction_id} } @trans; $self->print_at( $self->{transaction_offset}, - pack( 'C C C C', $next, @trans), + pack( 'C C C C C C C C C C C C C C C C', $next, @trans), ); #XXX Need to free the space used by the current transaction @@ -375,7 +375,7 @@ sub current_transactions { $self->lock; my $buffer = $self->read_at( $self->{transaction_offset}, 4 ); - my ($next, @trans) = unpack( 'C C C C', $buffer ); + my ($next, @trans) = unpack( 'C C C C C C C C C C C C C C C C', $buffer ); $self->unlock; diff --git a/lib/DBM/Deep/Hash.pm b/lib/DBM/Deep/Hash.pm index a86ac10..bebc926 100644 --- a/lib/DBM/Deep/Hash.pm +++ b/lib/DBM/Deep/Hash.pm @@ -91,7 +91,7 @@ sub FIRSTKEY { ## $self->lock( $self->LOCK_SH ); - my $result = $self->{engine}->get_next_key($self); + my $result = $self->_engine->get_next_key($self); $self->unlock(); @@ -110,14 +110,14 @@ sub NEXTKEY { ? $self->_fileobj->{filter_store_key}->($_[0]) : $_[0]; - my $prev_md5 = $self->{engine}{digest}->($prev_key); + my $prev_md5 = $self->_engine->{digest}->($prev_key); ## # Request shared lock for reading ## $self->lock( $self->LOCK_SH ); - my $result = $self->{engine}->get_next_key( $self, $prev_md5 ); + my $result = $self->_engine->get_next_key( $self, $prev_md5 ); $self->unlock(); diff --git a/t/02_hash.t b/t/02_hash.t index d913e03..0bd49a7 100644 --- a/t/02_hash.t +++ b/t/02_hash.t @@ -2,7 +2,7 @@ # DBM::Deep Test ## use strict; -use Test::More tests => 36; +use Test::More tests => 38; use Test::Exception; use t::common qw( new_fh ); @@ -18,7 +18,6 @@ $db->{key1} = "value1"; is( $db->get("key1"), "value1", "get() works with hash assignment" ); is( $db->fetch("key1"), "value1", "... fetch() works with hash assignment" ); is( $db->{key1}, "value1", "... and hash-access also works" ); - $db->put("key2", undef); is( $db->get("key2"), undef, "get() works with put()" ); is( $db->fetch("key2"), undef, "... fetch() works with put()" ); @@ -44,6 +43,7 @@ ok( !exists $db->{key4}, "And key4 doesn't exists anymore" ); ## # count keys ## + is( scalar keys %$db, 3, "keys() works against tied hash" ); ## @@ -51,7 +51,7 @@ is( scalar keys %$db, 3, "keys() works against tied hash" ); ## my $temphash = {}; while ( my ($key, $value) = each %$db ) { - $temphash->{$key} = $value; + $temphash->{$key} = $value; } is( $temphash->{key1}, 'value1', "First key copied successfully using tied interface" ); @@ -61,8 +61,8 @@ is( $temphash->{key3}, 'value3', "Third key copied successfully" ); $temphash = {}; my $key = $db->first_key(); while ($key) { - $temphash->{$key} = $db->get($key); - $key = $db->next_key($key); + $temphash->{$key} = $db->get($key); + $key = $db->next_key($key); } is( $temphash->{key1}, 'value1', "First key copied successfully using OO interface" ); @@ -75,6 +75,8 @@ is( $temphash->{key3}, 'value3', "Third key copied successfully" ); is( delete $db->{key2}, undef, "delete through tied inteface works" ); is( $db->delete("key1"), 'value1', "delete through OO inteface works" ); is( $db->{key3}, 'value3', "The other key is still there" ); +ok( !exists $db->{key1}, "key1 doesn't exist" ); +ok( !exists $db->{key2}, "key2 doesn't exist" ); is( scalar keys %$db, 1, "After deleting two keys, 1 remains" ); @@ -120,9 +122,9 @@ my $first_key = $db->first_key(); my $next_key = $db->next_key($first_key); ok( - (($first_key eq "key1") || ($first_key eq "key2")) && - (($next_key eq "key1") || ($next_key eq "key2")) && - ($first_key ne $next_key) + (($first_key eq "key1") || ($first_key eq "key2")) && + (($next_key eq "key1") || ($next_key eq "key2")) && + ($first_key ne $next_key) ,"keys() still works if you replace long values with shorter ones" ); diff --git a/t/03_bighash.t b/t/03_bighash.t index 8aff353..cf082bd 100644 --- a/t/03_bighash.t +++ b/t/03_bighash.t @@ -8,6 +8,8 @@ use t::common qw( new_fh ); use_ok( 'DBM::Deep' ); +diag "This test can take up to a minute to run. Please be patient."; + my ($fh, $filename) = new_fh(); my $db = DBM::Deep->new( file => $filename, @@ -32,7 +34,6 @@ for ( 0 .. $max_keys ) { } is( $count, $max_keys, "We read $count keys" ); - my @keys = sort keys %$db; cmp_ok( scalar(@keys), '==', $max_keys + 1, "Number of keys is correct" ); my @control = sort map { "hello $_" } 0 .. $max_keys; diff --git a/t/04_array.t b/t/04_array.t index e5babd3..e916028 100644 --- a/t/04_array.t +++ b/t/04_array.t @@ -203,9 +203,8 @@ is($returned[0], "middle ABC"); $db->[0] = [ 1 .. 3 ]; $db->[1] = { a => 'foo' }; -is( $db->[1]->fetch('a'), 'foo', "Reuse of same space with hash successful" ); is( $db->[0]->length, 3, "Reuse of same space with array successful" ); - +is( $db->[1]->fetch('a'), 'foo', "Reuse of same space with hash successful" ); # Test autovivification $db->[9999]{bar} = 1; diff --git a/t/05_bigarray.t b/t/05_bigarray.t index 93a3db9..24b9071 100644 --- a/t/05_bigarray.t +++ b/t/05_bigarray.t @@ -7,6 +7,8 @@ use t::common qw( new_fh ); use_ok( 'DBM::Deep' ); +diag "This test can take up to a minute to run. Please be patient."; + my ($fh, $filename) = new_fh(); my $db = DBM::Deep->new( file => $filename, diff --git a/t/33_audit_trail.t b/t/28_audit_trail.t similarity index 100% rename from t/33_audit_trail.t rename to t/28_audit_trail.t diff --git a/t/28_transactions.t b/t/33_transactions.t similarity index 94% rename from t/28_transactions.t rename to t/33_transactions.t index f888946..2971a84 100644 --- a/t/28_transactions.t +++ b/t/33_transactions.t @@ -1,5 +1,5 @@ use strict; -use Test::More tests => 58; +use Test::More tests => 62; use Test::Deep; use t::common qw( new_fh ); @@ -35,10 +35,7 @@ $db1->begin_work; is( $db2->{other_x}, 'foo', "DB2 set other_x within DB1's transaction, so DB2 can see it" ); is( $db1->{other_x}, undef, "Since other_x was added after the transaction began, DB1 doesn't see it." ); -TODO: { - local $TODO = "keys aren't working yet"; cmp_bag( [ keys %$db1 ], [qw( x )], "DB1 keys correct" ); -} cmp_bag( [ keys %$db2 ], [qw( x other_x )], "DB2 keys correct" ); $db1->rollback; @@ -83,10 +80,7 @@ $db1->begin_work; ok( !exists $db1->{x}, "DB1 deleted X in a transaction, so it can't see it anymore" ); is( $db2->{x}, 'z', "But, DB2 can still see it" ); -TODO: { - local $TODO = "keys aren't working yet"; cmp_bag( [ keys %$db1 ], [qw( other_x )], "DB1 keys correct" ); -} cmp_bag( [ keys %$db2 ], [qw( x )], "DB2 keys correct" ); $db1->rollback; @@ -104,12 +98,10 @@ $db1->begin_work; delete $db1->{x}; ok( !exists $db1->{x}, "DB1 deleted X in a transaction, so it can't see it anymore" ); +#__END__ is( $db2->{x}, 'z', "But, DB2 can still see it" ); -TODO: { - local $TODO = "keys aren't working yet"; cmp_bag( [ keys %$db1 ], [qw()], "DB1 keys correct" ); -} cmp_bag( [ keys %$db2 ], [qw( x )], "DB2 keys correct" ); $db1->commit; @@ -124,9 +116,6 @@ is( $db2->{foo}, 'bar', "Set foo to bar in DB2" ); cmp_bag( [ keys %$db1 ], [qw( foo )], "DB1 keys correct" ); cmp_bag( [ keys %$db2 ], [qw( foo )], "DB2 keys correct" ); -TODO: { - todo_skip 'Still need to work on clear()', 4; - $db1->begin_work; %$db1 = (); # clear() @@ -144,9 +133,8 @@ is( $db2->{foo}, 'bar', "Rollback means 'foo' is still there" ); cmp_bag( [ keys %$db1 ], [qw( foo )], "DB1 keys correct" ); cmp_bag( [ keys %$db2 ], [qw( foo )], "DB2 keys correct" ); -} - $db1->optimize; + is( $db1->{foo}, 'bar', 'After optimize, everything is ok' ); is( $db2->{foo}, 'bar', 'After optimize, everything is ok' ); @@ -165,4 +153,3 @@ Tests to add: * Two transactions running at the same time * Doing a clear on the head while a transaction is running # More than just two keys -* Arrays (in particular, how is length handled?) diff --git a/t/34_transaction_arrays.t b/t/34_transaction_arrays.t index 5043c91..ea50810 100644 --- a/t/34_transaction_arrays.t +++ b/t/34_transaction_arrays.t @@ -1,5 +1,5 @@ use strict; -use Test::More tests => 17; +use Test::More tests => 47; use Test::Deep; use t::common qw( new_fh ); @@ -51,3 +51,73 @@ is( $db2->[1], 'foo', "After DB1 transaction is over, DB2 can still see 1" ); cmp_ok( scalar(@$db1), '==', 2, "DB1 now has 2 elements" ); cmp_ok( scalar(@$db2), '==', 2, "DB2 still has 2 elements" ); +$db1->begin_work; + + is( $db1->[0], 'y', "DB1 transaction started, no actions - DB1's 0 is Y" ); + is( $db2->[0], 'y', "DB1 transaction started, no actions - DB2's 0 is Y" ); + + $db1->[2] = 'z'; + is( $db1->[2], 'z', "Within DB1 transaction, DB1's 2 is Z" ); + ok( !exists $db2->[2], "Within DB1 transaction, DB2 cannot see 2" ); + + cmp_ok( scalar(@$db1), '==', 3, "DB1 has 3 elements" ); + cmp_ok( scalar(@$db2), '==', 2, "DB2 has 2 elements" ); + +$db1->commit; + +is( $db1->[0], 'y', "After rollback, DB1's 0 is Y" ); +is( $db2->[0], 'y', "After rollback, DB2's 0 is Y" ); + +is( $db1->[2], 'z', "After DB1 transaction is over, DB1 can still see 2" ); +is( $db2->[2], 'z', "After DB1 transaction is over, DB2 can now see 2" ); + +cmp_ok( scalar(@$db1), '==', 3, "DB1 now has 2 elements" ); +cmp_ok( scalar(@$db2), '==', 3, "DB2 still has 2 elements" ); + +$db1->begin_work; + + push @$db1, 'foo'; + unshift @$db1, 'bar'; + + cmp_ok( scalar(@$db1), '==', 5, "DB1 now has 5 elements" ); + cmp_ok( scalar(@$db2), '==', 3, "DB2 still has 3 elements" ); + + is( $db1->[0], 'bar' ); + is( $db1->[-1], 'foo' ); + +$db1->rollback; + +cmp_ok( scalar(@$db1), '==', 3, "DB1 is back to 3 elements" ); +cmp_ok( scalar(@$db2), '==', 3, "DB2 still has 3 elements" ); + +$db1->begin_work; + + push @$db1, 'foo'; + unshift @$db1, 'bar'; + + cmp_ok( scalar(@$db1), '==', 5, "DB1 now has 5 elements" ); + cmp_ok( scalar(@$db2), '==', 3, "DB2 still has 3 elements" ); + +$db1->commit; + +cmp_ok( scalar(@$db1), '==', 5, "DB1 is still at 5 elements" ); +cmp_ok( scalar(@$db2), '==', 5, "DB2 now has 5 elements" ); + +is( $db1->[0], 'bar' ); +is( $db1->[-1], 'foo' ); + +is( $db2->[0], 'bar' ); +is( $db2->[-1], 'foo' ); + +$db1->begin_work; + + @$db1 = (); # clear() + + cmp_ok( scalar(@$db1), '==', 0, "DB1 now has 0 elements" ); + cmp_ok( scalar(@$db2), '==', 5, "DB2 still has 5 elements" ); + +$db1->rollback; + +cmp_ok( scalar(@$db1), '==', 5, "DB1 now has 5 elements" ); +cmp_ok( scalar(@$db2), '==', 5, "DB2 still has 5 elements" ); + diff --git a/t/35_transaction_multiple.t b/t/35_transaction_multiple.t new file mode 100644 index 0000000..659d9a8 --- /dev/null +++ b/t/35_transaction_multiple.t @@ -0,0 +1,111 @@ +use strict; +use Test::More tests => 51; +use Test::Deep; +use t::common qw( new_fh ); + +use_ok( 'DBM::Deep' ); + +my ($fh, $filename) = new_fh(); +my $db1 = DBM::Deep->new( + file => $filename, + locking => 1, + autoflush => 1, +); + +my $db2 = DBM::Deep->new( + file => $filename, + locking => 1, + autoflush => 1, +); + +my $db3 = DBM::Deep->new( + file => $filename, + locking => 1, + autoflush => 1, +); + +$db1->{foo} = 'bar'; +is( $db1->{foo}, 'bar', "Before transaction, DB1's foo is bar" ); +is( $db2->{foo}, 'bar', "Before transaction, DB2's foo is bar" ); +is( $db3->{foo}, 'bar', "Before transaction, DB3's foo is bar" ); + +$db1->begin_work; + +is( $db1->{foo}, 'bar', "Before transaction work, DB1's foo is bar" ); +is( $db2->{foo}, 'bar', "Before transaction work, DB2's foo is bar" ); +is( $db3->{foo}, 'bar', "Before transaction work, DB3's foo is bar" ); + +$db1->{foo} = 'bar2'; + +is( $db1->{foo}, 'bar2', "After DB1 foo to bar2, DB1's foo is bar2" ); +is( $db2->{foo}, 'bar', "After DB1 foo to bar2, DB2's foo is bar" ); +is( $db3->{foo}, 'bar', "After DB1 foo to bar2, DB3's foo is bar" ); + +$db1->{bar} = 'foo'; + +ok( exists $db1->{bar}, "After DB1 set bar to foo, DB1's bar exists" ); +ok( !exists $db2->{bar}, "After DB1 set bar to foo, DB2's bar doesn't exist" ); +ok( !exists $db3->{bar}, "After DB1 set bar to foo, DB3's bar doesn't exist" ); + +$db2->begin_work; + +is( $db1->{foo}, 'bar2', "After DB2 transaction begin, DB1's foo is bar2" ); +is( $db2->{foo}, 'bar', "After DB2 transaction begin, DB2's foo is bar" ); +is( $db3->{foo}, 'bar', "After DB2 transaction begin, DB3's foo is bar" ); + +ok( exists $db1->{bar}, "After DB2 transaction begin, DB1's bar exists" ); +ok( !exists $db2->{bar}, "After DB2 transaction begin, DB2's bar doesn't exist" ); +ok( !exists $db3->{bar}, "After DB2 transaction begin, DB3's bar doesn't exist" ); + +$db2->{foo} = 'bar333'; + +is( $db1->{foo}, 'bar2', "After DB2 foo to bar2, DB1's foo is bar2" ); +is( $db2->{foo}, 'bar333', "After DB2 foo to bar2, DB2's foo is bar333" ); +is( $db3->{foo}, 'bar', "After DB2 foo to bar2, DB3's foo is bar" ); + +$db2->{bar} = 'mybar'; + +ok( exists $db1->{bar}, "After DB2 set bar to mybar, DB1's bar exists" ); +ok( exists $db2->{bar}, "After DB2 set bar to mybar, DB2's bar exists" ); +ok( !exists $db3->{bar}, "After DB2 set bar to mybar, DB3's bar doesn't exist" ); + +is( $db1->{bar}, 'foo', "DB1's bar is still foo" ); +is( $db2->{bar}, 'mybar', "DB2's bar is now mybar" ); + +$db2->{mykey} = 'myval'; + +ok( !exists $db1->{mykey}, "After DB2 set mykey to myval, DB1's mykey doesn't exist" ); +ok( exists $db2->{mykey}, "After DB2 set mykey to myval, DB2's mykey exists" ); +ok( !exists $db3->{mykey}, "After DB2 set mykey to myval, DB3's mykey doesn't exist" ); + +cmp_bag( [ keys %$db1 ], [qw( foo bar )], "DB1 keys correct" ); +cmp_bag( [ keys %$db2 ], [qw( foo bar mykey )], "DB2 keys correct" ); +cmp_bag( [ keys %$db3 ], [qw( foo )], "DB3 keys correct" ); + +$db1->commit; + +is( $db1->{foo}, 'bar2', "After DB1 commit, DB1's foo is bar2" ); +is( $db2->{foo}, 'bar333', "After DB1 commit, DB2's foo is bar333" ); +is( $db3->{foo}, 'bar2', "After DB1 commit, DB3's foo is bar2" ); + +is( $db1->{bar}, 'foo', "DB1's bar is still foo" ); +is( $db2->{bar}, 'mybar', "DB2's bar is still mybar" ); +is( $db3->{bar}, 'foo', "DB3's bar is now foo" ); + +cmp_bag( [ keys %$db1 ], [qw( foo bar )], "DB1 keys correct" ); +cmp_bag( [ keys %$db2 ], [qw( foo bar mykey )], "DB2 keys correct" ); +cmp_bag( [ keys %$db3 ], [qw( foo bar )], "DB3 keys correct" ); + +$db2->commit; + +is( $db1->{foo}, 'bar333', "After DB2 commit, DB1's foo is bar333" ); +is( $db2->{foo}, 'bar333', "After DB2 commit, DB2's foo is bar333" ); +is( $db3->{foo}, 'bar333', "After DB2 commit, DB3's foo is bar333" ); + +is( $db1->{bar}, 'mybar', "DB1's bar is now mybar" ); +is( $db2->{bar}, 'mybar', "DB2's bar is still mybar" ); +is( $db3->{bar}, 'mybar', "DB3's bar is now mybar" ); + +cmp_bag( [ keys %$db1 ], [qw( foo bar mykey )], "DB1 keys correct" ); +cmp_bag( [ keys %$db2 ], [qw( foo bar mykey )], "DB2 keys correct" ); +cmp_bag( [ keys %$db3 ], [qw( foo bar mykey )], "DB3 keys correct" );