Files
unkin-agent 2e3d24cc05
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
fix(db): restore the PostgreSQL upgrade path
The v12.0 rebase replaced the PostgreSQL provider's initial migration instead of
adding to it, so an existing database kept the pre-12.0 schema while the code
migrations ran against it and startup aborted on a missing NormalizedUsername.

- restore 20260305010333_InitialPostgreSql as the baseline
- add 20260306000000_UpgradeToServer12Schema carrying it to the 12.0 model
- convert OwnerId and PrimaryVersionId to uuid with explicit casts, clear
  unparseable ids and repoint dangling owners at the placeholder item
- drop orphaned permissions and preferences before UserId becomes non-nullable
- add 20260524120336_AddUniqueNormalizedUsernameIndex after the code migration
  that fills the column in
- read an empty or unknown encoding.xml EncoderPreset as the default
- cover both paths against a real PostgreSQL and guard the migration ordering
2026-09-12 15:32:12 +10:00

81 lines
3.7 KiB
C#

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;
/// <summary>
/// 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.
/// </summary>
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<string> PostgreSqlMigrationIds()
{
const string DummyConnectionString = "Host=localhost;Database=jellyfin;Username=postgres;Password=postgres";
using var dataSource = new NpgsqlDataSourceBuilder(DummyConnectionString).Build();
var optionsBuilder = new DbContextOptionsBuilder<JellyfinDbContext>();
var provider = new PostgreSqlDatabaseProvider(dataSource);
provider.Initialise(optionsBuilder, new DatabaseConfigurationOptions { DatabaseType = "PostgreSQL" });
using var context = new JellyfinDbContext(
optionsBuilder.Options,
NullLogger<JellyfinDbContext>.Instance,
provider,
new NoLockBehavior(NullLogger<NoLockBehavior>.Instance));
return [.. context.GetService<IMigrationsAssembly>().Migrations.Keys.OrderBy(id => id, StringComparer.Ordinal)];
}
private static IReadOnlyList<string> CoreInitialisationCodeMigrationIds()
{
return [.. typeof(JellyfinMigrationAttribute).Assembly.GetTypes()
.Select(type => (Type: type, Metadata: type.GetCustomAttribute<JellyfinMigrationAttribute>()))
.Where(candidate => candidate.Metadata?.Stage == JellyfinMigrationStageTypes.CoreInitialisation)
.Select(candidate => new CodeMigration(candidate.Type, candidate.Metadata!, null).BuildCodeMigrationId())
.OrderBy(id => id, StringComparer.Ordinal)];
}
}