From 95646e1d539df024a6220c9cb3303447fc299aec Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sun, 19 Jul 2026 22:52:39 +0200 Subject: [PATCH] fix(db): create follow-up reminder index on MariaDB The reconciler's IX_JobApplications_OwnerUserId_FollowUpAt was declared as (OwnerUserId(191), FollowUpAt) with no prefix length on FollowUpAt. But FollowUpAt is `text` on MariaDB -- JobApplications is migration-owned and the migration was scaffolded against SQLite, which stores DateTimeOffset as TEXT. A text column cannot be indexed without a prefix length, so this index failed the 3072-byte key check on EVERY MariaDB boot, was caught by TryCreateIndex, and was silently skipped -- leaving the follow-up reminder query (OwnerUserId + FollowUpAt) unindexed. Two consequences, both real: - the index the code intends to create never existed on MariaDB - every healthy boot logged "Specified key was too long", which deploy/first-production-deployment.md lists as a STOP-AND-ROLL-BACK signal -- so an operator following the runbook could abort a good deploy The author already handled the identical problem for the longtext Status column one line below with Status(50). Apply the same fix: FollowUpAt(20). ISO-8601 date strings sort lexicographically, so a 20-char prefix ("YYYY-MM-DD HH:MM:SS") keeps the index useful for the reminder scan. Verified on a fresh empty MariaDB 11 container: the index is now created (both key parts present), zero "Specified key was too long" lines, zero skipped indexes, zero unhandled exceptions, 42 tables, app healthy. This was the only unprefixed text column in any reconciler composite index -- the datetime columns on reconciler-owned tables are datetime(6). SQLite is unaffected (its CREATE INDEX has no key-length limit). Co-Authored-By: Claude Opus 4.8 --- .../Services/StartupInitializationExtensions.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/JobTrackerApi/Services/StartupInitializationExtensions.cs b/JobTrackerApi/Services/StartupInitializationExtensions.cs index cfde895..1cf1a36 100644 --- a/JobTrackerApi/Services/StartupInitializationExtensions.cs +++ b/JobTrackerApi/Services/StartupInitializationExtensions.cs @@ -1937,7 +1937,14 @@ public static class StartupInitializationExtensions // Hot-path composite indexes for tenant-scoped list/board/stats/analytics // (OwnerUserId + IsDeleted) and reminders (OwnerUserId + FollowUpAt). TryCreateIndex("JobApplications", "IX_JobApplications_OwnerUserId_IsDeleted", "`OwnerUserId`(191), `IsDeleted`"); - TryCreateIndex("JobApplications", "IX_JobApplications_OwnerUserId_FollowUpAt", "`OwnerUserId`(191), `FollowUpAt`"); + // FollowUpAt is `text` on MariaDB (the migration was scaffolded against SQLite, which + // stores DateTimeOffset as TEXT). A text column cannot be indexed without a prefix + // length, so without one this index ALWAYS failed the 3072-byte key check, was caught + // and skipped on every boot, and left the follow-up reminder query unindexed — while + // logging a "Specified key was too long" line the deploy runbook flags as a rollback + // signal. Prefix it like Status(50) below. ISO-8601 date strings sort lexicographically, + // so a 20-char prefix ("YYYY-MM-DD HH:MM:SS") keeps the index useful for the reminder scan. + TryCreateIndex("JobApplications", "IX_JobApplications_OwnerUserId_FollowUpAt", "`OwnerUserId`(191), `FollowUpAt`(20)"); // 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)");