feat: implement migration tracking and history management for PostgreSQL

This commit is contained in:
2026-05-04 12:47:12 -04:00
parent e67c191843
commit 4ccd84342b
12 changed files with 191 additions and 192 deletions
@@ -15,13 +15,13 @@ using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Server.Implementations.Migrations;
using Jellyfin.Server.Implementations.StorageHelpers;
using Jellyfin.Server.Implementations.SystemBackupService;
using MediaBrowser.Controller;
using MediaBrowser.Controller.SystemBackupService;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -156,34 +156,31 @@ public class BackupService : IBackupService
await using (dbContext.ConfigureAwait(false))
{
// restore migration history manually
var historyEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{nameof(HistoryRow)}.json")));
var historyEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{MigrationTracker.BackupEntryName}.json")));
if (historyEntry is null)
{
_logger.LogInformation("No backup of the history table in archive. This is required for Jellyfin operation");
throw new InvalidOperationException("Cannot restore backup that has no History data.");
}
HistoryRow[] historyEntries;
MigrationRecord[] historyEntries;
var historyArchive = await historyEntry.OpenAsync().ConfigureAwait(false);
await using (historyArchive.ConfigureAwait(false))
{
historyEntries = await JsonSerializer.DeserializeAsync<HistoryRow[]>(historyArchive).ConfigureAwait(false) ??
historyEntries = await JsonSerializer.DeserializeAsync<MigrationRecord[]>(historyArchive).ConfigureAwait(false) ??
throw new InvalidOperationException("Cannot restore backup that has no History data.");
}
var historyRepository = dbContext.GetService<IHistoryRepository>();
await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
await MigrationTracker.EnsureTableExistsAsync(dbContext).ConfigureAwait(false);
foreach (var item in await historyRepository.GetAppliedMigrationsAsync(CancellationToken.None).ConfigureAwait(false))
foreach (var item in await MigrationTracker.GetAppliedMigrationIdsAsync(dbContext, CancellationToken.None).ConfigureAwait(false))
{
var insertScript = historyRepository.GetDeleteScript(item.MigrationId);
await dbContext.Database.ExecuteSqlRawAsync(insertScript).ConfigureAwait(false);
await MigrationTracker.DeleteAsync(dbContext, item).ConfigureAwait(false);
}
foreach (var item in historyEntries)
{
var insertScript = historyRepository.GetInsertScript(item);
await dbContext.Database.ExecuteSqlRawAsync(insertScript).ConfigureAwait(false);
await MigrationTracker.InsertAsync(dbContext, item.MigrationId, item.ProductVersion).ConfigureAwait(false);
}
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
@@ -310,8 +307,7 @@ public class BackupService : IBackupService
}
// include the migration history as well
var historyRepository = dbContext.GetService<IHistoryRepository>();
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
var migrations = await MigrationTracker.GetAppliedMigrationsAsync(dbContext).ConfigureAwait(false);
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes =
[
@@ -319,7 +315,7 @@ public class BackupService : IBackupService
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
(Type: typeof(MigrationRecord), SourceName: MigrationTracker.BackupEntryName, ValueFactory: () => migrations.ToAsyncEnumerable())
];
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();