feat: implement migration tracking and history management for PostgreSQL
This commit is contained in:
@@ -1223,29 +1223,26 @@ namespace Emby.Server.Implementations.Library
|
|||||||
private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
|
private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var tasks = PostScanTasks.ToList();
|
var tasks = PostScanTasks.ToList();
|
||||||
|
|
||||||
var numComplete = 0;
|
|
||||||
var numTasks = tasks.Count;
|
var numTasks = tasks.Count;
|
||||||
|
|
||||||
foreach (var task in tasks)
|
if (numTasks == 0)
|
||||||
{
|
{
|
||||||
// Prevent access to modified closure
|
_itemRepository.UpdateInheritedValues();
|
||||||
var currentNumComplete = numComplete;
|
progress.Report(100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressValues = new double[numTasks];
|
||||||
|
|
||||||
|
async Task RunOneTask(ILibraryPostScanTask task, int taskIndex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Running post-scan task {0}", task.GetType().Name);
|
||||||
var innerProgress = new Progress<double>(pct =>
|
var innerProgress = new Progress<double>(pct =>
|
||||||
{
|
{
|
||||||
double innerPercent = pct;
|
progressValues[taskIndex] = pct;
|
||||||
innerPercent /= 100;
|
progress.Report(progressValues.Average());
|
||||||
innerPercent += currentNumComplete;
|
|
||||||
|
|
||||||
innerPercent /= numTasks;
|
|
||||||
innerPercent *= 100;
|
|
||||||
|
|
||||||
progress.Report(innerPercent);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
_logger.LogDebug("Running post-scan task {0}", task.GetType().Name);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
|
await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -1260,12 +1257,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
_logger.LogError(ex, "Error running post-scan task");
|
_logger.LogError(ex, "Error running post-scan task");
|
||||||
}
|
}
|
||||||
|
|
||||||
numComplete++;
|
progressValues[taskIndex] = 100;
|
||||||
double percent = numComplete;
|
progress.Report(progressValues.Average());
|
||||||
percent /= numTasks;
|
|
||||||
progress.Report(percent * 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks.Select((task, i) => RunOneTask(task, i))).ConfigureAwait(false);
|
||||||
|
|
||||||
_itemRepository.UpdateInheritedValues();
|
_itemRepository.UpdateInheritedValues();
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
@@ -1997,7 +1994,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
|
public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_itemRepository.SaveItems(items, cancellationToken);
|
_itemRepository.SaveItemsAsync(items, cancellationToken).GetAwaiter().GetResult();
|
||||||
|
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
@@ -2165,7 +2162,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
item.DateLastSaved = DateTime.UtcNow;
|
item.DateLastSaved = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
_itemRepository.SaveItems(items, cancellationToken);
|
await _itemRepository.SaveItemsAsync(items, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (parent is Folder folder)
|
if (parent is Folder folder)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ using System.Text.Json.Serialization;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Database.Implementations;
|
using Jellyfin.Database.Implementations;
|
||||||
|
using Jellyfin.Server.Implementations.Migrations;
|
||||||
using Jellyfin.Server.Implementations.StorageHelpers;
|
using Jellyfin.Server.Implementations.StorageHelpers;
|
||||||
using Jellyfin.Server.Implementations.SystemBackupService;
|
using Jellyfin.Server.Implementations.SystemBackupService;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.SystemBackupService;
|
using MediaBrowser.Controller.SystemBackupService;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -156,34 +156,31 @@ public class BackupService : IBackupService
|
|||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
// restore migration history manually
|
// 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)
|
if (historyEntry is null)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("No backup of the history table in archive. This is required for Jellyfin operation");
|
_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.");
|
throw new InvalidOperationException("Cannot restore backup that has no History data.");
|
||||||
}
|
}
|
||||||
|
|
||||||
HistoryRow[] historyEntries;
|
MigrationRecord[] historyEntries;
|
||||||
var historyArchive = await historyEntry.OpenAsync().ConfigureAwait(false);
|
var historyArchive = await historyEntry.OpenAsync().ConfigureAwait(false);
|
||||||
await using (historyArchive.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.");
|
throw new InvalidOperationException("Cannot restore backup that has no History data.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
await MigrationTracker.EnsureTableExistsAsync(dbContext).ConfigureAwait(false);
|
||||||
await historyRepository.CreateIfNotExistsAsync().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 MigrationTracker.DeleteAsync(dbContext, item).ConfigureAwait(false);
|
||||||
await dbContext.Database.ExecuteSqlRawAsync(insertScript).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var item in historyEntries)
|
foreach (var item in historyEntries)
|
||||||
{
|
{
|
||||||
var insertScript = historyRepository.GetInsertScript(item);
|
await MigrationTracker.InsertAsync(dbContext, item.MigrationId, item.ProductVersion).ConfigureAwait(false);
|
||||||
await dbContext.Database.ExecuteSqlRawAsync(insertScript).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||||
@@ -310,8 +307,7 @@ public class BackupService : IBackupService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// include the migration history as well
|
// include the migration history as well
|
||||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
var migrations = await MigrationTracker.GetAppliedMigrationsAsync(dbContext).ConfigureAwait(false);
|
||||||
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes =
|
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)
|
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
|
||||||
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
|
.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)!)))),
|
.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();
|
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
|
||||||
|
|
||||||
|
|||||||
@@ -1157,18 +1157,17 @@ public sealed class BaseItemRepository
|
|||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UPSERT all providers using raw SQL with ON CONFLICT
|
// UPSERT all providers in a single UNNEST-based INSERT instead of one round-trip per provider
|
||||||
// This is atomic and handles concurrent operations correctly
|
var providerItemIds = providersToUpsert.Select(_ => entity.Id).ToArray();
|
||||||
foreach (var provider in providersToUpsert)
|
var providerIds = providersToUpsert.Select(p => p.ProviderId).ToArray();
|
||||||
{
|
var providerValues = providersToUpsert.Select(p => p.ProviderValue).ToArray();
|
||||||
await context.Database.ExecuteSqlAsync(
|
await context.Database.ExecuteSqlAsync(
|
||||||
$@"INSERT INTO library.base_item_providers (item_id, provider_id, provider_value)
|
$@"INSERT INTO library.base_item_providers (item_id, provider_id, provider_value)
|
||||||
VALUES ({entity.Id}, {provider.ProviderId}, {provider.ProviderValue})
|
SELECT * FROM UNNEST({providerItemIds}, {providerIds}, {providerValues})
|
||||||
ON CONFLICT (item_id, provider_id)
|
ON CONFLICT (item_id, provider_id)
|
||||||
DO UPDATE SET provider_value = EXCLUDED.provider_value",
|
DO UPDATE SET provider_value = EXCLUDED.provider_value",
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
|
||||||
|
|
||||||
// CRITICAL: Clear the navigation property so EF Core doesn't try to track/save these
|
// CRITICAL: Clear the navigation property so EF Core doesn't try to track/save these
|
||||||
entity.Provider = null;
|
entity.Provider = null;
|
||||||
@@ -1291,7 +1290,6 @@ public sealed class BaseItemRepository
|
|||||||
Value = f.Value
|
Value = f.Value
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
context.ItemValues.AddRange(missingItemValues);
|
context.ItemValues.AddRange(missingItemValues);
|
||||||
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
|
var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
|
||||||
var valueMap = itemValueMaps
|
var valueMap = itemValueMaps
|
||||||
@@ -1332,19 +1330,36 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Hoist ancestor queries outside the per-item loop to avoid N×2 DB round-trips
|
||||||
|
var ancestorItemIds = tuples
|
||||||
|
.Where(t => t.Item.SupportsAncestors && t.AncestorIds != null)
|
||||||
|
.Select(t => t.Item.Id)
|
||||||
|
.ToArray();
|
||||||
|
var allNeededAncestorIds = tuples
|
||||||
|
.Where(t => t.Item.SupportsAncestors && t.AncestorIds != null)
|
||||||
|
.SelectMany(t => t.AncestorIds!)
|
||||||
|
.Distinct()
|
||||||
|
.ToArray();
|
||||||
|
var allExistingAncestorIds = ancestorItemIds.Length > 0
|
||||||
|
? await context.AncestorIds
|
||||||
|
.Where(e => ancestorItemIds.Contains(e.ItemId))
|
||||||
|
.ToListAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false)
|
||||||
|
: [];
|
||||||
|
var validAncestorIdSet = allNeededAncestorIds.Length > 0
|
||||||
|
? (await context.BaseItems
|
||||||
|
.Where(e => allNeededAncestorIds.Contains(e.Id))
|
||||||
|
.Select(f => f.Id)
|
||||||
|
.ToArrayAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false)).ToHashSet()
|
||||||
|
: [];
|
||||||
|
|
||||||
foreach (var item in tuples)
|
foreach (var item in tuples)
|
||||||
{
|
{
|
||||||
if (item.Item.SupportsAncestors && item.AncestorIds != null)
|
if (item.Item.SupportsAncestors && item.AncestorIds != null)
|
||||||
{
|
{
|
||||||
var existingAncestorIds = await context.AncestorIds
|
var existingAncestorIds = allExistingAncestorIds.Where(e => e.ItemId == item.Item.Id).ToList();
|
||||||
.Where(e => e.ItemId == item.Item.Id)
|
var validAncestorIds = item.AncestorIds.Where(validAncestorIdSet.Contains).ToArray();
|
||||||
.ToListAsync(cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
var validAncestorIds = await context.BaseItems
|
|
||||||
.Where(e => item.AncestorIds.Contains(e.Id))
|
|
||||||
.Select(f => f.Id)
|
|
||||||
.ToArrayAsync(cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
foreach (var ancestorId in validAncestorIds)
|
foreach (var ancestorId in validAncestorIds)
|
||||||
{
|
{
|
||||||
var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);
|
var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// <copyright file="MigrationRecord.cs" company="PlaceholderCompany">
|
||||||
|
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||||
|
// </copyright>
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Implementations.Migrations;
|
||||||
|
|
||||||
|
/// <summary>Represents one row of the EF migrations history table.</summary>
|
||||||
|
public sealed class MigrationRecord
|
||||||
|
{
|
||||||
|
/// <summary>Gets or sets the migration identifier.</summary>
|
||||||
|
public string MigrationId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the product version that applied this migration.</summary>
|
||||||
|
public string ProductVersion { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Database.Implementations;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Implementations.Migrations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plain-SQL tracker for the EF migrations history table.
|
||||||
|
/// Replaces the EF-internal IHistoryRepository / SnakeCaseHistoryRepository.
|
||||||
|
/// </summary>
|
||||||
|
public static class MigrationTracker
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Entry name used when writing/reading migration history from backup archives.
|
||||||
|
/// Kept as "HistoryRow" for backward compatibility with existing backup files.
|
||||||
|
/// </summary>
|
||||||
|
public const string BackupEntryName = "HistoryRow";
|
||||||
|
|
||||||
|
private const string CreateTableSql = """
|
||||||
|
CREATE TABLE IF NOT EXISTS public."__EFMigrationsHistory" (
|
||||||
|
migration_id character varying(150) NOT NULL,
|
||||||
|
product_version character varying(32) NOT NULL,
|
||||||
|
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY (migration_id)
|
||||||
|
)
|
||||||
|
""";
|
||||||
|
|
||||||
|
/// <summary>Ensures the migrations history table exists.</summary>
|
||||||
|
/// <param name="dbContext">The database context.</param>
|
||||||
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the operation.</returns>
|
||||||
|
public static async Task EnsureTableExistsAsync(JellyfinDbContext dbContext, CancellationToken ct = default)
|
||||||
|
=> await dbContext.Database.ExecuteSqlRawAsync(CreateTableSql, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
/// <summary>Returns the migration_id of every applied migration.</summary>
|
||||||
|
/// <param name="dbContext">The database context.</param>
|
||||||
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
/// <returns>A read-only list of applied migration identifiers.</returns>
|
||||||
|
public static async Task<IReadOnlyList<string>> GetAppliedMigrationIdsAsync(JellyfinDbContext dbContext, CancellationToken ct = default)
|
||||||
|
=> await dbContext.Database
|
||||||
|
.SqlQueryRaw<string>("""SELECT migration_id AS "Value" FROM public."__EFMigrationsHistory" """)
|
||||||
|
.ToListAsync(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
/// <summary>Returns all applied migration rows.</summary>
|
||||||
|
/// <param name="dbContext">The database context.</param>
|
||||||
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
/// <returns>A read-only list of <see cref="MigrationRecord"/> instances.</returns>
|
||||||
|
public static async Task<IReadOnlyList<MigrationRecord>> GetAppliedMigrationsAsync(JellyfinDbContext dbContext, CancellationToken ct = default)
|
||||||
|
=> await dbContext.Database
|
||||||
|
.SqlQueryRaw<MigrationRecord>("""
|
||||||
|
SELECT migration_id AS "MigrationId", product_version AS "ProductVersion"
|
||||||
|
FROM public."__EFMigrationsHistory"
|
||||||
|
""")
|
||||||
|
.ToListAsync(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
/// <summary>Inserts a migration row, ignoring conflicts.</summary>
|
||||||
|
/// <param name="dbContext">The database context.</param>
|
||||||
|
/// <param name="migrationId">The migration identifier.</param>
|
||||||
|
/// <param name="productVersion">The product version.</param>
|
||||||
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the operation.</returns>
|
||||||
|
public static async Task InsertAsync(JellyfinDbContext dbContext, string migrationId, string productVersion, CancellationToken ct = default)
|
||||||
|
=> await dbContext.Database.ExecuteSqlAsync(
|
||||||
|
$"""
|
||||||
|
INSERT INTO public."__EFMigrationsHistory" (migration_id, product_version)
|
||||||
|
VALUES ({migrationId}, {productVersion}) ON CONFLICT DO NOTHING
|
||||||
|
""",
|
||||||
|
ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
/// <summary>Deletes a migration row by id.</summary>
|
||||||
|
/// <param name="dbContext">The database context.</param>
|
||||||
|
/// <param name="migrationId">The migration identifier to delete.</param>
|
||||||
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the operation.</returns>
|
||||||
|
public static async Task DeleteAsync(JellyfinDbContext dbContext, string migrationId, CancellationToken ct = default)
|
||||||
|
=> await dbContext.Database.ExecuteSqlAsync(
|
||||||
|
$"""DELETE FROM public."__EFMigrationsHistory" WHERE migration_id = {migrationId}""",
|
||||||
|
ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Server.Implementations.Serialization;
|
using Emby.Server.Implementations.Serialization;
|
||||||
using Jellyfin.Database.Implementations;
|
using Jellyfin.Database.Implementations;
|
||||||
|
using Jellyfin.Server.Implementations.Migrations;
|
||||||
using Jellyfin.Server.Implementations.SystemBackupService;
|
using Jellyfin.Server.Implementations.SystemBackupService;
|
||||||
using Jellyfin.Server.Migrations.Stages;
|
using Jellyfin.Server.Migrations.Stages;
|
||||||
using Jellyfin.Server.ServerSetupApp;
|
using Jellyfin.Server.ServerSetupApp;
|
||||||
@@ -24,7 +25,6 @@ using MediaBrowser.Controller.SystemBackupService;
|
|||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage;
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -123,18 +123,15 @@ internal class JellyfinMigrationService
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
await MigrationTracker.EnsureTableExistsAsync(dbContext).ConfigureAwait(false);
|
||||||
|
var appliedMigrations = await MigrationTracker.GetAppliedMigrationIdsAsync(dbContext).ConfigureAwait(false);
|
||||||
await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
|
var migrationsToSeed = flatApplyMigrations
|
||||||
var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
|
||||||
var startupScripts = flatApplyMigrations
|
|
||||||
.Where(e => !appliedMigrations.Any(f => f != e.BuildCodeMigrationId()))
|
.Where(e => !appliedMigrations.Any(f => f != e.BuildCodeMigrationId()))
|
||||||
.Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.BuildCodeMigrationId(), GetJellyfinVersion()))))
|
|
||||||
.ToArray();
|
.ToArray();
|
||||||
foreach (var item in startupScripts)
|
foreach (var item in migrationsToSeed)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Seed migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
|
logger.LogInformation("Seed migration {Key}-{Name}.", item.Metadata.Key, item.Metadata.Name);
|
||||||
await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
|
await MigrationTracker.InsertAsync(dbContext, item.BuildCodeMigrationId(), GetJellyfinVersion()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,8 +152,8 @@ internal class JellyfinMigrationService
|
|||||||
var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
await MigrationTracker.EnsureTableExistsAsync(dbContext).ConfigureAwait(false);
|
||||||
var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
var appliedMigrations = await MigrationTracker.GetAppliedMigrationIdsAsync(dbContext).ConfigureAwait(false);
|
||||||
var lastOldAppliedMigration = Migrations
|
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.
|
.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)))
|
.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.
|
// 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 oldMigrations)
|
||||||
foreach (var item in startupScripts)
|
|
||||||
{
|
{
|
||||||
logger.LogInformation("Migrate migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
|
logger.LogInformation("Migrate migration {Key}-{Name}.", item.Metadata.Key, item.Metadata.Name);
|
||||||
await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
|
await MigrationTracker.InsertAsync(dbContext, item.BuildCodeMigrationId(), GetJellyfinVersion()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogInformation("Rename old migration.xml to migration.xml.backup");
|
logger.LogInformation("Rename old migration.xml to migration.xml.backup");
|
||||||
@@ -250,51 +246,14 @@ internal class JellyfinMigrationService
|
|||||||
logger.LogInformation("Empty database created successfully");
|
logger.LogInformation("Empty database created successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
await MigrationTracker.EnsureTableExistsAsync(dbContext).ConfigureAwait(false);
|
||||||
|
var appliedMigrations = await MigrationTracker.GetAppliedMigrationIdsAsync(dbContext).ConfigureAwait(false);
|
||||||
// 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);
|
|
||||||
var pendingCodeMigrations = migrationStage
|
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)))
|
.Select(e => (Key: e.BuildCodeMigrationId(), Migration: new InternalCodeMigration(e, serviceProvider, dbContext)))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
(string Key, InternalDatabaseMigration Migration)[] pendingDatabaseMigrations = [];
|
(string Key, IInternalMigration Migration)[] pendingMigrations = [.. pendingCodeMigrations];
|
||||||
|
|
||||||
// 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];
|
|
||||||
logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingMigrations.Length, stage);
|
logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingMigrations.Length, stage);
|
||||||
var migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
|
var migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
|
||||||
|
|
||||||
@@ -415,16 +374,15 @@ internal class JellyfinMigrationService
|
|||||||
{
|
{
|
||||||
logger.LogInformation("Prepare system for possible migrations");
|
logger.LogInformation("Prepare system for possible migrations");
|
||||||
JellyfinMigrationBackupAttribute backupInstruction;
|
JellyfinMigrationBackupAttribute backupInstruction;
|
||||||
IReadOnlyList<HistoryRow> appliedMigrations;
|
IReadOnlyList<string> appliedMigrations;
|
||||||
var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
await MigrationTracker.EnsureTableExistsAsync(dbContext).ConfigureAwait(false);
|
||||||
var migrationsAssembly = dbContext.GetService<IMigrationsAssembly>();
|
appliedMigrations = await MigrationTracker.GetAppliedMigrationIdsAsync(dbContext).ConfigureAwait(false);
|
||||||
appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
|
||||||
backupInstruction = new JellyfinMigrationBackupAttribute()
|
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;
|
bool isSqliteProvider = false;
|
||||||
|
|
||||||
backupInstruction = Migrations.SelectMany(e => e)
|
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
|
.Where(e => isSqliteProvider || !e.Metadata.RequiresSqlite) // Skip SQLite migrations for non-SQLite providers
|
||||||
.Select(e => e.BackupRequirements)
|
.Select(e => e.BackupRequirements)
|
||||||
.Where(e => e is not null)
|
.Where(e => e is not null)
|
||||||
@@ -525,27 +483,8 @@ internal class JellyfinMigrationService
|
|||||||
{
|
{
|
||||||
await _codeMigration.Perform(_serviceProvider, logger, CancellationToken.None).ConfigureAwait(false);
|
await _codeMigration.Perform(_serviceProvider, logger, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
var historyRepository = _dbContext.GetService<IHistoryRepository>();
|
await MigrationTracker.InsertAsync(_dbContext, _codeMigration.BuildCodeMigrationId(), GetJellyfinVersion()).ConfigureAwait(false);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -553,18 +553,6 @@ Global
|
|||||||
{8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Release|x64.Build.0 = Release|Any CPU
|
{8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Release|x86.ActiveCfg = Release|Any CPU
|
{8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Release|x86.Build.0 = Release|Any CPU
|
{8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{8C9F9221-8415-496C-B1F5-E7756F03FA59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{8C9F9221-8415-496C-B1F5-E7756F03FA59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{8C9F9221-8415-496C-B1F5-E7756F03FA59}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8C9F9221-8415-496C-B1F5-E7756F03FA59}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{8C9F9221-8415-496C-B1F5-E7756F03FA59}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{8C9F9221-8415-496C-B1F5-E7756F03FA59}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
@@ -630,7 +618,6 @@ Global
|
|||||||
{C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
|
{C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
|
||||||
{8C6B2B13-58A4-4506-9DAB-1F882A093FE0} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
|
{8C6B2B13-58A4-4506-9DAB-1F882A093FE0} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
|
||||||
{4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
|
{4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
|
||||||
{A5590358-33CC-4B39-BDE7-DC62FEB03C76} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}
|
|
||||||
{8C9F9221-8415-496C-B1F5-E7756F03FA59} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}
|
{8C9F9221-8415-496C-B1F5-E7756F03FA59} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}
|
||||||
{11643D0F-6761-4EF7-AB71-6F9F8DE00714} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
|
{11643D0F-6761-4EF7-AB71-6F9F8DE00714} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
|
||||||
{E1A314DC-837D-4472-AC15-EC08947C55B7} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}
|
{E1A314DC-837D-4472-AC15-EC08947C55B7} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}
|
||||||
|
|||||||
@@ -420,6 +420,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
// Create a list for our validated children
|
// Create a list for our validated children
|
||||||
var newItems = new List<BaseItem>();
|
var newItems = new List<BaseItem>();
|
||||||
|
var changedItems = new List<BaseItem>();
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
@@ -436,7 +437,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None)
|
if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None)
|
||||||
{
|
{
|
||||||
await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
changedItems.Add(currentChild);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -484,6 +485,11 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
LibraryManager.CreateItems(newItems, this, cancellationToken);
|
LibraryManager.CreateItems(newItems, this, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changedItems.Count > 0)
|
||||||
|
{
|
||||||
|
await LibraryManager.UpdateItemsAsync(changedItems, this, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
// After removing items, reattach any detached user data to remaining children
|
// After removing items, reattach any detached user data to remaining children
|
||||||
// that share the same user data keys (eg. same episode replaced with a new file).
|
// that share the same user data keys (eg. same episode replaced with a new file).
|
||||||
if (actuallyRemoved.Count > 0)
|
if (actuallyRemoved.Count > 0)
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
|||||||
if (fanoutConcurrency <= 0)
|
if (fanoutConcurrency <= 0)
|
||||||
{
|
{
|
||||||
// in case the user did not set a limit manually, we can assume he has 3 or more cores as already checked by ShouldForceSequentialOperation.
|
// in case the user did not set a limit manually, we can assume he has 3 or more cores as already checked by ShouldForceSequentialOperation.
|
||||||
return Environment.ProcessorCount - 3;
|
return Math.Max(2, Environment.ProcessorCount / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fanoutConcurrency;
|
return fanoutConcurrency;
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ public sealed class BulkOperationTransaction : IAsyncDisposable, IDisposable
|
|||||||
_transaction?.Dispose();
|
_transaction?.Dispose();
|
||||||
_transaction = null;
|
_transaction = null;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -134,7 +133,6 @@ public sealed class BulkOperationTransaction : IAsyncDisposable, IDisposable
|
|||||||
|
|
||||||
_transaction = null;
|
_transaction = null;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -289,7 +289,6 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
|
|||||||
maxRetryDelay: TimeSpan.FromSeconds(5),
|
maxRetryDelay: TimeSpan.FromSeconds(5),
|
||||||
errorCodesToAdd: new[] { "57P01" }); // Add "terminating connection" to retryable errors
|
errorCodesToAdd: new[] { "57P01" }); // Add "terminating connection" to retryable errors
|
||||||
})
|
})
|
||||||
.ReplaceService<Microsoft.EntityFrameworkCore.Migrations.IHistoryRepository, SnakeCaseHistoryRepository>()
|
|
||||||
.ConfigureWarnings(warnings =>
|
.ConfigureWarnings(warnings =>
|
||||||
warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));
|
warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));
|
||||||
|
|
||||||
|
|||||||
-33
@@ -1,33 +0,0 @@
|
|||||||
// <copyright file="SnakeCaseHistoryRepository.cs" company="PlaceholderCompany">
|
|
||||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
|
||||||
// </copyright>
|
|
||||||
|
|
||||||
#pragma warning disable EF1001 // NpgsqlHistoryRepository is an internal EF Core infrastructure type; intentional use in this PostgreSQL fork.
|
|
||||||
|
|
||||||
namespace Jellyfin.Database.Providers.Postgres;
|
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Internal;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A custom history repository that uses snake_case column names for the
|
|
||||||
/// <c>__EFMigrationsHistory</c> table, matching the PostgreSQL naming convention
|
|
||||||
/// used throughout this fork.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class SnakeCaseHistoryRepository : NpgsqlHistoryRepository
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="SnakeCaseHistoryRepository"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dependencies">Repository dependencies injected by EF Core.</param>
|
|
||||||
public SnakeCaseHistoryRepository(HistoryRepositoryDependencies dependencies)
|
|
||||||
: base(dependencies)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
protected override string MigrationIdColumnName => "migration_id";
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
protected override string ProductVersionColumnName => "product_version";
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user