fix(db): repair fresh migration chain
CI and Deploy / test (pull_request) Successful in 5m19s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-15 20:47:36 +02:00
parent 191de69c48
commit 74a1e0d845
15 changed files with 297 additions and 35 deletions
@@ -11,6 +11,7 @@ namespace JobTrackerApi.Migrations
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
var mysql = ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase);
migrationBuilder.CreateTable(
name: "Companies",
columns: table => new
@@ -39,7 +40,20 @@ namespace JobTrackerApi.Migrations
ResponseDate = table.Column<DateTime>(type: "TEXT", nullable: true),
Notes = table.Column<string>(type: "TEXT", nullable: true),
CoverLetterText = table.Column<string>(type: "TEXT", nullable: true),
JobUrl = table.Column<string>(type: "TEXT", nullable: true)
JobUrl = table.Column<string>(type: "TEXT", nullable: true),
// These stable columns predated EF ownership and were historically supplied by
// startup reconciliation. Include them for new databases so the later SQLite
// DateApplied rebuild has a complete source shape under standalone EF tooling.
OwnerUserId = table.Column<string>(type: mysql ? "varchar(255)" : "TEXT", nullable: true),
ShortSummary = table.Column<string>(type: mysql ? "longtext" : "TEXT", nullable: true),
TailoredCvText = table.Column<string>(type: mysql ? "longtext" : "TEXT", nullable: true),
TailoredCvUpdatedAt = table.Column<DateTime>(type: mysql ? "datetime(6)" : "TEXT", nullable: true),
LastReminderEmailSentAt = table.Column<DateTime>(type: mysql ? "datetime(6)" : "TEXT", nullable: true),
RecruiterMessageDraft = table.Column<string>(type: mysql ? "longtext" : "TEXT", nullable: true),
SalaryMin = table.Column<decimal>(type: mysql ? "decimal(18,2)" : "TEXT", nullable: true),
SalaryMax = table.Column<decimal>(type: mysql ? "decimal(18,2)" : "TEXT", nullable: true),
SalaryCurrency = table.Column<string>(type: mysql ? "varchar(8)" : "TEXT", nullable: true),
SalaryPeriod = table.Column<string>(type: mysql ? "varchar(16)" : "TEXT", nullable: true)
},
constraints: table =>
{
@@ -12,6 +12,50 @@ namespace JobTrackerApi.Migrations
protected override void Up(MigrationBuilder migrationBuilder)
{
var mysql = ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase);
// Identity historically came from startup reconciliation rather than an EF migration.
// Ensure the base user table exists so a blank standalone EF chain can reach this
// additive migration. Normal application startup already created it, making this a no-op.
migrationBuilder.Sql(mysql
? """
CREATE TABLE IF NOT EXISTS `AspNetUsers` (
`Id` varchar(255) NOT NULL,
`UserName` varchar(256) NULL,
`NormalizedUserName` varchar(256) NULL,
`Email` varchar(256) NULL,
`NormalizedEmail` varchar(256) NULL,
`EmailConfirmed` tinyint(1) NOT NULL,
`PasswordHash` longtext NULL,
`SecurityStamp` longtext NULL,
`ConcurrencyStamp` longtext NULL,
`PhoneNumber` longtext NULL,
`PhoneNumberConfirmed` tinyint(1) NOT NULL,
`TwoFactorEnabled` tinyint(1) NOT NULL,
`LockoutEnd` datetime(6) NULL,
`LockoutEnabled` tinyint(1) NOT NULL,
`AccessFailedCount` int NOT NULL,
CONSTRAINT `PK_AspNetUsers` PRIMARY KEY (`Id`)
) CHARACTER SET=utf8mb4;
"""
: """
CREATE TABLE IF NOT EXISTS "AspNetUsers" (
"Id" TEXT NOT NULL CONSTRAINT "PK_AspNetUsers" PRIMARY KEY,
"UserName" TEXT NULL,
"NormalizedUserName" TEXT NULL,
"Email" TEXT NULL,
"NormalizedEmail" TEXT NULL,
"EmailConfirmed" INTEGER NOT NULL,
"PasswordHash" TEXT NULL,
"SecurityStamp" TEXT NULL,
"ConcurrencyStamp" TEXT NULL,
"PhoneNumber" TEXT NULL,
"PhoneNumberConfirmed" INTEGER NOT NULL,
"TwoFactorEnabled" INTEGER NOT NULL,
"LockoutEnd" TEXT NULL,
"LockoutEnabled" INTEGER NOT NULL,
"AccessFailedCount" INTEGER NOT NULL
);
""");
migrationBuilder.AddColumn<string>(
name: "PendingEmail",
table: "AspNetUsers",
@@ -47,7 +47,7 @@ namespace JobTrackerApi.Migrations
`ByteSize` bigint NOT NULL,
`Sha256` char(64) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
CONSTRAINT `PK_AccountDeletionFiles` PRIMARY KEY (`Id`),
CONSTRAINT `FK_AccountDeletionFiles_AccountDeletionRequests_AccountDeletionRequestId`
CONSTRAINT `FK_AccountDeletionFiles_Request`
FOREIGN KEY (`AccountDeletionRequestId`) REFERENCES `AccountDeletionRequests` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
""");
@@ -11,6 +11,49 @@ namespace JobTrackerApi.Migrations
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// AiInteractions was historically reconciler-owned and its migration is intentionally
// a no-op. A blank standalone EF chain still needs an empty source table for the
// content-free legacy backfill below; application startup already created the same
// table, so CREATE IF NOT EXISTS preserves existing rows and mixed-version startup.
migrationBuilder.Sql(ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase)
? """
CREATE TABLE IF NOT EXISTS `AiInteractions` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`Module` varchar(64) NOT NULL,
`Mode` longtext NULL,
`Title` longtext NOT NULL,
`Provider` longtext NOT NULL,
`ResultJson` longtext NOT NULL,
`InputCharacterCount` int NOT NULL DEFAULT 0,
`OutputCharacterCount` int NOT NULL DEFAULT 0,
`EstimatedTokenCount` int NOT NULL DEFAULT 0,
`CreatedAtUtc` datetime(6) NOT NULL,
CONSTRAINT `PK_AiInteractions` PRIMARY KEY (`Id`),
CONSTRAINT `FK_AiInteractions_JobApplications_JobApplicationId`
FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
"""
: """
CREATE TABLE IF NOT EXISTS "AiInteractions" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_AiInteractions" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"Module" TEXT NOT NULL,
"Mode" TEXT NULL,
"Title" TEXT NOT NULL,
"Provider" TEXT NOT NULL,
"ResultJson" TEXT NOT NULL,
"InputCharacterCount" INTEGER NOT NULL DEFAULT 0,
"OutputCharacterCount" INTEGER NOT NULL DEFAULT 0,
"EstimatedTokenCount" INTEGER NOT NULL DEFAULT 0,
"CreatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_AiInteractions_JobApplications_JobApplicationId"
FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
""");
if (ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase))
{
migrationBuilder.Sql("""
@@ -402,8 +402,6 @@ public static class StartupInitializationExtensions
{
// EF migrations are used for the app schema. In some environments `dotnet ef` isnt available,
// so create the ASP.NET Core Identity tables directly if they dont exist yet.
if (HasTable(c, "AspNetUsers")) return;
Exec(c, """
CREATE TABLE IF NOT EXISTS "AspNetRoles" (
"Id" TEXT NOT NULL CONSTRAINT "PK_AspNetRoles" PRIMARY KEY,
@@ -2014,11 +2012,24 @@ public static class StartupInitializationExtensions
{
using var migrationScope = app.Services.CreateScope();
var migrationDb = migrationScope.ServiceProvider.GetRequiredService<JobTrackerContext>();
var migrator = migrationDb.Database.GetService<IMigrator>();
while (migrationDb.Database.GetPendingMigrations().FirstOrDefault() is { } migration)
if (useSqliteBootstrap)
{
migrator.Migrate(migration);
ReconcileSchema();
var migrator = migrationDb.Database.GetService<IMigrator>();
while (migrationDb.Database.GetPendingMigrations().FirstOrDefault() is { } migration)
{
migrator.Migrate(migration);
ReconcileSchema();
}
}
else
{
// MariaDB ALTER operations do not rebuild tables from a snapshot, so they do
// not need SQLite's per-migration reconciliation. Reconciling between its
// historical migrations can add a later column (for example Companies.Source)
// immediately before the migration that owns it, producing a duplicate-column
// failure on a clean database. Apply the chain first, then use the common final
// reconciliation pass for provider-safe repairs and reconciler-owned tables.
migrationDb.Database.Migrate();
}
}
catch (Exception ex)