[Rt-commit] rt branch, 4.2/optimize-cgm-table, updated. rt-4.0.5-326-gca557da

Ruslan Zakirov ruz at bestpractical.com
Mon Apr 9 15:51:19 EDT 2012


The branch, 4.2/optimize-cgm-table has been updated
       via  ca557da50dcfbf84e6a1cfc9f48fe694d59c3abd (commit)
       via  81e3392d09be87ed749364087e4685616f82a992 (commit)
       via  a34ab0ed4a256ef167de14aa0122a7ac79b0d780 (commit)
       via  38de6a7355117c359c6f812a106c8ec00c6da783 (commit)
       via  4c0e4a2ede4faa0f1a1347426e72a5f6050550d0 (commit)
       via  d7683c876e25e22e95fa561650ffa5118c4bda7c (commit)
       via  bc39464fa10ca05952bcd6f23296eeb910998f99 (commit)
       via  9a64ee74a37caff1d489be9f68b0e1ed56c55e2a (commit)
       via  f4883e6f44743eca731eadf75b285faffc8f02dd (commit)
       via  ffdb4d8e6d7a8d64ec2e3e953740140d6036ee86 (commit)
       via  ffad239b618f0a4e9bce9f3307ace89e86672bad (commit)
       via  38252bdafe1431b026a1a057aee826da52fddd07 (commit)
       via  af82e29208bbaeca18e1046f92b6c03ba102f4b7 (commit)
       via  761e3b09fb14c57b3b55bbba7366686da942b7cc (commit)
      from  2441affbf34ff22d24324948b7e35be8f0c2b8c1 (commit)

Summary of changes:
 lib/RT/Shredder.pm                             |    7 +-
 lib/RT/Shredder/CachedGroupMember.pm           |   83 ++++++---
 lib/RT/Shredder/GroupMember.pm                 |   12 +-
 lib/RT/Shredder/Record.pm                      |   11 +-
 lib/RT/Test.pm                                 |   38 ++++
 t/shredder/utils.pl => lib/RT/Test/Shredder.pm |  202 ++++++---------------
 sbin/rt-validator.in                           |  233 +++++++++++++++++++-----
 t/shredder/00load.t                            |   10 +-
 t/shredder/00skeleton.t                        |   18 +--
 t/shredder/01basics.t                          |   20 +--
 t/shredder/01ticket.t                          |   42 ++---
 t/shredder/02group_member.t                    |   34 ++--
 t/shredder/02queue.t                           |   51 +++---
 t/shredder/02template.t                        |   33 ++--
 t/shredder/02user.t                            |   30 ++--
 t/shredder/03plugin.t                          |   10 +-
 t/shredder/03plugin_summary.t                  |   11 +-
 t/shredder/03plugin_tickets.t                  |   30 ++--
 t/shredder/03plugin_users.t                    |   10 +-
 t/validator/group_members.t                    |   62 ++-----
 20 files changed, 475 insertions(+), 472 deletions(-)
 rename t/shredder/utils.pl => lib/RT/Test/Shredder.pm (55%)

- Log -----------------------------------------------------------------
commit 761e3b09fb14c57b3b55bbba7366686da942b7cc
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Mar 28 23:56:50 2012 +0400

    we don't have rights delegations anymore

diff --git a/lib/RT/Shredder/GroupMember.pm b/lib/RT/Shredder/GroupMember.pm
index 5f06fae..f43a0be 100644
--- a/lib/RT/Shredder/GroupMember.pm
+++ b/lib/RT/Shredder/GroupMember.pm
@@ -75,8 +75,6 @@ sub __DependsOn
     $objs->Limit( FIELD => 'ImmediateParentId', VALUE => $self->GroupId );
     push( @$list, $objs );
 
-    # XXX: right delegations should be cleaned here
-
     $deps->_PushDependencies(
             BaseObject => $self,
             Flags => DEPENDS_ON,

commit af82e29208bbaeca18e1046f92b6c03ba102f4b7
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Mar 29 15:49:32 2012 +0400

    move run_validator into RT::Test

diff --git a/lib/RT/Test.pm b/lib/RT/Test.pm
index 50188e9..276ad67 100644
--- a/lib/RT/Test.pm
+++ b/lib/RT/Test.pm
@@ -1075,6 +1075,43 @@ sub clean_caught_mails {
     unlink $tmp{'mailbox'};
 }
 
+my $validator_path = "$RT::SbinPath/rt-validator";
+sub run_validator {
+    my $self = shift;
+    my %args = (check => 1, resolve => 0, force => 1, @_ );
+
+    my $cmd = $validator_path;
+    die "Couldn't find $cmd command" unless -f $cmd;
+
+    while( my ($k,$v) = each %args ) {
+        next unless $v;
+        $cmd .= " --$k '$v'";
+    }
+    $cmd .= ' 2>&1';
+
+    require IPC::Open2;
+    my ($child_out, $child_in);
+    my $pid = IPC::Open2::open2($child_out, $child_in, $cmd);
+    close $child_in;
+
+    my $result = do { local $/; <$child_out> };
+    close $child_out;
+    waitpid $pid, 0;
+
+    DBIx::SearchBuilder::Record::Cachable->FlushCache
+        if $args{'resolve'};
+
+    return ($?, $result);
+}
+
+sub db_is_valid {
+    local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+    my $self = shift;
+    my ($ecode, $res) = $self->run_validator;
+    Test::More::is( $res, '', 'no invalid records' );
+}
+
 =head2 get_relocatable_dir
 
 Takes a path relative to the location of the test file that is being
diff --git a/t/validator/group_members.t b/t/validator/group_members.t
index 6e08831..1ceb214 100644
--- a/t/validator/group_members.t
+++ b/t/validator/group_members.t
@@ -35,45 +35,13 @@ sub load_or_create_group {
     return $group;
 }
 
-my $validator_path = "$RT::SbinPath/rt-validator";
-sub run_validator {
-    my %args = (check => 1, resolve => 0, force => 1, @_ );
-
-    my $cmd = $validator_path;
-    die "Couldn't find $cmd command" unless -f $cmd;
-
-    while( my ($k,$v) = each %args ) {
-        next unless $v;
-        $cmd .= " --$k '$v'";
-    }
-    $cmd .= ' 2>&1';
-
-    require IPC::Open2;
-    my ($child_out, $child_in);
-    my $pid = IPC::Open2::open2($child_out, $child_in, $cmd);
-    close $child_in;
-
-    my $result = do { local $/; <$child_out> };
-    close $child_out;
-    waitpid $pid, 0;
-
-    DBIx::SearchBuilder::Record::Cachable->FlushCache
-        if $args{'resolve'};
-
-    return ($?, $result);
-}
-
-{
-    my ($ecode, $res) = run_validator();
-    is $res, '', 'empty result';
-}
+RT::Test->db_is_valid;
 
 {
     my $group = load_or_create_group('test', Members => [] );
     ok $group, "loaded or created a group";
 
-    my ($ecode, $res) = run_validator();
-    is $res, '', 'empty result';
+    RT::Test->db_is_valid;
 }
 
 # G1 -> G2
@@ -87,20 +55,18 @@ sub run_validator {
     ok $group2->HasMember( $group1->id ), "has member";
     ok $group2->HasMemberRecursively( $group1->id ), "has member";
 
-    my ($ecode, $res) = run_validator();
-    is $res, '', 'empty result';
+    RT::Test->db_is_valid;
 
     $RT::Handle->dbh->do("DELETE FROM CachedGroupMembers");
     DBIx::SearchBuilder::Record::Cachable->FlushCache;
     ok !$group2->HasMemberRecursively( $group1->id ), "has no member, broken DB";
 
-    ($ecode, $res) = run_validator(resolve => 1);
+    my ($ecode, $res) = RT::Test->run_validator(resolve => 1);
 
     ok $group2->HasMember( $group1->id ), "has member";
     ok $group2->HasMemberRecursively( $group1->id ), "has member";
 
-    ($ecode, $res) = run_validator();
-    is $res, '', 'empty result';
+    RT::Test->db_is_valid;
 }
 
 # G1 <- G2 <- G3 <- G4 <- G5
@@ -120,15 +86,14 @@ sub run_validator {
         push @groups, $group;
     }
 
-    my ($ecode, $res) = run_validator();
-    is $res, '', 'empty result';
+    RT::Test->db_is_valid;
 
     $RT::Handle->dbh->do("DELETE FROM CachedGroupMembers");
     DBIx::SearchBuilder::Record::Cachable->FlushCache;
 
     ok !$groups[1]->HasMemberRecursively( $groups[0]->id ), "has no member, broken DB";
 
-    ($ecode, $res) = run_validator(resolve => 1);
+    my ($ecode, $res) = RT::Test->run_validator(resolve => 1);
 
     for ( my $i = 1; $i < @groups; $i++ ) {
         ok $groups[$i]->HasMember( $groups[$i-1]->id ),
@@ -138,8 +103,7 @@ sub run_validator {
             foreach 0..$i-1;
     }
 
-    ($ecode, $res) = run_validator();
-    is $res, '', 'empty result';
+    RT::Test->db_is_valid;
 }
 
 # G1 <- (G2, G3, G4, G5)
@@ -154,8 +118,7 @@ sub run_validator {
     my $parent = load_or_create_group( 'test1', Members => \@groups );
     ok $parent, "loaded or created a group";
 
-    my ($ecode, $res) = run_validator();
-    is $res, '', 'empty result';
+    RT::Test->db_is_valid;
 }
 
 # G1 <- (G2, G3, G4) <- G5
@@ -173,7 +136,6 @@ sub run_validator {
     my $parent = load_or_create_group( 'test1', Members => \@groups );
     ok $parent, "loaded or created a group";
 
-    my ($ecode, $res) = run_validator();
-    is $res, '', 'empty result';
+    RT::Test->db_is_valid;
 }
 

commit 38252bdafe1431b026a1a057aee826da52fddd07
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Apr 3 22:23:10 2012 +0400

    make sure validator exits with meaningful exit code

diff --git a/sbin/rt-validator.in b/sbin/rt-validator.in
index ba6d6f3..be8a2d5 100644
--- a/sbin/rt-validator.in
+++ b/sbin/rt-validator.in
@@ -81,7 +81,7 @@ GetOptions(
 if ( $opt{help} || !$opt{check} ) {
     require Pod::Usage;
     print Pod::Usage::pod2usage( { verbose => 2 } );
-    exit;
+    exit 2;
 }
 
 usage_warning() if $opt{'resolve'} && !$opt{'force'};
@@ -208,7 +208,7 @@ foreach my $table ( qw(Users Groups) ) {
             ." The script can either create the missing record in Principals"
             ." or delete the record in $table.";
         my ($type) = ($table =~ /^(.*)s$/);
-        check_integrity(
+        return check_integrity(
             $table, 'id' => 'Principals', 'id',
             join_condition => 't.PrincipalType = ?',
             bind_values => [ $type ],
@@ -236,7 +236,7 @@ foreach my $table ( qw(Users Groups) ) {
             ." In some cases it's possible to manually resurrect such records,"
             ." but this utility can only delete records.";
 
-        check_integrity(
+        return check_integrity(
             'Principals', 'id' => $table, 'id',
             condition   => 's.PrincipalType = ?',
             bind_values => [ $table =~ /^(.*)s$/ ],
@@ -251,8 +251,9 @@ foreach my $table ( qw(Users Groups) ) {
 }
 
 push @CHECKS, 'User <-> ACL equivalence group' => sub {
+    my $res = 1;
     # from user to group
-    check_integrity(
+    $res *= check_integrity(
         'Users', 'id' => 'Groups', 'Instance',
         join_condition   => 't.Domain = ? AND t.Type = ?',
         bind_values => [ 'ACLEquivalence',  'UserEquiv' ],
@@ -268,7 +269,7 @@ push @CHECKS, 'User <-> ACL equivalence group' => sub {
         },
     );
     # from group to user
-    check_integrity(
+    $res *= check_integrity(
         'Groups', 'Instance' => 'Users', 'id',
         condition   => 's.Domain = ? AND s.Type = ?',
         bind_values => [ 'ACLEquivalence',  'UserEquiv' ],
@@ -282,25 +283,27 @@ push @CHECKS, 'User <-> ACL equivalence group' => sub {
         },
     );
     # one ACL equiv group for each user
-    check_uniqueness(
+    $res *= check_uniqueness(
         'Groups',
         columns     => ['Instance'],
         condition   => '.Domain = ? AND .Type = ?',
         bind_values => [ 'ACLEquivalence',  'UserEquiv' ],
     );
+    return $res;
 };
 
 # check integrity of Queue role groups
 push @CHECKS, 'Queues <-> Role Groups' => sub {
     # XXX: we check only that there is at least one group for a queue
     # from queue to group
-    check_integrity(
+    my $res = 1;
+    $res *= check_integrity(
         'Queues', 'id' => 'Groups', 'Instance',
         join_condition   => 't.Domain = ?',
         bind_values => [ 'RT::Queue-Role' ],
     );
     # from group to queue
-    check_integrity(
+    $res *= check_integrity(
         'Groups', 'Instance' => 'Queues', 'id',
         condition   => 's.Domain = ?',
         bind_values => [ 'RT::Queue-Role' ],
@@ -313,19 +316,21 @@ push @CHECKS, 'Queues <-> Role Groups' => sub {
             delete_record( 'Groups', $id );
         },
     );
+    return $res;
 };
 
 # check integrity of Ticket role groups
 push @CHECKS, 'Tickets <-> Role Groups' => sub {
     # XXX: we check only that there is at least one group for a queue
     # from queue to group
-    check_integrity(
+    my $res = 1;
+    $res *= check_integrity(
         'Tickets', 'id' => 'Groups', 'Instance',
         join_condition   => 't.Domain = ?',
         bind_values => [ 'RT::Ticket-Role' ],
     );
     # from group to ticket
-    check_integrity(
+    $res *= check_integrity(
         'Groups', 'Instance' => 'Tickets', 'id',
         condition   => 's.Domain = ?',
         bind_values => [ 'RT::Ticket-Role' ],
@@ -338,12 +343,13 @@ push @CHECKS, 'Tickets <-> Role Groups' => sub {
             delete_record( 'Groups', $id );
         },
     );
+    return $res;
 };
 
 # additional CHECKS on groups
 push @CHECKS, 'Role Groups (Instance, Type) uniqueness' => sub {
     # Check that Domain, Instance and Type are unique
-    check_uniqueness(
+    return check_uniqueness(
         'Groups',
         columns     => ['Domain', 'Instance', 'Type'],
         condition   => '.Domain LIKE ?',
@@ -352,7 +358,7 @@ push @CHECKS, 'Role Groups (Instance, Type) uniqueness' => sub {
 };
 
 push @CHECKS, 'System internal group uniqueness' => sub {
-    check_uniqueness(
+    return check_uniqueness(
         'Groups',
         columns     => ['Instance', 'Type'],
         condition   => '.Domain = ?',
@@ -362,7 +368,7 @@ push @CHECKS, 'System internal group uniqueness' => sub {
 
 # CHECK that user defined group names are unique
 push @CHECKS, 'User Defined Group Name uniqueness' => sub {
-    check_uniqueness(
+    return check_uniqueness(
         'Groups',
         columns         => ['Name'],
         condition       => '.Domain = ?',
@@ -386,7 +392,8 @@ push @CHECKS, 'GMs -> Groups, Members' => sub {
     my $msg = "A record in GroupMembers references an object that doesn't exist."
         ." Maybe you deleted a group or principal directly from the database?"
         ." Usually it's OK to delete such records.";
-    check_integrity(
+    my $res = 1;
+    $res *= check_integrity(
         'GroupMembers', 'GroupId' => 'Groups', 'id',
         action => sub {
             my $id = shift;
@@ -395,7 +402,7 @@ push @CHECKS, 'GMs -> Groups, Members' => sub {
             delete_record( 'GroupMembers', $id );
         },
     );
-    check_integrity(
+    $res *= check_integrity(
         'GroupMembers', 'MemberId' => 'Principals', 'id',
         action => sub {
             my $id = shift;
@@ -404,12 +411,14 @@ push @CHECKS, 'GMs -> Groups, Members' => sub {
             delete_record( 'GroupMembers', $id );
         },
     );
+    return $res;
 };
 
 # CGM and GM
 push @CHECKS, 'CGM vs. GM' => sub {
+    my $res = 1;
     # all GM record should be duplicated in CGM
-    check_integrity(
+    $res *= check_integrity(
         GroupMembers       => ['GroupId', 'MemberId'],
         CachedGroupMembers => ['GroupId', 'MemberId'],
         action => sub {
@@ -429,7 +438,7 @@ push @CHECKS, 'CGM vs. GM' => sub {
     );
 
     # each group should have a CGM record where MemberId == GroupId
-    check_integrity(
+    $res *= check_integrity(
         Groups => ['id', 'id'],
         CachedGroupMembers => ['GroupId', 'MemberId'],
         action => sub {
@@ -445,13 +454,13 @@ push @CHECKS, 'CGM vs. GM' => sub {
             die "Couldn't load group #$id" unless $g->id;
             die "Loaded group by $id has id ". $g->id  unless $g->id == $id;
             my $cgm = create_record( 'CachedGroupMembers',
-                GroupId => $id, MemberId => $id,
+                GroupId => $id, MemberId => $id, Disabled => 0,
             );
         },
     );
     # and back, each record in CGM with MemberId == GroupId without exceptions
     # should reference a group
-    check_integrity(
+    $res *= check_integrity(
         CachedGroupMembers => ['GroupId', 'MemberId'],
         Groups => ['id', 'id'],
         condition => "s.GroupId = s.MemberId",
@@ -494,6 +503,7 @@ END
 
         my $sth = execute_query( $query );
         while ( my ($id) = $sth->fetchrow_array ) {
+            $res = 0;
             print STDERR "CGM #$id has no corresponding record in GM or pair in CGM table\n";
             next unless prompt(
                 'Delete',
@@ -521,6 +531,7 @@ WHERE
 END
         my $sth = execute_query( $query );
         while ( my ($g, $m, $d) = $sth->fetchrow_array ) {
+            $res = 0;
             print STDERR "CGM table has no record ($g, $m), but ($g -> X -> $m) pair exist in CGM\n";
             next unless prompt(
                 'Create',
@@ -531,12 +542,14 @@ END
             );
         }
     }
+    return $res;
 };
 
 # Disabled in CGM
 push @CHECKS, 'Disabled in CGM' => sub {
+    my $res = 1;
     # make sure every disabled group forms disabled CGM records
-    check_integrity(
+    $res *= check_integrity(
         Principals         => ['id'],
         CachedGroupMembers => ['GroupId'],
         condition          => "s.PrincipalType = ? AND s.Disabled != 0",
@@ -553,7 +566,7 @@ push @CHECKS, 'Disabled in CGM' => sub {
         },
     );
     # make sure every enabled group has enabled (G,G) record in CGM
-    check_integrity(
+    $res *= check_integrity(
         Principals         => ['id', 'id'],
         CachedGroupMembers => ['GroupId', 'MemberId'],
         condition          => "s.PrincipalType = ? AND s.Disabled = 0",
@@ -585,6 +598,7 @@ WHERE   p.id = GM.GroupId
 END
         my $sth = execute_query( $query );
         while ( my ($g, $m) = $sth->fetchrow_array ) {
+            $res = 0;
             print STDERR "CGM ($g, $m) is not active while it should be\n";
             next unless prompt(
                 'Update',
@@ -599,11 +613,14 @@ END
 
     # now time to deal with indirect CGM records
     # TODO.
+
+    return $res;
 };
 
 # Tickets
 push @CHECKS, 'Tickets -> other' => sub {
-    check_integrity(
+    my $res = 1;
+    $res *= check_integrity(
         'Tickets', 'EffectiveId' => 'Tickets', 'id',
         action => sub {
             my $id = shift;
@@ -615,19 +632,21 @@ push @CHECKS, 'Tickets -> other' => sub {
             delete_record( 'Tickets', $id );
         },
     );
-    check_integrity(
+    $res *= check_integrity(
         'Tickets', 'Queue' => 'Queues', 'id',
     );
-    check_integrity(
+    $res *= check_integrity(
         'Tickets', 'Owner' => 'Users', 'id',
     );
     # XXX: check that owner is only member of owner role group
+    return $res;
 };
 
 
 push @CHECKS, 'Transactions -> other' => sub {
+    my $res = 1;
     foreach my $model ( @models ) {
-        check_integrity(
+        $res *= check_integrity(
             'Transactions', 'ObjectId' => m2t($model), 'id',
             condition   => 's.ObjectType = ?',
             bind_values => [ "RT::$model" ],
@@ -642,13 +661,13 @@ push @CHECKS, 'Transactions -> other' => sub {
         );
     }
     # type = CustomField
-    check_integrity(
+    $res *= check_integrity(
         'Transactions', 'Field' => 'CustomFields', 'id',
         condition   => 's.Type = ?',
         bind_values => [ 'CustomField' ],
     );
     # type = Take, Untake, Force, Steal or Give
-    check_integrity(
+    $res *= check_integrity(
         'Transactions', 'OldValue' => 'Users', 'id',
         condition   => 's.Type IN (?, ?, ?, ?, ?)',
         bind_values => [ qw(Take Untake Force Steal Give) ],
@@ -662,7 +681,7 @@ push @CHECKS, 'Transactions -> other' => sub {
             delete_record( 'Transactions', $id );
         },
     );
-    check_integrity(
+    $res *= check_integrity(
         'Transactions', 'NewValue' => 'Users', 'id',
         condition   => 's.Type IN (?, ?, ?, ?, ?)',
         bind_values => [ qw(Take Untake Force Steal Give) ],
@@ -677,7 +696,7 @@ push @CHECKS, 'Transactions -> other' => sub {
         },
     );
     # type = DelWatcher
-    check_integrity(
+    $res *= check_integrity(
         'Transactions', 'OldValue' => 'Principals', 'id',
         condition   => 's.Type = ?',
         bind_values => [ 'DelWatcher' ],
@@ -692,7 +711,7 @@ push @CHECKS, 'Transactions -> other' => sub {
         },
     );
     # type = AddWatcher
-    check_integrity(
+    $res *= check_integrity(
         'Transactions', 'NewValue' => 'Principals', 'id',
         condition   => 's.Type = ?',
         bind_values => [ 'AddWatcher' ],
@@ -711,7 +730,7 @@ push @CHECKS, 'Transactions -> other' => sub {
 #   handled in 'Links: *' checks as {New,Old}Value store URIs
 
     # type = Set, Field = Queue
-    check_integrity(
+    $res *= check_integrity(
         'Transactions', 'NewValue' => 'Queues', 'id',
         condition   => 's.Type = ? AND s.Field = ?',
         bind_values => [ 'Set', 'Queue' ],
@@ -725,7 +744,7 @@ push @CHECKS, 'Transactions -> other' => sub {
             delete_record( 'Transactions', $id );
         },
     );
-    check_integrity(
+    $res *= check_integrity(
         'Transactions', 'OldValue' => 'Queues', 'id',
         condition   => 's.Type = ? AND s.Field = ?',
         bind_values => [ 'Set', 'Queue' ],
@@ -740,17 +759,19 @@ push @CHECKS, 'Transactions -> other' => sub {
         },
     );
     # Reminders
-    check_integrity(
+    $res *= check_integrity(
         'Transactions', 'NewValue' => 'Tickets', 'id',
         join_condition => 't.Type = ?',
         condition      => 's.Type IN (?, ?, ?)',
         bind_values    => [ 'reminder', 'AddReminder', 'OpenReminder', 'ResolveReminder' ],
     );
+    return $res;
 };
 
 # Attachments
 push @CHECKS, 'Attachments -> other' => sub {
-    check_integrity(
+    my $res = 1;
+    $res *= check_integrity(
         Attachments  => 'TransactionId', Transactions => 'id',
         action => sub {
             my $id = shift;
@@ -760,7 +781,7 @@ push @CHECKS, 'Attachments -> other' => sub {
             delete_record( 'Attachments', $id );
         },
     );
-    check_integrity(
+    $res *= check_integrity(
         Attachments => 'Parent', Attachments => 'id',
         action => sub {
             my $id = shift;
@@ -770,64 +791,72 @@ push @CHECKS, 'Attachments -> other' => sub {
             delete_record( 'Attachments', $id );
         },
     );
-    check_integrity(
+    $res *= check_integrity(
         Attachments => 'Parent',
         Attachments => 'id',
         join_condition => 's.TransactionId = t.TransactionId',
     );
+    return $res;
 };
 
 push @CHECKS, 'CustomFields and friends' => sub {
+    my $res = 1;
     #XXX: ObjectCustomFields needs more love
-    check_integrity(
+    $res *= check_integrity(
         'CustomFieldValues', 'CustomField' => 'CustomFields', 'id',
     );
-    check_integrity(
+    $res *= check_integrity(
         'ObjectCustomFieldValues', 'CustomField' => 'CustomFields', 'id',
     );
     foreach my $model ( @models ) {
-        check_integrity(
+        $res *= check_integrity(
             'ObjectCustomFieldValues', 'ObjectId' => m2t($model), 'id',
             condition   => 's.ObjectType = ?',
             bind_values => [ "RT::$model" ],
         );
     }
+    return $res;
 };
 
 push @CHECKS, Templates => sub {
-    check_integrity(
+    return check_integrity(
         'Templates', 'Queue' => 'Queues', 'id',
     );
 };
 
 push @CHECKS, Scrips => sub {
-    check_integrity(
+    my $res = 1;
+    $res *= check_integrity(
         'Scrips', 'Queue' => 'Queues', 'id',
     );
-    check_integrity(
+    $res *= check_integrity(
         'Scrips', 'ScripCondition' => 'ScripConditions', 'id',
     );
-    check_integrity(
+    $res *= check_integrity(
         'Scrips', 'ScripAction' => 'ScripActions', 'id',
     );
-    check_integrity(
+    $res *= check_integrity(
         'Scrips', 'Template' => 'Templates', 'id',
     );
+    return $res;
 };
 
 push @CHECKS, Attributes => sub {
+    my $res = 1;
     foreach my $model ( @models ) {
-        check_integrity(
+        $res *= check_integrity(
             'Attributes', 'ObjectId' => m2t($model), 'id',
             condition   => 's.ObjectType = ?',
             bind_values => [ "RT::$model" ],
         );
     }
+    return $res;
 };
 
 # Fix situations when Creator or LastUpdatedBy references ACL equivalence
 # group of a user instead of user
 push @CHECKS, 'FIX: LastUpdatedBy and Creator' => sub {
+    my $res = 1;
     my %fix = ();
     foreach my $model ( @models ) {
         my $class = "RT::$model";
@@ -857,6 +886,7 @@ END
 
             my $sth = execute_query( $query, 'ACLEquivalence', 'UserEquiv' );
             while ( my ($rid, $gid, $uid) = $sth->fetchrow_array ) {
+                $res = 0;
                 print STDERR "Record #$rid in $table refers to ACL equivalence group #$gid of user #$uid";
                 print STDERR " when must reference user.\n";
                 $action->( $gid, $uid );
@@ -879,16 +909,18 @@ END
         }
         $redo_check{'FIX: LastUpdatedBy and Creator'} = 1;
     }
+    return $res;
 };
 
 push @CHECKS, 'LastUpdatedBy and Creator' => sub {
+    my $res = 1;
     foreach my $model ( @models ) {
         my $class = "RT::$model";
         my $object = $class->new( RT->SystemUser );
         my $table = $object->Table;
         foreach my $column ( qw(LastUpdatedBy Creator) ) {
             next unless $object->_Accessible( $column, 'auto' );
-            check_integrity(
+            $res *= check_integrity(
                 $table, $column => 'Users', 'id',
                 action => sub {
                     my ($id, %prop) = @_;
@@ -906,9 +938,11 @@ push @CHECKS, 'LastUpdatedBy and Creator' => sub {
             );
         }
     }
+    return $res;
 };
 
 push @CHECKS, 'Links: wrong organization' => sub {
+    my $res = 1;
     my @URI_USES = (
         { model => 'Transaction', column => 'OldValue', Additional => { Type => 'DeleteLink' } },
         { model => 'Transaction', column => 'NewValue', Additional => { Type => 'AddLink' } },
@@ -934,6 +968,7 @@ push @CHECKS, 'Links: wrong organization' => sub {
         }
         my $sth = execute_query( $query, @binds );
         while ( my ($id, $value) = $sth->fetchrow_array ) {
+            $res = 0;
             print STDERR "Record #$id in $table. Value of $column column most probably is an incorrect link\n";
             my ($wrong_org) = ( $value =~ m{^\Q$scheme\E://(.+)/[^/]+/[0-9]*$} );
             next unless my $replace_with = prompt(
@@ -957,9 +992,11 @@ push @CHECKS, 'Links: wrong organization' => sub {
             last; # plenty of chances we covered all cases with one update
         }
     }
+    return $res;
 };
 
 push @CHECKS, 'Links: LocalX for non-ticket' => sub {
+    my $res = 1;
     my $rt_uri = RT::URI::fsck_com_rt->new( $RT::SystemUser );
     my $scheme = $rt_uri->Scheme;
     my $prefix = $rt_uri->LocalURIPrefix;
@@ -973,6 +1010,7 @@ push @CHECKS, 'Links: LocalX for non-ticket' => sub {
 
         my $sth = execute_query( "SELECT id FROM $table WHERE $where", @binds );
         while ( my ($id, $value) = $sth->fetchrow_array ) {
+            $res = 0;
             print STDERR "Record #$id in $table. Value of Local$dir is not 0\n";
             next unless my $replace_with = prompt(
                 'Replace',
@@ -987,9 +1025,11 @@ push @CHECKS, 'Links: LocalX for non-ticket' => sub {
             last; # we covered all cases with one update
         }
     }
+    return $res;
 };
 
 push @CHECKS, 'Links: LocalX != X' => sub {
+    my $res = 1;
     my $rt_uri = RT::URI::fsck_com_rt->new( $RT::SystemUser );
     my $scheme = $rt_uri->Scheme;
     my $prefix = $rt_uri->LocalURIPrefix .'/ticket/';
@@ -1006,6 +1046,7 @@ push @CHECKS, 'Links: LocalX != X' => sub {
 
         my $sth = execute_query( "SELECT id FROM $table WHERE $where", @binds );
         while ( my ($id, $value) = $sth->fetchrow_array ) {
+            $res = 0;
             print STDERR "Record #$id in $table. Value of $dir doesn't match ticket id in Local$dir\n";
             next unless my $replace_with = prompt(
                 'Replace',
@@ -1022,9 +1063,11 @@ push @CHECKS, 'Links: LocalX != X' => sub {
             last; # we covered all cases with one update
         }
     }
+    return $res;
 };
 
 push @CHECKS, 'Links: missing object' => sub {
+    my $res = 1;
     my @URI_USES = (
         { model => 'Transaction', column => 'OldValue', Additional => { Type => 'DeleteLink' } },
         { model => 'Transaction', column => 'NewValue', Additional => { Type => 'AddLink' } },
@@ -1058,6 +1101,7 @@ push @CHECKS, 'Links: missing object' => sub {
 
             my $sth = execute_query( $query, @binds );
             while ( my ($sid) = $sth->fetchrow_array ) {
+                $res = 0;
                 print STDERR "Link in $scolumn column in record #$sid in $stable table points"
                     ." to not existing object.\n";
                 next unless prompt(
@@ -1072,6 +1116,7 @@ push @CHECKS, 'Links: missing object' => sub {
             }
         }
     }
+    return $res;
 };
 
 
@@ -1083,8 +1128,9 @@ if ($opt{'links-only'}) {
     @do_check = grep { /^Links:/ } @do_check;
 }
 
+my $status = 1;
 while ( my $check = shift @do_check ) {
-    $CHECKS{ $check }->();
+    $status *= $CHECKS{ $check }->();
 
     foreach my $redo ( keys %redo_check ) {
         die "check $redo doesn't exist" unless $CHECKS{ $redo };
@@ -1093,6 +1139,8 @@ while ( my $check = shift @do_check ) {
         push @do_check, $redo;
     }
 }
+exit 1 unless $status;
+exit 0;
 
 sub check_integrity {
     my ($stable, @scols) = (shift, shift);
@@ -1128,8 +1176,12 @@ sub check_integrity {
         push @binds, 0;
     }
 
+    my $res = 1;
+
     my $sth = execute_query( $query, @binds );
     while ( my ($sid, @set) = $sth->fetchrow_array ) {
+        $res = 0;
+
         print STDERR "Record #$sid in $stable references a nonexistent record in $ttable\n";
         for ( my $i = 0; $i < @scols; $i++ ) {
             print STDERR "\t$scols[$i] => '$set[$i]' => $tcols[$i]\n";
@@ -1138,6 +1190,7 @@ sub check_integrity {
         $args{'action'}->( $sid, map { $scols[$_] => $set[$_] } (0 .. (@scols-1)) )
             if $args{'action'};
     }
+    return $res;
 }
 
 sub describe {
@@ -1197,13 +1250,16 @@ sub check_uniqueness {
         $args{'bind_values'}? (@{ $args{'bind_values'} }, @{ $args{'bind_values'} }): (),
         $args{'extra_values'}? (@{ $args{'extra_values'} }): ()
     );
+    my $res = 1;
     while ( my ($sid, $tid, @set) = $sth->fetchrow_array ) {
+        $res = 0;
         print STDERR "Record #$tid in $on has the same set of values as $sid\n";
         for ( my $i = 0; $i < @columns; $i++ ) {
             print STDERR "\t$columns[$i] => '$set[$i]'\n";
         }
         $args{'action'}->( $tid, map { $columns[$_] => $set[$_] } (0 .. (@columns-1)) ) if $args{'action'};
     }
+    return $res;
 }
 
 sub load_record {

commit ffad239b618f0a4e9bce9f3307ace89e86672bad
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Apr 6 00:25:41 2012 +0400

    complete validation and fixing Disabled column in CGM

diff --git a/sbin/rt-validator.in b/sbin/rt-validator.in
index be8a2d5..452fab5 100644
--- a/sbin/rt-validator.in
+++ b/sbin/rt-validator.in
@@ -548,7 +548,7 @@ END
 # Disabled in CGM
 push @CHECKS, 'Disabled in CGM' => sub {
     my $res = 1;
-    # make sure every disabled group forms disabled CGM records
+    # make sure every disabled group forms only disabled CGM records
     $res *= check_integrity(
         Principals         => ['id'],
         CachedGroupMembers => ['GroupId'],
@@ -611,9 +611,86 @@ END
         }
     }
 
-    # now time to deal with indirect CGM records
-    # TODO.
+    # make sure every active CGM that is not real GM has
+    # active joint
+    {
+         my $query = <<END;
+SELECT CGM.GroupId, CGM.MemberId
+FROM CachedGroupMembers CGM
+WHERE   CGM.Disabled = 0
+    AND CGM.GroupId != CGM.MemberId
+    AND NOT EXISTS (
+        SELECT 1 FROM GroupMembers GM
+        WHERE GM.GroupId = CGM.GroupId
+            AND GM.MemberId = CGM.MemberId
+    )
+    AND NOT EXISTS (
+        SELECT 1 FROM CachedGroupMembers CGML, CachedGroupMembers CGMR
+        WHERE
+                CGML.GroupId = CGM.GroupId
+            AND CGML.MemberId = CGMR.GroupId
+            AND CGMR.MemberId = CGM.MemberId
+
+            AND CGML.Disabled = 0
+            AND CGMR.Disabled = 0
 
+            AND CGML.GroupId != CGML.MemberId
+            AND CGMR.GroupId != CGMR.MemberId
+    )
+END
+        my $sth = execute_query( $query );
+        while ( my ($g, $m) = $sth->fetchrow_array ) {
+            $res = 0;
+            print STDERR "CGM ($g, $m) is active while it should not be\n";
+            next unless prompt(
+                'Update',
+                "Active records in CachedGroupMembers."
+            );
+            update_records('CachedGroupMembers',
+                { GroupId => $g, MemberId => $m },
+                { Disabled => 1 }
+            );
+        }
+    }
+
+    # escape if we had problems
+    return $res unless $res;
+
+    # make sure every inactive CGM don't have active joint
+    {
+         my $query = <<END;
+SELECT CGM.GroupId, CGM.MemberId
+FROM CachedGroupMembers CGM
+WHERE   CGM.Disabled != 0
+    AND CGM.GroupId != CGM.MemberId
+    AND EXISTS (
+        SELECT 1 FROM CachedGroupMembers CGML, CachedGroupMembers CGMR
+        WHERE
+                CGML.GroupId = CGM.GroupId
+            AND CGML.MemberId = CGMR.GroupId
+            AND CGMR.MemberId = CGM.MemberId
+
+            AND CGML.Disabled = 0
+            AND CGMR.Disabled = 0
+
+            AND CGML.GroupId != CGML.MemberId
+            AND CGMR.GroupId != CGMR.MemberId
+    )
+END
+        my $sth = execute_query( $query );
+        while ( my ($g, $m) = $sth->fetchrow_array ) {
+            $res = 0;
+            print STDERR "CGM ($g, $m) is not active while it should be\n";
+            next unless prompt(
+                'Update',
+                "Inactive records in CachedGroupMembers."
+            );
+            update_records('CachedGroupMembers',
+                { GroupId => $g, MemberId => $m },
+                { Disabled => 0 }
+            );
+        }
+    }
     return $res;
 };
 

commit ffdb4d8e6d7a8d64ec2e3e953740140d6036ee86
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Apr 6 00:27:44 2012 +0400

    print validation errors if any

diff --git a/lib/RT/Test.pm b/lib/RT/Test.pm
index 276ad67..9f876b2 100644
--- a/lib/RT/Test.pm
+++ b/lib/RT/Test.pm
@@ -1109,7 +1109,8 @@ sub db_is_valid {
 
     my $self = shift;
     my ($ecode, $res) = $self->run_validator;
-    Test::More::is( $res, '', 'no invalid records' );
+    Test::More::is( $ecode, 0, 'no invalid records' )
+        or Test::More::diag "errors:\n$res";
 }
 
 =head2 get_relocatable_dir

commit f4883e6f44743eca731eadf75b285faffc8f02dd
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Apr 6 00:29:57 2012 +0400

    check exit code when we expect DB to be invalid

diff --git a/t/validator/group_members.t b/t/validator/group_members.t
index 1ceb214..237d9b5 100644
--- a/t/validator/group_members.t
+++ b/t/validator/group_members.t
@@ -2,7 +2,7 @@
 use strict;
 use warnings;
 
-use RT::Test tests => 60;
+use RT::Test tests => 62;
 
 sub load_or_create_group {
     my $name = shift;
@@ -62,6 +62,7 @@ RT::Test->db_is_valid;
     ok !$group2->HasMemberRecursively( $group1->id ), "has no member, broken DB";
 
     my ($ecode, $res) = RT::Test->run_validator(resolve => 1);
+    isnt($ecode, 0, 'non-zero exit code');
 
     ok $group2->HasMember( $group1->id ), "has member";
     ok $group2->HasMemberRecursively( $group1->id ), "has member";
@@ -94,6 +95,7 @@ RT::Test->db_is_valid;
     ok !$groups[1]->HasMemberRecursively( $groups[0]->id ), "has no member, broken DB";
 
     my ($ecode, $res) = RT::Test->run_validator(resolve => 1);
+    isnt($ecode, 0, 'non-zero exit code');
 
     for ( my $i = 1; $i < @groups; $i++ ) {
         ok $groups[$i]->HasMember( $groups[$i-1]->id ),

commit bc39464fa10ca05952bcd6f23296eeb910998f99
Merge: f4883e6 9a64ee7
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Apr 6 21:49:58 2012 +0400

    Merge branch '4.2/test-shredder-pm' into 4.2/optimize-cgm-table


commit d7683c876e25e22e95fa561650ffa5118c4bda7c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Apr 9 18:45:49 2012 +0400

    validate database in tests after each wipeout

diff --git a/t/shredder/01basics.t b/t/shredder/01basics.t
index aea4e49..368c9ba 100644
--- a/t/shredder/01basics.t
+++ b/t/shredder/01basics.t
@@ -3,7 +3,7 @@ use strict;
 use warnings;
 
 use Test::Deep;
-use RT::Test::Shredder tests => 3;
+use RT::Test::Shredder tests => 4;
 my $test = "RT::Test::Shredder";
 
 $test->create_savepoint();
@@ -20,4 +20,6 @@ ok( $id, "load ticket" ) or diag( "error: $msg" );
 my $shredder = $test->shredder_new();
 $shredder->Wipeout( Object => $ticket );
 
+$test->db_is_valid;
+
 cmp_deeply( $test->dump_current_and_savepoint(), "current DB equal to savepoint");
diff --git a/t/shredder/01ticket.t b/t/shredder/01ticket.t
index 6f4183d..4ef4992 100644
--- a/t/shredder/01ticket.t
+++ b/t/shredder/01ticket.t
@@ -3,7 +3,7 @@ use strict;
 use warnings;
 
 use Test::Deep;
-use RT::Test::Shredder tests => 15;
+use RT::Test::Shredder tests => 20;
 my $test = "RT::Test::Shredder";
 
 $test->create_savepoint('clean');
@@ -26,6 +26,7 @@ use RT::Tickets;
     my $shredder = $test->shredder_new();
     $shredder->PutObjects( Objects => $tickets );
     $shredder->WipeoutAll;
+    $test->db_is_valid;
 }
 cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to savepoint");
 
@@ -44,10 +45,12 @@ cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to sav
     my $shredder = $test->shredder_new();
     $shredder->PutObjects( Objects => $child );
     $shredder->WipeoutAll;
+    $test->db_is_valid;
     cmp_deeply( $test->dump_current_and_savepoint('parent_ticket'), "current DB equal to savepoint");
 
     $shredder->PutObjects( Objects => $parent );
     $shredder->WipeoutAll;
+    $test->db_is_valid;
 }
 cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to savepoint");
 
@@ -61,16 +64,18 @@ cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to sav
 
     my $child = RT::Ticket->new( RT->SystemUser );
     my ($cid) = $child->Create( Subject => 'test', Queue => 1 );
-    ok( $cid, "created new ticket" );
+    ok( $cid, "created new ticket #$cid" );
 
     ($status, $msg) = $parent->AddLink( Type => 'DependsOn', Target => $cid );
     ok( $status, "Added link between tickets") or diag("error: $msg");
     my $shredder = $test->shredder_new();
     $shredder->PutObjects( Objects => $child );
     $shredder->WipeoutAll;
+    $test->db_is_valid;
     cmp_deeply( $test->dump_current_and_savepoint('parent_ticket'), "current DB equal to savepoint");
 
     $shredder->PutObjects( Objects => $parent );
     $shredder->WipeoutAll;
+    $test->db_is_valid;
 }
 cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to savepoint");
diff --git a/t/shredder/02group_member.t b/t/shredder/02group_member.t
index 4f881a0..4170c94 100644
--- a/t/shredder/02group_member.t
+++ b/t/shredder/02group_member.t
@@ -3,7 +3,7 @@ use strict;
 use warnings;
 
 use Test::Deep;
-use RT::Test::Shredder tests => 22;
+use RT::Test::Shredder tests => 26;
 my $test = "RT::Test::Shredder";
 
 ### nested membership check
@@ -41,14 +41,17 @@ my $test = "RT::Test::Shredder";
 	my $shredder = $test->shredder_new();
 	$shredder->PutObjects( Objects => $members );
 	$shredder->WipeoutAll();
+	$test->db_is_valid;
 	cmp_deeply( $test->dump_current_and_savepoint('buadd'), "current DB equal to savepoint");
 	
 	$shredder->PutObjects( Objects => $user );
 	$shredder->WipeoutAll();
+	$test->db_is_valid;
 	cmp_deeply( $test->dump_current_and_savepoint('bucreate'), "current DB equal to savepoint");
 	
 	$shredder->PutObjects( Objects => [$pgroup, $cgroup] );
 	$shredder->WipeoutAll();
+	$test->db_is_valid;
 	cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to savepoint");
 }
 
@@ -85,6 +88,7 @@ my $test = "RT::Test::Shredder";
 	my $shredder = $test->shredder_new();
 	$shredder->PutObjects( Objects => $member );
 	$shredder->WipeoutAll();
+	$test->db_is_valid;
 
 	$ticket = RT::Ticket->new( RT->SystemUser );
 	($status, $msg) = $ticket->Load( $id );
diff --git a/t/shredder/02queue.t b/t/shredder/02queue.t
index 5b83e0f..00213ec 100644
--- a/t/shredder/02queue.t
+++ b/t/shredder/02queue.t
@@ -3,7 +3,7 @@ use strict;
 use warnings;
 
 use Test::Deep;
-use RT::Test::Shredder tests => 16;
+use RT::Test::Shredder tests => 21;
 my $test = "RT::Test::Shredder";
 
 diag 'simple queue' if $ENV{TEST_VERBOSE};
@@ -16,6 +16,7 @@ diag 'simple queue' if $ENV{TEST_VERBOSE};
 	my $shredder = $test->shredder_new();
 	$shredder->PutObjects( Objects => $queue );
 	$shredder->WipeoutAll;
+	$test->db_is_valid;
 	cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to savepoint");
 }
 
@@ -39,6 +40,7 @@ diag 'queue with scrip' if $ENV{TEST_VERBOSE};
 	my $shredder = $test->shredder_new();
 	$shredder->PutObjects( Objects => $queue );
 	$shredder->WipeoutAll;
+	$test->db_is_valid;
 	cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to savepoint");
 }
 
@@ -60,6 +62,7 @@ diag 'queue with template' if $ENV{TEST_VERBOSE};
 	my $shredder = $test->shredder_new();
 	$shredder->PutObjects( Objects => $queue );
 	$shredder->WipeoutAll;
+	$test->db_is_valid;
 	cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to savepoint");
 }
 
@@ -83,6 +86,7 @@ diag 'queue with a right granted' if $ENV{TEST_VERBOSE};
 	my $shredder = $test->shredder_new();
 	$shredder->PutObjects( Objects => $queue );
 	$shredder->WipeoutAll;
+	$test->db_is_valid;
 	cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to savepoint");
 }
 
@@ -108,6 +112,7 @@ diag 'queue with a watcher' if $ENV{TEST_VERBOSE};
 	my $shredder = $test->shredder_new();
 	$shredder->PutObjects( Objects => $queue );
 	$shredder->WipeoutAll;
+	$test->db_is_valid;
 	cmp_deeply( $test->dump_current_and_savepoint('bqcreate'), "current DB equal to savepoint");
 
 #	$shredder->PutObjects( Objects => $group );
diff --git a/t/shredder/02template.t b/t/shredder/02template.t
index 2f9b4ab..27c9bac 100644
--- a/t/shredder/02template.t
+++ b/t/shredder/02template.t
@@ -3,7 +3,7 @@ use strict;
 use warnings;
 
 use Test::Deep;
-use RT::Test::Shredder tests => 7;
+use RT::Test::Shredder tests => 10;
 my $test = "RT::Test::Shredder";
 
 diag 'global template' if $ENV{TEST_VERBOSE};
@@ -19,6 +19,7 @@ diag 'global template' if $ENV{TEST_VERBOSE};
 	my $shredder = $test->shredder_new();
 	$shredder->PutObjects( Objects => $template );
 	$shredder->WipeoutAll;
+	$test->db_is_valid;
 	cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to savepoint");
 }
 
@@ -36,6 +37,7 @@ diag 'local template' if $ENV{TEST_VERBOSE};
 	my $shredder = $test->shredder_new();
 	$shredder->PutObjects( Objects => $template );
 	$shredder->WipeoutAll;
+	$test->db_is_valid;
 	cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to savepoint");
 }
 
@@ -63,5 +65,6 @@ diag 'template used in scrip' if $ENV{TEST_VERBOSE};
 	my $shredder = $test->shredder_new();
 	$shredder->PutObjects( Objects => $template );
 	$shredder->WipeoutAll;
+	$test->db_is_valid;
 	cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to savepoint");
 }
diff --git a/t/shredder/02user.t b/t/shredder/02user.t
index 776a26d..0aa80ee 100644
--- a/t/shredder/02user.t
+++ b/t/shredder/02user.t
@@ -3,7 +3,7 @@ use strict;
 use warnings;
 
 use Test::Deep;
-use RT::Test::Shredder tests => 8;
+use RT::Test::Shredder tests => 10;
 my $test = "RT::Test::Shredder";
 
 $test->create_savepoint('clean');
@@ -38,6 +38,7 @@ $test->create_savepoint('aucreate'); # after user create
     my $shredder = $test->shredder_new();
     $shredder->PutResolver( BaseClass => 'RT::User', Code => $resolver );
     $shredder->Wipeout( Object => $user );
+    $test->db_is_valid;
     cmp_deeply( $test->dump_current_and_savepoint('bucreate'), "current DB equal to savepoint");
 }
 
@@ -49,5 +50,6 @@ $test->create_savepoint('aucreate'); # after user create
     my $shredder = $test->shredder_new();
     eval { $shredder->Wipeout( Object => $user ) };
     ok($@, "wipeout throw exception if no resolvers");
+    $test->db_is_valid;
     cmp_deeply( $test->dump_current_and_savepoint('aucreate'), "current DB equal to savepoint");
 }
diff --git a/t/shredder/03plugin_tickets.t b/t/shredder/03plugin_tickets.t
index 5e223ae..536e6ce 100644
--- a/t/shredder/03plugin_tickets.t
+++ b/t/shredder/03plugin_tickets.t
@@ -3,7 +3,7 @@ use strict;
 use warnings;
 
 use Test::Deep;
-use RT::Test::Shredder tests => 45;
+use RT::Test::Shredder tests => 49;
 my $test = "RT::Test::Shredder";
 
 use_ok('RT::Shredder');
@@ -55,6 +55,7 @@ use_ok('RT::Tickets');
     my $shredder = $test->shredder_new();
     $shredder->PutObjects( Objects => \@objs );
     $shredder->WipeoutAll;
+    $test->db_is_valid;
 }
 cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to savepoint");
 
@@ -97,6 +98,7 @@ cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to sav
     my $shredder = $test->shredder_new();
     $shredder->PutObjects( Objects => \@objs );
     $shredder->WipeoutAll;
+    $test->db_is_valid;
 }
 cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to savepoint");
 
@@ -133,6 +135,7 @@ cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to sav
     my $shredder = $test->shredder_new();
     $shredder->PutObjects( Objects => \@objs );
     $shredder->WipeoutAll;
+    $test->db_is_valid;
 
     my $ticket = RT::Ticket->new( RT->SystemUser );
     $ticket->Load( $cid1 );
@@ -140,5 +143,6 @@ cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to sav
 
     $shredder->PutObjects( Objects => $ticket );
     $shredder->WipeoutAll;
+    $test->db_is_valid;
 }
 cmp_deeply( $test->dump_current_and_savepoint('clean'), "current DB equal to savepoint");

commit 4c0e4a2ede4faa0f1a1347426e72a5f6050550d0
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Apr 9 18:50:19 2012 +0400

    make sure IsLinkValid doesn't skip anything
    
    shredder doesn't care if link links deleted tickets,
    often it's actually the case.

diff --git a/lib/RT/Shredder/Record.pm b/lib/RT/Shredder/Record.pm
index d1c74bf..c053dd5 100644
--- a/lib/RT/Shredder/Record.pm
+++ b/lib/RT/Shredder/Record.pm
@@ -145,12 +145,13 @@ sub __DependsOn
     push( @$list, $objs );
 
 # Links
-    if ( $self->can('_Links') ) {
-        # XXX: We don't use Links->Next as it's dies when object
-        #      is linked to object that doesn't exist
-        #      also, ->Next skip links to deleted tickets :(
+    if ( $self->can('Links') ) {
+        # make sure we don't skip any record
+        no warnings 'redefine';
+        local *RT::Links::IsValidLink = sub { 1 };
+
         foreach ( qw(Base Target) ) {
-            my $objs = $self->_Links( $_ );
+            my $objs = $self->Links( $_ );
             $objs->_DoSearch;
             push @$list, $objs->ItemsArrayRef;
         }

commit 38de6a7355117c359c6f812a106c8ec00c6da783
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Apr 9 18:53:34 2012 +0400

    ImmediateParent is not in the DB

diff --git a/lib/RT/Shredder/GroupMember.pm b/lib/RT/Shredder/GroupMember.pm
index f43a0be..6b06637 100644
--- a/lib/RT/Shredder/GroupMember.pm
+++ b/lib/RT/Shredder/GroupMember.pm
@@ -71,8 +71,8 @@ sub __DependsOn
     my $list = [];
 
     my $objs = RT::CachedGroupMembers->new( $self->CurrentUser );
+    $objs->Limit( FIELD => 'GroupId', VALUE => $self->GroupId );
     $objs->Limit( FIELD => 'MemberId', VALUE => $self->MemberId );
-    $objs->Limit( FIELD => 'ImmediateParentId', VALUE => $self->GroupId );
     push( @$list, $objs );
 
     $deps->_PushDependencies(

commit a34ab0ed4a256ef167de14aa0122a7ac79b0d780
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Apr 9 18:56:21 2012 +0400

    wipeout GMs using API so it handles CGMs

diff --git a/lib/RT/Shredder/GroupMember.pm b/lib/RT/Shredder/GroupMember.pm
index 6b06637..5ebfe6e 100644
--- a/lib/RT/Shredder/GroupMember.pm
+++ b/lib/RT/Shredder/GroupMember.pm
@@ -134,6 +134,14 @@ sub __DependsOn
     return $self->SUPER::__DependsOn( %args );
 }
 
+sub __Wipeout
+{
+    my $self = shift;
+    my $msg = $self->_AsString ." wiped out";
+    $self->Delete;
+    $RT::Logger->info( $msg );
+    return;
+}
 
 #TODO: If we plan write export tool we also should fetch parent groups
 # now we only wipeout things.

commit 81e3392d09be87ed749364087e4685616f82a992
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Apr 9 23:35:22 2012 +0400

    change how we wipeout CGM records
    
    actual delete is performed by API via GM record,
    CGM.pm only deals with dumping.

diff --git a/lib/RT/Shredder/CachedGroupMember.pm b/lib/RT/Shredder/CachedGroupMember.pm
index f5542a2..ea767fe 100644
--- a/lib/RT/Shredder/CachedGroupMember.pm
+++ b/lib/RT/Shredder/CachedGroupMember.pm
@@ -57,47 +57,67 @@ use RT::Shredder::Constants;
 use RT::Shredder::Exceptions;
 use RT::Shredder::Dependency;
 
-
-sub __DependsOn
+sub _AsInsertQuery
 {
     my $self = shift;
-    my %args = (
-            Shredder => undef,
-            Dependencies => undef,
-            @_,
-           );
-    my $deps = $args{'Dependencies'};
-    my $list = [];
-
-# deep memebership
-    my $objs = RT::CachedGroupMembers->new( $self->CurrentUser );
-    $objs->Limit( FIELD => 'Via', VALUE => $self->Id );
-    $objs->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $self->Id );
-    push( @$list, $objs );
+    return $self->SUPER::_AsInsertQuery( @_ )
+        if $self->MemberId == $self->GroupId;
+
+    my $table = $self->Table;
+    my $dbh = $RT::Handle->dbh;
+    my @quoted = ( map $dbh->quote($self->$_()), qw(GroupId MemberId Disabled) );
+
+    my $query =
+        "SELECT ". join( ', ', @quoted ) .' WHERE NOT EXISTS ('
+            ."SELECT id FROM $table WHERE GroupId = $quoted[0] AND MemberId = $quoted[1]"
+        .')'
+    ;
+    my $res = $self->BuildInsertFromSelectQuery( $query ) ."\n";
+
+    $query = "SELECT CGM1.GroupId, CGM2.MemberId, CASE WHEN CGM1.Disabled + CGM2.Disabled > 0 THEN 1 ELSE 0 END FROM
+        $table CGM1 CROSS JOIN $table CGM2
+        LEFT JOIN $table CGM3
+            ON CGM3.GroupId = CGM1.GroupId AND CGM3.MemberId = CGM2.MemberId
+        WHERE
+            CGM1.MemberId = $quoted[0] AND (CGM1.GroupId != CGM1.MemberId OR CGM1.MemberId = $quoted[1])
+            AND CGM3.id IS NULL
+    ";
+
+    if ( $self->MemberObj->IsGroup ) {
+        $query .= "
+            AND CGM2.GroupId = $quoted[1]
+            AND (CGM2.GroupId != CGM2.MemberId OR CGM2.GroupId = $quoted[1])
+        ";
+    }
+    else {
+        $query .= " AND CGM2.GroupId = $quoted[0] AND CGM2.MemberId = $quoted[1]";
+    }
+    $res .= $self->BuildInsertFromSelectQuery( $query ) ."\n";
 
-# principal lost group membership and lost some rights which he could delegate to
-# some body
+    return $res;
+}
 
-# XXX: Here is problem cause HasMemberRecursively would return true allways
-# cause we didn't delete anything yet. :(
-    # if pricipal is not member anymore(could be via other groups) then proceed
-    if( $self->GroupObj->Object->HasMemberRecursively( $self->MemberObj ) ) {
-        my $acl = RT::ACL->new( $self->CurrentUser );
-        $acl->LimitToPrincipal( Id => $self->GroupId );
+sub BuildInsertFromSelectQuery {
+    my $self = shift;
+    my $query = shift;
 
+    my $table = $self->Table;
+    if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
+        $query = "(SELECT ${table}_seq.nextval, insert_from.* FROM ($query) insert_from)";
     }
+    return "INSERT INTO $table(GroupId, MemberId, Disabled) $query;";
+}
 
+sub __Wipeout {
+    my $self = shift;
+    return $self->SUPER::__Wipeout( @_ )
+        if $self->MemberId == $self->GroupId;
 
-    $deps->_PushDependencies(
-            BaseObject => $self,
-            Flags => DEPENDS_ON,
-            TargetObjects => $list,
-            Shredder => $args{'Shredder'}
-        );
-
-    return $self->SUPER::__DependsOn( %args );
+    # GroupMember takes care of wiping other records
+    return 1;
 }
 
+
 #TODO: If we plan write export tool we also should fetch parent groups
 # now we only wipeout things.
 
@@ -140,4 +160,5 @@ sub __Relates
         );
     return $self->SUPER::__Relates( %args );
 }
+
 1;

commit ca557da50dcfbf84e6a1cfc9f48fe694d59c3abd
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Apr 9 23:50:33 2012 +0400

    update shredder's docs

diff --git a/lib/RT/Shredder.pm b/lib/RT/Shredder.pm
index 10d3536..bb45022 100644
--- a/lib/RT/Shredder.pm
+++ b/lib/RT/Shredder.pm
@@ -133,7 +133,12 @@ SQL commands to re-insert your objects into the RT database.
 
     mysql -u your_rt_user -p your_rt_database < /path/to/rt/var/data/shredder/dump.sql
 
-That's it.i This will restore everything you'd deleted during a
+3) Validate database with the following command. This is required as
+   DB may be inconsistent because of changes since shredding, also some
+   restore operations are hard to implement in SQL that will work
+   on any DB runs on.
+
+That's it. This will restore everything you'd deleted during a
 shredding session when the file had been created.
 
 =head1 CONFIGURATION

-----------------------------------------------------------------------


More information about the Rt-commit mailing list