diff --git a/JobTrackerApi/Services/StartupInitializationExtensions.cs b/JobTrackerApi/Services/StartupInitializationExtensions.cs index c3cf867..3665ba8 100644 --- a/JobTrackerApi/Services/StartupInitializationExtensions.cs +++ b/JobTrackerApi/Services/StartupInitializationExtensions.cs @@ -977,105 +977,61 @@ public static class StartupInitializationExtensions cmd.ExecuteNonQuery(); } - if (!MySqlIndexExists(conn, "Companies", "IX_Companies_OwnerUserId")) + // Schema reconciliation must never crash app startup: an index that fails + // (e.g. combined key exceeds MySQL's 3072-byte limit because an older + // migration made OwnerUserId wider than the varchar(255) this reconciler + // assumes) is logged and skipped rather than taking prod down. OwnerUserId + // is prefix-indexed at 191 chars (safe under utf8mb4's 767-byte legacy + // per-column key limit, and far longer than the GUID-like Identity ids + // actually stored there) so composite indexes stay well under the cap + // regardless of the column's declared width. + void TryCreateIndex(string table, string indexName, string columnsSql) { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "CREATE INDEX `IX_Companies_OwnerUserId` ON `Companies` (`OwnerUserId`);"; - cmd.ExecuteNonQuery(); + if (MySqlIndexExists(conn, table, indexName)) return; + try + { + using var cmd = conn.CreateCommand(); + cmd.CommandText = $"CREATE INDEX `{indexName}` ON `{table}` ({columnsSql});"; + cmd.ExecuteNonQuery(); + } + catch (Exception ex) + { + app.Logger.LogWarning(ex, "Skipping index {Index} on {Table} during startup reconciliation.", indexName, table); + } } - if (!MySqlIndexExists(conn, "JobApplications", "IX_JobApplications_OwnerUserId")) + void TryCreateUniqueIndex(string table, string indexName, string columnsSql) { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "CREATE INDEX `IX_JobApplications_OwnerUserId` ON `JobApplications` (`OwnerUserId`);"; - cmd.ExecuteNonQuery(); + if (MySqlIndexExists(conn, table, indexName)) return; + try + { + using var cmd = conn.CreateCommand(); + cmd.CommandText = $"CREATE UNIQUE INDEX `{indexName}` ON `{table}` ({columnsSql});"; + cmd.ExecuteNonQuery(); + } + catch (Exception ex) + { + app.Logger.LogWarning(ex, "Skipping unique index {Index} on {Table} during startup reconciliation.", indexName, table); + } } + TryCreateIndex("Companies", "IX_Companies_OwnerUserId", "`OwnerUserId`(191)"); + TryCreateIndex("JobApplications", "IX_JobApplications_OwnerUserId", "`OwnerUserId`(191)"); + // Hot-path composite indexes for tenant-scoped list/board/stats/analytics // (OwnerUserId + IsDeleted) and reminders (OwnerUserId + FollowUpAt). - if (!MySqlIndexExists(conn, "JobApplications", "IX_JobApplications_OwnerUserId_IsDeleted")) - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "CREATE INDEX `IX_JobApplications_OwnerUserId_IsDeleted` ON `JobApplications` (`OwnerUserId`, `IsDeleted`);"; - cmd.ExecuteNonQuery(); - } - - if (!MySqlIndexExists(conn, "JobApplications", "IX_JobApplications_OwnerUserId_FollowUpAt")) - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "CREATE INDEX `IX_JobApplications_OwnerUserId_FollowUpAt` ON `JobApplications` (`OwnerUserId`, `FollowUpAt`);"; - cmd.ExecuteNonQuery(); - } - - if (!MySqlIndexExists(conn, "CvUploadArtifacts", "IX_CvUploadArtifacts_OwnerUserId_UploadedAtUtc")) - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "CREATE INDEX `IX_CvUploadArtifacts_OwnerUserId_UploadedAtUtc` ON `CvUploadArtifacts` (`OwnerUserId`, `UploadedAtUtc`);"; - cmd.ExecuteNonQuery(); - } - - if (!MySqlIndexExists(conn, "CvExtractionRuns", "IX_CvExtractionRuns_OwnerUserId_StartedAtUtc")) - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "CREATE INDEX `IX_CvExtractionRuns_OwnerUserId_StartedAtUtc` ON `CvExtractionRuns` (`OwnerUserId`, `StartedAtUtc`);"; - cmd.ExecuteNonQuery(); - } - - if (!MySqlIndexExists(conn, "CvExtractionRuns", "IX_CvExtractionRuns_ArtifactId")) - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "CREATE INDEX `IX_CvExtractionRuns_ArtifactId` ON `CvExtractionRuns` (`ArtifactId`);"; - cmd.ExecuteNonQuery(); - } - - if (!MySqlIndexExists(conn, "GmailConnections", "IX_GmailConnections_OwnerUserId")) - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "CREATE INDEX `IX_GmailConnections_OwnerUserId` ON `GmailConnections` (`OwnerUserId`);"; - cmd.ExecuteNonQuery(); - } - - if (!MySqlIndexExists(conn, "GmailConnections", "IX_GmailConnections_OwnerUserId_GmailAddress")) - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "CREATE UNIQUE INDEX `IX_GmailConnections_OwnerUserId_GmailAddress` ON `GmailConnections` (`OwnerUserId`, `GmailAddress`);"; - cmd.ExecuteNonQuery(); - } - - if (!MySqlIndexExists(conn, "MicrosoftGraphConnections", "IX_MicrosoftGraphConnections_OwnerUserId")) - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "CREATE INDEX `IX_MicrosoftGraphConnections_OwnerUserId` ON `MicrosoftGraphConnections` (`OwnerUserId`);"; - cmd.ExecuteNonQuery(); - } - - if (!MySqlIndexExists(conn, "MicrosoftGraphConnections", "IX_MicrosoftGraphConnections_OwnerUserId_MailAddress")) - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "CREATE UNIQUE INDEX `IX_MicrosoftGraphConnections_OwnerUserId_MailAddress` ON `MicrosoftGraphConnections` (`OwnerUserId`, `MailAddress`);"; - cmd.ExecuteNonQuery(); - } - - if (!MySqlIndexExists(conn, "ImapConnections", "IX_ImapConnections_OwnerUserId")) - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "CREATE UNIQUE INDEX `IX_ImapConnections_OwnerUserId` ON `ImapConnections` (`OwnerUserId`);"; - cmd.ExecuteNonQuery(); - } - - if (!MySqlIndexExists(conn, "TailoredCvDrafts", "IX_TailoredCvDrafts_OwnerUserId_JobApplicationId")) - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "CREATE UNIQUE INDEX `IX_TailoredCvDrafts_OwnerUserId_JobApplicationId` ON `TailoredCvDrafts` (`OwnerUserId`, `JobApplicationId`);"; - cmd.ExecuteNonQuery(); - } - - if (!MySqlIndexExists(conn, "TailoredCvDrafts", "IX_TailoredCvDrafts_JobApplicationId")) - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "CREATE INDEX `IX_TailoredCvDrafts_JobApplicationId` ON `TailoredCvDrafts` (`JobApplicationId`);"; - cmd.ExecuteNonQuery(); - } + TryCreateIndex("JobApplications", "IX_JobApplications_OwnerUserId_IsDeleted", "`OwnerUserId`(191), `IsDeleted`"); + TryCreateIndex("JobApplications", "IX_JobApplications_OwnerUserId_FollowUpAt", "`OwnerUserId`(191), `FollowUpAt`"); + TryCreateIndex("CvUploadArtifacts", "IX_CvUploadArtifacts_OwnerUserId_UploadedAtUtc", "`OwnerUserId`(191), `UploadedAtUtc`"); + TryCreateIndex("CvExtractionRuns", "IX_CvExtractionRuns_OwnerUserId_StartedAtUtc", "`OwnerUserId`(191), `StartedAtUtc`"); + TryCreateIndex("CvExtractionRuns", "IX_CvExtractionRuns_ArtifactId", "`ArtifactId`"); + TryCreateIndex("GmailConnections", "IX_GmailConnections_OwnerUserId", "`OwnerUserId`(191)"); + TryCreateUniqueIndex("GmailConnections", "IX_GmailConnections_OwnerUserId_GmailAddress", "`OwnerUserId`(191), `GmailAddress`(191)"); + TryCreateIndex("MicrosoftGraphConnections", "IX_MicrosoftGraphConnections_OwnerUserId", "`OwnerUserId`(191)"); + TryCreateUniqueIndex("MicrosoftGraphConnections", "IX_MicrosoftGraphConnections_OwnerUserId_MailAddress", "`OwnerUserId`(191), `MailAddress`(191)"); + TryCreateUniqueIndex("ImapConnections", "IX_ImapConnections_OwnerUserId", "`OwnerUserId`(191)"); + TryCreateUniqueIndex("TailoredCvDrafts", "IX_TailoredCvDrafts_OwnerUserId_JobApplicationId", "`OwnerUserId`(191), `JobApplicationId`"); + TryCreateIndex("TailoredCvDrafts", "IX_TailoredCvDrafts_JobApplicationId", "`JobApplicationId`"); } }