refactor(db): migrate AI interaction history

Move append-only AI results into an additive provider-aware migration. Preserve generated content, usage counters, indexes, and application cascade semantics.
This commit is contained in:
cesnimda
2026-08-30 18:48:42 +02:00
parent bc28893b6f
commit ae57d1afde
9 changed files with 148 additions and 54 deletions
@@ -705,31 +705,11 @@ public static class StartupInitializationExtensions
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerLanguages_OwnerUserId_CareerProfileId_SortOrder" ON "CareerLanguages" ("OwnerUserId", "CareerProfileId", "SortOrder");""");
}
// Phase 5 AI Workspace: append-only AI interaction history per job application.
static void EnsureAiInteractionsTable(DbConnection c)
static void ReconcileAiInteractionUsageColumns(DbConnection c)
{
Exec(c, """
CREATE TABLE IF NOT EXISTS "AiInteractions" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_AiInteractions" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"Module" TEXT NOT NULL,
"Mode" TEXT NULL,
"Title" TEXT NOT NULL,
"Provider" TEXT NOT NULL,
"ResultJson" TEXT NOT NULL,
"InputCharacterCount" INTEGER NOT NULL DEFAULT 0,
"OutputCharacterCount" INTEGER NOT NULL DEFAULT 0,
"EstimatedTokenCount" INTEGER NOT NULL DEFAULT 0,
"CreatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_AiInteractions_JobApplications_JobApplicationId" FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
""");
EnsureColumn(c, "AiInteractions", "InputCharacterCount", "ALTER TABLE AiInteractions ADD COLUMN InputCharacterCount INTEGER NOT NULL DEFAULT 0;");
EnsureColumn(c, "AiInteractions", "OutputCharacterCount", "ALTER TABLE AiInteractions ADD COLUMN OutputCharacterCount INTEGER NOT NULL DEFAULT 0;");
EnsureColumn(c, "AiInteractions", "EstimatedTokenCount", "ALTER TABLE AiInteractions ADD COLUMN EstimatedTokenCount INTEGER NOT NULL DEFAULT 0;");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_AiInteractions_JobApplicationId" ON "AiInteractions" ("JobApplicationId");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_AiInteractions_Owner_Job_Module_Created" ON "AiInteractions" ("OwnerUserId", "JobApplicationId", "Module", "CreatedAtUtc");""");
}
// Phase 5 Milestone 2: the application checklist (workflow guidance over readiness signals).
@@ -803,7 +783,7 @@ public static class StartupInitializationExtensions
ReconcileGmailConnectionColumns(conn);
EnsureCareerProfileTables(conn);
EnsureAiInteractionsTable(conn);
ReconcileAiInteractionUsageColumns(conn);
EnsureApplicationChecklistTable(conn);
EnsureCoverLetterVersionsTable(conn);
EnsureInterviewPrepItemsTable(conn);
@@ -1200,28 +1180,6 @@ public static class StartupInitializationExtensions
DropMalformedMySqlTable(conn, "CoverLetterVersions", "CreatedAtUtc", "datetime");
DropMalformedMySqlTable(conn, "InterviewPrepItems", "CreatedAtUtc", "datetime");
if (!HasMySqlTable(conn, "AiInteractions") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `AiInteractions` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`Module` varchar(64) NOT NULL,
`Mode` varchar(64) NULL,
`Title` varchar(255) NOT NULL,
`Provider` varchar(100) NOT NULL,
`ResultJson` longtext NOT NULL,
`InputCharacterCount` int NOT NULL DEFAULT 0,
`OutputCharacterCount` int NOT NULL DEFAULT 0,
`EstimatedTokenCount` int NOT NULL DEFAULT 0,
`CreatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_AiInteractions_JobApplications_JobApplicationId` FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
);";
cmd.ExecuteNonQuery();
}
if (!HasMySqlTable(conn, "ApplicationChecklistItems") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();
@@ -13,6 +13,7 @@ internal static class StartupSchemaOwnership
"AccountDeletionRequests",
"AiUsageRecords",
"AiWorkspaceNotes",
"AiInteractions",
"Attachments",
"Companies",
"Correspondences",
@@ -43,7 +44,6 @@ internal static class StartupSchemaOwnership
internal static readonly IReadOnlySet<string> ReconcilerOwnedTables = new HashSet<string>(StringComparer.Ordinal)
{
"AiInteractions",
"ApplicationChecklistItems",
"AspNetRoleClaims",
"AspNetRoles",
@@ -67,5 +67,5 @@ internal static class StartupSchemaOwnership
// Historical migrations contain guarded compatibility bootstraps for these tables so direct
// EF tooling can traverse the chain. Their current creation owner remains the reconciler.
internal static readonly IReadOnlySet<string> MigrationCompatibilityBootstrapTables =
new HashSet<string>(StringComparer.Ordinal) { "AiInteractions", "AspNetUsers" };
new HashSet<string>(StringComparer.Ordinal) { "AspNetUsers" };
}