library: stop artist art updates from reindexing artist search

The materialized art update rewrote every artist row on every library-changing sync. That fired the FTS5 content-sync triggers for each artist, which was the real cost: the same SQL is ~10ms with the triggers removed and ~4.7s for 100 changed artists with them in place. Only update rows whose cover lists actually changed, and replace the generated artist search triggers with UPDATE OF name, sortName so art-only writes never touch the FTS index. Existing databases get this through migration 36->37; fresh databases get it from the onCreate callback.
This commit is contained in:
austinried
2026-10-20 10:13:20 +00:00
parent 783d7f2ebc
commit 0a5b3512da
7 changed files with 2055 additions and 3 deletions
@@ -11,6 +11,7 @@ fun createAndroidDatabase(context: Context): SubtracksDatabase {
.databaseBuilder<SubtracksDatabase>(context, "subtracks.db")
.setDriver(driver)
.addMigrations(*MIGRATIONS.map(::LoggedMigration).toTypedArray())
.addCallback(ArtistSearchTriggerOptimization)
.addCallback(SqliteLifecycleLogging)
.build()
}
@@ -114,6 +114,12 @@ class LoggedMigration(
}
}
internal object ArtistSearchTriggerOptimization : RoomDatabase.Callback() {
override suspend fun onCreate(connection: SQLiteConnection) {
optimizeArtistSearchTriggers(connection)
}
}
internal object SqliteLifecycleLogging : RoomDatabase.Callback() {
override suspend fun onCreate(connection: SQLiteConnection) {
Log.i("sql", "created fresh database")
@@ -230,7 +230,10 @@ internal const val ARTIST_ART_UPDATE_SQL =
"coverArts = COALESCE((SELECT coverArts FROM covers WHERE covers.artistId = artists.id), ''), " +
"downloadedCoverArts = COALESCE((SELECT coverArts FROM downloaded_covers " +
"WHERE downloaded_covers.artistId = artists.id), '') " +
"WHERE sourceId = :sourceId"
"WHERE sourceId = :sourceId AND (" +
"coverArts != COALESCE((SELECT coverArts FROM covers WHERE covers.artistId = artists.id), '') " +
"OR downloadedCoverArts != COALESCE((SELECT coverArts FROM downloaded_covers " +
"WHERE downloaded_covers.artistId = artists.id), ''))"
internal const val DOWNLOADED_ARTIST_ART_UPDATE_SQL =
"WITH downloaded_albums AS (SELECT DISTINCT artistId, albumId FROM ($DOWNLOADED_COVER_ALBUM_ROWS) " +
@@ -248,7 +251,9 @@ internal const val DOWNLOADED_ARTIST_ART_UPDATE_SQL =
") GROUP BY artistId) " +
"UPDATE artists SET downloadedCoverArts = COALESCE((SELECT coverArts FROM downloaded_covers " +
"WHERE downloaded_covers.artistId = artists.id), '') " +
"WHERE sourceId = :sourceId AND id IN (:artistIds)"
"WHERE sourceId = :sourceId AND id IN (:artistIds) AND " +
"downloadedCoverArts != COALESCE((SELECT coverArts FROM downloaded_covers " +
"WHERE downloaded_covers.artistId = artists.id), '')"
internal const val ARTISTS_FILTER =
"FROM artists WHERE sourceId = :sourceId " +
@@ -705,6 +705,29 @@ val MIGRATION_35_36 =
}
}
val MIGRATION_36_37 =
object : Migration(36, 37) {
override suspend fun migrate(connection: SQLiteConnection) {
optimizeArtistSearchTriggers(connection)
}
}
internal fun optimizeArtistSearchTriggers(connection: SQLiteConnection) {
connection.execSQL("DROP TRIGGER IF EXISTS room_fts_content_sync_artist_search_BEFORE_UPDATE")
connection.execSQL("DROP TRIGGER IF EXISTS room_fts_content_sync_artist_search_AFTER_UPDATE")
connection.execSQL(
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_artist_search_BEFORE_UPDATE " +
"BEFORE UPDATE OF `name`, `sortName` ON `artists` BEGIN " +
"DELETE FROM `artist_search` WHERE `rowid`=OLD.`rowid`; END",
)
connection.execSQL(
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_artist_search_AFTER_UPDATE " +
"AFTER UPDATE OF `name`, `sortName` ON `artists` BEGIN " +
"INSERT INTO `artist_search`(`rowid`, `name`, `sortName`) " +
"VALUES (NEW.`rowid`, NEW.`name`, NEW.`sortName`); END",
)
}
private suspend fun backfillArtistArt(connection: SQLiteConnection) {
val sourceIds = mutableListOf<Long>()
connection.prepare("SELECT id FROM sources").use { statement ->
@@ -755,4 +778,5 @@ val MIGRATIONS: Array<Migration> =
MIGRATION_33_34,
MIGRATION_34_35,
MIGRATION_35_36,
MIGRATION_36_37,
)
@@ -57,7 +57,7 @@ import com.subtracks.data.model.UpNextEntry
SongDownload::class,
PendingAction::class,
],
version = 36,
version = 37,
exportSchema = true,
)
@ColumnTypeConverters(