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:
@@ -126,6 +126,11 @@ public sealed class MigrationChainTests
|
|||||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerProjects`", script, StringComparison.Ordinal);
|
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerProjects`", script, StringComparison.Ordinal);
|
||||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerCertifications`", script, StringComparison.Ordinal);
|
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerCertifications`", script, StringComparison.Ordinal);
|
||||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerLanguages`", script, StringComparison.Ordinal);
|
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerLanguages`", script, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("CREATE TABLE IF NOT EXISTS `AspNetRoles`", script, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("CREATE TABLE IF NOT EXISTS `AspNetUserClaims`", script, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("CREATE TABLE IF NOT EXISTS `AspNetUserLogins`", script, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("CREATE TABLE IF NOT EXISTS `AspNetUserRoles`", script, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("CREATE TABLE IF NOT EXISTS `AspNetUserTokens`", script, StringComparison.Ordinal);
|
||||||
Assert.Contains("`UiLanguage` varchar(16)", script, StringComparison.Ordinal);
|
Assert.Contains("`UiLanguage` varchar(16)", script, StringComparison.Ordinal);
|
||||||
Assert.All(
|
Assert.All(
|
||||||
Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value),
|
Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value),
|
||||||
@@ -1015,6 +1020,89 @@ public sealed class MigrationChainTests
|
|||||||
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
|
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Identity_adoption_preserves_accounts_credentials_roles_logins_claims_and_tokens()
|
||||||
|
{
|
||||||
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
await using var db = Context(connection);
|
||||||
|
var migrator = db.GetService<IMigrator>();
|
||||||
|
await migrator.MigrateAsync("20260830133000_AdoptCareerProfileSchema");
|
||||||
|
await ExecuteAsync(connection, """
|
||||||
|
CREATE TABLE "AspNetRoles" (
|
||||||
|
"Id" TEXT NOT NULL PRIMARY KEY, "Name" TEXT NULL,
|
||||||
|
"NormalizedName" TEXT NULL, "ConcurrencyStamp" TEXT NULL);
|
||||||
|
CREATE TABLE "AspNetRoleClaims" (
|
||||||
|
"Id" INTEGER PRIMARY KEY AUTOINCREMENT, "RoleId" TEXT NOT NULL,
|
||||||
|
"ClaimType" TEXT NULL, "ClaimValue" TEXT NULL,
|
||||||
|
FOREIGN KEY ("RoleId") REFERENCES "AspNetRoles" ("Id") ON DELETE CASCADE);
|
||||||
|
CREATE TABLE "AspNetUserClaims" (
|
||||||
|
"Id" INTEGER PRIMARY KEY AUTOINCREMENT, "UserId" TEXT NOT NULL,
|
||||||
|
"ClaimType" TEXT NULL, "ClaimValue" TEXT NULL,
|
||||||
|
FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE);
|
||||||
|
CREATE TABLE "AspNetUserLogins" (
|
||||||
|
"LoginProvider" TEXT NOT NULL, "ProviderKey" TEXT NOT NULL,
|
||||||
|
"ProviderDisplayName" TEXT NULL, "UserId" TEXT NOT NULL,
|
||||||
|
PRIMARY KEY ("LoginProvider", "ProviderKey"),
|
||||||
|
FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE);
|
||||||
|
CREATE TABLE "AspNetUserRoles" (
|
||||||
|
"UserId" TEXT NOT NULL, "RoleId" TEXT NOT NULL,
|
||||||
|
PRIMARY KEY ("UserId", "RoleId"),
|
||||||
|
FOREIGN KEY ("RoleId") REFERENCES "AspNetRoles" ("Id") ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE);
|
||||||
|
CREATE TABLE "AspNetUserTokens" (
|
||||||
|
"UserId" TEXT NOT NULL, "LoginProvider" TEXT NOT NULL, "Name" TEXT NOT NULL,
|
||||||
|
"Value" TEXT NULL, PRIMARY KEY ("UserId", "LoginProvider", "Name"),
|
||||||
|
FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE);
|
||||||
|
|
||||||
|
INSERT INTO "AspNetUsers"
|
||||||
|
("Id", "UserName", "NormalizedUserName", "Email", "NormalizedEmail",
|
||||||
|
"EmailConfirmed", "PasswordHash", "PhoneNumberConfirmed", "TwoFactorEnabled",
|
||||||
|
"LockoutEnabled", "AccessFailedCount", "AiEnabled", "ExternalAiProcessingAllowed",
|
||||||
|
"DeletionStatus", "EmailFollowUpRemindersEnabled", "UiLanguage")
|
||||||
|
VALUES ('user-fixture', 'connor', 'CONNOR', 'connor@example.test', 'CONNOR@EXAMPLE.TEST',
|
||||||
|
1, 'preserved-password-hash', 0, 1, 1, 0, 1, 0, 'active', 1, 'nb');
|
||||||
|
INSERT INTO "AspNetRoles" VALUES ('role-admin', 'Admin', 'ADMIN', 'role-stamp');
|
||||||
|
INSERT INTO "AspNetRoleClaims" ("RoleId", "ClaimType", "ClaimValue")
|
||||||
|
VALUES ('role-admin', 'permission', 'admin.manage');
|
||||||
|
INSERT INTO "AspNetUserClaims" ("UserId", "ClaimType", "ClaimValue")
|
||||||
|
VALUES ('user-fixture', 'locale', 'nb');
|
||||||
|
INSERT INTO "AspNetUserLogins" VALUES ('google', 'subject-1', 'Google', 'user-fixture');
|
||||||
|
INSERT INTO "AspNetUserRoles" VALUES ('user-fixture', 'role-admin');
|
||||||
|
INSERT INTO "AspNetUserTokens" VALUES ('user-fixture', 'authenticator', 'refresh', 'encrypted-token');
|
||||||
|
""");
|
||||||
|
|
||||||
|
await migrator.MigrateAsync();
|
||||||
|
Assert.Equal("preserved-password-hash", await ScalarAsync<string>(connection,
|
||||||
|
"SELECT PasswordHash FROM AspNetUsers WHERE Id = 'user-fixture';"));
|
||||||
|
Assert.Equal("nb", await ScalarAsync<string>(connection,
|
||||||
|
"SELECT UiLanguage FROM AspNetUsers WHERE Id = 'user-fixture';"));
|
||||||
|
Assert.Equal("admin.manage", await ScalarAsync<string>(connection,
|
||||||
|
"SELECT ClaimValue FROM AspNetRoleClaims WHERE RoleId = 'role-admin';"));
|
||||||
|
Assert.Equal("encrypted-token", await ScalarAsync<string>(connection,
|
||||||
|
"SELECT Value FROM AspNetUserTokens WHERE UserId = 'user-fixture';"));
|
||||||
|
|
||||||
|
await migrator.MigrateAsync("20260830133000_AdoptCareerProfileSchema");
|
||||||
|
Assert.Equal("subject-1", await ScalarAsync<string>(connection,
|
||||||
|
"SELECT ProviderKey FROM AspNetUserLogins WHERE UserId = 'user-fixture';"));
|
||||||
|
await migrator.MigrateAsync();
|
||||||
|
Assert.Equal(8L, await ScalarAsync<long>(connection, """
|
||||||
|
SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name IN (
|
||||||
|
'RoleNameIndex', 'IX_AspNetRoleClaims_RoleId', 'EmailIndex', 'UserNameIndex',
|
||||||
|
'IX_AspNetUsers_MicrosoftTenantId_MicrosoftObjectId',
|
||||||
|
'IX_AspNetUserClaims_UserId', 'IX_AspNetUserLogins_UserId', 'IX_AspNetUserRoles_RoleId');
|
||||||
|
"""));
|
||||||
|
|
||||||
|
await ExecuteAsync(connection, "DELETE FROM AspNetUsers WHERE Id = 'user-fixture';");
|
||||||
|
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM AspNetUserClaims;"));
|
||||||
|
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM AspNetUserLogins;"));
|
||||||
|
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM AspNetUserRoles;"));
|
||||||
|
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM AspNetUserTokens;"));
|
||||||
|
await ExecuteAsync(connection, "DELETE FROM AspNetRoles WHERE Id = 'role-admin';");
|
||||||
|
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM AspNetRoleClaims;"));
|
||||||
|
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
|
||||||
|
}
|
||||||
|
|
||||||
private static JobTrackerContext Context(SqliteConnection connection)
|
private static JobTrackerContext Context(SqliteConnection connection)
|
||||||
{
|
{
|
||||||
var currentUser = new Mock<ICurrentUserService>();
|
var currentUser = new Mock<ICurrentUserService>();
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ public sealed class StartupSchemaOwnershipTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Compatibility_bootstraps_remain_reconciler_owned_until_migrated()
|
public void Compatibility_bootstraps_do_not_create_a_second_current_owner()
|
||||||
{
|
{
|
||||||
Assert.All(
|
Assert.All(
|
||||||
StartupSchemaOwnership.MigrationCompatibilityBootstrapTables,
|
StartupSchemaOwnership.MigrationCompatibilityBootstrapTables,
|
||||||
table => Assert.Contains(table, StartupSchemaOwnership.ReconcilerOwnedTables));
|
table => Assert.True(
|
||||||
Assert.Empty(StartupSchemaOwnership.MigrationCompatibilityBootstrapTables
|
StartupSchemaOwnership.MigrationOwnedTables.Contains(table) ^
|
||||||
.Intersect(StartupSchemaOwnership.MigrationOwnedTables, StringComparer.Ordinal));
|
StartupSchemaOwnership.ReconcilerOwnedTables.Contains(table)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 provider = (app.Configuration["Database:Provider"] ?? "sqlite").Trim().ToLowerInvariant();
|
||||||
var useSqliteBootstrap = provider is not "mysql" and not "mariadb";
|
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.
|
// The schema reconciler. Runs TWICE: once before Migrate() and once after.
|
||||||
//
|
//
|
||||||
// Before: legacy databases need their Identity tables and hand-added columns present, and
|
// Before: legacy databases need hand-added columns repaired and the legacy
|
||||||
// the legacy migration-history stamp written, or Migrate() collides with them.
|
// 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
|
// 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
|
// pass, so index, column, and AUTO_INCREMENT repairs were skipped. The second pass finds
|
||||||
// JobApplications, index and AUTO_INCREMENT repairs) was skipped. The second pass finds them
|
// the migration-created tables and finishes compatibility repair.
|
||||||
// and finishes the job.
|
|
||||||
//
|
//
|
||||||
// Every statement in here is existence-guarded, so running it twice is a no-op scan on an
|
// 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.
|
// 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.
|
// ReconcileSchema runs twice; the second pass may inherit an already-open connection.
|
||||||
if (conn.State != System.Data.ConnectionState.Open) conn.Open();
|
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` isn’t available,
|
|
||||||
// so create the ASP.NET Core Identity tables directly if they don’t 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", "FirstName", "ALTER TABLE AspNetUsers ADD COLUMN FirstName TEXT NULL;");
|
||||||
EnsureColumn(conn, "AspNetUsers", "LastName", "ALTER TABLE AspNetUsers ADD COLUMN LastName 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;");
|
EnsureColumn(conn, "AspNetUsers", "DisplayName", "ALTER TABLE AspNetUsers ADD COLUMN DisplayName TEXT NULL;");
|
||||||
@@ -564,8 +346,8 @@ public static class StartupInitializationExtensions
|
|||||||
ReconcileCareerProfileColumns(conn);
|
ReconcileCareerProfileColumns(conn);
|
||||||
ReconcileAiInteractionUsageColumns(conn);
|
ReconcileAiInteractionUsageColumns(conn);
|
||||||
|
|
||||||
// Once the base app tables exist, provision this reconciler-owned schema set and
|
// Once the base app tables exist, stamp the historical migration before later
|
||||||
// stamp its historical migration before later migrations rebuild JobApplications.
|
// migrations rebuild JobApplications.
|
||||||
var isLegacy =
|
var isLegacy =
|
||||||
HasMigration(conn, "20260310174114_AddCorrespondence") &&
|
HasMigration(conn, "20260310174114_AddCorrespondence") &&
|
||||||
!HasMigration(conn, legacyMigrationId);
|
!HasMigration(conn, legacyMigrationId);
|
||||||
@@ -649,8 +431,6 @@ public static class StartupInitializationExtensions
|
|||||||
DbConnection conn = db.Database.GetDbConnection();
|
DbConnection conn = db.Database.GetDbConnection();
|
||||||
// ReconcileSchema runs twice; the second pass may inherit an already-open connection.
|
// ReconcileSchema runs twice; the second pass may inherit an already-open connection.
|
||||||
if (conn.State != System.Data.ConnectionState.Open) conn.Open();
|
if (conn.State != System.Data.ConnectionState.Open) conn.Open();
|
||||||
EnsureIdentityTablesMySql(conn);
|
|
||||||
|
|
||||||
|
|
||||||
static bool MySqlIntPrimaryKeyIsAutoIncrement(DbConnection c, string table, string column)
|
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
|
// 1. Reconcile what already exists. This runs before Migrate() so legacy schemas are
|
||||||
// runs before Migrate() so legacy schemas are repaired before migrations inspect them.
|
// repaired before migrations inspect them.
|
||||||
ReconcileSchema();
|
ReconcileSchema();
|
||||||
|
|
||||||
// 2. Apply one migration at a time, reconciling after each. Some historical migrations
|
// 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)
|
// historical migrations can add a later column (for example Companies.Source)
|
||||||
// immediately before the migration that owns it, producing a duplicate-column
|
// 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
|
// 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();
|
migrationDb.Database.Migrate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -935,9 +715,8 @@ public static class StartupInitializationExtensions
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Reconcile again, now that the migration-owned tables exist: creates the
|
// 3. Reconcile again, now that migration-owned tables exist, applying the index and
|
||||||
// reconciler-owned tables that reference them and applies the index / AUTO_INCREMENT
|
// AUTO_INCREMENT repairs pass 1 had to skip. Existing databases receive a no-op scan.
|
||||||
// repairs that pass 1 had to skip. Idempotent, so on an existing database it is a scan.
|
|
||||||
ReconcileSchema();
|
ReconcileSchema();
|
||||||
|
|
||||||
// Optional: seed an initial admin user for local username/password login.
|
// Optional: seed an initial admin user for local username/password login.
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ internal static class StartupSchemaOwnership
|
|||||||
"AiWorkspaceNotes",
|
"AiWorkspaceNotes",
|
||||||
"AiInteractions",
|
"AiInteractions",
|
||||||
"ApplicationChecklistItems",
|
"ApplicationChecklistItems",
|
||||||
|
"AspNetRoleClaims",
|
||||||
|
"AspNetRoles",
|
||||||
|
"AspNetUserClaims",
|
||||||
|
"AspNetUserLogins",
|
||||||
|
"AspNetUserRoles",
|
||||||
|
"AspNetUsers",
|
||||||
|
"AspNetUserTokens",
|
||||||
"Attachments",
|
"Attachments",
|
||||||
"CareerCertifications",
|
"CareerCertifications",
|
||||||
"CareerEducations",
|
"CareerEducations",
|
||||||
@@ -53,19 +60,10 @@ internal static class StartupSchemaOwnership
|
|||||||
"UserSessions",
|
"UserSessions",
|
||||||
};
|
};
|
||||||
|
|
||||||
internal static readonly IReadOnlySet<string> ReconcilerOwnedTables = new HashSet<string>(StringComparer.Ordinal)
|
internal static readonly IReadOnlySet<string> ReconcilerOwnedTables = new HashSet<string>(StringComparer.Ordinal);
|
||||||
{
|
|
||||||
"AspNetRoleClaims",
|
|
||||||
"AspNetRoles",
|
|
||||||
"AspNetUserClaims",
|
|
||||||
"AspNetUserLogins",
|
|
||||||
"AspNetUserRoles",
|
|
||||||
"AspNetUsers",
|
|
||||||
"AspNetUserTokens",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Historical migrations contain guarded compatibility bootstraps for these tables so direct
|
// 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 =
|
internal static readonly IReadOnlySet<string> MigrationCompatibilityBootstrapTables =
|
||||||
new HashSet<string>(StringComparer.Ordinal) { "AspNetUsers" };
|
new HashSet<string>(StringComparer.Ordinal) { "AspNetUsers" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -558,7 +558,7 @@ SEC-008 implements the same durable state machine with `<final>.uploading` and `
|
|||||||
|
|
||||||
### P3-1 — Reduce dual schema ownership incrementally
|
### P3-1 — Reduce dual schema ownership incrementally
|
||||||
|
|
||||||
**Status (2026-08-30): in progress.** All 49 model tables now have an executable, disjoint creation-owner classification. Twenty-eight formerly reconciler-created tables—including authentication support, email-provider connections, CV persistence/history, job-workspace AI notes, append-only AI interactions, workflow checklists, cover-letter history, durable interview preparation, and the complete Career Profile aggregate—have moved through additive provider-aware migrations with legacy-row preservation, downgrade/retry safety, startup-DDL removal, fresh SQLite runtime proof and generated MariaDB SQL. The Gmail decision migration also closes its missing MariaDB creation path. Only the seven ASP.NET Identity tables remain startup-created; see `docs/verification/jt-019-schema-ownership.md` and V-194–V-207.
|
**Status (2026-08-30): complete.** All 49 model tables have an executable, disjoint creation-owner classification and are migration-owned. All 35 formerly startup-created tables—including authentication/Identity, email-provider connections, CV persistence/history, job-workspace AI notes, workflow documents, durable interview preparation, and the complete Career Profile aggregate—moved through additive provider-aware migrations with legacy-row preservation, downgrade/retry safety, startup-DDL removal, fresh SQLite chain proof, and generated MariaDB SQL. The reconciler now performs guarded compatibility repair only. See `docs/verification/jt-019-schema-ownership.md` and V-194–V-208.
|
||||||
|
|
||||||
- **Findings/scope:** JT-019; inventory reconciler operations and move stable schema ownership to EF migrations in small steps.
|
- **Findings/scope:** JT-019; inventory reconciler operations and move stable schema ownership to EF migrations in small steps.
|
||||||
- **Dependencies:** provider upgrade fixtures and P2-2 restore safety.
|
- **Dependencies:** provider upgrade fixtures and P2-2 restore safety.
|
||||||
|
|||||||
@@ -239,3 +239,4 @@ Output was reduced to filenames and commit counts. The token artifact appears un
|
|||||||
| V-205 | Ownership/migration chain; populated adoption/downgrade/retry; document/FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move append-only cover-letter revision history to migration ownership without losing recoverable documents | PASS — focused ownership/migration 17/17; manual and AI revisions plus source/action metadata survive adoption, downgrade and re-upgrade; owner/job/version index exists; deleting the parent application cascades through history; generated MariaDB SQL is provider-safe; startup no longer creates the table; full backend 733/733 | MariaDB SQL generated only; no provider account or production migration. Guarded malformed-empty/index/auto-increment repair remains temporarily. Sixteen reconciler-owned tables remain | JT-019 cover-letter history transfer complete; interview-preparation items and career-profile aggregate continue incrementally |
|
| V-205 | Ownership/migration chain; populated adoption/downgrade/retry; document/FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move append-only cover-letter revision history to migration ownership without losing recoverable documents | PASS — focused ownership/migration 17/17; manual and AI revisions plus source/action metadata survive adoption, downgrade and re-upgrade; owner/job/version index exists; deleting the parent application cascades through history; generated MariaDB SQL is provider-safe; startup no longer creates the table; full backend 733/733 | MariaDB SQL generated only; no provider account or production migration. Guarded malformed-empty/index/auto-increment repair remains temporarily. Sixteen reconciler-owned tables remain | JT-019 cover-letter history transfer complete; interview-preparation items and career-profile aggregate continue incrementally |
|
||||||
| V-206 | Ownership/migration chain; populated adoption/downgrade/retry; practice-state/FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move durable interview-preparation items to migration ownership without losing user or AI preparation content | PASS — focused ownership/migration 18/18; user-authored and AI-generated questions, answers, prepared state and source metadata survive adoption, downgrade and re-upgrade; owner/job/sort index exists; deleting the parent application cascades through preparation items; generated MariaDB SQL is provider-safe; startup no longer creates the table; full backend 734/734 | MariaDB SQL generated only; no provider account or production migration. Guarded malformed-empty/index/auto-increment repair remains temporarily. Fifteen reconciler-owned tables remain | JT-019 feature-table transfers complete; the Career Profile aggregate and Identity group remain dependency-aware batches |
|
| V-206 | Ownership/migration chain; populated adoption/downgrade/retry; practice-state/FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move durable interview-preparation items to migration ownership without losing user or AI preparation content | PASS — focused ownership/migration 18/18; user-authored and AI-generated questions, answers, prepared state and source metadata survive adoption, downgrade and re-upgrade; owner/job/sort index exists; deleting the parent application cascades through preparation items; generated MariaDB SQL is provider-safe; startup no longer creates the table; full backend 734/734 | MariaDB SQL generated only; no provider account or production migration. Guarded malformed-empty/index/auto-increment repair remains temporarily. Fifteen reconciler-owned tables remain | JT-019 feature-table transfers complete; the Career Profile aggregate and Identity group remain dependency-aware batches |
|
||||||
| V-207 | Ownership/migration chain; populated aggregate adoption/downgrade/retry; JSON/text/FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move the canonical Career Profile, append-only history, and six relational child types to migration ownership without losing career data | PASS — focused ownership/migration 19/19; canonical and long-tail JSON, Norwegian text, version history, experience, education, skill, project, certification and language rows survive adoption, downgrade and re-upgrade; all 14 aggregate indexes exist; deleting the profile cascades through history and children; generated MariaDB SQL uses bounded indexed keys and provider-safe types; startup creates none of the eight tables; full backend 735/735 | MariaDB SQL generated only; no production migration. Guarded historical LongTailJson/index/auto-increment repairs remain temporarily. Seven reconciler-owned Identity tables remain | JT-019 feature and career transfers complete; isolate the Identity ownership boundary next |
|
| V-207 | Ownership/migration chain; populated aggregate adoption/downgrade/retry; JSON/text/FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move the canonical Career Profile, append-only history, and six relational child types to migration ownership without losing career data | PASS — focused ownership/migration 19/19; canonical and long-tail JSON, Norwegian text, version history, experience, education, skill, project, certification and language rows survive adoption, downgrade and re-upgrade; all 14 aggregate indexes exist; deleting the profile cascades through history and children; generated MariaDB SQL uses bounded indexed keys and provider-safe types; startup creates none of the eight tables; full backend 735/735 | MariaDB SQL generated only; no production migration. Guarded historical LongTailJson/index/auto-increment repairs remain temporarily. Seven reconciler-owned Identity tables remain | JT-019 feature and career transfers complete; isolate the Identity ownership boundary next |
|
||||||
|
| V-208 | Ownership/migration chain; populated Identity adoption/downgrade/retry; credential/preference/FK/index assertions; MariaDB script; full backend; fresh application startup | Repository root / disposable local SQLite | Complete JT-019 by moving the seven ASP.NET Identity tables to migration ownership without invalidating authentication data | PASS — focused ownership/migration 20/20; password hash, Bokmål preference, 2FA state, role assignment, role/user claims, external login and token survive adoption, downgrade and re-upgrade; all eight Identity indexes exist; user and role deletion cascades remain effective; generated MariaDB SQL is provider-safe; startup creates no model tables; full backend 736/736; a fresh application applies the complete chain, reaches Identity role initialization and listens normally | MariaDB SQL generated only; no production migration. The historical AspNetUsers compatibility bootstrap and guarded column/index repairs remain for chain traversal and legacy upgrades. Launch settings supplied an existing policy-invalid development admin password, producing a non-fatal seed warning | JT-019 complete; future work may retire individual repair statements only after provider-backed historical fixtures prove them redundant |
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ already-correct database. Two consequences worth knowing:
|
|||||||
Created by EF migrations, never by the reconciler:
|
Created by EF migrations, never by the reconciler:
|
||||||
|
|
||||||
`AccountDeletionFiles`, `AccountDeletionRequests`, `AiInteractions`, `AiUsageRecords`, `AiWorkspaceNotes`,
|
`AccountDeletionFiles`, `AccountDeletionRequests`, `AiInteractions`, `AiUsageRecords`, `AiWorkspaceNotes`,
|
||||||
`ApplicationChecklistItems`, `Attachments`, `CareerCertifications`, `CareerEducations`,
|
`ApplicationChecklistItems`, `AspNetRoleClaims`, `AspNetRoles`, `AspNetUserClaims`, `AspNetUserLogins`,
|
||||||
|
`AspNetUserRoles`, `AspNetUsers`, `AspNetUserTokens`, `Attachments`, `CareerCertifications`, `CareerEducations`,
|
||||||
`CareerExperiences`, `CareerLanguages`, `CareerProfiles`, `CareerProfileVersions`, `CareerProjects`,
|
`CareerExperiences`, `CareerLanguages`, `CareerProfiles`, `CareerProfileVersions`, `CareerProjects`,
|
||||||
`CareerSkills`, `Companies`, `CoverLetterVersions`,
|
`CareerSkills`, `Companies`, `CoverLetterVersions`,
|
||||||
`Correspondences`, `CvExtractionRuns`, `CvUploadArtifacts`, `CvVariants`, `CvVariantVersions`,
|
`Correspondences`, `CvExtractionRuns`, `CvUploadArtifacts`, `CvVariants`, `CvVariantVersions`,
|
||||||
@@ -132,6 +133,11 @@ child tables) moved in `20260830133000_AdoptCareerProfileSchema`. Canonical/long
|
|||||||
append-only history, stable child keys and ordering are retained; bounded MariaDB owner/item keys
|
append-only history, stable child keys and ordering are retained; bounded MariaDB owner/item keys
|
||||||
keep all aggregate indexes provider-safe.
|
keep all aggregate indexes provider-safe.
|
||||||
|
|
||||||
|
The ASP.NET Identity aggregate moved in `20260830134000_AdoptIdentitySchema`. Accounts, password
|
||||||
|
hashes, security/2FA state, UI preferences, roles, claims, external logins, tokens, indexes, and
|
||||||
|
cascades are retained. The earlier guarded `AspNetUsers` bootstrap remains only so historical
|
||||||
|
standalone migration traversal can reach later additive user-column migrations.
|
||||||
|
|
||||||
The reconciler may **repair** these (add a missing column, add an index, fix a non-`AUTO_INCREMENT`
|
The reconciler may **repair** these (add a missing column, add an index, fix a non-`AUTO_INCREMENT`
|
||||||
primary key) and may seed the default `RuleSettings` row — but it must never `CREATE TABLE` them.
|
primary key) and may seed the default `RuleSettings` row — but it must never `CREATE TABLE` them.
|
||||||
It used to create `RuleSettings`, which is precisely why a clean install failed with
|
It used to create `RuleSettings`, which is precisely why a clean install failed with
|
||||||
@@ -139,12 +145,8 @@ It used to create `RuleSettings`, which is precisely why a clean install failed
|
|||||||
|
|
||||||
### Reconciler-owned
|
### Reconciler-owned
|
||||||
|
|
||||||
The seven ASP.NET Identity tables are currently reconciler-owned, despite older wording that
|
None. The reconciler contains guarded historical repair logic, but it no longer creates EF model
|
||||||
called them migration-owned: `AspNetRoles`, `AspNetUsers`, `AspNetRoleClaims`, `AspNetUserClaims`,
|
tables. Compatibility bootstraps inside old migrations are chain prerequisites, not current owners.
|
||||||
`AspNetUserLogins`, `AspNetUserRoles`, and `AspNetUserTokens`. Guarded migration bootstraps for
|
|
||||||
`AspNetUsers` supports standalone traversal but does not yet transfer ownership. The historical
|
|
||||||
`AiInteractions` bootstrap remains in the migration chain, but its current owner is the later
|
|
||||||
provider-aware adoption migration.
|
|
||||||
|
|
||||||
`StartupSchemaOwnership` is the executable inventory. Its tests require every EF model table to
|
`StartupSchemaOwnership` is the executable inventory. Its tests require every EF model table to
|
||||||
have exactly one creation owner and keep compatibility bootstraps out of the migration-owned set.
|
have exactly one creation owner and keep compatibility bootstraps out of the migration-owned set.
|
||||||
@@ -222,9 +224,8 @@ everything inside it.
|
|||||||
|
|
||||||
Standalone EF tooling is also supported for a blank SQLite database. The historical initial
|
Standalone EF tooling is also supported for a blank SQLite database. The historical initial
|
||||||
migration now supplies the stable JobApplication columns required by later SQLite rebuilds, and
|
migration now supplies the stable JobApplication columns required by later SQLite rebuilds, and
|
||||||
guarded compatibility bootstraps provide the reconciler-owned source tables used by later additive
|
guarded compatibility bootstraps provide source tables used by later additive migrations.
|
||||||
migrations. Application startup may subsequently reconcile the remaining Identity and auxiliary
|
Application startup may subsequently apply guarded historical column/index repairs without losing rows.
|
||||||
tables without losing rows.
|
|
||||||
|
|
||||||
## Production upgrade
|
## Production upgrade
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ cohesive table groups. It does not attempt to delete the legacy reconciler whole
|
|||||||
- Added `20260830133000_AdoptCareerProfileSchema` for the canonical profile, append-only history,
|
- Added `20260830133000_AdoptCareerProfileSchema` for the canonical profile, append-only history,
|
||||||
and all six ordered relational child types. Legacy JSON, Norwegian text, stable keys, and cascade
|
and all six ordered relational child types. Legacy JSON, Norwegian text, stable keys, and cascade
|
||||||
ownership are retained.
|
ownership are retained.
|
||||||
|
- Added `20260830134000_AdoptIdentitySchema` for the complete ASP.NET Identity aggregate, preserving
|
||||||
|
accounts, password hashes, 2FA state, preferences, roles, claims, external logins, and tokens.
|
||||||
- Corrected the ownership runbook: new tables now default to migrations; the reconciler is legacy
|
- Corrected the ownership runbook: new tables now default to migrations; the reconciler is legacy
|
||||||
compatibility code to retire one dependency group at a time.
|
compatibility code to retire one dependency group at a time.
|
||||||
|
|
||||||
@@ -87,14 +89,21 @@ migration.
|
|||||||
- A populated Career Profile with history, long-tail JSON, Norwegian text, experience, education,
|
- A populated Career Profile with history, long-tail JSON, Norwegian text, experience, education,
|
||||||
skills, projects, certifications, and languages survives adoption, downgrade, and re-upgrade; all
|
skills, projects, certifications, and languages survives adoption, downgrade, and re-upgrade; all
|
||||||
14 aggregate indexes and parent cascades remain effective.
|
14 aggregate indexes and parent cascades remain effective.
|
||||||
|
- A populated Identity account with password hash, Bokmål preference, 2FA state, role assignment,
|
||||||
|
role/user claims, external login, and token survives adoption, downgrade, and re-upgrade; all eight
|
||||||
|
Identity indexes and user/role cascades remain effective.
|
||||||
- Generated MariaDB SQL contains the provider-correct `SystemEmailSettings` DDL.
|
- Generated MariaDB SQL contains the provider-correct `SystemEmailSettings` DDL.
|
||||||
- Full backend: 735/735 passed after the Career Profile aggregate transfer.
|
- Full backend: 736/736 passed after the Identity aggregate transfer.
|
||||||
|
- Fresh application startup over a new disposable SQLite data root applied the complete chain,
|
||||||
|
queried/seeded Identity roles, and reached the healthy listening state with no schema failure.
|
||||||
- Fresh application startup over a new disposable SQLite database applied
|
- Fresh application startup over a new disposable SQLite database applied
|
||||||
`20260830120000_AdoptSystemEmailSettingsSchema` and reached the healthy listening state.
|
`20260830120000_AdoptSystemEmailSettingsSchema` and reached the healthy listening state.
|
||||||
|
|
||||||
## Remaining JT-019 work
|
## Remaining JT-019 work
|
||||||
|
|
||||||
Seven model tables remain startup-created: the ASP.NET Identity group.
|
No EF model tables remain startup-created. JT-019's creation-ownership transfer is complete; the
|
||||||
|
startup reconciler now performs compatibility repair only and can be reduced further when historical
|
||||||
|
provider fixtures prove individual repairs obsolete.
|
||||||
with parent dependencies. Transfer them in small dependency-aware migrations with blank, populated,
|
with parent dependencies. Transfer them in small dependency-aware migrations with blank, populated,
|
||||||
retry and MariaDB runtime proof. Column/index repairs must remain until historical upgrade fixtures
|
retry and MariaDB runtime proof. Column/index repairs must remain until historical upgrade fixtures
|
||||||
prove each one redundant.
|
prove each one redundant.
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ Updated: 2026-08-30
|
|||||||
- Moved append-only cover-letter revision history into a provider-aware migration; manual and AI text, source/action metadata, ordering, and application cascades are preserved.
|
- Moved append-only cover-letter revision history into a provider-aware migration; manual and AI text, source/action metadata, ordering, and application cascades are preserved.
|
||||||
- Moved durable interview-preparation items into a provider-aware migration; user and AI content, answers, practice state, sources, ordering, and application cascades are preserved.
|
- Moved durable interview-preparation items into a provider-aware migration; user and AI content, answers, practice state, sources, ordering, and application cascades are preserved.
|
||||||
- Moved the complete Career Profile aggregate into a provider-aware migration; canonical and long-tail JSON, version history, all six relational child types, stable ordering, indexes, and cascades are preserved.
|
- Moved the complete Career Profile aggregate into a provider-aware migration; canonical and long-tail JSON, version history, all six relational child types, stable ordering, indexes, and cascades are preserved.
|
||||||
|
- Completed JT-019 by moving all seven ASP.NET Identity tables into provider-aware migration ownership; accounts, credentials, 2FA state, preferences, roles, claims, external logins, tokens, indexes, and cascades are preserved. Startup schema code is now repair-only.
|
||||||
|
|
||||||
### In progress
|
### In progress
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ Updated: 2026-08-30
|
|||||||
- Focused frontend: 2 suites, 6 tests passed.
|
- Focused frontend: 2 suites, 6 tests passed.
|
||||||
- Full frontend: 64 suites, 272 tests passed.
|
- Full frontend: 64 suites, 272 tests passed.
|
||||||
- Next production build and TypeScript: passed.
|
- Next production build and TypeScript: passed.
|
||||||
- Full backend: 735/735 tests passed after the Career Profile aggregate JT-019 transfer.
|
- Full backend: 736/736 tests passed after completing the Identity/JT-019 ownership transfer.
|
||||||
- Portable Playwright launcher: resolved the user-local .NET 9 SDK; backend Release build passed with 0 warnings/errors.
|
- Portable Playwright launcher: resolved the user-local .NET 9 SDK; backend Release build passed with 0 warnings/errors.
|
||||||
- Playwright: initial full run 9/10 exposed the intentional mobile Settings control change; updated focused rerun passed 1/1. A final complete browser rerun remains in the end-of-batch gate.
|
- Playwright: initial full run 9/10 exposed the intentional mobile Settings control change; updated focused rerun passed 1/1. A final complete browser rerun remains in the end-of-batch gate.
|
||||||
- Focused backend match/intelligence verification: 34/34 passed.
|
- Focused backend match/intelligence verification: 34/34 passed.
|
||||||
|
|||||||
Reference in New Issue
Block a user