< Summary - Jellyfin

Information
Class: Jellyfin.Database.Providers.Sqlite.SqliteDatabaseProvider
Assembly: Jellyfin.Database.Providers.Sqlite
File(s): /srv/git/jellyfin/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
Line coverage
44%
Covered lines: 40
Uncovered lines: 50
Coverable lines: 90
Total lines: 215
Line coverage: 44.4%
Branch coverage
25%
Covered branches: 4
Total branches: 16
Branch coverage: 25%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/4/2026 - 12:15:16 AM Line coverage: 41.8% (36/86) Branch coverage: 31.2% (5/16) Total lines: 2115/20/2026 - 12:15:44 AM Line coverage: 41.8% (36/86) Branch coverage: 25% (4/16) Total lines: 2118/9/2026 - 12:16:58 AM Line coverage: 44.4% (40/90) Branch coverage: 25% (4/16) Total lines: 215 5/4/2026 - 12:15:16 AM Line coverage: 41.8% (36/86) Branch coverage: 31.2% (5/16) Total lines: 2115/20/2026 - 12:15:44 AM Line coverage: 41.8% (36/86) Branch coverage: 25% (4/16) Total lines: 2118/9/2026 - 12:16:58 AM Line coverage: 44.4% (40/90) Branch coverage: 25% (4/16) Total lines: 215

Coverage delta

Coverage delta 7 -7

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Initialise(...)50%8894.28%
RunScheduledOptimisation()100%210%
OnModelCreating(...)100%11100%
RunShutdownTask()0%620%
ConfigureConventions(...)100%11100%
MigrationBackupFast(...)100%210%
RestoreBackupFast(...)0%620%
DeleteBackup(...)0%620%
PurgeDatabase()0%620%

File(s)

/srv/git/jellyfin/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.IO;
 5using System.Linq;
 6using System.Threading;
 7using System.Threading.Tasks;
 8using Jellyfin.Database.Implementations;
 9using Jellyfin.Database.Implementations.DbConfiguration;
 10using MediaBrowser.Common.Configuration;
 11using Microsoft.Data.Sqlite;
 12using Microsoft.EntityFrameworkCore;
 13using Microsoft.EntityFrameworkCore.Diagnostics;
 14using Microsoft.Extensions.Logging;
 15
 16namespace Jellyfin.Database.Providers.Sqlite;
 17
 18/// <summary>
 19/// Configures jellyfin to use an SQLite database.
 20/// </summary>
 21[JellyfinDatabaseProviderKey("Jellyfin-SQLite")]
 22public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
 23{
 24    private const string BackupFolderName = "SQLiteBackups";
 25    private readonly IApplicationPaths _applicationPaths;
 26    private readonly ILogger<SqliteDatabaseProvider> _logger;
 27
 28    /// <summary>
 29    /// Initializes a new instance of the <see cref="SqliteDatabaseProvider"/> class.
 30    /// </summary>
 31    /// <param name="applicationPaths">Service to construct the fallback when the old data path configuration is used.</
 32    /// <param name="logger">A logger.</param>
 33    public SqliteDatabaseProvider(IApplicationPaths applicationPaths, ILogger<SqliteDatabaseProvider> logger)
 34    {
 21235        _applicationPaths = applicationPaths;
 21236        _logger = logger;
 21237    }
 38
 39    /// <inheritdoc/>
 40    public IDbContextFactory<JellyfinDbContext>? DbContextFactory { get; set; }
 41
 42    /// <inheritdoc/>
 43    public void Initialise(DbContextOptionsBuilder options, DatabaseConfigurationOptions databaseConfiguration)
 44    {
 45        static T? GetOption<T>(ICollection<CustomDatabaseOption>? options, string key, Func<string, T> converter, Func<T
 46        {
 47            if (options is null)
 48            {
 49                return defaultValue is not null ? defaultValue() : default;
 50            }
 51
 52            var value = options.FirstOrDefault(e => e.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
 53            if (value is null)
 54            {
 55                return defaultValue is not null ? defaultValue() : default;
 56            }
 57
 58            return converter(value.Value);
 59        }
 60
 4461        var customOptions = databaseConfiguration.CustomProviderOptions?.Options;
 62
 4463        var sqliteConnectionBuilder = new SqliteConnectionStringBuilder
 4464        {
 4465            DataSource = GetOption(customOptions, "path", e => e, () => Path.Combine(_applicationPaths.DataPath, "jellyf
 4466            // Private, not Default: sqlite3_enable_shared_cache is process-global, so a plugin
 4467            // enabling it makes these connections share a cache too. Contention then surfaces as
 4468            // SQLITE_LOCKED ("database table is locked"), which the busy handler does not cover,
 4469            // so busy_timeout is skipped and the command fails at CommandTimeout instead.
 4470            Cache = GetOption(customOptions, "cache", Enum.Parse<SqliteCacheMode>, () => SqliteCacheMode.Private),
 4471            Pooling = GetOption(customOptions, "pooling", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreC
 4472            DefaultTimeout = GetOption(customOptions, "command-timeout", int.Parse, () => 60)
 4473        };
 74
 4475        var connectionString = sqliteConnectionBuilder.ToString();
 76
 77        // Log SQLite connection parameters
 4478        _logger.LogInformation("SQLite connection string: {ConnectionString}", connectionString);
 79
 4480        options
 4481            .UseSqlite(
 4482                connectionString,
 4483                sqLiteOptions => sqLiteOptions.MigrationsAssembly(GetType().Assembly))
 4484            // TODO: Remove when https://github.com/dotnet/efcore/pull/35873 is merged & released
 4485            .ConfigureWarnings(warnings =>
 4486                warnings.Ignore(RelationalEventId.NonTransactionalMigrationOperationWarning)
 4487                    .Ignore(RelationalEventId.MultipleCollectionIncludeWarning))
 4488            .AddInterceptors(new PragmaConnectionInterceptor(
 4489                _logger,
 4490                GetOption<int?>(customOptions, "cacheSize", e => int.Parse(e, CultureInfo.InvariantCulture)),
 4491                GetOption(customOptions, "lockingmode", e => e, () => "NORMAL")!,
 4492                GetOption(customOptions, "journalsizelimit", int.Parse, () => 134_217_728),
 4493                GetOption(customOptions, "tempstoremode", int.Parse, () => 2),
 4494                GetOption(customOptions, "syncmode", int.Parse, () => 1),
 4495                customOptions?.Where(e => e.Key.StartsWith("#PRAGMA:", StringComparison.OrdinalIgnoreCase)).ToDictionary
 96
 4497        var enableSensitiveDataLogging = GetOption(customOptions, "EnableSensitiveDataLogging", e => e.Equals(bool.TrueS
 4498        if (enableSensitiveDataLogging)
 99        {
 0100            options.EnableSensitiveDataLogging(enableSensitiveDataLogging);
 0101            _logger.LogInformation("EnableSensitiveDataLogging is enabled on SQLite connection");
 102        }
 44103    }
 104
 105    /// <inheritdoc/>
 106    public async Task RunScheduledOptimisation(CancellationToken cancellationToken)
 107    {
 0108        var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 0109        await using (context.ConfigureAwait(false))
 110        {
 0111            await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwa
 0112            await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
 0113            await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false);
 0114            await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwa
 0115            _logger.LogInformation("jellyfin.db optimized successfully!");
 116        }
 0117    }
 118
 119    /// <inheritdoc/>
 120    public void OnModelCreating(ModelBuilder modelBuilder)
 121    {
 2122        modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
 2123    }
 124
 125    /// <inheritdoc/>
 126    public async Task RunShutdownTask(CancellationToken cancellationToken)
 127    {
 0128        if (DbContextFactory is null)
 129        {
 0130            return;
 131        }
 132
 133        // Run before disposing the application
 0134        var context = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 0135        await using (context.ConfigureAwait(false))
 136        {
 0137            await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
 138        }
 139
 0140        SqliteConnection.ClearAllPools();
 0141    }
 142
 143    /// <inheritdoc/>
 144    public void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
 145    {
 2146        configurationBuilder.Conventions.Add(_ => new DoNotUseReturningClauseConvention());
 2147    }
 148
 149    /// <inheritdoc />
 150    public Task<string> MigrationBackupFast(CancellationToken cancellationToken)
 151    {
 0152        var key = DateTime.UtcNow.ToString("yyyyMMddhhmmss", CultureInfo.InvariantCulture);
 0153        var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
 0154        var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName);
 0155        Directory.CreateDirectory(backupFile);
 156
 0157        backupFile = Path.Combine(backupFile, $"{key}_jellyfin.db");
 0158        File.Copy(path, backupFile);
 0159        return Task.FromResult(key);
 160    }
 161
 162    /// <inheritdoc />
 163    public Task RestoreBackupFast(string key, CancellationToken cancellationToken)
 164    {
 165        // ensure there are absolutely no dangling Sqlite connections.
 0166        SqliteConnection.ClearAllPools();
 0167        var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
 0168        var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
 169
 0170        if (!File.Exists(backupFile))
 171        {
 0172            _logger.LogCritical("Tried to restore a backup that does not exist: {Key}", key);
 0173            return Task.CompletedTask;
 174        }
 175
 0176        File.Copy(backupFile, path, true);
 0177        return Task.CompletedTask;
 178    }
 179
 180    /// <inheritdoc />
 181    public Task DeleteBackup(string key)
 182    {
 0183        var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
 184
 0185        if (!File.Exists(backupFile))
 186        {
 0187            _logger.LogCritical("Tried to delete a backup that does not exist: {Key}", key);
 0188            return Task.CompletedTask;
 189        }
 190
 0191        File.Delete(backupFile);
 0192        return Task.CompletedTask;
 193    }
 194
 195    /// <inheritdoc/>
 196    public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames)
 197    {
 0198        ArgumentNullException.ThrowIfNull(tableNames);
 199
 0200        var deleteQueries = new List<string>();
 0201        foreach (var tableName in tableNames)
 202        {
 0203            deleteQueries.Add($"DELETE FROM \"{tableName}\";");
 204        }
 205
 0206        var deleteAllQuery =
 0207        $"""
 0208        PRAGMA foreign_keys = OFF;
 0209        {string.Join('\n', deleteQueries)}
 0210        PRAGMA foreign_keys = ON;
 0211        """;
 212
 0213        await dbContext.Database.ExecuteSqlRawAsync(deleteAllQuery).ConfigureAwait(false);
 0214    }
 215}