f294255f10
- 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.
88 lines
3.0 KiB
C#
88 lines
3.0 KiB
C#
using Marathon.Application.Abstractions;
|
|
using Marathon.Domain.Entities;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Marathon.Infrastructure.Persistence.Repositories;
|
|
|
|
internal sealed class AnomalyRepository : IAnomalyRepository
|
|
{
|
|
private readonly MarathonDbContext _db;
|
|
|
|
public AnomalyRepository(MarathonDbContext db) => _db = db;
|
|
|
|
public async Task<Anomaly?> GetAsync(Guid key, CancellationToken ct = default)
|
|
{
|
|
var idStr = key.ToString();
|
|
var entity = await _db.Anomalies.FirstOrDefaultAsync(a => a.Id == idStr, ct);
|
|
return entity is null ? null : Mapping.ToDomain(entity);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<Anomaly>> ListAsync(CancellationToken ct = default)
|
|
{
|
|
var entities = await _db.Anomalies.AsNoTracking().ToListAsync(ct);
|
|
return entities.Select(Mapping.ToDomain).ToList().AsReadOnly();
|
|
}
|
|
|
|
public async Task<int> CountSinceAsync(DateTimeOffset since, CancellationToken ct = default)
|
|
{
|
|
// Server-side COUNT(*) — the unread-badge hot path must not materialise the
|
|
// whole table (with EvidenceJson) just to count. DetectedAt is stored as the
|
|
// O-format TEXT key (see SqliteDateText); ">" matches the prior in-memory
|
|
// GetUnreadCountAsync semantics (strictly newer than the last-seen marker).
|
|
var sinceStr = SqliteDateText.Key(since);
|
|
return await _db.Anomalies.AsNoTracking()
|
|
.Where(a => a.DetectedAt.CompareTo(sinceStr) > 0)
|
|
.CountAsync(ct);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<Anomaly>> ListByDateRangeAsync(
|
|
DateTimeOffset? from,
|
|
DateTimeOffset? to,
|
|
CancellationToken ct = default)
|
|
{
|
|
var q = _db.Anomalies.AsNoTracking();
|
|
|
|
if (from is { } f)
|
|
{
|
|
var fromStr = SqliteDateText.Key(f);
|
|
q = q.Where(a => a.DetectedAt.CompareTo(fromStr) >= 0);
|
|
}
|
|
|
|
if (to is { } t)
|
|
{
|
|
var toStr = SqliteDateText.Key(t);
|
|
q = q.Where(a => a.DetectedAt.CompareTo(toStr) <= 0);
|
|
}
|
|
|
|
var entities = await q
|
|
.OrderByDescending(a => a.DetectedAt)
|
|
.ToListAsync(ct);
|
|
|
|
return entities.Select(Mapping.ToDomain).ToList().AsReadOnly();
|
|
}
|
|
|
|
public async Task AddAsync(Anomaly entity, CancellationToken ct = default)
|
|
{
|
|
var efEntity = Mapping.ToEntity(entity);
|
|
await _db.Anomalies.AddAsync(efEntity, ct);
|
|
}
|
|
|
|
public Task UpdateAsync(Anomaly entity, CancellationToken ct = default)
|
|
{
|
|
var efEntity = Mapping.ToEntity(entity);
|
|
_db.Anomalies.Update(efEntity);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task DeleteAsync(Guid key, CancellationToken ct = default)
|
|
{
|
|
var idStr = key.ToString();
|
|
var entity = await _db.Anomalies.FirstOrDefaultAsync(a => a.Id == idStr, ct);
|
|
if (entity is not null)
|
|
_db.Anomalies.Remove(entity);
|
|
}
|
|
|
|
public async Task SaveChangesAsync(CancellationToken ct = default) =>
|
|
await _db.SaveChangesAsync(ct);
|
|
}
|