The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package autodbTestObject;
use t::lib;
use strict;
use Carp;
# use Scalar::Util qw(looks_like_number reftype); # sigh. Test::Deep exports reftype, too
use Scalar::Util qw(looks_like_number refaddr);
use List::MoreUtils qw(uniq);
use DBI;
use Test::More;
# Test::Deep doesn't export cmp_details, deep_diag until recent version (0.104)
# so we import them "by hand"
use Test::Deep;
*cmp_details=\&Test::Deep::cmp_details;
*deep_diag=\&Test::Deep::deep_diag;
use Hash::AutoHash::Args;
use autodbUtil;
use base qw(Class::AutoClass);
use vars qw(@AUTO_ATTRIBUTES @OTHER_ATTRIBUTES %SYNONYMS %DEFAULTS);
# attributes
#   class being tested            -- computed from current_object if set
#   class2colls={class=>[colls]}
#   class2tables={class=>[tables]}-- computed from class2colls, coll2tables
#   class2transients={class=>[transient keys]}
#   coll2keys={coll=>[[basekeys],[llistkeys]]}
#   coll2basekeys={coll=>[basekeys]}
#   coll2listkeys={coll=>[listkeys]}
#     can set key info in coll2keys, or coll2xxxxkeys. whichever is easiest
#     if set in both, coll2xxxxkeys wins
#   coll2tables={coll=>[tables]} -- computed from coll2keys
#   tables=[tables]              -- computed from class2tables
#   correct_colls=[coll]         -- collections containing object
#                                -- often computed from class2colls
#   correct_tables=[tables]      -- computed from correct_colls, colls2tables
#   correct_diffs=number or {table=>diff}
#     if number, diff for each correct_tables
#     for 'multi' tests, diff is per object
#   default_diffs={table=>diff}  -- any table not mentioned has default of 1
#   labelprefix                  -- string prepended to every label
#   label                        -- sub to compute object-specific portion of label
#   new_args=>sub to construct objects. called w/ TestObject as arg
#     for test_put, these are objects tested.
#     for test_get, these are 'correct' objects
#   old_objects                  -- for put tests, objects already in db
#   put_type                     -- 'put', 'put-multi', 'put_objects', 'put_objects-multi'.
#                                -- 'put_objects' does it w/o args, 
#                                -- 'put_objects-multi' does it w/ args
#                                   default 'put'
#   get_args                     -- args to $autodb->get and find
#     can be a CODE ref or actual args
#   get_type                     -- 'get', 'find', 'find-getnext'. default 'get'
#   del_type                     -- 'del', 'del-multi'. default 'del'
#   object or objects            -- objects to test
#   correct_object or _objects   -- correct objects for retrieval test
#   actual_object or _objects    -- actual objects for retrieval test - synonym for objects
#   del_objects                  -- deleted objects. 
#                                   can be set by hand. updated by test_del
@AUTO_ATTRIBUTES=qw(class2colls class2transients coll2basekeys coll2listkeys default_diffs
		    old_objects del_objects
		    labelprefix 
		    new_args put_type get_args get_type del_type current_object);
@OTHER_ATTRIBUTES=qw(class label class2tables tables coll2keys coll2tables
		     correct_colls correct_tables correct_diffs 
		     object objects correct_object correct_objects);
%SYNONYMS=(last_object=>'current_object',actual_object=>'object',actual_objects=>'objects');
%DEFAULTS=
  (class2colls=>{},class2transients=>{},coll2keys=>{},coll2basekeys=>{},coll2listkeys=>{},
   default_diffs=>{},
   del_objects=>[],
   put_type=>'put',get_type=>'get',del_type=>'del'
   );
# new_args=>sub{my($test)=@_}
sub _init_self {
  my($self,$class,$args)=@_;
  return unless $class eq __PACKAGE__;    # to prevent subclasses from re-running this
  # $self->_init_test;
}
sub _init_test {
  my $self=shift;
  # update any test-specific parameters. Test::Deep exports 'set'. sigh...
  $self->Class::AutoClass::set(@_); 
  # initialize various memoized attributes. deleting the key forces recompute
  delete $self->{coll2tables};
  delete $self->{tables};
  $self->current_object(undef);	# to avoid confusion before entering test loop
  $self->_old_counts;		# initialize old_counts
}
# test and put some objects.
# objects may be constructed here or passed in.
sub test_put {
  my $self=shift;
  my($package,$file,$line)=caller; # for fails
  if (@_==1 && looks_like_number($_[0])) { # arg is number of objects
    $self->objects($self->make_objects(shift));
  }
  $self->_init_test(@_);
  $self->object($self->make_object) unless defined $self->objects;
  my $put_type=$self->put_type;
  if ($put_type=~/multi|objects/) {
    $self->_test_put_multi($file,$line);
  } else {
    $self->_test_put_one($file,$line);
  }
}

# tests that put one object at at time
sub _test_put_one {
  my($self,$file,$line)=@_;
  my @objects=@{$self->objects};
  my %old_objects=map {refaddr($_)=>1} @{$self->old_objects||[]};
  my $put_type=$self->put_type;
  for my $object (@objects) {
    $self->current_object($object); # all class- and object-specific attrs use this
    my $class=$self->class;
    my %coll2keys=%{$self->coll2keys};
    my @correct_colls=@{$self->correct_colls};
    my @tables=@{$self->tables};
    my @correct_tables=@{$self->correct_tables};
    # my $correct_diffs=$self->correct_diffs;
    my $correct_diffs=$old_objects{refaddr $object}? {}: $self->correct_diffs;

    my $ok=1;
    my $label=$self->label."oid before put";
    $ok&&=($old_objects{refaddr $object}? 
	   _ok_oldoid($object,$label,$file,$line):
	   _ok_newoid($object,$label,$file,$line,@tables));
    # $ok&&=_ok_newoid($object,$label,$file,$line,@tables);
    # report_pass($ok,$label);
    # NG 09-12-20: changed 'put_type' semantics so 'put' is only choice here
    # if ($put_type eq 'put') {
    #   autodb->put($object);
    # } elsif ($put_type=~/^put[_-]{0,1}objects/) {
    # autodb->put_objects($object);
    # }
    autodb->put($object);
    remember_oids($object);
    # remember_oids($object) unless $old_objects{refaddr $object};
    next unless $ok;		# skip remaining tests once we have a fail
    my $label=$self->label."oid after put";
    $ok&&=_ok_oldoid($object,$label,$file,$line);
    # report_pass($ok,$label);
    for my $coll (@correct_colls) {
      my $label=$self->label."collection $coll";
      $ok&&=_ok_collection($object,$label,$coll,@{$coll2keys{$coll}},$file,$line);
      report_pass($ok,$label);
    }
    my $actual_diffs=$self->diff_counts(@tables);
    my $details;
    $ok and ($ok,$details)=cmp_details($actual_diffs,$correct_diffs);
    # report($ok,$self->label."table counts",$file,$line,$details);
    report_fail($ok,$self->label."table counts",$file,$line,$details);
    report_pass($ok,$self->label."done");
    # cmp_deeply($actual_diffs,$correct_diffs,$self->label."table counts");
    # report_pass($ok,$label);
    $old_objects{refaddr $object}=1;
  }
  $self->objects(undef);     # clear so won't retest these objects next time
  $self->old_objects(undef); # clear for next time
  scalar @objects;
}
# tests that put multiple objects at once
sub _test_put_multi {
  my($self,$file,$line)=@_;
  my @objects=@{$self->objects};
  my %old_objects=map {refaddr($_)=>1} @{$self->old_objects||[]};
  my $put_type=$self->put_type;
  # do all the before-put tests
  for my $object (@objects) {
    $self->current_object($object); # all class- and object-specific attrs use this
    my $class=$self->class;
    my %coll2keys=%{$self->coll2keys};
    my @correct_colls=@{$self->correct_colls};
    my @tables=@{$self->tables};
    my @correct_tables=@{$self->correct_tables};
    # my $correct_diffs=$self->correct_diffs;
    my $correct_diffs=$old_objects{refaddr $object}? {}: $self->correct_diffs;

    my $ok=1;
    my $label=$self->label."oid before put";
    $ok&&=($old_objects{refaddr $object}? 
	   _ok_oldoid($object,$label,$file,$line):
	   _ok_newoid($object,$label,$file,$line,@tables));
  }
  # now put 'em
  # NG 09-12-20: changed 'put_type' semantics. 'multi' means do it w/args
  if ($put_type=~/objects/) {
    if ($put_type=~/multi/) {
      autodb->put_objects(@objects);
    } else {
      autodb->put_objects;
    }
  } else {
    autodb->put(@objects);
  }
  remember_oids(@objects);
  # now do the per-object after-put tests (and accumulate final diffs)
  my $final_diffs={};
  for my $object (@objects) {
    $self->current_object($object); # all class- and object-specific attrs use this
    my $class=$self->class;
    my %coll2keys=%{$self->coll2keys};
    my @correct_colls=@{$self->correct_colls};
    my @tables=@{$self->tables};
    my @correct_tables=@{$self->correct_tables};
    my $correct_diffs=$old_objects{refaddr $object}? {}: $self->correct_diffs;

    # remember_oids($object) unless $old_objects{refaddr $object};
    my $ok=1;
    my $label=$self->label."oid after put";
    $ok&&=_ok_oldoid($object,$label,$file,$line);
    # report_pass($ok,$label);
    for my $coll (@correct_colls) {
      my $label=$self->label."collection $coll";
      $ok&&=_ok_collection($object,$label,$coll,@{$coll2keys{$coll}},$file,$line);
      report_pass($ok,$label);
    }
    # add correct_diffs to final diffs
    while(my($table,$diff)=each %$correct_diffs) {
      $final_diffs->{$table}+=$diff;
    }
  }
  # test final diffs
  $self->current_object(undef);
  my $actual_diffs=$self->diff_counts;
  my($ok,$details)=cmp_details($actual_diffs,$final_diffs);
  # report($ok,$self->label."table counts",$file,$line,$details);
  report_fail($ok,$self->label."table counts",$file,$line,$details);
  report_pass($ok,$self->label."done");
  # cmp_deeply($actual_diffs,$correct_diffs,$self->label."table counts");
  # report_pass($ok,$label);

  $self->objects(undef);     # clear so won't retest these objects next time
  $self->old_objects(undef); # clear for next time
  scalar @objects;
}

# test objects retrieved from database
# retrieval may be done here or 'actual' objects may be passed in
# 'correct' objects may be constructed here or passed in
# to match actual and correct objects, choices are
#   if objects have 'ids', they are used
#   else actual and correct lists must be supplied in matching order
sub test_get {
  my $self=shift;
  my($package,$file,$line)=caller; # for fails
  if (@_==1 && looks_like_number($_[0])) { # arg is number of objects
    $self->correct_objects($self->make_objects(shift));
  }
  $self->_init_test(@_);
  $self->correct_object($self->make_object) unless defined $self->correct_objects;
  my @correct_objects=@{$self->correct_objects};
  my @actual_objects=defined $self->actual_objects? @{$self->actual_objects}: ();
  my $get_type=$self->get_type;
  # query the database if no 'actual' objects yet
  @actual_objects or @actual_objects=$self->do_get;
  unless(@actual_objects==@correct_objects) {
    report_fail(0,$self->label.'number of objects');
    diag('   got: '.scalar @actual_objects);
    diag('expect: '.scalar @correct_objects);
    return 0;
  }
  my $ok=1;
  reach_fetch(@actual_objects);	# make sure reachable objects in-memory
  my @actual_reach=reach(@actual_objects);
  for my $object (@actual_reach) {
    $self->current_object($object); # all class- and object-specific attrs use this
    my $class=$self->class;
    my %coll2keys=%{$self->coll2keys};
    my @correct_colls=@{$self->correct_colls};
    my @tables=@{$self->tables};
    my @correct_tables=@{$self->correct_tables};

    # my $ok=1;
    my $label=$self->label."(via reach) oid";
    $ok&&=_ok_oldoid($object,$label,$file,$line);
    # report_pass($ok,$label);
    for my $coll (@correct_colls) {
      my $label=$self->label."(via reach) collection $coll";
      $ok&&=_ok_collection($object,$label,$coll,$self->remove_transients(@{$coll2keys{$coll}}),
			   $file,$line);
      # $ok&&=_ok_collection($object,$label,$coll,@{$coll2keys{$coll}},$file,$line);
      # report_pass($ok,$label);
    }
    # report_pass($ok,$self->label.'(via reach) oid and collections');
  }
  $self->current_object(undef); # all class- and object-specific attrs use this
  report_pass($ok,
	      $self->label.(scalar @actual_reach).' objects (via reach) oids and collections');
  # at this point we know that objects-oids-ids are 1:1:1. if we were
  #   willing to assume that all objects have ids, cmp_deeply (which
  #   doesn't check correct sharing of objects) would be strong enough
  #   to complete the job. instead, just to be safe, reach_mark marks
  #   every object with a key that distinguishes shared vs. separate
  #   objects. (I think this works...)
  # NG 09-12-24: abandoned reach_mark. it traverses hash keys in unpredictable order
  #              giving unpredictable results. this means we have to assume that all
  #              objects have id's
  # NG 09-12-21: having changed reach_mark to process all objects in one go, have to arrange
  #              for objects to be in same order
  #              sort the lists by id if all object can do it
#   if (@actual_objects==grep {UNIVERSAL::can($_,'id')} @actual_objects) {
#     @actual_objects=sort {$a->id <=> $b->id} @actual_objects;
#     @correct_objects=sort {$a->id <=> $b->id} @correct_objects;
#   }
#   my @actual_copies=reach_mark(@actual_objects);
#   my @correct_copies=reach_mark(@correct_objects);
#   my @actual_copies=map {reach_mark($_)} @actual_objects;
#   my @correct_copies=map {reach_mark($_)} @correct_objects;

  # sort the lists by id if all object can do it
  # NG 09-12-21: probably redundant with sort above, but can't hurt...
  # NG 09-12-24: sort above commented out.  but we are now assuming all objects have id's
  # if (@actual_copies==grep {UNIVERSAL::can($_,'id')} @actual_copies) {
  my @actual_copies=sort {$a->id <=> $b->id} @actual_objects;
  my @correct_copies=sort {$a->id <=> $b->id} @correct_objects;
  # }
  # at this point, the lists are in matching order
  for(my $i=0; $i<@correct_copies; $i++) {
    $self->current_object($correct_copies[$i]); # label, remove_transients use this
    $self->remove_transients;                   # remove transients from current_object
    my($ok,$details)=cmp_details($actual_copies[$i],$correct_copies[$i]);
    report($ok,$self->label."contents",$file,$line,$details);
  }
  $self->actual_objects(undef);		        # clear so won't retest these objects next time
  $self->correct_objects(undef);                # clear so won't retest these objects next time
  scalar @correct_objects;
}

#####################################
# NG 10-09-08: added del test
# delete and test some objects.
sub test_del {
  my $self=shift;
  my($package,$file,$line)=caller; # for fails
  $self->_init_test(@_);
  my @objects=@{$self->objects};
  my %del_objects=map {refaddr($_)=>1} @{$self->del_objects||[]};
  my $del_type=$self->del_type;
  # do all the before-del tests
  for my $object (@objects) {
    $self->current_object($object); # all class- and object-specific attrs use this
    my $class=$self->class;
    my %coll2keys=%{$self->coll2keys};
    my @correct_colls=@{$self->correct_colls};
    my @tables=@{$self->tables};
    my @correct_tables=@{$self->correct_tables};
    my $correct_diffs=$del_objects{refaddr $object}? {}: $self->correct_diffs;

    my $ok=1;
    my $label=$self->label."oid before del";
    $ok&&=($del_objects{refaddr $object}?
	   _ok_deloid($object,$label,$file,$line,@correct_tables): 1);
  }
  if ($del_type=~/multi/) {
    autodb->del(@objects);
  } else {
    map {autodb->del($_)} @objects;
  }
  # now do the per-object after-del tests (and accumulate final diffs)
  my $final_diffs={};
  for my $object (@objects) {
    $self->current_object($object); # all class- and object-specific attrs use this
    my $class=$self->class;
    my %coll2keys=%{$self->coll2keys};
    my @correct_colls=@{$self->correct_colls};
    my @tables=@{$self->tables};
    my @correct_tables=@{$self->correct_tables};
    my $correct_diffs= $del_objects{refaddr $object}? {}: $self->correct_diffs;

    my $ok=1;
    my $label=$self->label."oid after del";
    $ok&&=_ok_deloid($object,$label,$file,$line,@correct_tables);
    # add correct_diffs to final diffs
    while(my($table,$diff)=each %$correct_diffs) {
      $final_diffs->{$table}-=$diff;
    }
  }
  # test final diffs
  $self->current_object(undef);
  my $actual_diffs=$self->diff_counts;
  my($ok,$details)=cmp_details($actual_diffs,$final_diffs);
  # report($ok,$self->label."table counts",$file,$line,$details);
  report_fail($ok,$self->label."table counts",$file,$line,$details);
  report_pass($ok,$self->label);
  # cmp_deeply($actual_diffs,$correct_diffs,$self->label."table counts");
  # report_pass($ok,$label);

  $self->objects(undef);     # clear so won't retest these objects next time
  push(@{$self->del_objects},@objects); # update for next time
  scalar @objects;
}

sub do_get {
  my $self=shift;
  my $get_args=@_? shift: $self->get_args;
  my $get_type=@_? shift: $self->get_type;
  my $correct_count=@_? shift: scalar @{$self->correct_objects};
  confess 'need to query database but get_args not set' unless $get_args;
  my(@actual_objects,$actual_count);
  if ('CODE' eq ref $get_args) {
    my %get_args=&$get_args($self);
    $get_args=\%get_args;
  }
  if ($get_type eq 'get') {
    @actual_objects=autodb->get($get_args);
    $actual_count=autodb->count($get_args);
  } elsif ($get_type=~/^find([_-]{0,1}get){0,1}$/) {
    my $cursor=autodb->find($get_args);
    @actual_objects=$cursor->get;
    $actual_count=$cursor->count;
  } elsif ($get_type=~/^find[_-]{0,1}get[_-]{0,1}next$/) {
    my $cursor=autodb->find($get_args);
    while (my $object=$cursor->get_next) {
      push(@actual_objects,$object);
    } 
    $actual_count=$cursor->count;
  } else {
    confess "invalid get_type $get_type";
  }
  # is($actual_count,scalar @correct_objects,$self->label.'count');
  unless($actual_count==$correct_count) {
    report_fail(0,$self->label.'count');
    diag("   got: $actual_count");
    diag("expect: $correct_count");
  }
  wantarray? @actual_objects: \@actual_objects;
}
# args are objects. last arg can be put_type
sub do_put {
  my $self=shift;
  my $put_type=!ref $_[$#_]? pop: $self->put_type;
  my @objects=@_;
  if ($put_type=~/multi|objects/) {
    if ($put_type=~/objects/) {
      if ($put_type=~/multi/) {
	autodb->put_objects(@objects);
      } else {
	autodb->put_objects;
      }
    } else {
      autodb->put(@objects);
    }
  } else {
    map {autodb->put($_)} @objects;
  }
}
sub make_object {
  my $self=shift;
  my $objects=$self->make_objects(1);
  # caller now responsible for saving in $self
  # $self->object($objects->[0]);
  $objects->[0];
}
sub make_objects {
  my $self=shift;
  my $n=@_? shift: 1;
  my $class=$self->class;
  my $new_args=$self->new_args;	              # sub
  confess 'need to make objects but new_args not set' unless $new_args; 
  confess 'need to make objects but new_args not CODE ref' unless 'CODE' eq ref $new_args; 
  my @objects=map {my %new_args=&$new_args($self); new $class %new_args;} (1..$n);
  # caller now responsible for saving in $self
  # $self->objects(\@objects);
  \@objects;
}
sub label {
  my $self=shift;
  my $label= @_? $self->{label}=$_[0]: ($self->{label});
  my @label=($self->labelprefix);
  if ('CODE' eq ref $label) {
    push(@label,&$label($self));
  } elsif ($label ne '') {	# empty string means 'no label'
    my $object=$self->current_object;
    push(@label,$label,(UNIVERSAL::can($object,'id')? $object->id: ()));
  }
  @label=map {s/^\s+|\s+$//g; $_} grep {length $_} @label; # strip leading and trailing whitespace
  $label=join(' ',@label).' ';
}
# compute from current_object if set, else use stored value if any
sub class {
  my $self=shift;
  $self->{class}=$_[0] if @_;
  my $ref=ref $self->current_object;
  $ref=$self->current_object->{_CLASS} if UNIVERSAL::isa($ref,'Class::AutoDB::Oid');
  $ref || $self->{class};
}

sub correct_colls {
  my $self=shift;
  if (@_) {
    (@_==1 && 'ARRAY' eq ref $_[0])? $self->{correct_colls}=$_[0]: 
      ($self->{correct_colls}=[@_]);
  }
  my $correct_colls=$self->{correct_colls};
  return $correct_colls if defined $correct_colls;
  # usual case. computed from class2colls
  # NOT memoized since can change as objects are processed 
  my $class=$self->class;
  $self->class2colls->{$class};
}
sub correct_tables {
  my $self=shift;
  if (@_) {
    (@_==1 && 'ARRAY' eq ref $_[0])? $self->{correct_tables}=$_[0]:
      ($self->{correct_tables}=[@_]);
  }
  my $correct_tables=$self->{correct_tables};
  return $correct_tables if defined $correct_tables;
  # usual case. computed from correct_colls which in turn is computed from class2colls
  # NOT memoized since can change as objects are processed 
  my $correct_colls=$self->correct_colls;
  return undef unless defined $correct_colls;
  my $coll2keys=$self->coll2keys;
  my @correct_tables=qw(_AutoDB);	        # everyone needs _AutoDB
  for my $coll (@$correct_colls) {
    my $pair=$coll2keys->{$coll};
    my @tables=($coll,map {$coll.'_'.$_} @{$pair->[1]});
    push(@correct_tables,@tables);
  }
  \@correct_tables;
  # wantarray? @$correct_tables: $correct_tables;
}
sub coll2keys {
  my $self=shift;
  if (@_) {
    (@_==1 && 'HASH' eq ref $_[0])? $self->{coll2keys}=$_[0]: 
      ($self->{coll2keys}={@_});
  }
  my $coll2keys=$self->{coll2keys};
  if (my $coll2basekeys=$self->coll2basekeys) {
    while(my($coll,$basekeys)=each %$coll2basekeys) {
      my $pair=$coll2keys->{$coll} || ($coll2keys->{$coll}=[[],[]]);
      $pair->[0]=$basekeys;
    }
    $self->coll2basekeys(undef); # so won't compute again
  }
  if (my $coll2listkeys=$self->coll2listkeys) {
    while(my($coll,$listkeys)=each %$coll2listkeys) {
      my $pair=$coll2keys->{$coll} || ($coll2keys->{$coll}=[[],[]]);
      $pair->[1]=$listkeys;
    }
    $self->coll2listkeys(undef); # so won't compute again
  }
  $coll2keys;
  # wantarray? %$coll2keys: $coll2keys;
}
sub coll2tables {
  my $self=shift;
  if (@_) {
    (@_==1 && 'HASH' eq ref $_[0])? $self->{coll2tables}=$_[0]: 
      ($self->{coll2tables}={@_});
  }
  my $coll2tables=$self->{coll2tables};
  return $coll2tables if defined $coll2tables;
  # else recompute it
  my $coll2keys=$self->coll2keys;
  my %coll2tables;
  while(my($coll,$pair)=each %$coll2keys) {
    my @tables=($coll,map {$coll.'_'.$_} @{$pair->[1]});
    $coll2tables{$coll}=\@tables;
  }
  $self->{coll2tables}=\%coll2tables;
  # wantarray? %$coll2tables: $coll2tables;
}
sub class2tables {
  my $self=shift;
  if (@_) {
    (@_==1 && 'HASH' eq ref $_[0])? $self->{class2tables}=$_[0]: 
      ($self->{class2tables}={@_});
  }
  my $class2tables=$self->{class2tables};
  return $class2tables if defined $class2tables;
  # else recompute it
  my $class2colls=$self->class2colls;
  my $coll2tables=$self->coll2tables;
  my %class2tables;
  while(my($class,$colls)=each %$class2colls) {
    my @tables;
    for my $coll (@$colls) {
      my $tables=$coll2tables->{$coll};
      push(@tables,@$tables);
    }
    $class2tables{$class}=\@tables;
  }
  $self->{class2tables}=\%class2tables;
}

# all tables
sub tables {
  my $self=shift;
  if (@_) {
    (@_==1 && 'ARRAY' eq ref $_[0])? $self->{tables}=$_[0]: ($self->{tables}={@_});
  }
  my $tables=$self->{tables};
  return $tables if defined $tables;
  # else recompute it
  my @tables=uniq(qw(_AutoDB),map {@$_} values %{$self->coll2tables});
  $self->{tables}=\@tables;
}
sub correct_diffs {
  my $self=shift;
  return $self->{correct_diffs}=$_[0] if @_;
  # my $correct_diffs=@_? $self->{correct_diffs}=$_[0]: ($self->{correct_diffs});
  my $correct_diffs=$self->{correct_diffs};
  if (ref $correct_diffs) { 	# make sure _AutoDB is there
    $correct_diffs->{_AutoDB}=1 unless defined $correct_diffs->{_AutoDB}
  } elsif (my $correct_tables=$self->correct_tables) { # need correct_tables for this form
    my $diff=defined $correct_diffs? $correct_diffs: 1;	# hang onto old value
    $correct_diffs={};
    my @correct_tables=@{$self->correct_tables};
    my $default_diffs=$self->default_diffs;
    # NG 10-09-09: use defaults for any tables mentioned in default_diffs
    @$correct_diffs{@correct_tables}=
      map {defined $default_diffs->{$_}? $default_diffs->{$_}: $diff} @correct_tables;
    # $self->correct_diffs($correct_diffs);
  } else { 			# minimal default
    $correct_diffs={_AutoDB=>1};
  }
  $correct_diffs;
}
sub object {
  my $self=shift;
  $self->objects([@_]) if @_;
  $self->objects->[0];
}
sub objects {
  my $self=shift;
  if (@_) {
    (@_==1 && 'ARRAY' eq ref $_[0])? $self->{objects}=$_[0]:
      (!defined $_[0]? $self->{objects}=undef: ($self->{objects}=[@_]));
    # (@_==1 && looks_like_number($_[0])? $self->make_objects($_[0]):
    # ($self->{objects}=[@_]));
  }
  $self->{objects};
}
sub correct_object {
  my $self=shift;
  $self->correct_objects([@_]) if @_;
  $self->correct_objects->[0];
}
sub correct_objects {
  my $self=shift;
  if (@_) {
    (@_==1 && 'ARRAY' eq ref $_[0])? $self->{correct_objects}=$_[0]:
      (!defined $_[0]? $self->{correct_objects}=undef: ($self->{correct_objects}=[@_]));
    # (@_==1 && looks_like_number($_[0])? $self->make_correct_objects($_[0]):
    # ($self->{correct_objects}=[@_]));
  }
  $self->{correct_objects};
}
# sub clear_objects {
#   my $self=shift;
#   $self->objects([]);
##  $self->object(undef);
# }
sub old_counts {
  my $self=shift;
  my @tables=@_? @_: @{$self->tables};
  my $old_counts=$self->{old_counts} || ($self->_old_counts(@tables));
  # $old_counts=norm_counts(map {$_=>$old_counts->{$_}} @tables);
  $old_counts;
}
sub update_counts {
  my($self,$new_counts)=@_;
  my $old_counts=$self->old_counts;
  my @tables=keys %$new_counts;
  @$old_counts{@tables}=@$new_counts{@tables};
}
sub diff_counts {
  my $self=shift;
  my @tables=@_? @_: @{$self->tables};
  my $old_counts=$self->old_counts(@tables);
  my $new_counts=actual_counts(@tables);
  my $diff_counts={};
  map {$diff_counts->{$_}=$new_counts->{$_}-$old_counts->{$_}} @tables;
  # update old_counts for next time
  $self->update_counts($new_counts);
  $diff_counts=norm_counts($diff_counts);
  # $diff_counts;
}
# sub _coll2keys {
#   my $self=shift;
#   my $coll2keys=$self->coll2keys;
#   if (my $coll2basekeys=$self->coll2basekeys) {
#     while(my($coll,$basekeys)=each %$coll2basekeys) {
#       my $pair=$coll2keys->{$coll} || ($coll2keys->{$coll}=[[],[]]);
#       $pair->[0]=$basekeys;
#     }
#     $self->coll2basekeys(undef); # so won't compute again
#   }
#   if (my $coll2listkeys=$self->coll2listkeys) {
#     while(my($coll,$listkeys)=each %$coll2listkeys) {
#       my $pair=$coll2keys->{$coll} || ($coll2keys->{$coll}=[[],[]]);
#       $pair->[1]=$listkeys;
#     }
#     $self->coll2listkeys(undef); # so won't compute again
#   }
#   # wantarray? %$coll2keys: $coll2keys;
# }
# sub _coll2tables {
#   my $self=shift;
#   delete $self->{coll2tables};	# delete the key so
#   $self->coll2tables;		# this call will recompute
#   my $coll2keys=$self->coll2keys;
#   my $coll2tables=$self->coll2tables;
#   my $tables=$self->tables([qw(_AutoDB)]);
#   my @tables;
#   while(my($coll,$pair)=each %$coll2keys) {
#     my @tables=($coll,map {$coll.'_'.$_} @{$pair->[1]});
#     $coll2tables->{$coll}=\@tables;
#     push(@$tables,@tables);
#   }
#   # wantarray? %$coll2tables: $coll2tables;
# }
# sub _correct_tables {
#   my $self=shift;
#   my @correct_colls=@{$self->correct_colls};
#   my $coll2keys=$self->coll2keys;
#   my $correct_tables=$self->correct_tables([qw(_AutoDB)]);
#   for my $coll (@correct_colls) {
#     my $pair=$coll2keys->{$coll};
#     my @tables=($coll,map {$coll.'_'.$_} @{$pair->[1]});
#     push(@$correct_tables,@tables);
#   }
#   # wantarray? @$correct_tables: $correct_tables;
# }
sub _old_counts {
  my $self=shift;
  my @tables=@_? @_: @{$self->tables};
  my $old_counts=norm_counts(actual_counts(@tables));
  $self->{old_counts}=$old_counts;
  # $old_counts;
}
# remove transients from current_object or arg. arg can be object or pair of keys array
sub remove_transients {
  my $self=shift;
  if (!@_ || Scalar::Util::blessed($_[0])) { # processing object if no arg or arg is blessed
    my $object=@_? shift: $self->current_object;
    my $class=ref $object;
    my $transients=$self->class2transients->{$class} || [];
    for my $key (@$transients) {
      delete $object->{$key}
    }
    return $object;
  } else {			            # processing key list

    my($basekeys,$listkeys)=@_>1? @_: @{$_[0]};
    my $transients=$self->class2transients->{$self->class};
    if ($transients && @$transients) {
      my %transients=map {$_=>$_} @$transients;
      # don't clobber args!!
      my @basekeys=grep {!$transients{$_}} @$basekeys;
      my @listkeys=grep {!$transients{$_}} @$listkeys;
      $basekeys=\@basekeys;
      $listkeys=\@listkeys;
    }
    wantarray? ($basekeys,$listkeys): [$basekeys,$listkeys];
  }
}
1;