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:
cesnimda
2026-08-30 21:54:54 +02:00
parent a906916acc
commit 90ca23de33
13 changed files with 404 additions and 355 deletions
@@ -11,9 +11,9 @@ namespace JobTrackerApi.Migrations
// longtext OwnerUserId and then indexed (OwnerUserId, CareerProfileId, SortOrder) over it,
// which exceeds MySQL's 3072-byte key limit — a clean MariaDB install died here.
//
// The six CareerProfile child tables are provisioned by the idempotent reconciler in
// StartupInitializationExtensions, which carries correct DDL per provider. Existing databases
// already have this migration recorded as applied, so emptying it changes nothing for them.
// The six CareerProfile child tables were initially provisioned by the reconciler. Explicit
// ownership moved to 20260830133000_AdoptCareerProfileSchema; this historical migration stays
// recorded so existing chains remain compatible.
// docs/infrastructure/database-ownership.md.
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
@@ -7,6 +7,8 @@ namespace JobTrackerApi.Migrations
/// <inheritdoc />
public partial class SyncCareerChildKeyLengths : Migration
{
// Historical snapshot-only migration. Provider-safe ownership moved to
// 20260830133000_AdoptCareerProfileSchema.
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
@@ -0,0 +1,213 @@
using System;
using JobTrackerApi.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace JobTrackerApi.Migrations;
/// <summary>
/// Moves the durable Career Profile aggregate into migration ownership without replacing the
/// canonical profile, append-only history, or ordered relational children.
/// </summary>
[DbContext(typeof(JobTrackerContext))]
[Migration("20260830133000_AdoptCareerProfileSchema")]
public sealed class AdoptCareerProfileSchema : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase))
{
migrationBuilder.Sql(MySqlDdl);
return;
}
migrationBuilder.Sql(SqliteDdl);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Preserve user career history that may predate explicit migration ownership.
}
private const string SqliteDdl = """
CREATE TABLE IF NOT EXISTS "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 IF NOT EXISTS "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 IF NOT EXISTS "CareerExperiences" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CareerExperiences" 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,
CONSTRAINT "FK_CareerExperiences_CareerProfiles_CareerProfileId"
FOREIGN KEY ("CareerProfileId") REFERENCES "CareerProfiles" ("Id") ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS "CareerEducations" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CareerEducations" 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,
CONSTRAINT "FK_CareerEducations_CareerProfiles_CareerProfileId"
FOREIGN KEY ("CareerProfileId") REFERENCES "CareerProfiles" ("Id") ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS "CareerSkills" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CareerSkills" 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,
CONSTRAINT "FK_CareerSkills_CareerProfiles_CareerProfileId"
FOREIGN KEY ("CareerProfileId") REFERENCES "CareerProfiles" ("Id") ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS "CareerProjects" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CareerProjects" 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,
CONSTRAINT "FK_CareerProjects_CareerProfiles_CareerProfileId"
FOREIGN KEY ("CareerProfileId") REFERENCES "CareerProfiles" ("Id") ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS "CareerCertifications" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CareerCertifications" 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,
CONSTRAINT "FK_CareerCertifications_CareerProfiles_CareerProfileId"
FOREIGN KEY ("CareerProfileId") REFERENCES "CareerProfiles" ("Id") ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS "CareerLanguages" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CareerLanguages" 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,
CONSTRAINT "FK_CareerLanguages_CareerProfiles_CareerProfileId"
FOREIGN KEY ("CareerProfileId") REFERENCES "CareerProfiles" ("Id") ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS "IX_CareerProfiles_OwnerUserId"
ON "CareerProfiles" ("OwnerUserId");
CREATE INDEX IF NOT EXISTS "IX_CareerProfileVersions_OwnerUserId_CareerProfileId_Version"
ON "CareerProfileVersions" ("OwnerUserId", "CareerProfileId", "Version");
CREATE INDEX IF NOT EXISTS "IX_CareerExperiences_CareerProfileId" ON "CareerExperiences" ("CareerProfileId");
CREATE INDEX IF NOT EXISTS "IX_CareerExperiences_OwnerUserId_CareerProfileId_SortOrder" ON "CareerExperiences" ("OwnerUserId", "CareerProfileId", "SortOrder");
CREATE INDEX IF NOT EXISTS "IX_CareerEducations_CareerProfileId" ON "CareerEducations" ("CareerProfileId");
CREATE INDEX IF NOT EXISTS "IX_CareerEducations_OwnerUserId_CareerProfileId_SortOrder" ON "CareerEducations" ("OwnerUserId", "CareerProfileId", "SortOrder");
CREATE INDEX IF NOT EXISTS "IX_CareerSkills_CareerProfileId" ON "CareerSkills" ("CareerProfileId");
CREATE INDEX IF NOT EXISTS "IX_CareerSkills_OwnerUserId_CareerProfileId_SortOrder" ON "CareerSkills" ("OwnerUserId", "CareerProfileId", "SortOrder");
CREATE INDEX IF NOT EXISTS "IX_CareerProjects_CareerProfileId" ON "CareerProjects" ("CareerProfileId");
CREATE INDEX IF NOT EXISTS "IX_CareerProjects_OwnerUserId_CareerProfileId_SortOrder" ON "CareerProjects" ("OwnerUserId", "CareerProfileId", "SortOrder");
CREATE INDEX IF NOT EXISTS "IX_CareerCertifications_CareerProfileId" ON "CareerCertifications" ("CareerProfileId");
CREATE INDEX IF NOT EXISTS "IX_CareerCertifications_OwnerUserId_CareerProfileId_SortOrder" ON "CareerCertifications" ("OwnerUserId", "CareerProfileId", "SortOrder");
CREATE INDEX IF NOT EXISTS "IX_CareerLanguages_CareerProfileId" ON "CareerLanguages" ("CareerProfileId");
CREATE INDEX IF NOT EXISTS "IX_CareerLanguages_OwnerUserId_CareerProfileId_SortOrder" ON "CareerLanguages" ("OwnerUserId", "CareerProfileId", "SortOrder");
""";
private const string MySqlDdl = """
CREATE TABLE IF NOT EXISTS `CareerProfiles` (
`Id` int NOT NULL AUTO_INCREMENT, `OwnerUserId` varchar(255) NOT NULL,
`ProfileJson` longtext NOT NULL, `LongTailJson` longtext NOT NULL,
`Version` int NOT NULL, `CreatedAtUtc` datetime(6) NOT NULL,
`UpdatedAtUtc` datetime(6) NOT NULL, PRIMARY KEY (`Id`)
) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `CareerProfileVersions` (
`Id` int NOT NULL AUTO_INCREMENT, `OwnerUserId` varchar(255) NOT NULL,
`CareerProfileId` int NOT NULL, `Version` int NOT NULL, `ProfileJson` longtext NOT NULL,
`Source` varchar(100) NOT NULL, `CreatedAtUtc` datetime(6) NOT NULL, PRIMARY KEY (`Id`),
CONSTRAINT `FK_CareerProfileVersions_CareerProfiles_CareerProfileId`
FOREIGN KEY (`CareerProfileId`) REFERENCES `CareerProfiles` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `CareerExperiences` (
`Id` int NOT NULL AUTO_INCREMENT, `Title` longtext NULL, `Company` longtext NULL,
`Location` longtext NULL, `Start` longtext NULL, `End` longtext NULL,
`StartDate` longtext NULL, `EndDate` longtext NULL, `IsCurrent` tinyint(1) NOT NULL DEFAULT 0,
`BulletsJson` longtext NOT NULL, `SkillsJson` longtext NOT NULL,
`CareerProfileId` int NOT NULL, `OwnerUserId` varchar(255) NOT NULL,
`ItemKey` varchar(255) NOT NULL, `SortOrder` int NOT NULL, PRIMARY KEY (`Id`),
CONSTRAINT `FK_CareerExperiences_CareerProfiles_CareerProfileId`
FOREIGN KEY (`CareerProfileId`) REFERENCES `CareerProfiles` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `CareerEducations` (
`Id` int NOT NULL AUTO_INCREMENT, `Qualification` longtext NULL,
`QualificationLevel` longtext NULL, `Institution` longtext NULL, `Location` longtext NULL,
`Start` longtext NULL, `End` longtext NULL, `StartDate` longtext NULL, `EndDate` longtext NULL,
`DetailsJson` longtext NOT NULL, `CareerProfileId` int NOT NULL,
`OwnerUserId` varchar(255) NOT NULL, `ItemKey` varchar(255) NOT NULL,
`SortOrder` int NOT NULL, PRIMARY KEY (`Id`),
CONSTRAINT `FK_CareerEducations_CareerProfiles_CareerProfileId`
FOREIGN KEY (`CareerProfileId`) REFERENCES `CareerProfiles` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `CareerSkills` (
`Id` int NOT NULL AUTO_INCREMENT, `Name` longtext NULL, `Category` longtext NULL,
`Proficiency` longtext NULL, `CareerProfileId` int NOT NULL,
`OwnerUserId` varchar(255) NOT NULL, `ItemKey` varchar(255) NOT NULL,
`SortOrder` int NOT NULL, PRIMARY KEY (`Id`),
CONSTRAINT `FK_CareerSkills_CareerProfiles_CareerProfileId`
FOREIGN KEY (`CareerProfileId`) REFERENCES `CareerProfiles` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `CareerProjects` (
`Id` int NOT NULL AUTO_INCREMENT, `Name` longtext NULL, `Role` longtext NULL,
`Location` longtext NULL, `Start` longtext NULL, `End` longtext NULL,
`StartDate` longtext NULL, `EndDate` longtext NULL, `BulletsJson` longtext NOT NULL,
`SkillsJson` longtext NOT NULL, `LinksJson` longtext NOT NULL,
`CareerProfileId` int NOT NULL, `OwnerUserId` varchar(255) NOT NULL,
`ItemKey` varchar(255) NOT NULL, `SortOrder` int NOT NULL, PRIMARY KEY (`Id`),
CONSTRAINT `FK_CareerProjects_CareerProfiles_CareerProfileId`
FOREIGN KEY (`CareerProfileId`) REFERENCES `CareerProfiles` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `CareerCertifications` (
`Id` int NOT NULL AUTO_INCREMENT, `Name` longtext NULL, `Issuer` longtext NULL,
`Location` longtext NULL, `Date` longtext NULL, `DateNormalized` longtext NULL,
`DetailsJson` longtext NOT NULL, `CareerProfileId` int NOT NULL,
`OwnerUserId` varchar(255) NOT NULL, `ItemKey` varchar(255) NOT NULL,
`SortOrder` int NOT NULL, PRIMARY KEY (`Id`),
CONSTRAINT `FK_CareerCertifications_CareerProfiles_CareerProfileId`
FOREIGN KEY (`CareerProfileId`) REFERENCES `CareerProfiles` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE TABLE IF NOT EXISTS `CareerLanguages` (
`Id` int NOT NULL AUTO_INCREMENT, `Name` longtext NULL, `Level` longtext NULL,
`Notes` longtext NULL, `CareerProfileId` int NOT NULL,
`OwnerUserId` varchar(255) NOT NULL, `ItemKey` varchar(255) NOT NULL,
`SortOrder` int NOT NULL, PRIMARY KEY (`Id`),
CONSTRAINT `FK_CareerLanguages_CareerProfiles_CareerProfileId`
FOREIGN KEY (`CareerProfileId`) REFERENCES `CareerProfiles` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE UNIQUE INDEX IF NOT EXISTS `IX_CareerProfiles_OwnerUserId` ON `CareerProfiles` (`OwnerUserId`);
CREATE INDEX IF NOT EXISTS `IX_CareerProfileVersions_OwnerUserId_CareerProfileId_Version` ON `CareerProfileVersions` (`OwnerUserId`, `CareerProfileId`, `Version`);
CREATE INDEX IF NOT EXISTS `IX_CareerExperiences_CareerProfileId` ON `CareerExperiences` (`CareerProfileId`);
CREATE INDEX IF NOT EXISTS `IX_CareerExperiences_OwnerUserId_CareerProfileId_SortOrder` ON `CareerExperiences` (`OwnerUserId`, `CareerProfileId`, `SortOrder`);
CREATE INDEX IF NOT EXISTS `IX_CareerEducations_CareerProfileId` ON `CareerEducations` (`CareerProfileId`);
CREATE INDEX IF NOT EXISTS `IX_CareerEducations_OwnerUserId_CareerProfileId_SortOrder` ON `CareerEducations` (`OwnerUserId`, `CareerProfileId`, `SortOrder`);
CREATE INDEX IF NOT EXISTS `IX_CareerSkills_CareerProfileId` ON `CareerSkills` (`CareerProfileId`);
CREATE INDEX IF NOT EXISTS `IX_CareerSkills_OwnerUserId_CareerProfileId_SortOrder` ON `CareerSkills` (`OwnerUserId`, `CareerProfileId`, `SortOrder`);
CREATE INDEX IF NOT EXISTS `IX_CareerProjects_CareerProfileId` ON `CareerProjects` (`CareerProfileId`);
CREATE INDEX IF NOT EXISTS `IX_CareerProjects_OwnerUserId_CareerProfileId_SortOrder` ON `CareerProjects` (`OwnerUserId`, `CareerProfileId`, `SortOrder`);
CREATE INDEX IF NOT EXISTS `IX_CareerCertifications_CareerProfileId` ON `CareerCertifications` (`CareerProfileId`);
CREATE INDEX IF NOT EXISTS `IX_CareerCertifications_OwnerUserId_CareerProfileId_SortOrder` ON `CareerCertifications` (`OwnerUserId`, `CareerProfileId`, `SortOrder`);
CREATE INDEX IF NOT EXISTS `IX_CareerLanguages_CareerProfileId` ON `CareerLanguages` (`CareerProfileId`);
CREATE INDEX IF NOT EXISTS `IX_CareerLanguages_OwnerUserId_CareerProfileId_SortOrder` ON `CareerLanguages` (`OwnerUserId`, `CareerProfileId`, `SortOrder`);
""";
}