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)];
}
}