From a5f8421aa888543b64761e9b496f5baf309bbb5c Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Sat, 4 Apr 2026 20:44:26 +0200 Subject: [PATCH] Split SQLite rebuild migration from roles change --- README.md | 2 +- RpgRoller.Tests/HostingCoverageTests.cs | 147 +++++++++- RpgRoller/Hosting/SqliteSchemaUpgrader.cs | 120 +++++--- ...zationRolesAndCampaignDeletion.Designer.cs | 5 - ...ddAuthorizationRolesAndCampaignDeletion.cs | 12 - ...26170000_AddAuthorizationRoles.Designer.cs | 258 ++++++++++++++++++ .../20260226170000_AddAuthorizationRoles.cs | 30 ++ 7 files changed, 513 insertions(+), 61 deletions(-) create mode 100644 RpgRoller/Migrations/20260226170000_AddAuthorizationRoles.Designer.cs create mode 100644 RpgRoller/Migrations/20260226170000_AddAuthorizationRoles.cs diff --git a/README.md b/README.md index 8b0b9b3..fba7c64 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ For migration authoring, use the local tool command form: dotnet dotnet-ef migrations add --project RpgRoller/RpgRoller.csproj --startup-project RpgRoller/RpgRoller.csproj ``` -For SQLite migrations, avoid `migrationBuilder.Sql(...)` in the same migration step that rebuilds a table for schema changes; prefer column defaults or a follow-up migration so EF Core does not warn about raw SQL executing while a rebuilt table is still pending. +For SQLite migrations, keep table-rebuild operations isolated from unrelated schema/data changes. If a migration needs both a rebuild-triggering change (for example `AlterColumn`) and another independent operation, split them into separate migrations so EF Core does not emit non-transactional migration warnings. ## Frontend Runtime diff --git a/RpgRoller.Tests/HostingCoverageTests.cs b/RpgRoller.Tests/HostingCoverageTests.cs index 18f98ef..bf36d05 100644 --- a/RpgRoller.Tests/HostingCoverageTests.cs +++ b/RpgRoller.Tests/HostingCoverageTests.cs @@ -199,6 +199,11 @@ public sealed class HostingCoverageTests var rolesHistoryCount = Convert.ToInt32(rolesHistoryCommand.ExecuteScalar()); Assert.Equal(1, rolesHistoryCount); + using var authorizationRolesHistoryCommand = verifyConnection.CreateCommand(); + authorizationRolesHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226170000_AddAuthorizationRoles';"; + var authorizationRolesHistoryCount = Convert.ToInt32(authorizationRolesHistoryCommand.ExecuteScalar()); + Assert.Equal(1, authorizationRolesHistoryCount); + using var rolemasterHistoryCommand = verifyConnection.CreateCommand(); rolemasterHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260402222501_AddRolemasterFumbleRange';"; var rolemasterHistoryCount = Convert.ToInt32(rolemasterHistoryCommand.ExecuteScalar()); @@ -206,20 +211,147 @@ public sealed class HostingCoverageTests } [Fact] - public void AuthorizationRolesAndCampaignDeletion_MigrationScript_DoesNotMixUsersSqlIntoCharactersRebuild() + public void AuthorizationMigrations_SplitCharactersRebuildFromRolesColumnAddition() { var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-migration-script-{Guid.NewGuid():N}.db"); var options = new DbContextOptionsBuilder().UseSqlite($"Data Source={dbPath}").Options; using var db = new RpgRollerDbContext(options); var migrator = db.GetService(); - var script = migrator.GenerateScript( + var charactersScript = migrator.GenerateScript( fromMigration: "20260226131003_AddSkillGroupPrototypes", toMigration: "20260226160859_AddAuthorizationRolesAndCampaignDeletion"); + var rolesScript = migrator.GenerateScript( + fromMigration: "20260226160859_AddAuthorizationRolesAndCampaignDeletion", + toMigration: "20260226170000_AddAuthorizationRoles"); - Assert.Contains("""ALTER TABLE "Users" ADD "Roles" TEXT NOT NULL DEFAULT 'admin';""", script); - Assert.Contains("""CREATE TABLE "ef_temp_Characters" (""", script); - Assert.DoesNotContain("UPDATE Users", script); + Assert.Contains("""CREATE TABLE "ef_temp_Characters" (""", charactersScript); + Assert.DoesNotContain("""ALTER TABLE "Users" ADD "Roles" TEXT NOT NULL DEFAULT 'admin';""", charactersScript); + Assert.Contains("""ALTER TABLE "Users" ADD "Roles" TEXT NOT NULL DEFAULT 'admin';""", rolesScript); + Assert.DoesNotContain("""CREATE TABLE "ef_temp_Characters" (""", rolesScript); + } + + [Fact] + public void SqliteSchemaUpgrader_BackfillsSplitAuthorizationRolesMigrationHistory() + { + var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-split-history-{Guid.NewGuid():N}.db"); + + using (var connection = new SqliteConnection($"Data Source={dbPath}")) + { + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = """ + CREATE TABLE "__EFMigrationsHistory" ( + "MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY, + "ProductVersion" TEXT NOT NULL + ); + + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES + ('20260226084000_InitialSchema', '10.0.2'), + ('20260226090000_ModelSync', '10.0.2'), + ('20260226100000_AddRollLogDice', '10.0.2'), + ('20260226124941_AddSkillGroupsAndCharacterOwnerTransfer', '10.0.2'), + ('20260226131003_AddSkillGroupPrototypes', '10.0.2'), + ('20260226160859_AddAuthorizationRolesAndCampaignDeletion', '10.0.2'); + + CREATE TABLE "Users" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY, + "Username" TEXT NOT NULL, + "UsernameNormalized" TEXT NOT NULL, + "PasswordHash" TEXT NOT NULL, + "DisplayName" TEXT NOT NULL, + "ActiveCharacterId" TEXT NULL, + "Roles" TEXT NOT NULL DEFAULT 'admin' + ); + + CREATE TABLE "Sessions" ( + "Token" TEXT NOT NULL CONSTRAINT "PK_Sessions" PRIMARY KEY, + "UserId" TEXT NOT NULL, + "CreatedAtUtc" TEXT NOT NULL + ); + + CREATE TABLE "Campaigns" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_Campaigns" PRIMARY KEY, + "GmUserId" TEXT NOT NULL, + "Name" TEXT NOT NULL, + "Ruleset" TEXT NOT NULL, + "Version" INTEGER NOT NULL + ); + + CREATE TABLE "Characters" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_Characters" PRIMARY KEY, + "OwnerUserId" TEXT NOT NULL, + "CampaignId" TEXT NULL, + "Name" TEXT NOT NULL + ); + + CREATE INDEX "IX_Characters_OwnerUserId" ON "Characters" ("OwnerUserId"); + CREATE INDEX "IX_Characters_CampaignId" ON "Characters" ("CampaignId"); + + CREATE TABLE "SkillGroups" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_SkillGroups" PRIMARY KEY, + "CharacterId" TEXT NOT NULL, + "Name" TEXT NOT NULL, + "AllowFumble" INTEGER NOT NULL, + "DiceRollDefinition" TEXT NOT NULL, + "WildDice" INTEGER NOT NULL + ); + + CREATE INDEX "IX_SkillGroups_CharacterId" ON "SkillGroups" ("CharacterId"); + + CREATE TABLE "Skills" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_Skills" PRIMARY KEY, + "CharacterId" TEXT NOT NULL, + "Name" TEXT NOT NULL, + "DiceRollDefinition" TEXT NOT NULL, + "WildDice" INTEGER NOT NULL, + "AllowFumble" INTEGER NOT NULL, + "SkillGroupId" TEXT NULL + ); + + CREATE INDEX "IX_Skills_CharacterId" ON "Skills" ("CharacterId"); + CREATE INDEX "IX_Skills_SkillGroupId" ON "Skills" ("SkillGroupId"); + + CREATE TABLE "RollLogEntries" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_RollLogEntries" PRIMARY KEY, + "CampaignId" TEXT NOT NULL, + "CharacterId" TEXT NOT NULL, + "SkillId" TEXT NOT NULL, + "RollerUserId" TEXT NOT NULL, + "Visibility" TEXT NOT NULL, + "Result" INTEGER NOT NULL, + "Breakdown" TEXT NOT NULL, + "TimestampUtc" TEXT NOT NULL, + "Dice" TEXT NOT NULL + ); + + CREATE INDEX "IX_RollLogEntries_CampaignId" ON "RollLogEntries" ("CampaignId"); + CREATE INDEX "IX_RollLogEntries_CharacterId" ON "RollLogEntries" ("CharacterId"); + CREATE INDEX "IX_RollLogEntries_RollerUserId" ON "RollLogEntries" ("RollerUserId"); + CREATE INDEX "IX_RollLogEntries_SkillId" ON "RollLogEntries" ("SkillId"); + """; + _ = command.ExecuteNonQuery(); + } + + var options = new DbContextOptionsBuilder().UseSqlite($"Data Source={dbPath}").Options; + using (var db = new RpgRollerDbContext(options)) + { + SqliteSchemaUpgrader.ApplyPendingChanges(db); + } + + using var verifyConnection = new SqliteConnection($"Data Source={dbPath}"); + verifyConnection.Open(); + + using var authorizationRolesHistoryCommand = verifyConnection.CreateCommand(); + authorizationRolesHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226170000_AddAuthorizationRoles';"; + var authorizationRolesHistoryCount = Convert.ToInt32(authorizationRolesHistoryCommand.ExecuteScalar()); + Assert.Equal(1, authorizationRolesHistoryCount); + + using var rolemasterHistoryCommand = verifyConnection.CreateCommand(); + rolemasterHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260402222501_AddRolemasterFumbleRange';"; + var rolemasterHistoryCount = Convert.ToInt32(rolemasterHistoryCommand.ExecuteScalar()); + Assert.Equal(1, rolemasterHistoryCount); } [Fact] @@ -343,5 +475,10 @@ public sealed class HostingCoverageTests skillGroupColumns.Add(skillGroupsTableInfoReader.GetString(1)); Assert.Contains("FumbleRange", skillGroupColumns); + + using var authorizationRolesHistoryCommand = verifyConnection.CreateCommand(); + authorizationRolesHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226170000_AddAuthorizationRoles';"; + var authorizationRolesHistoryCount = Convert.ToInt32(authorizationRolesHistoryCommand.ExecuteScalar()); + Assert.Equal(1, authorizationRolesHistoryCount); } } diff --git a/RpgRoller/Hosting/SqliteSchemaUpgrader.cs b/RpgRoller/Hosting/SqliteSchemaUpgrader.cs index 03ad517..a69e642 100644 --- a/RpgRoller/Hosting/SqliteSchemaUpgrader.cs +++ b/RpgRoller/Hosting/SqliteSchemaUpgrader.cs @@ -8,56 +8,60 @@ public static class SqliteSchemaUpgrader public static void ApplyPendingChanges(RpgRollerDbContext db) { if (db.Database.IsSqlite()) - EnsureLegacySchemaHistory(db); + { + db.Database.OpenConnection(); + try + { + EnsureLegacySchemaHistory(db); + EnsureSplitMigrationHistory(db); + } + finally + { + db.Database.CloseConnection(); + } + } db.Database.Migrate(); } private static void EnsureLegacySchemaHistory(RpgRollerDbContext db) { - db.Database.OpenConnection(); - try - { - if (TableExists(db, "__EFMigrationsHistory")) - return; + if (TableExists(db, "__EFMigrationsHistory")) + return; - if (!TableExists(db, "Skills")) - return; + if (!TableExists(db, "Skills")) + return; - if (!ColumnExists(db, "Skills", "WildDice") || !ColumnExists(db, "Skills", "AllowFumble")) - return; + if (!ColumnExists(db, "Skills", "WildDice") || !ColumnExists(db, "Skills", "AllowFumble")) + return; - using var createHistoryCommand = db.Database.GetDbConnection().CreateCommand(); - createHistoryCommand.CommandText = """ - CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( - "MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY, - "ProductVersion" TEXT NOT NULL - ); - """; - _ = createHistoryCommand.ExecuteNonQuery(); + using var createHistoryCommand = db.Database.GetDbConnection().CreateCommand(); + createHistoryCommand.CommandText = """ + CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( + "MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY, + "ProductVersion" TEXT NOT NULL + ); + """; + _ = createHistoryCommand.ExecuteNonQuery(); - using var insertHistoryCommand = db.Database.GetDbConnection().CreateCommand(); - insertHistoryCommand.CommandText = """ - INSERT OR IGNORE INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") - VALUES ($migrationId, $productVersion); - """; + InsertMigrationHistory(db, InitialMigrationId); + } - var migrationParameter = insertHistoryCommand.CreateParameter(); - migrationParameter.ParameterName = "$migrationId"; - migrationParameter.Value = InitialMigrationId; - insertHistoryCommand.Parameters.Add(migrationParameter); + private static void EnsureSplitMigrationHistory(RpgRollerDbContext db) + { + if (!TableExists(db, "__EFMigrationsHistory")) + return; - var productVersionParameter = insertHistoryCommand.CreateParameter(); - productVersionParameter.ParameterName = "$productVersion"; - productVersionParameter.Value = ProductVersion; - insertHistoryCommand.Parameters.Add(productVersionParameter); + if (!MigrationExists(db, CharactersCampaignDeletionMigrationId)) + return; - _ = insertHistoryCommand.ExecuteNonQuery(); - } - finally - { - db.Database.CloseConnection(); - } + if (MigrationExists(db, AuthorizationRolesMigrationId)) + return; + + if (!ColumnExists(db, "Users", "Roles")) + return; + + InsertMigrationHistory(db, AuthorizationRolesMigrationId); } private static bool TableExists(RpgRollerDbContext db, string tableName) @@ -87,6 +91,46 @@ public static class SqliteSchemaUpgrader return false; } + private static bool MigrationExists(RpgRollerDbContext db, string migrationId) + { + using var command = db.Database.GetDbConnection().CreateCommand(); + command.CommandText = """ + SELECT 1 + FROM "__EFMigrationsHistory" + WHERE "MigrationId" = $migrationId + LIMIT 1; + """; + var migrationParameter = command.CreateParameter(); + migrationParameter.ParameterName = "$migrationId"; + migrationParameter.Value = migrationId; + command.Parameters.Add(migrationParameter); + + return command.ExecuteScalar() is not null; + } + + private static void InsertMigrationHistory(RpgRollerDbContext db, string migrationId) + { + using var insertHistoryCommand = db.Database.GetDbConnection().CreateCommand(); + insertHistoryCommand.CommandText = """ + INSERT OR IGNORE INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ($migrationId, $productVersion); + """; + + var migrationParameter = insertHistoryCommand.CreateParameter(); + migrationParameter.ParameterName = "$migrationId"; + migrationParameter.Value = migrationId; + insertHistoryCommand.Parameters.Add(migrationParameter); + + var productVersionParameter = insertHistoryCommand.CreateParameter(); + productVersionParameter.ParameterName = "$productVersion"; + productVersionParameter.Value = ProductVersion; + insertHistoryCommand.Parameters.Add(productVersionParameter); + + _ = insertHistoryCommand.ExecuteNonQuery(); + } + private const string InitialMigrationId = "20260226084000_InitialSchema"; + private const string CharactersCampaignDeletionMigrationId = "20260226160859_AddAuthorizationRolesAndCampaignDeletion"; + private const string AuthorizationRolesMigrationId = "20260226170000_AddAuthorizationRoles"; private const string ProductVersion = "10.0.2"; -} \ No newline at end of file +} diff --git a/RpgRoller/Migrations/20260226160859_AddAuthorizationRolesAndCampaignDeletion.Designer.cs b/RpgRoller/Migrations/20260226160859_AddAuthorizationRolesAndCampaignDeletion.Designer.cs index 7f774b8..dfd607c 100644 --- a/RpgRoller/Migrations/20260226160859_AddAuthorizationRolesAndCampaignDeletion.Designer.cs +++ b/RpgRoller/Migrations/20260226160859_AddAuthorizationRolesAndCampaignDeletion.Designer.cs @@ -211,11 +211,6 @@ namespace RpgRoller.Migrations .IsRequired() .HasColumnType("TEXT"); - b.Property("Roles") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("TEXT"); - b.Property("Username") .IsRequired() .HasMaxLength(64) diff --git a/RpgRoller/Migrations/20260226160859_AddAuthorizationRolesAndCampaignDeletion.cs b/RpgRoller/Migrations/20260226160859_AddAuthorizationRolesAndCampaignDeletion.cs index 8fee79a..989eb4b 100644 --- a/RpgRoller/Migrations/20260226160859_AddAuthorizationRolesAndCampaignDeletion.cs +++ b/RpgRoller/Migrations/20260226160859_AddAuthorizationRolesAndCampaignDeletion.cs @@ -11,14 +11,6 @@ namespace RpgRoller.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.AddColumn( - name: "Roles", - table: "Users", - type: "TEXT", - maxLength: 256, - nullable: false, - defaultValue: "admin"); - migrationBuilder.AlterColumn( name: "CampaignId", table: "Characters", @@ -31,10 +23,6 @@ namespace RpgRoller.Migrations /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropColumn( - name: "Roles", - table: "Users"); - migrationBuilder.AlterColumn( name: "CampaignId", table: "Characters", diff --git a/RpgRoller/Migrations/20260226170000_AddAuthorizationRoles.Designer.cs b/RpgRoller/Migrations/20260226170000_AddAuthorizationRoles.Designer.cs new file mode 100644 index 0000000..834e106 --- /dev/null +++ b/RpgRoller/Migrations/20260226170000_AddAuthorizationRoles.Designer.cs @@ -0,0 +1,258 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RpgRoller.Data; + +#nullable disable + +namespace RpgRoller.Migrations +{ + [DbContext(typeof(RpgRollerDbContext))] + [Migration("20260226170000_AddAuthorizationRoles")] + partial class AddAuthorizationRoles + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.2"); + + modelBuilder.Entity("RpgRoller.Domain.Campaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("GmUserId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Ruleset") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GmUserId"); + + b.ToTable("Campaigns"); + }); + + modelBuilder.Entity("RpgRoller.Domain.Character", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CampaignId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("OwnerUserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("Characters"); + }); + + modelBuilder.Entity("RpgRoller.Domain.RollLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Breakdown") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("CampaignId") + .HasColumnType("TEXT"); + + b.Property("CharacterId") + .HasColumnType("TEXT"); + + b.Property("Dice") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Result") + .HasColumnType("INTEGER"); + + b.Property("RollerUserId") + .HasColumnType("TEXT"); + + b.Property("SkillId") + .HasColumnType("TEXT"); + + b.Property("TimestampUtc") + .HasColumnType("TEXT"); + + b.Property("Visibility") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("CharacterId"); + + b.HasIndex("RollerUserId"); + + b.HasIndex("SkillId"); + + b.ToTable("RollLogEntries"); + }); + + modelBuilder.Entity("RpgRoller.Domain.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AllowFumble") + .HasColumnType("INTEGER"); + + b.Property("CharacterId") + .HasColumnType("TEXT"); + + b.Property("DiceRollDefinition") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("SkillGroupId") + .HasColumnType("TEXT"); + + b.Property("WildDice") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("SkillGroupId"); + + b.ToTable("Skills"); + }); + + modelBuilder.Entity("RpgRoller.Domain.SkillGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AllowFumble") + .HasColumnType("INTEGER"); + + b.Property("CharacterId") + .HasColumnType("TEXT"); + + b.Property("DiceRollDefinition") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("WildDice") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.ToTable("SkillGroups"); + }); + + modelBuilder.Entity("RpgRoller.Domain.UserAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ActiveCharacterId") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UsernameNormalized") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UsernameNormalized") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RpgRoller.Domain.UserSession", b => + { + b.Property("Token") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAtUtc") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("Sessions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RpgRoller/Migrations/20260226170000_AddAuthorizationRoles.cs b/RpgRoller/Migrations/20260226170000_AddAuthorizationRoles.cs new file mode 100644 index 0000000..d5eb509 --- /dev/null +++ b/RpgRoller/Migrations/20260226170000_AddAuthorizationRoles.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RpgRoller.Migrations +{ + /// + public partial class AddAuthorizationRoles : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Roles", + table: "Users", + type: "TEXT", + maxLength: 256, + nullable: false, + defaultValue: "admin"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Roles", + table: "Users"); + } + } +}