feat: implement migration tracking and history management for PostgreSQL
This commit is contained in:
@@ -16,6 +16,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Serialization;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.Implementations.Migrations;
|
||||
using Jellyfin.Server.Implementations.SystemBackupService;
|
||||
using Jellyfin.Server.Migrations.Stages;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
@@ -24,7 +25,6 @@ using MediaBrowser.Controller.SystemBackupService;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -123,18 +123,15 @@ internal class JellyfinMigrationService
|
||||
}
|
||||
*/
|
||||
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
|
||||
await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
|
||||
var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
var startupScripts = flatApplyMigrations
|
||||
await MigrationTracker.EnsureTableExistsAsync(dbContext).ConfigureAwait(false);
|
||||
var appliedMigrations = await MigrationTracker.GetAppliedMigrationIdsAsync(dbContext).ConfigureAwait(false);
|
||||
var migrationsToSeed = flatApplyMigrations
|
||||
.Where(e => !appliedMigrations.Any(f => f != e.BuildCodeMigrationId()))
|
||||
.Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.BuildCodeMigrationId(), GetJellyfinVersion()))))
|
||||
.ToArray();
|
||||
foreach (var item in startupScripts)
|
||||
foreach (var item in migrationsToSeed)
|
||||
{
|
||||
logger.LogInformation("Seed migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
|
||||
await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
|
||||
logger.LogInformation("Seed migration {Key}-{Name}.", item.Metadata.Key, item.Metadata.Name);
|
||||
await MigrationTracker.InsertAsync(dbContext, item.BuildCodeMigrationId(), GetJellyfinVersion()).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,8 +152,8 @@ internal class JellyfinMigrationService
|
||||
var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
await MigrationTracker.EnsureTableExistsAsync(dbContext).ConfigureAwait(false);
|
||||
var appliedMigrations = await MigrationTracker.GetAppliedMigrationIdsAsync(dbContext).ConfigureAwait(false);
|
||||
var lastOldAppliedMigration = Migrations
|
||||
.SelectMany(e => e.Where(e => e.Metadata.Key is not null)) // only consider migrations that have the key set as its the reference marker for legacy migrations.
|
||||
.Where(e => migrationOptions.Applied.Any(f => f.Id.Equals(e.Metadata.Key!.Value)))
|
||||
@@ -173,11 +170,10 @@ internal class JellyfinMigrationService
|
||||
];
|
||||
// those are all migrations that had to run in the old migration system, even if not noted in the migration.xml file.
|
||||
|
||||
var startupScripts = oldMigrations.Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.BuildCodeMigrationId(), GetJellyfinVersion()))));
|
||||
foreach (var item in startupScripts)
|
||||
foreach (var item in oldMigrations)
|
||||
{
|
||||
logger.LogInformation("Migrate migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
|
||||
await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
|
||||
logger.LogInformation("Migrate migration {Key}-{Name}.", item.Metadata.Key, item.Metadata.Name);
|
||||
await MigrationTracker.InsertAsync(dbContext, item.BuildCodeMigrationId(), GetJellyfinVersion()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
logger.LogInformation("Rename old migration.xml to migration.xml.backup");
|
||||
@@ -250,51 +246,14 @@ internal class JellyfinMigrationService
|
||||
logger.LogInformation("Empty database created successfully");
|
||||
}
|
||||
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
|
||||
// Explicitly ensure the __EFMigrationsHistory table exists
|
||||
await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
|
||||
|
||||
// Fallback: Ensure the migrations history table exists with explicit SQL creation
|
||||
// This handles cases where CreateIfNotExistsAsync doesn't create the table
|
||||
try
|
||||
{
|
||||
var historyTableSql = @"
|
||||
CREATE TABLE IF NOT EXISTS public.__EFMigrationsHistory (
|
||||
MigrationId character varying(150) NOT NULL,
|
||||
ProductVersion character varying(32) NOT NULL,
|
||||
CONSTRAINT PK___EFMigrationsHistory PRIMARY KEY (MigrationId)
|
||||
);
|
||||
";
|
||||
await dbContext.Database.ExecuteSqlRawAsync(historyTableSql).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogDebug(ex, "Error creating __EFMigrationsHistory table with SQL (may already exist): {Message}", ex.Message);
|
||||
// Continue - table may already exist or will be created by EF Core's CreateIfNotExistsAsync
|
||||
}
|
||||
|
||||
var migrationsAssembly = dbContext.GetService<IMigrationsAssembly>();
|
||||
var appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
await MigrationTracker.EnsureTableExistsAsync(dbContext).ConfigureAwait(false);
|
||||
var appliedMigrations = await MigrationTracker.GetAppliedMigrationIdsAsync(dbContext).ConfigureAwait(false);
|
||||
var pendingCodeMigrations = migrationStage
|
||||
.Where(e => appliedMigrations.All(f => f.MigrationId != e.BuildCodeMigrationId()))
|
||||
.Where(e => !appliedMigrations.Contains(e.BuildCodeMigrationId()))
|
||||
.Select(e => (Key: e.BuildCodeMigrationId(), Migration: new InternalCodeMigration(e, serviceProvider, dbContext)))
|
||||
.ToArray();
|
||||
|
||||
(string Key, InternalDatabaseMigration Migration)[] pendingDatabaseMigrations = [];
|
||||
|
||||
// DISABLED for .NET 11 preview: EF Core database migrations don't work with preview SDK
|
||||
// Database schema is initialized from SQL scripts in PostgresDatabaseProvider instead
|
||||
/*
|
||||
if (stage is JellyfinMigrationStageTypes.CoreInitialisation)
|
||||
{
|
||||
pendingDatabaseMigrations = migrationsAssembly.Migrations.Where(f => appliedMigrations.All(e => e.MigrationId != f.Key))
|
||||
.Select(e => (Key: e.Key, Migration: new InternalDatabaseMigration(e, dbContext)))
|
||||
.ToArray();
|
||||
}
|
||||
*/
|
||||
|
||||
(string Key, IInternalMigration Migration)[] pendingMigrations = [.. pendingCodeMigrations, .. pendingDatabaseMigrations];
|
||||
(string Key, IInternalMigration Migration)[] pendingMigrations = [.. pendingCodeMigrations];
|
||||
logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingMigrations.Length, stage);
|
||||
var migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
|
||||
|
||||
@@ -415,16 +374,15 @@ internal class JellyfinMigrationService
|
||||
{
|
||||
logger.LogInformation("Prepare system for possible migrations");
|
||||
JellyfinMigrationBackupAttribute backupInstruction;
|
||||
IReadOnlyList<HistoryRow> appliedMigrations;
|
||||
IReadOnlyList<string> appliedMigrations;
|
||||
var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
var migrationsAssembly = dbContext.GetService<IMigrationsAssembly>();
|
||||
appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
await MigrationTracker.EnsureTableExistsAsync(dbContext).ConfigureAwait(false);
|
||||
appliedMigrations = await MigrationTracker.GetAppliedMigrationIdsAsync(dbContext).ConfigureAwait(false);
|
||||
backupInstruction = new JellyfinMigrationBackupAttribute()
|
||||
{
|
||||
JellyfinDb = migrationsAssembly.Migrations.Any(f => appliedMigrations.All(e => e.MigrationId != f.Key))
|
||||
JellyfinDb = false // Schema migrations are disabled; no EF schema migrations are ever pending
|
||||
};
|
||||
}
|
||||
|
||||
@@ -433,7 +391,7 @@ internal class JellyfinMigrationService
|
||||
bool isSqliteProvider = false;
|
||||
|
||||
backupInstruction = Migrations.SelectMany(e => e)
|
||||
.Where(e => appliedMigrations.All(f => f.MigrationId != e.BuildCodeMigrationId()))
|
||||
.Where(e => !appliedMigrations.Contains(e.BuildCodeMigrationId()))
|
||||
.Where(e => isSqliteProvider || !e.Metadata.RequiresSqlite) // Skip SQLite migrations for non-SQLite providers
|
||||
.Select(e => e.BackupRequirements)
|
||||
.Where(e => e is not null)
|
||||
@@ -525,27 +483,8 @@ internal class JellyfinMigrationService
|
||||
{
|
||||
await _codeMigration.Perform(_serviceProvider, logger, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var historyRepository = _dbContext.GetService<IHistoryRepository>();
|
||||
var createScript = historyRepository.GetInsertScript(new HistoryRow(_codeMigration.BuildCodeMigrationId(), GetJellyfinVersion()));
|
||||
await _dbContext.Database.ExecuteSqlRawAsync(createScript).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private class InternalDatabaseMigration : IInternalMigration
|
||||
{
|
||||
private readonly JellyfinDbContext _jellyfinDbContext;
|
||||
private KeyValuePair<string, TypeInfo> _databaseMigrationInfo;
|
||||
|
||||
public InternalDatabaseMigration(KeyValuePair<string, TypeInfo> databaseMigrationInfo, JellyfinDbContext jellyfinDbContext)
|
||||
{
|
||||
_databaseMigrationInfo = databaseMigrationInfo;
|
||||
_jellyfinDbContext = jellyfinDbContext;
|
||||
}
|
||||
|
||||
public async Task PerformAsync(IStartupLogger logger)
|
||||
{
|
||||
var migrator = _jellyfinDbContext.GetService<IMigrator>();
|
||||
await migrator.MigrateAsync(_databaseMigrationInfo.Key).ConfigureAwait(false);
|
||||
await MigrationTracker.InsertAsync(_dbContext, _codeMigration.BuildCodeMigrationId(), GetJellyfinVersion()).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user