refactor(db): migrate career profile aggregate
Transfer the canonical career profile, revision history, and six relational child collections to provider-aware migration ownership. Preserve legacy JSON, ordered child data, indexes, and cascade semantics.
This commit is contained in:
@@ -118,6 +118,14 @@ public sealed class MigrationChainTests
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `ApplicationChecklistItems`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CoverLetterVersions`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `InterviewPrepItems`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerProfiles`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerProfileVersions`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerExperiences`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerEducations`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerSkills`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerProjects`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerCertifications`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerLanguages`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("`UiLanguage` varchar(16)", script, StringComparison.Ordinal);
|
||||
Assert.All(
|
||||
Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value),
|
||||
@@ -878,6 +886,135 @@ public sealed class MigrationChainTests
|
||||
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Career_profile_adoption_preserves_canonical_history_and_relational_children()
|
||||
{
|
||||
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("20260830132000_AdoptInterviewPrepItemSchema");
|
||||
await ExecuteAsync(connection, """
|
||||
CREATE TABLE "CareerProfiles" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_CareerProfiles" PRIMARY KEY AUTOINCREMENT,
|
||||
"OwnerUserId" TEXT NOT NULL, "ProfileJson" TEXT NOT NULL,
|
||||
"LongTailJson" TEXT NOT NULL DEFAULT '', "Version" INTEGER NOT NULL,
|
||||
"CreatedAtUtc" TEXT NOT NULL, "UpdatedAtUtc" TEXT NOT NULL);
|
||||
CREATE TABLE "CareerProfileVersions" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_CareerProfileVersions" PRIMARY KEY AUTOINCREMENT,
|
||||
"OwnerUserId" TEXT NOT NULL, "CareerProfileId" INTEGER NOT NULL,
|
||||
"Version" INTEGER NOT NULL, "ProfileJson" TEXT NOT NULL, "Source" TEXT NOT NULL,
|
||||
"CreatedAtUtc" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_CareerProfileVersions_CareerProfiles_CareerProfileId"
|
||||
FOREIGN KEY ("CareerProfileId") REFERENCES "CareerProfiles" ("Id") ON DELETE CASCADE);
|
||||
CREATE TABLE "CareerExperiences" (
|
||||
"Id" INTEGER PRIMARY KEY AUTOINCREMENT, "Title" TEXT NULL, "Company" TEXT NULL,
|
||||
"Location" TEXT NULL, "Start" TEXT NULL, "End" TEXT NULL, "StartDate" TEXT NULL,
|
||||
"EndDate" TEXT NULL, "IsCurrent" INTEGER NOT NULL DEFAULT 0,
|
||||
"BulletsJson" TEXT NOT NULL DEFAULT '[]', "SkillsJson" TEXT NOT NULL DEFAULT '[]',
|
||||
"CareerProfileId" INTEGER NOT NULL, "OwnerUserId" TEXT NOT NULL,
|
||||
"ItemKey" TEXT NOT NULL, "SortOrder" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("CareerProfileId") REFERENCES "CareerProfiles" ("Id") ON DELETE CASCADE);
|
||||
CREATE TABLE "CareerEducations" (
|
||||
"Id" INTEGER PRIMARY KEY AUTOINCREMENT, "Qualification" TEXT NULL,
|
||||
"QualificationLevel" TEXT NULL, "Institution" TEXT NULL, "Location" TEXT NULL,
|
||||
"Start" TEXT NULL, "End" TEXT NULL, "StartDate" TEXT NULL, "EndDate" TEXT NULL,
|
||||
"DetailsJson" TEXT NOT NULL DEFAULT '[]', "CareerProfileId" INTEGER NOT NULL,
|
||||
"OwnerUserId" TEXT NOT NULL, "ItemKey" TEXT NOT NULL, "SortOrder" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("CareerProfileId") REFERENCES "CareerProfiles" ("Id") ON DELETE CASCADE);
|
||||
CREATE TABLE "CareerSkills" (
|
||||
"Id" INTEGER PRIMARY KEY AUTOINCREMENT, "Name" TEXT NULL, "Category" TEXT NULL,
|
||||
"Proficiency" TEXT NULL, "CareerProfileId" INTEGER NOT NULL,
|
||||
"OwnerUserId" TEXT NOT NULL, "ItemKey" TEXT NOT NULL, "SortOrder" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("CareerProfileId") REFERENCES "CareerProfiles" ("Id") ON DELETE CASCADE);
|
||||
CREATE TABLE "CareerProjects" (
|
||||
"Id" INTEGER PRIMARY KEY AUTOINCREMENT, "Name" TEXT NULL, "Role" TEXT NULL,
|
||||
"Location" TEXT NULL, "Start" TEXT NULL, "End" TEXT NULL, "StartDate" TEXT NULL,
|
||||
"EndDate" TEXT NULL, "BulletsJson" TEXT NOT NULL DEFAULT '[]',
|
||||
"SkillsJson" TEXT NOT NULL DEFAULT '[]', "LinksJson" TEXT NOT NULL DEFAULT '[]',
|
||||
"CareerProfileId" INTEGER NOT NULL, "OwnerUserId" TEXT NOT NULL,
|
||||
"ItemKey" TEXT NOT NULL, "SortOrder" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("CareerProfileId") REFERENCES "CareerProfiles" ("Id") ON DELETE CASCADE);
|
||||
CREATE TABLE "CareerCertifications" (
|
||||
"Id" INTEGER PRIMARY KEY AUTOINCREMENT, "Name" TEXT NULL, "Issuer" TEXT NULL,
|
||||
"Location" TEXT NULL, "Date" TEXT NULL, "DateNormalized" TEXT NULL,
|
||||
"DetailsJson" TEXT NOT NULL DEFAULT '[]', "CareerProfileId" INTEGER NOT NULL,
|
||||
"OwnerUserId" TEXT NOT NULL, "ItemKey" TEXT NOT NULL, "SortOrder" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("CareerProfileId") REFERENCES "CareerProfiles" ("Id") ON DELETE CASCADE);
|
||||
CREATE TABLE "CareerLanguages" (
|
||||
"Id" INTEGER PRIMARY KEY AUTOINCREMENT, "Name" TEXT NULL, "Level" TEXT NULL,
|
||||
"Notes" TEXT NULL, "CareerProfileId" INTEGER NOT NULL, "OwnerUserId" TEXT NOT NULL,
|
||||
"ItemKey" TEXT NOT NULL, "SortOrder" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("CareerProfileId") REFERENCES "CareerProfiles" ("Id") ON DELETE CASCADE);
|
||||
|
||||
INSERT INTO "CareerProfiles" VALUES
|
||||
(1, 'owner-fixture', '{"summary":"Canonical profile"}',
|
||||
'{"interests":["friluftsliv"]}', 3,
|
||||
'2026-08-01T09:00:00+00:00', '2026-08-30T09:00:00+00:00');
|
||||
INSERT INTO "CareerProfileVersions"
|
||||
("OwnerUserId", "CareerProfileId", "Version", "ProfileJson", "Source", "CreatedAtUtc")
|
||||
VALUES ('owner-fixture', 1, 2, '{"summary":"Previous version"}', 'manual',
|
||||
'2026-08-20T09:00:00+00:00');
|
||||
INSERT INTO "CareerExperiences"
|
||||
("Title", "Company", "IsCurrent", "BulletsJson", "SkillsJson", "CareerProfileId",
|
||||
"OwnerUserId", "ItemKey", "SortOrder")
|
||||
VALUES ('Systemutvikler', 'Eksempel AS', 1, '["Bygget pålitelig API"]', '[".NET"]',
|
||||
1, 'owner-fixture', 'experience-1', 10);
|
||||
INSERT INTO "CareerEducations"
|
||||
("Qualification", "Institution", "DetailsJson", "CareerProfileId", "OwnerUserId", "ItemKey", "SortOrder")
|
||||
VALUES ('Bachelor', 'Høgskolen', '["Programmering"]', 1, 'owner-fixture', 'education-1', 20);
|
||||
INSERT INTO "CareerSkills"
|
||||
("Name", "Category", "CareerProfileId", "OwnerUserId", "ItemKey", "SortOrder")
|
||||
VALUES ('C#', 'Backend', 1, 'owner-fixture', 'skill-1', 30);
|
||||
INSERT INTO "CareerProjects"
|
||||
("Name", "BulletsJson", "SkillsJson", "LinksJson", "CareerProfileId", "OwnerUserId", "ItemKey", "SortOrder")
|
||||
VALUES ('Jobbjakt', '["Dynamisk CV"]', '["React"]', '["https://example.test"]',
|
||||
1, 'owner-fixture', 'project-1', 40);
|
||||
INSERT INTO "CareerCertifications"
|
||||
("Name", "Issuer", "DetailsJson", "CareerProfileId", "OwnerUserId", "ItemKey", "SortOrder")
|
||||
VALUES ('Azure', 'Microsoft', '[]', 1, 'owner-fixture', 'certification-1', 50);
|
||||
INSERT INTO "CareerLanguages"
|
||||
("Name", "Level", "Notes", "CareerProfileId", "OwnerUserId", "ItemKey", "SortOrder")
|
||||
VALUES ('Norsk', 'B2', 'Aktivt lærende', 1, 'owner-fixture', 'language-1', 60);
|
||||
""");
|
||||
|
||||
await migrator.MigrateAsync();
|
||||
Assert.Equal("{\"interests\":[\"friluftsliv\"]}", await ScalarAsync<string>(connection,
|
||||
"SELECT LongTailJson FROM CareerProfiles WHERE Id = 1;"));
|
||||
Assert.Equal("Previous version", await ScalarAsync<string>(connection,
|
||||
"SELECT json_extract(ProfileJson, '$.summary') FROM CareerProfileVersions WHERE Version = 2;"));
|
||||
Assert.Equal(6L, await ScalarAsync<long>(connection, """
|
||||
SELECT (SELECT COUNT(*) FROM CareerExperiences) +
|
||||
(SELECT COUNT(*) FROM CareerEducations) +
|
||||
(SELECT COUNT(*) FROM CareerSkills) +
|
||||
(SELECT COUNT(*) FROM CareerProjects) +
|
||||
(SELECT COUNT(*) FROM CareerCertifications) +
|
||||
(SELECT COUNT(*) FROM CareerLanguages);
|
||||
"""));
|
||||
|
||||
await migrator.MigrateAsync("20260830132000_AdoptInterviewPrepItemSchema");
|
||||
Assert.Equal("Eksempel AS", await ScalarAsync<string>(connection,
|
||||
"SELECT Company FROM CareerExperiences WHERE ItemKey = 'experience-1';"));
|
||||
await migrator.MigrateAsync();
|
||||
Assert.Equal(14L, await ScalarAsync<long>(connection, """
|
||||
SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name IN (
|
||||
'IX_CareerProfiles_OwnerUserId',
|
||||
'IX_CareerProfileVersions_OwnerUserId_CareerProfileId_Version',
|
||||
'IX_CareerExperiences_CareerProfileId', 'IX_CareerExperiences_OwnerUserId_CareerProfileId_SortOrder',
|
||||
'IX_CareerEducations_CareerProfileId', 'IX_CareerEducations_OwnerUserId_CareerProfileId_SortOrder',
|
||||
'IX_CareerSkills_CareerProfileId', 'IX_CareerSkills_OwnerUserId_CareerProfileId_SortOrder',
|
||||
'IX_CareerProjects_CareerProfileId', 'IX_CareerProjects_OwnerUserId_CareerProfileId_SortOrder',
|
||||
'IX_CareerCertifications_CareerProfileId', 'IX_CareerCertifications_OwnerUserId_CareerProfileId_SortOrder',
|
||||
'IX_CareerLanguages_CareerProfileId', 'IX_CareerLanguages_OwnerUserId_CareerProfileId_SortOrder');
|
||||
"""));
|
||||
|
||||
await ExecuteAsync(connection, "DELETE FROM CareerProfiles WHERE Id = 1;");
|
||||
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM CareerProfileVersions;"));
|
||||
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM CareerExperiences;"));
|
||||
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM CareerLanguages;"));
|
||||
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
|
||||
}
|
||||
|
||||
private static JobTrackerContext Context(SqliteConnection connection)
|
||||
{
|
||||
var currentUser = new Mock<ICurrentUserService>();
|
||||
|
||||
Reference in New Issue
Block a user