[Rt-commit] rt branch, 4.2/optimize-cgm-table, updated. rt-4.0.0rc7-267-gdf50bf2

Ruslan Zakirov ruz at bestpractical.com
Mon Mar 19 14:04:16 EDT 2012


The branch, 4.2/optimize-cgm-table has been updated
       via  df50bf2b38e2cb90537fd43daa73769c17800426 (commit)
       via  6aea1e351417fe30057a0834fdb645c83d5989a0 (commit)
       via  198bcae55dfeb3f7a00ad5198350734ad16eacc1 (commit)
       via  855c7a9a2096355d50cd2228c502f1baab9ac348 (commit)
       via  f790d52987d7ee200953d9b30ef1a89165087a72 (commit)
       via  957783bc3ba86aa85a08b56a48a02824f1018409 (commit)
       via  9f514e2d7e0c3555cb2ec07ce10171f773c86e3f (commit)
       via  2b8f40d9a9f62a7c2dfdb321042d58d475984b6f (commit)
       via  833332eb04ec8129d7f97352560f8a11f3fadc5e (commit)
       via  205361b41329d842e39ec5bfbca4ff017ceeb06a (commit)
       via  e52fa63cd4ea614d75ccf2f02929a014b539f199 (commit)
       via  57c1c632651dc2ae2ac2923a881703f419eef5d1 (commit)
       via  9f7855d09c7b67324772777f7763bc592aa48acf (commit)
       via  ca37dc0b1252a6eeb7766248c5d996802b11dabf (commit)
      from  8adb70fba60b903c2aa564dce5810cb3d252cefa (commit)

Summary of changes:
 lib/RT/CachedGroupMember.pm |  416 +++++++++++++++++++++++++++++--------------
 lib/RT/Group.pm             |   25 +--
 lib/RT/GroupMember.pm       |    7 +-
 t/api/group.t               |   96 ----------
 t/api/group_members.t       |  266 +++++++++++++++++++++++++++
 5 files changed, 561 insertions(+), 249 deletions(-)
 delete mode 100644 t/api/group.t
 create mode 100644 t/api/group_members.t

- Log -----------------------------------------------------------------
commit ca37dc0b1252a6eeb7766248c5d996802b11dabf
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Mar 14 14:49:33 2012 +0400

    drop most mentions of ImmediateParent and Via

diff --git a/lib/RT/CachedGroupMember.pm b/lib/RT/CachedGroupMember.pm
index 899cf4e..7111872 100644
--- a/lib/RT/CachedGroupMember.pm
+++ b/lib/RT/CachedGroupMember.pm
@@ -82,14 +82,6 @@ Create takes a hash of values and creates a row in the database:
   'Member' is the RT::Principal  of the user or group we're adding to 
   the cache.
 
-  'ImmediateParent' is the RT::Principal of the group that this 
-  principal belongs to to get here
-
-  int(11) 'Via' is an internal reference to CachedGroupMembers->Id of
-  the "parent" record of this cached group member. It should be empty if 
-  this member is a "direct" member of this group. (In that case, it will 
-  be set to this cached group member's id after creation)
-
   This routine should _only_ be called by GroupMember->Create
 
 =cut
@@ -124,7 +116,7 @@ sub Create {
     unless ($id) {
         $RT::Logger->warning(
             "Couldn't create ". $args{'Member'} ." as a cached member of "
-            . $args{'Group'} ." via ". $args{'Via'}
+            . $args{'Group'}
         );
         return (undef);
     }
@@ -185,7 +177,7 @@ Deletes the current CachedGroupMember from the group it's in and cascades
 the delete to all submembers. This routine could be completely excised if
 mysql supported foreign keys with cascading deletes.
 
-=cut 
+=cut
 
 sub Delete {
     my $self = shift;
@@ -242,12 +234,12 @@ SetDisableds the current CachedGroupMember from the group it's in and cascades
 the SetDisabled to all submembers. This routine could be completely excised if
 mysql supported foreign keys with cascading SetDisableds.
 
-=cut 
+=cut
 
 sub SetDisabled {
     my $self = shift;
     my $val = shift;
- 
+
     # if it's already disabled, we're good.
     return (1) if ( $self->__Value('Disabled') == $val);
     my $err = $self->_Set(Field => 'Disabled', Value => $val);
@@ -256,7 +248,7 @@ sub SetDisabled {
         $RT::Logger->error( "Couldn't SetDisabled CachedGroupMember " . $self->Id .": $msg");
         return ($err);
     }
-    
+
     my $member = $self->MemberObj();
     if ( $member->IsGroup ) {
         my $deletable = RT::CachedGroupMembers->new( $self->CurrentUser );
@@ -292,22 +284,7 @@ sub GroupObj {
 
 
 
-=head2 ImmediateParentObj  
-
-Returns the RT::Principal object for this group ImmediateParent
-
-=cut
-
-sub ImmediateParentObj {
-    my $self      = shift;
-    my $principal = RT::Principal->new( $self->CurrentUser );
-    $principal->Load( $self->ImmediateParentId );
-    return ($principal);
-}
-
-
-
-=head2 MemberObj  
+=head2 MemberObj
 
 Returns the RT::Principal object for this group member
 
@@ -371,58 +348,17 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
 
 =cut
 
-
-=head2 Via
-
-Returns the current value of Via.
-(In the database, Via is stored as int(11).)
-
-
-
-=head2 SetVia VALUE
-
-
-Set Via to VALUE.
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, Via will be stored as a int(11).)
-
-
-=cut
-
-
-=head2 ImmediateParentId
-
-Returns the current value of ImmediateParentId.
-(In the database, ImmediateParentId is stored as int(11).)
-
-
-
-=head2 SetImmediateParentId VALUE
-
-
-Set ImmediateParentId to VALUE.
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, ImmediateParentId will be stored as a int(11).)
-
-
-=cut
-
-
 =head2 Disabled
 
 Returns the current value of Disabled.
 (In the database, Disabled is stored as smallint(6).)
 
-
-
 =head2 SetDisabled VALUE
 
-
 Set Disabled to VALUE.
 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
 (In the database, Disabled will be stored as a smallint(6).)
 
-
 =cut
 
 
@@ -680,21 +616,15 @@ IntermidiateParent. Review indexes on all databases. Create upgrade script.
 
 sub _CoreAccessible {
     {
-
         id =>
 		{read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
         GroupId =>
 		{read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
         MemberId =>
 		{read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
-        Via =>
-		{read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
-        ImmediateParentId =>
-		{read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
         Disabled =>
 		{read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
-
- }
+    }
 };
 
 RT::Base->_ImportOverlays();
diff --git a/lib/RT/Group.pm b/lib/RT/Group.pm
index 1ee0591..879c818 100644
--- a/lib/RT/Group.pm
+++ b/lib/RT/Group.pm
@@ -465,7 +465,7 @@ sub _Create {
     # in the ordinary case, this would fail badly because it would recurse and add all the members of this group as 
     # cached members. thankfully, we're creating the group now...so it has no members.
     my $cgm = RT::CachedGroupMember->new($self->CurrentUser);
-    $cgm->Create(Group =>$self->PrincipalObj, Member => $self->PrincipalObj, ImmediateParent => $self->PrincipalObj);
+    $cgm->Create( Group => $self->PrincipalObj, Member => $self->PrincipalObj );
 
 
     if ( $args{'_RecordTransaction'} ) {
@@ -666,9 +666,14 @@ This routine finds all the cached group members that are members of this group
     # a member of A, will delete C as a member of A without touching
     # C as a member of B
 
-    my $cached_submembers = RT::CachedGroupMembers->new( $self->CurrentUser );
-
-    $cached_submembers->Limit( FIELD    => 'ImmediateParentId', OPERATOR => '=', VALUE    => $self->Id);
+    my $cgm = RT::CachedGroupMember->new( $self->CurrentUser );
+    $cgm->LoadByCols( MemberId => $self->id, GroupId => $self->id );
+    my ($status) = $item->SetDisabled($val);
+    unless ( $status ) {
+        $RT::Handle->Rollback;
+        $RT::Logger->warning("Couldn't disable cached group member #". $cgm->Id);
+        return (undef);
+    }
 
     #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. 
     # TODO what about the groups key cache?
@@ -676,15 +681,6 @@ This routine finds all the cached group members that are members of this group
 
 
 
-    while ( my $item = $cached_submembers->Next() ) {
-        my $del_err = $item->SetDisabled($val);
-        unless ($del_err) {
-            $RT::Handle->Rollback();
-            $RT::Logger->warning("Couldn't disable cached group submember ".$item->Id);
-            return (undef);
-        }
-    }
-
     $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" );
 
     $RT::Handle->Commit();
@@ -693,7 +689,6 @@ This routine finds all the cached group members that are members of this group
     } else {
         return (1, $self->loc("Group enabled"));
     }
-
 }
 
 
diff --git a/lib/RT/GroupMember.pm b/lib/RT/GroupMember.pm
index eb7736e..93e9725 100644
--- a/lib/RT/GroupMember.pm
+++ b/lib/RT/GroupMember.pm
@@ -225,8 +225,6 @@ sub _StashUser {
     my $cached_id     = $cached_member->Create(
         Member          => $args{'Member'},
         Group           => $args{'Group'},
-        ImmediateParent => $args{'Group'},
-        Via             => '0'
     );
 
     unless ($cached_id) {

commit 9f7855d09c7b67324772777f7763bc592aa48acf
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Mar 15 22:54:24 2012 +0400

    CGM is disabled when group is disabled
    
    Old code does this and let's keep it this way for now

diff --git a/lib/RT/CachedGroupMember.pm b/lib/RT/CachedGroupMember.pm
index 7111872..6d06dd0 100644
--- a/lib/RT/CachedGroupMember.pm
+++ b/lib/RT/CachedGroupMember.pm
@@ -106,7 +106,7 @@ sub Create {
         $RT::Logger->debug("$self->Create: bogus Group argument");
     }
 
-    $args{'Disabled'} = ($args{'Group'}->Disabled || $args{'Member'}->Disabled)? 1 : 0;
+    $args{'Disabled'} = $args{'Group'}->Disabled? 1 : 0;
 
     my $id = $self->SUPER::Create(
         GroupId           => $args{'Group'}->Id,

commit 57c1c632651dc2ae2ac2923a881703f419eef5d1
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Mar 15 22:56:20 2012 +0400

    deal with CGM->Create when record already exists
    
    This happens when GM record is added and we already
    have alternative path.
    
    Disabled column needs special treatment. We only do
    something if old record is disabled and new one is not. In
    this case we activate existing record. Otherwise we bail
    as there is nothing to do.

diff --git a/lib/RT/CachedGroupMember.pm b/lib/RT/CachedGroupMember.pm
index 6d06dd0..b98b26f 100644
--- a/lib/RT/CachedGroupMember.pm
+++ b/lib/RT/CachedGroupMember.pm
@@ -108,7 +108,21 @@ sub Create {
 
     $args{'Disabled'} = $args{'Group'}->Disabled? 1 : 0;
 
-    my $id = $self->SUPER::Create(
+    $self->LoadByCols(
+        GroupId           => $args{'Group'}->Id,
+        MemberId          => $args{'Member'}->Id,
+    );
+
+    my $id;
+    if ( $id = $self->id ) {
+        if ( $self->Disabled != $args{'Disabled'} && $args{'Disabled'} == 0 ) {
+            my ($status) = $self->SetDisabled( 0 );
+            return undef unless $status;
+        }
+        return $id;
+    }
+
+    ($id) = $self->SUPER::Create(
         GroupId           => $args{'Group'}->Id,
         MemberId          => $args{'Member'}->Id,
         Disabled          => $args{'Disabled'},

commit e52fa63cd4ea614d75ccf2f02929a014b539f199
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Mar 15 23:02:27 2012 +0400

    new leaf nodes can not affect activity of other records

diff --git a/lib/RT/CachedGroupMember.pm b/lib/RT/CachedGroupMember.pm
index b98b26f..caf1a0c 100644
--- a/lib/RT/CachedGroupMember.pm
+++ b/lib/RT/CachedGroupMember.pm
@@ -137,7 +137,7 @@ sub Create {
     return $id if $args{'Member'}->id == $args{'Group'}->id;
 
     my $table = $self->Table;
-    unless ( $args{'Disabled'} ) {
+    if ( !$args{'Disabled'} && $args{'Member'}->IsGroup ) {
         # update existing records, in case we activated some paths
         my $query = "
             SELECT CGM3.id FROM

commit 205361b41329d842e39ec5bfbca4ff017ceeb06a
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Mar 15 23:04:13 2012 +0400

    deal with users and groups differently
    
    Users have no (U, U) records like groups, but at the same
    time they can not have descandants. Query becomes special
    because of the absent record, but less complex as it's a
    leaf.

diff --git a/lib/RT/CachedGroupMember.pm b/lib/RT/CachedGroupMember.pm
index caf1a0c..695aa6e 100644
--- a/lib/RT/CachedGroupMember.pm
+++ b/lib/RT/CachedGroupMember.pm
@@ -172,14 +172,23 @@ sub Create {
             ON CGM3.GroupId = CGM1.GroupId AND CGM3.MemberId = CGM2.MemberId
         WHERE
             CGM1.MemberId = ? AND (CGM1.GroupId != CGM1.MemberId OR CGM1.MemberId = ?)
-            AND CGM2.GroupId = ? AND (CGM2.GroupId != CGM2.MemberId OR CGM2.GroupId = ?)
             AND CGM3.id IS NULL
     ";
+    push @binds, $args{'Group'}->id, $args{'Group'}->id;
+
+    if ( $args{'Member'}->IsGroup ) {
+        $query .= "
+            AND CGM2.GroupId = ?
+            AND (CGM2.GroupId != CGM2.MemberId OR CGM2.GroupId = ?)
+        ";
+        push @binds, $args{'Member'}->id, $args{'Member'}->id;
+    }
+    else {
+        $query .= " AND CGM2.id = ?";
+        push @binds, $id;
+    }
     $RT::Handle->InsertFromSelect(
-        $table, ['GroupId', 'MemberId', 'Disabled'], $query,
-        @binds,
-        $args{'Group'}->id, $args{'Group'}->id,
-        $args{'Member'}->id, $args{'Member'}->id
+        $table, ['GroupId', 'MemberId', 'Disabled'], $query, @binds,
     );
 
     return $id;

commit 833332eb04ec8129d7f97352560f8a11f3fadc5e
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Mar 15 23:09:10 2012 +0400

    fix re-inserting records after delete
    
    it was just wrong

diff --git a/lib/RT/CachedGroupMember.pm b/lib/RT/CachedGroupMember.pm
index 695aa6e..78dcd5e 100644
--- a/lib/RT/CachedGroupMember.pm
+++ b/lib/RT/CachedGroupMember.pm
@@ -232,19 +232,24 @@ sub Delete {
     );
     return $res unless $res;
 
-    $query = "SELECT CGM1.GroupId, CGM2.MemberId FROM
-        $table CGM1 CROSS JOIN $table CGM2
-        LEFT JOIN $table CGM3
-            ON CGM3.GroupId = CGM1.GroupId AND CGM3.MemberId = CGM2.MemberId
+    $query =
+        "SELECT CGM1.GroupId, CGM2.MemberId,
+            CASE WHEN CGM3.Disabled + CGM4.Disabled > 0 THEN 1 ELSE 0 END
+        FROM $table CGM1 CROSS JOIN $table CGM2
+        JOIN $table CGM3 ON CGM3.GroupId != CGM3.MemberId AND CGM3.GroupId = CGM1.GroupId
+        JOIN $table CGM4 ON CGM4.GroupId != CGM4.MemberId AND CGM4.MemberId = CGM2.MemberId
+            AND CGM3.MemberId = CGM4.GroupId
+        LEFT JOIN $table CGM5
+            ON CGM5.GroupId = CGM1.GroupId AND CGM5.MemberId = CGM2.MemberId
         WHERE
-            CGM1.MemberId = ? AND (CGM1.GroupId != CGM1.MemberId OR CGM1.MemberId = ?)
-            AND CGM2.GroupId = ? AND (CGM2.GroupId != CGM2.MemberId OR CGM2.GroupId = ?)
-            AND CGM3.id IS NULL
+            CGM1.MemberId = ?
+            AND CGM2.GroupId = ?
+            AND CGM5.id IS NULL
     ";
     $res = $RT::Handle->InsertFromSelect(
-        $table, ['GroupId', 'MemberId'], $query,
-        $self->GroupId, $self->GroupId,
-        $self->MemberId, $self->MemberId,
+        $table, ['GroupId', 'MemberId', 'Disabled'], $query,
+        $self->GroupId,
+        $self->MemberId, 
     );
     return $res unless $res;
 

commit 2b8f40d9a9f62a7c2dfdb321042d58d475984b6f
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Mar 15 23:10:25 2012 +0400

    flush cache when method uses only mass updates

diff --git a/lib/RT/CachedGroupMember.pm b/lib/RT/CachedGroupMember.pm
index 78dcd5e..3c85d70 100644
--- a/lib/RT/CachedGroupMember.pm
+++ b/lib/RT/CachedGroupMember.pm
@@ -253,6 +253,8 @@ sub Delete {
     );
     return $res unless $res;
 
+    if ( my $m = $self->can('_FlushKeyCache') ) { $m->($self) };
+
     return 1;
 }
 

commit 9f514e2d7e0c3555cb2ec07ce10171f773c86e3f
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Mar 15 23:11:48 2012 +0400

    implement SetDisabled

diff --git a/lib/RT/CachedGroupMember.pm b/lib/RT/CachedGroupMember.pm
index 3c85d70..d0cb480 100644
--- a/lib/RT/CachedGroupMember.pm
+++ b/lib/RT/CachedGroupMember.pm
@@ -269,32 +269,73 @@ mysql supported foreign keys with cascading SetDisableds.
 sub SetDisabled {
     my $self = shift;
     my $val = shift;
+    $val = $val ? 1 : 0;
 
     # if it's already disabled, we're good.
-    return (1) if ( $self->__Value('Disabled') == $val);
-    my $err = $self->_Set(Field => 'Disabled', Value => $val);
-    my ($retval, $msg) = $err->as_array();
-    unless ($retval) {
-        $RT::Logger->error( "Couldn't SetDisabled CachedGroupMember " . $self->Id .": $msg");
-        return ($err);
-    }
+    return (1) if $self->__Value('Disabled') == $val;
+
+    if ( $val ) {
+        unless ( $self->GroupId == $self->MemberId ) {
+            $RT::Logger->error("SetDisabled should only be applied to (G->G) records");
+            return undef;
+        }
+
+        my $query = "SELECT main.id FROM CachedGroupMembers main
+            JOIN CachedGroupMembers CGM1 ON main.GroupId = CGM1.GroupId
+                AND CGM1.MemberId = ?
+                AND CGM1.Disabled = 0
+            JOIN CachedGroupMembers CGM2 ON main.MemberId = CGM2.MemberId
+                AND CGM2.GroupId = ?
+            WHERE main.Disabled = 0";
+
+        $RT::Handle->SimpleUpdateFromSelect(
+            $self->Table, { Disabled => 1 }, $query,
+            ($self->GroupId)x2,
+        ) or return undef;
+
+        $query = "SELECT main.id FROM CachedGroupMembers main
+            JOIN CachedGroupMembers CGM1 ON main.GroupId = CGM1.GroupId
+                AND CGM1.MemberId = ?
+                AND CGM1.Disabled = 0
+            JOIN CachedGroupMembers CGM2 ON main.MemberId = CGM2.MemberId
+                AND CGM2.GroupId = ?
 
-    my $member = $self->MemberObj();
-    if ( $member->IsGroup ) {
-        my $deletable = RT::CachedGroupMembers->new( $self->CurrentUser );
+            JOIN CachedGroupMembers CGM3 ON CGM3.Disabled = 0
+                AND main.GroupId = CGM3.GroupID
+            JOIN CachedGroupMembers CGM4 ON CGM4.Disabled = 0
+                AND main.MemberId = CGM4.MemberId
+                AND CGM4.GroupId = CGM3.MemberId
 
-        $deletable->Limit( FIELD    => 'Via', OPERATOR => '=', VALUE    => $self->id );
-        $deletable->Limit( FIELD    => 'id', OPERATOR => '!=', VALUE    => $self->id );
+            WHERE main.Disabled = 1";
 
-        while ( my $kid = $deletable->Next ) {
-            my $kid_err = $kid->SetDisabled($val );
-            unless ($kid_err) {
-                $RT::Logger->error( "Couldn't SetDisabled CachedGroupMember " . $kid->Id );
-                return ($kid_err);
-            }
+        $RT::Handle->SimpleUpdateFromSelect(
+            $self->Table, { Disabled => 0 }, $query,
+            ($self->GroupId)x2,
+        ) or return undef;
+    }
+    else {
+        my ($status, $msg) = $self->_Set(Field => 'Disabled', Value => $val);
+        unless ( $status ) {
+            $RT::Logger->error(
+                "Couldn't SetDisabled CachedGroupMember #" . $self->Id .": $msg"
+            );
+            return $status;
         }
+        my $query = "SELECT main.id FROM CachedGroupMembers main
+            JOIN CachedGroupMembers CGM1 ON main.GroupId = CGM1.GroupId
+                AND CGM1.MemberId = ?
+                AND CGM1.Disabled = 0
+            JOIN CachedGroupMembers CGM2 ON main.MemberId = CGM2.MemberId
+                AND CGM2.GroupId = ?
+            WHERE main.Disabled = 1";
+
+        $RT::Handle->SimpleUpdateFromSelect(
+            $self->Table, { Disabled => 0 }, $query,
+            $self->GroupId, $self->MemberId
+        ) or return undef;
     }
-    return ($err);
+    if ( my $m = $self->can('_FlushKeyCache') ) { $m->($self) };
+    return (1);
 }
 
 

commit 957783bc3ba86aa85a08b56a48a02824f1018409
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Mar 15 23:13:20 2012 +0400

    update documentation for developers

diff --git a/lib/RT/CachedGroupMember.pm b/lib/RT/CachedGroupMember.pm
index d0cb480..90af67c 100644
--- a/lib/RT/CachedGroupMember.pm
+++ b/lib/RT/CachedGroupMember.pm
@@ -435,6 +435,22 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
 
 =head1 FOR DEVELOPERS
 
+=head2 New structure without Via and ImmediateParent
+
+We have id, GroupId, MemberId, Disabled. In this schema
+we have unique index on GroupId and MemberId that will
+improve selects.
+
+Disabled column is complex as it's reflects all possible
+paths between group and member. If at least one active path
+exists then the record is active.
+
+When a GM record is added we do only two queries: insert
+new CGM records and update Disabled on old paths.
+
+When a GM record is deleted we do it in two steps: delete
+all potential candidates and re-insert them.
+
 =head2 SQL behind maintaining CGM table
 
 =head3 Terminology
@@ -593,7 +609,7 @@ Fun.
 Sadly this query perform much worth comparing to the insert operation. Problem is
 in the select.
 
-=head3 Delete all candidates and re-insert missing
+=head3 Delete all candidates and re-insert missing (our method)
 
 We can delete all candidates (An(G)->De(M)) from CGM table that are not
 real GM records: then insert records once again.
@@ -612,10 +628,16 @@ real GM records: then insert records once again.
 
 Then we can re-insert data back with insert from select described above.
 
+=head4 Disabled column
+
+We delete all (An(G)->De(M)) and then re-insert survivors, so no other
+records except inserted can gain or loose activity. See how we deal with
+it during insert.
+
 =head4 mysql performance
 
 This solution is faster than perviouse variant, 4-5 times slower than
-create operation and behaves linear.
+create operation, behaves linear.
 
 =head3 Recursive delete
 
@@ -678,10 +700,69 @@ do iterative delete like in the last solution. However, this will slowdown
 insert, probably not that much as I suspect we would be able to push new data
 in one query.
 
+=head2 Disabling a (G->G) record
+
+We're interested only in (G->G) records as CGM path is disabled if group
+is disabled. Disabled users don't affect CGM records.
+
+When (G->G) gets Disabled, 1) (G->De(G)) gets Disabled 2) all active
+(An(G)->De(G)) get disabled unless record has an alternative active path.
+
+First can be done without much problem:
+
+    UPDATE CGM SET Disabled => 1 WHERE GroupId = G;
+
+Second part is harder. Finding alternative path is harder and similar to performing
+delete in one query. Instead we can disable all candidates and then re-enable
+required.
+
+Selecting candidates is simple:
+
+    SELECT main.id FROM CachedGroupMembers main
+        JOIN CachedGroupMembers CGM1 ON main.GroupId = CGM1.GroupId AND CGM1.MemberId = G
+        JOIN CachedGroupMembers CGM2 ON main.MemberId = CGM2.MemberId AND CGM2.GroupId = G
+    WHERE main.Disabled = 0;
+
+We can narrow it down. If (G'->G) is disabled where G'~An(G) then activity
+of (G'->M') where M'~De(G) isn't affected by activity of (G->G):
+
+    SELECT main.id FROM CachedGroupMembers main
+        JOIN CachedGroupMembers CGM1 ON main.GroupId = CGM1.GroupId AND CGM1.MemberId = G
+            AND CGM1.Disabled = 0
+        JOIN CachedGroupMembers CGM2 ON main.MemberId = CGM2.MemberId AND CGM2.GroupId = G
+    WHERE main.Disabled = 0;
+
+Now we can re-enable records which still have active paths:
+
+    SELECT main.id FROM CachedGroupMembers main
+        JOIN CachedGroupMembers CGM1 ON main.GroupId = CGM1.GroupId AND CGM1.MemberId = G
+            AND CGM1.Disabled = 0
+        JOIN CachedGroupMembers CGM2 ON main.MemberId = CGM2.MemberId AND CGM2.GroupId = G
+
+        JOIN CachedGroupMembers CGM3 ON CGM3.Disabled = 0 AND main.GroupId = CGM3.GroupID
+        JOIN CachedGroupMembers CGM4 ON CGM4.Disabled = 0 AND main.MemberId = CGM4.MemberId
+            AND CGM4.GroupId = CGM3.MemberId
+
+    WHERE main.Disabled = 1;
+
+Enabling records is much easier, just update all candidates.
+
 =head2 TODO
 
-Update disabled on delete. Update SetDisabled method. Delete all uses of Via and
-IntermidiateParent. Review indexes on all databases. Create upgrade script.
+Update rt-validator and shredder. Review indexes on all databases.
+Create upgrade script.
+
+=head2 What's next
+
+We don't create self-referencing records for users and it complicates
+a few code paths in this module. However, we have ACL equiv groups for
+every user and these groups have (G->G) records and (G->U) record. So
+we have one additional group per user and two CGM records.
+
+We can give user's id to ACL equiv group, so G.id = U.id. In this case
+we get (G, G) pair that is at the same time (U->U) and (G->U) pairs.
+It simplifies code in this module and CGM table smaller by one record
+per user.
 
 =cut
 

commit f790d52987d7ee200953d9b30ef1a89165087a72
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Mar 15 23:14:25 2012 +0400

    typo

diff --git a/lib/RT/Group.pm b/lib/RT/Group.pm
index 879c818..68fbad7 100644
--- a/lib/RT/Group.pm
+++ b/lib/RT/Group.pm
@@ -668,7 +668,7 @@ This routine finds all the cached group members that are members of this group
 
     my $cgm = RT::CachedGroupMember->new( $self->CurrentUser );
     $cgm->LoadByCols( MemberId => $self->id, GroupId => $self->id );
-    my ($status) = $item->SetDisabled($val);
+    my ($status) = $cgm->SetDisabled($val);
     unless ( $status ) {
         $RT::Handle->Rollback;
         $RT::Logger->warning("Couldn't disable cached group member #". $cgm->Id);

commit 855c7a9a2096355d50cd2228c502f1baab9ac348
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Mar 15 23:15:02 2012 +0400

    return id on success from Group->_AddMember

diff --git a/lib/RT/Group.pm b/lib/RT/Group.pm
index 68fbad7..78c0e0e 100644
--- a/lib/RT/Group.pm
+++ b/lib/RT/Group.pm
@@ -934,7 +934,7 @@ sub _AddMember {
         InsideTransaction => $args{'InsideTransaction'}
     );
     if ($id) {
-        return ( 1, $self->loc("Member added: [_1]", $new_member_obj->Object->Name) );
+        return ( $id, $self->loc("Member added: [_1]", $new_member_obj->Object->Name) );
     }
     else {
         return(0, $self->loc("Couldn't add member to group"));

commit 198bcae55dfeb3f7a00ad5198350734ad16eacc1
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Mar 15 23:16:53 2012 +0400

    call Create in array context
    
    to make sure we always get first element and not last
    which is usually error message.
    
    yes, now some Create methods return scalar, but most of
    them return list and tomorrow we might want to changes
    that minority as well. Don't want to hunt for bugs after
    such change.

diff --git a/lib/RT/GroupMember.pm b/lib/RT/GroupMember.pm
index 93e9725..48a45cc 100644
--- a/lib/RT/GroupMember.pm
+++ b/lib/RT/GroupMember.pm
@@ -153,18 +153,17 @@ sub Create {
     }
 
 
-    my $id = $self->SUPER::Create(
+    my ($id) = $self->SUPER::Create(
         GroupId  => $gid,
         MemberId => $mid
     );
-
     unless ($id) {
         $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
         return (undef);
     }
 
     my $cached_member = RT::CachedGroupMember->new( $self->CurrentUser );
-    my $cached_id     = $cached_member->Create(
+    my ($cached_id)     = $cached_member->Create(
         Group           => $args{'Group'},
         Member          => $args{'Member'},
     );

commit 6aea1e351417fe30057a0834fdb645c83d5989a0
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Mar 16 21:48:00 2012 +0400

    fix deletes
    
    separate groups and users
    
    insert distinct set of records, new records we insert may
    have alternative paths, so we have to insert something
    consistent in Disabled column and update it later

diff --git a/lib/RT/CachedGroupMember.pm b/lib/RT/CachedGroupMember.pm
index 90af67c..b27cddb 100644
--- a/lib/RT/CachedGroupMember.pm
+++ b/lib/RT/CachedGroupMember.pm
@@ -213,18 +213,37 @@ sub Delete {
     }
 
     my $table = $self->Table;
-    my $query = "
-        SELECT CGM1.id FROM
-            CachedGroupMembers CGM1
-            JOIN CachedGroupMembers CGMA ON CGMA.MemberId = ?
-            JOIN CachedGroupMembers CGMD ON CGMD.GroupId = ?
-            LEFT JOIN GroupMembers GM1
-                ON GM1.GroupId = CGM1.GroupId AND GM1.MemberId = CGM1.MemberId
-        WHERE
-            CGM1.GroupId = CGMA.GroupId AND CGM1.MemberId = CGMD.MemberId
-            AND CGM1.GroupId != CGM1.MemberId
-            AND GM1.id IS NULL 
-    ";
+
+    my $member_is_group = $self->MemberObj->IsGroup;
+
+    my $query;
+    if ( $member_is_group ) {
+        $query = "
+            SELECT CGM1.id FROM
+                CachedGroupMembers CGM1
+                JOIN CachedGroupMembers CGMA ON CGMA.MemberId = ?
+                JOIN CachedGroupMembers CGMD ON CGMD.GroupId = ?
+                LEFT JOIN GroupMembers GM1
+                    ON GM1.GroupId = CGM1.GroupId AND GM1.MemberId = CGM1.MemberId
+            WHERE
+                CGM1.GroupId = CGMA.GroupId AND CGM1.MemberId = CGMD.MemberId
+                AND CGM1.GroupId != CGM1.MemberId
+                AND GM1.id IS NULL
+        ";
+    }
+    else {
+        $query = "
+            SELECT CGM1.id FROM
+                CachedGroupMembers CGM1
+                JOIN CachedGroupMembers CGMA ON CGMA.MemberId = ?
+                LEFT JOIN GroupMembers GM1
+                    ON GM1.GroupId = CGM1.GroupId AND GM1.MemberId = CGM1.MemberId
+            WHERE
+                CGM1.GroupId = CGMA.GroupId
+                AND CGM1.MemberId = ?
+                AND GM1.id IS NULL
+        ";
+    }
 
     my $res = $RT::Handle->DeleteFromSelect(
         $table, $query,
@@ -232,27 +251,76 @@ sub Delete {
     );
     return $res unless $res;
 
-    $query =
-        "SELECT CGM1.GroupId, CGM2.MemberId,
-            CASE WHEN CGM3.Disabled + CGM4.Disabled > 0 THEN 1 ELSE 0 END
-        FROM $table CGM1 CROSS JOIN $table CGM2
-        JOIN $table CGM3 ON CGM3.GroupId != CGM3.MemberId AND CGM3.GroupId = CGM1.GroupId
-        JOIN $table CGM4 ON CGM4.GroupId != CGM4.MemberId AND CGM4.MemberId = CGM2.MemberId
-            AND CGM3.MemberId = CGM4.GroupId
-        LEFT JOIN $table CGM5
-            ON CGM5.GroupId = CGM1.GroupId AND CGM5.MemberId = CGM2.MemberId
-        WHERE
-            CGM1.MemberId = ?
-            AND CGM2.GroupId = ?
-            AND CGM5.id IS NULL
-    ";
+    my @binds;
+    if ( $member_is_group ) {
+        $query =
+            "SELECT DISTINCT CGM1.GroupId, CGM2.MemberId, 1
+            FROM $table CGM1 CROSS JOIN $table CGM2
+            JOIN $table CGM3 ON CGM3.GroupId != CGM3.MemberId AND CGM3.GroupId = CGM1.GroupId
+            JOIN $table CGM4 ON CGM4.GroupId != CGM4.MemberId AND CGM4.MemberId = CGM2.MemberId
+                AND CGM3.MemberId = CGM4.GroupId
+            LEFT JOIN $table CGM5
+                ON CGM5.GroupId = CGM1.GroupId AND CGM5.MemberId = CGM2.MemberId
+            WHERE
+                CGM1.MemberId = ?
+                AND CGM2.GroupId = ?
+                AND CGM5.id IS NULL
+        ";
+        @binds = ($self->GroupId, $self->MemberId);
+
+    } else {
+        $query =
+            "SELECT DISTINCT CGM1.GroupId, ?, 1
+            FROM $table CGM1
+            JOIN $table CGM3 ON CGM3.GroupId != CGM3.MemberId AND CGM3.GroupId = CGM1.GroupId
+            JOIN $table CGM4 ON CGM4.GroupId != CGM4.MemberId AND CGM4.MemberId = ?
+                AND CGM3.MemberId = CGM4.GroupId
+            LEFT JOIN $table CGM5
+                ON CGM5.GroupId = CGM1.GroupId AND CGM5.MemberId = ?
+            WHERE
+                CGM1.MemberId = ?
+                AND CGM5.id IS NULL
+        ";
+        @binds = (
+            ($self->MemberId)x3,
+            $self->GroupId,
+        );
+    }
+
     $res = $RT::Handle->InsertFromSelect(
-        $table, ['GroupId', 'MemberId', 'Disabled'], $query,
-        $self->GroupId,
-        $self->MemberId, 
+        $table, ['GroupId', 'MemberId', 'Disabled'], $query, @binds
     );
     return $res unless $res;
 
+    if ( $res > 1 && $member_is_group ) {
+        $query =
+            "SELECT main.id
+            FROM $table main
+            JOIN $table CGMA ON CGMA.MemberId = ?
+            JOIN $table CGMD ON CGMD.GroupId = ?
+
+            JOIN $table CGM3 ON CGM3.GroupId != CGM3.MemberId
+                AND CGM3.GroupId = main.GroupId
+                AND CGM3.Disabled = 0
+            JOIN $table CGM4 ON CGM4.GroupId != CGM4.MemberId
+                AND CGM4.MemberId = main.MemberId
+                AND CGM4.Disabled = 0
+                AND CGM3.MemberId = CGM4.GroupId
+            WHERE
+                main.GroupId = CGMA.GroupId
+                AND main.MemberId = CGMD.MemberId
+                AND main.Disabled = 1
+        ";
+        $RT::Handle->SimpleUpdateFromSelect(
+            $table, { Disabled => 0 }, $query,
+            $self->GroupId,
+            $self->MemberId,
+        );
+        return $res unless $res;
+    }
+    elsif ( $res > 1 ) {
+        
+    }
     if ( my $m = $self->can('_FlushKeyCache') ) { $m->($self) };
 
     return 1;

commit df50bf2b38e2cb90537fd43daa73769c17800426
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Mar 17 00:14:37 2012 +0400

    replace group.t with group_members.t
    
    former was all about checking membership, new file
    checks much more and even does random rounds

diff --git a/t/api/group.t b/t/api/group.t
deleted file mode 100644
index 3ce3da9..0000000
--- a/t/api/group.t
+++ /dev/null
@@ -1,96 +0,0 @@
-
-use strict;
-use warnings;
-use RT;
-use RT::Test nodata => 1, tests => 38;
-
-
-{
-
-ok (require RT::Group);
-
-ok (my $group = RT::Group->new(RT->SystemUser), "instantiated a group object");
-ok (my ($id, $msg) = $group->CreateUserDefinedGroup( Name => 'TestGroup', Description => 'A test group',
-                    ), 'Created a new group');
-isnt ($id , 0, "Group id is $id");
-is ($group->Name , 'TestGroup', "The group's name is 'TestGroup'");
-my $ng = RT::Group->new(RT->SystemUser);
-
-ok($ng->LoadUserDefinedGroup('TestGroup'), "Loaded testgroup");
-is($ng->id , $group->id, "Loaded the right group");
-
-
-ok (($id,$msg) = $ng->AddMember('1'), "Added a member to the group");
-ok($id, $msg);
-ok (($id,$msg) = $ng->AddMember('2' ), "Added a member to the group");
-ok($id, $msg);
-ok (($id,$msg) = $ng->AddMember('3' ), "Added a member to the group");
-ok($id, $msg);
-
-# Group 1 now has members 1, 2 ,3
-
-my $group_2 = RT::Group->new(RT->SystemUser);
-ok (my ($id_2, $msg_2) = $group_2->CreateUserDefinedGroup( Name => 'TestGroup2', Description => 'A second test group'), , 'Created a new group');
-isnt ($id_2 , 0, "Created group 2 ok- $msg_2 ");
-ok (($id,$msg) = $group_2->AddMember($ng->PrincipalId), "Made TestGroup a member of testgroup2");
-ok($id, $msg);
-ok (($id,$msg) = $group_2->AddMember('1' ), "Added  member RT_System to the group TestGroup2");
-ok($id, $msg);
-
-# Group 2 how has 1, g1->{1, 2,3}
-
-my $group_3 = RT::Group->new(RT->SystemUser);
-ok (my ($id_3, $msg_3) = $group_3->CreateUserDefinedGroup( Name => 'TestGroup3', Description => 'A second test group'), 'Created a new group');
-isnt ($id_3 , 0, "Created group 3 ok - $msg_3");
-ok (($id,$msg) =$group_3->AddMember($group_2->PrincipalId), "Made TestGroup a member of testgroup2");
-ok($id, $msg);
-
-# g3 now has g2->{1, g1->{1,2,3}}
-
-my $principal_1 = RT::Principal->new(RT->SystemUser);
-$principal_1->Load('1');
-
-my $principal_2 = RT::Principal->new(RT->SystemUser);
-$principal_2->Load('2');
-
-ok (($id,$msg) = $group_3->AddMember('1' ), "Added  member RT_System to the group TestGroup2");
-ok($id, $msg);
-
-# g3 now has 1, g2->{1, g1->{1,2,3}}
-
-is($group_3->HasMember($principal_2), undef, "group 3 doesn't have member 2");
-ok($group_3->HasMemberRecursively($principal_2), "group 3 has member 2 recursively");
-ok($ng->HasMember($principal_2) , "group ".$ng->Id." has member 2");
-my ($delid , $delmsg) =$ng->DeleteMember($principal_2->Id);
-isnt ($delid ,0, "Sucessfully deleted it-".$delid."-".$delmsg);
-
-#Gotta reload the group objects, since we've been messing with various internals.
-# we shouldn't need to do this.
-#$ng->LoadUserDefinedGroup('TestGroup');
-#$group_2->LoadUserDefinedGroup('TestGroup2');
-#$group_3->LoadUserDefinedGroup('TestGroup');
-
-# G1 now has 1, 3
-# Group 2 how has 1, g1->{1, 3}
-# g3 now has  1, g2->{1, g1->{1, 3}}
-
-ok(!$ng->HasMember($principal_2)  , "group ".$ng->Id." no longer has member 2");
-is($group_3->HasMemberRecursively($principal_2), undef, "group 3 doesn't have member 2");
-is($group_2->HasMemberRecursively($principal_2), undef, "group 2 doesn't have member 2");
-is($ng->HasMember($principal_2), undef, "group 1 doesn't have member 2");
-is($group_3->HasMemberRecursively($principal_2), undef, "group 3 has member 2 recursively");
-
-
-
-}
-
-{
-
-ok(my $u = RT::Group->new(RT->SystemUser));
-ok($u->Load(4), "Loaded the first user");
-is($u->PrincipalObj->ObjectId , 4, "user 4 is the fourth principal");
-is($u->PrincipalObj->PrincipalType , 'Group' , "Principal 4 is a group");
-
-
-}
-
diff --git a/t/api/group_members.t b/t/api/group_members.t
new file mode 100644
index 0000000..268b864
--- /dev/null
+++ b/t/api/group_members.t
@@ -0,0 +1,266 @@
+use strict;
+use warnings;
+
+use RT::Test nodata => 1, tests => 38;
+
+my %GROUP;
+foreach my $name (qw(A B C D)) {
+    my $group = $GROUP{$name} = RT::Group->new( RT->SystemUser );
+    my ($status, $msg) = $group->CreateUserDefinedGroup( Name => $name );
+    ok $status, "created a group '$name'" or diag "error: $msg";
+}
+
+my %USER;
+foreach my $name (qw(a b c d)) {
+    my $user = $USER{$name} = RT::User->new( RT->SystemUser );
+    my ($status, $msg) = $user->Create( Name => $name );
+    ok $status, "created an user '$name'" or diag "error: $msg";
+}
+
+{
+    add_members_ok( A => qw(a b c) );
+    check_membership( A => [qw(a b c)] );
+
+    add_members_ok( B => qw(A) );
+    add_members_ok( C => qw(B) );
+    check_membership( A => [qw(a b c)], B => [qw(A)], C => [qw(B)] );
+
+    del_members_ok( A => 'b' );
+    check_membership( A => [qw(a c)], B => [qw(A)], C => [qw(B)] );
+
+    add_members_ok( A => qw(b) );
+    add_members_ok( B => qw(b) );
+    check_membership( A => [qw(a b c)], B => [qw(A b)], C => [qw(B)] );
+
+    del_members_ok( A => 'b' );
+    check_membership( A => [qw(a c)], B => [qw(A b)], C => [qw(B)] );
+
+    random_delete( A => [qw(a c)], B => [qw(A b)], C => [qw(B)] );
+}
+
+{
+    add_members_ok( A => qw(B C) );
+    add_members_ok( B => qw(D) );
+    add_members_ok( C => qw(D) );
+    add_members_ok( A => qw(D) );
+    check_membership( A => [qw(B C D)], B => [qw(D)], C => [qw(D)] );
+
+    del_members_ok( A => qw(D) );
+    check_membership( A => [qw(B C)], B => [qw(D)], C => [qw(D)] );
+    random_delete( A => [qw(B C)], B => [qw(D)], C => [qw(D)] );
+}
+
+{
+    add_members_ok( A => qw(B C) );
+    add_members_ok( B => qw(d) );
+    add_members_ok( C => qw(d) );
+    add_members_ok( A => qw(d) );
+    check_membership( A => [qw(B C d)], B => [qw(d)], C => [qw(d)] );
+
+    del_members_ok( A => qw(d) );
+    check_membership( A => [qw(B C)], B => [qw(d)], C => [qw(d)] );
+    random_delete( A => [qw(B C)], B => [qw(d)], C => [qw(d)] );
+}
+
+for (1..5) {
+    random_delete( random_build() );
+}
+
+sub random_build {
+    my (%GM, %RCGM);
+
+    my @groups = keys %GROUP;
+
+    my $i = 12;
+    while ( $i-- ) {
+        REPICK:
+        my $g = $groups[int rand @groups];
+        my @members = (keys %GROUP, keys %USER);
+        substract_list(
+            \@members,
+            $g,
+            $GM{$g}? @{$GM{$g}} : (),
+            $RCGM{$g}? @{$RCGM{$g}} : (),
+        );
+        unless ( @members ) {
+            substract_list(\@groups, $g);
+            die "boo" unless @groups;
+            goto REPICK;
+        }
+
+        my $m = $members[int rand @members];
+
+        my $error = "($g -> $m) to ". describe_state(%GM);
+        diag "going to add $error";
+
+        add_members_ok( $g => $m );
+        push @{ $GM{ $g }||=[] }, $m;
+        unless ( check_membership( %GM ) ) {
+            Test::More::diag("were adding $error") unless $ENV{'TEST_VERBOSE'};
+            exit 1;
+        }
+
+        %RCGM = reverse_gm( gm_to_cgm(%GM) );
+    }
+    return %GM;
+}
+
+sub random_delete {
+    my %GM = @_;
+
+    while ( my @groups = keys %GM ) {
+        my $g = $groups[ int rand @groups ];
+        my $m = $GM{ $g }->[ int rand @{ $GM{ $g } } ];
+
+        my $error = "($g -> $m) from ". describe_state(%GM);
+        diag "going to delete $error";
+
+        del_members_ok( $g => $m );
+        @{ $GM{ $g } } = grep $_ ne $m, @{ $GM{ $g } };
+        delete $GM{ $g } unless @{ $GM{ $g } };
+
+        unless ( check_membership( %GM ) ) {
+            Test::More::diag("were deleting $error") unless $ENV{'TEST_VERBOSE'};
+        }
+    }
+}
+
+sub describe_state {
+    my %GM = @_;
+    return '('. join(
+        ', ',
+        map { "$_ -> [". join( ' ', @{ $GM{ $_ } } )."]" } sort keys %GM
+    ) .')';
+}
+
+sub check_membership {
+    local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+    my %GM = @_;
+    my $res = _check_membership( HasMember => %GM );
+    my %CGM = gm_to_cgm(%GM);
+    $res &&= _check_membership( HasMemberRecursively => %CGM );
+    return $res;
+}
+
+sub gm_to_cgm {
+    my %GM = @_;
+
+    my $flat;
+    $flat = sub {
+        return unless $GM{ $_[0] };
+        return map { $_, $flat->($_) } @{ $GM{ $_[0] } };
+    };
+
+    my %CGM;
+    $CGM{ $_ } = [ $flat->( $_ ) ] foreach keys %GM;
+    return %CGM;
+}
+
+sub reverse_gm {
+    my %GM = @_;
+    my %res = @_;
+
+    foreach my $g ( keys %GM ) {
+        push @{ $res{$_}||=[] }, $g foreach @{ $GM{ $g } };
+    }
+    return %res;
+}
+
+sub _check_membership {
+    local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+    my $method = shift;
+    my %GM = @_;
+
+    my $not_ok = 0;
+    foreach my $gname ( keys %GROUP ) {
+        foreach my $mname ( grep $gname ne $_, keys %USER, keys %GROUP ) {
+            my $ok;
+            if ( $GM{$gname} && grep $mname eq $_, @{$GM{$gname}} ) {
+                #note "checking ($gname -> $mname) for presence";
+                unless ( $GROUP{$gname}->$method( ($USER{$mname}||$GROUP{$mname})->PrincipalObj ) ) {
+                    $not_ok = 1;
+                    note "Group $gname has no member $mname, but should";
+                }
+            } else {
+                #note "checking ($gname -> $mname) for absence";
+                if ( $GROUP{$gname}->$method( ($USER{$mname}||$GROUP{$mname})->PrincipalObj ) ) {
+                    $not_ok = 1;
+                    note "Group $gname has member $mname, but should not";
+                }
+            }
+        }
+    }
+    return ok !$not_ok, "$method is ok";
+}
+
+sub add_members_ok {
+    my ($g, @members) = @_;
+    foreach my $m (@members) {
+        my ($status, $msg) = $GROUP{$g}->AddMember( ($USER{$m}||$GROUP{$m})->PrincipalId );
+        ok $status, $msg;
+    }
+}
+sub del_members_ok {
+    my ($g, @members) = @_;
+    foreach my $m (@members) {
+        my ($status, $msg) = $GROUP{$g}->DeleteMember( ($USER{$m}||$GROUP{$m})->PrincipalId );
+        ok $status, $msg;
+    }
+}
+
+sub dump_gm {
+    my ($G, $M) = @_;
+    my $dbh = $RT::Handle->dbh;
+
+    my $gm_id = sub {
+        my ($G, $M) = @_;
+        return ($dbh->selectrow_array(
+            "SELECT id FROM GroupMembers WHERE GroupId = $G AND MemberId = $M"
+        ))[0] || 0;
+    };
+    my $cgm_id = sub {
+        my ($G, $M) = @_;
+        return ($dbh->selectrow_array(
+            "SELECT id FROM CachedGroupMembers WHERE GroupId = $G AND MemberId = $M"
+        ))[0] || 0;
+    };
+    my $anc = sub {
+        my $M = shift;
+        return @{$dbh->selectcol_arrayref(
+            "SELECT GroupId FROM CachedGroupMembers WHERE MemberId = $M"
+        )};
+    };
+    my $des = sub {
+        my $G = shift;
+        return @{$dbh->selectcol_arrayref(
+            "SELECT MemberId FROM CachedGroupMembers WHERE GroupId = $G"
+        )};
+    };
+    my $anc_des_pairs = sub {
+        my ($G,$M) = @_;
+
+        foreach my $A ( $anc->($G) ) {
+            foreach my $D ( $des->($M) ) {
+                next unless my $id = $cgm_id->($A, $D);
+                diag "\t($A,$D) (#$id)(GM#". $gm_id->($A, $D).")";
+            }
+        }
+    };
+
+    my $id;
+    diag "Dumping GM ($G, $M) (#". $gm_id->($G, $M) .')';
+    diag "CGM ($G, $M) (#". $cgm_id->($G, $M) .')';
+    diag "An($G): ". join ',', map "$_ (GM#". $gm_id->($_, $G) .")", $anc->($G);
+    diag "De($M): ". join ',', map "$_ (GM#". $gm_id->($M, $_) .")", $des->($M);
+    diag "(An($G), De($M)): ";
+    $anc_des_pairs->($G, $M);
+}
+
+sub substract_list {
+    my $list = shift;
+    foreach my $e ( @_ ) {
+        @$list = grep $_ ne $e, @$list;
+    }
+}

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


More information about the Rt-commit mailing list