From 2d4d6d8c3883fdf095a9762c29ad6ecd9c936ca8 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:56:57 -0500 Subject: [PATCH] Implement PostgreSqlDatabaseProvider methods and DI registration (#6) * Initial plan * feat: implement PostgreSqlDatabaseProvider all methods + DI registration Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com> --- .../Extensions/ServiceCollectionExtensions.cs | 2 + .../Jellyfin.Server.Implementations.csproj | 1 + .../PostgreSqlDatabaseProvider.cs | 50 ++++++++++++++++--- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index 932f9d625..75ac3f921 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ using System.Reflection; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.DbConfiguration; using Jellyfin.Database.Implementations.Locking; +using Jellyfin.Database.Providers.PostgreSQL; using Jellyfin.Database.Providers.Sqlite; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; @@ -24,6 +25,7 @@ public static class ServiceCollectionExtensions private static IEnumerable DatabaseProviderTypes() { yield return typeof(SqliteDatabaseProvider); + yield return typeof(PostgreSqlDatabaseProvider); } private static IDictionary GetSupportedDbProviders() diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 6693ab8db..88c1e1833 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -36,6 +36,7 @@ + diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDatabaseProvider.cs index 7b9301c43..1c919d228 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDatabaseProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations; @@ -14,13 +15,28 @@ namespace Jellyfin.Database.Providers.PostgreSQL; [JellyfinDatabaseProviderKey("Jellyfin-PostgreSQL")] public sealed class PostgreSqlDatabaseProvider : IJellyfinDatabaseProvider { + private const string BackupNotSupportedMessage = + "Automated migration backups are not supported for PostgreSQL. Use the jellyfin-pg-backup CronJob for nightly S3 backups."; + /// public IDbContextFactory? DbContextFactory { get; set; } /// public void Initialise(DbContextOptionsBuilder options, DatabaseConfigurationOptions databaseConfiguration) { - throw new NotImplementedException(); + var connectionString = + Environment.GetEnvironmentVariable("POSTGRES_CONNECTION_STRING") + ?? databaseConfiguration.CustomProviderOptions?.Options + ?.FirstOrDefault(o => o.Key.Equals("ConnectionString", StringComparison.OrdinalIgnoreCase)) + ?.Value + ?? databaseConfiguration.CustomProviderOptions?.ConnectionString + ?? throw new InvalidOperationException( + "No PostgreSQL connection string found. Set the POSTGRES_CONNECTION_STRING environment variable, " + + "or provide it via CustomProviderOptions.Options[\"ConnectionString\"] or CustomProviderOptions.ConnectionString."); + + options.UseNpgsql( + connectionString, + o => o.MigrationsAssembly(GetType().Assembly.FullName)); } /// @@ -34,9 +50,13 @@ public sealed class PostgreSqlDatabaseProvider : IJellyfinDatabaseProvider } /// - public Task RunScheduledOptimisation(CancellationToken cancellationToken) + public async Task RunScheduledOptimisation(CancellationToken cancellationToken) { - return Task.CompletedTask; + var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) + { + await context.Database.ExecuteSqlRawAsync("ANALYZE", cancellationToken).ConfigureAwait(false); + } } /// @@ -48,24 +68,38 @@ public sealed class PostgreSqlDatabaseProvider : IJellyfinDatabaseProvider /// public Task MigrationBackupFast(CancellationToken cancellationToken) { - throw new NotImplementedException(); + throw new NotSupportedException(BackupNotSupportedMessage); } /// public Task RestoreBackupFast(string key, CancellationToken cancellationToken) { - throw new NotImplementedException(); + throw new NotSupportedException(BackupNotSupportedMessage); } /// public Task DeleteBackup(string key) { - throw new NotImplementedException(); + throw new NotSupportedException(BackupNotSupportedMessage); } /// - public Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable? tableNames) + public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable? tableNames) { - throw new NotImplementedException(); + ArgumentNullException.ThrowIfNull(tableNames); + + await dbContext.Database.ExecuteSqlRawAsync("SET session_replication_role = 'replica'").ConfigureAwait(false); + try + { + foreach (var tableName in tableNames) + { + var truncateSql = "TRUNCATE TABLE \"" + tableName + "\" CASCADE"; + await dbContext.Database.ExecuteSqlRawAsync(truncateSql).ConfigureAwait(false); + } + } + finally + { + await dbContext.Database.ExecuteSqlRawAsync("SET session_replication_role = 'origin'").ConfigureAwait(false); + } } }