[
  {
    "type": "add_index",
    "description": "Spec 051 T8.3 (NFR-1 index decision): add (userId, clientId, lastUsedAt) composite index to kiosk_users.oauthRefreshTokens for the AppAdoptionDetector's per-app refresh-token freshness aggregate (WHERE userId IN (...) AND clientId IN ('team','live') GROUP BY userId, clientId). EXPLAIN on the T8.3 95th-percentile seeded profile (ob03, 50 staff, 30 adopted users, 113 total oauthRefreshTokens rows in dev) showed a full table scan (type=ALL, key=NULL) PLUS 'Using temporary; Using filesort' for the GROUP BY - the existing idx_userId/idx_userExpires indexes do not cover clientId or lastUsedAt, so MariaDB materializes a temp table to satisfy the GROUP BY. With this index present, the same query plan switches to type=range, key=idx_userId_clientId_lastUsedAt, Extra='Using index condition' - both the temp table AND the filesort are eliminated. Deliberately NOT bundling a userStoreAssignments(typeNum,isActive) composite index in this same migration - see 20260722_051_007 companion decision note in docs/runbooks/051-migration-rehearsal.md: EXPLAIN at the seeded scale (181 total userStoreAssignments rows) showed MariaDB choosing a full table scan for the Roster/Invites usa join EVEN AFTER adding that composite index experimentally to a throwaway clone - the optimizer's cost model prefers a full scan of this small a table regardless of available indexes, so shipping that index today would add write overhead with zero read-side benefit. Purely additive (CREATE INDEX-equivalent ALTER, no data movement) - safe, low-risk, no lock-risk-op observation needed (T8.2's rehearsal already established add_index on this exact table completes in ~100-200ms with zero blocking at 50k rows, an order of magnitude more rows than oauthRefreshTokens will realistically reach for a long time). KNOWN LIMITATION (T8 review): this op's check_query only confirms an index NAMED idx_userId_clientId_lastUsedAt EXISTS on oauthRefreshTokens - it does NOT verify that index actually covers (userId, clientId, lastUsedAt) in that column order. A same-named index with different columns (e.g. hand-created during an incident, or a future migration that drops/recreates it differently) would satisfy this check_query and cause the op to report 'already applied' without the query plan this migration exists to guarantee. If the EXPLAIN plan in AppAdoptionQueryExplainTest ever regresses to a full scan/temp-table despite this migration showing applied, verify via SHOW INDEX FROM oauthRefreshTokens that the column order truly is (userId, clientId, lastUsedAt) before assuming this op is a no-op.",
    "database": "kiosk_users",
    "check_query": "SELECT 1 AS exact_match FROM (SELECT NON_UNIQUE, INDEX_TYPE, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) columns_ FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA='kiosk_users' AND TABLE_NAME='oauthRefreshTokens' AND INDEX_NAME='idx_userId_clientId_lastUsedAt' GROUP BY NON_UNIQUE, INDEX_TYPE) i WHERE NON_UNIQUE=1 AND INDEX_TYPE='BTREE' AND columns_='userId,clientId,lastUsedAt'",
    "sql": "ALTER TABLE `oauthRefreshTokens` ADD INDEX `idx_userId_clientId_lastUsedAt` (`userId`, `clientId`, `lastUsedAt`)"
  }
]
