using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; namespace Jellyfin.Server.Implementations.Migrations; /// /// Plain-SQL tracker for the EF migrations history table. /// Replaces the EF-internal IHistoryRepository / SnakeCaseHistoryRepository. /// public static class MigrationTracker { /// /// Entry name used when writing/reading migration history from backup archives. /// Kept as "HistoryRow" for backward compatibility with existing backup files. /// 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) ) """; /// Ensures the migrations history table exists. /// The database context. /// Cancellation token. /// A representing the operation. public static async Task EnsureTableExistsAsync(JellyfinDbContext dbContext, CancellationToken ct = default) => await dbContext.Database.ExecuteSqlRawAsync(CreateTableSql, ct).ConfigureAwait(false); /// Returns the migration_id of every applied migration. /// The database context. /// Cancellation token. /// A read-only list of applied migration identifiers. public static async Task> GetAppliedMigrationIdsAsync(JellyfinDbContext dbContext, CancellationToken ct = default) => await dbContext.Database .SqlQueryRaw("""SELECT migration_id AS "Value" FROM public."__EFMigrationsHistory" """) .ToListAsync(ct).ConfigureAwait(false); /// Returns all applied migration rows. /// The database context. /// Cancellation token. /// A read-only list of instances. public static async Task> GetAppliedMigrationsAsync(JellyfinDbContext dbContext, CancellationToken ct = default) => await dbContext.Database .SqlQueryRaw(""" SELECT migration_id AS "MigrationId", product_version AS "ProductVersion" FROM public."__EFMigrationsHistory" """) .ToListAsync(ct).ConfigureAwait(false); /// Inserts a migration row, ignoring conflicts. /// The database context. /// The migration identifier. /// The product version. /// Cancellation token. /// A representing the operation. 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); /// Deletes a migration row by id. /// The database context. /// The migration identifier to delete. /// Cancellation token. /// A representing the operation. 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); }