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
@@ -546,163 +546,11 @@ public static class StartupInitializationExtensions
EnsureColumn(c, "GmailConnections", "LastSyncError", "ALTER TABLE GmailConnections ADD COLUMN LastSyncError TEXT NULL;");
}
// Career Workspace foundation (docs/career-workspace-implementation-roadmap.md
// Phase F1). Additive tables: ApplicationUser.ProfileCvStructureJson remains the
// authoritative column every existing read path uses; these mirror it so future
// Career Workspace features (variants, history UI) have a real table to build on.
static void EnsureCareerProfileTables(DbConnection c)
// Preserve the one historical additive column repair while migrations own table creation.
static void ReconcileCareerProfileColumns(DbConnection c)
{
Exec(c, """
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
);
""");
// LongTailJson was added to the CareerProfile model (Phase 3) but this CREATE and
// the MySQL one were never updated to match, and no column-repair existed — so a
// CareerProfiles table created before this line lacks the column and /api/cv/outline
// (CareerProfileService.LoadStructuredAsync) fails with "Unknown column LongTailJson".
// Additive repair for existing databases.
EnsureColumn(c, "CareerProfiles", "LongTailJson", """ALTER TABLE "CareerProfiles" ADD COLUMN "LongTailJson" TEXT NOT NULL DEFAULT '';""");
Exec(c, """
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
);
""");
Exec(c, """CREATE UNIQUE INDEX IF NOT EXISTS "IX_CareerProfiles_OwnerUserId" ON "CareerProfiles" ("OwnerUserId");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerProfileVersions_OwnerUserId_CareerProfileId_Version" ON "CareerProfileVersions" ("OwnerUserId", "CareerProfileId", "Version");""");
// The six relational children of CareerProfile. Reconciler-owned: their migration
// is a no-op because it was scaffolded against SQLite and produced an unindexable
// longtext OwnerUserId on MariaDB. docs/infrastructure/database-ownership.md.
Exec(c, """
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
);
""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerExperiences_CareerProfileId" ON "CareerExperiences" ("CareerProfileId");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerExperiences_OwnerUserId_CareerProfileId_SortOrder" ON "CareerExperiences" ("OwnerUserId", "CareerProfileId", "SortOrder");""");
Exec(c, """
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
);
""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerEducations_CareerProfileId" ON "CareerEducations" ("CareerProfileId");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerEducations_OwnerUserId_CareerProfileId_SortOrder" ON "CareerEducations" ("OwnerUserId", "CareerProfileId", "SortOrder");""");
Exec(c, """
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
);
""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerSkills_CareerProfileId" ON "CareerSkills" ("CareerProfileId");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerSkills_OwnerUserId_CareerProfileId_SortOrder" ON "CareerSkills" ("OwnerUserId", "CareerProfileId", "SortOrder");""");
Exec(c, """
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
);
""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerProjects_CareerProfileId" ON "CareerProjects" ("CareerProfileId");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerProjects_OwnerUserId_CareerProfileId_SortOrder" ON "CareerProjects" ("OwnerUserId", "CareerProfileId", "SortOrder");""");
Exec(c, """
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
);
""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerCertifications_CareerProfileId" ON "CareerCertifications" ("CareerProfileId");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerCertifications_OwnerUserId_CareerProfileId_SortOrder" ON "CareerCertifications" ("OwnerUserId", "CareerProfileId", "SortOrder");""");
Exec(c, """
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
);
""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerLanguages_CareerProfileId" ON "CareerLanguages" ("CareerProfileId");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerLanguages_OwnerUserId_CareerProfileId_SortOrder" ON "CareerLanguages" ("OwnerUserId", "CareerProfileId", "SortOrder");""");
EnsureColumn(c, "CareerProfiles", "LongTailJson",
"""ALTER TABLE "CareerProfiles" ADD COLUMN "LongTailJson" TEXT NOT NULL DEFAULT '';""");
}
static void ReconcileAiInteractionUsageColumns(DbConnection c)
@@ -713,7 +561,7 @@ public static class StartupInitializationExtensions
}
ReconcileGmailConnectionColumns(conn);
EnsureCareerProfileTables(conn);
ReconcileCareerProfileColumns(conn);
ReconcileAiInteractionUsageColumns(conn);
// Once the base app tables exist, provision this reconciler-owned schema set and
@@ -849,154 +697,28 @@ public static class StartupInitializationExtensions
EnsureMySqlIndex(conn, "CareerProfileVersions", "IX_CareerProfileVersions_OwnerUserId_CareerProfileId_Version", "`OwnerUserId`, `CareerProfileId`, `Version`");
// The six relational children of CareerProfile — reconciler-owned (their
// migration is a no-op). varchar OwnerUserId/ItemKey so the composite index fits
// inside MariaDB's key limit. docs/infrastructure/database-ownership.md.
if (!HasMySqlTable(conn, "CareerExperiences") && HasMySqlTable(conn, "CareerProfiles"))
{
using var cmdCareerExperiences = conn.CreateCommand();
cmdCareerExperiences.CommandText = @"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
);";
cmdCareerExperiences.ExecuteNonQuery();
}
// Guarded historical shape/index repairs only. Creation belongs to the current
// provider-aware Career Profile adoption migration.
EnsureMySqlAutoIncrementPrimaryKey(conn, "CareerExperiences", "Id");
EnsureMySqlIndex(conn, "CareerExperiences", "IX_CareerExperiences_CareerProfileId", "`CareerProfileId`");
EnsureMySqlIndex(conn, "CareerExperiences", "IX_CareerExperiences_OwnerUserId_CareerProfileId_SortOrder", "`OwnerUserId`, `CareerProfileId`, `SortOrder`");
if (!HasMySqlTable(conn, "CareerEducations") && HasMySqlTable(conn, "CareerProfiles"))
{
using var cmdCareerEducations = conn.CreateCommand();
cmdCareerEducations.CommandText = @"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
);";
cmdCareerEducations.ExecuteNonQuery();
}
EnsureMySqlAutoIncrementPrimaryKey(conn, "CareerEducations", "Id");
EnsureMySqlIndex(conn, "CareerEducations", "IX_CareerEducations_CareerProfileId", "`CareerProfileId`");
EnsureMySqlIndex(conn, "CareerEducations", "IX_CareerEducations_OwnerUserId_CareerProfileId_SortOrder", "`OwnerUserId`, `CareerProfileId`, `SortOrder`");
if (!HasMySqlTable(conn, "CareerSkills") && HasMySqlTable(conn, "CareerProfiles"))
{
using var cmdCareerSkills = conn.CreateCommand();
cmdCareerSkills.CommandText = @"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
);";
cmdCareerSkills.ExecuteNonQuery();
}
EnsureMySqlAutoIncrementPrimaryKey(conn, "CareerSkills", "Id");
EnsureMySqlIndex(conn, "CareerSkills", "IX_CareerSkills_CareerProfileId", "`CareerProfileId`");
EnsureMySqlIndex(conn, "CareerSkills", "IX_CareerSkills_OwnerUserId_CareerProfileId_SortOrder", "`OwnerUserId`, `CareerProfileId`, `SortOrder`");
if (!HasMySqlTable(conn, "CareerProjects") && HasMySqlTable(conn, "CareerProfiles"))
{
using var cmdCareerProjects = conn.CreateCommand();
cmdCareerProjects.CommandText = @"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
);";
cmdCareerProjects.ExecuteNonQuery();
}
EnsureMySqlAutoIncrementPrimaryKey(conn, "CareerProjects", "Id");
EnsureMySqlIndex(conn, "CareerProjects", "IX_CareerProjects_CareerProfileId", "`CareerProfileId`");
EnsureMySqlIndex(conn, "CareerProjects", "IX_CareerProjects_OwnerUserId_CareerProfileId_SortOrder", "`OwnerUserId`, `CareerProfileId`, `SortOrder`");
if (!HasMySqlTable(conn, "CareerCertifications") && HasMySqlTable(conn, "CareerProfiles"))
{
using var cmdCareerCertifications = conn.CreateCommand();
cmdCareerCertifications.CommandText = @"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
);";
cmdCareerCertifications.ExecuteNonQuery();
}
EnsureMySqlAutoIncrementPrimaryKey(conn, "CareerCertifications", "Id");
EnsureMySqlIndex(conn, "CareerCertifications", "IX_CareerCertifications_CareerProfileId", "`CareerProfileId`");
EnsureMySqlIndex(conn, "CareerCertifications", "IX_CareerCertifications_OwnerUserId_CareerProfileId_SortOrder", "`OwnerUserId`, `CareerProfileId`, `SortOrder`");
if (!HasMySqlTable(conn, "CareerLanguages") && HasMySqlTable(conn, "CareerProfiles"))
{
using var cmdCareerLanguages = conn.CreateCommand();
cmdCareerLanguages.CommandText = @"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
);";
cmdCareerLanguages.ExecuteNonQuery();
}
EnsureMySqlAutoIncrementPrimaryKey(conn, "CareerLanguages", "Id");
EnsureMySqlIndex(conn, "CareerLanguages", "IX_CareerLanguages_CareerProfileId", "`CareerProfileId`");
EnsureMySqlIndex(conn, "CareerLanguages", "IX_CareerLanguages_OwnerUserId_CareerProfileId_SortOrder", "`OwnerUserId`, `CareerProfileId`, `SortOrder`");
@@ -1055,46 +777,11 @@ public static class StartupInitializationExtensions
EnsureMySqlColumn(conn, "GmailConnections", "LastSyncStatus", "ALTER TABLE `GmailConnections` ADD COLUMN `LastSyncStatus` varchar(255) NULL;");
EnsureMySqlColumn(conn, "GmailConnections", "LastSyncError", "ALTER TABLE `GmailConnections` ADD COLUMN `LastSyncError` longtext NULL;");
// Career Workspace foundation (docs/career-workspace-implementation-roadmap.md
// Phase F1). Additive: AspNetUsers.ProfileCvStructureJson stays authoritative
// for every existing read path during the dual-write window.
if (!HasMySqlTable(conn, "CareerProfiles"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"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`)
);";
cmd.ExecuteNonQuery();
}
// Additive repair for a CareerProfiles table created before LongTailJson was added
// to the model. Without it, /api/cv/outline fails with "Unknown column LongTailJson".
// DEFAULT '' backfills existing rows and matches the non-nullable model property.
EnsureMySqlColumn(conn, "CareerProfiles", "LongTailJson", "ALTER TABLE `CareerProfiles` ADD COLUMN `LongTailJson` longtext NOT NULL DEFAULT '';");
if (!HasMySqlTable(conn, "CareerProfileVersions") && HasMySqlTable(conn, "CareerProfiles"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"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
);";
cmd.ExecuteNonQuery();
}
// 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
@@ -1211,11 +898,8 @@ public static class StartupInitializationExtensions
}
}
// 1. Reconcile what already exists, and create the reconciler-owned tables. This has to run
// BEFORE Migrate(): legacy schemas need repairing so migrations do not collide with
// them, and AddCareerProfileRelationalChildren adds children that reference
// CareerProfiles, a reconciler-owned table. Anything that references a migration-owned
// table is skipped here (the parent does not exist yet) and picked up in pass 3.
// 1. Reconcile what already exists and create the remaining Identity-owned tables. This
// runs before Migrate() so legacy schemas are repaired before migrations inspect them.
ReconcileSchema();
// 2. Apply one migration at a time, reconciling after each. Some historical migrations