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:
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using JobTrackerApi.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JobTrackerApi.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// Moves persisted job-workspace AI outputs into migration ownership without replacing existing
|
||||
/// interview preparation, candidate-fit, or focus-plan content.
|
||||
/// </summary>
|
||||
[DbContext(typeof(JobTrackerContext))]
|
||||
[Migration("20260830127000_AdoptJobWorkspaceNotesSchema")]
|
||||
public sealed class AdoptJobWorkspaceNotesSchema : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
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
|
||||
) CHARACTER SET=utf8mb4;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS `IX_InterviewPrepNotes_OwnerUserId_JobApplicationId`
|
||||
ON `InterviewPrepNotes` (`OwnerUserId`(191), `JobApplicationId`);
|
||||
|
||||
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
|
||||
) CHARACTER SET=utf8mb4;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS `IX_AiWorkspaceNotes_OwnerUserId_JobApplicationId_NoteType`
|
||||
ON `AiWorkspaceNotes` (`OwnerUserId`(191), `JobApplicationId`, `NoteType`);
|
||||
""");
|
||||
return;
|
||||
}
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
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
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "IX_InterviewPrepNotes_OwnerUserId_JobApplicationId"
|
||||
ON "InterviewPrepNotes" ("OwnerUserId", "JobApplicationId");
|
||||
|
||||
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
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "IX_AiWorkspaceNotes_OwnerUserId_JobApplicationId_NoteType"
|
||||
ON "AiWorkspaceNotes" ("OwnerUserId", "JobApplicationId", "NoteType");
|
||||
""");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Preserve generated and user-reviewed job-workspace notes from older installations.
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user