perf(sql): sweep/KPI covering indexes + sliced notification terminal purge

This commit is contained in:
Joseph Doherty
2026-08-14 19:55:48 -04:00
parent 0b201e410c
commit 600659d579
8 changed files with 2265 additions and 3 deletions
@@ -230,9 +230,30 @@ VALUES
/// <inheritdoc />
public async Task<int> DeleteTerminalOlderThanAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default)
{
return await _context.Notifications
// Time-sliced batches: each DELETE covers at most one hour of terminal rows,
// capping the lock/log footprint per statement — mirrors
// KpiHistoryRepository.PurgeOlderThanAsync (arch-review WP1.4; an unbounded
// single-statement ExecuteDeleteAsync would otherwise turn a long-idle catch-up
// of the default 365-day window into one giant transaction against
// dbo.Notifications). Pure EF LINQ (no raw SQL) so this stays portable across
// the SQL Server production provider and the SQLite provider the repository
// tests run against.
var total = 0;
var floor = await _context.Notifications
.Where(n => TerminalStatuses.Contains(n.Status) && n.CreatedAt < cutoff)
.ExecuteDeleteAsync(cancellationToken);
.MinAsync(n => (DateTimeOffset?)n.CreatedAt, cancellationToken);
while (floor is not null && floor < cutoff)
{
var ceiling = floor.Value.AddHours(1) < cutoff ? floor.Value.AddHours(1) : cutoff;
total += await _context.Notifications
.Where(n => TerminalStatuses.Contains(n.Status) && n.CreatedAt < ceiling)
.ExecuteDeleteAsync(cancellationToken);
floor = await _context.Notifications
.Where(n => TerminalStatuses.Contains(n.Status) && n.CreatedAt < cutoff)
.MinAsync(n => (DateTimeOffset?)n.CreatedAt, cancellationToken);
}
return total;
}
/// <inheritdoc />