refactor(db): migrate tailored CV drafts

Move job-specific tailored documents into an additive provider-aware migration. Preserve edited content, indexes, and application cascade semantics.
This commit is contained in:
cesnimda
2026-08-30 17:03:48 +02:00
parent e28cc0ad47
commit 9922426aa5
9 changed files with 159 additions and 63 deletions
@@ -0,0 +1,84 @@
using System;
using JobTrackerApi.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace JobTrackerApi.Migrations;
/// <summary>
/// Moves job-specific tailored CV drafts into migration ownership without replacing existing
/// generated or manually edited document content.
/// </summary>
[DbContext(typeof(JobTrackerContext))]
[Migration("20260830126000_AdoptTailoredCvDraftSchema")]
public sealed class AdoptTailoredCvDraftSchema : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase))
{
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS `TailoredCvDrafts` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`CanonicalProfileVersion` int NULL,
`TemplateId` varchar(100) NOT NULL,
`Headline` longtext NULL,
`SummaryJson` longtext NULL,
`SelectedSkillsJson` longtext NULL,
`ExperienceJson` longtext NULL,
`EducationJson` longtext NULL,
`CustomSectionsJson` longtext NULL,
`RenderOptionsJson` longtext NULL,
`GenerationContextHash` longtext NULL,
`LastGeneratedAtUtc` datetime(6) NULL,
`LastEditedAtUtc` datetime(6) NULL,
`Status` varchar(100) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_TailoredCvDrafts_JobApplications_JobApplicationId`
FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE UNIQUE INDEX IF NOT EXISTS `IX_TailoredCvDrafts_OwnerUserId_JobApplicationId`
ON `TailoredCvDrafts` (`OwnerUserId`(191), `JobApplicationId`);
CREATE INDEX IF NOT EXISTS `IX_TailoredCvDrafts_JobApplicationId`
ON `TailoredCvDrafts` (`JobApplicationId`);
""");
return;
}
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS "TailoredCvDrafts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_TailoredCvDrafts" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"CanonicalProfileVersion" INTEGER NULL,
"TemplateId" TEXT NOT NULL,
"Headline" TEXT NULL,
"SummaryJson" TEXT NULL,
"SelectedSkillsJson" TEXT NULL,
"ExperienceJson" TEXT NULL,
"EducationJson" TEXT NULL,
"CustomSectionsJson" TEXT NULL,
"RenderOptionsJson" TEXT NULL,
"GenerationContextHash" TEXT NULL,
"LastGeneratedAtUtc" TEXT NULL,
"LastEditedAtUtc" TEXT NULL,
"Status" TEXT NOT NULL,
CONSTRAINT "FK_TailoredCvDrafts_JobApplications_JobApplicationId"
FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS "IX_TailoredCvDrafts_OwnerUserId_JobApplicationId"
ON "TailoredCvDrafts" ("OwnerUserId", "JobApplicationId");
CREATE INDEX IF NOT EXISTS "IX_TailoredCvDrafts_JobApplicationId"
ON "TailoredCvDrafts" ("JobApplicationId");
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Preserve tailored document content that may pre-date migration ownership.
}
}
@@ -546,34 +546,6 @@ public static class StartupInitializationExtensions
EnsureColumn(c, "GmailConnections", "LastSyncError", "ALTER TABLE GmailConnections ADD COLUMN LastSyncError TEXT NULL;");
}
static void EnsureCvTables(DbConnection c)
{
Exec(c, """
CREATE TABLE IF NOT EXISTS "TailoredCvDrafts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_TailoredCvDrafts" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"CanonicalProfileVersion" INTEGER NULL,
"TemplateId" TEXT NOT NULL,
"Headline" TEXT NULL,
"SummaryJson" TEXT NULL,
"SelectedSkillsJson" TEXT NULL,
"ExperienceJson" TEXT NULL,
"EducationJson" TEXT NULL,
"CustomSectionsJson" TEXT NULL,
"RenderOptionsJson" TEXT NULL,
"GenerationContextHash" TEXT NULL,
"LastGeneratedAtUtc" TEXT NULL,
"LastEditedAtUtc" TEXT NULL,
"Status" TEXT NOT NULL,
CONSTRAINT "FK_TailoredCvDrafts_JobApplications_JobApplicationId" FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """CREATE UNIQUE INDEX IF NOT EXISTS "IX_TailoredCvDrafts_OwnerUserId_JobApplicationId" ON "TailoredCvDrafts" ("OwnerUserId", "JobApplicationId");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_TailoredCvDrafts_JobApplicationId" ON "TailoredCvDrafts" ("JobApplicationId");""");
}
// Career Workspace foundation (docs/career-workspace-implementation-roadmap.md
// Phase F1). Additive tables: ApplicationUser.ProfileCvStructureJson remains the
// authoritative column every existing read path uses; these mirror it so future
@@ -912,7 +884,6 @@ public static class StartupInitializationExtensions
}
ReconcileGmailConnectionColumns(conn);
EnsureCvTables(conn);
EnsureCareerProfileTables(conn);
EnsureInterviewPrepNotesTable(conn);
EnsureAiWorkspaceNotesTable(conn);
@@ -1261,32 +1232,6 @@ public static class StartupInitializationExtensions
EnsureMySqlColumn(conn, "GmailConnections", "LastSyncStatus", "ALTER TABLE `GmailConnections` ADD COLUMN `LastSyncStatus` varchar(255) NULL;");
EnsureMySqlColumn(conn, "GmailConnections", "LastSyncError", "ALTER TABLE `GmailConnections` ADD COLUMN `LastSyncError` longtext NULL;");
if (!HasMySqlTable(conn, "TailoredCvDrafts") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `TailoredCvDrafts` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`CanonicalProfileVersion` int NULL,
`TemplateId` varchar(100) NOT NULL,
`Headline` longtext NULL,
`SummaryJson` longtext NULL,
`SelectedSkillsJson` longtext NULL,
`ExperienceJson` longtext NULL,
`EducationJson` longtext NULL,
`CustomSectionsJson` longtext NULL,
`RenderOptionsJson` longtext NULL,
`GenerationContextHash` longtext NULL,
`LastGeneratedAtUtc` datetime(6) NULL,
`LastEditedAtUtc` datetime(6) NULL,
`Status` varchar(100) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_TailoredCvDrafts_JobApplications_JobApplicationId` FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
);";
cmd.ExecuteNonQuery();
}
// Career Workspace foundation (docs/career-workspace-implementation-roadmap.md
// Phase F1). Additive: AspNetUsers.ProfileCvStructureJson stays authoritative
// for every existing read path during the dual-write window.
@@ -28,6 +28,7 @@ internal static class StartupSchemaOwnership
"MicrosoftGraphConnections",
"RuleSettings",
"SystemEmailSettings",
"TailoredCvDrafts",
"TrustedDevices",
"TwoFactorRecoveryCodes",
"UserNotifications",
@@ -61,7 +62,6 @@ internal static class StartupSchemaOwnership
"CvVariantVersions",
"InterviewPrepItems",
"InterviewPrepNotes",
"TailoredCvDrafts",
};
// Historical migrations contain guarded compatibility bootstraps for these tables so direct