Files
archived-discord-bot/CompatBot/Database/ThumbnailDb.cs
13xforever 3ca3c4fef5 fix substring search in autocomplete providers
apparently, sqlite is using case-sensitive collation by default
but even if you add NOCASE collation to the column, ef core translates string.Contains() to instr function call, which ignores the collation
2025-08-06 16:04:44 +05:00

205 lines
6.9 KiB
C#

using System.ComponentModel.DataAnnotations;
using CompatApiClient;
using Microsoft.EntityFrameworkCore;
using Nito.AsyncEx;
namespace CompatBot.Database;
internal class ThumbnailDb : DbContext
{
private static readonly AsyncReaderWriterLock DbLockSource = new();
private static int openReadCount, openWriteCount;
private readonly IDisposable readWriteLock;
private readonly bool canWrite;
public DbSet<State> State { get; set; } = null!;
public DbSet<Thumbnail> Thumbnail { get; set; } = null!;
public DbSet<GameUpdateInfo> GameUpdateInfo { get; set; } = null!;
public DbSet<SyscallInfo> SyscallInfo { get; set; } = null!;
public DbSet<SyscallToProductMap> SyscallToProductMap { get; set; } = null!;
public DbSet<Metacritic> Metacritic { get; set; } = null!;
public DbSet<Fortune> Fortune { get; set; } = null!;
public DbSet<NamePool> NamePool { get; set; } = null!;
[Obsolete("For migrations only")]
public ThumbnailDb(): this(DbLockSource.WriterLock(Config.Cts.Token))
{
}
private ThumbnailDb(IDisposable readWriteLock, bool canWrite = false)
{
this.readWriteLock = readWriteLock;
this.canWrite = canWrite;
#if DEBUG
if (canWrite)
Interlocked.Increment(ref openWriteCount);
else
Interlocked.Increment(ref openReadCount);
//var st = new System.Diagnostics.StackTrace().GetCaller<ThumbnailDb>();
//Config.Log.Debug($"{nameof(ThumbnailDb)}>>>{(canWrite ? "Write" : "Read")} (r/w: {openReadCount}/{openWriteCount}) #{readWriteLock.GetHashCode():x8} from {st}");
#endif
}
public static async ValueTask<ThumbnailDb> OpenReadAsync()
=> new(await DbLockSource.ReaderLockAsync(Config.Cts.Token).ConfigureAwait(false));
public static async ValueTask<ThumbnailDb> OpenWriteAsync()
=> new(await DbLockSource.WriterLockAsync(Config.Cts.Token).ConfigureAwait(false));
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var dbPath = DbImporter.GetDbPath("thumbs.db", Environment.SpecialFolder.LocalApplicationData);
if (Config.EnableEfDebugLogging)
optionsBuilder.UseLoggerFactory(Config.LoggerFactory);
optionsBuilder.UseSqlite($""" Data Source="{dbPath}" """);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//configure indices
modelBuilder.Entity<State>().HasIndex(s => s.Locale).IsUnique().HasDatabaseName("state_locale");
modelBuilder.Entity<State>().HasIndex(s => s.Timestamp).HasDatabaseName("state_timestamp");
modelBuilder.Entity<Thumbnail>().HasIndex(m => m.ProductCode).IsUnique().HasDatabaseName("thumbnail_product_code");
modelBuilder.Entity<Thumbnail>().HasIndex(m => m.ContentId).IsUnique().HasDatabaseName("thumbnail_content_id");
modelBuilder.Entity<Thumbnail>().HasIndex(m => m.Timestamp).HasDatabaseName("thumbnail_timestamp");
modelBuilder.Entity<Thumbnail>().Property(m => m.Name).UseCollation("NOCASE");
modelBuilder.Entity<GameUpdateInfo>().HasIndex(ui => ui.ProductCode).IsUnique().HasDatabaseName("game_update_info_product_code");
modelBuilder.Entity<SyscallInfo>().HasIndex(sci => sci.Function).HasDatabaseName("syscall_info_function");
modelBuilder.Entity<SyscallToProductMap>().HasKey(m => new {m.ProductId, m.SyscallInfoId});
modelBuilder.Entity<Fortune>();
modelBuilder.Entity<NamePool>();
//configure default policy of Id being the primary key
modelBuilder.ConfigureDefaultPkConvention();
//configure name conversion for all configured entities from CamelCase to snake_case
modelBuilder.ConfigureMapping(NamingStyles.Underscore);
}
public override void Dispose()
{
base.Dispose();
readWriteLock.Dispose();
#if DEBUG
if (canWrite)
Interlocked.Decrement(ref openWriteCount);
else
Interlocked.Decrement(ref openReadCount);
Config.Log.Debug($"{nameof(ThumbnailDb)}<<<{(canWrite ? "Write" : "Read")} (r/w: {openReadCount}/{openWriteCount}) #{readWriteLock.GetHashCode():x8}");
#endif
}
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync();
readWriteLock.Dispose();
#if DEBUG
if (canWrite)
Interlocked.Decrement(ref openWriteCount);
else
Interlocked.Decrement(ref openReadCount);
Config.Log.Debug($"{nameof(ThumbnailDb)}<<<{(canWrite ? "Write" : "Read")} (r/w: {openReadCount}/{openWriteCount}) #{readWriteLock.GetHashCode():x8}");
#endif
}
}
internal class State
{
public int Id { get; set; }
public string? Locale { get; set; }
public long Timestamp { get; set; }
}
internal class Thumbnail
{
public int Id { get; set; }
[Required]
public string ProductCode { get; set; } = null!;
public string? ContentId { get; set; }
public string? Name { get; set; }
public string? Url { get; set; }
public string? EmbeddableUrl { get; set; }
public long Timestamp { get; set; }
public int? EmbedColor { get; set; }
public CompatStatus? CompatibilityStatus { get; set; }
public long? CompatibilityChangeDate { get; set; }
public int? MetacriticId { get; set; }
public Metacritic? Metacritic { get; set; }
public List<SyscallToProductMap> SyscallToProductMap { get; set; } = null!;
}
internal class GameUpdateInfo
{
public int Id { get; set; }
[Required]
public string ProductCode { get; set; } = null!;
public int MetaHash { get; set; }
[Required]
public string MetaXml { get; set; } = null!;
public long Timestamp { get; set; }
}
public enum CompatStatus : byte
{
Unknown = 0,
Nothing = 10,
Loadable = 20,
Intro = 30,
Ingame = 40,
Playable = 50,
}
internal class SyscallInfo
{
public int Id { get; set; }
[Required]
public string Function { get; set; } = null!;
public List<SyscallToProductMap> SyscallToProductMap { get; set; } = null!;
}
internal class SyscallToProductMap
{
public int ProductId { get; set; }
public Thumbnail Product { get; set; } = null!;
public int SyscallInfoId { get; set; }
public SyscallInfo SyscallInfo { get; set; } = null!;
}
internal class Metacritic
{
public int Id { get; set; }
[Required]
public string Title { get; set; } = null!;
public byte? CriticScore { get; set; }
public byte? UserScore { get; set; }
public string? Notes { get; set; }
public Metacritic WithTitle(string title)
{
return new()
{
Title = title,
CriticScore = CriticScore,
UserScore = UserScore,
Notes = Notes,
};
}
}
internal class Fortune
{
public int Id { get; set; }
[Required]
public string Content { get; set; } = null!;
}
internal class NamePool
{
public int Id { get; set; }
[Required]
public string Name { get; set; } = null!;
}