perf/fix(data): batch sync updates, paging tiebreaker, migration-registration test

#310: add a single-transaction MessageDao.updateHeaderContents(List<MessageEntity>)
and route MailSyncer's per-message updateHeaderContent loop through it, so a folder's
recent-window refresh commits once instead of once per message (fsync/journal write
per message, amplified on the encrypted cache).

#311: append the `id` primary key as a tiebreaker to the four paged MessageDao
`ORDER BY timestampMillis DESC` queries for a total order, so rows sharing a second
(bulk mail) can't duplicate or skip across a LIMIT/OFFSET page boundary. Pure query-text
change: the exported Room schema (identityHash) is derived from table/index structure,
not @Query SQL, so no schema re-export or version bump; id is already in the projection,
so no new index.

#312: expose the registered migration list as DatabaseModule.ALL_MIGRATIONS (spread into
addMigrations) and assert in MigrationTest that it equals the reflectively-discovered set
of every Migration val, so a migration forgotten in addMigrations fails a test instead of
crash-looping all upgrading users at DB open (there is deliberately no destructive fallback).

Tests: new MessageDaoTest cases for the batch update and the id tiebreaker (all four
pagers), and the MigrationTest registration assertion; wired updateHeaderContents into
MailSyncConcurrencyTest's fake DAO. Instrumented MessageDaoTest + MigrationTest (31 tests)
green on a local emulator; unit tests + androidTest compile + ktlint + detekt green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 02:30:50 -05:00
co-authored by Claude Opus 4.8
parent b64c7f8dda
commit edbb1eb839
6 changed files with 176 additions and 35 deletions
@@ -410,6 +410,11 @@ class MailSyncConcurrencyTest {
coEvery { dao.updateHeaderContent(any(), any(), any(), any(), any(), any()) } answers {
store.updateHeaderContent(firstArg(), secondArg(), thirdArg(), arg(3), arg(4), arg(5))
}
coEvery { dao.updateHeaderContents(any()) } answers {
firstArg<List<MessageEntity>>().forEach {
store.updateHeaderContent(it.id, it.sender, it.senderEmail, it.subject, it.timestampMillis, it.uid)
}
}
coEvery { dao.deleteByIds(any()) } answers { store.deleteByIds(firstArg()) }
coEvery { dao.deleteSyncedByAccountFolder(any(), any()) } answers {
store.deleteSyncedByAccountFolder(firstArg(), secondArg())