refactor(db): migrate cover letter history

Move append-only cover letter revisions into an additive provider-aware migration. Preserve manual and AI text, source metadata, ordering, and application cascade semantics.
This commit is contained in:
cesnimda
2026-08-30 18:55:20 +02:00
parent e933409698
commit 934b35a1e0
10 changed files with 137 additions and 47 deletions
@@ -0,0 +1,64 @@
using System;
using JobTrackerApi.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace JobTrackerApi.Migrations;
/// <summary>
/// Moves append-only cover-letter revision history into migration ownership without replacing
/// manually edited or AI-approved document versions.
/// </summary>
[DbContext(typeof(JobTrackerContext))]
[Migration("20260830131000_AdoptCoverLetterVersionSchema")]
public sealed class AdoptCoverLetterVersionSchema : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase))
{
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS `CoverLetterVersions` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`Version` int NOT NULL,
`Text` longtext NOT NULL,
`Source` varchar(32) NOT NULL,
`AiAction` varchar(32) NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_CoverLetterVersions_JobApplications_JobApplicationId`
FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE INDEX IF NOT EXISTS `IX_CoverLetterVersions_Owner_Job_Version`
ON `CoverLetterVersions` (`OwnerUserId`, `JobApplicationId`, `Version`);
""");
return;
}
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS "CoverLetterVersions" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CoverLetterVersions" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"Version" INTEGER NOT NULL,
"Text" TEXT NOT NULL,
"Source" TEXT NOT NULL,
"AiAction" TEXT NULL,
"CreatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_CoverLetterVersions_JobApplications_JobApplicationId"
FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "IX_CoverLetterVersions_Owner_Job_Version"
ON "CoverLetterVersions" ("OwnerUserId", "JobApplicationId", "Version");
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Preserve recoverable document history created before migration ownership.
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ namespace JobTrackerApi.Models;
// to be, so an AI rewrite or a bad edit is never destructive. Mirrors CvVariantVersion: restore
// re-saves an old snapshot as a new version rather than rewinding.
//
// Reconciler-owned (docs/infrastructure/database-ownership.md) — its migration is a no-op.
// Migration-owned; the historical no-op is followed by an additive provider-aware adoption.
public sealed class CoverLetterVersion
{
public int Id { get; set; }
@@ -712,25 +712,6 @@ public static class StartupInitializationExtensions
EnsureColumn(c, "AiInteractions", "EstimatedTokenCount", "ALTER TABLE AiInteractions ADD COLUMN EstimatedTokenCount INTEGER NOT NULL DEFAULT 0;");
}
// Phase 5.4: append-only cover letter history.
static void EnsureCoverLetterVersionsTable(DbConnection c)
{
Exec(c, """
CREATE TABLE IF NOT EXISTS "CoverLetterVersions" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CoverLetterVersions" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"Version" INTEGER NOT NULL,
"Text" TEXT NOT NULL,
"Source" TEXT NOT NULL,
"AiAction" TEXT NULL,
"CreatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_CoverLetterVersions_JobApplications_JobApplicationId" FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CoverLetterVersions_Owner_Job_Version" ON "CoverLetterVersions" ("OwnerUserId", "JobApplicationId", "Version");""");
}
// Phase 5.5: user-owned interview preparation (not the AI cache InterviewPrepNote).
static void EnsureInterviewPrepItemsTable(DbConnection c)
{
@@ -756,7 +737,6 @@ public static class StartupInitializationExtensions
ReconcileGmailConnectionColumns(conn);
EnsureCareerProfileTables(conn);
ReconcileAiInteractionUsageColumns(conn);
EnsureCoverLetterVersionsTable(conn);
EnsureInterviewPrepItemsTable(conn);
// Once the base app tables exist, provision this reconciler-owned schema set and
@@ -1151,24 +1131,6 @@ public static class StartupInitializationExtensions
DropMalformedMySqlTable(conn, "CoverLetterVersions", "CreatedAtUtc", "datetime");
DropMalformedMySqlTable(conn, "InterviewPrepItems", "CreatedAtUtc", "datetime");
if (!HasMySqlTable(conn, "CoverLetterVersions") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `CoverLetterVersions` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`Version` int NOT NULL,
`Text` longtext NOT NULL,
`Source` varchar(32) NOT NULL,
`AiAction` varchar(32) NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_CoverLetterVersions_JobApplications_JobApplicationId` FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
);";
cmd.ExecuteNonQuery();
}
if (!HasMySqlTable(conn, "InterviewPrepItems") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();
@@ -18,6 +18,7 @@ internal static class StartupSchemaOwnership
"Attachments",
"Companies",
"Correspondences",
"CoverLetterVersions",
"CvExtractionRuns",
"CvUploadArtifacts",
"CvVariants",
@@ -60,7 +61,6 @@ internal static class StartupSchemaOwnership
"CareerProfileVersions",
"CareerProjects",
"CareerSkills",
"CoverLetterVersions",
"InterviewPrepItems",
};