fix(db): provision CV builder + AI workspace tables via the MySQL-safe reconciler
CI and Deploy / test (push) Successful in 2m19s
CI and Deploy / deploy (push) Failing after 7s

Deploy failed on prod (MariaDB) while the test job was green: backend startup
threw during Database.Migrate(), so deploy.sh's post-deploy health check exited.

Root cause: AddCvVariants/AddAiInteractions were scaffolded against SQLite, so
they bake SQLite type names into their DDL — DateTimeOffset emits `TEXT`, bool/int
emit `INTEGER`, and the PK gets no AUTO_INCREMENT. Run against MariaDB that yields
a structurally wrong table, and the composite index over a TEXT column then trips
"ERROR 1071: Specified key was too long; max key length is 3072 bytes". SQLite
accepts all of it, which is why local/container verification passed.

Fix, following the pattern already used for CareerProfiles/AiWorkspaceNotes:
- both migrations become no-ops; the three tables are reconciler-owned
- reconciler provisions them idempotently per dialect (MySQL: varchar/int
  AUTO_INCREMENT/datetime(6); SQLite: CREATE TABLE IF NOT EXISTS)
- DropMalformedMySqlTable rebuilds a half-built table left by the failed
  migration, guarded on row count so a table with ANY rows is never dropped
- bound the indexed string columns with HasMaxLength so the model matches

Verified against a real MariaDB 11 container: reproduced error 1071, then
confirmed the corrected DDL yields auto_increment PKs, varchar/datetime columns
and all previously-failing indexes. 306 backend tests green; SQLite container
starts clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-18 20:41:29 +02:00
parent 7a74311a71
commit 1430313a20
4 changed files with 206 additions and 113 deletions
@@ -8,90 +8,20 @@ namespace JobTrackerApi.Migrations
/// <inheritdoc />
public partial class AddCvVariants : Migration
{
// Intentionally a no-op. CvVariants/CvVariantVersions are provisioned by the reconciler in
// StartupInitializationExtensions (idempotent, MySQL-safe: varchar not longtext so MariaDB can
// index PublicSlug/OwnerUserId). The original CreateTable + CREATE INDEX here indexed unbounded
// string columns, which succeeds on SQLite but throws on MariaDB ("BLOB/TEXT column used in key
// specification without a key length"), crashing backend startup on prod. Reconciler-owned now,
// like CareerProfiles/AiWorkspaceNotes. This migration stays recorded so the chain is intact.
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CvVariants",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
OwnerUserId = table.Column<string>(type: "TEXT", nullable: false),
PublicSlug = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
JobApplicationId = table.Column<int>(type: "INTEGER", nullable: true),
SettingsJson = table.Column<string>(type: "TEXT", nullable: false),
IsPublic = table.Column<bool>(type: "INTEGER", nullable: false),
Version = table.Column<int>(type: "INTEGER", nullable: false),
CreatedAtUtc = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
UpdatedAtUtc = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CvVariants", x => x.Id);
table.ForeignKey(
name: "FK_CvVariants_JobApplications_JobApplicationId",
column: x => x.JobApplicationId,
principalTable: "JobApplications",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "CvVariantVersions",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
OwnerUserId = table.Column<string>(type: "TEXT", nullable: false),
CvVariantId = table.Column<int>(type: "INTEGER", nullable: false),
Version = table.Column<int>(type: "INTEGER", nullable: false),
SettingsJson = table.Column<string>(type: "TEXT", nullable: false),
Source = table.Column<string>(type: "TEXT", nullable: false),
CreatedAtUtc = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CvVariantVersions", x => x.Id);
table.ForeignKey(
name: "FK_CvVariantVersions_CvVariants_CvVariantId",
column: x => x.CvVariantId,
principalTable: "CvVariants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_CvVariants_JobApplicationId",
table: "CvVariants",
column: "JobApplicationId");
migrationBuilder.CreateIndex(
name: "IX_CvVariants_OwnerUserId_UpdatedAtUtc",
table: "CvVariants",
columns: new[] { "OwnerUserId", "UpdatedAtUtc" });
migrationBuilder.CreateIndex(
name: "IX_CvVariants_PublicSlug",
table: "CvVariants",
column: "PublicSlug",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_CvVariantVersions_CvVariantId_Version",
table: "CvVariantVersions",
columns: new[] { "CvVariantId", "Version" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CvVariantVersions");
migrationBuilder.DropTable(
name: "CvVariants");
}
}
}
@@ -8,51 +8,18 @@ namespace JobTrackerApi.Migrations
/// <inheritdoc />
public partial class AddAiInteractions : Migration
{
// Intentionally a no-op — AiInteractions is provisioned by the reconciler in
// StartupInitializationExtensions (idempotent, MySQL-safe: Module/OwnerUserId are varchar so
// MariaDB can index them). The original CREATE INDEX here indexed unbounded string columns,
// which crashes backend startup on MariaDB. See the AddCvVariants migration for the full note.
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AiInteractions",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
OwnerUserId = table.Column<string>(type: "TEXT", nullable: false),
JobApplicationId = table.Column<int>(type: "INTEGER", nullable: false),
Module = table.Column<string>(type: "TEXT", nullable: false),
Mode = table.Column<string>(type: "TEXT", nullable: true),
Title = table.Column<string>(type: "TEXT", nullable: false),
Provider = table.Column<string>(type: "TEXT", nullable: false),
ResultJson = table.Column<string>(type: "TEXT", nullable: false),
CreatedAtUtc = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AiInteractions", x => x.Id);
table.ForeignKey(
name: "FK_AiInteractions_JobApplications_JobApplicationId",
column: x => x.JobApplicationId,
principalTable: "JobApplications",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AiInteractions_JobApplicationId",
table: "AiInteractions",
column: "JobApplicationId");
migrationBuilder.CreateIndex(
name: "IX_AiInteractions_OwnerUserId_JobApplicationId_Module_CreatedAtUtc",
table: "AiInteractions",
columns: new[] { "OwnerUserId", "JobApplicationId", "Module", "CreatedAtUtc" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AiInteractions");
}
}
}