refactor(db): migrate interview preparation

Move durable user-owned interview preparation into an additive provider-aware migration. Preserve questions, answers, practice state, ordering, and application cascade semantics.
This commit is contained in:
cesnimda
2026-08-30 18:58:55 +02:00
parent 934b35a1e0
commit a906916acc
10 changed files with 148 additions and 55 deletions
@@ -0,0 +1,70 @@
using System;
using JobTrackerApi.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace JobTrackerApi.Migrations;
/// <summary>
/// Moves durable user-owned interview preparation into migration ownership without replacing
/// questions, answers, research, or practice state.
/// </summary>
[DbContext(typeof(JobTrackerContext))]
[Migration("20260830132000_AdoptInterviewPrepItemSchema")]
public sealed class AdoptInterviewPrepItemSchema : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase))
{
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS `InterviewPrepItems` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`Category` varchar(32) NOT NULL,
`Title` varchar(500) NOT NULL,
`Content` longtext NULL,
`Source` varchar(16) NOT NULL,
`IsPrepared` tinyint(1) NOT NULL,
`SortOrder` int NOT NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
`UpdatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_InterviewPrepItems_JobApplications_JobApplicationId`
FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE INDEX IF NOT EXISTS `IX_InterviewPrepItems_Owner_Job_Sort`
ON `InterviewPrepItems` (`OwnerUserId`, `JobApplicationId`, `SortOrder`);
""");
return;
}
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS "InterviewPrepItems" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_InterviewPrepItems" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"Category" TEXT NOT NULL,
"Title" TEXT NOT NULL,
"Content" TEXT NULL,
"Source" TEXT NOT NULL,
"IsPrepared" INTEGER NOT NULL,
"SortOrder" INTEGER NOT NULL,
"CreatedAtUtc" TEXT NOT NULL,
"UpdatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_InterviewPrepItems_JobApplications_JobApplicationId"
FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "IX_InterviewPrepItems_Owner_Job_Sort"
ON "InterviewPrepItems" ("OwnerUserId", "JobApplicationId", "SortOrder");
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Preserve durable user-authored preparation from pre-migration installations.
}
}
+1 -2
View File
@@ -8,8 +8,7 @@ namespace JobTrackerApi.Models;
// own questions — and nothing regenerates it.
//
// One table for every category rather than a table per category: they differ only by label, and a new
// category must not need a migration. Reconciler-owned
// (docs/infrastructure/database-ownership.md); its migration is a no-op.
// category must not need a migration. Migration-owned through an additive provider-aware adoption.
public sealed class InterviewPrepItem
{
public int Id { get; set; }
@@ -712,32 +712,9 @@ public static class StartupInitializationExtensions
EnsureColumn(c, "AiInteractions", "EstimatedTokenCount", "ALTER TABLE AiInteractions ADD COLUMN EstimatedTokenCount INTEGER NOT NULL DEFAULT 0;");
}
// Phase 5.5: user-owned interview preparation (not the AI cache InterviewPrepNote).
static void EnsureInterviewPrepItemsTable(DbConnection c)
{
Exec(c, """
CREATE TABLE IF NOT EXISTS "InterviewPrepItems" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_InterviewPrepItems" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"Category" TEXT NOT NULL,
"Title" TEXT NOT NULL,
"Content" TEXT NULL,
"Source" TEXT NOT NULL,
"IsPrepared" INTEGER NOT NULL,
"SortOrder" INTEGER NOT NULL,
"CreatedAtUtc" TEXT NOT NULL,
"UpdatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_InterviewPrepItems_JobApplications_JobApplicationId" FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_InterviewPrepItems_Owner_Job_Sort" ON "InterviewPrepItems" ("OwnerUserId", "JobApplicationId", "SortOrder");""");
}
ReconcileGmailConnectionColumns(conn);
EnsureCareerProfileTables(conn);
ReconcileAiInteractionUsageColumns(conn);
EnsureInterviewPrepItemsTable(conn);
// Once the base app tables exist, provision this reconciler-owned schema set and
// stamp its historical migration before later migrations rebuild JobApplications.
@@ -1131,27 +1108,6 @@ public static class StartupInitializationExtensions
DropMalformedMySqlTable(conn, "CoverLetterVersions", "CreatedAtUtc", "datetime");
DropMalformedMySqlTable(conn, "InterviewPrepItems", "CreatedAtUtc", "datetime");
if (!HasMySqlTable(conn, "InterviewPrepItems") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `InterviewPrepItems` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`Category` varchar(32) NOT NULL,
`Title` varchar(500) NOT NULL,
`Content` longtext NULL,
`Source` varchar(16) NOT NULL,
`IsPrepared` tinyint(1) NOT NULL,
`SortOrder` int NOT NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
`UpdatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_InterviewPrepItems_JobApplications_JobApplicationId` FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
);";
cmd.ExecuteNonQuery();
}
EnsureMySqlAutoIncrementPrimaryKey(conn, "InterviewPrepItems", "Id");
EnsureMySqlIndex(conn, "InterviewPrepItems", "IX_InterviewPrepItems_Owner_Job_Sort", "`OwnerUserId`, `JobApplicationId`, `SortOrder`");
@@ -29,6 +29,7 @@ internal static class StartupSchemaOwnership
"GmailReviewDecisions",
"ImapConnections",
"InterviewPrepNotes",
"InterviewPrepItems",
"JobApplications",
"JobEvents",
"Jobs",
@@ -61,7 +62,6 @@ internal static class StartupSchemaOwnership
"CareerProfileVersions",
"CareerProjects",
"CareerSkills",
"InterviewPrepItems",
};
// Historical migrations contain guarded compatibility bootstraps for these tables so direct