File Coverage

blib/lib/Yote.pm
Criterion Covered Total %
statement 971 1297 74.8
branch 251 424 59.2
condition 47 100 47.0
subroutine 172 205 83.9
pod 1 1 100.0
total 1442 2027 71.1


line stmt bran cond sub pod time code
1             package Yote;
2              
3 1     1   428 use strict;
  1         2  
  1         23  
4 1     1   4 use warnings;
  1         2  
  1         21  
5 1     1   3 no warnings 'uninitialized';
  1         3  
  1         24  
6              
7 1     1   4 use vars qw($VERSION);
  1         1  
  1         129  
8              
9             $VERSION = '2.02';
10              
11             =head1 NAME
12              
13             Yote - Persistant Perl container objects in a directed graph of lazilly loaded nodes.
14              
15             =head1 DESCRIPTION
16              
17             This is for anyone who wants to store arbitrary structured state data and doesn't have
18             the time or inclination to write a schema or configure some framework. This can be used
19             orthagonally to any other storage system.
20              
21             Yote only loads data as it needs too. It does not load all stored containers at once.
22             Data is stored in a data directory and is stored using the Data::RecordStore module. A Yote
23             container is a key/value store where the values can be strings, numbers, arrays, hashes
24             or other Yote containers.
25              
26             The entry point for all Yote data stores is the root node. All objects in the store are
27             unreachable if they cannot trace a reference path back to this node. If they cannot, running
28             compress_store will remove them.
29              
30             There are lots of potential uses for Yote, and a few come to mind :
31              
32             * configuration data
33             * data modeling
34             * user preference data
35             * user account data
36             * game data
37             * shopping carts
38             * product information
39              
40             =head1 SYNOPSIS
41              
42             use Yote;
43              
44             my $store = Yote::open_store( '/path/to/data-directory' );
45              
46             my $root_node = $store->fetch_root;
47              
48             $root_node->add_to_myList( $store->newobj( {
49             someval => 123.53,
50             somehash => { A => 1 },
51             someobj => $store->newobj( { foo => "Bar" },
52             'Optional-Yote-Subclass-Package' );
53             } );
54              
55             # the root node now has a list 'myList' attached to it with the single
56             # value of a yote object that yote object has two fields,
57             # one of which is an other yote object.
58              
59             $root_node->add_to_myList( 42 );
60              
61             #
62             # New Yote container objects are created with $store->newobj. Note that
63             # they must find a reference path to the root to be protected from
64             # being deleted from the record store upon compression.
65             #
66             my $newObj = $store->newobj;
67              
68             $root_node->set_field( "Value" );
69              
70             my $val = $root_node->get_value( "default" );
71             # $val eq 'default'
72              
73             $val = $root_node->get_value( "Somethign Else" );
74             # $val eq 'default' (old value not overridden by a new default value)
75              
76              
77             my $otherval = $root_node->get( 'ot3rv@l', 'other default' );
78             # $otherval eq 'other default'
79              
80             $root_node->set( 'ot3rv@l', 'newy valuye' );
81             $otherval2 = $root_node->get( 'ot3rv@l', 'yet other default' );
82             # $otherval2 eq 'newy valuye'
83              
84             $root_node->set_value( "Something Else" );
85              
86             my $val = $root_node->get_value( "default" );
87             # $val eq 'Something Else'
88              
89             my $myList = $root_node->get_myList;
90              
91             for my $example (@$myList) {
92             print ">$example\n";
93             }
94              
95             #
96             # Each object gets a unique ID which can be used to fetch that
97             # object directly from the store.
98             #
99             my $someid = $root_node->get_someobj->{ID};
100              
101             my $someref = $store->fetch( $someid );
102              
103             #
104             # Even hashes and array have unique yote IDS. These can be
105             # determined by calling the _get_id method of the store.
106             #
107             my $hash = $root_node->set_ahash( { zoo => "Zar" } );
108             my $hash_id = $store->_get_id( $hash );
109             my $other_ref_to_hash = $store->fetch( $hash_id );
110              
111             #
112             # Anything that cannot trace a reference path to the root
113             # is eligable for being removed upon compression.
114             #
115              
116             =head1 PUBLIC METHODS
117              
118             =cut
119              
120              
121             =head2 open_store( '/path/to/directory' )
122              
123             Starts up a persistance engine and returns it.
124              
125             =cut
126              
127             sub open_store {
128 3     3 1 1030 my $path = pop;
129 3         17 my $store = Yote::ObjStore->_new( { store => $path } );
130 3         9 $store->_init;
131 3         5 $store;
132             }
133              
134             # ---------------------------------------------------------------------------------------------------------------------
135              
136             package Yote::ObjStore;
137              
138 1     1   5 use strict;
  1         1  
  1         18  
139 1     1   3 use warnings;
  1         2  
  1         21  
140 1     1   3 no warnings 'numeric';
  1         2  
  1         30  
141 1     1   4 no warnings 'uninitialized';
  1         2  
  1         23  
142 1     1   3 no warnings 'recursion';
  1         2  
  1         18  
143              
144 1     1   310 use File::Copy;
  1         3448  
  1         77  
145 1     1   6 use File::Path qw(make_path remove_tree);
  1         1  
  1         52  
146 1     1   4 use Scalar::Util qw(weaken);
  1         1  
  1         61  
147              
148 1     1   298 use Module::Loaded;
  1         499  
  1         2824  
149              
150             =head1 NAME
151              
152             Yote::ObjStore - manages Yote::Obj objects in a graph.
153              
154             =head1 DESCRIPTION
155              
156             The Yote::ObjStore does the following things :
157              
158             * fetches the root object
159             * creates new objects
160             * fetches existing objects by id
161             * saves all new or changed objects
162             * finds objects that cannot connect to the root node and removes them
163              
164             =cut
165              
166             # ------------------------------------------------------------------------------------------
167             # * PUBLIC CLASS METHODS *
168             # ------------------------------------------------------------------------------------------
169              
170             =head2 fetch_root
171              
172             Returns the root node of the graph. All things that can be
173             trace a reference path back to the root node are considered active
174             and are not removed when the object store is compressed.
175              
176             =cut
177             sub fetch_root {
178 8     8   533 my $self = shift;
179 8 50       21 die "fetch_root must be called on Yote store object" unless ref( $self );
180 8         21 my $root = $self->fetch( $self->_first_id );
181 8 100       16 unless( $root ) {
182 1         4 $root = $self->_newroot;
183 1         3 $root->{ID} = $self->_first_id;
184 1         3 $self->_stow( $root );
185             }
186 8         31 $root;
187             } #fetch_root
188              
189             =head2 newobj( { ... data .... }, optionalClass )
190              
191             Creates a container object initialized with the
192             incoming hash ref data. The class of the object must be either
193             Yote::Obj or a subclass of it. Yote::Obj is the default.
194              
195             Once created, the object will be saved in the data store when
196             $store->stow_all has been called. If the object is not attached
197             to the root or an object that can be reached by the root, it will be
198             remove when Yote::ObjStore::Compress is called.
199              
200             =cut
201             sub newobj {
202 3     3   30 my( $self, $data, $class ) = @_;
203 3   50     13 $class ||= 'Yote::Obj';
204 3         11 $class->_new( $self, $data );
205             }
206              
207             sub _newroot {
208 1     1   2 my $self = shift;
209 1         2 Yote::Obj->_new( $self, {}, $self->_first_id );
210             }
211              
212             =head2 copy_from_remote_store( $obj )
213              
214             This takes an object that belongs to a seperate store and makes
215             a deep copy of it.
216              
217             =cut
218             sub copy_from_remote_store {
219 0     0   0 my( $self, $obj ) = @_;
220 0         0 my $r = ref( $obj );
221 0 0       0 return $obj unless $r;
222 0 0       0 if( $r eq 'ARRAY' ) {
    0          
223 0         0 return [ map { $self->copy_from_remote_store($_) } @$obj ];
  0         0  
224             } elsif( $r eq 'HASH' ) {
225 0         0 return { map { $_ => $self->copy_from_remote_store($obj->{$_}) } keys %$obj };
  0         0  
226             } else {
227 0         0 my $data = { map { $_ => $self->copy_from_remote_store($obj->{DATA}{$_}) } keys %{$obj->{DATA}} };
  0         0  
  0         0  
228 0         0 return $self->newobj( $data, $r );
229             }
230             }
231              
232             =head2 cache_all()
233              
234             This turns on caching for the store. Any objects loaded will
235             remain cached until clear_cache is called. Normally, they
236             would be DESTROYed once their last reference was removed unless
237             they are in a state that needs stowing.
238              
239             =cut
240             sub cache_all {
241 0     0   0 my $self = shift;
242 0         0 $self->{CACHE_ALL} = 1;
243             }
244              
245             =head2 uncache( obj )
246              
247             This removes the object from the cache if it was in the cache
248              
249             =cut
250             sub uncache {
251 0     0   0 my( $self, $obj ) = @_;
252 0 0       0 if( ref( $obj ) ) {
253 0         0 delete $self->{CACHE}{$self->_get_id( $obj )};
254             }
255             }
256              
257              
258              
259             =head2 pause_cache()
260              
261             When called, no new objects will be added to the cache until
262             cache_all is called.
263              
264             =cut
265             sub pause_cache {
266 0     0   0 my $self = shift;
267 0         0 $self->{CACHE_ALL} = 0;
268             }
269              
270             =head2 clear_cache()
271              
272             When called, this dumps the object cache. Objects that
273             references or have changes that need to be stowed will
274             not be cleared.
275              
276             =cut
277             sub clear_cache {
278 0     0   0 my $self = shift;
279 0         0 $self->{_CACHE} = {};
280             }
281              
282              
283              
284             =head2 fetch( $id )
285              
286             Returns the object with the given id.
287              
288             =cut
289             sub fetch {
290 14314     14314   18699 my( $self, $id ) = @_;
291 14314 100       20051 return undef unless $id;
292             #
293             # Return the object if we have a reference to its dirty state.
294             #
295 14188         17790 my $ref = $self->{_DIRTY}{$id};
296 14188 100       18079 if( defined $ref ) {
297 5846         9060 return $ref;
298             } else {
299 8342         9361 $ref = $self->{_WEAK_REFS}{$id};
300 8342 100       11036 if( $ref ) {
301 2737         4042 return $ref;
302             }
303 5605         6393 undef $ref;
304             }
305 5605         9087 my $obj_arry = $self->{_DATASTORE}->_fetch( $id );
306              
307 5605 100       8714 if( $obj_arry ) {
308 5600         9963 my( $id, $class, $data ) = @$obj_arry;
309 5600 100       13049 if( $class eq 'ARRAY' ) {
    100          
    50          
    100          
310 11         12 my( @arry );
311 11         47 tie @arry, 'Yote::Array', $self, $id, @$data;
312 11         30 $self->_store_weak( $id, \@arry );
313 11         32 return \@arry;
314             }
315             elsif( $class eq 'Yote::ArrayGatekeeper' ) {
316 11         14 my( @arry );
317 11         47 tie @arry, 'Yote::ArrayGatekeeper', $self, $id, @$data;
318 11         31 $self->_store_weak( $id, \@arry );
319 11         48 return \@arry;
320             }
321             elsif( $class eq 'HASH' ) {
322 0         0 my( %hash );
323 0         0 tie %hash, 'Yote::Hash', $self, $id, @$data;
324 0         0 $self->_store_weak( $id, \%hash );
325 0         0 return \%hash;
326             }
327             elsif( $class eq 'Yote::BigHash' ) {
328 5557         6825 my( %hash );
329 5557         22002 tie %hash, 'Yote::BigHash', $self, $id, @$data;
330 5557         12639 $self->_store_weak( $id, \%hash );
331 5557         37148 return \%hash;
332             }
333             else {
334 21         21 my $obj;
335 21         45 eval {
336 21         31 my $path = $class;
337 21 50       48 unless( $INC{ $class } ) {
338 1     1   50 eval("use $class");
  0     1   0  
  0     1   0  
  1     1   57  
  0     1   0  
  0     1   0  
  1     1   50  
  0     1   0  
  0     1   0  
  1     1   56  
  0     1   0  
  0     1   0  
  1     1   53  
  0     1   0  
  0     1   0  
  1     1   53  
  0     1   0  
  0     1   0  
  1     1   53  
  0     1   0  
  0     1   0  
  1         57  
  0         0  
  0         0  
  1         53  
  0         0  
  0         0  
  1         52  
  0         0  
  0         0  
  1         54  
  0         0  
  0         0  
  1         52  
  0         0  
  0         0  
  1         53  
  0         0  
  0         0  
  1         54  
  0         0  
  0         0  
  1         61  
  0         0  
  0         0  
  1         97  
  0         0  
  0         0  
  1         83  
  0         0  
  0         0  
  1         54  
  0         0  
  0         0  
  1         51  
  0         0  
  0         0  
  1         51  
  0         0  
  0         0  
  1         87  
  0         0  
  0         0  
  21         1197  
339             }
340 21   33     11553 $obj = $self->{_WEAK_REFS}{$id} || $class->_instantiate( $id, $self );
341             };
342 21 50       49 die $@ if $@;
343 21         32 $obj->{DATA} = $data;
344 21         39 $obj->{ID} = $id;
345 21         44 $self->_store_weak( $id, $obj );
346 21         48 $obj->_load();
347 21         70 return $obj;
348             }
349             }
350 5         18 return undef;
351             } #fetch
352              
353             =head2 run_purger
354              
355             =cut
356             sub run_purger {
357 5     5   1025 my( $self, $make_tally, $copy_only ) = @_;
358 5         17 $self->stow_all();
359              
360 5         15 my $keep_db = $self->{_DATASTORE}->_generate_keep_db();
361              
362             # analyze to see what percentage would be kept
363 5         14 my $total = $keep_db->entry_count;
364 5         135 my $keep = 0;
365 5         13 for my $tid (1..$total) {
366 36         68 my( $has_keep ) = $keep_db->get_record( $tid )->[0];
367 36 100       1103 $keep++ if $has_keep;
368             }
369              
370             #
371             # If there are more things to keep than not, do a db purge,
372             # otherwise, rebuild the db.
373             #
374 5   66     20 my $do_purge = $keep > ( $total/2 ) && ! $copy_only;
375 5         7 my $purged;
376 5 100       20 if( $do_purge ) {
377 3         10 $purged = $self->{_DATASTORE}->_purge_objects( $keep_db, $make_tally );
378 3         16 $self->{_DATASTORE}->_update_recycle_ids( $keep_db );
379 3         12 $keep_db->unlink_store;
380             } else {
381 2         6 $purged = $self->_copy_active_ids( $keep_db );
382             # keep db is not copied over
383             }
384              
385 5         214 $purged;
386             } #run_purger
387              
388             sub _copy_active_ids {
389 2     2   5 my( $self, $copy_db ) = @_;
390 2         6 $self->stow_all();
391              
392 2         4 my $original_dir = $self->{args}{store};
393 2         4 my $backdir = $original_dir . '_COMPRESS_BACK_RECENT';
394 2         5 my $newdir = $original_dir . '_NEW_RECYC';
395              
396 2 100       52 if( -e $backdir ) {
397 1         3 my $oldback = $original_dir . '_COMPRESS_BACK_OLD';
398 1 50       28 if( -d $oldback ) {
399 0         0 warn "Removing old compression backup directory";
400 0         0 remove_tree( $oldback );
401             }
402 1 50       4 move( $backdir, $oldback ) or die $!;
403             }
404              
405 2 50       141 if( -x $newdir ) {
406 0         0 die "Unable to run compress store, temp directory '$newdir' already exists.";
407             }
408 2         13 my $newstore = Yote::ObjStore->_new( { store => $newdir } );
409              
410 2         6 my( @purges );
411 2         7 for my $keep_id ( 1..$copy_db->entry_count ) {
412              
413 14         503 my( $has_keep ) = $copy_db->get_record( $keep_id )->[0];
414 14 100       496 if( $has_keep ) {
    100          
415 4         12 my $obj = $self->fetch( $keep_id );
416              
417 4         13 $newstore->{_DATASTORE}{DATA_STORE}->ensure_entry_count( $keep_id - 1 );
418 4         211 $newstore->_dirty( $obj, $keep_id );
419 4         10 $newstore->_stow( $obj, $keep_id );
420             } elsif( $self->{_DATASTORE}{DATA_STORE}->has_id( $keep_id ) ) {
421 3         206 push @purges, $keep_id;
422             }
423             } #each entry id
424              
425             # reopen data store
426 2         9 $self->{_DATASTORE} = Yote::YoteDB->open( $self, $self->{args} );
427 2 50       9 move( $original_dir, $backdir ) or die $!;
428 2 50       125 move( $newdir, $original_dir ) or die $!;
429              
430 2         81 \@purges;
431              
432             } #_copy_active_ids
433              
434             =head2 has_id
435              
436             Returns true if there is a valid reference linked to the id
437              
438             =cut
439             sub has_id {
440 0     0   0 my( $self, $id ) = @_;
441 0         0 return $self->{_DATASTORE}{DATA_STORE}->has_id( $id );
442             }
443              
444             =head2 stow_all
445              
446             Saves all newly created or dirty objects.
447              
448             =cut
449             sub stow_all {
450 15     15   37 my $self = shift;
451 15         19 my @odata;
452 15         20 for my $obj (values %{$self->{_DIRTY}} ) {
  15         54  
453 77         86 my $cls;
454 77         94 my $ref = ref( $obj );
455 77 100       125 if( $ref eq 'ARRAY' ) {
    100          
456 48 100       102 $cls = ref(tied @$obj) eq 'Yote::ArrayGatekeeper' ? 'Yote::ArrayGatekeeper' : 'ARRAY';
457             } elsif( $ref eq 'HASH' ) {
458 23 50       40 $cls = ref(tied %$obj) eq 'Yote::BigHash' ? 'Yote::BigHash' : 'HASH';
459             } else {
460 6         8 $cls = $ref;
461             }
462 77         106 my( $text_rep ) = $self->_raw_data( $obj );
463 77         142 push( @odata, [ $self->_get_id( $obj ), $cls, $text_rep ] );
464             }
465 15         53 $self->{_DATASTORE}->_stow_all( \@odata );
466 15         55 $self->{_DIRTY} = {};
467             } #stow_all
468              
469              
470             =head2 stow( $obj )
471              
472             Saves that object to the database
473              
474             =cut
475             sub stow {
476 0     0   0 my( $self, $obj ) = @_;
477 0         0 my $cls;
478 0         0 my $ref = ref( $obj );
479 0 0 0     0 if( $ref eq 'ARRAY' || $ref eq 'Yote::Array' ) {
    0          
480 0         0 $cls = 'ARRAY';
481             } elsif( $ref eq 'HASH' ) {
482 0 0       0 $cls = ref(tied %$ref) eq 'Yote::BigHash' ? 'Yote::BigHash' : 'HASH';
483             } else {
484 0         0 $cls = $ref;
485             }
486 0         0 my $id = $self->_get_id( $obj );
487 0         0 my( $text_rep ) = $self->_raw_data( $obj );
488 0         0 $self->{_DATASTORE}->_stow( $id, $cls, $text_rep );
489 0         0 delete $self->{_DIRTY}{$id};
490             } #stow
491              
492              
493              
494             # -------------------------------
495             # * PRIVATE METHODS *
496             # -------------------------------
497             sub _new { #Yote::ObjStore
498 5     5   11 my( $pkg, $args ) = @_;
499 5         18 my $self = bless {
500             _DIRTY => {},
501             _WEAK_REFS => {},
502             args => $args,
503             }, $pkg;
504 5         22 $self->{_DATASTORE} = Yote::YoteDB->open( $self, $args );
505 5         12 $self;
506             } #_new
507              
508             sub _init {
509 3     3   5 my $self = shift;
510 3         6 for my $pkg ( qw( Yote::Obj Yote::Array Yote::Hash Yote::ArrayGatekeeper Yote::BigHash ) ) {
511 1 50   1   128 $INC{ $pkg } or eval("use $pkg");
  0     1   0  
  0     1   0  
  1     1   53  
  0     1   0  
  0     1   0  
  1     1   50  
  0     1   0  
  0     1   0  
  1     1   49  
  0     1   0  
  0     1   0  
  1     1   50  
  0     1   0  
  0     1   0  
  1         53  
  0         0  
  0         0  
  1         51  
  0         0  
  0         0  
  1         49  
  0         0  
  0         0  
  1         50  
  0         0  
  0         0  
  1         49  
  0         0  
  0         0  
  1         82  
  0         0  
  0         0  
  1         53  
  0         0  
  0         0  
  1         116  
  0         0  
  0         0  
  1         51  
  0         0  
  0         0  
  1         50  
  0         0  
  0         0  
  15         5353  
512             }
513 3         1140 $self->fetch_root;
514 3         8 $self->stow_all;
515 3         4 $self;
516             }
517              
518              
519             sub dirty_count {
520 0     0   0 my $self = shift;
521 0         0 return scalar( keys %{$self->{_DIRTY}} );
  0         0  
522             }
523              
524             #
525             # Markes given object as dirty.
526             #
527             sub _dirty {
528             # ( $self, $ref, $id
529 3019     3019   3935 $_[0]->{_DIRTY}->{$_[2]} = $_[1];
530             } #_dirty
531              
532             #
533             # Returns the first ID that is associated with the root Root object
534             #
535             sub _first_id {
536 10     10   35 shift->{_DATASTORE}->_first_id();
537             } #_first_id
538              
539             sub _get_id {
540             # for debugging I think?
541 221     221   329 shift->__get_id( shift );
542             }
543              
544             sub __get_id {
545 221     221   298 my( $self, $ref ) = @_;
546              
547 221         273 my $class = ref( $ref );
548 221 50       314 die "__get_id requires reference. got '$ref'" unless $class;
549              
550 221 50 33     568 if( $class eq 'Yote::Array') {
    100          
    50          
    100          
551 0         0 return $ref->[0];
552             }
553             elsif( $class eq 'ARRAY' ) {
554 125         158 my $tied = tied @$ref;
555 125 100       187 if( $tied ) {
556 101   33     138 $tied->[0] ||= $self->{_DATASTORE}->_get_id( "ARRAY" );
557 101         253 return $tied->[0];
558             }
559 24         36 my( @data ) = @$ref;
560 24         53 my $id = $self->{_DATASTORE}->_get_id( $class );
561 24         3930 tie @$ref, 'Yote::ArrayGatekeeper', $self, $id;
562 24 50       52 push( @$ref, @data ) if @data;
563 24         60 $self->_dirty( $ref, $id );
564 24         57 $self->_store_weak( $id, $ref );
565 24         63 return $id;
566             }
567             elsif( $class eq 'Yote::Hash' || $class eq 'Yote::BigHash' ) {
568 0         0 return $ref->[0];
569             }
570             elsif( $class eq 'HASH' ) {
571 72         96 my $tied = tied %$ref;
572 72 100       91 if( $tied ) {
573 49 50       69 my $useclass = ref($tied) eq 'Yote::BigHash' ? 'Yote::BigHash' : 'HASH';
574 49   33     68 $tied->[0] ||= $self->{_DATASTORE}->_get_id( $useclass );
575 49         119 return $tied->[0];
576             } else {
577 23         34 $class = 'Yote::BigHash';
578             }
579 23         55 my $id = $self->{_DATASTORE}->_get_id( $class );
580              
581 23         3534 my( %vals ) = %$ref;
582              
583 23         116 tie %$ref, 'Yote::BigHash', $self, $id;
584 23         47 for my $key (keys %vals) {
585 1         6 $ref->{$key} = $vals{$key};
586             }
587 23         48 $self->_dirty( $ref, $id );
588 23         51 $self->_store_weak( $id, $ref );
589 23         72 return $id;
590             }
591             else {
592 24 100       89 return $ref->{ID} if $ref->{ID};
593 3 50       6 if( $class eq 'Yote::Root' ) {
594 0         0 $ref->{ID} = $self->{_DATASTORE}->_first_id( $class );
595             } else {
596 3   33     11 $ref->{ID} ||= $self->{_DATASTORE}->_get_id( $class );
597             }
598 3         461 return $ref->{ID};
599             }
600              
601             } #_get_id
602              
603             sub _stow {
604 5     5   11 my( $self, $obj, $id ) = @_;
605              
606 5         10 my $class = ref( $obj );
607 5 50       10 return unless $class;
608 5   66     11 $id //= $self->_get_id( $obj );
609 5 50       8 die unless $id;
610              
611 5         10 my( $text_rep, $data ) = $self->_raw_data( $obj );
612              
613 5 100       26 if( $class eq 'ARRAY' ) {
    50          
    50          
    50          
    50          
    50          
614 2 50       6 my $useClass = ref( tied( @$obj ) ) eq 'Yote::ArrayGatekeeper' ? 'Yote::ArrayGatekeeper' : 'ARRAY';
615 2         5 $self->{_DATASTORE}->_stow( $id,$useClass, $text_rep );
616 2         620 $self->_clean( $id );
617             }
618             elsif( $class eq 'HASH' ) {
619 0 0       0 my $useClass = ref( tied( %$obj ) ) eq 'Yote::BigHash' ? 'Yote::BigHash' : 'HASH';
620 0         0 $self->{_DATASTORE}->_stow( $id,$useClass,$text_rep );
621 0         0 $self->_clean( $id );
622             }
623             elsif( $class eq 'Yote::Array' ) {
624 0 0       0 if( $self->_is_dirty( $id ) ) {
625 0         0 $self->{_DATASTORE}->_stow( $id,'ARRAY',$text_rep );
626 0         0 $self->_clean( $id );
627             }
628 0         0 for my $child (@$data) {
629 0 0 0     0 if( $child =~ /^[0-9]/ && $self->{_DIRTY}->{$child} ) {
630 0         0 $self->_stow( $child, $self->{_DIRTY}->{$child} );
631             }
632             }
633             }
634             elsif( $class eq 'Yote::ArrayGatekeeper' ) {
635 0 0       0 if( $self->_is_dirty( $id ) ) {
636 0         0 $self->{_DATASTORE}->_stow( $id,'Yote::ArrayGatekeeper',$text_rep );
637 0         0 $self->_clean( $id );
638             }
639 0         0 for my $child (@$data) {
640 0 0 0     0 if( $child =~ /^[0-9]/ && $self->{_DIRTY}->{$child} ) {
641 0         0 $self->_stow( $child, $self->{_DIRTY}->{$child} );
642             }
643             }
644             }
645             elsif( $class eq 'Yote::Hash' ) {
646 0 0       0 if( $self->_is_dirty( $id ) ) {
647 0         0 $self->{_DATASTORE}->_stow( $id, 'HASH', $text_rep );
648             }
649 0         0 $self->_clean( $id );
650 0         0 for my $child (values %$data) {
651 0 0 0     0 if( $child =~ /^[0-9]/ && $self->{_DIRTY}->{$child} ) {
652 0         0 $self->_stow( $child, $self->{_DIRTY}->{$child} );
653             }
654             }
655             }
656             elsif( $class eq 'Yote::BigHash' ) {
657 0 0       0 if( $self->_is_dirty( $id ) ) {
658 0         0 $self->{_DATASTORE}->_stow( $id, 'Yote::BigHash', $text_rep );
659             }
660 0         0 $self->_clean( $id );
661 0         0 for my $child (values %$data) {
662 0 0 0     0 if( $child =~ /^[0-9]/ && $self->{_DIRTY}->{$child} ) {
663 0         0 $self->_stow( $child, $self->{_DIRTY}->{$child} );
664             }
665             }
666             }
667             else {
668 3 50       10 if( $self->_is_dirty( $id ) ) {
669 3         12 $self->{_DATASTORE}->_stow( $id, $class, $text_rep );
670 3         1174 $self->_clean( $id );
671             }
672 3         8 for my $val (values %$data) {
673 2 50 33     16 if( $val =~ /^[0-9]/ && $self->{_DIRTY}->{$val} ) {
674 0         0 $self->_stow( $val, $self->{_DIRTY}->{$val} );
675             }
676             }
677             }
678 5         17 $id;
679             } #_stow
680              
681             sub _xform_in {
682 1519     1519   1979 my( $self, $val ) = @_;
683 1519 100       2067 if( ref( $val ) ) {
684 8         18 return $self->_get_id( $val );
685             }
686 1511 50       4135 return defined $val ? "v$val" : undef;
687             }
688              
689             sub _xform_out {
690 17466     17466   22379 my( $self, $val ) = @_;
691 17466 100       22962 return undef unless defined( $val );
692 17463 100       27552 if( index($val,'v') == 0 ) {
693 6706         20047 return substr( $val, 1 );
694             }
695 10757         15067 return $self->fetch( $val );
696             }
697              
698             sub _clean {
699 5     5   9 my( $self, $id ) = @_;
700 5         12 delete $self->{_DIRTY}{$id};
701             } #_clean
702              
703             sub _is_dirty {
704 3     3   7 my( $self, $obj ) = @_;
705 3 50       10 my $id = ref($obj) ? _get_id($obj) : $obj;
706 3         8 my $ans = $self->{_DIRTY}{$id};
707 3         8 $ans;
708             } #_is_dirty
709              
710             #
711             # Returns data structure representing object. References are integers. Values start with 'v'.
712             #
713             sub _raw_data {
714 82     82   103 my( $self, $obj ) = @_;
715 82         93 my $class = ref( $obj );
716 82 50       111 return unless $class;
717 82         109 my $id = $self->_get_id( $obj );
718 82 50       110 die unless $id;
719             # TODO : clean this up nice
720 82         89 my( $r, $is_array, $is_hash, $hash_type, $tied );
721 82 100       145 if( $class eq 'ARRAY' ) {
    100          
    50          
    50          
    50          
    50          
722 50         55 $tied = tied @$obj;
723 50 50       71 if( $tied ) {
724 50         55 $r = $tied->[1];
725 50         84 $is_array = ref( $tied );
726             } else {
727 0         0 die;
728             }
729             }
730             elsif( $class eq 'HASH' ) {
731 23         27 my $tied = tied %$obj;
732 23 50       29 if( ref( $tied ) eq 'Yote::BigHash' ) {
    0          
733 23         27 $r = $tied->[1];
734 23         28 $is_hash = $tied->[6];
735 23         30 $hash_type = $tied->[8];
736             } elsif( ref( $tied ) eq 'Yote::Hash' ) {
737             # is okey, the old hash is around for backwards compatability
738             } else {
739 0         0 die;
740             }
741             }
742             elsif( $class eq 'Yote::Array' ) {
743 0         0 $r = $obj->[1];
744 0         0 $is_array = 'Yote::Array';
745             }
746             elsif( $class eq 'Yote::ArrayGatekeeper' ) {
747 0         0 $tied = $obj;
748 0         0 $r = $obj->[1];
749 0         0 $is_array = 'Yote::ArrayGatekeeper';
750             }
751             elsif( $class eq 'Yote::Hash' ) {
752             # not seeing is_hash for the old hashes as there is no extra data for them
753 0         0 $r = $obj->[1];
754             }
755             elsif( $class eq 'Yote::BigHash' ) {
756 0         0 $r = $obj->[1];
757 0         0 $is_hash = $obj->[6];
758 0         0 $hash_type = $obj->[8];
759             }
760             else {
761 9         14 $r = $obj->{DATA};
762             }
763              
764 82 100       122 if( $is_hash ) {
765 23 100       31 if( $hash_type eq 'S' ) {
766 15 50       124 return join( "`", $hash_type, $is_hash, map { if( defined($_) ) { s/[\\]/\\\\/gs; s/`/\\`/gs; } $_ } %$r ), $r;
  1366         1661  
  1366         1360  
  1366         1263  
  1366         1695  
767             }
768 8 100       13 return join( "`", $hash_type, $is_hash, map { if( defined($_) ) { s/[\\]/\\\\/gs; s/`/\\`/gs; } $_ } @$r ), $r;
  84         100  
  21         24  
  21         22  
  84         133  
769             }
770 59 100       103 if( $is_array eq 'Yote::ArrayGatekeeper' ) {
    100          
771 26 50       55 return join( "`", $tied->[6],$tied->[4],$tied->[5],$tied->[7], map { if( defined($_) ) { s/[\\]/\\\\/gs; s/`/\\`/gs; } $_ } @$r ), $r;
  23         34  
  23         27  
  23         26  
  23         77  
772             } elsif( $is_array ) {
773 24 100       33 return join( "`", map { if( defined($_) ) { s/[\\]/\\\\/gs; s/`/\\`/gs; } $_ } @$r ), $r;
  631         748  
  548         575  
  548         524  
  631         804  
774             }
775 9 50       27 return join( "`", map { if( defined($_) ) { s/[\\]/\\\\/gs; s/`/\\`/gs; } $_ } %$r ), $r;
  30         50  
  30         46  
  30         41  
  30         88  
776              
777             } #_raw_data
778              
779              
780             sub _store_weak {
781 5677     5677   8679 my( $self, $id, $ref ) = @_;
782 5677 50       8747 die unless $ref;
783 5677         9329 $self->{_WEAK_REFS}{$id} = $ref;
784              
785 5677         12577 weaken( $self->{_WEAK_REFS}{$id} );
786             } #_store_weak
787              
788             # ---------------------------------------------------------------------------------------------------------------------
789              
790             =head1 NAME
791              
792             Yote::Obj - Generic container object for graph.
793              
794             =head1 DESCRIPTION
795              
796             A Yote::Obj is a container class that as a specific idiom for getters
797             and setters. This idiom is set up to avoid confusion and collision
798             with any method names.
799              
800             # sets the 'foo' field to the given value.
801             $obj->set_foo( { value => $store->newobj } );
802              
803             # returns the value for bar, and if none, sets it to 'default'
804             my $bar = $obj->get_bar( "default" );
805              
806             $obj->add_to_somelist( "Freddish" );
807             my $list = $obj->get_somelist;
808             $list->[ 0 ] == "Freddish";
809              
810              
811             $obj->remove_from_somelist( "Freddish" );
812              
813             =cut
814             package Yote::Obj;
815              
816 1     1   16 use strict;
  1         2  
  1         19  
817 1     1   4 use warnings;
  1         1  
  1         33  
818 1     1   8 no warnings 'uninitialized';
  1         3  
  1         192  
819              
820             #
821             # The string version of the yote object is simply its id. This allows
822             # objet ids to easily be stored as hash keys.
823             #
824             use overload
825 101     101   210 '""' => sub { shift->{ID} }, # for hash keys
826 0 0   0   0 eq => sub { ref($_[1]) && $_[1]->{ID} == $_[0]->{ID} },
827 0 0   0   0 ne => sub { ! ref($_[1]) || $_[1]->{ID} != $_[0]->{ID} },
828 0 0   0   0 '==' => sub { ref($_[1]) && $_[1]->{ID} == $_[0]->{ID} },
829 0 0   0   0 '!=' => sub { ! ref($_[1]) || $_[1]->{ID} != $_[0]->{ID} },
830 1     1   6 fallback => 1;
  1         1  
  1         8  
831              
832             =head2 absorb( hashref )
833              
834             pulls the hash data into this object.
835              
836             =cut
837             sub absorb {
838 4     4   7 my( $self, $data ) = @_;
839 4         6 my $obj_store = $self->{STORE};
840 4         18 for my $key ( sort keys %$data ) {
841 9         12 my $item = $data->{ $key };
842 9         14 $self->{DATA}{$key} = $obj_store->_xform_in( $item );
843             }
844 4         10 $obj_store->_dirty( $self, $self->{ID} );
845              
846             } #absorb
847              
848             sub id {
849 0     0   0 shift->{ID};
850             }
851              
852             =head2 set( $field, $value )
853              
854             Assigns the given value to the field in this object and returns the
855             assigned value.
856              
857             =cut
858             sub set {
859 0     0   0 my( $self, $fld, $val ) = @_;
860              
861 0         0 my $inval = $self->{STORE}->_xform_in( $val );
862 0 0       0 if( $self->{DATA}{$fld} ne $inval ) {
863 0         0 $self->{STORE}->_dirty( $self, $self->{ID} );
864             }
865              
866 0 0       0 unless( defined $inval ) {
867 0         0 delete $self->{DATA}{$fld};
868 0         0 return;
869             }
870 0         0 $self->{DATA}{$fld} = $inval;
871 0         0 return $self->{STORE}->_xform_out( $self->{DATA}{$fld} );
872             } #set
873              
874              
875             =head2 get( $field, $default-value )
876              
877             Returns the value assigned to the field, assinging the default
878             value to it if the value is currently not defined.
879              
880             =cut
881             sub get {
882 0     0   0 my( $self, $fld, $default ) = @_;
883 0         0 my $cur = $self->{DATA}{$fld};
884 0         0 my $store = $self->{STORE};
885 0 0 0     0 if( ! defined( $cur ) && defined( $default ) ) {
886 0 0       0 if( ref( $default ) ) {
887             # this must be done to make sure the reference is saved for cases where the reference has not yet made it to the store of things to save
888 0         0 $store->_dirty( $store->_get_id( $default ) );
889             }
890 0         0 $store->_dirty( $self, $self->{ID} );
891 0         0 $self->{DATA}{$fld} = $store->_xform_in( $default );
892             }
893 0         0 return $store->_xform_out( $self->{DATA}{$fld} );
894             } #get
895              
896              
897             # -----------------------
898             #
899             # Public Methods
900             # -----------------------
901             #
902             # Defines get_foo, set_foo, add_to_list, remove_from_list
903             #
904             sub AUTOLOAD {
905 12     12   48 my( $s, $arg ) = @_;
906 12         16 my $func = our $AUTOLOAD;
907              
908 12 100       111 if( $func =~/:add_to_(.*)/ ) {
    50          
    50          
    50          
    100          
    50          
909 1         3 my( $fld ) = $1;
910 1     1   409 no strict 'refs';
  1         3  
  1         95  
911             *$AUTOLOAD = sub {
912 1     1   3 my( $self, @vals ) = @_;
913 1         2 my $get = "get_$fld";
914 1         6 my $arry = $self->$get([]); # init array if need be
915 1         4 push( @$arry, @vals );
916 1         6 };
917 1     1   5 use strict 'refs';
  1         1  
  1         60  
918 1         3 goto &$AUTOLOAD;
919             } #add_to
920             elsif( $func =~/:add_once_to_(.*)/ ) {
921 0         0 my( $fld ) = $1;
922 1     1   4 no strict 'refs';
  1         2  
  1         94  
923             *$AUTOLOAD = sub {
924 0     0   0 my( $self, @vals ) = @_;
925 0         0 my $get = "get_$fld";
926 0         0 my $arry = $self->$get([]); # init array if need be
927 0         0 for my $val ( @vals ) {
928 0 0       0 unless( grep { $val eq $_ } @$arry ) {
  0         0  
929 0         0 push @$arry, $val;
930             }
931             }
932 0         0 };
933 1     1   5 use strict 'refs';
  1         1  
  1         50  
934 0         0 goto &$AUTOLOAD;
935             } #add_once_to
936             elsif( $func =~ /:remove_from_(.*)/ ) { #removes the first instance of the target thing from the list
937 0         0 my $fld = $1;
938 1     1   4 no strict 'refs';
  1         1  
  1         138  
939             *$AUTOLOAD = sub {
940 0     0   0 my( $self, @vals ) = @_;
941 0         0 my $get = "get_$fld";
942 0         0 my $arry = $self->$get([]); # init array if need be
943 0         0 for my $val (@vals ) {
944 0         0 for my $i (0..$#$arry) {
945 0 0       0 if( $arry->[$i] eq $val ) {
946 0         0 splice @$arry, $i, 1;
947 0         0 last;
948             }
949             }
950             }
951 0         0 };
952 1     1   5 use strict 'refs';
  1         2  
  1         63  
953 0         0 goto &$AUTOLOAD;
954             }
955             elsif( $func =~ /:remove_all_from_(.*)/ ) { #removes the first instance of the target thing from the list
956 0         0 my $fld = $1;
957 1     1   5 no strict 'refs';
  1         2  
  1         145  
958             *$AUTOLOAD = sub {
959 0     0   0 my( $self, @vals ) = @_;
960 0         0 my $get = "get_$fld";
961 0         0 my $arry = $self->$get([]); # init array if need be
962 0         0 for my $val (@vals) {
963 0         0 my $count = grep { $_ eq $val } @$arry;
  0         0  
964 0         0 while( $count ) {
965 0         0 for my $i (0..$#$arry) {
966 0 0       0 if( $arry->[$i] eq $val ) {
967 0         0 --$count;
968 0         0 splice @$arry, $i, 1;
969 0 0       0 last unless $count;
970             }
971             }
972             }
973             }
974 0         0 };
975 1     1   6 use strict 'refs';
  1         1  
  1         55  
976 0         0 goto &$AUTOLOAD;
977             }
978             elsif ( $func =~ /:set_(.*)/ ) {
979 1         6 my $fld = $1;
980 1     1   5 no strict 'refs';
  1         1  
  1         126  
981             *$AUTOLOAD = sub {
982 1     1   4 my( $self, $val ) = @_;
983 1         36 my $inval = $self->{STORE}->_xform_in( $val );
984 1 50       13 $self->{STORE}->_dirty( $self, $self->{ID} ) if $self->{DATA}{$fld} ne $inval;
985 1 50       5 unless( defined $inval ) {
986 0         0 delete $self->{DATA}{$fld};
987 0         0 return;
988             }
989 1         4 $self->{DATA}{$fld} = $inval;
990 1         6 return $self->{STORE}->_xform_out( $self->{DATA}{$fld} );
991 1         11 };
992 1         6 goto &$AUTOLOAD;
993             }
994             elsif( $func =~ /:get_(.*)/ ) {
995 10         22 my $fld = $1;
996 1     1   5 no strict 'refs';
  1         2  
  1         169  
997             *$AUTOLOAD = sub {
998 27     27   1403 my( $self, $init_val ) = @_;
999 27 100 66     271 if( ! defined( $self->{DATA}{$fld} ) && defined($init_val) ) {
1000 3 50       8 if( ref( $init_val ) ) {
1001             # this must be done to make sure the reference is saved for cases where the reference has not yet made it to the store of things to save
1002 3         10 $self->{STORE}->_dirty( $init_val, $self->{STORE}->_get_id( $init_val ) );
1003             }
1004 3         9 $self->{STORE}->_dirty( $self, $self->{ID} );
1005 3         10 $self->{DATA}{$fld} = $self->{STORE}->_xform_in( $init_val );
1006             }
1007 27         55 return $self->{STORE}->_xform_out( $self->{DATA}{$fld} );
1008 10         55 };
1009 1     1   9 use strict 'refs';
  1         2  
  1         251  
1010 10         34 goto &$AUTOLOAD;
1011             }
1012             else {
1013 0         0 die "Unknown Yote::Obj function '$func'";
1014             }
1015              
1016             } #AUTOLOAD
1017              
1018             # -----------------------
1019             #
1020             # Overridable Methods
1021             # -----------------------
1022              
1023             =head2 _init
1024              
1025             This is called the first time an object is created. It is not
1026             called when the object is loaded from storage. This can be used
1027             to set up defaults. This is meant to be overridden.
1028              
1029             =cut
1030       4     sub _init {}
1031              
1032             =head2 _init
1033              
1034             This is called each time the object is loaded from the data store.
1035             This is meant to be overridden.
1036              
1037             =cut
1038       21     sub _load {}
1039              
1040              
1041              
1042             # -----------------------
1043             #
1044             # Private Methods
1045             #
1046             # -----------------------
1047              
1048              
1049             sub _new { #new Yote::Obj
1050 4     4   9 my( $pkg, $obj_store, $data, $_id ) = @_;
1051              
1052 4   33     14 my $class = ref($pkg) || $pkg;
1053 4         14 my $obj = bless {
1054             DATA => {},
1055             STORE => $obj_store,
1056             }, $class;
1057 4   66     33 $obj->{ID} = $_id || $obj_store->_get_id( $obj );
1058 4         11 $obj_store->_dirty( $obj, $obj->{ID} );
1059 4         10 $obj->_init(); #called the first time the object is created.
1060              
1061 4 50       9 if( ref( $data ) eq 'HASH' ) {
    0          
1062 4         10 $obj->absorb( $data );
1063             } elsif( $data ) {
1064 0         0 die "Yote::Obj::new must be called with hash or undef. Was called with '". ref( $data ) . "'";
1065             }
1066 4         18 return $obj;
1067             } #_new
1068              
1069             sub _store {
1070 0     0   0 return shift->{STORE};
1071             }
1072              
1073             #
1074             # Called by the object provider; returns a Yote::Obj the object
1075             # provider will stuff data into. Takes the class and id as arguments.
1076             #
1077             sub _instantiate {
1078 21     21   115 bless { ID => $_[1], DATA => {}, STORE => $_[2] }, $_[0];
1079             } #_instantiate
1080              
1081             sub DESTROY {
1082 24     24   49 my $self = shift;
1083              
1084 24         123 delete $self->{STORE}{_WEAK_REFS}{$self->{ID}};
1085             }
1086              
1087              
1088             # ---------------------------------------------------------------------------------------------------------------------
1089              
1090             package Yote::Array;
1091              
1092             ############################################################################################################
1093             # This module is used transparently by Yote to link arrays into its graph structure. This is not meant to #
1094             # be called explicitly or modified. #
1095             ############################################################################################################
1096              
1097 1     1   5 use strict;
  1         1  
  1         18  
1098 1     1   3 use warnings;
  1         2  
  1         24  
1099              
1100 1     1   4 no warnings 'uninitialized';
  1         1  
  1         30  
1101 1     1   290 use Tie::Array;
  1         912  
  1         653  
1102              
1103             sub TIEARRAY {
1104 39     39   91 my( $class, $obj_store, $id, @list ) = @_;
1105 39         61 my $storage = [];
1106              
1107             # once the array is tied, an additional data field will be added
1108             # so obj will be [ $id, $storage, $obj_store ]
1109 39         83 my $obj = bless [$id,$storage,$obj_store], $class;
1110 39         72 for my $item (@list) {
1111 38         55 push( @$storage, $item );
1112             }
1113 39         89 return $obj;
1114             }
1115              
1116             sub FETCH {
1117 2979     2979   3397 my( $self, $idx ) = @_;
1118 2979         3724 return $self->[2]->_xform_out ( $self->[1][$idx] );
1119             }
1120              
1121             sub FETCHSIZE {
1122 304     304   308 my $self = shift;
1123 304         294 return scalar(@{$self->[1]});
  304         482  
1124             }
1125              
1126             sub STORE {
1127 16     16   21 my( $self, $idx, $val ) = @_;
1128 16         39 $self->[2]->_dirty( $self->[2]{_WEAK_REFS}{$self->[0]}, $self->[0] );
1129 16         26 $self->[1][$idx] = $self->[2]->_xform_in( $val );
1130             }
1131             sub STORESIZE {
1132 11     11   19 my( $self, $size ) = @_;
1133 11         12 my $aref = $self->[1];
1134 11         26 $#$aref = $size-1;
1135             }
1136              
1137             sub EXISTS {
1138 4     4   5 my( $self, $idx ) = @_;
1139 4         19 return defined( $self->[1][$idx] );
1140             }
1141             sub DELETE {
1142 1     1   2 my( $self, $idx ) = @_;
1143 1         4 $self->[2]->_dirty( $self->[2]{_WEAK_REFS}{$self->[0]}, $self->[0] );
1144 1         3 delete $self->[1][$idx];
1145             }
1146              
1147             sub CLEAR {
1148 0     0   0 my $self = shift;
1149 0         0 $self->[2]->_dirty( $self->[2]{_WEAK_REFS}{$self->[0]}, $self->[0] );
1150 0         0 @{$self->[1]} = ();
  0         0  
1151             }
1152             sub PUSH {
1153 288     288   457 my( $self, @vals ) = @_;
1154 288         584 $self->[2]->_dirty( $self->[2]{_WEAK_REFS}{$self->[0]}, $self->[0] );
1155 288         300 push( @{$self->[1]}, map { $self->[2]->_xform_in($_) } @vals );
  288         489  
  562         782  
1156             }
1157             sub POP {
1158 2     2   3 my $self = shift;
1159 2         7 $self->[2]->_dirty( $self->[2]{_WEAK_REFS}{$self->[0]}, $self->[0] );
1160 2         3 return $self->[2]->_xform_out( pop @{$self->[1]} );
  2         5  
1161             }
1162             sub SHIFT {
1163 6     6   7 my( $self ) = @_;
1164 6         14 $self->[2]->_dirty( $self->[2]{_WEAK_REFS}{$self->[0]}, $self->[0] );
1165 6         7 my $val = splice @{$self->[1]}, 0, 1;
  6         11  
1166 6         9 return $self->[2]->_xform_out( $val );
1167             }
1168             sub UNSHIFT {
1169 0     0   0 my( $self, @vals ) = @_;
1170 0         0 $self->[2]->_dirty( $self->[2]{_WEAK_REFS}{$self->[0]}, $self->[0] );
1171 0         0 unshift @{$self->[1]}, map {$self->[2]->_xform_in($_)} @vals;
  0         0  
  0         0  
1172             }
1173             sub SPLICE {
1174 12     12   22 my( $self, $offset, $length, @vals ) = @_;
1175 12         27 $self->[2]->_dirty( $self->[2]{_WEAK_REFS}{$self->[0]}, $self->[0] );
1176 12         14 return map { $self->[2]->_xform_out($_) } splice @{$self->[1]}, $offset, $length, map {$self->[2]->_xform_in($_)} @vals;
  0         0  
  12         18  
  24         36  
1177             }
1178       0     sub EXTEND {}
1179              
1180             sub DESTROY {
1181 33     33   42 my $self = shift;
1182 33         158 delete $self->[2]->{_WEAK_REFS}{$self->[0]};
1183             }
1184              
1185             # ---------------------------------------------------------------------------------------------------------------------
1186              
1187             package Yote::ArrayGatekeeper;
1188              
1189             ############################################################################################################
1190             # This module is used transparently by Yote to link arrays into its graph structure. This is not meant to #
1191             # be called explicitly or modified. #
1192             ############################################################################################################
1193              
1194 1     1   6 use strict;
  1         2  
  1         17  
1195 1     1   3 use warnings;
  1         2  
  1         23  
1196              
1197 1     1   4 no warnings 'uninitialized';
  1         11  
  1         24  
1198 1     1   4 use Tie::Array;
  1         1  
  1         37  
1199              
1200             $Yote::ArrayGatekeeper::BLOCK_SIZE = 1024;
1201             $Yote::ArrayGatekeeper::BLOCK_COUNT = 1024;
1202              
1203             use constant {
1204 1         1849 ID => 0,
1205             BLOCKS => 1,
1206             DSTORE => 2,
1207             CAPACITY => 3,
1208             BLOCK_COUNT => 4,
1209             BLOCK_SIZE => 5,
1210             ITEM_COUNT => 6,
1211             LEVEL => 7,
1212 1     1   4 };
  1         1  
1213              
1214             sub TIEARRAY {
1215 37     37   95 my( $class, $obj_store, $id, $item_count, $block_count, $block_size, $level, @list ) = @_;
1216              
1217 37   66     118 $block_size ||= $Yote::ArrayGatekeeper::BLOCK_SIZE;
1218 37   66     90 $block_count ||= $Yote::ArrayGatekeeper::BLOCK_COUNT;
1219 37   100     101 $item_count ||= 0;
1220 37   100     85 $level ||= 1;
1221 37         58 my $capacity = $block_size * $block_count;
1222              
1223 37         59 my $blocks = [@list];
1224              
1225             # once the array is tied, an additional data field will be added
1226             # so obj will be [ $id, $storage, $obj_store ]
1227 37         90 my $obj = bless [$id,$blocks,$obj_store,$capacity,$block_count,$block_size, $item_count, $level], $class;
1228 37         99 return $obj;
1229             } #TIEARRAY
1230              
1231             sub _ensure_capacity {
1232 3841     3841   4132 my( $self, $size ) = @_;
1233              
1234 3841 100       5809 if( $size > $self->[CAPACITY] ) {
1235 1         3 my $store = $self->[DSTORE];
1236             #
1237             # make a new gatekeeper and moves the buckets of this
1238             # one into the new gatekeeper
1239             #
1240 1         3 my $new_id = $store->{_DATASTORE}->_get_id( 'Yote::ArrayGatekeeper' );
1241              
1242 1         146 my $newkeeper = [];
1243 1         3 tie @$newkeeper, 'Yote::ArrayGatekeeper', $store, $new_id, $self->[ITEM_COUNT], $self->[BLOCK_COUNT], $self->[BLOCK_SIZE], $self->[LEVEL], @{$self->[BLOCKS]};
  1         6  
1244              
1245 1         4 $store->_store_weak( $new_id, $newkeeper );
1246 1         4 $store->_dirty( $store->{_WEAK_REFS}{$new_id}, $new_id );
1247              
1248 1         2 $self->[LEVEL]++;
1249 1         2 $self->[BLOCKS] = [ $new_id ];
1250 1         2 $self->[BLOCK_SIZE] = $self->[CAPACITY];
1251 1         3 $self->[CAPACITY] = $self->[BLOCK_SIZE] * $self->[BLOCK_COUNT];
1252 1         3 $store->_dirty( $store->{_WEAK_REFS}{$self->[ID]}, $self->[ID] );
1253             }
1254              
1255             } #_ensure_capacity
1256              
1257             sub _dirty {
1258 328     328   336 my $self = shift;
1259 328         583 $self->[DSTORE]->_dirty( $self->[DSTORE]{_WEAK_REFS}{$self->[ID]}, $self->[ID] );
1260             }
1261              
1262             # returns the block object ( behind the tied ), the block index
1263             # and the first item index in the block
1264             sub _block {
1265 3552     3552   4062 my( $self, $idx ) = @_;
1266 3552         5605 $self->_ensure_capacity( $idx + 1 );
1267              
1268              
1269 3552         5230 my $block_idx = int($idx / $self->[BLOCK_SIZE]); #block size
1270              
1271 3552         3672 my $store = $self->[DSTORE];
1272              
1273 3552         3775 my $block_id = $self->[BLOCKS][$block_idx];
1274 3552         3345 my $block;
1275 3552 100       4110 if( $block_id ) {
    100          
1276 3523         4284 $block = $store->fetch($block_id);
1277             } elsif( $self->[LEVEL] == 1 ) {
1278 28         51 $block = [];
1279 28         56 my $block_id = $store->{_DATASTORE}->_get_id( "ARRAY" );
1280 28         3821 tie @$block, 'Yote::Array', $store, $block_id;
1281 28         67 $store->_store_weak( $block_id, $block );
1282 28         79 $self->[2]->_dirty( $self->[2]{_WEAK_REFS}{$block_id}, $block_id );
1283              
1284 28         54 $self->[BLOCKS][$block_idx] = $block_id;
1285 28         49 $self->_dirty;
1286             } else {
1287 1         3 my $firstblock = tied( @{$store->fetch($self->[BLOCKS][0])} );
  1         3  
1288              
1289 1         2 $block = [];
1290 1         3 my $block_id = $store->{_DATASTORE}->_get_id( "Yote::ArrayGatekeeper" );
1291 1         128 tie @$block, 'Yote::ArrayGatekeeper', $store, $block_id, 0, $firstblock->[BLOCK_COUNT], $firstblock->[BLOCK_SIZE], $firstblock->[LEVEL];
1292 1         4 $store->_store_weak( $block_id, $block );
1293 1         3 $self->[2]->_dirty( $self->[2]{_WEAK_REFS}{$block_id}, $block_id );
1294              
1295 1         2 $self->[BLOCKS][$block_idx] = $block_id;
1296 1         3 $self->_dirty;
1297             }
1298 3552         6719 return ( $block, tied( @$block), $block_idx, $block_idx * $self->[BLOCK_SIZE] );
1299             }
1300              
1301             sub FETCH {
1302 3178     3178   4102 my( $self, $idx ) = @_;
1303 3178         3877 my( $block, $tied_block, $block_idx, $block_start_idx ) = $self->_block( $idx );
1304 3178         4826 return $tied_block->FETCH( $idx - $block_start_idx );
1305             }
1306              
1307             sub FETCHSIZE {
1308 3529     3529   16296 shift->[ITEM_COUNT];
1309             }
1310              
1311             sub STORE {
1312 30     30   296 my( $self, $idx, $val ) = @_;
1313 30         43 my( $block, $tied_block, $block_idx, $block_start_idx ) = $self->_block( $idx );
1314 30         39 my $last_block_idx = $self->[ITEM_COUNT] - 1;
1315 30         60 $tied_block->STORE( $idx - $block_start_idx, $val );
1316 30 100       48 if( $idx > $last_block_idx ) {
1317 2         10 $self->[ITEM_COUNT] = $idx + 1;
1318             }
1319             }
1320             sub STORESIZE {
1321 6     6   9 my( $self, $size ) = @_;
1322             # fixes the size of the array
1323 6 100       15 if( $size < $self->[ITEM_COUNT] ) {
1324 4         9 my( $block, $tied_block, $block_idx, $block_start_idx ) = $self->_block( $self->[BLOCK_SIZE] * int($size/$self->[BLOCK_SIZE]) );
1325 4         5 my $blocks = $self->[BLOCKS];
1326 4         11 $#$blocks = $block_idx; #removes further blocks
1327 4         12 $tied_block->STORESIZE( $size - $block_start_idx );
1328 4         7 $self->_dirty;
1329 4         6 $self->[ITEM_COUNT] = $size;
1330             }
1331             }
1332              
1333             sub EXISTS {
1334 8     8   265 my( $self, $idx ) = @_;
1335 8         15 my( $block, $tied_block, $block_idx, $block_start_idx ) = $self->_block( $idx );
1336 8         18 return $tied_block->EXISTS( $idx - $block_start_idx );
1337             }
1338             sub DELETE {
1339 2     2   426 my( $self, $idx ) = @_;
1340 2         5 my( $block, $tied_block, $block_idx, $block_start_idx ) = $self->_block( $idx );
1341              
1342 2 50       7 if( (1+$idx) == $self->[ITEM_COUNT] ) {
1343             # in this case, it is removing the last item here. so shorten the
1344             # array until the
1345 0         0 my $curr_block_start_idx = $block_start_idx;
1346 0         0 my $blocks = $self->[BLOCKS];
1347 0         0 my $prev_idx = $idx - 1;
1348 0   0     0 while( ! $self->EXISTS( $prev_idx ) && $prev_idx >= 0 ) {
1349 0 0       0 if( $curr_block_start_idx == $prev_idx ) {
1350 0         0 pop @$blocks;
1351             }
1352 0         0 $self->[ITEM_COUNT]--;
1353 0         0 $prev_idx--;
1354             }
1355 0         0 $self->_dirty;
1356             }
1357 2         7 return $tied_block->DELETE( $idx - $block_start_idx );
1358             }
1359              
1360             sub CLEAR {
1361 1     1   1 my $self = shift;
1362 1         4 $self->_dirty;
1363 1         2 $self->[ITEM_COUNT] = 0;
1364 1         1 @{$self->[BLOCKS]} = ();
  1         4  
1365             }
1366             sub PUSH {
1367 281     281   524 my( $self, @vals ) = @_;
1368              
1369 281 50       416 return unless @vals;
1370              
1371 281         539 $self->_ensure_capacity( $self->[ITEM_COUNT] + @vals );
1372              
1373 281         424 my( $block, $tied_block, $block_idx, $block_start_idx ) = $self->_block( $self->[ITEM_COUNT] );
1374              
1375 281         378 $self->[ITEM_COUNT] += @vals;
1376 281         463 $self->_dirty;
1377 281         290 my $idx_at = 0;
1378 281         492 while( @vals ) {
1379              
1380 288         318 $idx_at += $self->[BLOCK_SIZE];
1381 288         410 my $room = $self->[BLOCK_SIZE] - ($tied_block->FETCHSIZE);
1382 288         444 my( @part ) = splice @vals, 0, $room;
1383 288         519 $tied_block->PUSH( @part );
1384 288 100       1125 if( @vals ) {
1385 7         13 ($block,$tied_block,undef,undef) = $self->_block( $idx_at );
1386             }
1387             }
1388             }
1389             sub POP {
1390 4     4   6 my $self = shift;
1391              
1392 4         7 my $blocks = $self->[BLOCKS];
1393 4 50       7 if( @$blocks ) {
1394 4         5 my $lastblock = tied @{$self->[DSTORE]->fetch($blocks->[$#$blocks])};
  4         9  
1395 4         10 my $val = $lastblock->POP;
1396 4 50       10 if( @$lastblock == 0 ) {
1397 0         0 pop @$blocks;
1398             }
1399 4         5 $self->[ITEM_COUNT]--;
1400 4         6 $self->_dirty;
1401 4         8 return $val;
1402             }
1403 0         0 return;
1404             }
1405             sub SHIFT {
1406 3     3   5 my( $self ) = @_;
1407 3         4 my $blocks = $self->[1];
1408 3         3 my $store = $self->[DSTORE];
1409 3 50       18 if( @$blocks ) {
1410 3         4 my $block = tied( @{$store->fetch( $blocks->[0] )} );
  3         6  
1411 3         10 my $val = $block->SHIFT;
1412 3         9 for( my $i=1; $i<@$blocks; $i++ ) {
1413 5         6 my $now_block = tied( @{$store->fetch( $blocks->[$i] )} );
  5         6  
1414 5         6 my $prev_block = tied( @{$store->fetch( $blocks->[$i-1] )} );
  5         10  
1415 5         8 $prev_block->PUSH( $now_block->SHIFT );
1416 5 50       16 if( $#$blocks == -1 ) {
1417 0         0 pop @$blocks;
1418 0         0 last;
1419             }
1420             }
1421 3         5 $self->[ITEM_COUNT]--;
1422 3         7 $self->_dirty;
1423 3         7 return $val;
1424             }
1425 0         0 return;
1426             }
1427              
1428             sub UNSHIFT {
1429 1     1   5 shift->_unshift(0,@_);
1430             }
1431              
1432             sub _unshift {
1433 6     6   12 my( $self, $offset, @vals ) = @_;
1434              
1435 6 50       12 return unless @vals;
1436              
1437 6         10 my $newcount = $self->[ITEM_COUNT] + @vals;
1438 6         12 $self->_ensure_capacity( $newcount );
1439              
1440 6         12 my( $block, $tied_block, $block_idx, $block_start_idx ) = $self->_block( $offset );
1441              
1442 6 100       12 if( $self->[LEVEL] == 1 ) {
1443 4         9 while( @vals ) {
1444 12         22 $tied_block->SPLICE( $offset , 0, @vals );
1445 12         16 $offset = 0;
1446              
1447 12 100       22 if( $#$block >= $self->[BLOCK_SIZE] ) {
1448 8         16 (@vals) = @$block[$self->[BLOCK_SIZE]..$#$block];
1449 8         24 $#$block = $self->[BLOCK_SIZE] - 1;
1450              
1451 8         15 ( $block, $tied_block, $block_idx, $block_start_idx ) = $self->_block( (1+$block_idx)*$self->[BLOCK_SIZE] );
1452              
1453             } else {
1454 4         10 @vals = ();
1455             }
1456             }
1457             } else {
1458 2         4 while( @vals ) {
1459 4         7 my $overflow = (@vals + $tied_block->[ITEM_COUNT]) - $self->[BLOCK_SIZE];
1460 4 100       8 if( $overflow > 0 ) {
1461 2         4 my $cut_index = $self->[BLOCK_SIZE] - @vals;
1462 2         6 my( @backlog ) = @$block[$cut_index..$#$block];
1463              
1464 2         8 $tied_block->STORESIZE( $cut_index );
1465              
1466 2         4 my @additions = splice @vals, 0, (1+$cut_index);
1467              
1468 2         13 $tied_block->_unshift( $offset, @additions ); #offset is only on the first block
1469 2         3 $offset = 0;
1470 2         3 push @vals, @backlog;
1471              
1472 2 50       6 if( @vals ) {
1473 2         5 ( $block, $tied_block, $block_idx, $block_start_idx ) = $self->_block( (1+$block_idx)*$self->[BLOCK_SIZE] );
1474             }
1475             } else {
1476 2         7 $tied_block->SPLICE( $offset, 0, @vals );
1477 2         5 @vals = ();
1478             }
1479             }
1480             }
1481              
1482 6         8 $self->[ITEM_COUNT] = $newcount;
1483 6         9 $self->_dirty;
1484              
1485             } #UNSHIFT
1486              
1487             sub SPLICE {
1488 3     3   7 my( $self, $offset, $remove_length, @vals ) = @_;
1489              
1490 3 50 33     9 return unless @vals || $remove_length;
1491              
1492 3         3 my @removed;
1493              
1494 3         4 my $delta = @vals - $remove_length;
1495 3         6 my $new_size = $self->[ITEM_COUNT] + $delta;
1496              
1497              
1498 3 100       7 if( $delta > 0 ) {
1499 2         3 $self->_ensure_capacity( $new_size );
1500             }
1501              
1502             #
1503             # add things
1504             #
1505              
1506 3 50       6 if( @vals ) {
1507             # this adjusts the item count
1508 3         86 $self->_unshift( $offset, @vals );
1509             }
1510              
1511 3 100       7 if( $remove_length > 0 ) {
1512             #
1513             # remove things by shifting things down over the removed items
1514             #
1515 1         2 my $remove_start_idx = $offset + @vals;
1516 1         2 my $remove_end_idx = $remove_start_idx + $remove_length;
1517 1         2 my $new_last_idx = $new_size - 1;
1518              
1519 1         3 while ( $remove_start_idx <= $new_last_idx ) {
1520            
1521 13         18 my( $remstart ) = $self->_block( $remove_start_idx );
1522 13         21 my( $remend ) = $self->_block( $remove_end_idx + 1 );
1523              
1524 13         19 my $removed = $self->FETCH( $remove_start_idx );
1525 13 100       22 if( @removed < $remove_length ) {
1526 5         9 push @removed, $removed;
1527             }
1528 13         19 $self->STORE( $remove_start_idx, $self->FETCH( $remove_end_idx ) );
1529 13         16 $remove_start_idx++;
1530 13         20 $remove_end_idx++;
1531             }
1532             }
1533             #
1534             # Trim this array to its new size
1535             #
1536 3         51 $self->STORESIZE( $new_size );
1537              
1538 3         9 return @removed;
1539              
1540             } #SPLICE
1541              
1542       0     sub EXTEND {
1543             }
1544              
1545             sub DESTROY {
1546 34     34   43 my $self = shift;
1547 34         150 delete $self->[2]->{_WEAK_REFS}{$self->[0]};
1548             }
1549              
1550             # ---------------------------------------------------------------------------------------
1551              
1552             package Yote::Hash;
1553              
1554             ######################################################################################
1555             # This module is used transparently by Yote to link hashes into its graph structure. #
1556             # This is not meant to be called explicitly or modified. #
1557             ######################################################################################
1558              
1559 1     1   7 use strict;
  1         1  
  1         17  
1560 1     1   4 use warnings;
  1         1  
  1         23  
1561              
1562 1     1   4 no warnings 'uninitialized';
  1         1  
  1         26  
1563              
1564 1     1   301 use Tie::Hash;
  1         689  
  1         374  
1565              
1566             sub TIEHASH {
1567 0     0   0 my( $class, $obj_store, $id, %hash ) = @_;
1568              
1569 0         0 my $storage = {};
1570             # after $obj_store is a list reference of
1571             # id, data, store
1572 0         0 my $obj = bless [ $id, $storage,$obj_store ], $class;
1573 0         0 for my $key (keys %hash) {
1574 0         0 $storage->{$key} = $hash{$key};
1575             }
1576 0         0 return $obj;
1577             }
1578              
1579             sub STORE {
1580 0     0   0 my( $self, $key, $val ) = @_;
1581 0         0 $self->[2]->_dirty( $self->[2]{_WEAK_REFS}{$self->[0]}, $self->[0] );
1582 0         0 $self->[1]{$key} = $self->[2]->_xform_in( $val );
1583             }
1584              
1585             sub FIRSTKEY {
1586 0     0   0 my $self = shift;
1587 0         0 my $a = scalar keys %{$self->[1]};
  0         0  
1588 0         0 my( $k, $val ) = each %{$self->[1]};
  0         0  
1589 0 0       0 return wantarray ? ( $k => $val ) : $k;
1590             }
1591             sub NEXTKEY {
1592 0     0   0 my $self = shift;
1593 0         0 my( $k, $val ) = each %{$self->[1]};
  0         0  
1594 0 0       0 return wantarray ? ( $k => $val ) : $k;
1595             }
1596              
1597             sub FETCH {
1598 0     0   0 my( $self, $key ) = @_;
1599 0         0 return $self->[2]->_xform_out( $self->[1]{$key} );
1600             }
1601              
1602             sub EXISTS {
1603 0     0   0 my( $self, $key ) = @_;
1604 0         0 return defined( $self->[1]{$key} );
1605             }
1606             sub DELETE {
1607 0     0   0 my( $self, $key ) = @_;
1608 0         0 $self->[2]->_dirty( $self->[2]{_WEAK_REFS}{$self->[0]}, $self->[0] );
1609 0         0 return delete $self->[1]{$key};
1610             }
1611             sub CLEAR {
1612 0     0   0 my $self = shift;
1613 0         0 $self->[2]->_dirty( $self->[2]{_WEAK_REFS}{$self->[0]}, $self->[0] );
1614 0         0 %{$self->[1]} = ();
  0         0  
1615             }
1616              
1617             sub DESTROY {
1618 0     0   0 my $self = shift;
1619 0         0 delete $self->[2]->{_WEAK_REFS}{$self->[0]};
1620             }
1621              
1622              
1623             # ---------------------------------------------------------------------------------------
1624              
1625             package Yote::BigHash;
1626              
1627             ######################################################################################
1628             # This module is used transparently by Yote to link hashes into its graph structure. #
1629             # This is not meant to be called explicitly or modified. #
1630             ######################################################################################
1631              
1632 1     1   6 use strict;
  1         1  
  1         21  
1633 1     1   4 use warnings;
  1         1  
  1         20  
1634              
1635 1     1   4 no warnings 'uninitialized';
  1         1  
  1         26  
1636 1     1   4 no warnings 'numeric';
  1         1  
  1         27  
1637              
1638 1     1   7 use Tie::Hash;
  1         1  
  1         33  
1639              
1640             $Yote::BigHash::SIZE = 977;
1641              
1642             use constant {
1643 1         321 ID => 0,
1644             DATA => 1,
1645             DSTORE => 2,
1646             NEXT => 3,
1647             KEYS => 4,
1648             DEEP => 5,
1649             SIZE => 6,
1650             THRESH => 7,
1651             TYPE => 8,
1652 1     1   3 };
  1         2  
1653              
1654             sub _bucket {
1655 5057     5057   6702 my( $self, $key, $return_undef ) = @_;
1656 5057         5194 my $hval = 0;
1657 5057         9681 foreach (split //, $key) {
1658 10078         13231 $hval = $hval*33 - ord($_);
1659             }
1660              
1661 5057         7050 $hval = $hval % $self->[SIZE];
1662 5057         6623 my $obj_id = $self->[DATA][$hval];
1663 5057         5555 my $store = $self->[DSTORE];
1664 5057 100       7078 unless( $obj_id ) {
1665 21 50       30 return ($hval, undef) if $return_undef;
1666 21         27 my $bucket = [];
1667 21         37 my $id = $store->_get_id( $bucket );
1668 21         41 $self->[DATA][$hval] = $id;
1669 21         45 return $hval, $bucket;
1670             }
1671 5036         7116 $hval, $store->_xform_out( $obj_id );
1672             }
1673              
1674             sub TIEHASH {
1675 5580     5580   47603 my( $class, $obj_store, $id, $type, $size, @fetch_buckets ) = @_;
1676              
1677 5580   100     8762 $type ||= 'S'; #small
1678 5580   66     7565 $size ||= $Yote::BigHash::SIZE;
1679              
1680             my $deep_buckets = $type eq 'B' ?
1681 5580 100 100     8799 [ map { $_ > 0 && ref( $obj_store->_xform_out($_) ) eq 'HASH' ? 1 : 0 } @fetch_buckets ] : [];
  15232 100       33046  
1682              
1683 1     1   6 no warnings 'numeric';
  1         2  
  1         1220  
1684             #
1685             # after $obj_store is a list reference of
1686             # id, data, store
1687 5580         6597 my $obj;
1688 5580 100       7091 if( $type eq 'S' ) {
1689 4194         60778 $obj = bless [ $id, {@fetch_buckets}, $obj_store, [], 0, $deep_buckets, $size, $size * 2, $type ], $class;
1690             }
1691             else {
1692 1386         5534 $obj = bless [ $id, [@fetch_buckets], $obj_store, [], 0, $deep_buckets, $size, $size * 2, $type ], $class;
1693             }
1694              
1695 5580         27249 return $obj;
1696             }
1697              
1698             sub STORE {
1699 2268     2268   6924 my( $self, $key, $val ) = @_;
1700              
1701 2268         2661 my $store = $self->[DSTORE];
1702 2268         4217 $store->_dirty( $store->{_WEAK_REFS}{$self->[ID]}, $self->[ID] );
1703              
1704 2268 100       3228 if( $self->[TYPE] eq 'S' ) {
1705 904         954 my $data = $self->[DATA];
1706 904 100       1306 $self->[KEYS]++ unless exists $data->{$key}; #obj count
1707 904         1114 $data->{$key} = $store->_xform_in($val);
1708              
1709 904 100       1545 if( $self->[KEYS] > $self->[THRESH] ) {
1710 8         11 $self->[TYPE] = 'B'; #big
1711             #convert to buckets
1712 8         13 $self->[DATA] = [];
1713 8         55 for my $key (keys %$data) {
1714 218         394 $self->STORE( $key, $store->_xform_out($data->{$key}) );
1715             }
1716             }
1717 904         1999 return;
1718             }
1719              
1720 1364         1867 my( $bid, $bucket ) = $self->_bucket( $key );
1721              
1722 1364 100       2080 if( $self->[DEEP][$bid] ) {
1723 1077 50       2076 $self->[KEYS]++ unless exists $bucket->{$key}; #obj count
1724 1077         2820 $bucket->{$key} = $val;
1725             } else {
1726 287 100       479 if( @$bucket > $self->[THRESH] ) {
1727 21         33 my $newbuck = {};
1728 21         36 my $id = $store->_get_id( $newbuck );
1729              
1730 21         36 my $tied = tied %$newbuck;
1731 21         30 $tied->[SIZE] = $self->[SIZE] * 2;
1732 21         31 $tied->[THRESH] = $tied->[SIZE] * 2;
1733 21         55 for( my $i=0; $i<$#$bucket; $i+=2 ) {
1734 266         481 $newbuck->{$bucket->[$i]} = $bucket->[$i+1];
1735             }
1736 21 50       58 $self->[KEYS]++ unless exists $newbuck->{$key};
1737 21         60 $newbuck->{$key} = $val;
1738 21         42 $self->[DEEP][$bid] = 1;
1739 21         27 $self->[DATA][$bid] = $id;
1740 21         77 return;
1741             }
1742 266         452 for( my $i=0; $i<$#$bucket; $i+=2 ) {
1743 1666 50       2810 if( $bucket->[$i] eq $key ) {
1744 0         0 $bucket->[$i+1] = $val;
1745 0         0 return;
1746             }
1747             }
1748 266         319 $self->[KEYS]++; #obj count
1749 266         381 push @$bucket, $key, $val;
1750             }
1751             } #STORE
1752              
1753             sub FIRSTKEY {
1754 45     45   243 my $self = shift;
1755              
1756 45 100       76 if( $self->[TYPE] eq 'S' ) {
1757 29         32 my $a = scalar keys %{$self->[DATA]}; #reset things
  29         59  
1758 29         38 my( $k, $val ) = each %{$self->[DATA]};
  29         59  
1759 29 50       69 return wantarray ? ( $k => $val ) : $k;
1760             }
1761 16         21 @{ $self->[NEXT] } = ( 0, undef, undef );
  16         30  
1762 16         34 return $self->NEXTKEY;
1763             }
1764              
1765             sub NEXTKEY {
1766 4114     4114   4329 my $self = shift;
1767              
1768 4114 100       5677 if( $self->[TYPE] eq 'S' ) {
1769 1370         1278 my( $k, $val ) = each %{$self->[DATA]};
  1370         2211  
1770 1370 50       2531 return wantarray ? ( $k, $val ) : $k;
1771             }
1772              
1773 2744         2783 my $buckets = $self->[DATA];
1774 2744         2698 my $store = $self->[DSTORE];
1775 2744         2783 my $current = $self->[NEXT];
1776              
1777 2744         3402 my( $bucket_idx, $idx_in_bucket ) = @$current;
1778              
1779 2744         3788 for( my $bid = $bucket_idx; $bid < @$buckets; $bid++ ) {
1780 2896 50       4282 my $bucket = defined( $bid ) ? $store->_xform_out($buckets->[$bid]) : undef;
1781 2896 100       4143 if( $bucket ) {
1782 2770 50       3612 if( $self->[DEEP][$bid] ) {
1783 2770         3013 my $tied = tied %$bucket;
1784 2770 100       4182 my $key = defined( $idx_in_bucket) ? $tied->NEXTKEY : $tied->FIRSTKEY;
1785 2770 100       3763 if( defined($key) ) {
1786             # the bucket must be there to keep a weak reference
1787             # to itself. If it was not here, it would load from
1788             # the filesystem each call to NEXTKEY
1789 2728         3579 @$current = ( $bid, 0, $bucket );
1790 2728 50       5550 return wantarray ? ( $key => $bucket->{$key} ): $key;
1791             }
1792             } else {
1793 0 0       0 if( $idx_in_bucket < $#$bucket ) {
1794 0         0 @$current = ( $bid, $idx_in_bucket + 2, undef );
1795 0   0     0 my $key = $bucket->[$idx_in_bucket||0];
1796 0 0       0 return wantarray ? ( $key => $bucket->[$idx_in_bucket+1] ) : $key;
1797             }
1798             }
1799 42         55 undef $bucket;
1800             }
1801 168         254 undef $idx_in_bucket;
1802             }
1803 16         46 @$current = ( 0, undef, undef );
1804 16         408 return undef;
1805              
1806             } #NEXTKEY
1807              
1808             sub FETCH {
1809 3612     3612   10119 my( $self, $key ) = @_;
1810 3612 100       6227 if( $self->[TYPE] eq 'S' ) {
1811 1762         2675 my $x = $self->[DSTORE]->_xform_out($self->[DATA]{$key});
1812 1762         3196 return $self->[DSTORE]->_xform_out( $self->[DATA]{$key} );
1813             }
1814 1850         2893 my( $bid, $bucket ) = $self->_bucket( $key );
1815 1850 100       3307 if( $self->[DEEP][$bid] ) {
1816 1836         4989 return $bucket->{$key};
1817             } else {
1818 14         26 for( my $i=0; $i<$#$bucket; $i+=2 ) {
1819 186 100       307 if( $bucket->[$i] eq $key ) {
1820 14         27 return $bucket->[$i+1];
1821             }
1822             }
1823             }
1824             } #FETCH
1825              
1826             sub EXISTS {
1827 3603     3603   14136 my( $self, $key ) = @_;
1828 3603 100       5978 if( $self->[TYPE] eq 'S' ) {
1829 1760         5285 return exists $self->[DATA]{$key};
1830             }
1831              
1832 1843         2825 my( $bid, $bucket ) = $self->_bucket( $key );
1833 1843 100       3275 if( $self->[DEEP][$bid] ) {
1834 1822         4377 return exists $bucket->{$key};
1835             } else {
1836 21         45 for( my $i=0; $i<$#$bucket; $i+=2 ) {
1837 304 50       488 if( $bucket->[$i] eq $key ) {
1838 0         0 return 1;
1839             }
1840             }
1841             }
1842 21         50 return 0;
1843             } #EXISTS
1844              
1845             sub DELETE {
1846 1     1   451 my( $self, $key ) = @_;
1847              
1848 1         2 my $store = $self->[DSTORE];
1849              
1850 1 50       4 if( $self->[TYPE] eq 'S' ) {
1851 1 50       3 if( exists $self->[DATA]{$key} ) {
1852 1         3 $store->_dirty( $store->{_WEAK_REFS}{$self->[ID]}, $self->[ID] );
1853 1         1 $self->[KEYS]--;
1854 1         3 delete $self->[DATA]{$key};
1855             }
1856 1         3 return;
1857             }
1858              
1859 0         0 my( $bid, $bucket ) = $self->_bucket( $key, 'return_undef' );
1860 0 0       0 return 0 unless $bucket;
1861              
1862             # TODO - see if the buckets revert back to arrays
1863 0 0       0 if( $self->[DEEP][$bid] ) {
1864 0         0 $store->_dirty( $store->{_WEAK_REFS}{$self->[ID]}, $self->[ID] );
1865 0 0       0 $self->[KEYS]-- if exists $bucket->{$key}; #obj count
1866 0         0 return delete $bucket->{$key};
1867             } else {
1868 0         0 for( my $i=0; $i<$#$bucket; $i+=2 ) {
1869 0 0       0 if( $bucket->[$i] eq $key ) {
1870 0         0 splice @$bucket, $i, 2;
1871 0         0 $store->_dirty( $store->{_WEAK_REFS}{$self->[ID]}, $self->[ID] );
1872 0         0 $self->[KEYS]--; #obj count
1873 0         0 return 1;
1874             }
1875             }
1876             }
1877 0         0 return 0;
1878             } #DELETE
1879              
1880             sub CLEAR {
1881 0     0   0 my $self = shift;
1882 0         0 my $store = $self->[DSTORE];
1883 0         0 $self->[KEYS] = 0;
1884 0         0 $store->_dirty( $store->{_WEAK_REFS}{$self->[ID]}, $self->[ID] );
1885              
1886 0 0       0 if( $self->[TYPE] eq 'S' ) {
1887 0         0 $self->[DATA] = {};
1888             }
1889              
1890 0         0 my $buckets = $self->[DATA];
1891 0         0 for( my $bid=0; $bid<@$buckets; $bid++ ) {
1892 0 0       0 if( $self->[DEEP][$bid] ) {
1893 0         0 my $buck = tied %{$buckets->[$bid]};
  0         0  
1894 0         0 $buck->CLEAR;
1895             } else {
1896 0         0 my $buck = $buckets->[$bid];
1897 0         0 splice @$buck, 0, scalar( @$buck );
1898             }
1899             }
1900 0         0 splice @$buckets, 0, scalar(@$buckets);
1901             }
1902              
1903             sub DESTROY {
1904 5580     5580   8282 my $self = shift;
1905              
1906             #remove all WEAK_REFS to the buckets
1907 5580         21528 undef $self->[DATA];
1908              
1909 5580         20824 delete $self->[DSTORE]->{_WEAK_REFS}{$self->[ID]};
1910             }
1911              
1912             # ---------------------------------------------------------------------------------------
1913              
1914             package Yote::YoteDB;
1915              
1916 1     1   7 use strict;
  1         2  
  1         22  
1917 1     1   3 use warnings;
  1         2  
  1         19  
1918              
1919 1     1   3 no warnings 'uninitialized';
  1         1  
  1         26  
1920              
1921 1     1   328 use Data::RecordStore;
  1         8726  
  1         27  
1922              
1923 1     1   5 use File::Path qw(make_path);
  1         2  
  1         41  
1924              
1925             use constant {
1926 1         1463 CLS => 1,
1927             DATA => 2,
1928 1     1   4 };
  1         2  
1929              
1930             #
1931             # This the main index and stores in which table and position
1932             # in that table that this object lives.
1933             #
1934             sub open {
1935 7     7   16 my( $pkg, $obj_store, $args ) = @_;
1936 7   33     29 my $class = ref( $pkg ) || $pkg;
1937              
1938 7         7 my $DATA_STORE;
1939 7         10 eval {
1940 7         38 $DATA_STORE = Data::RecordStore->open( $args->{ store } );
1941             };
1942 7 50       1893 if( $@ ) {
1943 0 0       0 if( $@ =~ /old format/ ) {
1944 0         0 die "This yote store is of an older format. It can be converted using the yote_explorer";
1945             }
1946 0         0 die $@;
1947             }
1948 7         30 my $self = bless {
1949             args => $args,
1950             OBJ_STORE => $obj_store,
1951             DATA_STORE => $DATA_STORE,
1952             }, $class;
1953 7         31 $self->{DATA_STORE}->ensure_entry_count( 1 );
1954 7         363 $self;
1955             } #open
1956              
1957             #
1958             # Return a list reference containing [ id, class, data ] that
1959             # corresponds to the $id argument. This is used by Yote::ObjStore
1960             # to build the yote object.
1961             #
1962             sub _fetch {
1963 5633     5633   8328 my( $self, $id ) = @_;
1964 5633         11749 my $data = $self->{DATA_STORE}->fetch( $id );
1965              
1966 5633 100       445327 return undef unless $data;
1967              
1968 5628         9113 my $pos = index( $data, ' ' ); #there is a always a space after the class.
1969 5628 50       8430 $pos = ( length( $data ) ) if $pos == -1;
1970 5628 50       7259 die "Malformed record '$data'" if $pos == -1;
1971 5628         8222 my $class = substr $data, 0, $pos;
1972 5628         8534 my $val = substr $data, $pos + 1;
1973 5628         9523 my $ret = [$id,$class,$val];
1974              
1975             # so foo` or foo\\` but not foo\\\`
1976             # also this will never start with a `
1977 5628         83822 my $parts = [ split /\`/, $val, -1 ];
1978              
1979             # check to see if any of the parts were split on escapes
1980             # like mypart`foo`oo (should be translated to mypart\`foo\`oo
1981 5628 100       24897 if( 0 < grep { /\\$/ } @$parts ) {
  432295         528486  
1982 9         13 my $newparts = [];
1983              
1984 9         10 my $is_hanging = 0;
1985 9         11 my $working_part = '';
1986              
1987 9         15 for my $part (@$parts) {
1988              
1989             # if the part ends in a hanging escape
1990 126 100       390 if( $part =~ /(^|[^\\])((\\\\)+)?[\\]$/ ) {
    100          
1991 54 100       81 if( $is_hanging ) {
1992 18         26 $working_part .= "`$part";
1993             } else {
1994 36         50 $working_part = $part;
1995             }
1996 54         66 $is_hanging = 1;
1997             } elsif( $is_hanging ) {
1998 36         63 my $newpart = "$working_part`$part";
1999 36         98 $newpart =~ s/\\`/`/gs;
2000 36         72 $newpart =~ s/\\\\/\\/gs;
2001 36         56 push @$newparts, $newpart;
2002 36         61 $is_hanging = 0;
2003             } else {
2004             # normal part
2005 36         63 push @$newparts, $part;
2006             }
2007             }
2008 9 50       20 if( $is_hanging ) {
2009 0         0 die "Error in parsing parts\n";
2010             }
2011 9         24 $parts = $newparts;
2012             }
2013              
2014 5628 100 100     19023 if( $class eq 'ARRAY' || $class eq 'Yote::BigHash' || $class eq 'Yote::ArrayGatekeeper' ) {
      100        
2015 5593         8158 $ret->[DATA] = $parts;
2016             } else {
2017 35         130 $ret->[DATA] = { @$parts };
2018             }
2019              
2020 5628         8193 $ret;
2021             } #_fetch
2022              
2023             #
2024             # The first object in a yote data store can trace a reference to
2025             # all active objects.
2026             #
2027             sub _first_id {
2028 10     10   22 return 1;
2029             } #_first_id
2030              
2031             #
2032             # Create a new object id and return it.
2033             #
2034             sub _get_id {
2035 80     80   101 my $self = shift;
2036 80         210 $self->{DATA_STORE}->next_id;
2037             } #_get_id
2038              
2039              
2040             # used for debugging and testing
2041             sub _max_id {
2042 3     3   15 shift->{DATA_STORE}->entry_count;
2043             }
2044              
2045             sub _generate_keep_db {
2046 6     6   11 my $self = shift;
2047 6         46 my $mark_to_keep_store = Data::RecordStore::FixedStore->open( "I", $self->{args}{store} . '/PURGE_KEEP' );
2048              
2049 6         527 $mark_to_keep_store->empty();
2050 6         258 $mark_to_keep_store->ensure_entry_count( $self->{DATA_STORE}->entry_count );
2051              
2052 6         539 my $check_store = Data::RecordStore::FixedStore->open( "L", $self->{args}{store} . '/CHECK' );
2053 6         436 $check_store->empty();
2054              
2055 6         262 $mark_to_keep_store->put_record( 1, [ 1 ] );
2056              
2057 6         481 my( %seen );
2058 6         15 my( @checks ) = ( 1 );
2059              
2060 6         10 for my $referenced_id ( grep { $_ != 1 } grep { defined($self->{OBJ_STORE}{_WEAK_REFS}{$_}) } keys %{ $self->{OBJ_STORE}{_WEAK_REFS} } ) {
  10         25  
  14         40  
  6         23  
2061 5         13 push @checks, $referenced_id;
2062             }
2063              
2064             #
2065             # While there are items to check, check them.
2066             #
2067 6   66     21 while( @checks || $check_store->entry_count > 0 ) {
2068 28   33     53 my $check_id = shift( @checks ) || $check_store->pop->[0];
2069 28         93 $mark_to_keep_store->put_record( $check_id, [ 1 ] );
2070              
2071 28         1983 my $obj_arry = $self->_fetch( $check_id );
2072 28         53 $seen{$check_id} = 1;
2073 28         33 my( @additions );
2074              
2075 28 100       71 if( $obj_arry->[CLS] eq 'Yote::BigHash' ) {
    100          
    100          
2076 4         6 my $type = shift @{$obj_arry->[DATA]};
  4         10  
2077 4         9 shift @{$obj_arry->[DATA]}; #remove the size
  4         9  
2078 4 50       9 if( $type eq 'S' ) {
2079 4         6 my $d = {@{$obj_arry->[DATA]}};
  4         11  
2080 4 50       13 ( @additions ) = grep { /^[^v]/ && ! $seen{$_}++ } values %$d;
  4         34  
2081             } else { #BIG type
2082 0 0       0 ( @additions ) = grep { /^[^v]/ && ! $seen{$_}++ } @{$obj_arry->[DATA]};
  0         0  
  0         0  
2083             }
2084             }
2085             elsif ( $obj_arry->[CLS] eq 'Yote::ArrayGatekeeper' ) {
2086 8         13 shift @{$obj_arry->[DATA]}; #item_count
  8         14  
2087 8         14 shift @{$obj_arry->[DATA]}; #block_count
  8         10  
2088 8         15 shift @{$obj_arry->[DATA]}; #block_size
  8         12  
2089 8         11 shift @{$obj_arry->[DATA]}; #level
  8         12  
2090 8 50       11 ( @additions ) = grep { /^[^v]/ && ! $seen{$_}++ } @{$obj_arry->[DATA]};
  2         19  
  8         16  
2091             }
2092             elsif ( ref( $obj_arry->[DATA] ) eq 'ARRAY' ) {
2093 2 100       3 ( @additions ) = grep { /^[^v]/ && ! $seen{$_}++ } @{$obj_arry->[DATA]};
  20         42  
  2         3  
2094             }
2095             else { # Yote::Obj
2096 14 100       20 ( @additions ) = grep { /^[^v]/ && ! $seen{$_}++ } values %{$obj_arry->[DATA]};
  34         138  
  14         37  
2097             }
2098 28 50       53 if( @checks > 1_000_000 ) {
2099 0         0 for my $cid (@checks) {
2100 0         0 my( $has_keep ) = $mark_to_keep_store->get_record( $cid )->[0];
2101 0 0       0 unless( $has_keep ) {
2102 0         0 $check_store->push( [ $cid ] );
2103             }
2104             }
2105 0         0 splice @checks;
2106             }
2107 28 50       55 if( scalar( keys(%seen) ) > 1_000_000 ) {
2108 0         0 %seen = ();
2109             }
2110 28         107 push @checks, @additions;
2111             }
2112 6         267 $check_store->unlink_store;
2113              
2114 6         283 $mark_to_keep_store;
2115              
2116             } #_generate_keep_db
2117              
2118             #
2119             # Checks to see if the last entries of the stores can be popped off, making the purging quicker
2120             #
2121             sub _truncate_dbs {
2122 5     5   14 my( $self, $mark_to_keep_store, $keep_tally ) = @_;
2123             #loop through each database
2124 5         14 my $stores = $self->{DATA_STORE}->all_stores;
2125 5         492 my( @purged );
2126 5         10 for my $store (@$stores) {
2127 21         502 my $fn = $store->{FILENAME}; $fn =~ s!/[^/]+$!!;
  21         106  
2128 21         31 my $keep;
2129 21   100     53 while( ! $keep && $store->entry_count ) {
2130 12         339 my( $check_id ) = @{ $store->get_record($store->entry_count) };
  12         27  
2131 12         1029 ( $keep ) = $mark_to_keep_store->get_record( $check_id )->[0];
2132 12 100       377 if( ! $keep ) {
2133 2 50       7 if( $self->{DATA_STORE}->delete( $check_id ) ) {
2134 2 50       327 if( $keep_tally ) {
2135 2         3 push @purged, $check_id;
2136             }
2137 2         7 $mark_to_keep_store->put_record( $check_id, [ 2 ] ); #mark as already removed by truncate
2138             }
2139             }
2140             }
2141             }
2142 5         14 \@purged;
2143             }
2144              
2145              
2146             sub _update_recycle_ids {
2147 3     3   6 my( $self, $mark_to_keep_store ) = @_;
2148              
2149 3 50       5 return unless $mark_to_keep_store->entry_count > 0;
2150              
2151 3         82 my $store = $self->{DATA_STORE};
2152              
2153              
2154             # find the higest still existing ID and cap the index to this
2155 3         3 my $highest_keep_id;
2156 3         9 for my $cand (reverse ( 1..$mark_to_keep_store->entry_count )) {
2157 4         74 my( $keep ) = $mark_to_keep_store->get_record( $cand )->[0];
2158 4 100       108 if( $keep ) {
2159 3         11 $store->set_entry_count( $cand );
2160 3         123 $highest_keep_id = $cand;
2161 3         5 last;
2162             }
2163             }
2164              
2165 3         22 $store->empty_recycler;
2166              
2167             # iterate each id in the entire object store and add those
2168             # not marked for keeping into the recycling
2169 3         132 for my $cand (reverse( 1.. $highest_keep_id) ) {
2170 21         240 my( $keep ) = $mark_to_keep_store->get_record( $cand )->[0];
2171 21 100       774 unless( $keep ) {
2172 2         7 $store->recycle( $cand );
2173             }
2174             }
2175             } #_update_recycle_ids
2176              
2177              
2178             sub _purge_objects {
2179 4     4   20 my( $self, $mark_to_keep_store, $keep_tally ) = @_;
2180              
2181 4         9 my $purged = $self->_truncate_dbs( $mark_to_keep_store );
2182              
2183 4         8 for my $cand ( 1..$mark_to_keep_store->entry_count) { #iterate each id in the entire object store
2184 30         239 my( $keep ) = $mark_to_keep_store->get_record( $cand )->[0];
2185              
2186 30 50 66     949 die "Tried to purge root entry" if $cand == 1 && ! $keep;
2187 30 100       55 if ( ! $keep ) {
2188 4 100       14 if( $self->{DATA_STORE}->delete( $cand ) ) {
2189 1         262 $mark_to_keep_store->put_record( $cand, [ 3 ] ); #mark as already removed by purge
2190 1 50       54 if( $keep_tally ) {
2191 1         3 push @$purged, $cand;
2192             }
2193             }
2194             }
2195             }
2196              
2197 4         53 $purged;
2198              
2199             } #_purge_objects
2200              
2201              
2202             #
2203             # Saves the object data for object $id to the data store.
2204             #
2205             sub _stow { #Yote::YoteDB::_stow
2206 82     82   153 my( $self, $id, $class, $data ) = @_;
2207 82         165 my $save_data = "$class $data";
2208 82         180 $self->{DATA_STORE}->stow( $save_data, $id );
2209             } #_stow
2210              
2211             #
2212             # Takes a list of object data references and stows them all in the datastore.
2213             # returns how many are stowed.
2214             #
2215             sub _stow_all {
2216 15     15   29 my( $self, $objs ) = @_;
2217 15         23 my $count = 0;
2218 15         26 for my $o ( @$objs ) {
2219 77         19802 $count += $self->_stow( @$o );
2220             }
2221 15         1798 return $count;
2222             } #_stow_all
2223              
2224             1;
2225              
2226             __END__