< 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
41%
Covered lines: 36
Uncovered lines: 50
Coverable lines: 86
Total lines: 211
Line coverage: 41.8%
Branch coverage
31%
Covered branches: 5
Total branches: 16
Branch coverage: 31.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 58.9% (33/56) Branch coverage: 41.6% (5/12) Total lines: 2084/19/2026 - 12:14:27 AM Line coverage: 39.7% (33/83) Branch coverage: 31.2% (5/16) Total lines: 2085/4/2026 - 12:15:16 AM Line coverage: 41.8% (36/86) Branch coverage: 31.2% (5/16) Total lines: 211 1/23/2026 - 12:11:06 AM Line coverage: 58.9% (33/56) Branch coverage: 41.6% (5/12) Total lines: 2084/19/2026 - 12:14:27 AM Line coverage: 39.7% (33/83) Branch coverage: 31.2% (5/16) Total lines: 2085/4/2026 - 12:15:16 AM Line coverage: 41.8% (36/86) Branch coverage: 31.2% (5/16) Total lines: 211

Coverage delta

Coverage delta 20 -20

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Initialise(...)62.5%8893.54%
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    {
 4335        _applicationPaths = applicationPaths;
 4336        _logger = logger;
 4337    }
 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
 4261        var customOptions = databaseConfiguration.CustomProviderOptions?.Options;
 62
 4263        var sqliteConnectionBuilder = new SqliteConnectionStringBuilder
 4264        {
 4265            DataSource = GetOption(customOptions, "path", e => e, () => Path.Combine(_applicationPaths.DataPath, "jellyf
 4266            Cache = GetOption(customOptions, "cache", Enum.Parse<SqliteCacheMode>, () => SqliteCacheMode.Default),
 4267            Pooling = GetOption(customOptions, "pooling", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreC
 4268            DefaultTimeout = GetOption(customOptions, "command-timeout", int.Parse, () => 60)
 4269        };
 70
 4271        var connectionString = sqliteConnectionBuilder.ToString();
 72
 73        // Log SQLite connection parameters
 4274        _logger.LogInformation("SQLite connection string: {ConnectionString}", connectionString);
 75
 4276        options
 4277            .UseSqlite(
 4278                connectionString,
 4279                sqLiteOptions => sqLiteOptions.MigrationsAssembly(GetType().Assembly))
 4280            // TODO: Remove when https://github.com/dotnet/efcore/pull/35873 is merged & released
 4281            .ConfigureWarnings(warnings =>
 4282                warnings.Ignore(RelationalEventId.NonTransactionalMigrationOperationWarning)
 4283                    .Ignore(RelationalEventId.MultipleCollectionIncludeWarning))
 4284            .AddInterceptors(new PragmaConnectionInterceptor(
 4285                _logger,
 4286                GetOption<int?>(customOptions, "cacheSize", e => int.Parse(e, CultureInfo.InvariantCulture)),
 4287                GetOption(customOptions, "lockingmode", e => e, () => "NORMAL")!,
 4288                GetOption(customOptions, "journalsizelimit", int.Parse, () => 134_217_728),
 4289                GetOption(customOptions, "tempstoremode", int.Parse, () => 2),
 4290                GetOption(customOptions, "syncmode", int.Parse, () => 1),
 4291                customOptions?.Where(e => e.Key.StartsWith("#PRAGMA:", StringComparison.OrdinalIgnoreCase)).ToDictionary
 92
 4293        var enableSensitiveDataLogging = GetOption(customOptions, "EnableSensitiveDataLogging", e => e.Equals(bool.TrueS
 4294        if (enableSensitiveDataLogging)
 95        {
 096            options.EnableSensitiveDataLogging(enableSensitiveDataLogging);
 097            _logger.LogInformation("EnableSensitiveDataLogging is enabled on SQLite connection");
 98        }
 4299    }
 100
 101    /// <inheritdoc/>
 102    public async Task RunScheduledOptimisation(CancellationToken cancellationToken)
 103    {
 0104        var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 0105        await using (context.ConfigureAwait(false))
 106        {
 0107            await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwa
 0108            await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
 0109            await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false);
 0110            await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwa
 0111            _logger.LogInformation("jellyfin.db optimized successfully!");
 112        }
 0113    }
 114
 115    /// <inheritdoc/>
 116    public void OnModelCreating(ModelBuilder modelBuilder)
 117    {
 2118        modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
 2119    }
 120
 121    /// <inheritdoc/>
 122    public async Task RunShutdownTask(CancellationToken cancellationToken)
 123    {
 0124        if (DbContextFactory is null)
 125        {
 0126            return;
 127        }
 128
 129        // Run before disposing the application
 0130        var context = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 0131        await using (context.ConfigureAwait(false))
 132        {
 0133            await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
 134        }
 135
 0136        SqliteConnection.ClearAllPools();
 0137    }
 138
 139    /// <inheritdoc/>
 140    public void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
 141    {
 2142        configurationBuilder.Conventions.Add(_ => new DoNotUseReturningClauseConvention());
 2143    }
 144
 145    /// <inheritdoc />
 146    public Task<string> MigrationBackupFast(CancellationToken cancellationToken)
 147    {
 0148        var key = DateTime.UtcNow.ToString("yyyyMMddhhmmss", CultureInfo.InvariantCulture);
 0149        var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
 0150        var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName);
 0151        Directory.CreateDirectory(backupFile);
 152
 0153        backupFile = Path.Combine(backupFile, $"{key}_jellyfin.db");
 0154        File.Copy(path, backupFile);
 0155        return Task.FromResult(key);
 156    }
 157
 158    /// <inheritdoc />
 159    public Task RestoreBackupFast(string key, CancellationToken cancellationToken)
 160    {
 161        // ensure there are absolutely no dangling Sqlite connections.
 0162        SqliteConnection.ClearAllPools();
 0163        var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
 0164        var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
 165
 0166        if (!File.Exists(backupFile))
 167        {
 0168            _logger.LogCritical("Tried to restore a backup that does not exist: {Key}", key);
 0169            return Task.CompletedTask;
 170        }
 171
 0172        File.Copy(backupFile, path, true);
 0173        return Task.CompletedTask;
 174    }
 175
 176    /// <inheritdoc />
 177    public Task DeleteBackup(string key)
 178    {
 0179        var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
 180
 0181        if (!File.Exists(backupFile))
 182        {
 0183            _logger.LogCritical("Tried to delete a backup that does not exist: {Key}", key);
 0184            return Task.CompletedTask;
 185        }
 186
 0187        File.Delete(backupFile);
 0188        return Task.CompletedTask;
 189    }
 190
 191    /// <inheritdoc/>
 192    public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames)
 193    {
 0194        ArgumentNullException.ThrowIfNull(tableNames);
 195
 0196        var deleteQueries = new List<string>();
 0197        foreach (var tableName in tableNames)
 198        {
 0199            deleteQueries.Add($"DELETE FROM \"{tableName}\";");
 200        }
 201
 0202        var deleteAllQuery =
 0203        $"""
 0204        PRAGMA foreign_keys = OFF;
 0205        {string.Join('\n', deleteQueries)}
 0206        PRAGMA foreign_keys = ON;
 0207        """;
 208
 0209        await dbContext.Database.ExecuteSqlRawAsync(deleteAllQuery).ConfigureAwait(false);
 0210    }
 211}