Skip to content

Commit bdb4684

Browse files
committed
Merge remote-tracking branch 'origin/hotfix-18.3' into develop
2 parents 0a696aa + 861d37e commit bdb4684

1 file changed

Lines changed: 103 additions & 14 deletions

File tree

Rock/Jobs/RockCleanup.cs

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2185,7 +2185,10 @@ private int LocationCleanup()
21852185

21862186
/// <summary>
21872187
/// Delete group membership duplicates if they are not allowed by web.config and return the
2188-
/// number of records deleted.
2188+
/// number of records deleted. Before deleting, re-map any GroupMemberAssignment rows that
2189+
/// reference a non-oldest duplicate to reference the oldest GroupMember for that person/group/role.
2190+
/// If the remap would violate the unique index on (GroupMemberId, GroupId, LocationId, ScheduleId),
2191+
/// delete the newer conflicting assignment instead.
21892192
/// </summary>
21902193
/// <returns>The number of records deleted</returns>
21912194
private int GroupMembershipCleanup()
@@ -2203,30 +2206,116 @@ private int GroupMembershipCleanup()
22032206

22042207
var groupMemberService = new GroupMemberService( rockContext );
22052208
var groupMemberHistoricalService = new GroupMemberHistoricalService( rockContext );
2209+
var groupMemberAssignmentService = new GroupMemberAssignmentService( rockContext );
22062210

2207-
var duplicateQuery = groupMemberService.Queryable()
2208-
2209-
// Duplicates are the same person, group, and role occurring more than once
2211+
// Build a per-duplicate mapping: (Duplicate GroupMemberId) -> (Oldest GroupMemberId)
2212+
// We need this so we can remap any GroupMemberAssignemnts to the surviving GroupMember record.
2213+
// NOTE: ThenBy(Id) gives deterministic ordering when CreatedDateTime ties.
2214+
var remapDict = groupMemberService.Queryable()
22102215
.GroupBy( m => new { m.PersonId, m.GroupId, m.GroupRoleId } )
2211-
2212-
// Filter out sets with only one occurrence because those are not duplicates
22132216
.Where( g => g.Count() > 1 )
2217+
.SelectMany( g =>
2218+
g.OrderBy( gm => gm.CreatedDateTime ?? DateTime.MinValue )
2219+
.ThenBy( gm => gm.Id )
2220+
.Skip( 1 )
2221+
.Select( dup => new
2222+
{
2223+
DuplicateId = dup.Id,
2224+
OldestId = g.OrderBy( gm => gm.CreatedDateTime ?? DateTime.MinValue )
2225+
.ThenBy( gm => gm.Id )
2226+
.Select( gm => gm.Id )
2227+
.FirstOrDefault()
2228+
} ) )
2229+
.ToDictionary( x => x.DuplicateId, x => x.OldestId );
2230+
2231+
if ( !remapDict.Any() )
2232+
{
2233+
return 0;
2234+
}
2235+
2236+
var duplicateGroupMemberIds = remapDict.Keys.ToList();
2237+
var oldestIds = remapDict.Values.Distinct().ToList();
2238+
2239+
// Step 1: Re-map GroupMemberAssignments, handling unique index collisions
2240+
// Unique index columns: GroupMemberId, GroupId, LocationId, ScheduleId
2241+
// If target exists, delete the newer assignment instead of remapping.
2242+
2243+
Func<int, int, int?, int?, string> key = ( groupMemberId, groupId, locationId, scheduleId ) =>
2244+
$"{groupMemberId}|{groupId}|{locationId?.ToString() ?? "NULL"}|{scheduleId?.ToString() ?? "NULL"}";
2245+
2246+
// Load source assignments (ones pointing at duplicates).
2247+
var sourceAssignments = groupMemberAssignmentService.Queryable()
2248+
.Where( a => duplicateGroupMemberIds.Contains( a.GroupMemberId ) )
2249+
.ToList();
2250+
2251+
// Load existing assignments for oldest ids (to detect collisions).
2252+
var existingTargetKeys = new HashSet<string>(
2253+
groupMemberAssignmentService.Queryable()
2254+
.Where( a => oldestIds.Contains( a.GroupMemberId ) )
2255+
.Select( a => new
2256+
{
2257+
a.GroupMemberId,
2258+
a.GroupId,
2259+
a.LocationId,
2260+
a.ScheduleId
2261+
} )
2262+
.ToList()
2263+
.Select( x => key( x.GroupMemberId, x.GroupId, x.LocationId, x.ScheduleId ) )
2264+
);
2265+
2266+
// Reserve keys for assignments that already exist, after they are moved (prevents collisions within the batch).
2267+
var duplicateAssignmentIdsToDelete = new List<int>();
2268+
2269+
foreach ( var assignment in sourceAssignments )
2270+
{
2271+
int oldestGroupMemberId;
2272+
if ( !remapDict.TryGetValue( assignment.GroupMemberId, out oldestGroupMemberId ) || oldestGroupMemberId <= 0 )
2273+
{
2274+
continue;
2275+
}
2276+
2277+
if ( assignment.GroupMemberId == oldestGroupMemberId )
2278+
{
2279+
continue;
2280+
}
2281+
2282+
var targetKey = key( oldestGroupMemberId, assignment.GroupId, assignment.LocationId, assignment.ScheduleId );
2283+
2284+
if ( existingTargetKeys.Contains( targetKey ) )
2285+
{
2286+
duplicateAssignmentIdsToDelete.Add( assignment.Id );
2287+
continue;
2288+
}
22142289

2215-
// Leave the oldest membership and delete the others
2216-
.SelectMany( g => g.OrderBy( gm => gm.CreatedDateTime ).Skip( 1 ) );
2290+
// Point this assignment to the oldest, surviving group member id
2291+
assignment.GroupMemberId = oldestGroupMemberId;
2292+
existingTargetKeys.Add( targetKey );
2293+
}
2294+
2295+
// Delete any assignments that were going to be duplicates.
2296+
if ( duplicateAssignmentIdsToDelete.Any() )
2297+
{
2298+
var assignmentsToDeleteQuery = groupMemberAssignmentService.Queryable()
2299+
.Where( a => duplicateAssignmentIdsToDelete.Contains( a.Id ) );
22172300

2218-
// Get the IDs to delete the history
2219-
var groupMemberIds = duplicateQuery.Select( d => d.Id );
2301+
groupMemberAssignmentService.DeleteRange( assignmentsToDeleteQuery );
2302+
}
2303+
2304+
// Step 2: Delete history rows for duplicates.
22202305
var historyQuery = groupMemberHistoricalService.Queryable()
2221-
.Where( gmh => groupMemberIds.Contains( gmh.GroupMemberId ) );
2306+
.Where( gmh => duplicateGroupMemberIds.Contains( gmh.GroupMemberId ) );
2307+
2308+
// Step 3: Delete duplicate GroupMember rows (non-oldest).
2309+
var duplicatesQuery = groupMemberService.Queryable()
2310+
.Where( gm => duplicateGroupMemberIds.Contains( gm.Id ) );
22222311

2223-
// Delete the history and duplicate memberships
22242312
groupMemberHistoricalService.DeleteRange( historyQuery );
2225-
groupMemberService.DeleteRange( duplicateQuery );
2313+
groupMemberService.DeleteRange( duplicatesQuery );
2314+
22262315
rockContext.SaveChanges();
22272316

22282317
// Return the count of memberships deleted
2229-
return groupMemberIds.Count();
2318+
return duplicateGroupMemberIds.Count;
22302319
}
22312320

22322321
private int DeleteDuplicatePreviousFamilyLocations()

0 commit comments

Comments
 (0)