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
@@ -18,6 +18,17 @@ internal sealed class SnapshotConfiguration : IEntityTypeConfiguration<SnapshotE
builder.HasIndex(s => s.EventCode).HasDatabaseName("IX_Snapshots_EventCode");
// Snapshots is the largest table (live cadence 510s, 90-day retention) and
// every hot read filters EventCode + CapturedAt range, often with an ORDER BY
// CapturedAt. These composite indexes let SQLite satisfy the filter and the
// ordering from the index instead of scanning + sorting the table.
builder.HasIndex(s => new { s.EventCode, s.CapturedAt })
.HasDatabaseName("IX_Snapshots_EventCode_CapturedAt");
// Covers GetLatestPreMatchAsync: EventCode + Source filter, ORDER BY CapturedAt DESC.
builder.HasIndex(s => new { s.EventCode, s.Source, s.CapturedAt })
.HasDatabaseName("IX_Snapshots_EventCode_Source_CapturedAt");
builder.HasMany(s => s.Bets)
.WithOne(b => b.Snapshot)
.HasForeignKey(b => b.SnapshotId)