perf: batch repository reads, index snapshots, centralize date encoding
- Add IEventRepository/IResultRepository.GetManyAsync to kill N+1 lookups at 6 sites (backtest, outcome eval, both bet-journal paths, anomaly browsing, results selection); guarded by a Received(1).GetManyAsync test. - Add EventRepository.QueryAsync to push date+sport filtering to SQL (was load-whole-range-then-filter); search/sort stay in-memory for Cyrillic order. - Add AnomalyRepository.CountSinceAsync (unread badge) + ListByDateRangeAsync (feed date filter); add Event/Snapshot count methods for the dashboard. - Add composite indexes IX_Snapshots_EventCode_CapturedAt and _EventCode_Source_CapturedAt via a new migration + model snapshot. - Introduce SqliteDateText as the single source of the O-format date encoding shared by Mapping (read/write) and the repositories' range predicates. - Fix LiveOddsPoller cadence drift (budget sleep against cycle time); make DetectAnomalies dedup O(1) per event; add Event.Title to dedup the title join. Tests adapted to the batched GetManyAsync via a TestFixtures bridge.
This commit is contained in:
@@ -22,6 +22,14 @@ public sealed class EvaluateAnomalyOutcomesUseCaseTests
|
||||
private readonly IEventRepository _events = Substitute.For<IEventRepository>();
|
||||
private readonly IResultRepository _results = Substitute.For<IResultRepository>();
|
||||
|
||||
public EvaluateAnomalyOutcomesUseCaseTests()
|
||||
{
|
||||
// Use cases batch event/result loads via GetManyAsync; route those through
|
||||
// the per-id GetAsync stubs each test already configures.
|
||||
TestFixtures.BridgeGetMany(_events);
|
||||
TestFixtures.BridgeGetMany(_results);
|
||||
}
|
||||
|
||||
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
|
||||
private static readonly DateTimeOffset BaseTime =
|
||||
new(2026, 5, 10, 18, 0, 0, MoscowOffset);
|
||||
@@ -287,6 +295,36 @@ public sealed class EvaluateAnomalyOutcomesUseCaseTests
|
||||
report.EventTitles[id].Should().Be("Team A vs Team B");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_BatchEventAndResultLoads_InsteadOfPerIdGetAsync()
|
||||
{
|
||||
// Regression guard for the N+1 fix: the use case must resolve events/results
|
||||
// via the batched GetManyAsync, never the per-id GetAsync in a loop. We stub
|
||||
// GetManyAsync directly (overriding the constructor bridge) so DidNotReceive()
|
||||
// on GetAsync is meaningful.
|
||||
var id1 = new EventId("11111111");
|
||||
var id2 = new EventId("22222222");
|
||||
|
||||
_anomalies.ListAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new[] { MakeAnomaly(id1, 0.55m), MakeAnomaly(id2, 0.55m) }.ToList().AsReadOnly());
|
||||
|
||||
_events.GetManyAsync(Arg.Any<IReadOnlyCollection<EventId>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new Dictionary<EventId, Event> { [id1] = MakeEvent(id1, 11), [id2] = MakeEvent(id2, 6) });
|
||||
_results.GetManyAsync(Arg.Any<IReadOnlyCollection<EventId>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new Dictionary<EventId, EventResult>());
|
||||
|
||||
await CreateSut().ExecuteAsync(CancellationToken.None);
|
||||
|
||||
await _events.Received(1)
|
||||
.GetManyAsync(Arg.Any<IReadOnlyCollection<EventId>>(), Arg.Any<CancellationToken>());
|
||||
await _events.DidNotReceive()
|
||||
.GetAsync(Arg.Any<EventId>(), Arg.Any<CancellationToken>());
|
||||
await _results.Received(1)
|
||||
.GetManyAsync(Arg.Any<IReadOnlyCollection<EventId>>(), Arg.Any<CancellationToken>());
|
||||
await _results.DidNotReceive()
|
||||
.GetAsync(Arg.Any<EventId>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_HandleMissingEvent_By_OmittingFromSportBuckets()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user