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:
2026-05-28 22:34:08 +03:00
parent 0d52b7beff
commit f294255f10
30 changed files with 522 additions and 145 deletions
@@ -32,6 +32,21 @@ public sealed class ResultsLoaderTests : MarathonTestContext
sp.GetRequiredService<IEventRepository>(),
sp.GetRequiredService<IResultRepository>(),
NullLogger<PullResultsUseCase>.Instance));
// PullResultsUseCase batches selection-mode candidate resolution via
// GetManyAsync; route it through whatever GetAsync the test configures.
_eventRepo.GetManyAsync(Arg.Any<IReadOnlyCollection<EventId>>(), Arg.Any<CancellationToken>())
.Returns(ci =>
{
var ids = ci.Arg<IReadOnlyCollection<EventId>>();
var dict = new Dictionary<EventId, Event>();
foreach (var id in ids.Distinct())
{
var ev = _eventRepo.GetAsync(id, CancellationToken.None).GetAwaiter().GetResult();
if (ev is not null) dict[id] = ev;
}
return (IReadOnlyDictionary<EventId, Event>)dict;
});
}
[Fact]