using System; using Microsoft.EntityFrameworkCore; namespace RolemasterDb.App.Data; public static class RolemasterDbSchemaUpgrader { public static async Task EnsureLatestAsync(RolemasterDbContext dbContext, CancellationToken cancellationToken = default) { await EnsureAttackTableFumbleColumnsAsync(dbContext, cancellationToken); await dbContext.Database.ExecuteSqlRawAsync( """ CREATE TABLE IF NOT EXISTS "CriticalBranches" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_CriticalBranches" PRIMARY KEY AUTOINCREMENT, "CriticalResultId" INTEGER NOT NULL, "BranchKind" TEXT NOT NULL, "ConditionKey" TEXT NULL, "ConditionText" TEXT NOT NULL, "ConditionJson" TEXT NOT NULL, "RawText" TEXT NOT NULL, "DescriptionText" TEXT NOT NULL, "RawAffixText" TEXT NULL, "ParsedJson" TEXT NOT NULL, "SortOrder" INTEGER NOT NULL, CONSTRAINT "FK_CriticalBranches_CriticalResults_CriticalResultId" FOREIGN KEY ("CriticalResultId") REFERENCES "CriticalResults" ("Id") ON DELETE CASCADE ); """, cancellationToken); await dbContext.Database.ExecuteSqlRawAsync( """ CREATE INDEX IF NOT EXISTS "IX_CriticalBranches_CriticalResultId" ON "CriticalBranches" ("CriticalResultId"); """, cancellationToken); await dbContext.Database.ExecuteSqlRawAsync( """ CREATE INDEX IF NOT EXISTS "IX_CriticalBranches_CriticalResultId_SortOrder" ON "CriticalBranches" ("CriticalResultId", "SortOrder"); """, cancellationToken); await dbContext.Database.ExecuteSqlRawAsync( """ CREATE TABLE IF NOT EXISTS "CriticalEffects" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_CriticalEffects" PRIMARY KEY AUTOINCREMENT, "CriticalResultId" INTEGER NULL, "CriticalBranchId" INTEGER NULL, "EffectCode" TEXT NOT NULL, "Target" TEXT NULL, "ValueInteger" INTEGER NULL, "ValueDecimal" TEXT NULL, "ValueExpression" TEXT NULL, "DurationRounds" INTEGER NULL, "PerRound" INTEGER NULL, "Modifier" INTEGER NULL, "BodyPart" TEXT NULL, "IsPermanent" INTEGER NOT NULL, "SourceType" TEXT NOT NULL, "SourceText" TEXT NULL, CONSTRAINT "FK_CriticalEffects_CriticalResults_CriticalResultId" FOREIGN KEY ("CriticalResultId") REFERENCES "CriticalResults" ("Id") ON DELETE CASCADE, CONSTRAINT "FK_CriticalEffects_CriticalBranches_CriticalBranchId" FOREIGN KEY ("CriticalBranchId") REFERENCES "CriticalBranches" ("Id") ON DELETE CASCADE ); """, cancellationToken); await dbContext.Database.ExecuteSqlRawAsync( """ CREATE INDEX IF NOT EXISTS "IX_CriticalEffects_EffectCode" ON "CriticalEffects" ("EffectCode"); """, cancellationToken); await dbContext.Database.ExecuteSqlRawAsync( """ CREATE INDEX IF NOT EXISTS "IX_CriticalEffects_CriticalResultId" ON "CriticalEffects" ("CriticalResultId"); """, cancellationToken); await dbContext.Database.ExecuteSqlRawAsync( """ CREATE INDEX IF NOT EXISTS "IX_CriticalEffects_CriticalBranchId" ON "CriticalEffects" ("CriticalBranchId"); """, cancellationToken); } private static async Task EnsureAttackTableFumbleColumnsAsync(RolemasterDbContext dbContext, CancellationToken cancellationToken) { if (!await ColumnExistsAsync(dbContext, "AttackTables", "FumbleMinRoll", cancellationToken)) { await dbContext.Database.ExecuteSqlRawAsync( """ ALTER TABLE "AttackTables" ADD COLUMN "FumbleMinRoll" INTEGER NULL; """, cancellationToken); } if (!await ColumnExistsAsync(dbContext, "AttackTables", "FumbleMaxRoll", cancellationToken)) { await dbContext.Database.ExecuteSqlRawAsync( """ ALTER TABLE "AttackTables" ADD COLUMN "FumbleMaxRoll" INTEGER NULL; """, cancellationToken); } } private static async Task ColumnExistsAsync( RolemasterDbContext dbContext, string tableName, string columnName, CancellationToken cancellationToken) { var connection = dbContext.Database.GetDbConnection(); var shouldClose = connection.State != System.Data.ConnectionState.Open; if (shouldClose) { await connection.OpenAsync(cancellationToken); } try { await using var command = connection.CreateCommand(); command.CommandText = $"PRAGMA table_info(\"{tableName}\");"; await using var reader = await command.ExecuteReaderAsync(cancellationToken); while (await reader.ReadAsync(cancellationToken)) { if (string.Equals(reader["name"]?.ToString(), columnName, StringComparison.Ordinal)) { return true; } } return false; } finally { if (shouldClose) { await connection.CloseAsync(); } } } }