using Cleanuparr.Domain.Enums; using Cleanuparr.Persistence.Converters; using Cleanuparr.Persistence.Models.Events; using Cleanuparr.Persistence.Models.State; using Cleanuparr.Persistence.Providers; using Cleanuparr.Shared.Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Cleanuparr.Persistence; /// /// Database context for events /// public class EventsContext : DbContext { public DbSet Events { get; set; } public DbSet ManualEvents { get; set; } public DbSet Strikes { get; set; } public DbSet DownloadItems { get; set; } public DbSet JobRuns { get; set; } public DbSet SeekerHistory { get; set; } public DbSet SearchQueue { get; set; } public DbSet CustomFormatScoreEntries { get; set; } public DbSet CustomFormatScoreHistory { get; set; } public DbSet SeekerCommandTrackers { get; set; } private readonly IDatabaseProvider _provider; public EventsContext() : this(DatabaseProviderFactory.Current) { } public EventsContext(IDatabaseProvider provider) { _provider = provider; } public EventsContext(DbContextOptions options) : this(options, DatabaseProviderFactory.Current) { } public EventsContext(DbContextOptions options, IDatabaseProvider provider) : base(options) { _provider = provider; } public static EventsContext CreateStaticInstance() { IDatabaseProvider provider = DatabaseProviderFactory.Current; DbContextOptionsBuilder optionsBuilder = new(); provider.ConfigureContext(optionsBuilder, DbContextKind.Events); return new EventsContext(optionsBuilder.Options, provider); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { SetDbContextOptions(optionsBuilder); } protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { _provider.ConfigureConventions(configurationBuilder); } public static string GetLikePattern(string input) { string escaped = input.ToLowerInvariant() .Replace("\\", "\\\\") .Replace("%", "\\%") .Replace("_", "\\_"); return $"%{escaped}%"; } protected override void OnModelCreating(ModelBuilder modelBuilder) { string? schema = _provider.GetSchema(DbContextKind.Events); if (schema is not null) { modelBuilder.HasDefaultSchema(schema); } modelBuilder.Entity(entity => { entity.Property(e => e.ItemHash) .HasConversion(new LowercaseStringConverter()); entity.HasOne(e => e.Strike) .WithMany() .HasForeignKey(e => e.StrikeId) .OnDelete(DeleteBehavior.SetNull); entity.HasOne(e => e.JobRun) .WithMany(j => j.Events) .HasForeignKey(e => e.JobRunId) .OnDelete(DeleteBehavior.SetNull); }); modelBuilder.Entity(entity => { // Race-proof gate: at most one UNRESOLVED event per (Type, ItemHash). // Partial unique index — resolved rows are exempt, so history/cooldown is unaffected. entity.HasIndex(e => new { e.Type, e.ItemHash }) .IsUnique() .HasFilter(_provider.GetUnresolvedEventFilter()); }); modelBuilder.Entity(entity => { entity.Property(d => d.DownloadId) .HasConversion(new LowercaseStringConverter()); }); modelBuilder.Entity(entity => { entity.HasIndex(s => new { s.ArrInstanceId, s.ExternalItemId, s.ItemType, s.SeasonNumber, s.CycleId }).IsUnique(); }); modelBuilder.Entity(entity => { entity.HasIndex(s => s.ArrInstanceId); }); modelBuilder.Entity(entity => { entity.HasIndex(s => new { s.ArrInstanceId, s.ExternalItemId, s.EpisodeId }).IsUnique(); entity.HasIndex(s => s.LastUpgradedAt); }); modelBuilder.Entity(entity => { entity.HasIndex(s => new { s.ArrInstanceId, s.ExternalItemId, s.EpisodeId }); entity.HasIndex(s => s.RecordedAt); }); modelBuilder.Entity(entity => { entity.HasIndex(s => s.ArrInstanceId); }); modelBuilder.ApplyLowercaseEnumConversions(); } private void SetDbContextOptions(DbContextOptionsBuilder optionsBuilder) { if (optionsBuilder.IsConfigured) { return; } _provider.ConfigureContext(optionsBuilder, DbContextKind.Events); } }