Add PostgreSQL provider and EF Core 10 support

Introduce Jellyfin.Database.Providers.Postgres project, enabling PostgreSQL as a database backend using Entity Framework Core 10 and Npgsql. Add custom value converters and model builder extensions for PostgreSQL compatibility. Update solution-wide dependencies to Microsoft.EntityFrameworkCore 10.0.3 and Npgsql.EntityFrameworkCore.PostgreSQL 9.0.2 (as a temporary workaround until 10.x is released). Regenerate all relevant NuGet, build, and cache files. Add new and updated binaries, debug symbols, and XML documentation for affected projects. No application source code changes outside of the new provider and supporting infrastructure.
This commit is contained in:
2026-02-20 16:52:36 -05:00
parent ae274053a8
commit fd03e1f564
261 changed files with 1678 additions and 107 deletions
@@ -0,0 +1,177 @@
// <copyright file="PostgresDatabaseProvider.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Database.Providers.Postgres;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.DbConfiguration;
using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Npgsql;
/// <summary>
/// Configures Jellyfin to use a PostgreSQL database.
/// </summary>
[JellyfinDatabaseProviderKey("Jellyfin-PostgreSQL")]
public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
{
private readonly IApplicationPaths applicationPaths;
private readonly ILogger<PostgresDatabaseProvider> logger;
/// <summary>
/// Initializes a new instance of the <see cref="PostgresDatabaseProvider"/> class.
/// </summary>
/// <param name="applicationPaths">Service to construct the fallback when the old data path configuration is used.</param>
/// <param name="logger">A logger.</param>
public PostgresDatabaseProvider(IApplicationPaths applicationPaths, ILogger<PostgresDatabaseProvider> logger)
{
this.applicationPaths = applicationPaths;
this.logger = logger;
}
/// <inheritdoc/>
public IDbContextFactory<JellyfinDbContext>? DbContextFactory { get; set; }
/// <inheritdoc/>
public void Initialise(DbContextOptionsBuilder options, DatabaseConfigurationOptions databaseConfiguration)
{
static T? GetOption<T>(ICollection<CustomDatabaseOption>? options, string key, Func<string, T> converter, Func<T>? defaultValue = null)
{
if (options is null)
{
return defaultValue is not null ? defaultValue() : default;
}
var value = options.FirstOrDefault(e => e.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
if (value is null)
{
return defaultValue is not null ? defaultValue() : default;
}
return converter(value.Value);
}
var customOptions = databaseConfiguration.CustomProviderOptions?.Options;
var connectionBuilder = new NpgsqlConnectionStringBuilder
{
Host = GetOption(customOptions, "host", e => e, () => "localhost")!,
Port = GetOption(customOptions, "port", int.Parse, () => 5432),
Database = GetOption(customOptions, "database", e => e, () => "jellyfin")!,
Username = GetOption(customOptions, "username", e => e, () => "jellyfin")!,
Password = GetOption(customOptions, "password", e => e, () => string.Empty)!,
Pooling = GetOption(customOptions, "pooling", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), () => true),
CommandTimeout = GetOption(customOptions, "command-timeout", int.Parse, () => 30),
Timeout = GetOption(customOptions, "connection-timeout", int.Parse, () => 15)
};
var connectionString = connectionBuilder.ToString();
// Log PostgreSQL connection parameters (without password)
logger.LogInformation(
"PostgreSQL connection: Host={Host}, Port={Port}, Database={Database}, Username={Username}, Pooling={Pooling}",
connectionBuilder.Host,
connectionBuilder.Port,
connectionBuilder.Database,
connectionBuilder.Username,
connectionBuilder.Pooling);
options
.UseNpgsql(
connectionString,
npgsqlOptions => npgsqlOptions.MigrationsAssembly(GetType().Assembly.FullName));
var enableSensitiveDataLogging = GetOption(customOptions, "EnableSensitiveDataLogging", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), () => false);
if (enableSensitiveDataLogging)
{
options.EnableSensitiveDataLogging(enableSensitiveDataLogging);
logger.LogInformation("EnableSensitiveDataLogging is enabled on PostgreSQL connection");
}
}
/// <inheritdoc/>
public async Task RunScheduledOptimisation(CancellationToken cancellationToken)
{
var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
// Run PostgreSQL optimization commands
await context.Database.ExecuteSqlRawAsync("VACUUM ANALYZE", cancellationToken).ConfigureAwait(false);
logger.LogInformation("PostgreSQL database optimized successfully!");
}
}
/// <inheritdoc/>
public void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
}
/// <inheritdoc/>
public Task RunShutdownTask(CancellationToken cancellationToken)
{
if (DbContextFactory is null)
{
return Task.CompletedTask;
}
// Clear connection pools
NpgsqlConnection.ClearAllPools();
return Task.CompletedTask;
}
/// <inheritdoc/>
public void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
// PostgreSQL-specific conventions can be added here if needed
}
/// <inheritdoc />
public async Task<string> MigrationBackupFast(CancellationToken cancellationToken)
{
var key = DateTime.UtcNow.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
logger.LogWarning("Fast backup is not implemented for PostgreSQL. Use pg_dump for proper backups.");
// Return a key for compatibility, but actual backup should be done externally
return await Task.FromResult(key).ConfigureAwait(false);
}
/// <inheritdoc />
public Task RestoreBackupFast(string key, CancellationToken cancellationToken)
{
logger.LogWarning("Fast restore is not implemented for PostgreSQL. Use pg_restore for proper restoration.");
return Task.CompletedTask;
}
/// <inheritdoc />
public Task DeleteBackup(string key)
{
logger.LogWarning("Backup deletion is not implemented for PostgreSQL. Manage backups externally.");
return Task.CompletedTask;
}
/// <inheritdoc/>
public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames)
{
ArgumentNullException.ThrowIfNull(tableNames);
var deleteQueries = new List<string>();
foreach (var tableName in tableNames)
{
deleteQueries.Add($"TRUNCATE TABLE \"{tableName}\" CASCADE;");
}
var deleteAllQuery = string.Join('\n', deleteQueries);
await dbContext.Database.ExecuteSqlRawAsync(deleteAllQuery).ConfigureAwait(false);
}
}