perf: index chat messages by their room

The query in the previous commit reads the newest line of every room the user is
in, once per room, and Room re-runs the whole thing every time a message lands
anywhere. Without an index on `ChatMessage.chatRoomId` each of those lookups is a
scan of every message on the device: work that grows with the entire history
rather than with the room, on the hot path of every arriving message. A device
with ten rooms and a few thousand messages does tens of thousands of row reads to
redraw a list whose visible change is one line of text.

Room has wanted this index since the foreign key was declared and has said so on
every build -- `chatRoomId column references a foreign key but it is not part of
an index. This may trigger full table scans whenever parent table is modified` --
which is the same warning it still emits for a dozen other `chatRoomId` columns.
Those stay as they are; this one now has a reader that makes it matter.

**Schema v15, and Room writes the migration itself.** Adding an index changes no
columns and moves no rows, which is one of the shapes `AutoMigration` handles
without a spec, so this is an entry in the list rather than another manual
migration alongside MIGRATION_13_14.

The generated 15.json differs from 14.json in exactly one place, checked rather
than assumed: `index_ChatMessage_chatRoomId` appears on ChatMessage, and no
table's fields, createSql or other indices move at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso Ngako
2026-09-06 18:21:31 +02:00
parent 50feb6fa0c
commit 81965ba2d3
3 changed files with 5661 additions and 1 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -177,7 +177,7 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
UnsignedNostrEvent::class,
Zap::class
],
version = 14,
version = 15,
autoMigrations = [
// v2 only adds the DkgSession/DkgParticipantMessage tables, so Room can
// generate the migration itself — nothing existing changes shape.
@@ -255,6 +255,12 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
// meanings would have met. Room can rename a column and cannot rewrite the
// rows in the same breath, so this is a manual migration passed to the
// builder rather than an entry here. See MIGRATION_13_14.
//
// v15 adds an index on ChatMessage.chatRoomId. No column changes and no
// rows move -- the chat list now looks up each room's newest line, and
// without the index that lookup reads every message on the device. Room
// creates an index on its own.
AutoMigration(from = 14, to = 15),
]
)
@ColumnTypeConverters(MantraConverters::class)

View File

@@ -2,6 +2,7 @@ package press.mantra.compose.database.model
import androidx.room3.Entity
import androidx.room3.ForeignKey
import androidx.room3.Index
import androidx.room3.PrimaryKey
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.name
import press.mantra.compose.database.MantraDatabase
@@ -69,6 +70,11 @@ import kotlin.time.Instant
onDelete = ForeignKey.CASCADE,
),
],
// The chat list reads the newest line of every room the user is in, once per
// room, and re-reads all of them each time a message lands anywhere. Without
// this that is a scan of every message this device holds, per room, per
// arrival -- work that grows with the whole history rather than with the room.
indices = [Index("chatRoomId")],
)
data class ChatMessage(
@PrimaryKey(autoGenerate = true)