From 717d1b9963cfb91c95e25db880788b0ff2d72c18 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sun, 12 Jul 2026 20:22:23 +0200 Subject: [PATCH] perf(db): add remaining hot-path indexes (status filter, correspondence/event FKs) Co-Authored-By: Claude Sonnet 5 --- Data/JobTrackerContext.cs | 11 +++++++++++ .../Services/StartupInitializationExtensions.cs | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/Data/JobTrackerContext.cs b/Data/JobTrackerContext.cs index 9f699e4..de1cc1f 100644 --- a/Data/JobTrackerContext.cs +++ b/Data/JobTrackerContext.cs @@ -71,6 +71,11 @@ namespace JobTrackerApi.Data modelBuilder.Entity() .HasIndex(j => new { j.OwnerUserId, j.FollowUpAt }); + // Board/list endpoints that filter by both IsDeleted and Status. Same MySQL + // longtext-prefix caveat as above; the reconciler applies `Status(50)` there. + modelBuilder.Entity() + .HasIndex(j => new { j.OwnerUserId, j.IsDeleted, j.Status }); + modelBuilder.Entity() .HasIndex(c => c.OwnerUserId); @@ -81,6 +86,9 @@ namespace JobTrackerApi.Data .HasForeignKey(c => c.JobApplicationId) .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .HasIndex(c => c.JobApplicationId); + modelBuilder.Entity() .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId); @@ -111,6 +119,9 @@ namespace JobTrackerApi.Data .HasForeignKey(e => e.JobApplicationId) .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .HasIndex(e => e.JobApplicationId); + modelBuilder.Entity() .HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId); diff --git a/JobTrackerApi/Services/StartupInitializationExtensions.cs b/JobTrackerApi/Services/StartupInitializationExtensions.cs index 3665ba8..20ca4be 100644 --- a/JobTrackerApi/Services/StartupInitializationExtensions.cs +++ b/JobTrackerApi/Services/StartupInitializationExtensions.cs @@ -688,6 +688,17 @@ public static class StartupInitializationExtensions { Exec(conn, """CREATE INDEX IF NOT EXISTS "IX_JobApplications_OwnerUserId_IsDeleted" ON "JobApplications" ("OwnerUserId", "IsDeleted");"""); Exec(conn, """CREATE INDEX IF NOT EXISTS "IX_JobApplications_OwnerUserId_FollowUpAt" ON "JobApplications" ("OwnerUserId", "FollowUpAt");"""); + Exec(conn, """CREATE INDEX IF NOT EXISTS "IX_JobApplications_OwnerUserId_IsDeleted_Status" ON "JobApplications" ("OwnerUserId", "IsDeleted", "Status");"""); + } + + if (HasTable(conn, "Correspondences")) + { + Exec(conn, """CREATE INDEX IF NOT EXISTS "IX_Correspondences_JobApplicationId" ON "Correspondences" ("JobApplicationId");"""); + } + + if (HasTable(conn, "JobEvents")) + { + Exec(conn, """CREATE INDEX IF NOT EXISTS "IX_JobEvents_JobApplicationId" ON "JobEvents" ("JobApplicationId");"""); } // Ensure data folder exists before creating/opening SQLite files. @@ -1022,6 +1033,11 @@ public static class StartupInitializationExtensions // (OwnerUserId + IsDeleted) and reminders (OwnerUserId + FollowUpAt). TryCreateIndex("JobApplications", "IX_JobApplications_OwnerUserId_IsDeleted", "`OwnerUserId`(191), `IsDeleted`"); TryCreateIndex("JobApplications", "IX_JobApplications_OwnerUserId_FollowUpAt", "`OwnerUserId`(191), `FollowUpAt`"); + // Status is longtext in MySQL (see JobTrackerContext.OnModelCreating), so it + // needs an explicit prefix length to be indexable under MariaDB's key-length rules. + TryCreateIndex("JobApplications", "IX_JobApplications_OwnerUserId_IsDeleted_Status", "`OwnerUserId`(191), `IsDeleted`, `Status`(50)"); + TryCreateIndex("Correspondences", "IX_Correspondences_JobApplicationId", "`JobApplicationId`"); + TryCreateIndex("JobEvents", "IX_JobEvents_JobApplicationId", "`JobApplicationId`"); TryCreateIndex("CvUploadArtifacts", "IX_CvUploadArtifacts_OwnerUserId_UploadedAtUtc", "`OwnerUserId`(191), `UploadedAtUtc`"); TryCreateIndex("CvExtractionRuns", "IX_CvExtractionRuns_OwnerUserId_StartedAtUtc", "`OwnerUserId`(191), `StartedAtUtc`"); TryCreateIndex("CvExtractionRuns", "IX_CvExtractionRuns_ArtifactId", "`ArtifactId`");