using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.DbConfiguration; using Jellyfin.Database.Implementations.Locking; using Jellyfin.Database.Providers.PostgreSQL; using Jellyfin.Server.Migrations; using Jellyfin.Server.Migrations.Stages; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.Logging.Abstractions; using Npgsql; using Xunit; namespace Jellyfin.Server.Tests.Migrations; /// /// Schema migrations and the code migrations of the CoreInitialisation stage are sorted into one list by id, so the /// ids the PostgreSQL provider ships decide which schema those code migrations see. /// public class PostgreSqlMigrationOrderingTests { private const string UsernameCodeMigrationId = "20260522092304_UpdateNormalizedUsername"; private const string UsernameIndexMigrationId = "20260524120336_AddUniqueNormalizedUsernameIndex"; [Fact] public void SchemaMigrations_AreOrderedBeforeTheCodeMigrationsThatNeedThem() { var schemaMigrationIds = PostgreSqlMigrationIds().Where(id => !string.Equals(id, UsernameIndexMigrationId, StringComparison.Ordinal)); foreach (var codeMigrationId in CoreInitialisationCodeMigrationIds()) { foreach (var schemaMigrationId in schemaMigrationIds) { Assert.True( string.CompareOrdinal(schemaMigrationId, codeMigrationId) < 0, $"Schema migration {schemaMigrationId} has to be ordered before code migration {codeMigrationId}, " + "otherwise the code migration runs against a schema that predates it."); } } } [Fact] public void UniqueUsernameIndex_IsOrderedAfterTheUsernameCodeMigration() { Assert.Contains(UsernameIndexMigrationId, PostgreSqlMigrationIds()); Assert.Contains(UsernameCodeMigrationId, CoreInitialisationCodeMigrationIds()); Assert.True( string.CompareOrdinal(UsernameIndexMigrationId, UsernameCodeMigrationId) > 0, "The unique index can only be created once the code migration has filled in the normalized usernames."); } private static IReadOnlyList PostgreSqlMigrationIds() { const string DummyConnectionString = "Host=localhost;Database=jellyfin;Username=postgres;Password=postgres"; using var dataSource = new NpgsqlDataSourceBuilder(DummyConnectionString).Build(); var optionsBuilder = new DbContextOptionsBuilder(); var provider = new PostgreSqlDatabaseProvider(dataSource); provider.Initialise(optionsBuilder, new DatabaseConfigurationOptions { DatabaseType = "PostgreSQL" }); using var context = new JellyfinDbContext( optionsBuilder.Options, NullLogger.Instance, provider, new NoLockBehavior(NullLogger.Instance)); return [.. context.GetService().Migrations.Keys.OrderBy(id => id, StringComparer.Ordinal)]; } private static IReadOnlyList CoreInitialisationCodeMigrationIds() { return [.. typeof(JellyfinMigrationAttribute).Assembly.GetTypes() .Select(type => (Type: type, Metadata: type.GetCustomAttribute())) .Where(candidate => candidate.Metadata?.Stage == JellyfinMigrationStageTypes.CoreInitialisation) .Select(candidate => new CodeMigration(candidate.Type, candidate.Metadata!, null).BuildCodeMigrationId()) .OrderBy(id => id, StringComparer.Ordinal)]; } }