refactor(db): migrate Identity schema

Complete schema ownership by moving all ASP.NET Identity tables to an additive provider-aware migration. Preserve credentials, security state, preferences, roles, claims, external logins, tokens, indexes, and cascades.
This commit is contained in:
cesnimda
2026-08-30 22:02:35 +02:00
parent 90ca23de33
commit 058d13de47
10 changed files with 311 additions and 261 deletions
@@ -0,0 +1,173 @@
using System;
using JobTrackerApi.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace JobTrackerApi.Migrations;
/// <summary>
/// Establishes one migration owner for the ASP.NET Identity aggregate while preserving all
/// existing users, credentials, roles, claims, external logins, and tokens.
/// </summary>
[DbContext(typeof(JobTrackerContext))]
[Migration("20260830134000_AdoptIdentitySchema")]
public sealed class AdoptIdentitySchema : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase)
? MySqlDdl
: SqliteDdl);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Identity data may predate this migration; downgrade must never remove accounts.
}
private const string SqliteDdl = """
CREATE TABLE IF NOT EXISTS "AspNetRoles" (
"Id" TEXT NOT NULL CONSTRAINT "PK_AspNetRoles" PRIMARY KEY,
"Name" TEXT NULL, "NormalizedName" TEXT NULL, "ConcurrencyStamp" TEXT NULL);
CREATE TABLE IF NOT EXISTS "AspNetUsers" (
"Id" TEXT NOT NULL CONSTRAINT "PK_AspNetUsers" PRIMARY KEY,
"UserName" TEXT NULL, "NormalizedUserName" TEXT NULL,
"Email" TEXT NULL, "NormalizedEmail" TEXT NULL, "EmailConfirmed" INTEGER NOT NULL,
"PasswordHash" TEXT NULL, "SecurityStamp" TEXT NULL, "ConcurrencyStamp" TEXT NULL,
"PhoneNumber" TEXT NULL, "PhoneNumberConfirmed" INTEGER NOT NULL,
"TwoFactorEnabled" INTEGER NOT NULL, "LockoutEnd" TEXT NULL,
"LockoutEnabled" INTEGER NOT NULL, "AccessFailedCount" INTEGER NOT NULL,
"FirstName" TEXT NULL, "LastName" TEXT NULL, "DisplayName" TEXT NULL,
"ProfileCvText" TEXT NULL, "ProfileCvStructureJson" TEXT NULL,
"CurrentCvUploadArtifactId" INTEGER NULL, "CurrentCvExtractionRunId" INTEGER NULL,
"CurrentCvProfileVersion" INTEGER NULL, "AvatarImageDataUrl" TEXT NULL,
"GoogleSubject" TEXT NULL, "GoogleEmail" TEXT NULL, "GoogleLinkedAt" TEXT NULL,
"MicrosoftSubject" TEXT NULL, "MicrosoftEmail" TEXT NULL, "MicrosoftLinkedAt" TEXT NULL,
"MicrosoftTenantId" TEXT NULL, "MicrosoftObjectId" TEXT NULL,
"TotpSecretEncrypted" TEXT NULL, "TotpPendingSecretEncrypted" TEXT NULL,
"TotpEnabledAtUtc" TEXT NULL, "PendingEmail" TEXT NULL,
"PendingEmailRequestedAtUtc" TEXT NULL, "AiEnabled" INTEGER NOT NULL DEFAULT 1,
"ExternalAiProcessingAllowed" INTEGER NOT NULL DEFAULT 0,
"StripeCustomerId" TEXT NULL, "StripeSubscriptionId" TEXT NULL,
"StripeSubscriptionStatus" TEXT NULL, "StripeLastEventCreatedUtc" TEXT NULL,
"DeletionRequestedAtUtc" TEXT NULL, "DeletionStatus" TEXT NOT NULL DEFAULT 'active',
"EmailFollowUpRemindersEnabled" INTEGER NOT NULL DEFAULT 1, "UiLanguage" TEXT NULL);
CREATE TABLE IF NOT EXISTS "AspNetRoleClaims" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_AspNetRoleClaims" PRIMARY KEY AUTOINCREMENT,
"RoleId" TEXT NOT NULL, "ClaimType" TEXT NULL, "ClaimValue" TEXT NULL,
CONSTRAINT "FK_AspNetRoleClaims_AspNetRoles_RoleId"
FOREIGN KEY ("RoleId") REFERENCES "AspNetRoles" ("Id") ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS "AspNetUserClaims" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_AspNetUserClaims" PRIMARY KEY AUTOINCREMENT,
"UserId" TEXT NOT NULL, "ClaimType" TEXT NULL, "ClaimValue" TEXT NULL,
CONSTRAINT "FK_AspNetUserClaims_AspNetUsers_UserId"
FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS "AspNetUserLogins" (
"LoginProvider" TEXT NOT NULL, "ProviderKey" TEXT NOT NULL,
"ProviderDisplayName" TEXT NULL, "UserId" TEXT NOT NULL,
CONSTRAINT "PK_AspNetUserLogins" PRIMARY KEY ("LoginProvider", "ProviderKey"),
CONSTRAINT "FK_AspNetUserLogins_AspNetUsers_UserId"
FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS "AspNetUserRoles" (
"UserId" TEXT NOT NULL, "RoleId" TEXT NOT NULL,
CONSTRAINT "PK_AspNetUserRoles" PRIMARY KEY ("UserId", "RoleId"),
CONSTRAINT "FK_AspNetUserRoles_AspNetRoles_RoleId"
FOREIGN KEY ("RoleId") REFERENCES "AspNetRoles" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_AspNetUserRoles_AspNetUsers_UserId"
FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS "AspNetUserTokens" (
"UserId" TEXT NOT NULL, "LoginProvider" TEXT NOT NULL, "Name" TEXT NOT NULL,
"Value" TEXT NULL,
CONSTRAINT "PK_AspNetUserTokens" PRIMARY KEY ("UserId", "LoginProvider", "Name"),
CONSTRAINT "FK_AspNetUserTokens_AspNetUsers_UserId"
FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE);
CREATE UNIQUE INDEX IF NOT EXISTS "RoleNameIndex" ON "AspNetRoles" ("NormalizedName");
CREATE INDEX IF NOT EXISTS "IX_AspNetRoleClaims_RoleId" ON "AspNetRoleClaims" ("RoleId");
CREATE INDEX IF NOT EXISTS "EmailIndex" ON "AspNetUsers" ("NormalizedEmail");
CREATE UNIQUE INDEX IF NOT EXISTS "UserNameIndex" ON "AspNetUsers" ("NormalizedUserName");
CREATE UNIQUE INDEX IF NOT EXISTS "IX_AspNetUsers_MicrosoftTenantId_MicrosoftObjectId"
ON "AspNetUsers" ("MicrosoftTenantId", "MicrosoftObjectId");
CREATE INDEX IF NOT EXISTS "IX_AspNetUserClaims_UserId" ON "AspNetUserClaims" ("UserId");
CREATE INDEX IF NOT EXISTS "IX_AspNetUserLogins_UserId" ON "AspNetUserLogins" ("UserId");
CREATE INDEX IF NOT EXISTS "IX_AspNetUserRoles_RoleId" ON "AspNetUserRoles" ("RoleId");
""";
private const string MySqlDdl = """
CREATE TABLE IF NOT EXISTS `AspNetRoles` (
`Id` varchar(255) NOT NULL, `Name` varchar(256) NULL,
`NormalizedName` varchar(256) NULL, `ConcurrencyStamp` longtext NULL,
PRIMARY KEY (`Id`)) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `AspNetUsers` (
`Id` varchar(255) NOT NULL, `UserName` varchar(256) NULL,
`NormalizedUserName` varchar(256) NULL, `Email` varchar(256) NULL,
`NormalizedEmail` varchar(256) NULL, `EmailConfirmed` tinyint(1) NOT NULL,
`PasswordHash` longtext NULL, `SecurityStamp` longtext NULL,
`ConcurrencyStamp` longtext NULL, `PhoneNumber` longtext NULL,
`PhoneNumberConfirmed` tinyint(1) NOT NULL, `TwoFactorEnabled` tinyint(1) NOT NULL,
`LockoutEnd` datetime(6) NULL, `LockoutEnabled` tinyint(1) NOT NULL,
`AccessFailedCount` int NOT NULL, `FirstName` longtext NULL, `LastName` longtext NULL,
`DisplayName` longtext NULL, `ProfileCvText` longtext NULL,
`ProfileCvStructureJson` longtext NULL, `CurrentCvUploadArtifactId` int NULL,
`CurrentCvExtractionRunId` int NULL, `CurrentCvProfileVersion` int NULL,
`AvatarImageDataUrl` longtext NULL, `GoogleSubject` longtext NULL,
`GoogleEmail` longtext NULL, `GoogleLinkedAt` datetime(6) NULL,
`MicrosoftSubject` longtext NULL, `MicrosoftEmail` longtext NULL,
`MicrosoftLinkedAt` datetime(6) NULL, `MicrosoftTenantId` varchar(36) NULL,
`MicrosoftObjectId` varchar(36) NULL, `TotpSecretEncrypted` longtext NULL,
`TotpPendingSecretEncrypted` longtext NULL, `TotpEnabledAtUtc` datetime(6) NULL,
`PendingEmail` varchar(320) NULL, `PendingEmailRequestedAtUtc` datetime(6) NULL,
`AiEnabled` tinyint(1) NOT NULL DEFAULT 1,
`ExternalAiProcessingAllowed` tinyint(1) NOT NULL DEFAULT 0,
`StripeCustomerId` varchar(255) NULL, `StripeSubscriptionId` varchar(255) NULL,
`StripeSubscriptionStatus` varchar(64) NULL, `StripeLastEventCreatedUtc` datetime(6) NULL,
`DeletionRequestedAtUtc` datetime(6) NULL,
`DeletionStatus` varchar(32) NOT NULL DEFAULT 'active',
`EmailFollowUpRemindersEnabled` tinyint(1) NOT NULL DEFAULT 1,
`UiLanguage` varchar(16) NULL, PRIMARY KEY (`Id`)) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `AspNetRoleClaims` (
`Id` int NOT NULL AUTO_INCREMENT, `RoleId` varchar(255) NOT NULL,
`ClaimType` longtext NULL, `ClaimValue` longtext NULL, PRIMARY KEY (`Id`),
CONSTRAINT `FK_AspNetRoleClaims_AspNetRoles_RoleId`
FOREIGN KEY (`RoleId`) REFERENCES `AspNetRoles` (`Id`) ON DELETE CASCADE)
CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `AspNetUserClaims` (
`Id` int NOT NULL AUTO_INCREMENT, `UserId` varchar(255) NOT NULL,
`ClaimType` longtext NULL, `ClaimValue` longtext NULL, PRIMARY KEY (`Id`),
CONSTRAINT `FK_AspNetUserClaims_AspNetUsers_UserId`
FOREIGN KEY (`UserId`) REFERENCES `AspNetUsers` (`Id`) ON DELETE CASCADE)
CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `AspNetUserLogins` (
`LoginProvider` varchar(255) NOT NULL, `ProviderKey` varchar(255) NOT NULL,
`ProviderDisplayName` longtext NULL, `UserId` varchar(255) NOT NULL,
PRIMARY KEY (`LoginProvider`, `ProviderKey`),
CONSTRAINT `FK_AspNetUserLogins_AspNetUsers_UserId`
FOREIGN KEY (`UserId`) REFERENCES `AspNetUsers` (`Id`) ON DELETE CASCADE)
CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `AspNetUserRoles` (
`UserId` varchar(255) NOT NULL, `RoleId` varchar(255) NOT NULL,
PRIMARY KEY (`UserId`, `RoleId`),
CONSTRAINT `FK_AspNetUserRoles_AspNetRoles_RoleId`
FOREIGN KEY (`RoleId`) REFERENCES `AspNetRoles` (`Id`) ON DELETE CASCADE,
CONSTRAINT `FK_AspNetUserRoles_AspNetUsers_UserId`
FOREIGN KEY (`UserId`) REFERENCES `AspNetUsers` (`Id`) ON DELETE CASCADE)
CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `AspNetUserTokens` (
`UserId` varchar(255) NOT NULL, `LoginProvider` varchar(255) NOT NULL,
`Name` varchar(255) NOT NULL, `Value` longtext NULL,
PRIMARY KEY (`UserId`, `LoginProvider`, `Name`),
CONSTRAINT `FK_AspNetUserTokens_AspNetUsers_UserId`
FOREIGN KEY (`UserId`) REFERENCES `AspNetUsers` (`Id`) ON DELETE CASCADE)
CHARACTER SET=utf8mb4;
CREATE UNIQUE INDEX IF NOT EXISTS `RoleNameIndex` ON `AspNetRoles` (`NormalizedName`);
CREATE INDEX IF NOT EXISTS `IX_AspNetRoleClaims_RoleId` ON `AspNetRoleClaims` (`RoleId`);
CREATE INDEX IF NOT EXISTS `EmailIndex` ON `AspNetUsers` (`NormalizedEmail`);
CREATE UNIQUE INDEX IF NOT EXISTS `UserNameIndex` ON `AspNetUsers` (`NormalizedUserName`);
CREATE UNIQUE INDEX IF NOT EXISTS `IX_AspNetUsers_MicrosoftTenantId_MicrosoftObjectId`
ON `AspNetUsers` (`MicrosoftTenantId`, `MicrosoftObjectId`);
CREATE INDEX IF NOT EXISTS `IX_AspNetUserClaims_UserId` ON `AspNetUserClaims` (`UserId`);
CREATE INDEX IF NOT EXISTS `IX_AspNetUserLogins_UserId` ON `AspNetUserLogins` (`UserId`);
CREATE INDEX IF NOT EXISTS `IX_AspNetUserRoles_RoleId` ON `AspNetUserRoles` (`RoleId`);
""";
}
@@ -267,118 +267,13 @@ public static class StartupInitializationExtensions
var provider = (app.Configuration["Database:Provider"] ?? "sqlite").Trim().ToLowerInvariant();
var useSqliteBootstrap = provider is not "mysql" and not "mariadb";
static void EnsureIdentityTablesMySql(DbConnection c)
{
using var cmd = c.CreateCommand();
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS `AspNetRoles` (
`Id` varchar(255) NOT NULL,
`Name` varchar(256) NULL,
`NormalizedName` varchar(256) NULL,
`ConcurrencyStamp` longtext NULL,
PRIMARY KEY (`Id`)
) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `AspNetUsers` (
`Id` varchar(255) NOT NULL,
`UserName` varchar(256) NULL,
`NormalizedUserName` varchar(256) NULL,
`Email` varchar(256) NULL,
`NormalizedEmail` varchar(256) NULL,
`EmailConfirmed` tinyint(1) NOT NULL,
`PasswordHash` longtext NULL,
`SecurityStamp` longtext NULL,
`ConcurrencyStamp` longtext NULL,
`PhoneNumber` longtext NULL,
`PhoneNumberConfirmed` tinyint(1) NOT NULL,
`TwoFactorEnabled` tinyint(1) NOT NULL,
`LockoutEnd` datetime(6) NULL,
`LockoutEnabled` tinyint(1) NOT NULL,
`AccessFailedCount` int NOT NULL,
`FirstName` longtext NULL,
`LastName` longtext NULL,
`DisplayName` longtext NULL,
`ProfileCvText` longtext NULL,
`ProfileCvStructureJson` longtext NULL,
`CurrentCvUploadArtifactId` int NULL,
`CurrentCvExtractionRunId` int NULL,
`CurrentCvProfileVersion` int NULL,
`AvatarImageDataUrl` longtext NULL,
`GoogleSubject` longtext NULL,
`GoogleEmail` longtext NULL,
`GoogleLinkedAt` datetime(6) NULL,
`MicrosoftSubject` longtext NULL,
`MicrosoftEmail` longtext NULL,
`MicrosoftLinkedAt` datetime(6) NULL,
`TotpSecretEncrypted` longtext NULL,
`TotpPendingSecretEncrypted` longtext NULL,
`TotpEnabledAtUtc` datetime(6) NULL,
PRIMARY KEY (`Id`)
) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `AspNetRoleClaims` (
`Id` int NOT NULL AUTO_INCREMENT,
`RoleId` varchar(255) NOT NULL,
`ClaimType` longtext NULL,
`ClaimValue` longtext NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_AspNetRoleClaims_AspNetRoles_RoleId` FOREIGN KEY (`RoleId`) REFERENCES `AspNetRoles` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `AspNetUserClaims` (
`Id` int NOT NULL AUTO_INCREMENT,
`UserId` varchar(255) NOT NULL,
`ClaimType` longtext NULL,
`ClaimValue` longtext NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_AspNetUserClaims_AspNetUsers_UserId` FOREIGN KEY (`UserId`) REFERENCES `AspNetUsers` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `AspNetUserLogins` (
`LoginProvider` varchar(255) NOT NULL,
`ProviderKey` varchar(255) NOT NULL,
`ProviderDisplayName` longtext NULL,
`UserId` varchar(255) NOT NULL,
PRIMARY KEY (`LoginProvider`, `ProviderKey`),
CONSTRAINT `FK_AspNetUserLogins_AspNetUsers_UserId` FOREIGN KEY (`UserId`) REFERENCES `AspNetUsers` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `AspNetUserRoles` (
`UserId` varchar(255) NOT NULL,
`RoleId` varchar(255) NOT NULL,
PRIMARY KEY (`UserId`, `RoleId`),
CONSTRAINT `FK_AspNetUserRoles_AspNetRoles_RoleId` FOREIGN KEY (`RoleId`) REFERENCES `AspNetRoles` (`Id`) ON DELETE CASCADE,
CONSTRAINT `FK_AspNetUserRoles_AspNetUsers_UserId` FOREIGN KEY (`UserId`) REFERENCES `AspNetUsers` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `AspNetUserTokens` (
`UserId` varchar(255) NOT NULL,
`LoginProvider` varchar(255) NOT NULL,
`Name` varchar(255) NOT NULL,
`Value` longtext NULL,
PRIMARY KEY (`UserId`, `LoginProvider`, `Name`),
CONSTRAINT `FK_AspNetUserTokens_AspNetUsers_UserId` FOREIGN KEY (`UserId`) REFERENCES `AspNetUsers` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE UNIQUE INDEX IF NOT EXISTS `RoleNameIndex` ON `AspNetRoles` (`NormalizedName`);
CREATE INDEX IF NOT EXISTS `IX_AspNetRoleClaims_RoleId` ON `AspNetRoleClaims` (`RoleId`);
CREATE INDEX IF NOT EXISTS `EmailIndex` ON `AspNetUsers` (`NormalizedEmail`);
CREATE UNIQUE INDEX IF NOT EXISTS `UserNameIndex` ON `AspNetUsers` (`NormalizedUserName`);
CREATE INDEX IF NOT EXISTS `IX_AspNetUserClaims_UserId` ON `AspNetUserClaims` (`UserId`);
CREATE INDEX IF NOT EXISTS `IX_AspNetUserLogins_UserId` ON `AspNetUserLogins` (`UserId`);
CREATE INDEX IF NOT EXISTS `IX_AspNetUserRoles_RoleId` ON `AspNetUserRoles` (`RoleId`);
";
cmd.ExecuteNonQuery();
}
// The schema reconciler. Runs TWICE: once before Migrate() and once after.
//
// Before: legacy databases need their Identity tables and hand-added columns present, and
// the legacy migration-history stamp written, or Migrate() collides with them.
// Before: legacy databases need hand-added columns repaired and the legacy
// migration-history stamp written, or Migrate() can collide with them.
// After: on a brand-new database the migration-owned tables did not exist during the first
// pass, so everything that depends on them (reconciler-owned tables that FK into
// JobApplications, index and AUTO_INCREMENT repairs) was skipped. The second pass finds them
// and finishes the job.
// pass, so index, column, and AUTO_INCREMENT repairs were skipped. The second pass finds
// the migration-created tables and finishes compatibility repair.
//
// Every statement in here is existence-guarded, so running it twice is a no-op scan on an
// already-correct database. docs/infrastructure/database-ownership.md.
@@ -398,119 +293,6 @@ public static class StartupInitializationExtensions
// ReconcileSchema runs twice; the second pass may inherit an already-open connection.
if (conn.State != System.Data.ConnectionState.Open) conn.Open();
static void EnsureIdentityTables(DbConnection c)
{
// EF migrations are used for the app schema. In some environments `dotnet ef` isnt available,
// so create the ASP.NET Core Identity tables directly if they dont exist yet.
Exec(c, """
CREATE TABLE IF NOT EXISTS "AspNetRoles" (
"Id" TEXT NOT NULL CONSTRAINT "PK_AspNetRoles" PRIMARY KEY,
"Name" TEXT NULL,
"NormalizedName" TEXT NULL,
"ConcurrencyStamp" TEXT NULL
);
""");
Exec(c, """
CREATE TABLE IF NOT EXISTS "AspNetUsers" (
"Id" TEXT NOT NULL CONSTRAINT "PK_AspNetUsers" PRIMARY KEY,
"UserName" TEXT NULL,
"NormalizedUserName" TEXT NULL,
"Email" TEXT NULL,
"NormalizedEmail" TEXT NULL,
"EmailConfirmed" INTEGER NOT NULL,
"PasswordHash" TEXT NULL,
"SecurityStamp" TEXT NULL,
"ConcurrencyStamp" TEXT NULL,
"PhoneNumber" TEXT NULL,
"PhoneNumberConfirmed" INTEGER NOT NULL,
"TwoFactorEnabled" INTEGER NOT NULL,
"LockoutEnd" TEXT NULL,
"LockoutEnabled" INTEGER NOT NULL,
"AccessFailedCount" INTEGER NOT NULL,
"FirstName" TEXT NULL,
"LastName" TEXT NULL,
"DisplayName" TEXT NULL,
"ProfileCvText" TEXT NULL,
"ProfileCvStructureJson" TEXT NULL,
"CurrentCvUploadArtifactId" INTEGER NULL,
"CurrentCvExtractionRunId" INTEGER NULL,
"CurrentCvProfileVersion" INTEGER NULL,
"AvatarImageDataUrl" TEXT NULL,
"GoogleSubject" TEXT NULL,
"GoogleEmail" TEXT NULL,
"GoogleLinkedAt" TEXT NULL,
"MicrosoftSubject" TEXT NULL,
"MicrosoftEmail" TEXT NULL,
"MicrosoftLinkedAt" TEXT NULL,
"TotpSecretEncrypted" TEXT NULL,
"TotpPendingSecretEncrypted" TEXT NULL,
"TotpEnabledAtUtc" TEXT NULL
);
""");
Exec(c, """
CREATE TABLE IF NOT EXISTS "AspNetRoleClaims" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_AspNetRoleClaims" PRIMARY KEY AUTOINCREMENT,
"RoleId" TEXT NOT NULL,
"ClaimType" TEXT NULL,
"ClaimValue" TEXT NULL,
CONSTRAINT "FK_AspNetRoleClaims_AspNetRoles_RoleId" FOREIGN KEY ("RoleId") REFERENCES "AspNetRoles" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """
CREATE TABLE IF NOT EXISTS "AspNetUserClaims" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_AspNetUserClaims" PRIMARY KEY AUTOINCREMENT,
"UserId" TEXT NOT NULL,
"ClaimType" TEXT NULL,
"ClaimValue" TEXT NULL,
CONSTRAINT "FK_AspNetUserClaims_AspNetUsers_UserId" FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """
CREATE TABLE IF NOT EXISTS "AspNetUserLogins" (
"LoginProvider" TEXT NOT NULL,
"ProviderKey" TEXT NOT NULL,
"ProviderDisplayName" TEXT NULL,
"UserId" TEXT NOT NULL,
CONSTRAINT "PK_AspNetUserLogins" PRIMARY KEY ("LoginProvider", "ProviderKey"),
CONSTRAINT "FK_AspNetUserLogins_AspNetUsers_UserId" FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """
CREATE TABLE IF NOT EXISTS "AspNetUserRoles" (
"UserId" TEXT NOT NULL,
"RoleId" TEXT NOT NULL,
CONSTRAINT "PK_AspNetUserRoles" PRIMARY KEY ("UserId", "RoleId"),
CONSTRAINT "FK_AspNetUserRoles_AspNetRoles_RoleId" FOREIGN KEY ("RoleId") REFERENCES "AspNetRoles" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_AspNetUserRoles_AspNetUsers_UserId" FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """
CREATE TABLE IF NOT EXISTS "AspNetUserTokens" (
"UserId" TEXT NOT NULL,
"LoginProvider" TEXT NOT NULL,
"Name" TEXT NOT NULL,
"Value" TEXT NULL,
CONSTRAINT "PK_AspNetUserTokens" PRIMARY KEY ("UserId", "LoginProvider", "Name"),
CONSTRAINT "FK_AspNetUserTokens_AspNetUsers_UserId" FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """CREATE UNIQUE INDEX IF NOT EXISTS "RoleNameIndex" ON "AspNetRoles" ("NormalizedName");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_AspNetRoleClaims_RoleId" ON "AspNetRoleClaims" ("RoleId");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "EmailIndex" ON "AspNetUsers" ("NormalizedEmail");""");
Exec(c, """CREATE UNIQUE INDEX IF NOT EXISTS "UserNameIndex" ON "AspNetUsers" ("NormalizedUserName");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_AspNetUserClaims_UserId" ON "AspNetUserClaims" ("UserId");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_AspNetUserLogins_UserId" ON "AspNetUserLogins" ("UserId");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_AspNetUserRoles_RoleId" ON "AspNetUserRoles" ("RoleId");""");
}
EnsureIdentityTables(conn);
EnsureColumn(conn, "AspNetUsers", "FirstName", "ALTER TABLE AspNetUsers ADD COLUMN FirstName TEXT NULL;");
EnsureColumn(conn, "AspNetUsers", "LastName", "ALTER TABLE AspNetUsers ADD COLUMN LastName TEXT NULL;");
EnsureColumn(conn, "AspNetUsers", "DisplayName", "ALTER TABLE AspNetUsers ADD COLUMN DisplayName TEXT NULL;");
@@ -564,8 +346,8 @@ public static class StartupInitializationExtensions
ReconcileCareerProfileColumns(conn);
ReconcileAiInteractionUsageColumns(conn);
// Once the base app tables exist, provision this reconciler-owned schema set and
// stamp its historical migration before later migrations rebuild JobApplications.
// Once the base app tables exist, stamp the historical migration before later
// migrations rebuild JobApplications.
var isLegacy =
HasMigration(conn, "20260310174114_AddCorrespondence") &&
!HasMigration(conn, legacyMigrationId);
@@ -649,8 +431,6 @@ public static class StartupInitializationExtensions
DbConnection conn = db.Database.GetDbConnection();
// ReconcileSchema runs twice; the second pass may inherit an already-open connection.
if (conn.State != System.Data.ConnectionState.Open) conn.Open();
EnsureIdentityTablesMySql(conn);
static bool MySqlIntPrimaryKeyIsAutoIncrement(DbConnection c, string table, string column)
{
@@ -898,8 +678,8 @@ public static class StartupInitializationExtensions
}
}
// 1. Reconcile what already exists and create the remaining Identity-owned tables. This
// runs before Migrate() so legacy schemas are repaired before migrations inspect them.
// 1. Reconcile what already exists. This runs before Migrate() so legacy schemas are
// repaired before migrations inspect them.
ReconcileSchema();
// 2. Apply one migration at a time, reconciling after each. Some historical migrations
@@ -925,7 +705,7 @@ public static class StartupInitializationExtensions
// historical migrations can add a later column (for example Companies.Source)
// immediately before the migration that owns it, producing a duplicate-column
// failure on a clean database. Apply the chain first, then use the common final
// reconciliation pass for provider-safe repairs and reconciler-owned tables.
// reconciliation pass for provider-safe legacy repairs.
migrationDb.Database.Migrate();
}
}
@@ -935,9 +715,8 @@ public static class StartupInitializationExtensions
throw;
}
// 3. Reconcile again, now that the migration-owned tables exist: creates the
// reconciler-owned tables that reference them and applies the index / AUTO_INCREMENT
// repairs that pass 1 had to skip. Idempotent, so on an existing database it is a scan.
// 3. Reconcile again, now that migration-owned tables exist, applying the index and
// AUTO_INCREMENT repairs pass 1 had to skip. Existing databases receive a no-op scan.
ReconcileSchema();
// Optional: seed an initial admin user for local username/password login.
@@ -15,6 +15,13 @@ internal static class StartupSchemaOwnership
"AiWorkspaceNotes",
"AiInteractions",
"ApplicationChecklistItems",
"AspNetRoleClaims",
"AspNetRoles",
"AspNetUserClaims",
"AspNetUserLogins",
"AspNetUserRoles",
"AspNetUsers",
"AspNetUserTokens",
"Attachments",
"CareerCertifications",
"CareerEducations",
@@ -53,19 +60,10 @@ internal static class StartupSchemaOwnership
"UserSessions",
};
internal static readonly IReadOnlySet<string> ReconcilerOwnedTables = new HashSet<string>(StringComparer.Ordinal)
{
"AspNetRoleClaims",
"AspNetRoles",
"AspNetUserClaims",
"AspNetUserLogins",
"AspNetUserRoles",
"AspNetUsers",
"AspNetUserTokens",
};
internal static readonly IReadOnlySet<string> ReconcilerOwnedTables = new HashSet<string>(StringComparer.Ordinal);
// Historical migrations contain guarded compatibility bootstraps for these tables so direct
// EF tooling can traverse the chain. Their current creation owner remains the reconciler.
// EF tooling can traverse the chain. The later adoption migration is the current owner.
internal static readonly IReadOnlySet<string> MigrationCompatibilityBootstrapTables =
new HashSet<string>(StringComparer.Ordinal) { "AspNetUsers" };
}