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
@@ -113,6 +113,8 @@ public sealed class MigrationChainTests
Assert.Contains("CREATE TABLE IF NOT EXISTS `TailoredCvDrafts`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `InterviewPrepNotes`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `AiWorkspaceNotes`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `CvVariants`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `CvVariantVersions`", script, StringComparison.Ordinal);
Assert.Contains("`UiLanguage` varchar(16)", script, StringComparison.Ordinal);
Assert.All(
Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value),
@@ -579,6 +581,81 @@ public sealed class MigrationChainTests
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
[Fact]
public async Task Cv_variant_adoption_preserves_settings_versions_and_relationships()
{
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
await using var db = Context(connection);
var migrator = db.GetService<IMigrator>();
await migrator.MigrateAsync("20260830127000_AdoptJobWorkspaceNotesSchema");
await ExecuteAsync(connection, """
INSERT INTO Companies (Name) VALUES ('Fixture company');
INSERT INTO JobApplications
(CompanyId, JobTitle, DateApplied, SavedAt, Status, ResponseReceived, OwnerUserId,
HasResume, HasCoverLetter, HasPortfolio, HasOtherAttachment, IsDeleted)
VALUES
(1, 'Fixture role', '2026-08-01T09:00:00', '2026-07-20T09:00:00',
'Applied', 0, 'owner-fixture', 0, 0, 0, 0, 0);
CREATE TABLE "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
);
CREATE TABLE "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
);
INSERT INTO "CvVariants"
("OwnerUserId", "PublicSlug", "Name", "JobApplicationId", "SettingsJson",
"IsPublic", "Version", "CreatedAtUtc", "UpdatedAtUtc")
VALUES ('owner-fixture', 'public-fixture', 'Backend CV', 1,
'{"templateId":"code","accent":"#123456"}', 1, 2,
'2026-08-30T09:00:00+00:00', '2026-08-30T09:10:00+00:00');
INSERT INTO "CvVariantVersions"
("OwnerUserId", "CvVariantId", "Version", "SettingsJson", "Source", "CreatedAtUtc")
VALUES ('owner-fixture', 1, 1, '{"templateId":"code"}', 'create',
'2026-08-30T09:00:00+00:00'),
('owner-fixture', 1, 2, '{"templateId":"code","accent":"#123456"}', 'autosave',
'2026-08-30T09:10:00+00:00');
""");
await migrator.MigrateAsync();
Assert.Equal("public-fixture", await ScalarAsync<string>(connection,
"SELECT PublicSlug FROM CvVariants WHERE Id = 1;"));
Assert.Equal(2L, await ScalarAsync<long>(connection,
"SELECT COUNT(*) FROM CvVariantVersions WHERE CvVariantId = 1;"));
await migrator.MigrateAsync("20260830127000_AdoptJobWorkspaceNotesSchema");
Assert.Equal("Backend CV", await ScalarAsync<string>(connection,
"SELECT Name FROM CvVariants WHERE Id = 1;"));
await migrator.MigrateAsync();
Assert.Equal(4L, await ScalarAsync<long>(connection, """
SELECT COUNT(*) FROM sqlite_master
WHERE type = 'index' AND name IN (
'IX_CvVariants_JobApplicationId',
'IX_CvVariants_OwnerUserId_UpdatedAtUtc',
'IX_CvVariants_PublicSlug',
'IX_CvVariantVersions_CvVariantId_Version');
"""));
await ExecuteAsync(connection, "DELETE FROM JobApplications WHERE Id = 1;");
Assert.Equal(1L, await ScalarAsync<long>(connection,
"SELECT COUNT(*) FROM CvVariants WHERE JobApplicationId IS NULL;"));
await ExecuteAsync(connection, "DELETE FROM CvVariants WHERE Id = 1;");
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM CvVariantVersions;"));
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
private static JobTrackerContext Context(SqliteConnection connection)
{
var currentUser = new Mock<ICurrentUserService>();