using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Providers.Sqlite; using Jellyfin.Server.Implementations.Tests.Item; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Jellyfin.Server.Implementations.Tests.Data; /// /// Statistics taken on a freshly created database describe every table as a single row, and SQLite then plans /// the user data and series queries of a filled library as full scans (#17886). /// public sealed class SqliteDatabaseStatisticsTests : SqliteDbTestFixture { private readonly SqliteDatabaseProvider _provider; public SqliteDatabaseStatisticsTests() { _provider = new SqliteDatabaseProvider(ApplicationPaths, NullLogger.Instance) { DbContextFactory = CreateDbContextFactory() }; } [Fact] public async Task RunScheduledOptimisation_EmptyLibrary_RecordsNoStatistics() { SeedFolders(3); await _provider.RunScheduledOptimisation(CancellationToken.None); Assert.Null(ReadAnalyzedItemCount()); } [Fact] public async Task RunScheduledOptimisation_LibraryWithItems_RecordsStatistics() { SeedFolders(1); SeedEpisodes(4); await _provider.RunScheduledOptimisation(CancellationToken.None); Assert.Equal(CountItems(), ReadAnalyzedItemCount()); } [Fact] public async Task RefreshStatistics_NoStatistics_Analyzes() { SeedEpisodes(5); await _provider.RefreshStatistics(CancellationToken.None); Assert.Equal(CountItems(), ReadAnalyzedItemCount()); } [Fact] public async Task RefreshStatistics_LibraryChanged_Reanalyzes() { SeedEpisodes(10); Analyze(); SeedEpisodes(5); await _provider.RefreshStatistics(CancellationToken.None); Assert.Equal(CountItems(), ReadAnalyzedItemCount()); } [Fact] public async Task RefreshStatistics_EmptyLibrary_RecordsNoStatistics() { SeedFolders(2); await _provider.RefreshStatistics(CancellationToken.None); Assert.Null(ReadAnalyzedItemCount()); } private void SeedFolders(int count) { using var context = CreateDbContext(); context.BaseItems.AddRange(Enumerable.Range(0, count).Select(_ => new BaseItemEntity { Id = Guid.NewGuid(), Type = "MediaBrowser.Controller.Entities.Folder", IsFolder = true })); context.SaveChanges(); } private void SeedEpisodes(int count) { using var context = CreateDbContext(); context.BaseItems.AddRange(Enumerable.Range(0, count).Select(_ => new BaseItemEntity { Id = Guid.NewGuid(), Type = "MediaBrowser.Controller.Entities.TV.Episode", IsFolder = false })); context.SaveChanges(); } private void Analyze() { using var context = CreateDbContext(); context.Database.ExecuteSqlRaw("ANALYZE"); } private long CountItems() { using var context = CreateDbContext(); return context.BaseItems.LongCount(); } private long? ReadAnalyzedItemCount() { using var context = CreateDbContext(); var hasStatistics = context.Database .SqlQueryRaw("SELECT count(*) AS \"Value\" FROM sqlite_schema WHERE type = 'table' AND name = 'sqlite_stat1'") .Single(); if (hasStatistics == 0) { return null; } return context.Database .SqlQueryRaw("SELECT max(CAST(stat AS INTEGER)) AS \"Value\" FROM sqlite_stat1 WHERE tbl = 'BaseItems'") .Single(); } }