Files

81 lines
4.2 KiB
C#

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