refactor(db): migrate job workspace notes

Move interview preparation and AI workspace output persistence into an additive provider-aware migration. Preserve reviewed results, uniqueness, and application cascade semantics.
This commit is contained in:
cesnimda
2026-08-30 17:22:04 +02:00
parent 9922426aa5
commit 778b365f9e
9 changed files with 186 additions and 93 deletions
@@ -705,50 +705,6 @@ public static class StartupInitializationExtensions
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerLanguages_OwnerUserId_CareerProfileId_SortOrder" ON "CareerLanguages" ("OwnerUserId", "CareerProfileId", "SortOrder");""");
}
// Interview prep persistence (career-workspace-implementation-roadmap.md Phase F5):
// stop re-running the AI call on every tab open by persisting the last generated
// note per job, keyed by the attachment selection it was generated from.
static void EnsureInterviewPrepNotesTable(DbConnection c)
{
Exec(c, """
CREATE TABLE IF NOT EXISTS "InterviewPrepNotes" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_InterviewPrepNotes" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"AttachmentContextSignature" TEXT NOT NULL,
"Summary" TEXT NOT NULL,
"TalkingPointsJson" TEXT NOT NULL,
"LikelyQuestionsJson" TEXT NOT NULL,
"WeakSpotsJson" TEXT NOT NULL,
"GeneratedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_InterviewPrepNotes_JobApplications_JobApplicationId" FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """CREATE UNIQUE INDEX IF NOT EXISTS "IX_InterviewPrepNotes_OwnerUserId_JobApplicationId" ON "InterviewPrepNotes" ("OwnerUserId", "JobApplicationId");""");
}
// Generic AI workspace note persistence (candidate fit, focus plan) -- same
// rationale as EnsureInterviewPrepNotesTable, generalized for DTOs too irregular
// for per-field columns.
static void EnsureAiWorkspaceNotesTable(DbConnection c)
{
Exec(c, """
CREATE TABLE IF NOT EXISTS "AiWorkspaceNotes" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_AiWorkspaceNotes" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"NoteType" TEXT NOT NULL,
"AttachmentContextSignature" TEXT NOT NULL,
"ResultJson" TEXT NOT NULL,
"GeneratedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_AiWorkspaceNotes_JobApplications_JobApplicationId" FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """CREATE UNIQUE INDEX IF NOT EXISTS "IX_AiWorkspaceNotes_OwnerUserId_JobApplicationId_NoteType" ON "AiWorkspaceNotes" ("OwnerUserId", "JobApplicationId", "NoteType");""");
}
// Phase 4 CV Builder: a variant is a lens over the master profile; versions are its
// autosave history. Reconciler-owned (not migration-owned) so the schema is MySQL-safe
// on prod -- see the AddCvVariants migration note. docs/architecture/cv-builder.md.
@@ -885,8 +841,6 @@ public static class StartupInitializationExtensions
ReconcileGmailConnectionColumns(conn);
EnsureCareerProfileTables(conn);
EnsureInterviewPrepNotesTable(conn);
EnsureAiWorkspaceNotesTable(conn);
EnsureCvBuilderTables(conn);
EnsureAiInteractionsTable(conn);
EnsureApplicationChecklistTable(conn);
@@ -1273,44 +1227,6 @@ public static class StartupInitializationExtensions
cmd.ExecuteNonQuery();
}
// Interview prep persistence (career-workspace-implementation-roadmap.md Phase F5).
if (!HasMySqlTable(conn, "InterviewPrepNotes") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `InterviewPrepNotes` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`AttachmentContextSignature` longtext NOT NULL,
`Summary` longtext NOT NULL,
`TalkingPointsJson` longtext NOT NULL,
`LikelyQuestionsJson` longtext NOT NULL,
`WeakSpotsJson` longtext NOT NULL,
`GeneratedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_InterviewPrepNotes_JobApplications_JobApplicationId` FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
);";
cmd.ExecuteNonQuery();
}
// Generic AI workspace note persistence (candidate fit, focus plan).
if (!HasMySqlTable(conn, "AiWorkspaceNotes") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `AiWorkspaceNotes` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`NoteType` varchar(50) NOT NULL,
`AttachmentContextSignature` longtext NOT NULL,
`ResultJson` longtext NOT NULL,
`GeneratedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_AiWorkspaceNotes_JobApplications_JobApplicationId` FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
);";
cmd.ExecuteNonQuery();
}
// Phase 4 CV Builder + Phase 5 AI Workspace. Reconciler-owned rather than
// migration-owned: those migrations were scaffolded against SQLite, so on MariaDB
// they emit TEXT datetimes and a PK with no AUTO_INCREMENT, and the composite
@@ -12,6 +12,7 @@ internal static class StartupSchemaOwnership
"AccountDeletionFiles",
"AccountDeletionRequests",
"AiUsageRecords",
"AiWorkspaceNotes",
"Attachments",
"Companies",
"Correspondences",
@@ -22,6 +23,7 @@ internal static class StartupSchemaOwnership
"GmailConnections",
"GmailReviewDecisions",
"ImapConnections",
"InterviewPrepNotes",
"JobApplications",
"JobEvents",
"Jobs",
@@ -40,7 +42,6 @@ internal static class StartupSchemaOwnership
internal static readonly IReadOnlySet<string> ReconcilerOwnedTables = new HashSet<string>(StringComparer.Ordinal)
{
"AiInteractions",
"AiWorkspaceNotes",
"ApplicationChecklistItems",
"AspNetRoleClaims",
"AspNetRoles",
@@ -61,7 +62,6 @@ internal static class StartupSchemaOwnership
"CvVariants",
"CvVariantVersions",
"InterviewPrepItems",
"InterviewPrepNotes",
};
// Historical migrations contain guarded compatibility bootstraps for these tables so direct