Adds a covering index for the unified-inbox summary scan and the Room migration + schema export it requires. Pure additive index — no column/table changes, no data transformation.
Index:index_messages_folder_inInbox_timestampMillis on messages (folder, inInbox, timestampMillis).
The paged "All inboxes" query — MessageDao.pagingUnifiedFolderSummaries — filters WHERE folder = ? AND inInbox = 1 ORDER BY timestampMillis DESC. No index led with folder, so the planner walked the whole table via index_messages_timestampMillis and filtered folder/inInbox per row. The new index makes the two equality predicates an index seek and supplies the timestampMillis ordering, so the whole-table SCAN becomes a bounded SEARCH with no temp B-tree sort. The column order (folder, inInbox, timestampMillis) is a perfect prefix match: two equalities then the sort column.
EXPLAIN QUERY PLAN (before / after)
Captured with sqlite3 against the exact v19 messages DDL + indices from the exported schema, for the production query:
BEFORE (v19 indices only):
SCAN messages USING INDEX index_messages_timestampMillis
AFTER (+ index_messages_folder_inInbox_timestampMillis):
SEARCH messages USING INDEX index_messages_folder_inInbox_timestampMillis (folder=? AND inInbox=?)
The "before" matches the plan already documented in docs/perf/issue-124-unified-inbox-paging.md. The instrumented MigrationTest re-asserts the "after" plan on the real Android SQLite (SEARCH on the new index, no USE TEMP B-TREE), so the index is verified genuinely covering the filter+order, not merely present.
Migration + schema
MIGRATION_19_20 = CREATE INDEX IF NOT EXISTS ... (folder, inInbox, timestampMillis), registered in DatabaseModule.
DB version bumped 19 → 20; matching @Index added to MessageEntity.
New schema exported and committed: app/schemas/.../20.json (its only diff from 19.json is the added index).
Tests
New MigrationTest.migrate19To20_addsUnifiedInboxCoveringIndexUsedByTheSummaryScan: runs the migration, asserts the index exists over exactly (folder, inInbox, timestampMillis), cached rows survive, and the summary query now plans as a SEARCH on the index with no temp B-tree.
The existing auto-discovering chain tests (migrationsFormOneUnbrokenChainUpToTheLatestExportedSchema, migratingFromV7ReplaysEveryMigrationAndPreservesData) pick up 19→20 automatically and validate the migrated schema against 20.json.
Ran locally on the API 36 emulator (connectedDebugAndroidTest, MigrationTest) plus the compile/unit/static gate (testDebugUnitTest, compileDebugAndroidTestKotlin, ktlintCheck, detekt).
Closes #187.
## What
Adds a covering index for the unified-inbox summary scan and the Room migration + schema export it requires. Pure additive index — no column/table changes, no data transformation.
**Index:** `index_messages_folder_inInbox_timestampMillis` on `messages (folder, inInbox, timestampMillis)`.
The paged "All inboxes" query — `MessageDao.pagingUnifiedFolderSummaries` — filters `WHERE folder = ? AND inInbox = 1 ORDER BY timestampMillis DESC`. No index led with `folder`, so the planner walked the whole table via `index_messages_timestampMillis` and filtered `folder`/`inInbox` per row. The new index makes the two equality predicates an index seek and supplies the `timestampMillis` ordering, so the whole-table `SCAN` becomes a bounded `SEARCH` with no temp B-tree sort. The column order `(folder, inInbox, timestampMillis)` is a perfect prefix match: two equalities then the sort column.
## EXPLAIN QUERY PLAN (before / after)
Captured with `sqlite3` against the exact v19 `messages` DDL + indices from the exported schema, for the production query:
```
BEFORE (v19 indices only):
SCAN messages USING INDEX index_messages_timestampMillis
AFTER (+ index_messages_folder_inInbox_timestampMillis):
SEARCH messages USING INDEX index_messages_folder_inInbox_timestampMillis (folder=? AND inInbox=?)
```
The "before" matches the plan already documented in `docs/perf/issue-124-unified-inbox-paging.md`. The instrumented `MigrationTest` re-asserts the "after" plan on the real Android SQLite (SEARCH on the new index, no `USE TEMP B-TREE`), so the index is verified genuinely covering the filter+order, not merely present.
## Migration + schema
- `MIGRATION_19_20` = `CREATE INDEX IF NOT EXISTS ... (folder, inInbox, timestampMillis)`, registered in `DatabaseModule`.
- DB version bumped 19 → 20; matching `@Index` added to `MessageEntity`.
- New schema exported and committed: `app/schemas/.../20.json` (its only diff from `19.json` is the added index).
## Tests
- New `MigrationTest.migrate19To20_addsUnifiedInboxCoveringIndexUsedByTheSummaryScan`: runs the migration, asserts the index exists over exactly `(folder, inInbox, timestampMillis)`, cached rows survive, and the summary query now plans as a `SEARCH` on the index with no temp B-tree.
- The existing auto-discovering chain tests (`migrationsFormOneUnbrokenChainUpToTheLatestExportedSchema`, `migratingFromV7ReplaysEveryMigrationAndPreservesData`) pick up 19→20 automatically and validate the migrated schema against `20.json`.
- Ran locally on the API 36 emulator (`connectedDebugAndroidTest`, `MigrationTest`) plus the compile/unit/static gate (`testDebugUnitTest`, `compileDebugAndroidTestKotlin`, `ktlintCheck`, `detekt`).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Coordinator handoff (deferred to maintainer): the schema-version test fix for the v20 bump is done and pushed (DatabaseEncryptionTest now asserts 20). Remaining: this PR conflicts with main in DatabaseModule.kt — #320 (merged) refactored migration registration to .addMigrations(*ALL_MIGRATIONS) with an introspectable ALL_MIGRATIONS list + a test asserting registered==declared. To land: git merge origin/main, then add MIGRATION_19_20 to the ALL_MIGRATIONS list (so #320's databaseModuleRegistersEveryDeclaredMigration test stays green), and re-arm. Auto-merge disabled so it doesn't land half-resolved. It's a perf optimization (covering index, #187), not a review finding.
Coordinator handoff (deferred to maintainer): the schema-version test fix for the v20 bump is done and pushed (`DatabaseEncryptionTest` now asserts 20). Remaining: this PR conflicts with `main` in **DatabaseModule.kt** — #320 (merged) refactored migration registration to `.addMigrations(*ALL_MIGRATIONS)` with an introspectable `ALL_MIGRATIONS` list + a test asserting registered==declared. To land: `git merge origin/main`, then add `MIGRATION_19_20` to the `ALL_MIGRATIONS` list (so #320's `databaseModuleRegistersEveryDeclaredMigration` test stays green), and re-arm. Auto-merge disabled so it doesn't land half-resolved. It's a perf optimization (covering index, #187), not a review finding.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #187.
What
Adds a covering index for the unified-inbox summary scan and the Room migration + schema export it requires. Pure additive index — no column/table changes, no data transformation.
Index:
index_messages_folder_inInbox_timestampMillisonmessages (folder, inInbox, timestampMillis).The paged "All inboxes" query —
MessageDao.pagingUnifiedFolderSummaries— filtersWHERE folder = ? AND inInbox = 1 ORDER BY timestampMillis DESC. No index led withfolder, so the planner walked the whole table viaindex_messages_timestampMillisand filteredfolder/inInboxper row. The new index makes the two equality predicates an index seek and supplies thetimestampMillisordering, so the whole-tableSCANbecomes a boundedSEARCHwith no temp B-tree sort. The column order(folder, inInbox, timestampMillis)is a perfect prefix match: two equalities then the sort column.EXPLAIN QUERY PLAN (before / after)
Captured with
sqlite3against the exact v19messagesDDL + indices from the exported schema, for the production query:The "before" matches the plan already documented in
docs/perf/issue-124-unified-inbox-paging.md. The instrumentedMigrationTestre-asserts the "after" plan on the real Android SQLite (SEARCH on the new index, noUSE TEMP B-TREE), so the index is verified genuinely covering the filter+order, not merely present.Migration + schema
MIGRATION_19_20=CREATE INDEX IF NOT EXISTS ... (folder, inInbox, timestampMillis), registered inDatabaseModule.@Indexadded toMessageEntity.app/schemas/.../20.json(its only diff from19.jsonis the added index).Tests
MigrationTest.migrate19To20_addsUnifiedInboxCoveringIndexUsedByTheSummaryScan: runs the migration, asserts the index exists over exactly(folder, inInbox, timestampMillis), cached rows survive, and the summary query now plans as aSEARCHon the index with no temp B-tree.migrationsFormOneUnbrokenChainUpToTheLatestExportedSchema,migratingFromV7ReplaysEveryMigrationAndPreservesData) pick up 19→20 automatically and validate the migrated schema against20.json.connectedDebugAndroidTest,MigrationTest) plus the compile/unit/static gate (testDebugUnitTest,compileDebugAndroidTestKotlin,ktlintCheck,detekt).🤖 Generated with Claude Code
Coordinator handoff (deferred to maintainer): the schema-version test fix for the v20 bump is done and pushed (
DatabaseEncryptionTestnow asserts 20). Remaining: this PR conflicts withmainin DatabaseModule.kt — #320 (merged) refactored migration registration to.addMigrations(*ALL_MIGRATIONS)with an introspectableALL_MIGRATIONSlist + a test asserting registered==declared. To land:git merge origin/main, then addMIGRATION_19_20to theALL_MIGRATIONSlist (so #320'sdatabaseModuleRegistersEveryDeclaredMigrationtest stays green), and re-arm. Auto-merge disabled so it doesn't land half-resolved. It's a perf optimization (covering index, #187), not a review finding.