All auditing now goes through a method on ::File
[dbsrgits/DBM-Deep.git] / lib / DBM / Deep / Engine.pm
index fa04d49..309c274 100644 (file)
@@ -6,6 +6,14 @@ use strict;
 use warnings;
 
 use Fcntl qw( :DEFAULT :flock :seek );
+use Scalar::Util ();
+
+# File-wide notes:
+# * All the local($/,$\); are to protect read() and print() from -l.
+# * To add to bucket_size, make sure you modify the following:
+#   - calculate_sizes()
+#   - _get_key_subloc()
+#   - add_bucket() - where the buckets are printed
 
 ##
 # Setup file and tag signatures.  These should never change.
@@ -44,12 +52,13 @@ sub new {
         max_buckets => 16,
 
         fileobj => undef,
+        obj     => undef,
     }, $class;
 
     if ( defined $args->{pack_size} ) {
         if ( lc $args->{pack_size} eq 'small' ) {
             $args->{long_size} = 2;
-            $args->{long_pack} = 'S';
+            $args->{long_pack} = 'n';
         }
         elsif ( lc $args->{pack_size} eq 'medium' ) {
             $args->{long_size} = 4;
@@ -69,6 +78,7 @@ sub new {
         next unless exists $args->{$param};
         $self->{$param} = $args->{$param};
     }
+    Scalar::Util::weaken( $self->{obj} ) if $self->{obj};
 
     if ( $self->{max_buckets} < 16 ) {
         warn "Floor of max_buckets is 16. Setting it to 16 from '$self->{max_buckets}'\n";
@@ -86,7 +96,6 @@ sub calculate_sizes {
 
     #XXX Does this need to be updated with different hashing algorithms?
     $self->{index_size}       = (2**8) * $self->{long_size};
-#ACID This needs modified - DONE
     $self->{bucket_size}      = $self->{hash_size} + $self->{long_size} * 3;
     $self->{bucket_list_size} = $self->{max_buckets} * $self->{bucket_size};
 
@@ -96,6 +105,8 @@ sub calculate_sizes {
 sub write_file_header {
     my $self = shift;
 
+    local($/,$\);
+
     my $fh = $self->_fh;
 
     my $loc = $self->_request_space( length( SIG_FILE ) + 21 );
@@ -105,20 +116,24 @@ sub write_file_header {
         SIG_HEADER,
         pack('N', 1),  # header version
         pack('N', 12), # header size
-        pack('N', 0),  # file version
-        pack('S', $self->{long_size}),
+        pack('N', 0),  # currently running transaction IDs
+        pack('n', $self->{long_size}),
         pack('A', $self->{long_pack}),
-        pack('S', $self->{data_size}),
+        pack('n', $self->{data_size}),
         pack('A', $self->{data_pack}),
-        pack('S', $self->{max_buckets}),
+        pack('n', $self->{max_buckets}),
     );
 
+    $self->_fileobj->set_transaction_offset( 13 );
+
     return;
 }
 
 sub read_file_header {
     my $self = shift;
 
+    local($/,$\);
+
     my $fh = $self->_fh;
 
     seek($fh, 0 + $self->_fileobj->{file_offset}, SEEK_SET);
@@ -132,20 +147,23 @@ sub read_file_header {
     );
 
     unless ( $file_signature eq SIG_FILE ) {
-        $self->{fileobj}->close;
+        $self->_fileobj->close;
         $self->_throw_error( "Signature not found -- file is not a Deep DB" );
     }
 
     unless ( $sig_header eq SIG_HEADER ) {
-        $self->{fileobj}->close;
+        $self->_fileobj->close;
         $self->_throw_error( "Old file version found." );
     }
 
     my $buffer2;
     $bytes_read += read( $fh, $buffer2, $size );
-    my ($file_version, @values) = unpack( 'N S A S A S', $buffer2 );
+    my ($running_transactions, @values) = unpack( 'N n A n A n', $buffer2 );
+
+    $self->_fileobj->set_transaction_offset( 13 );
+
     if ( @values < 5 || grep { !defined } @values ) {
-        $self->{fileobj}->close;
+        $self->_fileobj->close;
         $self->_throw_error("Corrupted file - bad header");
     }
 
@@ -159,6 +177,8 @@ sub setup_fh {
     my $self = shift;
     my ($obj) = @_;
 
+    local($/,$\);
+
     my $fh = $self->_fh;
     flock $fh, LOCK_EX;
 
@@ -172,6 +192,8 @@ sub setup_fh {
         # File is empty -- write header and master index
         ##
         if (!$bytes_read) {
+            $self->_fileobj->audit( "# Database created on" );
+
             $self->write_file_header;
 
             $obj->{base_offset} = $self->_request_space( $self->tag_size( $self->{index_size} ) );
@@ -192,10 +214,14 @@ sub setup_fh {
             ##
             # Get our type from master index header
             ##
-            my $tag = $self->load_tag($obj->_base_offset)
-                or $self->_throw_error("Corrupted file, no master index record");
+            my $tag = $self->load_tag($obj->_base_offset);
+            unless ( $tag ) {
+                flock $fh, LOCK_UN;
+                $self->_throw_error("Corrupted file, no master index record");
+            }
 
             unless ($obj->_type eq $tag->{signature}) {
+                flock $fh, LOCK_UN;
                 $self->_throw_error("File type mismatch");
             }
         }
@@ -230,6 +256,8 @@ sub write_tag {
     my ($offset, $sig, $content) = @_;
     my $size = length( $content );
 
+    local($/,$\);
+
     my $fh = $self->_fh;
 
     if ( defined $offset ) {
@@ -255,6 +283,8 @@ sub load_tag {
     my $self = shift;
     my ($offset) = @_;
 
+    local($/,$\);
+
 #    print join(':',map{$_||''}caller(1)), $/;
 
     my $fh = $self->_fh;
@@ -367,7 +397,10 @@ sub add_bucket {
     # plain (undigested) key and value.
     ##
     my $self = shift;
-    my ($tag, $md5, $plain_key, $value) = @_;
+    my ($tag, $md5, $plain_key, $value, $deleted, $orig_key) = @_;
+    $deleted ||= 0;
+
+    local($/,$\);
 
     # This verifies that only supported values will be stored.
     {
@@ -390,9 +423,14 @@ sub add_bucket {
 
     my $actual_length = $self->_length_needed( $value, $plain_key );
 
-    my ($subloc, $offset, $size) = $self->_find_in_buckets( $tag, $md5 );
+    #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 @transactions;
+    if ( $self->_fileobj->transaction_id == 0 ) {
+        @transactions = $self->_fileobj->current_transactions;
+    }
 
-    print "$subloc - $offset - $size\n";
 #    $self->_release_space( $size, $subloc );
     # Updating a known md5
 #XXX This needs updating to use _release_space
@@ -412,7 +450,7 @@ sub add_bucket {
             );
             print( $fh pack($self->{long_pack}, $location ) );
             print( $fh pack($self->{long_pack}, $actual_length ) );
-            print( $fh pack($self->{long_pack}, $root->transaction_id ) );
+            print( $fh pack('n n', $root->transaction_id, $deleted ) );
         }
     }
     # Adding a new md5
@@ -422,7 +460,14 @@ sub add_bucket {
         seek( $fh, $tag->{offset} + $offset + $root->{file_offset}, SEEK_SET );
         print( $fh $md5 . pack($self->{long_pack}, $location ) );
         print( $fh pack($self->{long_pack}, $actual_length ) );
-        print( $fh pack($self->{long_pack}, $root->transaction_id ) );
+        print( $fh pack('n n', $root->transaction_id, $deleted ) );
+
+        for ( @transactions ) {
+            my $tag2 = $self->load_tag( $tag->{offset} - SIG_SIZE - $self->{data_size} );
+            $self->_fileobj->{transaction_id} = $_;
+            $self->add_bucket( $tag2, $md5, '', '', 1, $orig_key );
+            $self->_fileobj->{transaction_id} = 0;
+        }
     }
     # If bucket didn't fit into list, split into a new index level
     # split_index() will do the _request_space() call
@@ -430,14 +475,16 @@ sub add_bucket {
         $location = $self->split_index( $md5, $tag );
     }
 
-    $self->write_value( $location, $plain_key, $value );
+    $self->write_value( $location, $plain_key, $value, $orig_key );
 
     return $result;
 }
 
 sub write_value {
     my $self = shift;
-    my ($location, $key, $value) = @_;
+    my ($location, $key, $value, $orig_key) = @_;
+
+    local($/,$\);
 
     my $fh = $self->_fh;
     my $root = $self->_fileobj;
@@ -511,6 +558,8 @@ sub write_value {
         tie %$value, 'DBM::Deep', {
             base_offset => $location,
             fileobj     => $root,
+            parent      => $self->{obj},
+            parent_key  => $orig_key,
         };
         %$value = %x;
     }
@@ -519,6 +568,8 @@ sub write_value {
         tie @$value, 'DBM::Deep', {
             base_offset => $location,
             fileobj     => $root,
+            parent      => $self->{obj},
+            parent_key  => $orig_key,
         };
         @$value = @x;
     }
@@ -530,6 +581,8 @@ sub split_index {
     my $self = shift;
     my ($md5, $tag) = @_;
 
+    local($/,$\);
+
     my $fh = $self->_fh;
     my $root = $self->_fileobj;
 
@@ -552,7 +605,7 @@ sub split_index {
     my $keys = $tag->{content}
              . $md5 . pack($self->{long_pack}, $newtag_loc)
                     . pack($self->{long_pack}, 0)  # size
-                    . pack($self->{long_pack}, 0); # transaction #
+                    . pack($self->{long_pack}, 0); # transaction ID
 
     my @newloc = ();
     BUCKET:
@@ -609,7 +662,9 @@ sub split_index {
 
 sub read_from_loc {
     my $self = shift;
-    my ($subloc) = @_;
+    my ($subloc, $orig_key) = @_;
+
+    local($/,$\);
 
     my $fh = $self->_fh;
 
@@ -625,9 +680,11 @@ sub read_from_loc {
     ##
     if (($signature eq SIG_HASH) || ($signature eq SIG_ARRAY)) {
         my $new_obj = DBM::Deep->new({
-            type => $signature,
+            type        => $signature,
             base_offset => $subloc,
             fileobj     => $self->_fileobj,
+            parent      => $self->{obj},
+            parent_key  => $orig_key,
         });
 
         if ($new_obj->_fileobj->{autobless}) {
@@ -668,7 +725,7 @@ sub read_from_loc {
             read( $fh, $new_loc, $size );
             $new_loc = unpack( $self->{long_pack}, $new_loc );
 
-            return $self->read_from_loc( $new_loc );
+            return $self->read_from_loc( $new_loc, $orig_key );
         }
         else {
             return;
@@ -698,11 +755,12 @@ sub get_bucket_value {
     # Fetch single value given tag and MD5 digested key.
     ##
     my $self = shift;
-    my ($tag, $md5) = @_;
+    my ($tag, $md5, $orig_key) = @_;
 
-    my ($subloc, $offset, $size) = $self->_find_in_buckets( $tag, $md5 );
-    if ( $subloc ) {
-        return $self->read_from_loc( $subloc );
+    #ACID - This is a read. Can find exact or HEAD
+    my ($subloc, $offset, $size,$is_deleted) = $self->_find_in_buckets( $tag, $md5 );
+    if ( $subloc && !$is_deleted ) {
+        return $self->read_from_loc( $subloc, $orig_key );
     }
     return;
 }
@@ -712,9 +770,12 @@ sub delete_bucket {
     # Delete single key/value pair given tag and MD5 digested key.
     ##
     my $self = shift;
-    my ($tag, $md5) = @_;
+    my ($tag, $md5, $orig_key) = @_;
 
-    my ($subloc, $offset, $size) = $self->_find_in_buckets( $tag, $md5 );
+    local($/,$\);
+
+    #ACID - This is a mutation. Must only find the exact transaction
+    my ($subloc, $offset, $size) = $self->_find_in_buckets( $tag, $md5, 1 );
 #XXX This needs _release_space()
     if ( $subloc ) {
         my $fh = $self->_fh;
@@ -734,8 +795,9 @@ sub bucket_exists {
     my $self = shift;
     my ($tag, $md5) = @_;
 
-    my ($subloc, $offset, $size) = $self->_find_in_buckets( $tag, $md5 );
-    return $subloc && 1;
+    #ACID - This is a read. Can find exact or HEAD
+    my ($subloc, $offset, $size, $is_deleted) = $self->_find_in_buckets( $tag, $md5 );
+    return ($subloc && !$is_deleted) && 1;
 }
 
 sub find_bucket_list {
@@ -746,6 +808,8 @@ sub find_bucket_list {
     my ($offset, $md5, $args) = @_;
     $args = {} unless $args;
 
+    local($/,$\);
+
     ##
     # Locate offset for bucket list using digest index system
     ##
@@ -816,6 +880,8 @@ sub traverse_index {
     my $self = shift;
     my ($obj, $offset, $ch, $force_return_next) = @_;
 
+    local($/,$\);
+
     my $tag = $self->load_tag( $offset );
 
     my $fh = $self->_fh;
@@ -919,15 +985,14 @@ sub get_next_key {
 
 # Utilities
 
-#ACID This needs modified - DONE
 sub _get_key_subloc {
     my $self = shift;
     my ($keys, $idx) = @_;
 
-    my ($key, $subloc, $size, $transaction) = unpack(
+    my ($key, $subloc, $size, $transaction_id, $is_deleted) = 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} $self->{long_pack} $self->{long_pack}",
+        "a$self->{hash_size} $self->{long_pack}2 n2",
         substr(
             $keys,
             ($idx * $self->{bucket_size}),
@@ -935,26 +1000,40 @@ sub _get_key_subloc {
         ),
     );
 
-    return ($key, $subloc, $size, $transaction);
+    return ($key, $subloc, $size, $transaction_id, $is_deleted);
 }
 
 sub _find_in_buckets {
     my $self = shift;
-    my ($tag, $md5) = @_;
+    my ($tag, $md5, $exact) = @_;
 
     my $trans_id = $self->_fileobj->transaction_id;
 
+    my @zero;
+
     BUCKET:
     for ( my $i = 0; $i < $self->{max_buckets}; $i++ ) {
-        my ($key, $subloc, $size, $transaction_id) = $self->_get_key_subloc(
+        my ($key, $subloc, $size, $transaction_id, $is_deleted) = $self->_get_key_subloc(
             $tag->{content}, $i,
         );
 
-        return ($subloc, $i * $self->{bucket_size}, $size) unless $subloc;
+        my @rv = ($subloc, $i * $self->{bucket_size}, $size, $is_deleted);
+
+        unless ( $subloc ) {
+            if ( !$exact && @zero and $trans_id ) {
+                @rv = ($zero[2], $zero[0] * $self->{bucket_size},$zero[3],$is_deleted);
+            }
+            return @rv;
+        }
+
+        next BUCKET if $key ne $md5;
 
-        next BUCKET if $key ne $md5 || $transaction_id != $trans_id;
+        # Save off the HEAD in case we need it.
+        @zero = ($i,$key,$subloc,$size,$transaction_id,$is_deleted) if $transaction_id == 0;
 
-        return ($subloc, $i * $self->{bucket_size}, $size);
+        next BUCKET if $transaction_id != $trans_id;
+
+        return @rv;
     }
 
     return;
@@ -974,6 +1053,8 @@ sub _release_space {
     my $self = shift;
     my ($size, $loc) = @_;
 
+    local($/,$\);
+
     my $next_loc = 0;
 
     my $fh = $self->_fh;
@@ -999,6 +1080,8 @@ sub _read_at {
     my $self = shift;
     my ($spot, $amount, $unpack) = @_;
 
+    local($/,$\);
+
     my $fh = $self->_fh;
     seek( $fh, $spot + $self->_fileobj->{file_offset}, SEEK_SET );
 
@@ -1021,6 +1104,8 @@ sub _print_at {
     my $self = shift;
     my ($spot, $data) = @_;
 
+    local($/,$\);
+
     my $fh = $self->_fh;
     seek( $fh, $spot, SEEK_SET );
     print( $fh $data );
@@ -1031,6 +1116,8 @@ sub _print_at {
 sub get_file_version {
     my $self = shift;
 
+    local($/,$\);
+
     my $fh = $self->_fh;
 
     seek( $fh, 13 + $self->_fileobj->{file_offset}, SEEK_SET );
@@ -1047,6 +1134,8 @@ sub write_file_version {
     my $self = shift;
     my ($new_version) = @_;
 
+    local($/,$\);
+
     my $fh = $self->_fh;
 
     seek( $fh, 13 + $self->_fileobj->{file_offset}, SEEK_SET );