7f716bf95b622812c23ddca14045cd2fd30c391c
[dbsrgits/DBM-Deep.git] / lib / DBM / Deep / Engine.pm
1 package DBM::Deep::Engine;
2
3 use 5.006_000;
4
5 use strict;
6 use warnings;
7
8 our $VERSION = q(1.0010);
9
10 # Never import symbols into our namespace. We are a class, not a library.
11 # -RobK, 2008-05-27
12 use Scalar::Util ();
13
14 #use Data::Dumper ();
15
16 # File-wide notes:
17 # * Every method in here assumes that the storage has been appropriately
18 #   safeguarded. This can be anything from flock() to some sort of manual
19 #   mutex. But, it's the caller's responsability to make sure that this has
20 #   been done.
21
22 # Setup file and tag signatures.  These should never change.
23 sub SIG_FILE     () { 'DPDB' }
24 sub SIG_HEADER   () { 'h'    }
25 sub SIG_HASH     () { 'H'    }
26 sub SIG_ARRAY    () { 'A'    }
27 sub SIG_NULL     () { 'N'    }
28 sub SIG_DATA     () { 'D'    }
29 sub SIG_INDEX    () { 'I'    }
30 sub SIG_BLIST    () { 'B'    }
31 sub SIG_FREE     () { 'F'    }
32 sub SIG_SIZE     () {  1     }
33
34 my $STALE_SIZE = 2;
35
36 # Please refer to the pack() documentation for further information
37 my %StP = (
38     1 => 'C', # Unsigned char value (no order needed as it's just one byte)
39     2 => 'n', # Unsigned short in "network" (big-endian) order
40     4 => 'N', # Unsigned long in "network" (big-endian) order
41     8 => 'Q', # Usigned quad (no order specified, presumably machine-dependent)
42 );
43
44 ################################################################################
45
46 sub new {
47     my $class = shift;
48     my ($args) = @_;
49
50     my $self = bless {
51         byte_size   => 4,
52
53         digest      => undef,
54         hash_size   => 16,  # In bytes
55         hash_chars  => 256, # Number of chars the algorithm uses per byte
56         max_buckets => 16,
57         num_txns    => 1,   # The HEAD
58         trans_id    => 0,   # Default to the HEAD
59
60         data_sector_size => 64, # Size in bytes of each data sector
61
62         entries => {}, # This is the list of entries for transactions
63         storage => undef,
64     }, $class;
65
66     # Never allow byte_size to be set directly.
67     delete $args->{byte_size};
68     if ( defined $args->{pack_size} ) {
69         if ( lc $args->{pack_size} eq 'small' ) {
70             $args->{byte_size} = 2;
71         }
72         elsif ( lc $args->{pack_size} eq 'medium' ) {
73             $args->{byte_size} = 4;
74         }
75         elsif ( lc $args->{pack_size} eq 'large' ) {
76             $args->{byte_size} = 8;
77         }
78         else {
79             DBM::Deep->_throw_error( "Unknown pack_size value: '$args->{pack_size}'" );
80         }
81     }
82
83     # Grab the parameters we want to use
84     foreach my $param ( keys %$self ) {
85         next unless exists $args->{$param};
86         $self->{$param} = $args->{$param};
87     }
88
89     my %validations = (
90         max_buckets      => { floor => 16, ceil => 256 },
91         num_txns         => { floor => 1,  ceil => 255 },
92         data_sector_size => { floor => 32, ceil => 256 },
93     );
94
95     while ( my ($attr, $c) = each %validations ) {
96         if (   !defined $self->{$attr}
97             || !length $self->{$attr}
98             || $self->{$attr} =~ /\D/
99             || $self->{$attr} < $c->{floor}
100         ) {
101             $self->{$attr} = '(undef)' if !defined $self->{$attr};
102             warn "Floor of $attr is $c->{floor}. Setting it to $c->{floor} from '$self->{$attr}'\n";
103             $self->{$attr} = $c->{floor};
104         }
105         elsif ( $self->{$attr} > $c->{ceil} ) {
106             warn "Ceiling of $attr is $c->{ceil}. Setting it to $c->{ceil} from '$self->{$attr}'\n";
107             $self->{$attr} = $c->{ceil};
108         }
109     }
110
111     if ( !$self->{digest} ) {
112         require Digest::MD5;
113         $self->{digest} = \&Digest::MD5::md5;
114     }
115
116     return $self;
117 }
118
119 ################################################################################
120
121 sub read_value {
122     my $self = shift;
123     my ($obj, $key) = @_;
124
125     # This will be a Reference sector
126     my $sector = $self->_load_sector( $obj->_base_offset )
127         or return;
128
129     if ( $sector->staleness != $obj->_staleness ) {
130         return;
131     }
132
133     my $key_md5 = $self->_apply_digest( $key );
134
135     my $value_sector = $sector->get_data_for({
136         key_md5    => $key_md5,
137         allow_head => 1,
138     });
139
140     unless ( $value_sector ) {
141         $value_sector = DBM::Deep::Engine::Sector::Null->new({
142             engine => $self,
143             data   => undef,
144         });
145
146         $sector->write_data({
147             key_md5 => $key_md5,
148             key     => $key,
149             value   => $value_sector,
150         });
151     }
152
153     return $value_sector->data;
154 }
155
156 sub get_classname {
157     my $self = shift;
158     my ($obj) = @_;
159
160     # This will be a Reference sector
161     my $sector = $self->_load_sector( $obj->_base_offset )
162         or DBM::Deep->_throw_error( "How did get_classname fail (no sector for '$obj')?!" );
163
164     if ( $sector->staleness != $obj->_staleness ) {
165         return;
166     }
167
168     return $sector->get_classname;
169 }
170
171 sub make_reference {
172     my $self = shift;
173     my ($obj, $old_key, $new_key) = @_;
174
175     # This will be a Reference sector
176     my $sector = $self->_load_sector( $obj->_base_offset )
177         or DBM::Deep->_throw_error( "How did get_classname fail (no sector for '$obj')?!" );
178
179     if ( $sector->staleness != $obj->_staleness ) {
180         return;
181     }
182
183     my $old_md5 = $self->_apply_digest( $old_key );
184
185     my $value_sector = $sector->get_data_for({
186         key_md5    => $old_md5,
187         allow_head => 1,
188     });
189
190     unless ( $value_sector ) {
191         $value_sector = DBM::Deep::Engine::Sector::Null->new({
192             engine => $self,
193             data   => undef,
194         });
195
196         $sector->write_data({
197             key_md5 => $old_md5,
198             key     => $old_key,
199             value   => $value_sector,
200         });
201     }
202
203     if ( $value_sector->isa( 'DBM::Deep::Engine::Sector::Reference' ) ) {
204         $sector->write_data({
205             key     => $new_key,
206             key_md5 => $self->_apply_digest( $new_key ),
207             value   => $value_sector,
208         });
209         $value_sector->increment_refcount;
210     }
211     else {
212         $sector->write_data({
213             key     => $new_key,
214             key_md5 => $self->_apply_digest( $new_key ),
215             value   => $value_sector->clone,
216         });
217     }
218 }
219
220 sub key_exists {
221     my $self = shift;
222     my ($obj, $key) = @_;
223
224     # This will be a Reference sector
225     my $sector = $self->_load_sector( $obj->_base_offset )
226         or return '';
227
228     if ( $sector->staleness != $obj->_staleness ) {
229         return '';
230     }
231
232     my $data = $sector->get_data_for({
233         key_md5    => $self->_apply_digest( $key ),
234         allow_head => 1,
235     });
236
237     # exists() returns 1 or '' for true/false.
238     return $data ? 1 : '';
239 }
240
241 sub delete_key {
242     my $self = shift;
243     my ($obj, $key) = @_;
244
245     my $sector = $self->_load_sector( $obj->_base_offset )
246         or return;
247
248     if ( $sector->staleness != $obj->_staleness ) {
249         return;
250     }
251
252     return $sector->delete_key({
253         key_md5    => $self->_apply_digest( $key ),
254         allow_head => 0,
255     });
256 }
257
258 sub write_value {
259     my $self = shift;
260     my ($obj, $key, $value) = @_;
261
262     my $r = Scalar::Util::reftype( $value ) || '';
263     {
264         last if $r eq '';
265         last if $r eq 'HASH';
266         last if $r eq 'ARRAY';
267
268         DBM::Deep->_throw_error(
269             "Storage of references of type '$r' is not supported."
270         );
271     }
272
273     # This will be a Reference sector
274     my $sector = $self->_load_sector( $obj->_base_offset )
275         or DBM::Deep->_throw_error( "Cannot write to a deleted spot in DBM::Deep." );
276
277     if ( $sector->staleness != $obj->_staleness ) {
278         DBM::Deep->_throw_error( "Cannot write to a deleted spot in DBM::Deep." );
279     }
280
281     my ($class, $type);
282     if ( !defined $value ) {
283         $class = 'DBM::Deep::Engine::Sector::Null';
284     }
285     elsif ( $r eq 'ARRAY' || $r eq 'HASH' ) {
286         my $tmpvar;
287         if ( $r eq 'ARRAY' ) {
288             $tmpvar = tied @$value;
289         } elsif ( $r eq 'HASH' ) {
290             $tmpvar = tied %$value;
291         }
292
293         my $is_dbm_deep = eval { local $SIG{'__DIE__'}; $tmpvar->isa( 'DBM::Deep' ); };
294         if ( $is_dbm_deep ) {
295             unless ( $tmpvar->_engine->storage == $self->storage ) {
296                 DBM::Deep->_throw_error( "Cannot store values across DBM::Deep files. Please use export() instead." );
297             }
298
299             # First, verify if we're storing the same thing to this spot. If we are, then
300             # this should be a no-op. -EJS, 2008-05-19
301             my $loc = $sector->get_data_location_for({
302                 key_md5 => $self->_apply_digest( $key ),
303                 allow_head => 1,
304             });
305
306             if ( defined($loc) && $loc == $tmpvar->_base_offset ) {
307                 return 1;
308             }
309
310             my $value_sector = $self->_load_sector( $tmpvar->_base_offset );
311             $sector->write_data({
312                 key     => $key,
313                 key_md5 => $self->_apply_digest( $key ),
314                 value   => $value_sector,
315             });
316             $value_sector->increment_refcount;
317
318             return 1;
319         }
320         if ( $r eq 'ARRAY' && tied(@$value) ) {
321             DBM::Deep->_throw_error( "Cannot store something that is tied." );
322         }
323         if ( $r eq 'HASH' && tied(%$value) ) {
324             DBM::Deep->_throw_error( "Cannot store something that is tied." );
325         }
326         $class = 'DBM::Deep::Engine::Sector::Reference';
327         $type = substr( $r, 0, 1 );
328     }
329     else {
330         if ( tied($value) ) {
331             DBM::Deep->_throw_error( "Cannot store something that is tied." );
332         }
333         $class = 'DBM::Deep::Engine::Sector::Scalar';
334     }
335
336     # Create this after loading the reference sector in case something bad happens.
337     # This way, we won't allocate value sector(s) needlessly.
338     my $value_sector = $class->new({
339         engine => $self,
340         data   => $value,
341         type   => $type,
342     });
343
344     $sector->write_data({
345         key     => $key,
346         key_md5 => $self->_apply_digest( $key ),
347         value   => $value_sector,
348     });
349
350     # This code is to make sure we write all the values in the $value to the disk
351     # and to make sure all changes to $value after the assignment are reflected
352     # on disk. This may be counter-intuitive at first, but it is correct dwimmery.
353     #   NOTE - simply tying $value won't perform a STORE on each value. Hence, the
354     # copy to a temp value.
355     if ( $r eq 'ARRAY' ) {
356         my @temp = @$value;
357         tie @$value, 'DBM::Deep', {
358             base_offset => $value_sector->offset,
359             staleness   => $value_sector->staleness,
360             storage     => $self->storage,
361             engine      => $self,
362         };
363         @$value = @temp;
364         bless $value, 'DBM::Deep::Array' unless Scalar::Util::blessed( $value );
365     }
366     elsif ( $r eq 'HASH' ) {
367         my %temp = %$value;
368         tie %$value, 'DBM::Deep', {
369             base_offset => $value_sector->offset,
370             staleness   => $value_sector->staleness,
371             storage     => $self->storage,
372             engine      => $self,
373         };
374
375         %$value = %temp;
376         bless $value, 'DBM::Deep::Hash' unless Scalar::Util::blessed( $value );
377     }
378
379     return 1;
380 }
381
382 # XXX Add staleness here
383 sub get_next_key {
384     my $self = shift;
385     my ($obj, $prev_key) = @_;
386
387     # XXX Need to add logic about resetting the iterator if any key in the reference has changed
388     unless ( $prev_key ) {
389         $obj->{iterator} = DBM::Deep::Iterator->new({
390             base_offset => $obj->_base_offset,
391             engine      => $self,
392         });
393     }
394
395     return $obj->{iterator}->get_next_key( $obj );
396 }
397
398 ################################################################################
399
400 sub setup_fh {
401     my $self = shift;
402     my ($obj) = @_;
403
404     # We're opening the file.
405     unless ( $obj->_base_offset ) {
406         my $bytes_read = $self->_read_file_header;
407
408         # Creating a new file
409         unless ( $bytes_read ) {
410             $self->_write_file_header;
411
412             # 1) Create Array/Hash entry
413             my $initial_reference = DBM::Deep::Engine::Sector::Reference->new({
414                 engine => $self,
415                 type   => $obj->_type,
416             });
417             $obj->{base_offset} = $initial_reference->offset;
418             $obj->{staleness} = $initial_reference->staleness;
419
420             $self->storage->flush;
421         }
422         # Reading from an existing file
423         else {
424             $obj->{base_offset} = $bytes_read;
425             my $initial_reference = DBM::Deep::Engine::Sector::Reference->new({
426                 engine => $self,
427                 offset => $obj->_base_offset,
428             });
429             unless ( $initial_reference ) {
430                 DBM::Deep->_throw_error("Corrupted file, no master index record");
431             }
432
433             unless ($obj->_type eq $initial_reference->type) {
434                 DBM::Deep->_throw_error("File type mismatch");
435             }
436
437             $obj->{staleness} = $initial_reference->staleness;
438         }
439     }
440
441     return 1;
442 }
443
444 sub begin_work {
445     my $self = shift;
446     my ($obj) = @_;
447
448     if ( $self->trans_id ) {
449         DBM::Deep->_throw_error( "Cannot begin_work within an active transaction" );
450     }
451
452     my @slots = $self->read_txn_slots;
453     my $found;
454     for my $i ( 0 .. $#slots ) {
455         next if $slots[$i];
456
457         $slots[$i] = 1;
458         $self->set_trans_id( $i + 1 );
459         $found = 1;
460         last;
461     }
462     unless ( $found ) {
463         DBM::Deep->_throw_error( "Cannot allocate transaction ID" );
464     }
465     $self->write_txn_slots( @slots );
466
467     if ( !$self->trans_id ) {
468         DBM::Deep->_throw_error( "Cannot begin_work - no available transactions" );
469     }
470
471     return;
472 }
473
474 sub rollback {
475     my $self = shift;
476     my ($obj) = @_;
477
478     if ( !$self->trans_id ) {
479         DBM::Deep->_throw_error( "Cannot rollback without an active transaction" );
480     }
481
482     # Each entry is the file location for a bucket that has a modification for
483     # this transaction. The entries need to be expunged.
484     foreach my $entry (@{ $self->get_entries } ) {
485         # Remove the entry here
486         my $read_loc = $entry
487           + $self->hash_size
488           + $self->byte_size
489           + $self->byte_size
490           + ($self->trans_id - 1) * ( $self->byte_size + $STALE_SIZE );
491
492         my $data_loc = $self->storage->read_at( $read_loc, $self->byte_size );
493         $data_loc = unpack( $StP{$self->byte_size}, $data_loc );
494         $self->storage->print_at( $read_loc, pack( $StP{$self->byte_size}, 0 ) );
495
496         if ( $data_loc > 1 ) {
497             $self->_load_sector( $data_loc )->free;
498         }
499     }
500
501     $self->clear_entries;
502
503     my @slots = $self->read_txn_slots;
504     $slots[$self->trans_id-1] = 0;
505     $self->write_txn_slots( @slots );
506     $self->inc_txn_staleness_counter( $self->trans_id );
507     $self->set_trans_id( 0 );
508
509     return 1;
510 }
511
512 sub commit {
513     my $self = shift;
514     my ($obj) = @_;
515
516     if ( !$self->trans_id ) {
517         DBM::Deep->_throw_error( "Cannot commit without an active transaction" );
518     }
519
520     foreach my $entry (@{ $self->get_entries } ) {
521         # Overwrite the entry in head with the entry in trans_id
522         my $base = $entry
523           + $self->hash_size
524           + $self->byte_size;
525
526         my $head_loc = $self->storage->read_at( $base, $self->byte_size );
527         $head_loc = unpack( $StP{$self->byte_size}, $head_loc );
528
529         my $spot = $base + $self->byte_size + ($self->trans_id - 1) * ( $self->byte_size + $STALE_SIZE );
530         my $trans_loc = $self->storage->read_at(
531             $spot, $self->byte_size,
532         );
533
534         $self->storage->print_at( $base, $trans_loc );
535         $self->storage->print_at(
536             $spot,
537             pack( $StP{$self->byte_size} . ' ' . $StP{$STALE_SIZE}, (0) x 2 ),
538         );
539
540         if ( $head_loc > 1 ) {
541             $self->_load_sector( $head_loc )->free;
542         }
543     }
544
545     $self->clear_entries;
546
547     my @slots = $self->read_txn_slots;
548     $slots[$self->trans_id-1] = 0;
549     $self->write_txn_slots( @slots );
550     $self->inc_txn_staleness_counter( $self->trans_id );
551     $self->set_trans_id( 0 );
552
553     return 1;
554 }
555
556 sub read_txn_slots {
557     my $self = shift;
558     my $bl = $self->txn_bitfield_len;
559     my $num_bits = $bl * 8;
560     return split '', unpack( 'b'.$num_bits,
561         $self->storage->read_at(
562             $self->trans_loc, $bl,
563         )
564     );
565 }
566
567 sub write_txn_slots {
568     my $self = shift;
569     my $num_bits = $self->txn_bitfield_len * 8;
570     $self->storage->print_at( $self->trans_loc,
571         pack( 'b'.$num_bits, join('', @_) ),
572     );
573 }
574
575 sub get_running_txn_ids {
576     my $self = shift;
577     my @transactions = $self->read_txn_slots;
578     my @trans_ids = map { $_+1} grep { $transactions[$_] } 0 .. $#transactions;
579 }
580
581 sub get_txn_staleness_counter {
582     my $self = shift;
583     my ($trans_id) = @_;
584
585     # Hardcode staleness of 0 for the HEAD
586     return 0 unless $trans_id;
587
588     return unpack( $StP{$STALE_SIZE},
589         $self->storage->read_at(
590             $self->trans_loc + $self->txn_bitfield_len + $STALE_SIZE * ($trans_id - 1),
591             $STALE_SIZE,
592         )
593     );
594 }
595
596 sub inc_txn_staleness_counter {
597     my $self = shift;
598     my ($trans_id) = @_;
599
600     # Hardcode staleness of 0 for the HEAD
601     return 0 unless $trans_id;
602
603     $self->storage->print_at(
604         $self->trans_loc + $self->txn_bitfield_len + $STALE_SIZE * ($trans_id - 1),
605         pack( $StP{$STALE_SIZE}, $self->get_txn_staleness_counter( $trans_id ) + 1 ),
606     );
607 }
608
609 sub get_entries {
610     my $self = shift;
611     return [ keys %{ $self->{entries}{$self->trans_id} ||= {} } ];
612 }
613
614 sub add_entry {
615     my $self = shift;
616     my ($trans_id, $loc) = @_;
617
618     $self->{entries}{$trans_id} ||= {};
619     $self->{entries}{$trans_id}{$loc} = undef;
620 }
621
622 # If the buckets are being relocated because of a reindexing, the entries
623 # mechanism needs to be made aware of it.
624 sub reindex_entry {
625     my $self = shift;
626     my ($old_loc, $new_loc) = @_;
627
628     TRANS:
629     while ( my ($trans_id, $locs) = each %{ $self->{entries} } ) {
630         foreach my $orig_loc ( keys %{ $locs } ) {
631             if ( $orig_loc == $old_loc ) {
632                 delete $locs->{orig_loc};
633                 $locs->{$new_loc} = undef;
634                 next TRANS;
635             }
636         }
637     }
638 }
639
640 sub clear_entries {
641     my $self = shift;
642     delete $self->{entries}{$self->trans_id};
643 }
644
645 ################################################################################
646
647 {
648     my $header_fixed = length( SIG_FILE ) + 1 + 4 + 4;
649     my $this_file_version = 3;
650
651     sub _write_file_header {
652         my $self = shift;
653
654         my $nt = $self->num_txns;
655         my $bl = $self->txn_bitfield_len;
656
657         my $header_var = 1 + 1 + 1 + 1 + $bl + $STALE_SIZE * ($nt - 1) + 3 * $self->byte_size;
658
659         my $loc = $self->storage->request_space( $header_fixed + $header_var );
660
661         $self->storage->print_at( $loc,
662             SIG_FILE,
663             SIG_HEADER,
664             pack('N', $this_file_version), # At this point, we're at 9 bytes
665             pack('N', $header_var),        # header size
666             # --- Above is $header_fixed. Below is $header_var
667             pack('C', $self->byte_size),
668
669             # These shenanigans are to allow a 256 within a C
670             pack('C', $self->max_buckets - 1),
671             pack('C', $self->data_sector_size - 1),
672
673             pack('C', $nt),
674             pack('C' . $bl, 0 ),                           # Transaction activeness bitfield
675             pack($StP{$STALE_SIZE}.($nt-1), 0 x ($nt-1) ), # Transaction staleness counters
676             pack($StP{$self->byte_size}, 0), # Start of free chain (blist size)
677             pack($StP{$self->byte_size}, 0), # Start of free chain (data size)
678             pack($StP{$self->byte_size}, 0), # Start of free chain (index size)
679         );
680
681         #XXX Set these less fragilely
682         $self->set_trans_loc( $header_fixed + 4 );
683         $self->set_chains_loc( $header_fixed + 4 + $bl + $STALE_SIZE * ($nt-1) );
684
685         return;
686     }
687
688     sub _read_file_header {
689         my $self = shift;
690
691         my $buffer = $self->storage->read_at( 0, $header_fixed );
692         return unless length($buffer);
693
694         my ($file_signature, $sig_header, $file_version, $size) = unpack(
695             'A4 A N N', $buffer
696         );
697
698         unless ( $file_signature eq SIG_FILE ) {
699             $self->storage->close;
700             DBM::Deep->_throw_error( "Signature not found -- file is not a Deep DB" );
701         }
702
703         unless ( $sig_header eq SIG_HEADER ) {
704             $self->storage->close;
705             DBM::Deep->_throw_error( "Pre-1.00 file version found" );
706         }
707
708         unless ( $file_version == $this_file_version ) {
709             $self->storage->close;
710             DBM::Deep->_throw_error(
711                 "Wrong file version found - " .  $file_version .
712                 " - expected " . $this_file_version
713             );
714         }
715
716         my $buffer2 = $self->storage->read_at( undef, $size );
717         my @values = unpack( 'C C C C', $buffer2 );
718
719         if ( @values != 4 || grep { !defined } @values ) {
720             $self->storage->close;
721             DBM::Deep->_throw_error("Corrupted file - bad header");
722         }
723
724         #XXX Add warnings if values weren't set right
725         @{$self}{qw(byte_size max_buckets data_sector_size num_txns)} = @values;
726
727         # These shenangians are to allow a 256 within a C
728         $self->{max_buckets} += 1;
729         $self->{data_sector_size} += 1;
730
731         my $bl = $self->txn_bitfield_len;
732
733         my $header_var = scalar(@values) + $bl + $STALE_SIZE * ($self->num_txns - 1) + 3 * $self->byte_size;
734         unless ( $size == $header_var ) {
735             $self->storage->close;
736             DBM::Deep->_throw_error( "Unexpected size found ($size <-> $header_var)." );
737         }
738
739         $self->set_trans_loc( $header_fixed + scalar(@values) );
740         $self->set_chains_loc( $header_fixed + scalar(@values) + $bl + $STALE_SIZE * ($self->num_txns - 1) );
741
742         return length($buffer) + length($buffer2);
743     }
744 }
745
746 sub _load_sector {
747     my $self = shift;
748     my ($offset) = @_;
749
750     # Add a catch for offset of 0 or 1
751     return if !$offset || $offset <= 1;
752
753     my $type = $self->storage->read_at( $offset, 1 );
754     return if $type eq chr(0);
755
756     if ( $type eq $self->SIG_ARRAY || $type eq $self->SIG_HASH ) {
757         return DBM::Deep::Engine::Sector::Reference->new({
758             engine => $self,
759             type   => $type,
760             offset => $offset,
761         });
762     }
763     # XXX Don't we need key_md5 here?
764     elsif ( $type eq $self->SIG_BLIST ) {
765         return DBM::Deep::Engine::Sector::BucketList->new({
766             engine => $self,
767             type   => $type,
768             offset => $offset,
769         });
770     }
771     elsif ( $type eq $self->SIG_INDEX ) {
772         return DBM::Deep::Engine::Sector::Index->new({
773             engine => $self,
774             type   => $type,
775             offset => $offset,
776         });
777     }
778     elsif ( $type eq $self->SIG_NULL ) {
779         return DBM::Deep::Engine::Sector::Null->new({
780             engine => $self,
781             type   => $type,
782             offset => $offset,
783         });
784     }
785     elsif ( $type eq $self->SIG_DATA ) {
786         return DBM::Deep::Engine::Sector::Scalar->new({
787             engine => $self,
788             type   => $type,
789             offset => $offset,
790         });
791     }
792     # This was deleted from under us, so just return and let the caller figure it out.
793     elsif ( $type eq $self->SIG_FREE ) {
794         return;
795     }
796
797     DBM::Deep->_throw_error( "'$offset': Don't know what to do with type '$type'" );
798 }
799
800 sub _apply_digest {
801     my $self = shift;
802     return $self->{digest}->(@_);
803 }
804
805 sub _add_free_blist_sector { shift->_add_free_sector( 0, @_ ) }
806 sub _add_free_data_sector { shift->_add_free_sector( 1, @_ ) }
807 sub _add_free_index_sector { shift->_add_free_sector( 2, @_ ) }
808
809 sub _add_free_sector {
810     my $self = shift;
811     my ($multiple, $offset, $size) = @_;
812
813     my $chains_offset = $multiple * $self->byte_size;
814
815     my $storage = $self->storage;
816
817     # Increment staleness.
818     # XXX Can this increment+modulo be done by "&= 0x1" ?
819     my $staleness = unpack( $StP{$STALE_SIZE}, $storage->read_at( $offset + SIG_SIZE, $STALE_SIZE ) );
820     $staleness = ($staleness + 1 ) % ( 2 ** ( 8 * $STALE_SIZE ) );
821     $storage->print_at( $offset + SIG_SIZE, pack( $StP{$STALE_SIZE}, $staleness ) );
822
823     my $old_head = $storage->read_at( $self->chains_loc + $chains_offset, $self->byte_size );
824
825     $storage->print_at( $self->chains_loc + $chains_offset,
826         pack( $StP{$self->byte_size}, $offset ),
827     );
828
829     # Record the old head in the new sector after the signature and staleness counter
830     $storage->print_at( $offset + SIG_SIZE + $STALE_SIZE, $old_head );
831 }
832
833 sub _request_blist_sector { shift->_request_sector( 0, @_ ) }
834 sub _request_data_sector { shift->_request_sector( 1, @_ ) }
835 sub _request_index_sector { shift->_request_sector( 2, @_ ) }
836
837 sub _request_sector {
838     my $self = shift;
839     my ($multiple, $size) = @_;
840
841     my $chains_offset = $multiple * $self->byte_size;
842
843     my $old_head = $self->storage->read_at( $self->chains_loc + $chains_offset, $self->byte_size );
844     my $loc = unpack( $StP{$self->byte_size}, $old_head );
845
846     # We don't have any free sectors of the right size, so allocate a new one.
847     unless ( $loc ) {
848         my $offset = $self->storage->request_space( $size );
849
850         # Zero out the new sector. This also guarantees correct increases
851         # in the filesize.
852         $self->storage->print_at( $offset, chr(0) x $size );
853
854         return $offset;
855     }
856
857     # Read the new head after the signature and the staleness counter
858     my $new_head = $self->storage->read_at( $loc + SIG_SIZE + $STALE_SIZE, $self->byte_size );
859     $self->storage->print_at( $self->chains_loc + $chains_offset, $new_head );
860     $self->storage->print_at(
861         $loc + SIG_SIZE + $STALE_SIZE,
862         pack( $StP{$self->byte_size}, 0 ),
863     );
864
865     return $loc;
866 }
867
868 ################################################################################
869
870 sub storage     { $_[0]{storage} }
871 sub byte_size   { $_[0]{byte_size} }
872 sub hash_size   { $_[0]{hash_size} }
873 sub hash_chars  { $_[0]{hash_chars} }
874 sub num_txns    { $_[0]{num_txns} }
875 sub max_buckets { $_[0]{max_buckets} }
876 sub blank_md5   { chr(0) x $_[0]->hash_size }
877 sub data_sector_size { $_[0]{data_sector_size} }
878
879 # This is a calculated value
880 sub txn_bitfield_len {
881     my $self = shift;
882     unless ( exists $self->{txn_bitfield_len} ) {
883         my $temp = ($self->num_txns) / 8;
884         if ( $temp > int( $temp ) ) {
885             $temp = int( $temp ) + 1;
886         }
887         $self->{txn_bitfield_len} = $temp;
888     }
889     return $self->{txn_bitfield_len};
890 }
891
892 sub trans_id     { $_[0]{trans_id} }
893 sub set_trans_id { $_[0]{trans_id} = $_[1] }
894
895 sub trans_loc     { $_[0]{trans_loc} }
896 sub set_trans_loc { $_[0]{trans_loc} = $_[1] }
897
898 sub chains_loc     { $_[0]{chains_loc} }
899 sub set_chains_loc { $_[0]{chains_loc} = $_[1] }
900
901 sub cache       { $_[0]{cache} ||= {} }
902 sub clear_cache { %{$_[0]->cache} = () }
903
904 sub _dump_file {
905     my $self = shift;
906
907     # Read the header
908     my $spot = $self->_read_file_header();
909
910     my %types = (
911         0 => 'B',
912         1 => 'D',
913         2 => 'I',
914     );
915
916     my %sizes = (
917         'D' => $self->data_sector_size,
918         'B' => DBM::Deep::Engine::Sector::BucketList->new({engine=>$self,offset=>1})->size,
919         'I' => DBM::Deep::Engine::Sector::Index->new({engine=>$self,offset=>1})->size,
920     );
921
922     my $return = "";
923
924     # Header values
925     $return .= "NumTxns: " . $self->num_txns . $/;
926
927     # Read the free sector chains
928     my %sectors;
929     foreach my $multiple ( 0 .. 2 ) {
930         $return .= "Chains($types{$multiple}):";
931         my $old_loc = $self->chains_loc + $multiple * $self->byte_size;
932         while ( 1 ) {
933             my $loc = unpack(
934                 $StP{$self->byte_size},
935                 $self->storage->read_at( $old_loc, $self->byte_size ),
936             );
937
938             # We're now out of free sectors of this kind.
939             unless ( $loc ) {
940                 last;
941             }
942
943             $sectors{ $types{$multiple} }{ $loc } = undef;
944             $old_loc = $loc + SIG_SIZE + $STALE_SIZE;
945             $return .= " $loc";
946         }
947         $return .= $/;
948     }
949
950     SECTOR:
951     while ( $spot < $self->storage->{end} ) {
952         # Read each sector in order.
953         my $sector = $self->_load_sector( $spot );
954         if ( !$sector ) {
955             # Find it in the free-sectors that were found already
956             foreach my $type ( keys %sectors ) {
957                 if ( exists $sectors{$type}{$spot} ) {
958                     my $size = $sizes{$type};
959                     $return .= sprintf "%08d: %s %04d\n", $spot, 'F' . $type, $size;
960                     $spot += $size;
961                     next SECTOR;
962                 }
963             }
964
965             die "********\n$return\nDidn't find free sector for $spot in chains\n********\n";
966         }
967         else {
968             $return .= sprintf "%08d: %s  %04d", $spot, $sector->type, $sector->size;
969             if ( $sector->type eq 'D' ) {
970                 $return .= ' ' . $sector->data;
971             }
972             elsif ( $sector->type eq 'A' || $sector->type eq 'H' ) {
973                 $return .= ' REF: ' . $sector->get_refcount;
974             }
975             elsif ( $sector->type eq 'B' ) {
976                 foreach my $bucket ( $sector->chopped_up ) {
977                     $return .= "\n    ";
978                     $return .= sprintf "%08d", unpack($StP{$self->byte_size},
979                         substr( $bucket->[-1], $self->hash_size, $self->byte_size),
980                     );
981                     my $l = unpack( $StP{$self->byte_size},
982                         substr( $bucket->[-1],
983                             $self->hash_size + $self->byte_size,
984                             $self->byte_size,
985                         ),
986                     );
987                     $return .= sprintf " %08d", $l;
988                     foreach my $txn ( 0 .. $self->num_txns - 2 ) {
989                         my $l = unpack( $StP{$self->byte_size},
990                             substr( $bucket->[-1],
991                                 $self->hash_size + 2 * $self->byte_size + $txn * ($self->byte_size + $STALE_SIZE),
992                                 $self->byte_size,
993                             ),
994                         );
995                         $return .= sprintf " %08d", $l;
996                     }
997                 }
998             }
999             $return .= $/;
1000
1001             $spot += $sector->size;
1002         }
1003     }
1004
1005     return $return;
1006 }
1007
1008 ################################################################################
1009
1010 package DBM::Deep::Iterator;
1011
1012 sub new {
1013     my $class = shift;
1014     my ($args) = @_;
1015
1016     my $self = bless {
1017         breadcrumbs => [],
1018         engine      => $args->{engine},
1019         base_offset => $args->{base_offset},
1020     }, $class;
1021
1022     Scalar::Util::weaken( $self->{engine} );
1023
1024     return $self;
1025 }
1026
1027 sub reset { $_[0]{breadcrumbs} = [] }
1028
1029 sub get_sector_iterator {
1030     my $self = shift;
1031     my ($loc) = @_;
1032
1033     my $sector = $self->{engine}->_load_sector( $loc )
1034         or return;
1035
1036     if ( $sector->isa( 'DBM::Deep::Engine::Sector::Index' ) ) {
1037         return DBM::Deep::Iterator::Index->new({
1038             iterator => $self,
1039             sector   => $sector,
1040         });
1041     }
1042     elsif ( $sector->isa( 'DBM::Deep::Engine::Sector::BucketList' ) ) {
1043         return DBM::Deep::Iterator::BucketList->new({
1044             iterator => $self,
1045             sector   => $sector,
1046         });
1047     }
1048
1049     DBM::Deep->_throw_error( "get_sector_iterator(): Why did $loc make a $sector?" );
1050 }
1051
1052 sub get_next_key {
1053     my $self = shift;
1054     my ($obj) = @_;
1055
1056     my $crumbs = $self->{breadcrumbs};
1057     my $e = $self->{engine};
1058
1059     unless ( @$crumbs ) {
1060         # This will be a Reference sector
1061         my $sector = $e->_load_sector( $self->{base_offset} )
1062             # If no sector is found, thist must have been deleted from under us.
1063             or return;
1064
1065         if ( $sector->staleness != $obj->_staleness ) {
1066             return;
1067         }
1068
1069         my $loc = $sector->get_blist_loc
1070             or return;
1071
1072         push @$crumbs, $self->get_sector_iterator( $loc );
1073     }
1074
1075     FIND_NEXT_KEY: {
1076         # We're at the end.
1077         unless ( @$crumbs ) {
1078             $self->reset;
1079             return;
1080         }
1081
1082         my $iterator = $crumbs->[-1];
1083
1084         # This level is done.
1085         if ( $iterator->at_end ) {
1086             pop @$crumbs;
1087             redo FIND_NEXT_KEY;
1088         }
1089
1090         if ( $iterator->isa( 'DBM::Deep::Iterator::Index' ) ) {
1091             # If we don't have any more, it will be caught at the
1092             # prior check.
1093             if ( my $next = $iterator->get_next_iterator ) {
1094                 push @$crumbs, $next;
1095             }
1096             redo FIND_NEXT_KEY;
1097         }
1098
1099         unless ( $iterator->isa( 'DBM::Deep::Iterator::BucketList' ) ) {
1100             DBM::Deep->_throw_error(
1101                 "Should have a bucketlist iterator here - instead have $iterator"
1102             );
1103         }
1104
1105         # At this point, we have a BucketList iterator
1106         my $key = $iterator->get_next_key;
1107         if ( defined $key ) {
1108             return $key;
1109         }
1110         #XXX else { $iterator->set_to_end() } ?
1111
1112         # We hit the end of the bucketlist iterator, so redo
1113         redo FIND_NEXT_KEY;
1114     }
1115
1116     DBM::Deep->_throw_error( "get_next_key(): How did we get here?" );
1117 }
1118
1119 package DBM::Deep::Iterator::Index;
1120
1121 sub new {
1122     my $self = bless $_[1] => $_[0];
1123     $self->{curr_index} = 0;
1124     return $self;
1125 }
1126
1127 sub at_end {
1128     my $self = shift;
1129     return $self->{curr_index} >= $self->{iterator}{engine}->hash_chars;
1130 }
1131
1132 sub get_next_iterator {
1133     my $self = shift;
1134
1135     my $loc;
1136     while ( !$loc ) {
1137         return if $self->at_end;
1138         $loc = $self->{sector}->get_entry( $self->{curr_index}++ );
1139     }
1140
1141     return $self->{iterator}->get_sector_iterator( $loc );
1142 }
1143
1144 package DBM::Deep::Iterator::BucketList;
1145
1146 sub new {
1147     my $self = bless $_[1] => $_[0];
1148     $self->{curr_index} = 0;
1149     return $self;
1150 }
1151
1152 sub at_end {
1153     my $self = shift;
1154     return $self->{curr_index} >= $self->{iterator}{engine}->max_buckets;
1155 }
1156
1157 sub get_next_key {
1158     my $self = shift;
1159
1160     return if $self->at_end;
1161
1162     my $idx = $self->{curr_index}++;
1163
1164     my $data_loc = $self->{sector}->get_data_location_for({
1165         allow_head => 1,
1166         idx        => $idx,
1167     }) or return;
1168
1169     #XXX Do we want to add corruption checks here?
1170     return $self->{sector}->get_key_for( $idx )->data;
1171 }
1172
1173 package DBM::Deep::Engine::Sector;
1174
1175 sub new {
1176     my $self = bless $_[1], $_[0];
1177     Scalar::Util::weaken( $self->{engine} );
1178     $self->_init;
1179     return $self;
1180 }
1181
1182 #sub _init {}
1183 #sub clone { DBM::Deep->_throw_error( "Must be implemented in the child class" ); }
1184
1185 sub engine { $_[0]{engine} }
1186 sub offset { $_[0]{offset} }
1187 sub type   { $_[0]{type} }
1188
1189 sub base_size {
1190    my $self = shift;
1191    return $self->engine->SIG_SIZE + $STALE_SIZE;
1192 }
1193
1194 sub free {
1195     my $self = shift;
1196
1197     my $e = $self->engine;
1198
1199     $e->storage->print_at( $self->offset, $e->SIG_FREE );
1200     # Skip staleness counter
1201     $e->storage->print_at( $self->offset + $self->base_size,
1202         chr(0) x ($self->size - $self->base_size),
1203     );
1204
1205     my $free_meth = $self->free_meth;
1206     $e->$free_meth( $self->offset, $self->size );
1207
1208     return;
1209 }
1210
1211 package DBM::Deep::Engine::Sector::Data;
1212
1213 our @ISA = qw( DBM::Deep::Engine::Sector );
1214
1215 # This is in bytes
1216 sub size { $_[0]{engine}->data_sector_size }
1217 sub free_meth { return '_add_free_data_sector' }
1218
1219 sub clone {
1220     my $self = shift;
1221     return ref($self)->new({
1222         engine => $self->engine,
1223         type   => $self->type,
1224         data   => $self->data,
1225     });
1226 }
1227
1228 package DBM::Deep::Engine::Sector::Scalar;
1229
1230 our @ISA = qw( DBM::Deep::Engine::Sector::Data );
1231
1232 sub free {
1233     my $self = shift;
1234
1235     my $chain_loc = $self->chain_loc;
1236
1237     $self->SUPER::free();
1238
1239     if ( $chain_loc ) {
1240         $self->engine->_load_sector( $chain_loc )->free;
1241     }
1242
1243     return;
1244 }
1245
1246 sub type { $_[0]{engine}->SIG_DATA }
1247 sub _init {
1248     my $self = shift;
1249
1250     my $engine = $self->engine;
1251
1252     unless ( $self->offset ) {
1253         my $data_section = $self->size - $self->base_size - $engine->byte_size - 1;
1254
1255         $self->{offset} = $engine->_request_data_sector( $self->size );
1256
1257         my $data = delete $self->{data};
1258         my $dlen = length $data;
1259         my $continue = 1;
1260         my $curr_offset = $self->offset;
1261         while ( $continue ) {
1262
1263             my $next_offset = 0;
1264
1265             my ($leftover, $this_len, $chunk);
1266             if ( $dlen > $data_section ) {
1267                 $leftover = 0;
1268                 $this_len = $data_section;
1269                 $chunk = substr( $data, 0, $this_len );
1270
1271                 $dlen -= $data_section;
1272                 $next_offset = $engine->_request_data_sector( $self->size );
1273                 $data = substr( $data, $this_len );
1274             }
1275             else {
1276                 $leftover = $data_section - $dlen;
1277                 $this_len = $dlen;
1278                 $chunk = $data;
1279
1280                 $continue = 0;
1281             }
1282
1283             $engine->storage->print_at( $curr_offset, $self->type ); # Sector type
1284             # Skip staleness
1285             $engine->storage->print_at( $curr_offset + $self->base_size,
1286                 pack( $StP{$engine->byte_size}, $next_offset ),  # Chain loc
1287                 pack( $StP{1}, $this_len ),                      # Data length
1288                 $chunk,                                          # Data to be stored in this sector
1289                 chr(0) x $leftover,                              # Zero-fill the rest
1290             );
1291
1292             $curr_offset = $next_offset;
1293         }
1294
1295         return;
1296     }
1297 }
1298
1299 sub data_length {
1300     my $self = shift;
1301
1302     my $buffer = $self->engine->storage->read_at(
1303         $self->offset + $self->base_size + $self->engine->byte_size, 1
1304     );
1305
1306     return unpack( $StP{1}, $buffer );
1307 }
1308
1309 sub chain_loc {
1310     my $self = shift;
1311     return unpack(
1312         $StP{$self->engine->byte_size},
1313         $self->engine->storage->read_at(
1314             $self->offset + $self->base_size,
1315             $self->engine->byte_size,
1316         ),
1317     );
1318 }
1319
1320 sub data {
1321     my $self = shift;
1322
1323     my $data;
1324     while ( 1 ) {
1325         my $chain_loc = $self->chain_loc;
1326
1327         $data .= $self->engine->storage->read_at(
1328             $self->offset + $self->base_size + $self->engine->byte_size + 1, $self->data_length,
1329         );
1330
1331         last unless $chain_loc;
1332
1333         $self = $self->engine->_load_sector( $chain_loc );
1334     }
1335
1336     return $data;
1337 }
1338
1339 package DBM::Deep::Engine::Sector::Null;
1340
1341 our @ISA = qw( DBM::Deep::Engine::Sector::Data );
1342
1343 sub type { $_[0]{engine}->SIG_NULL }
1344 sub data_length { 0 }
1345 sub data { return }
1346
1347 sub _init {
1348     my $self = shift;
1349
1350     my $engine = $self->engine;
1351
1352     unless ( $self->offset ) {
1353         my $leftover = $self->size - $self->base_size - 1 * $engine->byte_size - 1;
1354
1355         $self->{offset} = $engine->_request_data_sector( $self->size );
1356         $engine->storage->print_at( $self->offset, $self->type ); # Sector type
1357         # Skip staleness counter
1358         $engine->storage->print_at( $self->offset + $self->base_size,
1359             pack( $StP{$engine->byte_size}, 0 ),  # Chain loc
1360             pack( $StP{1}, $self->data_length ),  # Data length
1361             chr(0) x $leftover,                   # Zero-fill the rest
1362         );
1363
1364         return;
1365     }
1366 }
1367
1368 package DBM::Deep::Engine::Sector::Reference;
1369
1370 our @ISA = qw( DBM::Deep::Engine::Sector::Data );
1371
1372 sub _init {
1373     my $self = shift;
1374
1375     my $e = $self->engine;
1376
1377     unless ( $self->offset ) {
1378         my $classname = Scalar::Util::blessed( delete $self->{data} );
1379         my $leftover = $self->size - $self->base_size - 3 * $e->byte_size;
1380
1381         my $class_offset = 0;
1382         if ( defined $classname ) {
1383             my $class_sector = DBM::Deep::Engine::Sector::Scalar->new({
1384                 engine => $e,
1385                 data   => $classname,
1386             });
1387             $class_offset = $class_sector->offset;
1388         }
1389
1390         $self->{offset} = $e->_request_data_sector( $self->size );
1391         $e->storage->print_at( $self->offset, $self->type ); # Sector type
1392         # Skip staleness counter
1393         $e->storage->print_at( $self->offset + $self->base_size,
1394             pack( $StP{$e->byte_size}, 0 ),             # Index/BList loc
1395             pack( $StP{$e->byte_size}, $class_offset ), # Classname loc
1396             pack( $StP{$e->byte_size}, 1 ),             # Initial refcount
1397             chr(0) x $leftover,                         # Zero-fill the rest
1398         );
1399     }
1400     else {
1401         $self->{type} = $e->storage->read_at( $self->offset, 1 );
1402     }
1403
1404     $self->{staleness} = unpack(
1405         $StP{$STALE_SIZE},
1406         $e->storage->read_at( $self->offset + $e->SIG_SIZE, $STALE_SIZE ),
1407     );
1408
1409     return;
1410 }
1411
1412 sub staleness { $_[0]{staleness} }
1413
1414 sub get_data_location_for {
1415     my $self = shift;
1416     my ($args) = @_;
1417
1418     # Assume that the head is not allowed unless otherwise specified.
1419     $args->{allow_head} = 0 unless exists $args->{allow_head};
1420
1421     # Assume we don't create a new blist location unless otherwise specified.
1422     $args->{create} = 0 unless exists $args->{create};
1423
1424     my $blist = $self->get_bucket_list({
1425         key_md5 => $args->{key_md5},
1426         key => $args->{key},
1427         create  => $args->{create},
1428     });
1429     return unless $blist && $blist->{found};
1430
1431     # At this point, $blist knows where the md5 is. What it -doesn't- know yet
1432     # is whether or not this transaction has this key. That's part of the next
1433     # function call.
1434     my $location = $blist->get_data_location_for({
1435         allow_head => $args->{allow_head},
1436     }) or return;
1437
1438     return $location;
1439 }
1440
1441 sub get_data_for {
1442     my $self = shift;
1443     my ($args) = @_;
1444
1445     my $location = $self->get_data_location_for( $args )
1446         or return;
1447
1448     return $self->engine->_load_sector( $location );
1449 }
1450
1451 sub write_data {
1452     my $self = shift;
1453     my ($args) = @_;
1454
1455     my $blist = $self->get_bucket_list({
1456         key_md5 => $args->{key_md5},
1457         key => $args->{key},
1458         create  => 1,
1459     }) or DBM::Deep->_throw_error( "How did write_data fail (no blist)?!" );
1460
1461     # Handle any transactional bookkeeping.
1462     if ( $self->engine->trans_id ) {
1463         if ( ! $blist->has_md5 ) {
1464             $blist->mark_deleted({
1465                 trans_id => 0,
1466             });
1467         }
1468     }
1469     else {
1470         my @trans_ids = $self->engine->get_running_txn_ids;
1471         if ( $blist->has_md5 ) {
1472             if ( @trans_ids ) {
1473                 my $old_value = $blist->get_data_for;
1474                 foreach my $other_trans_id ( @trans_ids ) {
1475                     next if $blist->get_data_location_for({
1476                         trans_id   => $other_trans_id,
1477                         allow_head => 0,
1478                     });
1479                     $blist->write_md5({
1480                         trans_id => $other_trans_id,
1481                         key      => $args->{key},
1482                         key_md5  => $args->{key_md5},
1483                         value    => $old_value->clone,
1484                     });
1485                 }
1486             }
1487         }
1488         else {
1489             if ( @trans_ids ) {
1490                 foreach my $other_trans_id ( @trans_ids ) {
1491                     #XXX This doesn't seem to possible to ever happen . . .
1492                     next if $blist->get_data_location_for({ trans_id => $other_trans_id, allow_head => 0 });
1493                     $blist->mark_deleted({
1494                         trans_id => $other_trans_id,
1495                     });
1496                 }
1497             }
1498         }
1499     }
1500
1501     #XXX Is this safe to do transactionally?
1502     # Free the place we're about to write to.
1503     if ( $blist->get_data_location_for({ allow_head => 0 }) ) {
1504         $blist->get_data_for({ allow_head => 0 })->free;
1505     }
1506
1507     $blist->write_md5({
1508         key      => $args->{key},
1509         key_md5  => $args->{key_md5},
1510         value    => $args->{value},
1511     });
1512 }
1513
1514 sub delete_key {
1515     my $self = shift;
1516     my ($args) = @_;
1517
1518     # XXX What should happen if this fails?
1519     my $blist = $self->get_bucket_list({
1520         key_md5 => $args->{key_md5},
1521     }) or DBM::Deep->_throw_error( "How did delete_key fail (no blist)?!" );
1522
1523     # Save the location so that we can free the data
1524     my $location = $blist->get_data_location_for({
1525         allow_head => 0,
1526     });
1527     my $old_value = $location && $self->engine->_load_sector( $location );
1528
1529     my @trans_ids = $self->engine->get_running_txn_ids;
1530
1531     # If we're the HEAD and there are running txns, then we need to clone this value to the other
1532     # transactions to preserve Isolation.
1533     if ( $self->engine->trans_id == 0 ) {
1534         if ( @trans_ids ) {
1535             foreach my $other_trans_id ( @trans_ids ) {
1536                 next if $blist->get_data_location_for({ trans_id => $other_trans_id, allow_head => 0 });
1537                 $blist->write_md5({
1538                     trans_id => $other_trans_id,
1539                     key      => $args->{key},
1540                     key_md5  => $args->{key_md5},
1541                     value    => $old_value->clone,
1542                 });
1543             }
1544         }
1545     }
1546
1547     my $data;
1548     if ( @trans_ids ) {
1549         $blist->mark_deleted( $args );
1550
1551         if ( $old_value ) {
1552             $data = $old_value->data;
1553             $old_value->free;
1554         }
1555     }
1556     else {
1557         $data = $blist->delete_md5( $args );
1558     }
1559
1560     return $data;
1561 }
1562
1563 sub get_blist_loc {
1564     my $self = shift;
1565
1566     my $e = $self->engine;
1567     my $blist_loc = $e->storage->read_at( $self->offset + $self->base_size, $e->byte_size );
1568     return unpack( $StP{$e->byte_size}, $blist_loc );
1569 }
1570
1571 sub get_bucket_list {
1572     my $self = shift;
1573     my ($args) = @_;
1574     $args ||= {};
1575
1576     # XXX Add in check here for recycling?
1577
1578     my $engine = $self->engine;
1579
1580     my $blist_loc = $self->get_blist_loc;
1581
1582     # There's no index or blist yet
1583     unless ( $blist_loc ) {
1584         return unless $args->{create};
1585
1586         my $blist = DBM::Deep::Engine::Sector::BucketList->new({
1587             engine  => $engine,
1588             key_md5 => $args->{key_md5},
1589         });
1590
1591         $engine->storage->print_at( $self->offset + $self->base_size,
1592             pack( $StP{$engine->byte_size}, $blist->offset ),
1593         );
1594
1595         return $blist;
1596     }
1597
1598     my $sector = $engine->_load_sector( $blist_loc )
1599         or DBM::Deep->_throw_error( "Cannot read sector at $blist_loc in get_bucket_list()" );
1600     my $i = 0;
1601     my $last_sector = undef;
1602     while ( $sector->isa( 'DBM::Deep::Engine::Sector::Index' ) ) {
1603         $blist_loc = $sector->get_entry( ord( substr( $args->{key_md5}, $i++, 1 ) ) );
1604         $last_sector = $sector;
1605         if ( $blist_loc ) {
1606             $sector = $engine->_load_sector( $blist_loc )
1607                 or DBM::Deep->_throw_error( "Cannot read sector at $blist_loc in get_bucket_list()" );
1608         }
1609         else {
1610             $sector = undef;
1611             last;
1612         }
1613     }
1614
1615     # This means we went through the Index sector(s) and found an empty slot
1616     unless ( $sector ) {
1617         return unless $args->{create};
1618
1619         DBM::Deep->_throw_error( "No last_sector when attempting to build a new entry" )
1620             unless $last_sector;
1621
1622         my $blist = DBM::Deep::Engine::Sector::BucketList->new({
1623             engine  => $engine,
1624             key_md5 => $args->{key_md5},
1625         });
1626
1627         $last_sector->set_entry( ord( substr( $args->{key_md5}, $i - 1, 1 ) ) => $blist->offset );
1628
1629         return $blist;
1630     }
1631
1632     $sector->find_md5( $args->{key_md5} );
1633
1634     # See whether or not we need to reindex the bucketlist
1635     # Yes, the double-braces are there for a reason. if() doesn't create a redo-able block,
1636     # so we have to create a bare block within the if() for redo-purposes. Patch and idea
1637     # submitted by sprout@cpan.org. -RobK, 2008-01-09
1638     if ( !$sector->has_md5 && $args->{create} && $sector->{idx} == -1 ) {{
1639         my $redo;
1640
1641         my $new_index = DBM::Deep::Engine::Sector::Index->new({
1642             engine => $engine,
1643         });
1644
1645         my %blist_cache;
1646         #XXX q.v. the comments for this function.
1647         foreach my $entry ( $sector->chopped_up ) {
1648             my ($spot, $md5) = @{$entry};
1649             my $idx = ord( substr( $md5, $i, 1 ) );
1650
1651             # XXX This is inefficient
1652             my $blist = $blist_cache{$idx}
1653                 ||= DBM::Deep::Engine::Sector::BucketList->new({
1654                     engine => $engine,
1655                 });
1656
1657             $new_index->set_entry( $idx => $blist->offset );
1658
1659             my $new_spot = $blist->write_at_next_open( $md5 );
1660             $engine->reindex_entry( $spot => $new_spot );
1661         }
1662
1663         # Handle the new item separately.
1664         {
1665             my $idx = ord( substr( $args->{key_md5}, $i, 1 ) );
1666
1667             # If all the previous blist's items have been thrown into one
1668             # blist and the new item belongs in there too, we need
1669             # another index.
1670             if ( keys %blist_cache == 1 and each %blist_cache == $idx ) {
1671                 ++$i, ++$redo;
1672             } else {
1673                 my $blist = $blist_cache{$idx}
1674                     ||= DBM::Deep::Engine::Sector::BucketList->new({
1675                         engine => $engine,
1676                     });
1677     
1678                 $new_index->set_entry( $idx => $blist->offset );
1679     
1680                 #XXX THIS IS HACKY!
1681                 $blist->find_md5( $args->{key_md5} );
1682                 $blist->write_md5({
1683                     key     => $args->{key},
1684                     key_md5 => $args->{key_md5},
1685                     value   => DBM::Deep::Engine::Sector::Null->new({
1686                         engine => $engine,
1687                         data   => undef,
1688                     }),
1689                 });
1690             }
1691 #            my $blist = $blist_cache{$idx}
1692 #                ||= DBM::Deep::Engine::Sector::BucketList->new({
1693 #                    engine => $engine,
1694 #                });
1695 #
1696 #            $new_index->set_entry( $idx => $blist->offset );
1697 #
1698 #            #XXX THIS IS HACKY!
1699 #            $blist->find_md5( $args->{key_md5} );
1700 #            $blist->write_md5({
1701 #                key     => $args->{key},
1702 #                key_md5 => $args->{key_md5},
1703 #                value   => DBM::Deep::Engine::Sector::Null->new({
1704 #                    engine => $engine,
1705 #                    data   => undef,
1706 #                }),
1707 #            });
1708         }
1709
1710         if ( $last_sector ) {
1711             $last_sector->set_entry(
1712                 ord( substr( $args->{key_md5}, $i - 1, 1 ) ),
1713                 $new_index->offset,
1714             );
1715         } else {
1716             $engine->storage->print_at( $self->offset + $self->base_size,
1717                 pack( $StP{$engine->byte_size}, $new_index->offset ),
1718             );
1719         }
1720
1721         $sector->clear;
1722         $sector->free;
1723
1724         if ( $redo ) {
1725             (undef, $sector) = %blist_cache;
1726             $last_sector = $new_index;
1727             redo;
1728         }
1729
1730         $sector = $blist_cache{ ord( substr( $args->{key_md5}, $i, 1 ) ) };
1731         $sector->find_md5( $args->{key_md5} );
1732     }}
1733
1734     return $sector;
1735 }
1736
1737 sub get_class_offset {
1738     my $self = shift;
1739
1740     my $e = $self->engine;
1741     return unpack(
1742         $StP{$e->byte_size},
1743         $e->storage->read_at(
1744             $self->offset + $self->base_size + 1 * $e->byte_size, $e->byte_size,
1745         ),
1746     );
1747 }
1748
1749 sub get_classname {
1750     my $self = shift;
1751
1752     my $class_offset = $self->get_class_offset;
1753
1754     return unless $class_offset;
1755
1756     return $self->engine->_load_sector( $class_offset )->data;
1757 }
1758
1759 sub data {
1760     my $self = shift;
1761
1762     unless ( $self->engine->cache->{ $self->offset } ) {
1763         my $new_obj = DBM::Deep->new({
1764             type        => $self->type,
1765             base_offset => $self->offset,
1766             staleness   => $self->staleness,
1767             storage     => $self->engine->storage,
1768             engine      => $self->engine,
1769         });
1770
1771         if ( $self->engine->storage->{autobless} ) {
1772             my $classname = $self->get_classname;
1773             if ( defined $classname ) {
1774                 bless $new_obj, $classname;
1775             }
1776         }
1777
1778         $self->engine->cache->{$self->offset} = $new_obj;
1779     }
1780     return $self->engine->cache->{$self->offset};
1781 }
1782
1783 sub free {
1784     my $self = shift;
1785
1786     # We're not ready to be removed yet.
1787     if ( $self->decrement_refcount > 0 ) {
1788         return;
1789     }
1790
1791     # Rebless the object into DBM::Deep::Null.
1792     eval { %{ $self->engine->cache->{ $self->offset } } = (); };
1793     eval { @{ $self->engine->cache->{ $self->offset } } = (); };
1794     bless $self->engine->cache->{ $self->offset }, 'DBM::Deep::Null';
1795     delete $self->engine->cache->{ $self->offset };
1796
1797     my $blist_loc = $self->get_blist_loc;
1798     $self->engine->_load_sector( $blist_loc )->free if $blist_loc;
1799
1800     my $class_loc = $self->get_class_offset;
1801     $self->engine->_load_sector( $class_loc )->free if $class_loc;
1802
1803     $self->SUPER::free();
1804 }
1805
1806 sub increment_refcount {
1807     my $self = shift;
1808
1809     my $refcount = $self->get_refcount;
1810
1811     $refcount++;
1812
1813     $self->write_refcount( $refcount );
1814
1815     return $refcount;
1816 }
1817
1818 sub decrement_refcount {
1819     my $self = shift;
1820
1821     my $refcount = $self->get_refcount;
1822
1823     $refcount--;
1824
1825     $self->write_refcount( $refcount );
1826
1827     return $refcount;
1828 }
1829
1830 sub get_refcount {
1831     my $self = shift;
1832
1833     my $e = $self->engine;
1834     return unpack(
1835         $StP{$e->byte_size},
1836         $e->storage->read_at(
1837             $self->offset + $self->base_size + 2 * $e->byte_size, $e->byte_size,
1838         ),
1839     );
1840 }
1841
1842 sub write_refcount {
1843     my $self = shift;
1844     my ($num) = @_;
1845
1846     my $e = $self->engine;
1847     $e->storage->print_at(
1848         $self->offset + $self->base_size + 2 * $e->byte_size,
1849         pack( $StP{$e->byte_size}, $num ),
1850     );
1851 }
1852
1853 package DBM::Deep::Engine::Sector::BucketList;
1854
1855 our @ISA = qw( DBM::Deep::Engine::Sector );
1856
1857 sub _init {
1858     my $self = shift;
1859
1860     my $engine = $self->engine;
1861
1862     unless ( $self->offset ) {
1863         my $leftover = $self->size - $self->base_size;
1864
1865         $self->{offset} = $engine->_request_blist_sector( $self->size );
1866         $engine->storage->print_at( $self->offset, $engine->SIG_BLIST ); # Sector type
1867         # Skip staleness counter
1868         $engine->storage->print_at( $self->offset + $self->base_size,
1869             chr(0) x $leftover, # Zero-fill the data
1870         );
1871     }
1872
1873     if ( $self->{key_md5} ) {
1874         $self->find_md5;
1875     }
1876
1877     return $self;
1878 }
1879
1880 sub clear {
1881     my $self = shift;
1882     $self->engine->storage->print_at( $self->offset + $self->base_size,
1883         chr(0) x ($self->size - $self->base_size), # Zero-fill the data
1884     );
1885 }
1886
1887 sub size {
1888     my $self = shift;
1889     unless ( $self->{size} ) {
1890         my $e = $self->engine;
1891         # Base + numbuckets * bucketsize
1892         $self->{size} = $self->base_size + $e->max_buckets * $self->bucket_size;
1893     }
1894     return $self->{size};
1895 }
1896
1897 sub free_meth { return '_add_free_blist_sector' }
1898
1899 sub free {
1900     my $self = shift;
1901
1902     my $e = $self->engine;
1903     foreach my $bucket ( $self->chopped_up ) {
1904         my $rest = $bucket->[-1];
1905
1906         # Delete the keysector
1907         my $l = unpack( $StP{$e->byte_size}, substr( $rest, $e->hash_size, $e->byte_size ) );
1908         my $s = $e->_load_sector( $l ); $s->free if $s;
1909
1910         # Delete the HEAD sector
1911         $l = unpack( $StP{$e->byte_size},
1912             substr( $rest,
1913                 $e->hash_size + $e->byte_size,
1914                 $e->byte_size,
1915             ),
1916         );
1917         $s = $e->_load_sector( $l ); $s->free if $s;
1918
1919         foreach my $txn ( 0 .. $e->num_txns - 2 ) {
1920             my $l = unpack( $StP{$e->byte_size},
1921                 substr( $rest,
1922                     $e->hash_size + 2 * $e->byte_size + $txn * ($e->byte_size + $STALE_SIZE),
1923                     $e->byte_size,
1924                 ),
1925             );
1926             my $s = $e->_load_sector( $l ); $s->free if $s;
1927         }
1928     }
1929
1930     $self->SUPER::free();
1931 }
1932
1933 sub bucket_size {
1934     my $self = shift;
1935     unless ( $self->{bucket_size} ) {
1936         my $e = $self->engine;
1937         # Key + head (location) + transactions (location + staleness-counter)
1938         my $location_size = $e->byte_size + $e->byte_size + ($e->num_txns - 1) * ($e->byte_size + $STALE_SIZE);
1939         $self->{bucket_size} = $e->hash_size + $location_size;
1940     }
1941     return $self->{bucket_size};
1942 }
1943
1944 # XXX This is such a poor hack. I need to rethink this code.
1945 sub chopped_up {
1946     my $self = shift;
1947
1948     my $e = $self->engine;
1949
1950     my @buckets;
1951     foreach my $idx ( 0 .. $e->max_buckets - 1 ) {
1952         my $spot = $self->offset + $self->base_size + $idx * $self->bucket_size;
1953         my $md5 = $e->storage->read_at( $spot, $e->hash_size );
1954
1955         #XXX If we're chopping, why would we ever have the blank_md5?
1956         last if $md5 eq $e->blank_md5;
1957
1958         my $rest = $e->storage->read_at( undef, $self->bucket_size - $e->hash_size );
1959         push @buckets, [ $spot, $md5 . $rest ];
1960     }
1961
1962     return @buckets;
1963 }
1964
1965 sub write_at_next_open {
1966     my $self = shift;
1967     my ($entry) = @_;
1968
1969     #XXX This is such a hack!
1970     $self->{_next_open} = 0 unless exists $self->{_next_open};
1971
1972     my $spot = $self->offset + $self->base_size + $self->{_next_open}++ * $self->bucket_size;
1973     $self->engine->storage->print_at( $spot, $entry );
1974
1975     return $spot;
1976 }
1977
1978 sub has_md5 {
1979     my $self = shift;
1980     unless ( exists $self->{found} ) {
1981         $self->find_md5;
1982     }
1983     return $self->{found};
1984 }
1985
1986 sub find_md5 {
1987     my $self = shift;
1988
1989     $self->{found} = undef;
1990     $self->{idx}   = -1;
1991
1992     if ( @_ ) {
1993         $self->{key_md5} = shift;
1994     }
1995
1996     # If we don't have an MD5, then what are we supposed to do?
1997     unless ( exists $self->{key_md5} ) {
1998         DBM::Deep->_throw_error( "Cannot find_md5 without a key_md5 set" );
1999     }
2000
2001     my $e = $self->engine;
2002     foreach my $idx ( 0 .. $e->max_buckets - 1 ) {
2003         my $potential = $e->storage->read_at(
2004             $self->offset + $self->base_size + $idx * $self->bucket_size, $e->hash_size,
2005         );
2006
2007         if ( $potential eq $e->blank_md5 ) {
2008             $self->{idx} = $idx;
2009             return;
2010         }
2011
2012         if ( $potential eq $self->{key_md5} ) {
2013             $self->{found} = 1;
2014             $self->{idx} = $idx;
2015             return;
2016         }
2017     }
2018
2019     return;
2020 }
2021
2022 sub write_md5 {
2023     my $self = shift;
2024     my ($args) = @_;
2025
2026     DBM::Deep->_throw_error( "write_md5: no key" ) unless exists $args->{key};
2027     DBM::Deep->_throw_error( "write_md5: no key_md5" ) unless exists $args->{key_md5};
2028     DBM::Deep->_throw_error( "write_md5: no value" ) unless exists $args->{value};
2029
2030     my $engine = $self->engine;
2031
2032     $args->{trans_id} = $engine->trans_id unless exists $args->{trans_id};
2033
2034     my $spot = $self->offset + $self->base_size + $self->{idx} * $self->bucket_size;
2035     $engine->add_entry( $args->{trans_id}, $spot );
2036
2037     unless ($self->{found}) {
2038         my $key_sector = DBM::Deep::Engine::Sector::Scalar->new({
2039             engine => $engine,
2040             data   => $args->{key},
2041         });
2042
2043         $engine->storage->print_at( $spot,
2044             $args->{key_md5},
2045             pack( $StP{$engine->byte_size}, $key_sector->offset ),
2046         );
2047     }
2048
2049     my $loc = $spot
2050       + $engine->hash_size
2051       + $engine->byte_size;
2052
2053     if ( $args->{trans_id} ) {
2054         $loc += $engine->byte_size + ($args->{trans_id} - 1) * ( $engine->byte_size + $STALE_SIZE );
2055
2056         $engine->storage->print_at( $loc,
2057             pack( $StP{$engine->byte_size}, $args->{value}->offset ),
2058             pack( $StP{$STALE_SIZE}, $engine->get_txn_staleness_counter( $args->{trans_id} ) ),
2059         );
2060     }
2061     else {
2062         $engine->storage->print_at( $loc,
2063             pack( $StP{$engine->byte_size}, $args->{value}->offset ),
2064         );
2065     }
2066 }
2067
2068 sub mark_deleted {
2069     my $self = shift;
2070     my ($args) = @_;
2071     $args ||= {};
2072
2073     my $engine = $self->engine;
2074
2075     $args->{trans_id} = $engine->trans_id unless exists $args->{trans_id};
2076
2077     my $spot = $self->offset + $self->base_size + $self->{idx} * $self->bucket_size;
2078     $engine->add_entry( $args->{trans_id}, $spot );
2079
2080     my $loc = $spot
2081       + $engine->hash_size
2082       + $engine->byte_size;
2083
2084     if ( $args->{trans_id} ) {
2085         $loc += $engine->byte_size + ($args->{trans_id} - 1) * ( $engine->byte_size + $STALE_SIZE );
2086
2087         $engine->storage->print_at( $loc,
2088             pack( $StP{$engine->byte_size}, 1 ), # 1 is the marker for deleted
2089             pack( $StP{$STALE_SIZE}, $engine->get_txn_staleness_counter( $args->{trans_id} ) ),
2090         );
2091     }
2092     else {
2093         $engine->storage->print_at( $loc,
2094             pack( $StP{$engine->byte_size}, 1 ), # 1 is the marker for deleted
2095         );
2096     }
2097
2098 }
2099
2100 sub delete_md5 {
2101     my $self = shift;
2102     my ($args) = @_;
2103
2104     my $engine = $self->engine;
2105     return undef unless $self->{found};
2106
2107     # Save the location so that we can free the data
2108     my $location = $self->get_data_location_for({
2109         allow_head => 0,
2110     });
2111     my $key_sector = $self->get_key_for;
2112
2113     my $spot = $self->offset + $self->base_size + $self->{idx} * $self->bucket_size;
2114     $engine->storage->print_at( $spot,
2115         $engine->storage->read_at(
2116             $spot + $self->bucket_size,
2117             $self->bucket_size * ( $engine->max_buckets - $self->{idx} - 1 ),
2118         ),
2119         chr(0) x $self->bucket_size,
2120     );
2121
2122     $key_sector->free;
2123
2124     my $data_sector = $self->engine->_load_sector( $location );
2125     my $data = $data_sector->data;
2126     $data_sector->free;
2127
2128     return $data;
2129 }
2130
2131 sub get_data_location_for {
2132     my $self = shift;
2133     my ($args) = @_;
2134     $args ||= {};
2135
2136     $args->{allow_head} = 0 unless exists $args->{allow_head};
2137     $args->{trans_id}   = $self->engine->trans_id unless exists $args->{trans_id};
2138     $args->{idx}        = $self->{idx} unless exists $args->{idx};
2139
2140     my $e = $self->engine;
2141
2142     my $spot = $self->offset + $self->base_size
2143       + $args->{idx} * $self->bucket_size
2144       + $e->hash_size
2145       + $e->byte_size;
2146
2147     if ( $args->{trans_id} ) {
2148         $spot += $e->byte_size + ($args->{trans_id} - 1) * ( $e->byte_size + $STALE_SIZE );
2149     }
2150
2151     my $buffer = $e->storage->read_at(
2152         $spot,
2153         $e->byte_size + $STALE_SIZE,
2154     );
2155     my ($loc, $staleness) = unpack( $StP{$e->byte_size} . ' ' . $StP{$STALE_SIZE}, $buffer );
2156
2157     # XXX Merge the two if-clauses below
2158     if ( $args->{trans_id} ) {
2159         # We have found an entry that is old, so get rid of it
2160         if ( $staleness != (my $s = $e->get_txn_staleness_counter( $args->{trans_id} ) ) ) {
2161             $e->storage->print_at(
2162                 $spot,
2163                 pack( $StP{$e->byte_size} . ' ' . $StP{$STALE_SIZE}, (0) x 2 ), 
2164             );
2165             $loc = 0;
2166         }
2167     }
2168
2169     # If we're in a transaction and we never wrote to this location, try the
2170     # HEAD instead.
2171     if ( $args->{trans_id} && !$loc && $args->{allow_head} ) {
2172         return $self->get_data_location_for({
2173             trans_id   => 0,
2174             allow_head => 1,
2175             idx        => $args->{idx},
2176         });
2177     }
2178
2179     return $loc <= 1 ? 0 : $loc;
2180 }
2181
2182 sub get_data_for {
2183     my $self = shift;
2184     my ($args) = @_;
2185     $args ||= {};
2186
2187     return unless $self->{found};
2188     my $location = $self->get_data_location_for({
2189         allow_head => $args->{allow_head},
2190     });
2191     return $self->engine->_load_sector( $location );
2192 }
2193
2194 sub get_key_for {
2195     my $self = shift;
2196     my ($idx) = @_;
2197     $idx = $self->{idx} unless defined $idx;
2198
2199     if ( $idx >= $self->engine->max_buckets ) {
2200         DBM::Deep->_throw_error( "get_key_for(): Attempting to retrieve $idx" );
2201     }
2202
2203     my $location = $self->engine->storage->read_at(
2204         $self->offset + $self->base_size + $idx * $self->bucket_size + $self->engine->hash_size,
2205         $self->engine->byte_size,
2206     );
2207     $location = unpack( $StP{$self->engine->byte_size}, $location );
2208     DBM::Deep->_throw_error( "get_key_for: No location?" ) unless $location;
2209
2210     return $self->engine->_load_sector( $location );
2211 }
2212
2213 package DBM::Deep::Engine::Sector::Index;
2214
2215 our @ISA = qw( DBM::Deep::Engine::Sector );
2216
2217 sub _init {
2218     my $self = shift;
2219
2220     my $engine = $self->engine;
2221
2222     unless ( $self->offset ) {
2223         my $leftover = $self->size - $self->base_size;
2224
2225         $self->{offset} = $engine->_request_index_sector( $self->size );
2226         $engine->storage->print_at( $self->offset, $engine->SIG_INDEX ); # Sector type
2227         # Skip staleness counter
2228         $engine->storage->print_at( $self->offset + $self->base_size,
2229             chr(0) x $leftover, # Zero-fill the rest
2230         );
2231     }
2232
2233     return $self;
2234 }
2235
2236 #XXX Change here
2237 sub size {
2238     my $self = shift;
2239     unless ( $self->{size} ) {
2240         my $e = $self->engine;
2241         $self->{size} = $self->base_size + $e->byte_size * $e->hash_chars;
2242     }
2243     return $self->{size};
2244 }
2245
2246 sub free_meth { return '_add_free_index_sector' }
2247
2248 sub free {
2249     my $self = shift;
2250     my $e = $self->engine;
2251
2252     for my $i ( 0 .. $e->hash_chars - 1 ) {
2253         my $l = $self->get_entry( $i ) or next;
2254         $e->_load_sector( $l )->free;
2255     }
2256
2257     $self->SUPER::free();
2258 }
2259
2260 sub _loc_for {
2261     my $self = shift;
2262     my ($idx) = @_;
2263     return $self->offset + $self->base_size + $idx * $self->engine->byte_size;
2264 }
2265
2266 sub get_entry {
2267     my $self = shift;
2268     my ($idx) = @_;
2269
2270     my $e = $self->engine;
2271
2272     DBM::Deep->_throw_error( "get_entry: Out of range ($idx)" )
2273         if $idx < 0 || $idx >= $e->hash_chars;
2274
2275     return unpack(
2276         $StP{$e->byte_size},
2277         $e->storage->read_at( $self->_loc_for( $idx ), $e->byte_size ),
2278     );
2279 }
2280
2281 sub set_entry {
2282     my $self = shift;
2283     my ($idx, $loc) = @_;
2284
2285     my $e = $self->engine;
2286
2287     DBM::Deep->_throw_error( "set_entry: Out of range ($idx)" )
2288         if $idx < 0 || $idx >= $e->hash_chars;
2289
2290     $self->engine->storage->print_at(
2291         $self->_loc_for( $idx ),
2292         pack( $StP{$e->byte_size}, $loc ),
2293     );
2294 }
2295
2296 # This was copied from MARCEL's Class::Null. However, I couldn't use it because
2297 # I need an undef value, not an implementation of the Null Class pattern.
2298 package DBM::Deep::Null;
2299
2300 use overload
2301     'bool'   => sub { undef },
2302     '""'     => sub { undef },
2303     '0+'     => sub { undef },
2304     fallback => 1,
2305     nomethod => 'AUTOLOAD';
2306
2307 sub AUTOLOAD { return; }
2308
2309 1;
2310 __END__