library: keep artist art maintenance self-healing and cheap

Move change detection into a desired CTE with UPDATE ... FROM so mass updates do not turn the trailing WHERE into a quadratic correlated scan, and so no-op passes write zero rows. Fold the trigger optimization into the 35->36 migration and re-check/rewrite it from onOpen, so a future migration cannot silently restore the full UPDATE triggers. Re-resolve artist detail art when the active source becomes ready, and cover artist search renames in a Room-level test.
This commit is contained in:
austinried
2026-10-20 11:23:02 +00:00
parent 0a5b3512da
commit 77e3fa87f1
11 changed files with 85 additions and 1996 deletions
@@ -682,6 +682,24 @@ class LibraryDaoTest {
assertEquals(listOf("art-2", "art-1"), dao.artistOnce(sourceId, "ar-1")!!.downloadedCoverArts)
}
@Test
fun artistNameUpdatesKeepTheSearchIndexValid() =
runTest {
val sourceId = source()
val dao = db.libraryDao()
dao.upsertArtists(listOf(artist(sourceId, "ar-1", "Artist", albumCount = 0)))
dao.upsertArtists(listOf(artist(sourceId, "ar-1", "Renamed", albumCount = 0)))
val hits =
db.useReaderConnection { connection ->
connection.usePrepared("SELECT COUNT(*) FROM artist_search WHERE artist_search MATCH 'Renamed'") {
it.step()
it.getLong(0)
}
}
assertEquals(1L, hits)
}
@Test
fun artistArtDeduplicatesCoversBeforeApplyingTheFourCoverBudget() =
runTest {
@@ -222,9 +222,10 @@ class MigrationsTest {
fun theArtistSearchTriggersIgnoreArtOnlyUpdates() =
runTest {
driver.open(":memory:").use { migrated ->
applySchema(migrated, 36)
MIGRATION_36_37.migrate(migrated)
applySchema(migrated, 35)
MIGRATION_35_36.migrate(migrated)
assertTrue(artistSearchTriggersOptimized(migrated))
assertTrue(
triggerSql(migrated, "room_fts_content_sync_artist_search_BEFORE_UPDATE")
.contains("UPDATE OF `name`, `sortName`"),
@@ -240,7 +241,7 @@ class MigrationsTest {
fun theFreshDatabaseCallbackOptimizesTheArtistSearchTriggers() =
runTest {
driver.open(":memory:").use { connection ->
applySchema(connection, 37)
applySchema(connection, 36)
connection.execSQL(
"INSERT INTO sources (id, name, address, isActive, createdAt) VALUES (1, 's', 'http://x', 1, 0)",
)
@@ -249,15 +250,12 @@ class MigrationsTest {
"VALUES (1, 'ar1', 'Artist', 0, NULL)",
)
ArtistSearchTriggerOptimization.onCreate(connection)
ArtistSearchTriggerOptimization.onOpen(connection)
assertTrue(artistSearchTriggersOptimized(connection))
val before = long(connection, "SELECT total_changes()")
connection.execSQL("UPDATE artists SET coverArts = 'a' WHERE id = 'ar1'")
assertEquals(before + 1, long(connection, "SELECT total_changes()"))
assertTrue(
triggerSql(connection, "room_fts_content_sync_artist_search_AFTER_UPDATE")
.contains("UPDATE OF `name`, `sortName`"),
)
}
}
@@ -546,6 +544,10 @@ class MigrationsTest {
for (i in 0 until indices.length()) {
connection.execSQL(indices.getJSONObject(i).getString("createSql").replace("\${TABLE_NAME}", name))
}
val triggers = entity.optJSONArray("contentSyncTriggers") ?: continue
for (i in 0 until triggers.length()) {
connection.execSQL(triggers.getString(i))
}
}
}
@@ -39,7 +39,12 @@ class QueryTimingMeasurementTest {
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
val driver = LoggingSQLiteDriver(BundledSQLiteDriver()) { executions += it }
db = Room.inMemoryDatabaseBuilder(context, SubtracksDatabase::class.java).setDriver(driver).build()
db =
Room
.inMemoryDatabaseBuilder(context, SubtracksDatabase::class.java)
.setDriver(driver)
.addCallback(ArtistSearchTriggerOptimization)
.build()
}
@After
@@ -285,6 +285,15 @@ class SourceRepositoryTest {
assertTrue("expected the stored file, was ${ref.url}", ref.url.startsWith("file:"))
}
@Test
fun activeSourceBecomesReadyAfterThePreferencesLoad() =
runBlocking {
repository.addSource("nav", "http://a.example/", "u", "p", false)
val active = withTimeout(TEST_TIMEOUT_MS) { repository.activeSource.first { it != null } }
assertEquals(repository.activeSourceId().first(), active)
}
@Test
fun artistImageUsesTheNetworkOnlineAndOnlyCachedFilesOffline() =
runBlocking {