refactor(db): migrate CV variant history

Move saved CVs and append-only revisions into an additive provider-aware migration. Preserve public slugs, builder settings, job-link nulling, and revision cascades.
This commit is contained in:
cesnimda
2026-08-30 17:28:35 +02:00
parent 778b365f9e
commit bc28893b6f
9 changed files with 206 additions and 92 deletions
@@ -705,44 +705,6 @@ public static class StartupInitializationExtensions
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerLanguages_OwnerUserId_CareerProfileId_SortOrder" ON "CareerLanguages" ("OwnerUserId", "CareerProfileId", "SortOrder");""");
}
// 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.
static void EnsureCvBuilderTables(DbConnection c)
{
Exec(c, """
CREATE TABLE IF NOT EXISTS "CvVariants" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CvVariants" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"PublicSlug" TEXT NOT NULL,
"Name" TEXT NOT NULL,
"JobApplicationId" INTEGER NULL,
"SettingsJson" TEXT NOT NULL,
"IsPublic" INTEGER NOT NULL,
"Version" INTEGER NOT NULL,
"CreatedAtUtc" TEXT NOT NULL,
"UpdatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_CvVariants_JobApplications_JobApplicationId" FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE SET NULL
);
""");
Exec(c, """
CREATE TABLE IF NOT EXISTS "CvVariantVersions" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CvVariantVersions" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"CvVariantId" INTEGER NOT NULL,
"Version" INTEGER NOT NULL,
"SettingsJson" TEXT NOT NULL,
"Source" TEXT NOT NULL,
"CreatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_CvVariantVersions_CvVariants_CvVariantId" FOREIGN KEY ("CvVariantId") REFERENCES "CvVariants" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CvVariants_JobApplicationId" ON "CvVariants" ("JobApplicationId");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CvVariants_OwnerUserId_UpdatedAtUtc" ON "CvVariants" ("OwnerUserId", "UpdatedAtUtc");""");
Exec(c, """CREATE UNIQUE INDEX IF NOT EXISTS "IX_CvVariants_PublicSlug" ON "CvVariants" ("PublicSlug");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CvVariantVersions_CvVariantId_Version" ON "CvVariantVersions" ("CvVariantId", "Version");""");
}
// Phase 5 AI Workspace: append-only AI interaction history per job application.
static void EnsureAiInteractionsTable(DbConnection c)
{
@@ -841,7 +803,6 @@ public static class StartupInitializationExtensions
ReconcileGmailConnectionColumns(conn);
EnsureCareerProfileTables(conn);
EnsureCvBuilderTables(conn);
EnsureAiInteractionsTable(conn);
EnsureApplicationChecklistTable(conn);
EnsureCoverLetterVersionsTable(conn);
@@ -1227,12 +1188,10 @@ public static class StartupInitializationExtensions
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
// index over a TEXT column then exceeds MySQL's 3072-byte key limit -- which
// crashed backend startup on prod. Drop any such half-built table (only when it
// holds no rows) and rebuild from the correct MySQL DDL below.
// Historical Phase 4/5 migrations were scaffolded against SQLite, so on MariaDB
// they could emit TEXT datetimes and a PK with no AUTO_INCREMENT, then fail while
// indexing TEXT columns. Drop only malformed empty tables before the current
// provider-aware adoption migrations run; populated tables are never replaced.
// Children first: CvVariantVersions FKs into CvVariants.
DropMalformedMySqlTable(conn, "CvVariantVersions", "CreatedAtUtc", "datetime");
DropMalformedMySqlTable(conn, "CvVariants", "UpdatedAtUtc", "datetime");
@@ -1241,43 +1200,6 @@ public static class StartupInitializationExtensions
DropMalformedMySqlTable(conn, "CoverLetterVersions", "CreatedAtUtc", "datetime");
DropMalformedMySqlTable(conn, "InterviewPrepItems", "CreatedAtUtc", "datetime");
if (!HasMySqlTable(conn, "CvVariants") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `CvVariants` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`PublicSlug` varchar(64) NOT NULL,
`Name` varchar(255) NOT NULL,
`JobApplicationId` int NULL,
`SettingsJson` longtext NOT NULL,
`IsPublic` tinyint(1) NOT NULL,
`Version` int NOT NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
`UpdatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_CvVariants_JobApplications_JobApplicationId` FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE SET NULL
);";
cmd.ExecuteNonQuery();
}
if (!HasMySqlTable(conn, "CvVariantVersions") && HasMySqlTable(conn, "CvVariants"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `CvVariantVersions` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`CvVariantId` int NOT NULL,
`Version` int NOT NULL,
`SettingsJson` longtext NOT NULL,
`Source` varchar(100) NOT NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_CvVariantVersions_CvVariants_CvVariantId` FOREIGN KEY (`CvVariantId`) REFERENCES `CvVariants` (`Id`) ON DELETE CASCADE
);";
cmd.ExecuteNonQuery();
}
if (!HasMySqlTable(conn, "AiInteractions") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();